##// END OF EJS Templates
shelve: refactor option combination check to easily add new ones...
FUJIWARA Katsunori -
r21851:aad28ff8 default
parent child Browse files
Show More
@@ -1,704 +1,715 b''
1 # shelve.py - save/restore working directory state
1 # shelve.py - save/restore working directory state
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """save and restore changes to the working directory
8 """save and restore changes to the working directory
9
9
10 The "hg shelve" command saves changes made to the working directory
10 The "hg shelve" command saves changes made to the working directory
11 and reverts those changes, resetting the working directory to a clean
11 and reverts those changes, resetting the working directory to a clean
12 state.
12 state.
13
13
14 Later on, the "hg unshelve" command restores the changes saved by "hg
14 Later on, the "hg unshelve" command restores the changes saved by "hg
15 shelve". Changes can be restored even after updating to a different
15 shelve". Changes can be restored even after updating to a different
16 parent, in which case Mercurial's merge machinery will resolve any
16 parent, in which case Mercurial's merge machinery will resolve any
17 conflicts if necessary.
17 conflicts if necessary.
18
18
19 You can have more than one shelved change outstanding at a time; each
19 You can have more than one shelved change outstanding at a time; each
20 shelved change has a distinct name. For details, see the help for "hg
20 shelved change has a distinct name. For details, see the help for "hg
21 shelve".
21 shelve".
22 """
22 """
23
23
24 from mercurial.i18n import _
24 from mercurial.i18n import _
25 from mercurial.node import nullid, nullrev, bin, hex
25 from mercurial.node import nullid, nullrev, bin, hex
26 from mercurial import changegroup, cmdutil, scmutil, phases, commands
26 from mercurial import changegroup, cmdutil, scmutil, phases, commands
27 from mercurial import error, hg, mdiff, merge, patch, repair, util
27 from mercurial import error, hg, mdiff, merge, patch, repair, util
28 from mercurial import templatefilters, changegroup, exchange
28 from mercurial import templatefilters, changegroup, exchange
29 from mercurial import lock as lockmod
29 from mercurial import lock as lockmod
30 from hgext import rebase
30 from hgext import rebase
31 import errno
31 import errno
32
32
33 cmdtable = {}
33 cmdtable = {}
34 command = cmdutil.command(cmdtable)
34 command = cmdutil.command(cmdtable)
35 testedwith = 'internal'
35 testedwith = 'internal'
36
36
37 class shelvedfile(object):
37 class shelvedfile(object):
38 """Helper for the file storing a single shelve
38 """Helper for the file storing a single shelve
39
39
40 Handles common functions on shelve files (.hg/.files/.patch) using
40 Handles common functions on shelve files (.hg/.files/.patch) using
41 the vfs layer"""
41 the vfs layer"""
42 def __init__(self, repo, name, filetype=None):
42 def __init__(self, repo, name, filetype=None):
43 self.repo = repo
43 self.repo = repo
44 self.name = name
44 self.name = name
45 self.vfs = scmutil.vfs(repo.join('shelved'))
45 self.vfs = scmutil.vfs(repo.join('shelved'))
46 if filetype:
46 if filetype:
47 self.fname = name + '.' + filetype
47 self.fname = name + '.' + filetype
48 else:
48 else:
49 self.fname = name
49 self.fname = name
50
50
51 def exists(self):
51 def exists(self):
52 return self.vfs.exists(self.fname)
52 return self.vfs.exists(self.fname)
53
53
54 def filename(self):
54 def filename(self):
55 return self.vfs.join(self.fname)
55 return self.vfs.join(self.fname)
56
56
57 def unlink(self):
57 def unlink(self):
58 util.unlink(self.filename())
58 util.unlink(self.filename())
59
59
60 def stat(self):
60 def stat(self):
61 return self.vfs.stat(self.fname)
61 return self.vfs.stat(self.fname)
62
62
63 def opener(self, mode='rb'):
63 def opener(self, mode='rb'):
64 try:
64 try:
65 return self.vfs(self.fname, mode)
65 return self.vfs(self.fname, mode)
66 except IOError, err:
66 except IOError, err:
67 if err.errno != errno.ENOENT:
67 if err.errno != errno.ENOENT:
68 raise
68 raise
69 raise util.Abort(_("shelved change '%s' not found") % self.name)
69 raise util.Abort(_("shelved change '%s' not found") % self.name)
70
70
71 def applybundle(self):
71 def applybundle(self):
72 fp = self.opener()
72 fp = self.opener()
73 try:
73 try:
74 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
74 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
75 changegroup.addchangegroup(self.repo, gen, 'unshelve',
75 changegroup.addchangegroup(self.repo, gen, 'unshelve',
76 'bundle:' + self.vfs.join(self.fname))
76 'bundle:' + self.vfs.join(self.fname))
77 finally:
77 finally:
78 fp.close()
78 fp.close()
79
79
80 def writebundle(self, cg):
80 def writebundle(self, cg):
81 changegroup.writebundle(cg, self.fname, 'HG10UN', self.vfs)
81 changegroup.writebundle(cg, self.fname, 'HG10UN', self.vfs)
82
82
83 class shelvedstate(object):
83 class shelvedstate(object):
84 """Handle persistence during unshelving operations.
84 """Handle persistence during unshelving operations.
85
85
86 Handles saving and restoring a shelved state. Ensures that different
86 Handles saving and restoring a shelved state. Ensures that different
87 versions of a shelved state are possible and handles them appropriately.
87 versions of a shelved state are possible and handles them appropriately.
88 """
88 """
89 _version = 1
89 _version = 1
90 _filename = 'shelvedstate'
90 _filename = 'shelvedstate'
91
91
92 @classmethod
92 @classmethod
93 def load(cls, repo):
93 def load(cls, repo):
94 fp = repo.opener(cls._filename)
94 fp = repo.opener(cls._filename)
95 try:
95 try:
96 version = int(fp.readline().strip())
96 version = int(fp.readline().strip())
97
97
98 if version != cls._version:
98 if version != cls._version:
99 raise util.Abort(_('this version of shelve is incompatible '
99 raise util.Abort(_('this version of shelve is incompatible '
100 'with the version used in this repo'))
100 'with the version used in this repo'))
101 name = fp.readline().strip()
101 name = fp.readline().strip()
102 wctx = fp.readline().strip()
102 wctx = fp.readline().strip()
103 pendingctx = fp.readline().strip()
103 pendingctx = fp.readline().strip()
104 parents = [bin(h) for h in fp.readline().split()]
104 parents = [bin(h) for h in fp.readline().split()]
105 stripnodes = [bin(h) for h in fp.readline().split()]
105 stripnodes = [bin(h) for h in fp.readline().split()]
106 finally:
106 finally:
107 fp.close()
107 fp.close()
108
108
109 obj = cls()
109 obj = cls()
110 obj.name = name
110 obj.name = name
111 obj.wctx = repo[bin(wctx)]
111 obj.wctx = repo[bin(wctx)]
112 obj.pendingctx = repo[bin(pendingctx)]
112 obj.pendingctx = repo[bin(pendingctx)]
113 obj.parents = parents
113 obj.parents = parents
114 obj.stripnodes = stripnodes
114 obj.stripnodes = stripnodes
115
115
116 return obj
116 return obj
117
117
118 @classmethod
118 @classmethod
119 def save(cls, repo, name, originalwctx, pendingctx, stripnodes):
119 def save(cls, repo, name, originalwctx, pendingctx, stripnodes):
120 fp = repo.opener(cls._filename, 'wb')
120 fp = repo.opener(cls._filename, 'wb')
121 fp.write('%i\n' % cls._version)
121 fp.write('%i\n' % cls._version)
122 fp.write('%s\n' % name)
122 fp.write('%s\n' % name)
123 fp.write('%s\n' % hex(originalwctx.node()))
123 fp.write('%s\n' % hex(originalwctx.node()))
124 fp.write('%s\n' % hex(pendingctx.node()))
124 fp.write('%s\n' % hex(pendingctx.node()))
125 fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
125 fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
126 fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
126 fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
127 fp.close()
127 fp.close()
128
128
129 @classmethod
129 @classmethod
130 def clear(cls, repo):
130 def clear(cls, repo):
131 util.unlinkpath(repo.join(cls._filename), ignoremissing=True)
131 util.unlinkpath(repo.join(cls._filename), ignoremissing=True)
132
132
133 def createcmd(ui, repo, pats, opts):
133 def createcmd(ui, repo, pats, opts):
134 """subcommand that creates a new shelve"""
134 """subcommand that creates a new shelve"""
135
135
136 def publicancestors(ctx):
136 def publicancestors(ctx):
137 """Compute the public ancestors of a commit.
137 """Compute the public ancestors of a commit.
138
138
139 Much faster than the revset ancestors(ctx) & draft()"""
139 Much faster than the revset ancestors(ctx) & draft()"""
140 seen = set([nullrev])
140 seen = set([nullrev])
141 visit = util.deque()
141 visit = util.deque()
142 visit.append(ctx)
142 visit.append(ctx)
143 while visit:
143 while visit:
144 ctx = visit.popleft()
144 ctx = visit.popleft()
145 yield ctx.node()
145 yield ctx.node()
146 for parent in ctx.parents():
146 for parent in ctx.parents():
147 rev = parent.rev()
147 rev = parent.rev()
148 if rev not in seen:
148 if rev not in seen:
149 seen.add(rev)
149 seen.add(rev)
150 if parent.mutable():
150 if parent.mutable():
151 visit.append(parent)
151 visit.append(parent)
152
152
153 wctx = repo[None]
153 wctx = repo[None]
154 parents = wctx.parents()
154 parents = wctx.parents()
155 if len(parents) > 1:
155 if len(parents) > 1:
156 raise util.Abort(_('cannot shelve while merging'))
156 raise util.Abort(_('cannot shelve while merging'))
157 parent = parents[0]
157 parent = parents[0]
158
158
159 # we never need the user, so we use a generic user for all shelve operations
159 # we never need the user, so we use a generic user for all shelve operations
160 user = 'shelve@localhost'
160 user = 'shelve@localhost'
161 label = repo._bookmarkcurrent or parent.branch() or 'default'
161 label = repo._bookmarkcurrent or parent.branch() or 'default'
162
162
163 # slashes aren't allowed in filenames, therefore we rename it
163 # slashes aren't allowed in filenames, therefore we rename it
164 label = label.replace('/', '_')
164 label = label.replace('/', '_')
165
165
166 def gennames():
166 def gennames():
167 yield label
167 yield label
168 for i in xrange(1, 100):
168 for i in xrange(1, 100):
169 yield '%s-%02d' % (label, i)
169 yield '%s-%02d' % (label, i)
170
170
171 shelvedfiles = []
171 shelvedfiles = []
172
172
173 def commitfunc(ui, repo, message, match, opts):
173 def commitfunc(ui, repo, message, match, opts):
174 # check modified, added, removed, deleted only
174 # check modified, added, removed, deleted only
175 for flist in repo.status(match=match)[:4]:
175 for flist in repo.status(match=match)[:4]:
176 shelvedfiles.extend(flist)
176 shelvedfiles.extend(flist)
177 hasmq = util.safehasattr(repo, 'mq')
177 hasmq = util.safehasattr(repo, 'mq')
178 if hasmq:
178 if hasmq:
179 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
179 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
180 try:
180 try:
181 return repo.commit(message, user, opts.get('date'), match)
181 return repo.commit(message, user, opts.get('date'), match)
182 finally:
182 finally:
183 if hasmq:
183 if hasmq:
184 repo.mq.checkapplied = saved
184 repo.mq.checkapplied = saved
185
185
186 if parent.node() != nullid:
186 if parent.node() != nullid:
187 desc = "changes to '%s'" % parent.description().split('\n', 1)[0]
187 desc = "changes to '%s'" % parent.description().split('\n', 1)[0]
188 else:
188 else:
189 desc = '(changes in empty repository)'
189 desc = '(changes in empty repository)'
190
190
191 if not opts['message']:
191 if not opts['message']:
192 opts['message'] = desc
192 opts['message'] = desc
193
193
194 name = opts['name']
194 name = opts['name']
195
195
196 wlock = lock = tr = bms = None
196 wlock = lock = tr = bms = None
197 try:
197 try:
198 wlock = repo.wlock()
198 wlock = repo.wlock()
199 lock = repo.lock()
199 lock = repo.lock()
200
200
201 bms = repo._bookmarks.copy()
201 bms = repo._bookmarks.copy()
202 # use an uncommitted transaction to generate the bundle to avoid
202 # use an uncommitted transaction to generate the bundle to avoid
203 # pull races. ensure we don't print the abort message to stderr.
203 # pull races. ensure we don't print the abort message to stderr.
204 tr = repo.transaction('commit', report=lambda x: None)
204 tr = repo.transaction('commit', report=lambda x: None)
205
205
206 if name:
206 if name:
207 if shelvedfile(repo, name, 'hg').exists():
207 if shelvedfile(repo, name, 'hg').exists():
208 raise util.Abort(_("a shelved change named '%s' already exists")
208 raise util.Abort(_("a shelved change named '%s' already exists")
209 % name)
209 % name)
210 else:
210 else:
211 for n in gennames():
211 for n in gennames():
212 if not shelvedfile(repo, n, 'hg').exists():
212 if not shelvedfile(repo, n, 'hg').exists():
213 name = n
213 name = n
214 break
214 break
215 else:
215 else:
216 raise util.Abort(_("too many shelved changes named '%s'") %
216 raise util.Abort(_("too many shelved changes named '%s'") %
217 label)
217 label)
218
218
219 # ensure we are not creating a subdirectory or a hidden file
219 # ensure we are not creating a subdirectory or a hidden file
220 if '/' in name or '\\' in name:
220 if '/' in name or '\\' in name:
221 raise util.Abort(_('shelved change names may not contain slashes'))
221 raise util.Abort(_('shelved change names may not contain slashes'))
222 if name.startswith('.'):
222 if name.startswith('.'):
223 raise util.Abort(_("shelved change names may not start with '.'"))
223 raise util.Abort(_("shelved change names may not start with '.'"))
224
224
225 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
225 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
226
226
227 if not node:
227 if not node:
228 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
228 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
229 if stat[3]:
229 if stat[3]:
230 ui.status(_("nothing changed (%d missing files, see "
230 ui.status(_("nothing changed (%d missing files, see "
231 "'hg status')\n") % len(stat[3]))
231 "'hg status')\n") % len(stat[3]))
232 else:
232 else:
233 ui.status(_("nothing changed\n"))
233 ui.status(_("nothing changed\n"))
234 return 1
234 return 1
235
235
236 phases.retractboundary(repo, phases.secret, [node])
236 phases.retractboundary(repo, phases.secret, [node])
237
237
238 fp = shelvedfile(repo, name, 'files').opener('wb')
238 fp = shelvedfile(repo, name, 'files').opener('wb')
239 fp.write('\0'.join(shelvedfiles))
239 fp.write('\0'.join(shelvedfiles))
240
240
241 bases = list(publicancestors(repo[node]))
241 bases = list(publicancestors(repo[node]))
242 cg = changegroup.changegroupsubset(repo, bases, [node], 'shelve')
242 cg = changegroup.changegroupsubset(repo, bases, [node], 'shelve')
243 shelvedfile(repo, name, 'hg').writebundle(cg)
243 shelvedfile(repo, name, 'hg').writebundle(cg)
244 cmdutil.export(repo, [node],
244 cmdutil.export(repo, [node],
245 fp=shelvedfile(repo, name, 'patch').opener('wb'),
245 fp=shelvedfile(repo, name, 'patch').opener('wb'),
246 opts=mdiff.diffopts(git=True))
246 opts=mdiff.diffopts(git=True))
247
247
248
248
249 if ui.formatted():
249 if ui.formatted():
250 desc = util.ellipsis(desc, ui.termwidth())
250 desc = util.ellipsis(desc, ui.termwidth())
251 ui.status(_('shelved as %s\n') % name)
251 ui.status(_('shelved as %s\n') % name)
252 hg.update(repo, parent.node())
252 hg.update(repo, parent.node())
253 finally:
253 finally:
254 if bms:
254 if bms:
255 # restore old bookmarks
255 # restore old bookmarks
256 repo._bookmarks.update(bms)
256 repo._bookmarks.update(bms)
257 repo._bookmarks.write()
257 repo._bookmarks.write()
258 if tr:
258 if tr:
259 tr.abort()
259 tr.abort()
260 lockmod.release(lock, wlock)
260 lockmod.release(lock, wlock)
261
261
262 def cleanupcmd(ui, repo):
262 def cleanupcmd(ui, repo):
263 """subcommand that deletes all shelves"""
263 """subcommand that deletes all shelves"""
264
264
265 wlock = None
265 wlock = None
266 try:
266 try:
267 wlock = repo.wlock()
267 wlock = repo.wlock()
268 for (name, _) in repo.vfs.readdir('shelved'):
268 for (name, _) in repo.vfs.readdir('shelved'):
269 suffix = name.rsplit('.', 1)[-1]
269 suffix = name.rsplit('.', 1)[-1]
270 if suffix in ('hg', 'files', 'patch'):
270 if suffix in ('hg', 'files', 'patch'):
271 shelvedfile(repo, name).unlink()
271 shelvedfile(repo, name).unlink()
272 finally:
272 finally:
273 lockmod.release(wlock)
273 lockmod.release(wlock)
274
274
275 def deletecmd(ui, repo, pats):
275 def deletecmd(ui, repo, pats):
276 """subcommand that deletes a specific shelve"""
276 """subcommand that deletes a specific shelve"""
277 if not pats:
277 if not pats:
278 raise util.Abort(_('no shelved changes specified!'))
278 raise util.Abort(_('no shelved changes specified!'))
279 wlock = None
279 wlock = None
280 try:
280 try:
281 wlock = repo.wlock()
281 wlock = repo.wlock()
282 try:
282 try:
283 for name in pats:
283 for name in pats:
284 for suffix in 'hg files patch'.split():
284 for suffix in 'hg files patch'.split():
285 shelvedfile(repo, name, suffix).unlink()
285 shelvedfile(repo, name, suffix).unlink()
286 except OSError, err:
286 except OSError, err:
287 if err.errno != errno.ENOENT:
287 if err.errno != errno.ENOENT:
288 raise
288 raise
289 raise util.Abort(_("shelved change '%s' not found") % name)
289 raise util.Abort(_("shelved change '%s' not found") % name)
290 finally:
290 finally:
291 lockmod.release(wlock)
291 lockmod.release(wlock)
292
292
293 def listshelves(repo):
293 def listshelves(repo):
294 """return all shelves in repo as list of (time, filename)"""
294 """return all shelves in repo as list of (time, filename)"""
295 try:
295 try:
296 names = repo.vfs.readdir('shelved')
296 names = repo.vfs.readdir('shelved')
297 except OSError, err:
297 except OSError, err:
298 if err.errno != errno.ENOENT:
298 if err.errno != errno.ENOENT:
299 raise
299 raise
300 return []
300 return []
301 info = []
301 info = []
302 for (name, _) in names:
302 for (name, _) in names:
303 pfx, sfx = name.rsplit('.', 1)
303 pfx, sfx = name.rsplit('.', 1)
304 if not pfx or sfx != 'patch':
304 if not pfx or sfx != 'patch':
305 continue
305 continue
306 st = shelvedfile(repo, name).stat()
306 st = shelvedfile(repo, name).stat()
307 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
307 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
308 return sorted(info, reverse=True)
308 return sorted(info, reverse=True)
309
309
310 def listcmd(ui, repo, pats, opts):
310 def listcmd(ui, repo, pats, opts):
311 """subcommand that displays the list of shelves"""
311 """subcommand that displays the list of shelves"""
312 pats = set(pats)
312 pats = set(pats)
313 width = 80
313 width = 80
314 if not ui.plain():
314 if not ui.plain():
315 width = ui.termwidth()
315 width = ui.termwidth()
316 namelabel = 'shelve.newest'
316 namelabel = 'shelve.newest'
317 for mtime, name in listshelves(repo):
317 for mtime, name in listshelves(repo):
318 sname = util.split(name)[1]
318 sname = util.split(name)[1]
319 if pats and sname not in pats:
319 if pats and sname not in pats:
320 continue
320 continue
321 ui.write(sname, label=namelabel)
321 ui.write(sname, label=namelabel)
322 namelabel = 'shelve.name'
322 namelabel = 'shelve.name'
323 if ui.quiet:
323 if ui.quiet:
324 ui.write('\n')
324 ui.write('\n')
325 continue
325 continue
326 ui.write(' ' * (16 - len(sname)))
326 ui.write(' ' * (16 - len(sname)))
327 used = 16
327 used = 16
328 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
328 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
329 ui.write(age, label='shelve.age')
329 ui.write(age, label='shelve.age')
330 ui.write(' ' * (12 - len(age)))
330 ui.write(' ' * (12 - len(age)))
331 used += 12
331 used += 12
332 fp = open(name + '.patch', 'rb')
332 fp = open(name + '.patch', 'rb')
333 try:
333 try:
334 while True:
334 while True:
335 line = fp.readline()
335 line = fp.readline()
336 if not line:
336 if not line:
337 break
337 break
338 if not line.startswith('#'):
338 if not line.startswith('#'):
339 desc = line.rstrip()
339 desc = line.rstrip()
340 if ui.formatted():
340 if ui.formatted():
341 desc = util.ellipsis(desc, width - used)
341 desc = util.ellipsis(desc, width - used)
342 ui.write(desc)
342 ui.write(desc)
343 break
343 break
344 ui.write('\n')
344 ui.write('\n')
345 if not (opts['patch'] or opts['stat']):
345 if not (opts['patch'] or opts['stat']):
346 continue
346 continue
347 difflines = fp.readlines()
347 difflines = fp.readlines()
348 if opts['patch']:
348 if opts['patch']:
349 for chunk, label in patch.difflabel(iter, difflines):
349 for chunk, label in patch.difflabel(iter, difflines):
350 ui.write(chunk, label=label)
350 ui.write(chunk, label=label)
351 if opts['stat']:
351 if opts['stat']:
352 for chunk, label in patch.diffstatui(difflines, width=width,
352 for chunk, label in patch.diffstatui(difflines, width=width,
353 git=True):
353 git=True):
354 ui.write(chunk, label=label)
354 ui.write(chunk, label=label)
355 finally:
355 finally:
356 fp.close()
356 fp.close()
357
357
358 def checkparents(repo, state):
358 def checkparents(repo, state):
359 """check parent while resuming an unshelve"""
359 """check parent while resuming an unshelve"""
360 if state.parents != repo.dirstate.parents():
360 if state.parents != repo.dirstate.parents():
361 raise util.Abort(_('working directory parents do not match unshelve '
361 raise util.Abort(_('working directory parents do not match unshelve '
362 'state'))
362 'state'))
363
363
364 def pathtofiles(repo, files):
364 def pathtofiles(repo, files):
365 cwd = repo.getcwd()
365 cwd = repo.getcwd()
366 return [repo.pathto(f, cwd) for f in files]
366 return [repo.pathto(f, cwd) for f in files]
367
367
368 def unshelveabort(ui, repo, state, opts):
368 def unshelveabort(ui, repo, state, opts):
369 """subcommand that abort an in-progress unshelve"""
369 """subcommand that abort an in-progress unshelve"""
370 wlock = repo.wlock()
370 wlock = repo.wlock()
371 lock = None
371 lock = None
372 try:
372 try:
373 checkparents(repo, state)
373 checkparents(repo, state)
374
374
375 util.rename(repo.join('unshelverebasestate'),
375 util.rename(repo.join('unshelverebasestate'),
376 repo.join('rebasestate'))
376 repo.join('rebasestate'))
377 try:
377 try:
378 rebase.rebase(ui, repo, **{
378 rebase.rebase(ui, repo, **{
379 'abort' : True
379 'abort' : True
380 })
380 })
381 except Exception:
381 except Exception:
382 util.rename(repo.join('rebasestate'),
382 util.rename(repo.join('rebasestate'),
383 repo.join('unshelverebasestate'))
383 repo.join('unshelverebasestate'))
384 raise
384 raise
385
385
386 lock = repo.lock()
386 lock = repo.lock()
387
387
388 mergefiles(ui, repo, state.wctx, state.pendingctx)
388 mergefiles(ui, repo, state.wctx, state.pendingctx)
389
389
390 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
390 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
391 shelvedstate.clear(repo)
391 shelvedstate.clear(repo)
392 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
392 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
393 finally:
393 finally:
394 lockmod.release(lock, wlock)
394 lockmod.release(lock, wlock)
395
395
396 def mergefiles(ui, repo, wctx, shelvectx):
396 def mergefiles(ui, repo, wctx, shelvectx):
397 """updates to wctx and merges the changes from shelvectx into the
397 """updates to wctx and merges the changes from shelvectx into the
398 dirstate."""
398 dirstate."""
399 oldquiet = ui.quiet
399 oldquiet = ui.quiet
400 try:
400 try:
401 ui.quiet = True
401 ui.quiet = True
402 hg.update(repo, wctx.node())
402 hg.update(repo, wctx.node())
403 files = []
403 files = []
404 files.extend(shelvectx.files())
404 files.extend(shelvectx.files())
405 files.extend(shelvectx.parents()[0].files())
405 files.extend(shelvectx.parents()[0].files())
406
406
407 # revert will overwrite unknown files, so move them out of the way
407 # revert will overwrite unknown files, so move them out of the way
408 m, a, r, d, u = repo.status(unknown=True)[:5]
408 m, a, r, d, u = repo.status(unknown=True)[:5]
409 for file in u:
409 for file in u:
410 if file in files:
410 if file in files:
411 util.rename(file, file + ".orig")
411 util.rename(file, file + ".orig")
412 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
412 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
413 *pathtofiles(repo, files),
413 *pathtofiles(repo, files),
414 **{'no_backup': True})
414 **{'no_backup': True})
415 finally:
415 finally:
416 ui.quiet = oldquiet
416 ui.quiet = oldquiet
417
417
418 def unshelvecleanup(ui, repo, name, opts):
418 def unshelvecleanup(ui, repo, name, opts):
419 """remove related files after an unshelve"""
419 """remove related files after an unshelve"""
420 if not opts['keep']:
420 if not opts['keep']:
421 for filetype in 'hg files patch'.split():
421 for filetype in 'hg files patch'.split():
422 shelvedfile(repo, name, filetype).unlink()
422 shelvedfile(repo, name, filetype).unlink()
423
423
424 def unshelvecontinue(ui, repo, state, opts):
424 def unshelvecontinue(ui, repo, state, opts):
425 """subcommand to continue an in-progress unshelve"""
425 """subcommand to continue an in-progress unshelve"""
426 # We're finishing off a merge. First parent is our original
426 # We're finishing off a merge. First parent is our original
427 # parent, second is the temporary "fake" commit we're unshelving.
427 # parent, second is the temporary "fake" commit we're unshelving.
428 wlock = repo.wlock()
428 wlock = repo.wlock()
429 lock = None
429 lock = None
430 try:
430 try:
431 checkparents(repo, state)
431 checkparents(repo, state)
432 ms = merge.mergestate(repo)
432 ms = merge.mergestate(repo)
433 if [f for f in ms if ms[f] == 'u']:
433 if [f for f in ms if ms[f] == 'u']:
434 raise util.Abort(
434 raise util.Abort(
435 _("unresolved conflicts, can't continue"),
435 _("unresolved conflicts, can't continue"),
436 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
436 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
437
437
438 lock = repo.lock()
438 lock = repo.lock()
439
439
440 util.rename(repo.join('unshelverebasestate'),
440 util.rename(repo.join('unshelverebasestate'),
441 repo.join('rebasestate'))
441 repo.join('rebasestate'))
442 try:
442 try:
443 rebase.rebase(ui, repo, **{
443 rebase.rebase(ui, repo, **{
444 'continue' : True
444 'continue' : True
445 })
445 })
446 except Exception:
446 except Exception:
447 util.rename(repo.join('rebasestate'),
447 util.rename(repo.join('rebasestate'),
448 repo.join('unshelverebasestate'))
448 repo.join('unshelverebasestate'))
449 raise
449 raise
450
450
451 shelvectx = repo['tip']
451 shelvectx = repo['tip']
452 if not shelvectx in state.pendingctx.children():
452 if not shelvectx in state.pendingctx.children():
453 # rebase was a no-op, so it produced no child commit
453 # rebase was a no-op, so it produced no child commit
454 shelvectx = state.pendingctx
454 shelvectx = state.pendingctx
455
455
456 mergefiles(ui, repo, state.wctx, shelvectx)
456 mergefiles(ui, repo, state.wctx, shelvectx)
457
457
458 state.stripnodes.append(shelvectx.node())
458 state.stripnodes.append(shelvectx.node())
459 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
459 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
460 shelvedstate.clear(repo)
460 shelvedstate.clear(repo)
461 unshelvecleanup(ui, repo, state.name, opts)
461 unshelvecleanup(ui, repo, state.name, opts)
462 ui.status(_("unshelve of '%s' complete\n") % state.name)
462 ui.status(_("unshelve of '%s' complete\n") % state.name)
463 finally:
463 finally:
464 lockmod.release(lock, wlock)
464 lockmod.release(lock, wlock)
465
465
466 @command('unshelve',
466 @command('unshelve',
467 [('a', 'abort', None,
467 [('a', 'abort', None,
468 _('abort an incomplete unshelve operation')),
468 _('abort an incomplete unshelve operation')),
469 ('c', 'continue', None,
469 ('c', 'continue', None,
470 _('continue an incomplete unshelve operation')),
470 _('continue an incomplete unshelve operation')),
471 ('', 'keep', None,
471 ('', 'keep', None,
472 _('keep shelve after unshelving')),
472 _('keep shelve after unshelving')),
473 ('', 'date', '',
473 ('', 'date', '',
474 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
474 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
475 _('hg unshelve [SHELVED]'))
475 _('hg unshelve [SHELVED]'))
476 def unshelve(ui, repo, *shelved, **opts):
476 def unshelve(ui, repo, *shelved, **opts):
477 """restore a shelved change to the working directory
477 """restore a shelved change to the working directory
478
478
479 This command accepts an optional name of a shelved change to
479 This command accepts an optional name of a shelved change to
480 restore. If none is given, the most recent shelved change is used.
480 restore. If none is given, the most recent shelved change is used.
481
481
482 If a shelved change is applied successfully, the bundle that
482 If a shelved change is applied successfully, the bundle that
483 contains the shelved changes is deleted afterwards.
483 contains the shelved changes is deleted afterwards.
484
484
485 Since you can restore a shelved change on top of an arbitrary
485 Since you can restore a shelved change on top of an arbitrary
486 commit, it is possible that unshelving will result in a conflict
486 commit, it is possible that unshelving will result in a conflict
487 between your changes and the commits you are unshelving onto. If
487 between your changes and the commits you are unshelving onto. If
488 this occurs, you must resolve the conflict, then use
488 this occurs, you must resolve the conflict, then use
489 ``--continue`` to complete the unshelve operation. (The bundle
489 ``--continue`` to complete the unshelve operation. (The bundle
490 will not be deleted until you successfully complete the unshelve.)
490 will not be deleted until you successfully complete the unshelve.)
491
491
492 (Alternatively, you can use ``--abort`` to abandon an unshelve
492 (Alternatively, you can use ``--abort`` to abandon an unshelve
493 that causes a conflict. This reverts the unshelved changes, and
493 that causes a conflict. This reverts the unshelved changes, and
494 does not delete the bundle.)
494 does not delete the bundle.)
495 """
495 """
496 abortf = opts['abort']
496 abortf = opts['abort']
497 continuef = opts['continue']
497 continuef = opts['continue']
498 if not abortf and not continuef:
498 if not abortf and not continuef:
499 cmdutil.checkunfinished(repo)
499 cmdutil.checkunfinished(repo)
500
500
501 if abortf or continuef:
501 if abortf or continuef:
502 if abortf and continuef:
502 if abortf and continuef:
503 raise util.Abort(_('cannot use both abort and continue'))
503 raise util.Abort(_('cannot use both abort and continue'))
504 if shelved:
504 if shelved:
505 raise util.Abort(_('cannot combine abort/continue with '
505 raise util.Abort(_('cannot combine abort/continue with '
506 'naming a shelved change'))
506 'naming a shelved change'))
507
507
508 try:
508 try:
509 state = shelvedstate.load(repo)
509 state = shelvedstate.load(repo)
510 except IOError, err:
510 except IOError, err:
511 if err.errno != errno.ENOENT:
511 if err.errno != errno.ENOENT:
512 raise
512 raise
513 raise util.Abort(_('no unshelve operation underway'))
513 raise util.Abort(_('no unshelve operation underway'))
514
514
515 if abortf:
515 if abortf:
516 return unshelveabort(ui, repo, state, opts)
516 return unshelveabort(ui, repo, state, opts)
517 elif continuef:
517 elif continuef:
518 return unshelvecontinue(ui, repo, state, opts)
518 return unshelvecontinue(ui, repo, state, opts)
519 elif len(shelved) > 1:
519 elif len(shelved) > 1:
520 raise util.Abort(_('can only unshelve one change at a time'))
520 raise util.Abort(_('can only unshelve one change at a time'))
521 elif not shelved:
521 elif not shelved:
522 shelved = listshelves(repo)
522 shelved = listshelves(repo)
523 if not shelved:
523 if not shelved:
524 raise util.Abort(_('no shelved changes to apply!'))
524 raise util.Abort(_('no shelved changes to apply!'))
525 basename = util.split(shelved[0][1])[1]
525 basename = util.split(shelved[0][1])[1]
526 ui.status(_("unshelving change '%s'\n") % basename)
526 ui.status(_("unshelving change '%s'\n") % basename)
527 else:
527 else:
528 basename = shelved[0]
528 basename = shelved[0]
529
529
530 if not shelvedfile(repo, basename, 'files').exists():
530 if not shelvedfile(repo, basename, 'files').exists():
531 raise util.Abort(_("shelved change '%s' not found") % basename)
531 raise util.Abort(_("shelved change '%s' not found") % basename)
532
532
533 oldquiet = ui.quiet
533 oldquiet = ui.quiet
534 wlock = lock = tr = None
534 wlock = lock = tr = None
535 try:
535 try:
536 lock = repo.lock()
536 lock = repo.lock()
537 wlock = repo.wlock()
537 wlock = repo.wlock()
538
538
539 tr = repo.transaction('unshelve', report=lambda x: None)
539 tr = repo.transaction('unshelve', report=lambda x: None)
540 oldtiprev = len(repo)
540 oldtiprev = len(repo)
541
541
542 pctx = repo['.']
542 pctx = repo['.']
543 tmpwctx = pctx
543 tmpwctx = pctx
544 # The goal is to have a commit structure like so:
544 # The goal is to have a commit structure like so:
545 # ...-> pctx -> tmpwctx -> shelvectx
545 # ...-> pctx -> tmpwctx -> shelvectx
546 # where tmpwctx is an optional commit with the user's pending changes
546 # where tmpwctx is an optional commit with the user's pending changes
547 # and shelvectx is the unshelved changes. Then we merge it all down
547 # and shelvectx is the unshelved changes. Then we merge it all down
548 # to the original pctx.
548 # to the original pctx.
549
549
550 # Store pending changes in a commit
550 # Store pending changes in a commit
551 m, a, r, d = repo.status()[:4]
551 m, a, r, d = repo.status()[:4]
552 if m or a or r or d:
552 if m or a or r or d:
553 ui.status(_("temporarily committing pending changes "
553 ui.status(_("temporarily committing pending changes "
554 "(restore with 'hg unshelve --abort')\n"))
554 "(restore with 'hg unshelve --abort')\n"))
555 def commitfunc(ui, repo, message, match, opts):
555 def commitfunc(ui, repo, message, match, opts):
556 hasmq = util.safehasattr(repo, 'mq')
556 hasmq = util.safehasattr(repo, 'mq')
557 if hasmq:
557 if hasmq:
558 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
558 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
559
559
560 try:
560 try:
561 return repo.commit(message, 'shelve@localhost',
561 return repo.commit(message, 'shelve@localhost',
562 opts.get('date'), match)
562 opts.get('date'), match)
563 finally:
563 finally:
564 if hasmq:
564 if hasmq:
565 repo.mq.checkapplied = saved
565 repo.mq.checkapplied = saved
566
566
567 tempopts = {}
567 tempopts = {}
568 tempopts['message'] = "pending changes temporary commit"
568 tempopts['message'] = "pending changes temporary commit"
569 tempopts['date'] = opts.get('date')
569 tempopts['date'] = opts.get('date')
570 ui.quiet = True
570 ui.quiet = True
571 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
571 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
572 tmpwctx = repo[node]
572 tmpwctx = repo[node]
573
573
574 ui.quiet = True
574 ui.quiet = True
575 shelvedfile(repo, basename, 'hg').applybundle()
575 shelvedfile(repo, basename, 'hg').applybundle()
576 nodes = [ctx.node() for ctx in repo.set('%d:', oldtiprev)]
576 nodes = [ctx.node() for ctx in repo.set('%d:', oldtiprev)]
577 phases.retractboundary(repo, phases.secret, nodes)
577 phases.retractboundary(repo, phases.secret, nodes)
578
578
579 ui.quiet = oldquiet
579 ui.quiet = oldquiet
580
580
581 shelvectx = repo['tip']
581 shelvectx = repo['tip']
582
582
583 # If the shelve is not immediately on top of the commit
583 # If the shelve is not immediately on top of the commit
584 # we'll be merging with, rebase it to be on top.
584 # we'll be merging with, rebase it to be on top.
585 if tmpwctx.node() != shelvectx.parents()[0].node():
585 if tmpwctx.node() != shelvectx.parents()[0].node():
586 ui.status(_('rebasing shelved changes\n'))
586 ui.status(_('rebasing shelved changes\n'))
587 try:
587 try:
588 rebase.rebase(ui, repo, **{
588 rebase.rebase(ui, repo, **{
589 'rev' : [shelvectx.rev()],
589 'rev' : [shelvectx.rev()],
590 'dest' : str(tmpwctx.rev()),
590 'dest' : str(tmpwctx.rev()),
591 'keep' : True,
591 'keep' : True,
592 })
592 })
593 except error.InterventionRequired:
593 except error.InterventionRequired:
594 tr.close()
594 tr.close()
595
595
596 stripnodes = [repo.changelog.node(rev)
596 stripnodes = [repo.changelog.node(rev)
597 for rev in xrange(oldtiprev, len(repo))]
597 for rev in xrange(oldtiprev, len(repo))]
598 shelvedstate.save(repo, basename, pctx, tmpwctx, stripnodes)
598 shelvedstate.save(repo, basename, pctx, tmpwctx, stripnodes)
599
599
600 util.rename(repo.join('rebasestate'),
600 util.rename(repo.join('rebasestate'),
601 repo.join('unshelverebasestate'))
601 repo.join('unshelverebasestate'))
602 raise error.InterventionRequired(
602 raise error.InterventionRequired(
603 _("unresolved conflicts (see 'hg resolve', then "
603 _("unresolved conflicts (see 'hg resolve', then "
604 "'hg unshelve --continue')"))
604 "'hg unshelve --continue')"))
605
605
606 # refresh ctx after rebase completes
606 # refresh ctx after rebase completes
607 shelvectx = repo['tip']
607 shelvectx = repo['tip']
608
608
609 if not shelvectx in tmpwctx.children():
609 if not shelvectx in tmpwctx.children():
610 # rebase was a no-op, so it produced no child commit
610 # rebase was a no-op, so it produced no child commit
611 shelvectx = tmpwctx
611 shelvectx = tmpwctx
612
612
613 mergefiles(ui, repo, pctx, shelvectx)
613 mergefiles(ui, repo, pctx, shelvectx)
614 shelvedstate.clear(repo)
614 shelvedstate.clear(repo)
615
615
616 # The transaction aborting will strip all the commits for us,
616 # The transaction aborting will strip all the commits for us,
617 # but it doesn't update the inmemory structures, so addchangegroup
617 # but it doesn't update the inmemory structures, so addchangegroup
618 # hooks still fire and try to operate on the missing commits.
618 # hooks still fire and try to operate on the missing commits.
619 # Clean up manually to prevent this.
619 # Clean up manually to prevent this.
620 repo.unfiltered().changelog.strip(oldtiprev, tr)
620 repo.unfiltered().changelog.strip(oldtiprev, tr)
621
621
622 unshelvecleanup(ui, repo, basename, opts)
622 unshelvecleanup(ui, repo, basename, opts)
623 finally:
623 finally:
624 ui.quiet = oldquiet
624 ui.quiet = oldquiet
625 if tr:
625 if tr:
626 tr.release()
626 tr.release()
627 lockmod.release(lock, wlock)
627 lockmod.release(lock, wlock)
628
628
629 @command('shelve',
629 @command('shelve',
630 [('A', 'addremove', None,
630 [('A', 'addremove', None,
631 _('mark new/missing files as added/removed before shelving')),
631 _('mark new/missing files as added/removed before shelving')),
632 ('', 'cleanup', None,
632 ('', 'cleanup', None,
633 _('delete all shelved changes')),
633 _('delete all shelved changes')),
634 ('', 'date', '',
634 ('', 'date', '',
635 _('shelve with the specified commit date'), _('DATE')),
635 _('shelve with the specified commit date'), _('DATE')),
636 ('d', 'delete', None,
636 ('d', 'delete', None,
637 _('delete the named shelved change(s)')),
637 _('delete the named shelved change(s)')),
638 ('l', 'list', None,
638 ('l', 'list', None,
639 _('list current shelves')),
639 _('list current shelves')),
640 ('m', 'message', '',
640 ('m', 'message', '',
641 _('use text as shelve message'), _('TEXT')),
641 _('use text as shelve message'), _('TEXT')),
642 ('n', 'name', '',
642 ('n', 'name', '',
643 _('use the given name for the shelved commit'), _('NAME')),
643 _('use the given name for the shelved commit'), _('NAME')),
644 ('p', 'patch', None,
644 ('p', 'patch', None,
645 _('show patch')),
645 _('show patch')),
646 ('', 'stat', None,
646 ('', 'stat', None,
647 _('output diffstat-style summary of changes'))] + commands.walkopts,
647 _('output diffstat-style summary of changes'))] + commands.walkopts,
648 _('hg shelve [OPTION]... [FILE]...'))
648 _('hg shelve [OPTION]... [FILE]...'))
649 def shelvecmd(ui, repo, *pats, **opts):
649 def shelvecmd(ui, repo, *pats, **opts):
650 '''save and set aside changes from the working directory
650 '''save and set aside changes from the working directory
651
651
652 Shelving takes files that "hg status" reports as not clean, saves
652 Shelving takes files that "hg status" reports as not clean, saves
653 the modifications to a bundle (a shelved change), and reverts the
653 the modifications to a bundle (a shelved change), and reverts the
654 files so that their state in the working directory becomes clean.
654 files so that their state in the working directory becomes clean.
655
655
656 To restore these changes to the working directory, using "hg
656 To restore these changes to the working directory, using "hg
657 unshelve"; this will work even if you switch to a different
657 unshelve"; this will work even if you switch to a different
658 commit.
658 commit.
659
659
660 When no files are specified, "hg shelve" saves all not-clean
660 When no files are specified, "hg shelve" saves all not-clean
661 files. If specific files or directories are named, only changes to
661 files. If specific files or directories are named, only changes to
662 those files are shelved.
662 those files are shelved.
663
663
664 Each shelved change has a name that makes it easier to find later.
664 Each shelved change has a name that makes it easier to find later.
665 The name of a shelved change defaults to being based on the active
665 The name of a shelved change defaults to being based on the active
666 bookmark, or if there is no active bookmark, the current named
666 bookmark, or if there is no active bookmark, the current named
667 branch. To specify a different name, use ``--name``.
667 branch. To specify a different name, use ``--name``.
668
668
669 To see a list of existing shelved changes, use the ``--list``
669 To see a list of existing shelved changes, use the ``--list``
670 option. For each shelved change, this will print its name, age,
670 option. For each shelved change, this will print its name, age,
671 and description; use ``--patch`` or ``--stat`` for more details.
671 and description; use ``--patch`` or ``--stat`` for more details.
672
672
673 To delete specific shelved changes, use ``--delete``. To delete
673 To delete specific shelved changes, use ``--delete``. To delete
674 all shelved changes, use ``--cleanup``.
674 all shelved changes, use ``--cleanup``.
675 '''
675 '''
676 cmdutil.checkunfinished(repo)
676 cmdutil.checkunfinished(repo)
677
677
678 def checkopt(opt, incompatible):
678 allowables = [
679 ('addremove', 'create'), # 'create' is pseudo action
680 ('cleanup', 'cleanup'),
681 # ('date', 'create'), # ignored for passing '--date "0 0"' in tests
682 ('delete', 'delete'),
683 ('list', 'list'),
684 ('message', 'create'),
685 ('name', 'create'),
686 ('patch', 'list'),
687 ('stat', 'list'),
688 ]
689 def checkopt(opt):
679 if opts[opt]:
690 if opts[opt]:
680 for i in incompatible.split():
691 for i, allowable in allowables:
681 if opts[i]:
692 if opts[i] and opt != allowable:
682 raise util.Abort(_("options '--%s' and '--%s' may not be "
693 raise util.Abort(_("options '--%s' and '--%s' may not be "
683 "used together") % (opt, i))
694 "used together") % (opt, i))
684 return True
695 return True
685 if checkopt('cleanup', 'addremove delete list message name patch stat'):
696 if checkopt('cleanup'):
686 if pats:
697 if pats:
687 raise util.Abort(_("cannot specify names when using '--cleanup'"))
698 raise util.Abort(_("cannot specify names when using '--cleanup'"))
688 return cleanupcmd(ui, repo)
699 return cleanupcmd(ui, repo)
689 elif checkopt('delete', 'addremove cleanup list message name patch stat'):
700 elif checkopt('delete'):
690 return deletecmd(ui, repo, pats)
701 return deletecmd(ui, repo, pats)
691 elif checkopt('list', 'addremove cleanup delete message name'):
702 elif checkopt('list'):
692 return listcmd(ui, repo, pats, opts)
703 return listcmd(ui, repo, pats, opts)
693 else:
704 else:
694 for i in ('patch', 'stat'):
705 for i in ('patch', 'stat'):
695 if opts[i]:
706 if opts[i]:
696 raise util.Abort(_("option '--%s' may not be "
707 raise util.Abort(_("option '--%s' may not be "
697 "used when shelving a change") % (i,))
708 "used when shelving a change") % (i,))
698 return createcmd(ui, repo, pats, opts)
709 return createcmd(ui, repo, pats, opts)
699
710
700 def extsetup(ui):
711 def extsetup(ui):
701 cmdutil.unfinishedstates.append(
712 cmdutil.unfinishedstates.append(
702 [shelvedstate._filename, False, False,
713 [shelvedstate._filename, False, False,
703 _('unshelve already in progress'),
714 _('unshelve already in progress'),
704 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
715 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now