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