Mercurial > public > mercurial-scm > hg
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) |