Commits

Steve Borho committed 07672a7

partialcommit: rewrite while following localrepo.commit() more closely

* should fully support subrepos and file patterns
* correctly handle --subrepos, and --date options
* correctly abort on many user errors

  • Participants
  • Parent commits c8a049d

Comments (0)

Files changed (1)

File tortoisehg/hgqt/partialcommit.py

 from mercurial.i18n import _
 from mercurial.node import hex, nullid
 from mercurial import patch, commands, extensions, scmutil, encoding, context
-from mercurial import error, bookmarks, merge
+from mercurial import error, bookmarks, util
+from mercurial import merge as mergemod
 
 #
 # Note all the translatable strings in this file are copies of Mercurial
                           date, extra)
 
 def partialcommit(orig, ui, repo, *pats, **opts):
-    # partial commit requires explicit file list (no patterns)
-    # working folder must have a single parent
-    # does not emit as many warnings and messages as the real commit
-    # opts['message'] is mandatory
+    # opts['message'] is mandatory. --addremove is ignored
+    # partial selection patch will only affect modified (M) files.  All adds
+    # and removes and non-partial modifications are handled via wctx.
     if 'partials' not in opts:
         return orig(ui, repo, *pats, **opts)
 
+    def fail(f, msg):
+        raise util.Abort('%s: %s' % (f, msg))
+
     if opts.get('subrepos'):
         # Let --subrepos on the command line override config setting.
-        ui.setconfig('ui', 'commitsubrepos', True)
+        repo.ui.setconfig('ui', 'commitsubrepos', True)
 
-    files = [scmutil.canonpath(repo.root, repo.root, f) for f in pats]
+    force = opts.get('force')
+    date = opts.get('date')
+    if date:
+        opts['date'] = util.parsedate(date)
 
-    ms = merge.mergestate(repo)
-    for f in files:
-        if f in ms and ms[f] == 'u':
-            raise error.Abort(_("unresolved merge conflicts "
-                                "(see hg help resolve)"))
+    wlock = repo.wlock()
+    try:
+        wctx = repo[None]
+        merge = len(wctx.parents()) > 1
 
-    patchfile = opts['partials']
-    fp = open(patchfile, 'rb')
+        extra = {'branch': encoding.fromlocal(wctx.branch())}
+        if opts.get('close_branch'):
+            if repo['.'].node() not in repo.branchheads():
+                # The topo heads set is included in the branch heads set of the
+                # current branch, so it's sufficient to test branchheads
+                raise util.Abort(_('can only close branch heads'))
+            extra['close'] = 1
 
