##// END OF EJS Templates
merge: with stable
Augie Fackler -
r49840:533820f5 merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,315 +1,324 b''
1 # uncommit - undo the actions of a commit
1 # uncommit - undo the actions of a commit
2 #
2 #
3 # Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
3 # Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
5 # Pierre-Yves David <pierre-yves.david@ens-lyon.org>
5 # Pierre-Yves David <pierre-yves.david@ens-lyon.org>
6 # Patrick Mezard <patrick@mezard.eu>
6 # Patrick Mezard <patrick@mezard.eu>
7 # Copyright 2016 Facebook, Inc.
7 # Copyright 2016 Facebook, Inc.
8 #
8 #
9 # This software may be used and distributed according to the terms of the
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.
10 # GNU General Public License version 2 or any later version.
11
11
12 """uncommit part or all of a local changeset (EXPERIMENTAL)
12 """uncommit part or all of a local changeset (EXPERIMENTAL)
13
13
14 This command undoes the effect of a local commit, returning the affected
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
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,
16 removed in the changeset will be left unchanged, and so will remain modified,
17 added and removed in the working directory.
17 added and removed in the working directory.
18 """
18 """
19
19
20
20
21 from mercurial.i18n import _
21 from mercurial.i18n import _
22
22
23 from mercurial import (
23 from mercurial import (
24 cmdutil,
24 cmdutil,
25 commands,
25 commands,
26 context,
26 context,
27 copies as copiesmod,
27 copies as copiesmod,
28 error,
28 error,
29 obsutil,
29 obsutil,
30 pathutil,
30 pathutil,
31 pycompat,
31 pycompat,
32 registrar,
32 registrar,
33 rewriteutil,
33 rewriteutil,
34 scmutil,
34 scmutil,
35 )
35 )
36
36
37 cmdtable = {}
37 cmdtable = {}
38 command = registrar.command(cmdtable)
38 command = registrar.command(cmdtable)
39
39
40 configtable = {}
40 configtable = {}
41 configitem = registrar.configitem(configtable)
41 configitem = registrar.configitem(configtable)
42
42
43 configitem(
43 configitem(
44 b'experimental',
44 b'experimental',
45 b'uncommitondirtywdir',
45 b'uncommitondirtywdir',
46 default=False,
46 default=False,
47 )
47 )
48 configitem(
48 configitem(
49 b'experimental',
49 b'experimental',
50 b'uncommit.keep',
50 b'uncommit.keep',
51 default=False,
51 default=False,
52 )
52 )
53
53
54 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
54 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
55 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
55 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
56 # be specifying the version(s) of Mercurial they are tested with, or
56 # be specifying the version(s) of Mercurial they are tested with, or
57 # leave the attribute unspecified.
57 # leave the attribute unspecified.
58 testedwith = b'ships-with-hg-core'
58 testedwith = b'ships-with-hg-core'
59
59
60
60
61 def _commitfiltered(
61 def _commitfiltered(
62 repo, ctx, match, keepcommit, message=None, user=None, date=None
62 repo, ctx, match, keepcommit, message=None, user=None, date=None
63 ):
63 ):
64 """Recommit ctx with changed files not in match. Return the new
64 """Recommit ctx with changed files not in match. Return the new
65 node identifier, or None if nothing changed.
65 node identifier, or None if nothing changed.
66 """
66 """
67 base = ctx.p1()
67 base = ctx.p1()
68 # ctx
68 # ctx
69 initialfiles = set(ctx.files())
69 initialfiles = set(ctx.files())
70 exclude = {f for f in initialfiles if match(f)}
70 exclude = {f for f in initialfiles if match(f)}
71
71
72 # No files matched commit, so nothing excluded
72 # No files matched commit, so nothing excluded
73 if not exclude:
73 if not exclude:
74 return None
74 return None
75
75
76 # return the p1 so that we don't create an obsmarker later
76 # return the p1 so that we don't create an obsmarker later
77 if not keepcommit:
77 if not keepcommit:
78 return ctx.p1().node()
78 return ctx.p1().node()
79
79
80 files = initialfiles - exclude
80 files = initialfiles - exclude
81 # Filter copies
81 # Filter copies
82 copied = copiesmod.pathcopies(base, ctx)
82 copied = copiesmod.pathcopies(base, ctx)
83 copied = {dst: src for dst, src in copied.items() if dst in files}
83 copied = {dst: src for dst, src in copied.items() if dst in files}
84
84
85 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
85 def filectxfn(repo, memctx, path, contentctx=ctx, redirect=()):
86 if path not in contentctx:
86 if path not in contentctx:
87 return None
87 return None
88 fctx = contentctx[path]
88 fctx = contentctx[path]
89 mctx = context.memfilectx(
89 mctx = context.memfilectx(
90 repo,
90 repo,
91 memctx,
91 memctx,
92 fctx.path(),
92 fctx.path(),
93 fctx.data(),
93 fctx.data(),
94 fctx.islink(),
94 fctx.islink(),
95 fctx.isexec(),
95 fctx.isexec(),
96 copysource=copied.get(path),
96 copysource=copied.get(path),
97 )
97 )
98 return mctx
98 return mctx
99
99
100 if not files:
100 if not files:
101 repo.ui.status(_(b"note: keeping empty commit\n"))
101 repo.ui.status(_(b"note: keeping empty commit\n"))
102
102
103 if message is None:
103 if message is None:
104 message = ctx.description()
104 message = ctx.description()
105 if not user:
105 if not user:
106 user = ctx.user()
106 user = ctx.user()
107 if not date:
107 if not date:
108 date = ctx.date()
108 date = ctx.date()
109
109
110 new = context.memctx(
110 new = context.memctx(
111 repo,
111 repo,
112 parents=[base.node(), repo.nullid],
112 parents=[base.node(), repo.nullid],
113 text=message,
113 text=message,
114 files=files,
114 files=files,
115 filectxfn=filectxfn,
115 filectxfn=filectxfn,
116 user=user,
116 user=user,
117 date=date,
117 date=date,
118 extra=ctx.extra(),
118 extra=ctx.extra(),
119 )
119 )
120 return repo.commitctx(new)
120 return repo.commitctx(new)
121
121
122
122
123 @command(
123 @command(
124 b'uncommit',
124 b'uncommit',
125 [
125 [
126 (b'', b'keep', None, _(b'allow an empty commit after uncommitting')),
126 (b'', b'keep', None, _(b'allow an empty commit after uncommitting')),
127 (
127 (
128 b'',
128 b'',
129 b'allow-dirty-working-copy',
129 b'allow-dirty-working-copy',
130 False,
130 False,
131 _(b'allow uncommit with outstanding changes'),
131 _(b'allow uncommit with outstanding changes'),
132 ),
132 ),
133 (b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')),
133 (b'n', b'note', b'', _(b'store a note on uncommit'), _(b'TEXT')),
134 ]
134 ]
135 + commands.walkopts
135 + commands.walkopts
136 + commands.commitopts
136 + commands.commitopts
137 + commands.commitopts2
137 + commands.commitopts2
138 + commands.commitopts3,
138 + commands.commitopts3,
139 _(b'[OPTION]... [FILE]...'),
139 _(b'[OPTION]... [FILE]...'),
140 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
140 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
141 )
141 )
142 def uncommit(ui, repo, *pats, **opts):
142 def uncommit(ui, repo, *pats, **opts):
143 """uncommit part or all of a local changeset
143 """uncommit part or all of a local changeset
144
144
145 This command undoes the effect of a local commit, returning the affected
145 This command undoes the effect of a local commit, returning the affected
146 files to their uncommitted state. This means that files modified or
146 files to their uncommitted state. This means that files modified or
147 deleted in the changeset will be left unchanged, and so will remain
147 deleted in the changeset will be left unchanged, and so will remain
148 modified in the working directory.
148 modified in the working directory.
149
149
150 If no files are specified, the commit will be pruned, unless --keep is
150 If no files are specified, the commit will be pruned, unless --keep is
151 given.
151 given.
152 """
152 """
153 cmdutil.check_note_size(opts)
153 cmdutil.check_note_size(opts)
154 cmdutil.resolve_commit_options(ui, opts)
154 cmdutil.resolve_commit_options(ui, opts)
155 opts = pycompat.byteskwargs(opts)
155 opts = pycompat.byteskwargs(opts)
156
156
157 with repo.wlock(), repo.lock():
157 with repo.wlock(), repo.lock():
158
158
159 st = repo.status()
159 st = repo.status()
160 m, a, r, d = st.modified, st.added, st.removed, st.deleted
160 m, a, r, d = st.modified, st.added, st.removed, st.deleted
161 isdirtypath = any(set(m + a + r + d) & set(pats))
161 isdirtypath = any(set(m + a + r + d) & set(pats))
162 allowdirtywcopy = opts[
162 allowdirtywcopy = opts[
163 b'allow_dirty_working_copy'
163 b'allow_dirty_working_copy'
164 ] or repo.ui.configbool(b'experimental', b'uncommitondirtywdir')
164 ] or repo.ui.configbool(b'experimental', b'uncommitondirtywdir')
165 if not allowdirtywcopy and (not pats or isdirtypath):
165 if not allowdirtywcopy and (not pats or isdirtypath):
166 cmdutil.bailifchanged(
166 cmdutil.bailifchanged(
167 repo,
167 repo,
168 hint=_(b'requires --allow-dirty-working-copy to uncommit'),
168 hint=_(b'requires --allow-dirty-working-copy to uncommit'),
169 )
169 )
170 old = repo[b'.']
170 old = repo[b'.']
171 rewriteutil.precheck(repo, [old.rev()], b'uncommit')
171 rewriteutil.precheck(repo, [old.rev()], b'uncommit')
172 if len(old.parents()) > 1:
172 if len(old.parents()) > 1:
173 raise error.InputError(_(b"cannot uncommit merge changeset"))
173 raise error.InputError(_(b"cannot uncommit merge changeset"))
174
174
175 match = scmutil.match(old, pats, opts)
175 match = scmutil.match(old, pats, opts)
176
176
177 # Check all explicitly given files; abort if there's a problem.
177 # Check all explicitly given files; abort if there's a problem.
178 if match.files():
178 if match.files():
179 s = old.status(old.p1(), match, listclean=True)
179 s = old.status(old.p1(), match, listclean=True)
180 eligible = set(s.added) | set(s.modified) | set(s.removed)
180 eligible = set(s.added) | set(s.modified) | set(s.removed)
181
181
182 badfiles = set(match.files()) - eligible
182 badfiles = set(match.files()) - eligible
183
183
184 # Naming a parent directory of an eligible file is OK, even
184 # Naming a parent directory of an eligible file is OK, even
185 # if not everything tracked in that directory can be
185 # if not everything tracked in that directory can be
186 # uncommitted.
186 # uncommitted.
187 if badfiles:
187 if badfiles:
188 badfiles -= {f for f in pathutil.dirs(eligible)}
188 badfiles -= {f for f in pathutil.dirs(eligible)}
189
189
190 for f in sorted(badfiles):
190 for f in sorted(badfiles):
191 if f in s.clean:
191 if f in s.clean:
192 hint = _(
192 hint = _(
193 b"file was not changed in working directory parent"
193 b"file was not changed in working directory parent"
194 )
194 )
195 elif repo.wvfs.exists(f):
195 elif repo.wvfs.exists(f):
196 hint = _(b"file was untracked in working directory parent")
196 hint = _(b"file was untracked in working directory parent")
197 else:
197 else:
198 hint = _(b"file does not exist")
198 hint = _(b"file does not exist")
199
199
200 raise error.InputError(
200 raise error.InputError(
201 _(b'cannot uncommit "%s"') % scmutil.getuipathfn(repo)(f),
201 _(b'cannot uncommit "%s"') % scmutil.getuipathfn(repo)(f),
202 hint=hint,
202 hint=hint,
203 )
203 )
204
204
205 with repo.transaction(b'uncommit'):
205 with repo.transaction(b'uncommit'):
206 if not (opts[b'message'] or opts[b'logfile']):
206 if not (opts[b'message'] or opts[b'logfile']):
207 opts[b'message'] = old.description()
207 opts[b'message'] = old.description()
208 message = cmdutil.logmessage(ui, opts)
208 message = cmdutil.logmessage(ui, opts)
209
209
210 keepcommit = pats
210 keepcommit = pats
211 if not keepcommit:
211 if not keepcommit:
212 if opts.get(b'keep') is not None:
212 if opts.get(b'keep') is not None:
213 keepcommit = opts.get(b'keep')
213 keepcommit = opts.get(b'keep')
214 else:
214 else:
215 keepcommit = ui.configbool(
215 keepcommit = ui.configbool(
216 b'experimental', b'uncommit.keep'
216 b'experimental', b'uncommit.keep'
217 )
217 )
218 newid = _commitfiltered(
218 newid = _commitfiltered(
219 repo,
219 repo,
220 old,
220 old,
221 match,
221 match,
222 keepcommit,
222 keepcommit,
223 message=message,
223 message=message,
224 user=opts.get(b'user'),
224 user=opts.get(b'user'),
225 date=opts.get(b'date'),
225 date=opts.get(b'date'),
226 )
226 )
227 if newid is None:
227 if newid is None:
228 ui.status(_(b"nothing to uncommit\n"))
228 ui.status(_(b"nothing to uncommit\n"))
229 return 1
229 return 1
230
230
231 mapping = {}
231 mapping = {}
232 if newid != old.p1().node():
232 if newid != old.p1().node():
233 # Move local changes on filtered changeset
233 # Move local changes on filtered changeset
234 mapping[old.node()] = (newid,)
234 mapping[old.node()] = (newid,)
235 else:
235 else:
236 # Fully removed the old commit
236 # Fully removed the old commit
237 mapping[old.node()] = ()
237 mapping[old.node()] = ()
238
238
239 with repo.dirstate.parentchange():
239 with repo.dirstate.parentchange():
240 scmutil.movedirstate(repo, repo[newid], match)
240 scmutil.movedirstate(repo, repo[newid], match)
241
241
242 scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True)
242 scmutil.cleanupnodes(repo, mapping, b'uncommit', fixphase=True)
243
243
244
244
245 def predecessormarkers(ctx):
245 def predecessormarkers(ctx):
246 """yields the obsolete markers marking the given changeset as a successor"""
246 """yields the obsolete markers marking the given changeset as a successor"""
247 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
247 for data in ctx.repo().obsstore.predecessors.get(ctx.node(), ()):
248 yield obsutil.marker(ctx.repo(), data)
248 yield obsutil.marker(ctx.repo(), data)
249
249
250
250
251 @command(
251 @command(
252 b'unamend',
252 b'unamend',
253 [],
253 [],
254 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
254 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
255 helpbasic=True,
255 helpbasic=True,
256 )
256 )
257 def unamend(ui, repo, **opts):
257 def unamend(ui, repo, **opts):
258 """undo the most recent amend operation on a current changeset
258 """undo the most recent amend operation on a current changeset
259
259
260 This command will roll back to the previous version of a changeset,
260 This command will roll back to the previous version of a changeset,
261 leaving working directory in state in which it was before running
261 leaving working directory in state in which it was before running
262 `hg amend` (e.g. files modified as part of an amend will be
262 `hg amend` (e.g. files modified as part of an amend will be
263 marked as modified `hg status`)
263 marked as modified `hg status`)
264 """
264 """
265
265
266 unfi = repo.unfiltered()
266 unfi = repo.unfiltered()
267 with repo.wlock(), repo.lock(), repo.transaction(b'unamend'):
267 with repo.wlock(), repo.lock(), repo.transaction(b'unamend'):
268
268
269 # identify the commit from which to unamend
269 # identify the commit from which to unamend
270 curctx = repo[b'.']
270 curctx = repo[b'.']
271
271
272 rewriteutil.precheck(repo, [curctx.rev()], b'unamend')
272 rewriteutil.precheck(repo, [curctx.rev()], b'unamend')
273 if len(curctx.parents()) > 1:
273 if len(curctx.parents()) > 1:
274 raise error.InputError(_(b"cannot unamend merge changeset"))
274 raise error.InputError(_(b"cannot unamend merge changeset"))
275
275
276 expected_keys = (b'amend_source', b'unamend_source')
277 if not any(key in curctx.extra() for key in expected_keys):
278 raise error.InputError(
279 _(
280 b"working copy parent was not created by 'hg amend' or "
281 b"'hg unamend'"
282 )
283 )
284
276 # identify the commit to which to unamend
285 # identify the commit to which to unamend
277 markers = list(predecessormarkers(curctx))
286 markers = list(predecessormarkers(curctx))
278 if len(markers) != 1:
287 if len(markers) != 1:
279 e = _(b"changeset must have one predecessor, found %i predecessors")
288 e = _(b"changeset must have one predecessor, found %i predecessors")
280 raise error.InputError(e % len(markers))
289 raise error.InputError(e % len(markers))
281
290
282 prednode = markers[0].prednode()
291 prednode = markers[0].prednode()
283 predctx = unfi[prednode]
292 predctx = unfi[prednode]
284
293
285 # add an extra so that we get a new hash
294 # add an extra so that we get a new hash
286 # note: allowing unamend to undo an unamend is an intentional feature
295 # note: allowing unamend to undo an unamend is an intentional feature
287 extras = predctx.extra()
296 extras = predctx.extra()
288 extras[b'unamend_source'] = curctx.hex()
297 extras[b'unamend_source'] = curctx.hex()
289
298
290 def filectxfn(repo, ctx_, path):
299 def filectxfn(repo, ctx_, path):
291 try:
300 try:
292 return predctx.filectx(path)
301 return predctx.filectx(path)
293 except KeyError:
302 except KeyError:
294 return None
303 return None
295
304
296 # Make a new commit same as predctx
305 # Make a new commit same as predctx
297 newctx = context.memctx(
306 newctx = context.memctx(
298 repo,
307 repo,
299 parents=(predctx.p1(), predctx.p2()),
308 parents=(predctx.p1(), predctx.p2()),
300 text=predctx.description(),
309 text=predctx.description(),
301 files=predctx.files(),
310 files=predctx.files(),
302 filectxfn=filectxfn,
311 filectxfn=filectxfn,
303 user=predctx.user(),
312 user=predctx.user(),
304 date=predctx.date(),
313 date=predctx.date(),
305 extra=extras,
314 extra=extras,
306 )
315 )
307 newprednode = repo.commitctx(newctx)
316 newprednode = repo.commitctx(newctx)
308 newpredctx = repo[newprednode]
317 newpredctx = repo[newprednode]
309 dirstate = repo.dirstate
318 dirstate = repo.dirstate
310
319
311 with dirstate.parentchange():
320 with dirstate.parentchange():
312 scmutil.movedirstate(repo, newpredctx)
321 scmutil.movedirstate(repo, newpredctx)
313
322
314 mapping = {curctx.node(): (newprednode,)}
323 mapping = {curctx.node(): (newprednode,)}
315 scmutil.cleanupnodes(repo, mapping, b'unamend', fixphase=True)
324 scmutil.cleanupnodes(repo, mapping, b'unamend', fixphase=True)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now