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