Avoiding conflicts in .hgtags

Greg Ward gerg.ward+hg at gmail.com
Mon Dec 8 12:58:42 CST 2008


On 08 December 2008, I said:
> Script attached.  Feedback welcome.

Oops.  Forgot to attach the attachment.  Sigh.  This time for sure.

       Greg
-------------- next part --------------
#!/usr/bin/python

"""\
Specialized merge tool for handling Mercurial's .hgtags files.  Since
Mercurial lets the content of .hgtags vary across branches, it's
possible to get trivial conflicts that could easily be resolved by a
tool that understands what .hgtags means (e.g. if you add distinct
tags on different branches and then merge those branches).  This is
that tool.
"""

# XXX This is a very simple, naive prototype implementation.
# It sucks everything into memory, assumes the syntax of .hgtags
# is dead simple, has minimal error handling, etc.

import sys, os

class TagList(object):
    def __init__(self):
        self.tags = []                  # list of (name, changeset_id) tuples

    def __iter__(self):
        return iter(self.tags)

    def add(self, name, val):
        if val is not None:
            self.tags.append((name, val))

    def get(self, name):
        for (tagname, val) in self.tags:
            if name == tagname:
                return val
        return None

class MergeEngine(object):
    def __init__(self, myfilename, basefilename, yourfilename, outfile):
        self.myfilename = myfilename
        self.basefilename = basefilename
        self.yourfilename = yourfilename
        self.outfile = outfile

        # "mytags" is the content of .hgtags from the branch that we're
        # merging into, i.e. the first parent.  "yourtags" is from the
        # branch that is being merged from, i.e. the second parent.
        # "basetags" is the common ancestor of those two branches (I
        # think).

        self.mytags = self._readtags(myfilename)
        self.basetags = self._readtags(basefilename)
        self.yourtags = self._readtags(yourfilename)

        # Merge state
        self.merged = TagList()
        self.conflict = {}             # map tag name to (first, second) changeset IDs
        self.seen = set()              # set of tag names already processed
        
    def _readtags(self, filename):
        file = open(filename, "rt")
        tags = TagList()
        for line in file:
            (val, name) = line.strip().split()
            tags.add(name, val)
        file.close()
        return tags

    def merge(self):

        # Iterate over basetags to handle changed/removed tags.
        self._handle_existing_tags()

        # Iterate over mytags and yourtags to detect tags added since basetags.
        self._handle_added_tags()

        self._report()

        return (not self.conflict)      # true on successful merge, false on conflicts


    def _handle_existing_tags(self):
        # Iterate over basetags, looking for changed/removed tags and
        # resolving conflicts between mytags and yourtags.
        for (tag, baseval) in self.basetags:
            myval = self.mytags.get(tag)
            yourval = self.yourtags.get(tag)

            if myval == yourval:
                # same in both parents: doesn't matter if they are the same as
                # the base, we can merge successfully
                self.merged.add(tag, myval)
            elif myval is None and yourval == baseval:
                # deleted in first parent, untouched in second: delete it
                pass
            elif yourval is None and myval == baseval:
                # the converse: again, delete it
                pass
            elif myval != baseval and yourval == baseval:
                # changed in first parent: it wins
                self.merged.add(tag, myval)
            elif yourval != baseval and myval == baseval:
                # changed in second parent: it wins
                self.merged.add(tag, yourval)
            elif myval != baseval and yourval != baseval and myval != yourval:
                # changed in both parents: conflict!
                self.conflict[tag] = (myval, yourval)
            else:
                raise RuntimeError("impossible case: baseval = %r, myval = %r, yourval = %r"
                                   % (baseval, myval, yourval))

            self.seen.add(tag)

    def _handle_added_tags(self):
        for (tag, myval) in self.mytags:
            if tag in self.seen:
                # we have already handled this one above, so skip it
                continue

            yourval = self.yourtags.get(tag)
            if yourval is None:
                # added only in first parent: easy
                self.merged.add(tag, myval)
            elif myval == yourval:
                # added the same on both branches: easy
                self.merged.add(tag, myval)
            elif myval != yourval:
                # added differently on both branches: conflict!
                self.conflict[tag] = (myval, yourval)

            self.seen.add(tag)

        # And finally, detect tags added on second parent.
        for (tag, yourval) in self.yourtags:
            if tag in self.seen:
                continue

            self.merged.add(tag, yourval)

    def _report(self):

        write = self.outfile.write
        for (tag, val) in self.merged:
            write("%s %s\n" % (val, tag))

        for (tag, (first, second)) in self.conflict.items():
            write("<<<<<<< %s\n" % self.myfilename)
            if first is not None:
                print first, tag
            write("=======\n")
            if second is not None:
                write("%s %s\n" % (second, tag))
            write(">>>>>>> %s\n" % self.yourfilename)

def main():
    args = sys.argv[1:]
    progname = os.path.basename(sys.argv[0])
    if len(args) != 3:
        sys.exit("usage: %s <mine> <base> <yours>\n\n"
                 "error: wrong number of arguments\n"
                 % progname)

    ok = MergeEngine(args[0], args[1], args[2], sys.stdout).merge()
    if ok:
        sys.exit(0)
    else:
        sys.stderr.write("%s: warning: conflicts during merge\n" % progname)
        sys.exit(1)

main()


More information about the Mercurial mailing list