831 lines · 29.4 KB · Python
RawDownload
## Mercurial built-in replacement for cvsps.## Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>## This software may be used and distributed according to the terms of the# GNU General Public License version 2, incorporated herein by reference. import osimport reimport cPickle as picklefrom mercurial import utilfrom mercurial.i18n import _ class logentry(object):    '''Class logentry has the following attributes:        .author    - author name as CVS knows it        .branch    - name of branch this revision is on        .branches  - revision tuple of branches starting at this revision        .comment   - commit message        .date      - the commit date as a (time, tz) tuple        .dead      - true if file revision is dead        .file      - Name of file        .lines     - a tuple (+lines, -lines) or None        .parent    - Previous revision of this entry        .rcs       - name of file as returned from CVS        .revision  - revision number as tuple        .tags      - list of tags on the file        .synthetic - is this a synthetic "file ... added on ..." revision?        .mergepoint- the branch that has been merged from                     (if present in rlog output)        .branchpoints- the branches that start at the current entry    '''    def __init__(self, **entries):        self.__dict__.update(entries)     def __repr__(self):        return "<%s at 0x%x: %s %s>" % (self.__class__.__name__,                                        id(self),                                        self.file,                                        ".".join(map(str, self.revision))) class logerror(Exception):    pass def getrepopath(cvspath):    """Return the repository path from a CVS path.     >>> getrepopath('/foo/bar')    '/foo/bar'    >>> getrepopath('c:/foo/bar')    'c:/foo/bar'    >>> getrepopath(':pserver:10/foo/bar')    '/foo/bar'    >>> getrepopath(':pserver:10c:/foo/bar')    '/foo/bar'    >>> getrepopath(':pserver:/foo/bar')    '/foo/bar'    >>> getrepopath(':pserver:c:/foo/bar')    'c:/foo/bar'    >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')    '/foo/bar'    >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')    'c:/foo/bar'    """    # According to CVS manual, CVS paths are expressed like:    # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository    #    # Unfortunately, Windows absolute paths start with a drive letter    # like 'c:' making it harder to parse. Here we assume that drive    # letters are only one character long and any CVS component before    # the repository path is at least 2 characters long, and use this    # to disambiguate.    parts = cvspath.split(':')    if len(parts) == 1:        return parts[0]    # Here there is an ambiguous case if we have a port number    # immediately followed by a Windows driver letter. We assume this    # never happens and decide it must be CVS path component,    # therefore ignoring it.    if len(parts[-2]) > 1:        return parts[-1].lstrip('0123456789')    return parts[-2] + ':' + parts[-1] def createlog(ui, directory=None, root="", rlog=True, cache=None):    '''Collect the CVS rlog'''     # Because we store many duplicate commit log messages, reusing strings    # saves a lot of memory and pickle storage space.    _scache = {}    def scache(s):        "return a shared version of a string"        return _scache.setdefault(s, s)     ui.status(_('collecting CVS rlog\n'))     log = []      # list of logentry objects containing the CVS state     # patterns to match in CVS (r)log output, by state of use    re_00 = re.compile('RCS file: (.+)$')    re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')    re_02 = re.compile('cvs (r?log|server): (.+)\n$')    re_03 = re.compile("(Cannot access.+CVSROOT)|"                       "(can't create temporary directory.+)$")    re_10 = re.compile('Working file: (.+)$')    re_20 = re.compile('symbolic names:')    re_30 = re.compile('\t(.+): ([\\d.]+)$')    re_31 = re.compile('----------------------------$')    re_32 = re.compile('======================================='                       '======================================$')    re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')    re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'                       r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'                       r'(.*mergepoint:\s+([^;]+);)?')    re_70 = re.compile('branches: (.+);$')     file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')     prefix = ''   # leading path to strip of what we get from CVS     if directory is None:        # Current working directory         # Get the real directory in the repository        try:            prefix = open(os.path.join('CVS','Repository')).read().strip()            if prefix == ".":                prefix = ""            directory = prefix        except IOError:            raise logerror('Not a CVS sandbox')         if prefix and not prefix.endswith(os.sep):            prefix += os.sep         # Use the Root file in the sandbox, if it exists        try:            root = open(os.path.join('CVS','Root')).read().strip()        except IOError:            pass     if not root:        root = os.environ.get('CVSROOT', '')     # read log cache if one exists    oldlog = []    date = None     if cache:        cachedir = os.path.expanduser('~/.hg.cvsps')        if not os.path.exists(cachedir):            os.mkdir(cachedir)         # The cvsps cache pickle needs a uniquified name, based on the        # repository location. The address may have all sort of nasties        # in it, slashes, colons and such. So here we take just the        # alphanumerics, concatenated in a way that does not mix up the        # various components, so that        #    :pserver:user@server:/path        # and        #    /pserver/user/server/path        # are mapped to different cache file names.        cachefile = root.split(":") + [directory, "cache"]        cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]        cachefile = os.path.join(cachedir,                                 '.'.join([s for s in cachefile if s]))     if cache == 'update':        try:            ui.note(_('reading cvs log cache %s\n') % cachefile)            oldlog = pickle.load(open(cachefile))            ui.note(_('cache has %d log entries\n') % len(oldlog))        except Exception, e:            ui.note(_('error reading cache: %r\n') % e)         if oldlog:            date = oldlog[-1].date    # last commit date as a (time,tz) tuple            date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')     # build the CVS commandline    cmd = ['cvs', '-q']    if root:        cmd.append('-d%s' % root)        p = util.normpath(getrepopath(root))        if not p.endswith('/'):            p += '/'        prefix = p + util.normpath(prefix)    cmd.append(['log', 'rlog'][rlog])    if date:        # no space between option and date string        cmd.append('-d>%s' % date)    cmd.append(directory)     # state machine begins here    tags = {}     # dictionary of revisions on current file with their tags    branchmap = {} # mapping between branch names and revision numbers    state = 0    store = False # set when a new record can be appended     cmd = [util.shellquote(arg) for arg in cmd]    ui.note(_("running %s\n") % (' '.join(cmd)))    ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root))     pfp = util.popen(' '.join(cmd))    peek = pfp.readline()    while True:        line = peek        if line == '':            break        peek = pfp.readline()        if line.endswith('\n'):            line = line[:-1]        #ui.debug('state=%d line=%r\n' % (state, line))         if state == 0:            # initial state, consume input until we see 'RCS file'            match = re_00.match(line)            if match:                rcs = match.group(1)                tags = {}                if rlog:                    filename = util.normpath(rcs[:-2])                    if filename.startswith(prefix):                        filename = filename[len(prefix):]                    if filename.startswith('/'):                        filename = filename[1:]                    if filename.startswith('Attic/'):                        filename = filename[6:]                    else:                        filename = filename.replace('/Attic/', '/')                    state = 2                    continue                state = 1                continue            match = re_01.match(line)            if match:                raise Exception(match.group(1))            match = re_02.match(line)            if match:                raise Exception(match.group(2))            if re_03.match(line):                raise Exception(line)         elif state == 1:            # expect 'Working file' (only when using log instead of rlog)            match = re_10.match(line)            assert match, _('RCS file must be followed by working file')            filename = util.normpath(match.group(1))            state = 2         elif state == 2:            # expect 'symbolic names'            if re_20.match(line):                branchmap = {}                state = 3         elif state == 3:            # read the symbolic names and store as tags            match = re_30.match(line)            if match:                rev = [int(x) for x in match.group(2).split('.')]                 # Convert magic branch number to an odd-numbered one                revn = len(rev)                if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:                    rev = rev[:-2] + rev[-1:]                rev = tuple(rev)                 if rev not in tags:                    tags[rev] = []                tags[rev].append(match.group(1))                branchmap[match.group(1)] = match.group(2)             elif re_31.match(line):                state = 5            elif re_32.match(line):                state = 0         elif state == 4:            # expecting '------' separator before first revision            if re_31.match(line):                state = 5            else:                assert not re_32.match(line), _('must have at least '                                                'some revisions')         elif state == 5:            # expecting revision number and possibly (ignored) lock indication            # we create the logentry here from values stored in states 0 to 4,            # as this state is re-entered for subsequent revisions of a file.            match = re_50.match(line)            assert match, _('expected revision number')            e = logentry(rcs=scache(rcs), file=scache(filename),                    revision=tuple([int(x) for x in match.group(1).split('.')]),                    branches=[], parent=None,                    synthetic=False)            state = 6         elif state == 6:            # expecting date, author, state, lines changed            match = re_60.match(line)            assert match, _('revision must be followed by date line')            d = match.group(1)            if d[2] == '/':                # Y2K                d = '19' + d             if len(d.split()) != 3:                # cvs log dates always in GMT                d = d + ' UTC'            e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',                                        '%Y/%m/%d %H:%M:%S',                                        '%Y-%m-%d %H:%M:%S'])            e.author = scache(match.group(2))            e.dead = match.group(3).lower() == 'dead'             if match.group(5):                if match.group(6):                    e.lines = (int(match.group(5)), int(match.group(6)))                else:                    e.lines = (int(match.group(5)), 0)            elif match.group(6):                e.lines = (0, int(match.group(6)))            else:                e.lines = None             if match.group(7): # cvsnt mergepoint                myrev = match.group(8).split('.')                if len(myrev) == 2: # head                    e.mergepoint = 'HEAD'                else:                    myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])                    branches = [b for b in branchmap if branchmap[b] == myrev]                    assert len(branches) == 1, 'unknown branch: %s' % e.mergepoint                    e.mergepoint = branches[0]            else:                e.mergepoint = None            e.comment = []            state = 7         elif state == 7:            # read the revision numbers of branches that start at this revision            # or store the commit log message otherwise            m = re_70.match(line)            if m:                e.branches = [tuple([int(y) for y in x.strip().split('.')])                                for x in m.group(1).split(';')]                state = 8            elif re_31.match(line) and re_50.match(peek):                state = 5                store = True            elif re_32.match(line):                state = 0                store = True            else:                e.comment.append(line)         elif state == 8:            # store commit log message            if re_31.match(line):                state = 5                store = True            elif re_32.match(line):                state = 0                store = True            else:                e.comment.append(line)         # When a file is added on a branch B1, CVS creates a synthetic        # dead trunk revision 1.1 so that the branch has a root.        # Likewise, if you merge such a file to a later branch B2 (one        # that already existed when the file was added on B1), CVS        # creates a synthetic dead revision 1.1.x.1 on B2.  Don't drop        # these revisions now, but mark them synthetic so        # createchangeset() can take care of them.        if (store and              e.dead and              e.revision[-1] == 1 and      # 1.1 or 1.1.x.1              len(e.comment) == 1 and              file_added_re.match(e.comment[0])):            ui.debug(_('found synthetic revision in %s: %r\n')                     % (e.rcs, e.comment[0]))            e.synthetic = True         if store:            # clean up the results and save in the log.            store = False            e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])            e.comment = scache('\n'.join(e.comment))             revn = len(e.revision)            if revn > 3 and (revn % 2) == 0:                e.branch = tags.get(e.revision[:-1], [None])[0]            else:                e.branch = None             # find the branches starting from this revision            branchpoints = set()            for branch, revision in branchmap.iteritems():                revparts = tuple([int(i) for i in revision.split('.')])                if revparts[-2] == 0 and revparts[-1] % 2 == 0:                    # normal branch                    if revparts[:-2] == e.revision:                        branchpoints.add(branch)                elif revparts == (1,1,1): # vendor branch                    if revparts in e.branches:                        branchpoints.add(branch)            e.branchpoints = branchpoints             log.append(e)             if len(log) % 100 == 0:                ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')     log.sort(key=lambda x: (x.rcs, x.revision))     # find parent revisions of individual files    versions = {}    for e in log:        branch = e.revision[:-1]        p = versions.get((e.rcs, branch), None)        if p is None:            p = e.revision[:-2]        e.parent = p        versions[(e.rcs, branch)] = e.revision     # update the log cache    if cache:        if log:            # join up the old and new logs            log.sort(key=lambda x: x.date)             if oldlog and oldlog[-1].date >= log[0].date:                raise logerror('Log cache overlaps with new log entries,'                               ' re-run without cache.')             log = oldlog + log             # write the new cachefile            ui.note(_('writing cvs log cache %s\n') % cachefile)            pickle.dump(log, open(cachefile, 'w'))        else:            log = oldlog     ui.status(_('%d log entries\n') % len(log))     return log  class changeset(object):    '''Class changeset has the following attributes:        .id        - integer identifying this changeset (list index)        .author    - author name as CVS knows it        .branch    - name of branch this changeset is on, or None        .comment   - commit message        .date      - the commit date as a (time,tz) tuple        .entries   - list of logentry objects in this changeset        .parents   - list of one or two parent changesets        .tags      - list of tags on this changeset        .synthetic - from synthetic revision "file ... added on branch ..."        .mergepoint- the branch that has been merged from                     (if present in rlog output)        .branchpoints- the branches that start at the current entry    '''    def __init__(self, **entries):        self.__dict__.update(entries)     def __repr__(self):        return "<%s at 0x%x: %s>" % (self.__class__.__name__,                                     id(self),                                     getattr(self, 'id', "(no id)")) def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):    '''Convert log into changesets.'''     ui.status(_('creating changesets\n'))     # Merge changesets     log.sort(key=lambda x: (x.comment, x.author, x.branch, x.date))     changesets = []    files = set()    c = None    for i, e in enumerate(log):         # Check if log entry belongs to the current changeset or not.         # Since CVS is file centric, two different file revisions with        # different branchpoints should be treated as belonging to two        # different changesets (and the ordering is important and not        # honoured by cvsps at this point).        #        # Consider the following case:        # foo 1.1 branchpoints: [MYBRANCH]        # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]        #        # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a        # later version of foo may be in MYBRANCH2, so foo should be the        # first changeset and bar the next and MYBRANCH and MYBRANCH2

Lines 1–500 of 831.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t