-    newrev = None
-    store = patch.filestore()
-    try:
-        # TODO: likely need to copy .hgsub/.hgsubstate code from
-        # localrepo.commit() lines 1303-1355, 1396-1404
+        match = scmutil.match(wctx, pats, opts)
+        if not force:
+            vdirs = []
+            match.dir = vdirs.append
+            match.bad = fail
+
+        if (not force and merge and match and
+            (match.files() or match.anypats())):
+            raise util.Abort(_('cannot partially commit a merge '
+                               '(do not specify files or patterns)'))
+
+        changes = repo.status(match=match, clean=force)
+        if force:
+            changes[0].extend(changes[6]) # mq may commit unchanged files
+
+        # check subrepos
+        subs = []
+        commitsubs = set()
+        newstate = wctx.substate.copy()
+        # only manage subrepos and .hgsubstate if .hgsub is present
+        if '.hgsub' in wctx:
+            # we'll decide whether to track this ourselves, thanks
+            if '.hgsubstate' in changes[0]:
+                changes[0].remove('.hgsubstate')
+            if '.hgsubstate' in changes[2]:
+                changes[2].remove('.hgsubstate')
+
+            # compare current state to last committed state
+            # build new substate based on last committed state
+            oldstate = wctx.p1().substate
+            for s in sorted(newstate.keys()):
+                if not match(s):
+                    # ignore working copy, use old state if present
+                    if s in oldstate:
+                        newstate[s] = oldstate[s]
+                        continue
+                    if not force:
+                        raise util.Abort(
+                            _("commit with new subrepo %s excluded") % s)
+                if wctx.sub(s).dirty(True):
+                    if not repo.ui.configbool('ui', 'commitsubrepos'):
+                        raise util.Abort(
+                            _("uncommitted changes in subrepo %s") % s,
+                            hint=_("use --subrepos for recursive commit"))
+                    subs.append(s)
+                    commitsubs.add(s)
+                else:
+                    bs = wctx.sub(s).basestate()
+                    newstate[s] = (newstate[s][0], bs, newstate[s][2])
+                    if oldstate.get(s, (None, None, None))[1] != bs:
+                        subs.append(s)
+
+            # check for removed subrepos
+            for p in wctx.parents():
+                r = [s for s in p.substate if s not in newstate]
+                subs += [s for s in r if match(s)]
+            if subs:
+                if (not match('.hgsub') and
+                    '.hgsub' in (wctx.modified() + wctx.added())):
+                    raise util.Abort(
+                        _("can't commit subrepos without .hgsub"))
+                changes[0].insert(0, '.hgsubstate')
+
+        elif '.hgsub' in changes[2]:
+            # clean up .hgsubstate when .hgsub is removed
+            if ('.hgsubstate' in wctx and
+                '.hgsubstate' not in changes[0] + changes[1] + changes[2]):
+                changes[2].insert(0, '.hgsubstate')
+
+        # make sure all explicit patterns are matched
+        if not force and match.files():
+            matched = set(changes[0] + changes[1] + changes[2])
+
+            for f in match.files():
+                f = repo.dirstate.normalize(f)
+                if f == '.' or f in matched or f in wctx.substate:
+                    continue
+                if f in changes[3]: # missing
+                    fail(f, _('file not found!'))
+                if f in vdirs: # visited directory
+                    d = f + '/'
+                    for mf in matched:
+                        if mf.startswith(d):
+                            break
+                    else:
+                        fail(f, _("no match under directory!"))
+                elif f not in repo.dirstate:
+                    fail(f, _("file not tracked!"))
+
+        if (not force and not extra.get("close") and not merge
+            and not (changes[0] or changes[1] or changes[2])
+            and wctx.branch() == wctx.p1().branch()):
+            return None
+
+        if merge and changes[3]:
+            raise util.Abort(_("cannot commit merge with missing files"))
+
+        ms = mergemod.mergestate(repo)
+        for f in changes[0]:
+            if f in ms and ms[f] == 'u':
+                raise util.Abort(_("unresolved merge conflicts "
+                                   "(see hg help resolve)"))
+
+        # commit subs and write new state
+        if subs:
+            for s in sorted(commitsubs):
+                sub = wctx.sub(s)
+                repo.ui.status(_('committing subrepository %s\n') %
+                    subrepo.subrelpath(sub))
+                sr = sub.commit(opts['message'], opts.get('user'), opts.get('date'))
+                newstate[s] = (newstate[s][0], sr)
+            subrepo.writestate(repo, newstate)
 
         p1, p2 = repo.dirstate.parents()
         hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
         repo.hook("precommit", throw=True, parent1=hookp1, parent2=hookp2)
 
-        extra = {'branch': encoding.fromlocal(repo[None].branch())}
-        if opts.get('close_branch'):
-            if p1 not in repo.branchheads():
-                # The topo heads set is included in the branch heads set of the
-                # current branch, so it's sufficient to test branchheads
-                raise error.Abort(_('can only close branch heads'))
-            extra['close'] = 1
+        newrev = None
+        patchfile = opts['partials']
+        fp = open(patchfile, 'rb')
+        store = patch.filestore()
+        try:
+            # patch files in tmp directory
+            try:
+                patch.patchrepo(ui, repo, repo['.'], store, fp, 1, None)
+            except patch.PatchError, e:
+                raise util.Abort(str(e))
 
-        # patch files in tmp directory
-        try:
-            patch.patchrepo(ui, repo, repo['.'], store, fp, 1, None)
-        except patch.PatchError, e:
-            raise error.Abort(str(e))
+            # create memctx, use to create a new changeset
+            matched = changes[0] + changes[1] + changes[2]
+            memctx = makememctx(repo, (p1, p2), opts['message'],
+                                opts.get('user'), opts.get('date'), extra,
+                                matched, store)
+            newrev = memctx.commit()
+        finally:
+            store.close()
+            fp.close()
+            os.unlink(patchfile)
 
-        # create new revision from memory
-        memctx = makememctx(repo, (p1, p2), opts['message'],
-                            opts.get('user'), opts.get('date'), extra,
-                            files, store)
-
-        newrev = memctx.commit()
-    finally:
-        store.close()
-        fp.close()
-        os.unlink(patchfile)
-
-    # move working directory to new revision
-    if newrev:
-        wlock = repo.wlock()
-        try:
+        # move working directory to new revision
+        if newrev:
             bookmarks.update(repo, [p1, p2], newrev)
             repo.setparents(newrev)
             ctx = repo[newrev]
             repo.dirstate.rebuild(ctx.node(), ctx.manifest())
             ms.reset()
-        finally:
-            wlock.release()
+    finally:
+        wlock.release()
 
     def commithook(node=hex(newrev), parent1=hookp1, parent2=hookp2):
         repo.hook("commit", node=node, parent1=parent1, parent2=parent2)