view mercurial/dirstate.py @ 1117:30ab5b8ee8ec

fix some rename/copy bugs - delete copy information when we update dirstate hg was keeping the copy state and marking things as copied on multiple commits - files that are renamed should have no parents if you do a rename/copy to an existing file, it should not be marked as descending from its previous revisions. - remove spurious print from filelog.renamed - add some more copy tests
author mpm@selenic.com
date Sat, 27 Aug 2005 22:04:17 -0700
parents 98988cc3723a
children d9e85a75dbda
line wrap: on
line source

"""
dirstate.py - working directory tracking for mercurial

Copyright 2005 Matt Mackall <mpm@selenic.com>

This software may be used and distributed according to the terms
of the GNU General Public License, incorporated herein by reference.
"""

import struct, os
from node import *
from demandload import *
demandload(globals(), "time bisect stat util re")

class dirstate:
    def __init__(self, opener, ui, root):
        self.opener = opener
        self.root = root
        self.dirty = 0
        self.ui = ui
        self.map = None
        self.pl = None
        self.copies = {}
        self.ignorefunc = None

    def wjoin(self, f):
        return os.path.join(self.root, f)

    def getcwd(self):
        cwd = os.getcwd()
        if cwd == self.root: return ''
        return cwd[len(self.root) + 1:]

    def ignore(self, f):
        if not self.ignorefunc:
            bigpat = []
            try:
                l = file(self.wjoin(".hgignore"))
                for pat in l:
                    p = pat.rstrip()
                    if p:
                        try:
                            re.compile(p)
                        except:
                            self.ui.warn("ignoring invalid ignore"
                                         + " regular expression '%s'\n" % p)
                        else:
                            bigpat.append(p)
            except IOError: pass

            if bigpat:
                s = "(?:%s)" % (")|(?:".join(bigpat))
                r = re.compile(s)
                self.ignorefunc = r.search
            else:
                self.ignorefunc = util.never

        return self.ignorefunc(f)

    def __del__(self):
        if self.dirty:
            self.write()

    def __getitem__(self, key):
        try:
            return self.map[key]
        except TypeError:
            self.read()
            return self[key]

    def __contains__(self, key):
        if not self.map: self.read()
        return key in self.map

    def parents(self):
        if not self.pl:
            self.read()
        return self.pl

    def markdirty(self):
        if not self.dirty:
            self.dirty = 1

    def setparents(self, p1, p2=nullid):
        self.markdirty()
        self.pl = p1, p2

    def state(self, key):
        try:
            return self[key][0]
        except KeyError:
            return "?"

    def read(self):
        if self.map is not None: return self.map

        self.map = {}
        self.pl = [nullid, nullid]
        try:
            st = self.opener("dirstate").read()
            if not st: return
        except: return

        self.pl = [st[:20], st[20: 40]]

        pos = 40
        while pos < len(st):
            e = struct.unpack(">cllll", st[pos:pos+17])
            l = e[4]
            pos += 17
            f = st[pos:pos + l]
            if '\0' in f:
                f, c = f.split('\0')
                self.copies[f] = c
            self.map[f] = e[:4]
            pos += l

    def copy(self, source, dest):
        self.read()
        self.markdirty()
        self.copies[dest] = source

    def copied(self, file):
        return self.copies.get(file, None)

    def update(self, files, state, **kw):
        ''' current states:
        n  normal
        m  needs merging
        r  marked for removal
        a  marked for addition'''

        if not files: return
        self.read()
        self.markdirty()
        for f in files:
            if state == "r":
                self.map[f] = ('r', 0, 0, 0)
            else:
                s = os.stat(os.path.join(self.root, f))
                st_size = kw.get('st_size', s.st_size)
                st_mtime = kw.get('st_mtime', s.st_mtime)
                self.map[f] = (state, s.st_mode, st_size, st_mtime)
            if self.copies.has_key(f):
                del self.copies[f]

    def forget(self, files):
        if not files: return
        self.read()
        self.markdirty()
        for f in files:
            try:
                del self.map[f]
            except KeyError:
                self.ui.warn("not in dirstate: %s!\n" % f)
                pass

    def clear(self):
        self.map = {}
        self.markdirty()

    def write(self):
        st = self.opener("dirstate", "w")
        st.write("".join(self.pl))
        for f, e in self.map.items():
            c = self.copied(f)
            if c:
                f = f + "\0" + c
            e = struct.pack(">cllll", e[0], e[1], e[2], e[3], len(f))
            st.write(e + f)
        self.dirty = 0

    def filterfiles(self, files):
        ret = {}
        unknown = []

        for x in files:
            if x is '.':
                return self.map.copy()
            if x not in self.map:
                unknown.append(x)
            else:
                ret[x] = self.map[x]

        if not unknown:
            return ret

        b = self.map.keys()
        b.sort()
        blen = len(b)

        for x in unknown:
            bs = bisect.bisect(b, x)
            if bs != 0 and  b[bs-1] == x:
                ret[x] = self.map[x]
                continue
            while bs < blen:
                s = b[bs]
                if len(s) > len(x) and s.startswith(x) and s[len(x)] == '/':
                    ret[s] = self.map[s]
                else:
                    break
                bs += 1
        return ret

    def walk(self, files=None, match=util.always, dc=None):
        self.read()

        # walk all files by default
        if not files:
            files = [self.root]
            if not dc:
                dc = self.map.copy()
        elif not dc:
            dc = self.filterfiles(files)

        known = {'.hg': 1}
        def seen(fn):
            if fn in known: return True
            known[fn] = 1
        def traverse():
            for ff in util.unique(files):
                f = os.path.join(self.root, ff)
                try:
                    st = os.stat(f)
                except OSError, inst:
                    if ff not in dc: self.ui.warn('%s: %s\n' % (
                        util.pathto(self.getcwd(), ff),
                        inst.strerror))
                    continue
                if stat.S_ISDIR(st.st_mode):
                    for dir, subdirs, fl in os.walk(f):
                        d = dir[len(self.root) + 1:]
                        nd = util.normpath(d)
                        if nd == '.': nd = ''
                        if seen(nd):
                            subdirs[:] = []
                            continue
                        for sd in subdirs:
                            ds = os.path.join(nd, sd +'/')
                            if self.ignore(ds) or not match(ds):
                                subdirs.remove(sd)
                        subdirs.sort()
                        fl.sort()
                        for fn in fl:
                            fn = util.pconvert(os.path.join(d, fn))
                            yield 'f', fn
                elif stat.S_ISREG(st.st_mode):
                    yield 'f', ff
                else:
                    kind = 'unknown'
                    if stat.S_ISCHR(st.st_mode): kind = 'character device'
                    elif stat.S_ISBLK(st.st_mode): kind = 'block device'
                    elif stat.S_ISFIFO(st.st_mode): kind = 'fifo'
                    elif stat.S_ISLNK(st.st_mode): kind = 'symbolic link'
                    elif stat.S_ISSOCK(st.st_mode): kind = 'socket'
                    self.ui.warn('%s: unsupported file type (type is %s)\n' % (
                        util.pathto(self.getcwd(), ff),
                        kind))

            ks = dc.keys()
            ks.sort()
            for k in ks:
                yield 'm', k

        # yield only files that match: all in dirstate, others only if
        # not in .hgignore

        for src, fn in util.unique(traverse()):
            fn = util.normpath(fn)
            if seen(fn): continue
            if fn not in dc and self.ignore(fn):
                continue
            if match(fn):
                yield src, fn

    def changes(self, files=None, match=util.always):
        self.read()
        if not files:
            dc = self.map.copy()
        else:
            dc = self.filterfiles(files)
        lookup, modified, added, unknown = [], [], [], []
        removed, deleted = [], []

        for src, fn in self.walk(files, match, dc=dc):
            try:
                s = os.stat(os.path.join(self.root, fn))
            except OSError:
                continue
            if not stat.S_ISREG(s.st_mode):
                continue
            c = dc.get(fn)
            if c:
                del dc[fn]
                if c[0] == 'm':
                    modified.append(fn)
                elif c[0] == 'a':
                    added.append(fn)
                elif c[0] == 'r':
                    unknown.append(fn)
                elif c[2] != s.st_size or (c[1] ^ s.st_mode) & 0100:
                    modified.append(fn)
                elif c[3] != s.st_mtime:
                    lookup.append(fn)
            else:
                unknown.append(fn)

        for fn, c in [(fn, c) for fn, c in dc.items() if match(fn)]:
            if c[0] == 'r':
                removed.append(fn)
            else:
                deleted.append(fn)
        return (lookup, modified, added, removed + deleted, unknown)