hgext/imerge.py
changeset 7803 6d99ff7b79b5
parent 7802 dd970a311ea8
parent 7801 e5627562b9f2
child 7810 b136c6c5c1c7
equal deleted inserted replaced
7802:dd970a311ea8 7803:6d99ff7b79b5
     1 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
       
     2 # Published under the GNU GPL
       
     3 
       
     4 '''
       
     5 imerge - interactive merge
       
     6 '''
       
     7 
       
     8 from mercurial.i18n import _
       
     9 from mercurial.node import hex, short
       
    10 from mercurial import commands, cmdutil, dispatch, fancyopts
       
    11 from mercurial import hg, filemerge, util, revlog
       
    12 import os, tarfile
       
    13 
       
    14 class InvalidStateFileException(Exception): pass
       
    15 
       
    16 class ImergeStateFile(object):
       
    17     def __init__(self, im):
       
    18         self.im = im
       
    19 
       
    20     def save(self, dest):
       
    21         tf = tarfile.open(dest, 'w:gz')
       
    22 
       
    23         st = os.path.join(self.im.path, 'status')
       
    24         tf.add(st, os.path.join('.hg', 'imerge', 'status'))
       
    25 
       
    26         for f in self.im.resolved:
       
    27             (fd, fo) = self.im.conflicts[f]
       
    28             abssrc = self.im.repo.wjoin(fd)
       
    29             tf.add(abssrc, fd)
       
    30 
       
    31         tf.close()
       
    32 
       
    33     def load(self, source):
       
    34         wlock = self.im.repo.wlock()
       
    35         lock = self.im.repo.lock()
       
    36 
       
    37         tf = tarfile.open(source, 'r')
       
    38         contents = tf.getnames()
       
    39         # tarfile normalizes path separators to '/'
       
    40         statusfile = '.hg/imerge/status'
       
    41         if statusfile not in contents:
       
    42             raise InvalidStateFileException('no status file')
       
    43 
       
    44         tf.extract(statusfile, self.im.repo.root)
       
    45         p1, p2 = self.im.load()
       
    46         if self.im.repo.dirstate.parents()[0] != p1.node():
       
    47             hg.clean(self.im.repo, p1.node())
       
    48         self.im.start(p2.node())
       
    49         for tarinfo in tf:
       
    50             tf.extract(tarinfo, self.im.repo.root)
       
    51         self.im.load()
       
    52 
       
    53 class Imerge(object):
       
    54     def __init__(self, ui, repo):
       
    55         self.ui = ui
       
    56         self.repo = repo
       
    57 
       
    58         self.path = repo.join('imerge')
       
    59         self.opener = util.opener(self.path)
       
    60 
       
    61         self.wctx = self.repo.workingctx()
       
    62         self.conflicts = {}
       
    63         self.resolved = []
       
    64 
       
    65     def merging(self):
       
    66         return len(self.wctx.parents()) > 1
       
    67 
       
    68     def load(self):
       
    69         # status format. \0-delimited file, fields are
       
    70         # p1, p2, conflict count, conflict filenames, resolved filenames
       
    71         # conflict filenames are tuples of localname, remoteorig, remotenew
       
    72 
       
    73         statusfile = self.opener('status')
       
    74 
       
    75         status = statusfile.read().split('\0')
       
    76         if len(status) < 3:
       
    77             raise util.Abort(_('invalid imerge status file'))
       
    78 
       
    79         try:
       
    80             parents = [self.repo.changectx(n) for n in status[:2]]
       
    81         except revlog.LookupError, e:
       
    82             raise util.Abort(_('merge parent %s not in repository') %
       
    83                              short(e.name))
       
    84 
       
    85         status = status[2:]
       
    86         conflicts = int(status.pop(0)) * 3
       
    87         self.resolved = status[conflicts:]
       
    88         for i in xrange(0, conflicts, 3):
       
    89             self.conflicts[status[i]] = (status[i+1], status[i+2])
       
    90 
       
    91         return parents
       
    92 
       
    93     def save(self):
       
    94         lock = self.repo.lock()
       
    95 
       
    96         if not os.path.isdir(self.path):
       
    97             os.mkdir(self.path)
       
    98         statusfile = self.opener('status', 'wb')
       
    99 
       
   100         out = [hex(n.node()) for n in self.wctx.parents()]
       
   101         out.append(str(len(self.conflicts)))
       
   102         conflicts = self.conflicts.items()
       
   103         conflicts.sort()
       
   104         for fw, fd_fo in conflicts:
       
   105             out.append(fw)
       
   106             out.extend(fd_fo)
       
   107         out.extend(self.resolved)
       
   108 
       
   109         statusfile.write('\0'.join(out))
       
   110 
       
   111     def remaining(self):
       
   112         return [f for f in self.conflicts if f not in self.resolved]
       
   113 
       
   114     def filemerge(self, fn, interactive=True):
       
   115         wlock = self.repo.wlock()
       
   116 
       
   117         (fd, fo) = self.conflicts[fn]
       
   118         p1, p2 = self.wctx.parents()
       
   119 
       
   120         # this could be greatly improved
       
   121         realmerge = os.environ.get('HGMERGE')
       
   122         if not interactive:
       
   123             os.environ['HGMERGE'] = 'internal:merge'
       
   124 
       
   125         # The filemerge ancestor algorithm does not work if self.wctx
       
   126         # already has two parents (in normal merge it doesn't yet). But
       
   127         # this is very dirty.
       
   128         self.wctx._parents.pop()
       
   129         try:
       
   130             # TODO: we should probably revert the file if merge fails
       
   131             return filemerge.filemerge(self.repo, fn, fd, fo, self.wctx, p2)
       
   132         finally:
       
   133             self.wctx._parents.append(p2)
       
   134             if realmerge:
       
   135                 os.environ['HGMERGE'] = realmerge
       
   136             elif not interactive:
       
   137                 del os.environ['HGMERGE']
       
   138 
       
   139     def start(self, rev=None):
       
   140         _filemerge = filemerge.filemerge
       
   141         def filemerge_(repo, fw, fd, fo, wctx, mctx):
       
   142             self.conflicts[fw] = (fd, fo)
       
   143 
       
   144         filemerge.filemerge = filemerge_
       
   145         commands.merge(self.ui, self.repo, rev=rev)
       
   146         filemerge.filemerge = _filemerge
       
   147 
       
   148         self.wctx = self.repo.workingctx()
       
   149         self.save()
       
   150 
       
   151     def resume(self):
       
   152         self.load()
       
   153 
       
   154         dp = self.repo.dirstate.parents()
       
   155         p1, p2 = self.wctx.parents()
       
   156         if p1.node() != dp[0] or p2.node() != dp[1]:
       
   157             raise util.Abort(_('imerge state does not match working directory'))
       
   158 
       
   159     def next(self):
       
   160         remaining = self.remaining()
       
   161         return remaining and remaining[0]
       
   162 
       
   163     def resolve(self, files):
       
   164         resolved = dict.fromkeys(self.resolved)
       
   165         for fn in files:
       
   166             if fn not in self.conflicts:
       
   167                 raise util.Abort(_('%s is not in the merge set') % fn)
       
   168             resolved[fn] = True
       
   169         self.resolved = resolved.keys()
       
   170         self.resolved.sort()
       
   171         self.save()
       
   172         return 0
       
   173 
       
   174     def unresolve(self, files):
       
   175         resolved = dict.fromkeys(self.resolved)
       
   176         for fn in files:
       
   177             if fn not in resolved:
       
   178                 raise util.Abort(_('%s is not resolved') % fn)
       
   179             del resolved[fn]
       
   180         self.resolved = resolved.keys()
       
   181         self.resolved.sort()
       
   182         self.save()
       
   183         return 0
       
   184 
       
   185     def pickle(self, dest):
       
   186         '''write current merge state to file to be resumed elsewhere'''
       
   187         state = ImergeStateFile(self)
       
   188         return state.save(dest)
       
   189 
       
   190     def unpickle(self, source):
       
   191         '''read merge state from file'''
       
   192         state = ImergeStateFile(self)
       
   193         return state.load(source)
       
   194 
       
   195 def load(im, source):
       
   196     if im.merging():
       
   197         raise util.Abort(_('there is already a merge in progress '
       
   198                            '(update -C <rev> to abort it)'))
       
   199     m, a, r, d =  im.repo.status()[:4]
       
   200     if m or a or r or d:
       
   201         raise util.Abort(_('working directory has uncommitted changes'))
       
   202 
       
   203     rc = im.unpickle(source)
       
   204     if not rc:
       
   205         status(im)
       
   206     return rc
       
   207 
       
   208 def merge_(im, filename=None, auto=False):
       
   209     success = True
       
   210     if auto and not filename:
       
   211         for fn in im.remaining():
       
   212             rc = im.filemerge(fn, interactive=False)
       
   213             if rc:
       
   214                 success = False
       
   215             else:
       
   216                 im.resolve([fn])
       
   217         if success:
       
   218             im.ui.write('all conflicts resolved\n')
       
   219         else:
       
   220             status(im)
       
   221         return 0
       
   222 
       
   223     if not filename:
       
   224         filename = im.next()
       
   225         if not filename:
       
   226             im.ui.write('all conflicts resolved\n')
       
   227             return 0
       
   228 
       
   229     rc = im.filemerge(filename, interactive=not auto)
       
   230     if not rc:
       
   231         im.resolve([filename])
       
   232         if not im.next():
       
   233             im.ui.write('all conflicts resolved\n')
       
   234     return rc
       
   235 
       
   236 def next(im):
       
   237     n = im.next()
       
   238     if n:
       
   239         im.ui.write('%s\n' % n)
       
   240     else:
       
   241         im.ui.write('all conflicts resolved\n')
       
   242     return 0
       
   243 
       
   244 def resolve(im, *files):
       
   245     if not files:
       
   246         raise util.Abort(_('resolve requires at least one filename'))
       
   247     return im.resolve(files)
       
   248 
       
   249 def save(im, dest):
       
   250     return im.pickle(dest)
       
   251 
       
   252 def status(im, **opts):
       
   253     if not opts.get('resolved') and not opts.get('unresolved'):
       
   254         opts['resolved'] = True
       
   255         opts['unresolved'] = True
       
   256 
       
   257     if im.ui.verbose:
       
   258         p1, p2 = [short(p.node()) for p in im.wctx.parents()]
       
   259         im.ui.note(_('merging %s and %s\n') % (p1, p2))
       
   260 
       
   261     conflicts = im.conflicts.keys()
       
   262     conflicts.sort()
       
   263     remaining = dict.fromkeys(im.remaining())
       
   264     st = []
       
   265     for fn in conflicts:
       
   266         if opts.get('no_status'):
       
   267             mode = ''
       
   268         elif fn in remaining:
       
   269             mode = 'U '
       
   270         else:
       
   271             mode = 'R '
       
   272         if ((opts.get('resolved') and fn not in remaining)
       
   273             or (opts.get('unresolved') and fn in remaining)):
       
   274             st.append((mode, fn))
       
   275     st.sort()
       
   276     for (mode, fn) in st:
       
   277         if im.ui.verbose:
       
   278             fo, fd = im.conflicts[fn]
       
   279             if fd != fn:
       
   280                 fn = '%s (%s)' % (fn, fd)
       
   281         im.ui.write('%s%s\n' % (mode, fn))
       
   282     if opts.get('unresolved') and not remaining:
       
   283         im.ui.write(_('all conflicts resolved\n'))
       
   284 
       
   285     return 0
       
   286 
       
   287 def unresolve(im, *files):
       
   288     if not files:
       
   289         raise util.Abort(_('unresolve requires at least one filename'))
       
   290     return im.unresolve(files)
       
   291 
       
   292 subcmdtable = {
       
   293     'load': (load, []),
       
   294     'merge':
       
   295         (merge_,
       
   296          [('a', 'auto', None, _('automatically resolve if possible'))]),
       
   297     'next': (next, []),
       
   298     'resolve': (resolve, []),
       
   299     'save': (save, []),
       
   300     'status':
       
   301         (status,
       
   302          [('n', 'no-status', None, _('hide status prefix')),
       
   303           ('', 'resolved', None, _('only show resolved conflicts')),
       
   304           ('', 'unresolved', None, _('only show unresolved conflicts'))]),
       
   305     'unresolve': (unresolve, [])
       
   306 }
       
   307 
       
   308 def dispatch_(im, args, opts):
       
   309     def complete(s, choices):
       
   310         candidates = []
       
   311         for choice in choices:
       
   312             if choice.startswith(s):
       
   313                 candidates.append(choice)
       
   314         return candidates
       
   315 
       
   316     c, args = args[0], list(args[1:])
       
   317     cmd = complete(c, subcmdtable.keys())
       
   318     if not cmd:
       
   319         raise cmdutil.UnknownCommand('imerge ' + c)
       
   320     if len(cmd) > 1:
       
   321         cmd.sort()
       
   322         raise cmdutil.AmbiguousCommand('imerge ' + c, cmd)
       
   323     cmd = cmd[0]
       
   324 
       
   325     func, optlist = subcmdtable[cmd]
       
   326     opts = {}
       
   327     try:
       
   328         args = fancyopts.fancyopts(args, optlist, opts)
       
   329         return func(im, *args, **opts)
       
   330     except fancyopts.getopt.GetoptError, inst:
       
   331         raise dispatch.ParseError('imerge', '%s: %s' % (cmd, inst))
       
   332     except TypeError:
       
   333         raise dispatch.ParseError('imerge', _('%s: invalid arguments') % cmd)
       
   334 
       
   335 def imerge(ui, repo, *args, **opts):
       
   336     '''interactive merge
       
   337 
       
   338     imerge lets you split a merge into pieces. When you start a merge
       
   339     with imerge, the names of all files with conflicts are recorded.
       
   340     You can then merge any of these files, and if the merge is
       
   341     successful, they will be marked as resolved. When all files are
       
   342     resolved, the merge is complete.
       
   343 
       
   344     If no merge is in progress, hg imerge [rev] will merge the working
       
   345     directory with rev (defaulting to the other head if the repository
       
   346     only has two heads). You may also resume a saved merge with
       
   347     hg imerge load <file>.
       
   348 
       
   349     If a merge is in progress, hg imerge will default to merging the
       
   350     next unresolved file.
       
   351 
       
   352     The following subcommands are available:
       
   353 
       
   354     status:
       
   355       show the current state of the merge
       
   356       options:
       
   357         -n --no-status:  do not print the status prefix
       
   358            --resolved:   only print resolved conflicts
       
   359            --unresolved: only print unresolved conflicts
       
   360     next:
       
   361       show the next unresolved file merge
       
   362     merge [<file>]:
       
   363       merge <file>. If the file merge is successful, the file will be
       
   364       recorded as resolved. If no file is given, the next unresolved
       
   365       file will be merged.
       
   366     resolve <file>...:
       
   367       mark files as successfully merged
       
   368     unresolve <file>...:
       
   369       mark files as requiring merging.
       
   370     save <file>:
       
   371       save the state of the merge to a file to be resumed elsewhere
       
   372     load <file>:
       
   373       load the state of the merge from a file created by save
       
   374     '''
       
   375 
       
   376     im = Imerge(ui, repo)
       
   377 
       
   378     if im.merging():
       
   379         im.resume()
       
   380     else:
       
   381         rev = opts.get('rev')
       
   382         if rev and args:
       
   383             raise util.Abort(_('please specify just one revision'))
       
   384 
       
   385         if len(args) == 2 and args[0] == 'load':
       
   386             pass
       
   387         else:
       
   388             if args:
       
   389                 rev = args[0]
       
   390             im.start(rev=rev)
       
   391             if opts.get('auto'):
       
   392                 args = ['merge', '--auto']
       
   393             else:
       
   394                 args = ['status']
       
   395 
       
   396     if not args:
       
   397         args = ['merge']
       
   398 
       
   399     return dispatch_(im, args, opts)
       
   400 
       
   401 cmdtable = {
       
   402     '^imerge':
       
   403     (imerge,
       
   404      [('r', 'rev', '', _('revision to merge')),
       
   405       ('a', 'auto', None, _('automatically merge where possible'))],
       
   406       _('hg imerge [command]'))
       
   407 }