comparison hgext/uncommit.py @ 34192:da2f5f19312c

uncommit: move fb-extension to core which uncommits a changeset uncommit extension in fb-hgext adds a uncommit command which by default uncommits a changeset and move all the changes to the working directory. If file names are passed, uncommit moves the changes from those files to the working directory and left the changeset with remaining committed files. The uncommit extension in fb-hgext does not creates an empty commit like the one in evolve extension unless user has specified ui.alllowemptycommit to True. The test file added is a combination of tests from test-uncommit.t, test-uncommit-merge.t and test-uncommit-bookmark.t from fb-hgext. .. feature:: A new uncommit extension which provides `hg uncommit` using which one can uncommit part or all of the changeset. This command undoes the effect of a local commit, returning the affected files to their uncommitted state. Differential Revision: https://phab.mercurial-scm.org/D529
author Pulkit Goyal <7895pulkit@gmail.com>
date Thu, 24 Aug 2017 22:55:56 +0530
parents
children f94442d46984
comparison
equal deleted inserted replaced
34191:e6b5e7329ff2 34192:da2f5f19312c
1 # uncommit - undo the actions of a commit
2 #
3 # Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
4 # Logilab SA <contact@logilab.fr>
5 # Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 # Patrick Mezard <patrick@mezard.eu>
7 # Copyright 2016 Facebook, Inc.
8 #
9 # This software may be used and distributed according to the terms of the
10 # GNU General Public License version 2 or any later version.
11
12 """uncommit part or all of a local changeset (EXPERIMENTAL)
13
14 This command undoes the effect of a local commit, returning the affected
15 files to their uncommitted state. This means that files modified, added or
16 removed in the changeset will be left unchanged, and so will remain modified,
17 added and removed in the working directory.
18 """
19
20 from __future__ import absolute_import
21
22 from mercurial.i18n import _
23
24 from mercurial import (
25 commands,
26 context,
27 copies,
28 error,
29 node,
30 obsolete,
31 registrar,
32 scmutil,
33 )
34
35 cmdtable = {}
36 command = registrar.command(cmdtable)
37
38 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
39 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
40 # be specifying the version(s) of Mercurial they are tested with, or
41 # leave the attribute unspecified.
42 testedwith = 'ships-with-hg-core'
43
44 def _commitfiltered(repo, ctx, match, allowempty):
45 """Recommit ctx with changed files not in match. Return the new
46 node identifier, or None if nothing changed.
47 """
48 base = ctx.p1()
49 # ctx
50 initialfiles = set(ctx.files())
51 exclude = set(f for f in initialfiles if match(f))
52
53 # No files matched commit, so nothing excluded
54 if not exclude:
55 return None
56
57 files = (initialfiles - exclude)
58 # return the p1 so that we don't create an obsmarker later
59 if not files and not allowempty:
60 return ctx.parents()[0].node()
61
62 # Filter copies
63 copied = copies.pathcopies(base, ctx)
64 copied = dict((dst, src) for dst, src in copied.iteritems()
65 if dst in files)
66 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
67 if path not in contentctx:
68 return None
69 fctx = contentctx[path]
70 mctx = context.memfilectx(repo, fctx.path(), fctx.data(),
71 fctx.islink(),
72 fctx.isexec(),
73 copied=copied.get(path))
74 return mctx
75
76 new = context.memctx(repo,
77 parents=[base.node(), node.nullid],
78 text=ctx.description(),
79 files=files,
80 filectxfn=filectxfn,
81 user=ctx.user(),
82 date=ctx.date(),
83 extra=ctx.extra())
84 # phase handling
85 commitphase = ctx.phase()
86 overrides = {('phases', 'new-commit'): commitphase}
87 with repo.ui.configoverride(overrides, 'uncommit'):
88 newid = repo.commitctx(new)
89 return newid
90
91 def _uncommitdirstate(repo, oldctx, match):
92 """Fix the dirstate after switching the working directory from
93 oldctx to a copy of oldctx not containing changed files matched by
94 match.
95 """
96 ctx = repo['.']
97 ds = repo.dirstate
98 copies = dict(ds.copies())
99 s = repo.status(oldctx.p1(), oldctx, match=match)
100 for f in s.modified:
101 if ds[f] == 'r':
102 # modified + removed -> removed
103 continue
104 ds.normallookup(f)
105
106 for f in s.added:
107 if ds[f] == 'r':
108 # added + removed -> unknown
109 ds.drop(f)
110 elif ds[f] != 'a':
111 ds.add(f)
112
113 for f in s.removed:
114 if ds[f] == 'a':
115 # removed + added -> normal
116 ds.normallookup(f)
117 elif ds[f] != 'r':
118 ds.remove(f)
119
120 # Merge old parent and old working dir copies
121 oldcopies = {}
122 for f in (s.modified + s.added):
123 src = oldctx[f].renamed()
124 if src:
125 oldcopies[f] = src[0]
126 oldcopies.update(copies)
127 copies = dict((dst, oldcopies.get(src, src))
128 for dst, src in oldcopies.iteritems())
129 # Adjust the dirstate copies
130 for dst, src in copies.iteritems():
131 if (src not in ctx or dst in ctx or ds[dst] != 'a'):
132 src = None
133 ds.copy(src, dst)
134
135 @command('uncommit',
136 [('', 'empty', False, _('allow an empty commit after uncommiting')),
137 ] + commands.walkopts,
138 _('[OPTION]... [FILE]...'))
139 def uncommit(ui, repo, *pats, **opts):
140 """uncommit part or all of a local changeset
141
142 This command undoes the effect of a local commit, returning the affected
143 files to their uncommitted state. This means that files modified or
144 deleted in the changeset will be left unchanged, and so will remain
145 modified in the working directory.
146 """
147
148 with repo.wlock(), repo.lock():
149 wctx = repo[None]
150
151 if wctx.parents()[0].node() == node.nullid:
152 raise error.Abort(_("cannot uncommit null changeset"))
153 if len(wctx.parents()) > 1:
154 raise error.Abort(_("cannot uncommit while merging"))
155 old = repo['.']
156 if not old.mutable():
157 raise error.Abort(_('cannot uncommit public changesets'))
158 if len(old.parents()) > 1:
159 raise error.Abort(_("cannot uncommit merge changeset"))
160 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt)
161 if not allowunstable and old.children():
162 raise error.Abort(_('cannot uncommit changeset with children'))
163
164 with repo.transaction('uncommit'):
165 match = scmutil.match(old, pats, opts)
166 newid = _commitfiltered(repo, old, match, opts.get('empty'))
167 if newid is None:
168 ui.status(_("nothing to uncommit\n"))
169 return 1
170
171 mapping = {}
172 if newid != old.p1().node():
173 # Move local changes on filtered changeset
174 mapping[old.node()] = (newid,)
175 else:
176 # Fully removed the old commit
177 mapping[old.node()] = ()
178
179 scmutil.cleanupnodes(repo, mapping, 'uncommit')
180
181 with repo.dirstate.parentchange():
182 repo.dirstate.setparents(newid, node.nullid)
183 _uncommitdirstate(repo, old, match)