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