hgext/imerge.py
changeset 5042 58006f8b8275
child 5054 ec70fd08e16c
equal deleted inserted replaced
5041:49059086c634 5042:58006f8b8275
       
     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 *
       
    10 from mercurial import commands, cmdutil, hg, merge, util
       
    11 import os, tarfile
       
    12 
       
    13 class InvalidStateFileException(Exception): pass
       
    14 
       
    15 class ImergeStateFile(object):
       
    16     def __init__(self, im):
       
    17         self.im = im
       
    18 
       
    19     def save(self, dest):
       
    20         tf = tarfile.open(dest, 'w:gz')
       
    21 
       
    22         st = os.path.join(self.im.path, 'status')
       
    23         tf.add(st, os.path.join('.hg', 'imerge', 'status'))
       
    24 
       
    25         for f in self.im.resolved:
       
    26             abssrc = self.im.repo.wjoin(f)
       
    27             tf.add(abssrc, f)
       
    28 
       
    29         tf.close()
       
    30 
       
    31     def load(self, source):
       
    32         wlock = self.im.repo.wlock()
       
    33         lock = self.im.repo.lock()
       
    34 
       
    35         tf = tarfile.open(source, 'r')
       
    36         contents = tf.getnames()
       
    37         statusfile = os.path.join('.hg', 'imerge', 'status')
       
    38         if statusfile not in contents:
       
    39             raise InvalidStateFileException('no status file')
       
    40 
       
    41         tf.extract(statusfile, self.im.repo.root)
       
    42         self.im.load()
       
    43         p1 = self.im.parents[0].node()
       
    44         p2 = self.im.parents[1].node()
       
    45         if self.im.repo.dirstate.parents()[0] != p1:
       
    46             hg.clean(self.im.repo, self.im.parents[0].node())
       
    47         self.im.start(p2)
       
    48         tf.extractall(self.im.repo.root)
       
    49         self.im.load()
       
    50 
       
    51 class Imerge(object):
       
    52     def __init__(self, ui, repo):
       
    53         self.ui = ui
       
    54         self.repo = repo
       
    55 
       
    56         self.path = repo.join('imerge')
       
    57         self.opener = util.opener(self.path)
       
    58 
       
    59         self.parents = [self.repo.changectx(n)
       
    60                         for n in self.repo.dirstate.parents()]
       
    61         self.conflicts = {}
       
    62         self.resolved = []
       
    63 
       
    64     def merging(self):
       
    65         return self.parents[1].node() != nullid
       
    66 
       
    67     def load(self):
       
    68         # status format. \0-delimited file, fields are
       
    69         # p1, p2, conflict count, conflict filenames, resolved filenames
       
    70         # conflict filenames are pairs of localname, remotename
       
    71 
       
    72         statusfile = self.opener('status')
       
    73 
       
    74         status = statusfile.read().split('\0')
       
    75         if len(status) < 3:
       
    76             raise util.Abort('invalid imerge status file')
       
    77 
       
    78         try:
       
    79             self.parents = [self.repo.changectx(n) for n in status[:2]]
       
    80         except LookupError:
       
    81             raise util.Abort('merge parent %s not in repository' % short(p))
       
    82 
       
    83         status = status[2:]
       
    84         conflicts = int(status.pop(0)) * 2
       
    85         self.resolved = status[conflicts:]
       
    86         for i in xrange(0, conflicts, 2):
       
    87             self.conflicts[status[i]] = status[i+1]
       
    88 
       
    89     def save(self):
       
    90         lock = self.repo.lock()
       
    91 
       
    92         if not os.path.isdir(self.path):
       
    93             os.mkdir(self.path)
       
    94         fd = self.opener('status', 'wb')
       
    95 
       
    96         out = [hex(n.node()) for n in self.parents]
       
    97         out.append(str(len(self.conflicts)))
       
    98         for f in sorted(self.conflicts):
       
    99             out.append(f)
       
   100             out.append(self.conflicts[f])
       
   101         out.extend(self.resolved)
       
   102 
       
   103         fd.write('\0'.join(out))
       
   104 
       
   105     def remaining(self):
       
   106         return [f for f in self.conflicts if f not in self.resolved]
       
   107 
       
   108     def filemerge(self, fn):
       
   109         wlock = self.repo.wlock()
       
   110 
       
   111         fo = self.conflicts[fn]
       
   112         return merge.filemerge(self.repo, fn, fo, self.parents[0],
       
   113                                self.parents[1])
       
   114 
       
   115     def start(self, rev=None):
       
   116         _filemerge = merge.filemerge
       
   117         def filemerge(repo, fw, fo, wctx, mctx):
       
   118             self.conflicts[fw] = fo
       
   119 
       
   120         merge.filemerge = filemerge
       
   121         commands.merge(self.ui, self.repo, rev=rev)
       
   122         merge.filemerge = _filemerge
       
   123 
       
   124         self.parents = [self.repo.changectx(n)
       
   125                         for n in self.repo.dirstate.parents()]
       
   126         self.save()
       
   127 
       
   128     def resume(self):
       
   129         self.load()
       
   130 
       
   131         dp = self.repo.dirstate.parents()
       
   132         if self.parents[0].node() != dp[0] or self.parents[1].node() != dp[1]:
       
   133             raise util.Abort('imerge state does not match working directory')
       
   134 
       
   135     def status(self):
       
   136         self.ui.write('merging %s and %s\n' % \
       
   137                       (short(self.parents[0].node()),
       
   138                        short(self.parents[1].node())))
       
   139 
       
   140         if self.resolved:
       
   141             self.ui.write('resolved:\n')
       
   142             for fn in self.resolved:
       
   143                 self.ui.write('  %s\n' % fn)
       
   144         remaining = [f for f in self.conflicts if f not in self.resolved]
       
   145         if remaining:
       
   146             self.ui.write('remaining:\n')
       
   147             for fn in remaining:
       
   148                 fo = self.conflicts[fn]
       
   149                 if fn == fo:
       
   150                     self.ui.write('  %s\n' % (fn,))
       
   151                 else:
       
   152                     self.ui.write('  %s (%s)\n' % (fn, fo))
       
   153         else:
       
   154             self.ui.write('all conflicts resolved\n')
       
   155 
       
   156     def next(self):
       
   157         remaining = self.remaining()
       
   158         return remaining and remaining[0]
       
   159 
       
   160     def resolve(self, files):
       
   161         resolved = dict.fromkeys(self.resolved)
       
   162         for fn in files:
       
   163             if fn not in self.conflicts:
       
   164                 raise util.Abort('%s is not in the merge set' % fn)
       
   165             resolved[fn] = True
       
   166         self.resolved = sorted(resolved)
       
   167         self.save()
       
   168         return 0
       
   169 
       
   170     def unresolve(self, files):
       
   171         resolved = dict.fromkeys(self.resolved)
       
   172         for fn in files:
       
   173             if fn not in resolved:
       
   174                 raise util.Abort('%s is not resolved' % fn)
       
   175             del resolved[fn]
       
   176         self.resolved = sorted(resolved)
       
   177         self.save()
       
   178         return 0
       
   179 
       
   180     def pickle(self, dest):
       
   181         '''write current merge state to file to be resumed elsewhere'''
       
   182         state = ImergeStateFile(self)
       
   183         return state.save(dest)
       
   184 
       
   185     def unpickle(self, source):
       
   186         '''read merge state from file'''
       
   187         state = ImergeStateFile(self)
       
   188         return state.load(source)
       
   189 
       
   190 def load(im, source):
       
   191     if im.merging():
       
   192         raise util.Abort('there is already a merge in progress '
       
   193                          '(update -C <rev> to abort it)' )
       
   194     m, a, r, d =  im.repo.status()[:4]
       
   195     if m or a or r or d:
       
   196         raise util.Abort('working directory has uncommitted changes')
       
   197 
       
   198     rc = im.unpickle(source)
       
   199     if not rc:
       
   200         im.status()
       
   201     return rc
       
   202 
       
   203 def merge_(im, filename=None):
       
   204     if not filename:
       
   205         filename = im.next()
       
   206         if not filename:
       
   207             im.ui.write('all conflicts resolved\n')
       
   208             return 0
       
   209 
       
   210     rc = im.filemerge(filename)
       
   211     if not rc:
       
   212         im.resolve([filename])
       
   213         if not im.next():
       
   214             im.ui.write('all conflicts resolved\n')
       
   215             return 0
       
   216     return rc
       
   217 
       
   218 def next(im):
       
   219     n = im.next()
       
   220     if n:
       
   221         im.ui.write('%s\n' % n)
       
   222     else:
       
   223         im.ui.write('all conflicts resolved\n')
       
   224     return 0
       
   225 
       
   226 def resolve(im, *files):
       
   227     if not files:
       
   228         raise util.Abort('resolve requires at least one filename')
       
   229     return im.resolve(files)
       
   230 
       
   231 def save(im, dest):
       
   232     return im.pickle(dest)
       
   233 
       
   234 def status(im):
       
   235     im.status()
       
   236     return 0
       
   237 
       
   238 def unresolve(im, *files):
       
   239     if not files:
       
   240         raise util.Abort('unresolve requires at least one filename')
       
   241     return im.unresolve(files)
       
   242 
       
   243 subcmdtable = {
       
   244     'load': load,
       
   245     'merge': merge_,
       
   246     'next': next,
       
   247     'resolve': resolve,
       
   248     'save': save,
       
   249     'status': status,
       
   250     'unresolve': unresolve
       
   251 }
       
   252 
       
   253 def dispatch(im, args, opts):
       
   254     def complete(s, choices):
       
   255         candidates = []
       
   256         for choice in choices:
       
   257             if choice.startswith(s):
       
   258                 candidates.append(choice)
       
   259         return candidates
       
   260 
       
   261     c, args = args[0], args[1:]
       
   262     cmd = complete(c, subcmdtable.keys())
       
   263     if not cmd:
       
   264         raise cmdutil.UnknownCommand('imerge ' + c)
       
   265     if len(cmd) > 1:
       
   266         raise cmdutil.AmbiguousCommand('imerge ' + c, sorted(cmd))
       
   267     cmd = cmd[0]
       
   268 
       
   269     func = subcmdtable[cmd]
       
   270     try:
       
   271         return func(im, *args)
       
   272     except TypeError:
       
   273         raise cmdutil.ParseError('imerge', '%s: invalid arguments' % cmd)
       
   274 
       
   275 def imerge(ui, repo, *args, **opts):
       
   276     '''interactive merge
       
   277 
       
   278     imerge lets you split a merge into pieces. When you start a merge
       
   279     with imerge, the names of all files with conflicts are recorded.
       
   280     You can then merge any of these files, and if the merge is
       
   281     successful, they will be marked as resolved. When all files are
       
   282     resolved, the merge is complete.
       
   283 
       
   284     If no merge is in progress, hg imerge [rev] will merge the working
       
   285     directory with rev (defaulting to the other head if the repository
       
   286     only has two heads). You may also resume a saved merge with
       
   287     hg imerge load <file>.
       
   288 
       
   289     If a merge is in progress, hg imerge will default to merging the
       
   290     next unresolved file.
       
   291 
       
   292     The following subcommands are available:
       
   293 
       
   294     status:
       
   295       show the current state of the merge
       
   296     next:
       
   297       show the next unresolved file merge
       
   298     merge [<file>]:
       
   299       merge <file>. If the file merge is successful, the file will be
       
   300       recorded as resolved. If no file is given, the next unresolved
       
   301       file will be merged.
       
   302     resolve <file>...:
       
   303       mark files as successfully merged
       
   304     unresolve <file>...:
       
   305       mark files as requiring merging.
       
   306     save <file>:
       
   307       save the state of the merge to a file to be resumed elsewhere
       
   308     load <file>:
       
   309       load the state of the merge from a file created by save
       
   310     '''
       
   311 
       
   312     im = Imerge(ui, repo)
       
   313 
       
   314     if im.merging():
       
   315         im.resume()
       
   316     else:
       
   317         rev = opts.get('rev')
       
   318         if rev and args:
       
   319             raise util.Abort('please specify just one revision')
       
   320         
       
   321         if len(args) == 2 and args[0] == 'load':
       
   322             pass
       
   323         else:
       
   324             if args:
       
   325                 rev = args[0]
       
   326             im.start(rev=rev)
       
   327             args = ['status']
       
   328 
       
   329     if not args:
       
   330         args = ['merge']
       
   331 
       
   332     return dispatch(im, args, opts)
       
   333 
       
   334 cmdtable = {
       
   335     '^imerge':
       
   336     (imerge,
       
   337      [('r', 'rev', '', _('revision to merge'))], 'hg imerge [command]')
       
   338 }