Mercurial > hgstorage
changeset 2:ab3f1dd999aa
added example code for merging
| author | Radomir Dopieralski <sheep@stxnext.pl> |
|---|---|
| date | Fri, 22 Apr 2011 11:22:04 +0200 |
| parents | 6d152b15f0d2 |
| children | 6671780796ef |
| files | examples/merging.py |
| diffstat | 1 files changed, 303 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/examples/merging.py Fri Apr 22 11:22:04 2011 +0200 @@ -0,0 +1,303 @@ +"""Utilities for carrying out merges in memory""" + +import os +from contextlib import contextmanager + +from mercurial import merge, node, node as hgnode, util +from mercurial.context import memctx, memfilectx +from mercurial.simplemerge import Merge3Text + +class memworkingctx(memctx): + """Like memctx, but tries to blend in as a workingctx""" + def node(self): + return None + +@contextmanager +def setworkingctx(repo, ctx): + """Context manager that sets repo's working context to ctx""" + class mergerepo(repo.__class__): + def __getitem__(self, changeid): + # repo[None] should return our custom working context + if changeid is None: + return ctx + return super(mergerepo, self).__getitem__(changeid) + oldcls = repo.__class__ + if ctx is not None: + repo.__class__ = mergerepo + try: + yield + finally: + repo.__class__ = oldcls + +def memfilemerge(fcd, fco, fca, **mergeopts): + """Perform a 3-way merge in memory + + For now, we ignore binaries (which simplemerge doesn't support). + """ + def isbin(ctx): + try: + return util.binary(ctx.data()) + except IOError: + return False + + if not fco.cmp(fcd): # files identical? + return fco.data(), False + + binary = isbin(fcd) or isbin(fco) or isbin(fca) + if not binary: + m3 = Merge3Text(fca.data(), fcd.data(), fco.data()) + out = list(m3.merge_lines(**mergeopts)) + return out, m3.conflicts + else: + return [], None + +def memmerge(repo, node1, node2, text='', user=None, date=None, extra=None, + diff3=False, local_marker='yours', remote_marker='theirs', + limit=None): + """Merge node2 into node1 in repo. + + The merge is done in-memory, and a 4-tuple of (memctx, status, + merged, conflict) is returned. status is a list containing 3 lists + listing modified, added, and removed files. merged is a set + containing filenames that required merging. conflict is a set + containing filenames that had merge conflicts. + + To commit the merge, call ctx.commit(). Note that if repo is a + bundlerepo, committing isn't possible. + + To get a diff of the merge: + + >>> ctx, status, conflicts = memmerge(repo, node1, node2) + >>> with setworkingctx(repo, ctx): + >>> patch.diff(repo, p1.node(), ctx.node(), changes=status) + + This works with both regular repos and bundlerepos. + """ + p1 = repo[node1] + p2 = repo[node2] + pa = p1.ancestor(p2) + # If the common ancestor is the same as p1, that means history is + # linear; if pa is the same as p2, that means we're trying to do + # backwards merge, which we don't support (only hg update does + # that). + if pa == p2: + # Nothing to merge + return None, None, set(), set() + + if isinstance(local_marker, unicode): + local_marker = local_marker.encode('utf-8') + if isinstance(remote_marker, unicode): + remote_marker = remote_marker.encode('utf-8') + + def mergeopts(f, f2): + opts = {'name_a': '%s:%s' % (local_marker, f), + 'name_b': '%s:%s' % (remote_marker, f2)} + if diff3: + opts['base_marker'] ='|||||||' + return opts + + for p in (p1, p2, pa): + # Ignore subrepo data + p.substate = {} + + modified, added, removed = [], [], [] + merged, conflicted = set(), set() + actions = merge.manifestmerge(repo, p1, p2, pa, False, False) + + if limit is not None and len(actions) > limit: + raise OverflowError('too many files to merge') + + actions.sort(key=merge.actionkey) + + output = {} + def getfilectx(repo_, mctx, f): + if f not in output: + raise IOError + fctx, data, mode, copied = output[f] + if data is None: + data = fctx.data() + fctx = memfilectx(f, data, 'l' in mode, 'x' in mode, copied) + # FIXME: Hack to appease patch.diff(), which doesn't work + # with memfilectxes. + fctx.filelog = lambda: None + return fctx + + # manifestmerge() generates a list of actions that we have to + # carry out to do the merge. The actions are as follows: + # + # - 'r': A file (f) was removed in the other head. + # + # - 'm': A 3-way merge needs to happen. f is the source file, f2 + # is the file in the other head, fd is the destination + # file. If f and f2 differ, there was a copy/rename. If the + # move flag is True, it was a rename. flags contains + # link/exec bits (same as filectx.flags()). + # + # - 'd': A file was renamed as the result of a directory being + # renamed. If f is non-empty, it's a rename of f to fd; if + # f2 is non-empty, it's a copy of f2 to fd. + # + # - 'g': A file (f) was added or modified in the other head, but + # there were no changes to it in our line of history. If f + # is in our head's manifest, it was a modification; if it's + # only in the other head's manifest, it was an addition. + # + # - 'e': Just the exec bit of a file (f) was changed in the other + # head. + for a in actions: + f, m = a[:2] + if f and f[0] == '/': + continue + if m == 'r': # remove file + removed.append(f) + elif m == 'm': # merge file changes between heads + f2, fd, flags, move = a[2:] + if f != f2: + if move: + # file rename + removed.append(f) + # file copy + if f != fd: + copied = f + fcd = p1[f] # local destination file (doesn't exist) + else: + copied = f2 + fcd = p2[f2] # local destination file (doesn't exist) + added.append(fd) + else: + copied = None + modified.append(f) + fcd = p1[fd] # local destination file (exists) + + fcl = p1[f] # local source file + fco = p2[f2] # other destination file + fca = fcl.ancestor(p2[f2], pa) # common ancestor + if not fca: + # no common ancestor + fca = repo.filectx(f, fileid=node.nullrev) + afile = fca.path() # ancestor file path + anode = node.hex(fca.filenode()) # ancestor file node + fca = repo.filectx(afile, fileid=anode) # base file + + out, conflicts = memfilemerge(fcd, fco, fca, **mergeopts(f, f2)) + merged.add(fd) + if conflicts: + conflicted.add(fd) + # merge result + output[fd] = (None, ''.join(out), flags, copied) + elif m == 'd': # rename file as part of a directory rename + f2, fd, flags = a[2:] + if f: + # file rename + removed.append(f) + added.append(fd) + fctx = p1[f] + output[fd] = (fctx, None, flags, f) + if f2: + # file copy + added.append(fd) + fctx = p2[f2] + output[fd] = (fctx, None, flags, f) + elif m == 'g': # update file (added/modified in other head) + flags = a[2] + # was it added or modified? + if f in p1.manifest(): + modified.append(f) + else: + added.append(f) + fctx = p2.filectx(f) + output[f] = (fctx, None, flags, None) + elif m == 'e': # update file execute bit + flags = a[2] + modified.append(f) + fctx = p2.filectx(f) + output[f] = (fctx, None, flags, None) + + status = [modified, added, removed] + + # create a memory context containing the potential merge + ctx = memworkingctx(repo, (p1.node(), p2.node()), text, + [fn for st in status for fn in st], + getfilectx, user, date, extra) + return ctx, status, merged, conflicted + +def memupdate(repo, node, branchmerge, force, partial): + """Experimental equivalent of hg's merge.update(). + + For cases that we can't handle, we shell out to + merge.update(). This requires a repository with a working copy to + work properly. + + For now, this is primarily meant to be plugged into hg so we can + run its test suite with our merge algorithm to verify the + algorithm's correctness. + """ + onode = node + wlock = repo.wlock() + try: + wc = repo[None] + if node is None: + try: + node = repo.branchtags()[wc.branch()] + except KeyError: + if wc.branch() == 'default': + node = repo.lookup('tip') + else: + return merge.update(repo, onode, branchmerge, force, + partial) + + pl = wc.parents() + p1, p2 = pl[0], repo[node] + pa = p1.ancestor(p2) + xp1, xp2 = str(p1), str(p2) + if len(pl) > 1 or pa in (p1, p2): + return merge.update(repo, onode, branchmerge, force, partial) + + ctx, status, merged, conflicts = memmerge(repo, p1.node(), p2.node()) + # Since we don't modify the working copy, we need to make note + # of where we merged so memstatus() and memcommit() can pick + # up where we left off. + with open(repo.join('memmerge'), 'wb') as f: + f.write(hgnode.hex(p1.node())) + f.write(' ') + f.write(hgnode.hex(p2.node())) + stats = (len(status[0]) + len(status[1]), len(merged), + len(status[2]), len(conflicts)) + finally: + wlock.release() + + if not partial: + repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3]) + return stats + +def memstatus(repo, node1, node2, match, a1, a2, a3, a4): + """Experimental version of hg's repo.status() that uses memmerge() + data. + + For now, this is primarily meant for testing our merge algorithm. + """ + if not os.path.exists(repo.join('memmerge')): + return repo.status(node1, node2, match, a1, a2, a3, a4) + with open(repo.join('memmerge'), 'rb') as f: + p1, p2 = f.read().strip().split() + ctx, status, merged, conflicts = memmerge(repo, p1, p2) + return status + [[], [], [], []] + +def memcommit(repo, message, user, date, match, editor, extra): + """Experimental version of hg's repo.commit() that uses + memmerge() data. + + For now, this is primarily meant for testing our merge algorithm. + """ + if not os.path.exists(repo.join('memmerge')): + return repo.commit(message, user, date, match, editor=editor, + extra=extra) + with open(repo.join('memmerge'), 'rb') as f: + p1, p2 = f.read().strip().split() + ctx, status, merged, conflicts = memmerge(repo, p1, p2, message, user, + date, extra) + branch = repo[None].branch() + repo.dirstate.branch = lambda: branch + n = ctx.commit() + os.unlink(repo.join('memmerge')) + return n
