##// END OF EJS Templates
Fix and unify transplant and bookmarks revsets doc registration
Patrick Mezard -
r12822:f13acb96 stable
parent child Browse files
Show More
@@ -1,570 +1,568 b''
1 # Mercurial extension to provide the 'hg bookmark' command
1 # Mercurial extension to provide the 'hg bookmark' command
2 #
2 #
3 # Copyright 2008 David Soria Parra <dsp@php.net>
3 # Copyright 2008 David Soria Parra <dsp@php.net>
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 '''track a line of development with movable markers
8 '''track a line of development with movable markers
9
9
10 Bookmarks are local movable markers to changesets. Every bookmark
10 Bookmarks are local movable markers to changesets. Every bookmark
11 points to a changeset identified by its hash. If you commit a
11 points to a changeset identified by its hash. If you commit a
12 changeset that is based on a changeset that has a bookmark on it, the
12 changeset that is based on a changeset that has a bookmark on it, the
13 bookmark shifts to the new changeset.
13 bookmark shifts to the new changeset.
14
14
15 It is possible to use bookmark names in every revision lookup (e.g.
15 It is possible to use bookmark names in every revision lookup (e.g.
16 :hg:`merge`, :hg:`update`).
16 :hg:`merge`, :hg:`update`).
17
17
18 By default, when several bookmarks point to the same changeset, they
18 By default, when several bookmarks point to the same changeset, they
19 will all move forward together. It is possible to obtain a more
19 will all move forward together. It is possible to obtain a more
20 git-like experience by adding the following configuration option to
20 git-like experience by adding the following configuration option to
21 your configuration file::
21 your configuration file::
22
22
23 [bookmarks]
23 [bookmarks]
24 track.current = True
24 track.current = True
25
25
26 This will cause Mercurial to track the bookmark that you are currently
26 This will cause Mercurial to track the bookmark that you are currently
27 using, and only update it. This is similar to git's approach to
27 using, and only update it. This is similar to git's approach to
28 branching.
28 branching.
29 '''
29 '''
30
30
31 from mercurial.i18n import _
31 from mercurial.i18n import _
32 from mercurial.node import nullid, nullrev, bin, hex, short
32 from mercurial.node import nullid, nullrev, bin, hex, short
33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
34 from mercurial import revset
34 from mercurial import revset
35 import os
35 import os
36
36
37 def write(repo):
37 def write(repo):
38 '''Write bookmarks
38 '''Write bookmarks
39
39
40 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 Write the given bookmark => hash dictionary to the .hg/bookmarks file
41 in a format equal to those of localtags.
41 in a format equal to those of localtags.
42
42
43 We also store a backup of the previous state in undo.bookmarks that
43 We also store a backup of the previous state in undo.bookmarks that
44 can be copied back on rollback.
44 can be copied back on rollback.
45 '''
45 '''
46 refs = repo._bookmarks
46 refs = repo._bookmarks
47 if os.path.exists(repo.join('bookmarks')):
47 if os.path.exists(repo.join('bookmarks')):
48 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
49 if repo._bookmarkcurrent not in refs:
49 if repo._bookmarkcurrent not in refs:
50 setcurrent(repo, None)
50 setcurrent(repo, None)
51 wlock = repo.wlock()
51 wlock = repo.wlock()
52 try:
52 try:
53 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 file = repo.opener('bookmarks', 'w', atomictemp=True)
54 for refspec, node in refs.iteritems():
54 for refspec, node in refs.iteritems():
55 file.write("%s %s\n" % (hex(node), refspec))
55 file.write("%s %s\n" % (hex(node), refspec))
56 file.rename()
56 file.rename()
57
57
58 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
58 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
59 try:
59 try:
60 os.utime(repo.sjoin('00changelog.i'), None)
60 os.utime(repo.sjoin('00changelog.i'), None)
61 except OSError:
61 except OSError:
62 pass
62 pass
63
63
64 finally:
64 finally:
65 wlock.release()
65 wlock.release()
66
66
67 def setcurrent(repo, mark):
67 def setcurrent(repo, mark):
68 '''Set the name of the bookmark that we are currently on
68 '''Set the name of the bookmark that we are currently on
69
69
70 Set the name of the bookmark that we are on (hg update <bookmark>).
70 Set the name of the bookmark that we are on (hg update <bookmark>).
71 The name is recorded in .hg/bookmarks.current
71 The name is recorded in .hg/bookmarks.current
72 '''
72 '''
73 current = repo._bookmarkcurrent
73 current = repo._bookmarkcurrent
74 if current == mark:
74 if current == mark:
75 return
75 return
76
76
77 refs = repo._bookmarks
77 refs = repo._bookmarks
78
78
79 # do not update if we do update to a rev equal to the current bookmark
79 # do not update if we do update to a rev equal to the current bookmark
80 if (mark and mark not in refs and
80 if (mark and mark not in refs and
81 current and refs[current] == repo.changectx('.').node()):
81 current and refs[current] == repo.changectx('.').node()):
82 return
82 return
83 if mark not in refs:
83 if mark not in refs:
84 mark = ''
84 mark = ''
85 wlock = repo.wlock()
85 wlock = repo.wlock()
86 try:
86 try:
87 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
87 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
88 file.write(mark)
88 file.write(mark)
89 file.rename()
89 file.rename()
90 finally:
90 finally:
91 wlock.release()
91 wlock.release()
92 repo._bookmarkcurrent = mark
92 repo._bookmarkcurrent = mark
93
93
94 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
94 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
95 '''track a line of development with movable markers
95 '''track a line of development with movable markers
96
96
97 Bookmarks are pointers to certain commits that move when
97 Bookmarks are pointers to certain commits that move when
98 committing. Bookmarks are local. They can be renamed, copied and
98 committing. Bookmarks are local. They can be renamed, copied and
99 deleted. It is possible to use bookmark names in :hg:`merge` and
99 deleted. It is possible to use bookmark names in :hg:`merge` and
100 :hg:`update` to merge and update respectively to a given bookmark.
100 :hg:`update` to merge and update respectively to a given bookmark.
101
101
102 You can use :hg:`bookmark NAME` to set a bookmark on the working
102 You can use :hg:`bookmark NAME` to set a bookmark on the working
103 directory's parent revision with the given name. If you specify
103 directory's parent revision with the given name. If you specify
104 a revision using -r REV (where REV may be an existing bookmark),
104 a revision using -r REV (where REV may be an existing bookmark),
105 the bookmark is assigned to that revision.
105 the bookmark is assigned to that revision.
106
106
107 Bookmarks can be pushed and pulled between repositories (see :hg:`help
107 Bookmarks can be pushed and pulled between repositories (see :hg:`help
108 push` and :hg:`help pull`). This requires the bookmark extension to be
108 push` and :hg:`help pull`). This requires the bookmark extension to be
109 enabled for both the local and remote repositories.
109 enabled for both the local and remote repositories.
110 '''
110 '''
111 hexfn = ui.debugflag and hex or short
111 hexfn = ui.debugflag and hex or short
112 marks = repo._bookmarks
112 marks = repo._bookmarks
113 cur = repo.changectx('.').node()
113 cur = repo.changectx('.').node()
114
114
115 if rename:
115 if rename:
116 if rename not in marks:
116 if rename not in marks:
117 raise util.Abort(_("a bookmark of this name does not exist"))
117 raise util.Abort(_("a bookmark of this name does not exist"))
118 if mark in marks and not force:
118 if mark in marks and not force:
119 raise util.Abort(_("a bookmark of the same name already exists"))
119 raise util.Abort(_("a bookmark of the same name already exists"))
120 if mark is None:
120 if mark is None:
121 raise util.Abort(_("new bookmark name required"))
121 raise util.Abort(_("new bookmark name required"))
122 marks[mark] = marks[rename]
122 marks[mark] = marks[rename]
123 del marks[rename]
123 del marks[rename]
124 if repo._bookmarkcurrent == rename:
124 if repo._bookmarkcurrent == rename:
125 setcurrent(repo, mark)
125 setcurrent(repo, mark)
126 write(repo)
126 write(repo)
127 return
127 return
128
128
129 if delete:
129 if delete:
130 if mark is None:
130 if mark is None:
131 raise util.Abort(_("bookmark name required"))
131 raise util.Abort(_("bookmark name required"))
132 if mark not in marks:
132 if mark not in marks:
133 raise util.Abort(_("a bookmark of this name does not exist"))
133 raise util.Abort(_("a bookmark of this name does not exist"))
134 if mark == repo._bookmarkcurrent:
134 if mark == repo._bookmarkcurrent:
135 setcurrent(repo, None)
135 setcurrent(repo, None)
136 del marks[mark]
136 del marks[mark]
137 write(repo)
137 write(repo)
138 return
138 return
139
139
140 if mark != None:
140 if mark != None:
141 if "\n" in mark:
141 if "\n" in mark:
142 raise util.Abort(_("bookmark name cannot contain newlines"))
142 raise util.Abort(_("bookmark name cannot contain newlines"))
143 mark = mark.strip()
143 mark = mark.strip()
144 if not mark:
144 if not mark:
145 raise util.Abort(_("bookmark names cannot consist entirely of "
145 raise util.Abort(_("bookmark names cannot consist entirely of "
146 "whitespace"))
146 "whitespace"))
147 if mark in marks and not force:
147 if mark in marks and not force:
148 raise util.Abort(_("a bookmark of the same name already exists"))
148 raise util.Abort(_("a bookmark of the same name already exists"))
149 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
149 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
150 and not force):
150 and not force):
151 raise util.Abort(
151 raise util.Abort(
152 _("a bookmark cannot have the name of an existing branch"))
152 _("a bookmark cannot have the name of an existing branch"))
153 if rev:
153 if rev:
154 marks[mark] = repo.lookup(rev)
154 marks[mark] = repo.lookup(rev)
155 else:
155 else:
156 marks[mark] = repo.changectx('.').node()
156 marks[mark] = repo.changectx('.').node()
157 setcurrent(repo, mark)
157 setcurrent(repo, mark)
158 write(repo)
158 write(repo)
159 return
159 return
160
160
161 if mark is None:
161 if mark is None:
162 if rev:
162 if rev:
163 raise util.Abort(_("bookmark name required"))
163 raise util.Abort(_("bookmark name required"))
164 if len(marks) == 0:
164 if len(marks) == 0:
165 ui.status(_("no bookmarks set\n"))
165 ui.status(_("no bookmarks set\n"))
166 else:
166 else:
167 for bmark, n in marks.iteritems():
167 for bmark, n in marks.iteritems():
168 if ui.configbool('bookmarks', 'track.current'):
168 if ui.configbool('bookmarks', 'track.current'):
169 current = repo._bookmarkcurrent
169 current = repo._bookmarkcurrent
170 if bmark == current and n == cur:
170 if bmark == current and n == cur:
171 prefix, label = '*', 'bookmarks.current'
171 prefix, label = '*', 'bookmarks.current'
172 else:
172 else:
173 prefix, label = ' ', ''
173 prefix, label = ' ', ''
174 else:
174 else:
175 if n == cur:
175 if n == cur:
176 prefix, label = '*', 'bookmarks.current'
176 prefix, label = '*', 'bookmarks.current'
177 else:
177 else:
178 prefix, label = ' ', ''
178 prefix, label = ' ', ''
179
179
180 if ui.quiet:
180 if ui.quiet:
181 ui.write("%s\n" % bmark, label=label)
181 ui.write("%s\n" % bmark, label=label)
182 else:
182 else:
183 ui.write(" %s %-25s %d:%s\n" % (
183 ui.write(" %s %-25s %d:%s\n" % (
184 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
184 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
185 label=label)
185 label=label)
186 return
186 return
187
187
188 def _revstostrip(changelog, node):
188 def _revstostrip(changelog, node):
189 srev = changelog.rev(node)
189 srev = changelog.rev(node)
190 tostrip = [srev]
190 tostrip = [srev]
191 saveheads = []
191 saveheads = []
192 for r in xrange(srev, len(changelog)):
192 for r in xrange(srev, len(changelog)):
193 parents = changelog.parentrevs(r)
193 parents = changelog.parentrevs(r)
194 if parents[0] in tostrip or parents[1] in tostrip:
194 if parents[0] in tostrip or parents[1] in tostrip:
195 tostrip.append(r)
195 tostrip.append(r)
196 if parents[1] != nullrev:
196 if parents[1] != nullrev:
197 for p in parents:
197 for p in parents:
198 if p not in tostrip and p > srev:
198 if p not in tostrip and p > srev:
199 saveheads.append(p)
199 saveheads.append(p)
200 return [r for r in tostrip if r not in saveheads]
200 return [r for r in tostrip if r not in saveheads]
201
201
202 def strip(oldstrip, ui, repo, node, backup="all"):
202 def strip(oldstrip, ui, repo, node, backup="all"):
203 """Strip bookmarks if revisions are stripped using
203 """Strip bookmarks if revisions are stripped using
204 the mercurial.strip method. This usually happens during
204 the mercurial.strip method. This usually happens during
205 qpush and qpop"""
205 qpush and qpop"""
206 revisions = _revstostrip(repo.changelog, node)
206 revisions = _revstostrip(repo.changelog, node)
207 marks = repo._bookmarks
207 marks = repo._bookmarks
208 update = []
208 update = []
209 for mark, n in marks.iteritems():
209 for mark, n in marks.iteritems():
210 if repo.changelog.rev(n) in revisions:
210 if repo.changelog.rev(n) in revisions:
211 update.append(mark)
211 update.append(mark)
212 oldstrip(ui, repo, node, backup)
212 oldstrip(ui, repo, node, backup)
213 if len(update) > 0:
213 if len(update) > 0:
214 for m in update:
214 for m in update:
215 marks[m] = repo.changectx('.').node()
215 marks[m] = repo.changectx('.').node()
216 write(repo)
216 write(repo)
217
217
218 def reposetup(ui, repo):
218 def reposetup(ui, repo):
219 if not repo.local():
219 if not repo.local():
220 return
220 return
221
221
222 class bookmark_repo(repo.__class__):
222 class bookmark_repo(repo.__class__):
223
223
224 @util.propertycache
224 @util.propertycache
225 def _bookmarks(self):
225 def _bookmarks(self):
226 '''Parse .hg/bookmarks file and return a dictionary
226 '''Parse .hg/bookmarks file and return a dictionary
227
227
228 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
228 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
229 in the .hg/bookmarks file.
229 in the .hg/bookmarks file.
230 Read the file and return a (name=>nodeid) dictionary
230 Read the file and return a (name=>nodeid) dictionary
231 '''
231 '''
232 try:
232 try:
233 bookmarks = {}
233 bookmarks = {}
234 for line in self.opener('bookmarks'):
234 for line in self.opener('bookmarks'):
235 sha, refspec = line.strip().split(' ', 1)
235 sha, refspec = line.strip().split(' ', 1)
236 bookmarks[refspec] = self.changelog.lookup(sha)
236 bookmarks[refspec] = self.changelog.lookup(sha)
237 except:
237 except:
238 pass
238 pass
239 return bookmarks
239 return bookmarks
240
240
241 @util.propertycache
241 @util.propertycache
242 def _bookmarkcurrent(self):
242 def _bookmarkcurrent(self):
243 '''Get the current bookmark
243 '''Get the current bookmark
244
244
245 If we use gittishsh branches we have a current bookmark that
245 If we use gittishsh branches we have a current bookmark that
246 we are on. This function returns the name of the bookmark. It
246 we are on. This function returns the name of the bookmark. It
247 is stored in .hg/bookmarks.current
247 is stored in .hg/bookmarks.current
248 '''
248 '''
249 mark = None
249 mark = None
250 if os.path.exists(self.join('bookmarks.current')):
250 if os.path.exists(self.join('bookmarks.current')):
251 file = self.opener('bookmarks.current')
251 file = self.opener('bookmarks.current')
252 # No readline() in posixfile_nt, reading everything is cheap
252 # No readline() in posixfile_nt, reading everything is cheap
253 mark = (file.readlines() or [''])[0]
253 mark = (file.readlines() or [''])[0]
254 if mark == '':
254 if mark == '':
255 mark = None
255 mark = None
256 file.close()
256 file.close()
257 return mark
257 return mark
258
258
259 def rollback(self, *args):
259 def rollback(self, *args):
260 if os.path.exists(self.join('undo.bookmarks')):
260 if os.path.exists(self.join('undo.bookmarks')):
261 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
261 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
262 return super(bookmark_repo, self).rollback(*args)
262 return super(bookmark_repo, self).rollback(*args)
263
263
264 def lookup(self, key):
264 def lookup(self, key):
265 if key in self._bookmarks:
265 if key in self._bookmarks:
266 key = self._bookmarks[key]
266 key = self._bookmarks[key]
267 return super(bookmark_repo, self).lookup(key)
267 return super(bookmark_repo, self).lookup(key)
268
268
269 def _bookmarksupdate(self, parents, node):
269 def _bookmarksupdate(self, parents, node):
270 marks = self._bookmarks
270 marks = self._bookmarks
271 update = False
271 update = False
272 if ui.configbool('bookmarks', 'track.current'):
272 if ui.configbool('bookmarks', 'track.current'):
273 mark = self._bookmarkcurrent
273 mark = self._bookmarkcurrent
274 if mark and marks[mark] in parents:
274 if mark and marks[mark] in parents:
275 marks[mark] = node
275 marks[mark] = node
276 update = True
276 update = True
277 else:
277 else:
278 for mark, n in marks.items():
278 for mark, n in marks.items():
279 if n in parents:
279 if n in parents:
280 marks[mark] = node
280 marks[mark] = node
281 update = True
281 update = True
282 if update:
282 if update:
283 write(self)
283 write(self)
284
284
285 def commitctx(self, ctx, error=False):
285 def commitctx(self, ctx, error=False):
286 """Add a revision to the repository and
286 """Add a revision to the repository and
287 move the bookmark"""
287 move the bookmark"""
288 wlock = self.wlock() # do both commit and bookmark with lock held
288 wlock = self.wlock() # do both commit and bookmark with lock held
289 try:
289 try:
290 node = super(bookmark_repo, self).commitctx(ctx, error)
290 node = super(bookmark_repo, self).commitctx(ctx, error)
291 if node is None:
291 if node is None:
292 return None
292 return None
293 parents = self.changelog.parents(node)
293 parents = self.changelog.parents(node)
294 if parents[1] == nullid:
294 if parents[1] == nullid:
295 parents = (parents[0],)
295 parents = (parents[0],)
296
296
297 self._bookmarksupdate(parents, node)
297 self._bookmarksupdate(parents, node)
298 return node
298 return node
299 finally:
299 finally:
300 wlock.release()
300 wlock.release()
301
301
302 def pull(self, remote, heads=None, force=False):
302 def pull(self, remote, heads=None, force=False):
303 result = super(bookmark_repo, self).pull(remote, heads, force)
303 result = super(bookmark_repo, self).pull(remote, heads, force)
304
304
305 self.ui.debug("checking for updated bookmarks\n")
305 self.ui.debug("checking for updated bookmarks\n")
306 rb = remote.listkeys('bookmarks')
306 rb = remote.listkeys('bookmarks')
307 changed = False
307 changed = False
308 for k in rb.keys():
308 for k in rb.keys():
309 if k in self._bookmarks:
309 if k in self._bookmarks:
310 nr, nl = rb[k], self._bookmarks[k]
310 nr, nl = rb[k], self._bookmarks[k]
311 if nr in self:
311 if nr in self:
312 cr = self[nr]
312 cr = self[nr]
313 cl = self[nl]
313 cl = self[nl]
314 if cl.rev() >= cr.rev():
314 if cl.rev() >= cr.rev():
315 continue
315 continue
316 if cr in cl.descendants():
316 if cr in cl.descendants():
317 self._bookmarks[k] = cr.node()
317 self._bookmarks[k] = cr.node()
318 changed = True
318 changed = True
319 self.ui.status(_("updating bookmark %s\n") % k)
319 self.ui.status(_("updating bookmark %s\n") % k)
320 else:
320 else:
321 self.ui.warn(_("not updating divergent"
321 self.ui.warn(_("not updating divergent"
322 " bookmark %s\n") % k)
322 " bookmark %s\n") % k)
323 if changed:
323 if changed:
324 write(repo)
324 write(repo)
325
325
326 return result
326 return result
327
327
328 def push(self, remote, force=False, revs=None, newbranch=False):
328 def push(self, remote, force=False, revs=None, newbranch=False):
329 result = super(bookmark_repo, self).push(remote, force, revs,
329 result = super(bookmark_repo, self).push(remote, force, revs,
330 newbranch)
330 newbranch)
331
331
332 self.ui.debug("checking for updated bookmarks\n")
332 self.ui.debug("checking for updated bookmarks\n")
333 rb = remote.listkeys('bookmarks')
333 rb = remote.listkeys('bookmarks')
334 for k in rb.keys():
334 for k in rb.keys():
335 if k in self._bookmarks:
335 if k in self._bookmarks:
336 nr, nl = rb[k], self._bookmarks[k]
336 nr, nl = rb[k], self._bookmarks[k]
337 if nr in self:
337 if nr in self:
338 cr = self[nr]
338 cr = self[nr]
339 cl = self[nl]
339 cl = self[nl]
340 if cl in cr.descendants():
340 if cl in cr.descendants():
341 r = remote.pushkey('bookmarks', k, nr, nl)
341 r = remote.pushkey('bookmarks', k, nr, nl)
342 if r:
342 if r:
343 self.ui.status(_("updating bookmark %s\n") % k)
343 self.ui.status(_("updating bookmark %s\n") % k)
344 else:
344 else:
345 self.ui.warn(_('updating bookmark %s'
345 self.ui.warn(_('updating bookmark %s'
346 ' failed!\n') % k)
346 ' failed!\n') % k)
347
347
348 return result
348 return result
349
349
350 def addchangegroup(self, *args, **kwargs):
350 def addchangegroup(self, *args, **kwargs):
351 parents = self.dirstate.parents()
351 parents = self.dirstate.parents()
352
352
353 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
353 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
354 if result > 1:
354 if result > 1:
355 # We have more heads than before
355 # We have more heads than before
356 return result
356 return result
357 node = self.changelog.tip()
357 node = self.changelog.tip()
358
358
359 self._bookmarksupdate(parents, node)
359 self._bookmarksupdate(parents, node)
360 return result
360 return result
361
361
362 def _findtags(self):
362 def _findtags(self):
363 """Merge bookmarks with normal tags"""
363 """Merge bookmarks with normal tags"""
364 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
364 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
365 tags.update(self._bookmarks)
365 tags.update(self._bookmarks)
366 return (tags, tagtypes)
366 return (tags, tagtypes)
367
367
368 if hasattr(repo, 'invalidate'):
368 if hasattr(repo, 'invalidate'):
369 def invalidate(self):
369 def invalidate(self):
370 super(bookmark_repo, self).invalidate()
370 super(bookmark_repo, self).invalidate()
371 for attr in ('_bookmarks', '_bookmarkcurrent'):
371 for attr in ('_bookmarks', '_bookmarkcurrent'):
372 if attr in self.__dict__:
372 if attr in self.__dict__:
373 delattr(self, attr)
373 delattr(self, attr)
374
374
375 repo.__class__ = bookmark_repo
375 repo.__class__ = bookmark_repo
376
376
377 def listbookmarks(repo):
377 def listbookmarks(repo):
378 # We may try to list bookmarks on a repo type that does not
378 # We may try to list bookmarks on a repo type that does not
379 # support it (e.g., statichttprepository).
379 # support it (e.g., statichttprepository).
380 if not hasattr(repo, '_bookmarks'):
380 if not hasattr(repo, '_bookmarks'):
381 return {}
381 return {}
382
382
383 d = {}
383 d = {}
384 for k, v in repo._bookmarks.iteritems():
384 for k, v in repo._bookmarks.iteritems():
385 d[k] = hex(v)
385 d[k] = hex(v)
386 return d
386 return d
387
387
388 def pushbookmark(repo, key, old, new):
388 def pushbookmark(repo, key, old, new):
389 w = repo.wlock()
389 w = repo.wlock()
390 try:
390 try:
391 marks = repo._bookmarks
391 marks = repo._bookmarks
392 if hex(marks.get(key, '')) != old:
392 if hex(marks.get(key, '')) != old:
393 return False
393 return False
394 if new == '':
394 if new == '':
395 del marks[key]
395 del marks[key]
396 else:
396 else:
397 if new not in repo:
397 if new not in repo:
398 return False
398 return False
399 marks[key] = repo[new].node()
399 marks[key] = repo[new].node()
400 write(repo)
400 write(repo)
401 return True
401 return True
402 finally:
402 finally:
403 w.release()
403 w.release()
404
404
405 def pull(oldpull, ui, repo, source="default", **opts):
405 def pull(oldpull, ui, repo, source="default", **opts):
406 # translate bookmark args to rev args for actual pull
406 # translate bookmark args to rev args for actual pull
407 if opts.get('bookmark'):
407 if opts.get('bookmark'):
408 # this is an unpleasant hack as pull will do this internally
408 # this is an unpleasant hack as pull will do this internally
409 source, branches = hg.parseurl(ui.expandpath(source),
409 source, branches = hg.parseurl(ui.expandpath(source),
410 opts.get('branch'))
410 opts.get('branch'))
411 other = hg.repository(hg.remoteui(repo, opts), source)
411 other = hg.repository(hg.remoteui(repo, opts), source)
412 rb = other.listkeys('bookmarks')
412 rb = other.listkeys('bookmarks')
413
413
414 for b in opts['bookmark']:
414 for b in opts['bookmark']:
415 if b not in rb:
415 if b not in rb:
416 raise util.Abort(_('remote bookmark %s not found!') % b)
416 raise util.Abort(_('remote bookmark %s not found!') % b)
417 opts.setdefault('rev', []).append(b)
417 opts.setdefault('rev', []).append(b)
418
418
419 result = oldpull(ui, repo, source, **opts)
419 result = oldpull(ui, repo, source, **opts)
420
420
421 # update specified bookmarks
421 # update specified bookmarks
422 if opts.get('bookmark'):
422 if opts.get('bookmark'):
423 for b in opts['bookmark']:
423 for b in opts['bookmark']:
424 # explicit pull overrides local bookmark if any
424 # explicit pull overrides local bookmark if any
425 ui.status(_("importing bookmark %s\n") % b)
425 ui.status(_("importing bookmark %s\n") % b)
426 repo._bookmarks[b] = repo[rb[b]].node()
426 repo._bookmarks[b] = repo[rb[b]].node()
427 write(repo)
427 write(repo)
428
428
429 return result
429 return result
430
430
431 def push(oldpush, ui, repo, dest=None, **opts):
431 def push(oldpush, ui, repo, dest=None, **opts):
432 dopush = True
432 dopush = True
433 if opts.get('bookmark'):
433 if opts.get('bookmark'):
434 dopush = False
434 dopush = False
435 for b in opts['bookmark']:
435 for b in opts['bookmark']:
436 if b in repo._bookmarks:
436 if b in repo._bookmarks:
437 dopush = True
437 dopush = True
438 opts.setdefault('rev', []).append(b)
438 opts.setdefault('rev', []).append(b)
439
439
440 result = 0
440 result = 0
441 if dopush:
441 if dopush:
442 result = oldpush(ui, repo, dest, **opts)
442 result = oldpush(ui, repo, dest, **opts)
443
443
444 if opts.get('bookmark'):
444 if opts.get('bookmark'):
445 # this is an unpleasant hack as push will do this internally
445 # this is an unpleasant hack as push will do this internally
446 dest = ui.expandpath(dest or 'default-push', dest or 'default')
446 dest = ui.expandpath(dest or 'default-push', dest or 'default')
447 dest, branches = hg.parseurl(dest, opts.get('branch'))
447 dest, branches = hg.parseurl(dest, opts.get('branch'))
448 other = hg.repository(hg.remoteui(repo, opts), dest)
448 other = hg.repository(hg.remoteui(repo, opts), dest)
449 rb = other.listkeys('bookmarks')
449 rb = other.listkeys('bookmarks')
450 for b in opts['bookmark']:
450 for b in opts['bookmark']:
451 # explicit push overrides remote bookmark if any
451 # explicit push overrides remote bookmark if any
452 if b in repo._bookmarks:
452 if b in repo._bookmarks:
453 ui.status(_("exporting bookmark %s\n") % b)
453 ui.status(_("exporting bookmark %s\n") % b)
454 new = repo[b].hex()
454 new = repo[b].hex()
455 elif b in rb:
455 elif b in rb:
456 ui.status(_("deleting remote bookmark %s\n") % b)
456 ui.status(_("deleting remote bookmark %s\n") % b)
457 new = '' # delete
457 new = '' # delete
458 else:
458 else:
459 ui.warn(_('bookmark %s does not exist on the local '
459 ui.warn(_('bookmark %s does not exist on the local '
460 'or remote repository!\n') % b)
460 'or remote repository!\n') % b)
461 return 2
461 return 2
462 old = rb.get(b, '')
462 old = rb.get(b, '')
463 r = other.pushkey('bookmarks', b, old, new)
463 r = other.pushkey('bookmarks', b, old, new)
464 if not r:
464 if not r:
465 ui.warn(_('updating bookmark %s failed!\n') % b)
465 ui.warn(_('updating bookmark %s failed!\n') % b)
466 if not result:
466 if not result:
467 result = 2
467 result = 2
468
468
469 return result
469 return result
470
470
471 def diffbookmarks(ui, repo, remote):
471 def diffbookmarks(ui, repo, remote):
472 ui.status(_("searching for changes\n"))
472 ui.status(_("searching for changes\n"))
473
473
474 lmarks = repo.listkeys('bookmarks')
474 lmarks = repo.listkeys('bookmarks')
475 rmarks = remote.listkeys('bookmarks')
475 rmarks = remote.listkeys('bookmarks')
476
476
477 diff = sorted(set(rmarks) - set(lmarks))
477 diff = sorted(set(rmarks) - set(lmarks))
478 for k in diff:
478 for k in diff:
479 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
479 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
480
480
481 if len(diff) <= 0:
481 if len(diff) <= 0:
482 ui.status(_("no changes found\n"))
482 ui.status(_("no changes found\n"))
483 return 1
483 return 1
484 return 0
484 return 0
485
485
486 def incoming(oldincoming, ui, repo, source="default", **opts):
486 def incoming(oldincoming, ui, repo, source="default", **opts):
487 if opts.get('bookmarks'):
487 if opts.get('bookmarks'):
488 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
488 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
489 other = hg.repository(hg.remoteui(repo, opts), source)
489 other = hg.repository(hg.remoteui(repo, opts), source)
490 ui.status(_('comparing with %s\n') % url.hidepassword(source))
490 ui.status(_('comparing with %s\n') % url.hidepassword(source))
491 return diffbookmarks(ui, repo, other)
491 return diffbookmarks(ui, repo, other)
492 else:
492 else:
493 return oldincoming(ui, repo, source, **opts)
493 return oldincoming(ui, repo, source, **opts)
494
494
495 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
495 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
496 if opts.get('bookmarks'):
496 if opts.get('bookmarks'):
497 dest = ui.expandpath(dest or 'default-push', dest or 'default')
497 dest = ui.expandpath(dest or 'default-push', dest or 'default')
498 dest, branches = hg.parseurl(dest, opts.get('branch'))
498 dest, branches = hg.parseurl(dest, opts.get('branch'))
499 other = hg.repository(hg.remoteui(repo, opts), dest)
499 other = hg.repository(hg.remoteui(repo, opts), dest)
500 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
500 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
501 return diffbookmarks(ui, other, repo)
501 return diffbookmarks(ui, other, repo)
502 else:
502 else:
503 return oldoutgoing(ui, repo, dest, **opts)
503 return oldoutgoing(ui, repo, dest, **opts)
504
504
505 def uisetup(ui):
505 def uisetup(ui):
506 extensions.wrapfunction(repair, "strip", strip)
506 extensions.wrapfunction(repair, "strip", strip)
507 if ui.configbool('bookmarks', 'track.current'):
507 if ui.configbool('bookmarks', 'track.current'):
508 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
508 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
509
509
510 entry = extensions.wrapcommand(commands.table, 'pull', pull)
510 entry = extensions.wrapcommand(commands.table, 'pull', pull)
511 entry[1].append(('B', 'bookmark', [],
511 entry[1].append(('B', 'bookmark', [],
512 _("bookmark to import"),
512 _("bookmark to import"),
513 _('BOOKMARK')))
513 _('BOOKMARK')))
514 entry = extensions.wrapcommand(commands.table, 'push', push)
514 entry = extensions.wrapcommand(commands.table, 'push', push)
515 entry[1].append(('B', 'bookmark', [],
515 entry[1].append(('B', 'bookmark', [],
516 _("bookmark to export"),
516 _("bookmark to export"),
517 _('BOOKMARK')))
517 _('BOOKMARK')))
518 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
518 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
519 entry[1].append(('B', 'bookmarks', False,
519 entry[1].append(('B', 'bookmarks', False,
520 _("compare bookmark")))
520 _("compare bookmark")))
521 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
521 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
522 entry[1].append(('B', 'bookmarks', False,
522 entry[1].append(('B', 'bookmarks', False,
523 _("compare bookmark")))
523 _("compare bookmark")))
524
524
525 pushkey.register('bookmarks', pushbookmark, listbookmarks)
525 pushkey.register('bookmarks', pushbookmark, listbookmarks)
526
526
527 def updatecurbookmark(orig, ui, repo, *args, **opts):
527 def updatecurbookmark(orig, ui, repo, *args, **opts):
528 '''Set the current bookmark
528 '''Set the current bookmark
529
529
530 If the user updates to a bookmark we update the .hg/bookmarks.current
530 If the user updates to a bookmark we update the .hg/bookmarks.current
531 file.
531 file.
532 '''
532 '''
533 res = orig(ui, repo, *args, **opts)
533 res = orig(ui, repo, *args, **opts)
534 rev = opts['rev']
534 rev = opts['rev']
535 if not rev and len(args) > 0:
535 if not rev and len(args) > 0:
536 rev = args[0]
536 rev = args[0]
537 setcurrent(repo, rev)
537 setcurrent(repo, rev)
538 return res
538 return res
539
539
540 def bmrevset(repo, subset, x):
540 def bmrevset(repo, subset, x):
541 """``bookmark([name])``
542 The named bookmark or all bookmarks.
543 """
541 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
544 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
542 if args:
545 if args:
543 bm = revset.getstring(args[0],
546 bm = revset.getstring(args[0],
544 _('the argument to bookmark must be a string'))
547 _('the argument to bookmark must be a string'))
545 bmrev = listbookmarks(repo).get(bm, None)
548 bmrev = listbookmarks(repo).get(bm, None)
546 if bmrev:
549 if bmrev:
547 bmrev = repo.changelog.rev(bin(bmrev))
550 bmrev = repo.changelog.rev(bin(bmrev))
548 return [r for r in subset if r == bmrev]
551 return [r for r in subset if r == bmrev]
549 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
552 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
550 return [r for r in subset if r in bms]
553 return [r for r in subset if r in bms]
554
555 def extsetup(ui):
551 revset.symbols['bookmark'] = bmrevset
556 revset.symbols['bookmark'] = bmrevset
552
557
553 def revsetdoc():
554 doc = help.loaddoc('revsets')()
555 doc += _('\nAdded by the bookmarks extension:\n\n'
556 '``bookmark([name])``\n'
557 ' The named bookmark or all bookmarks.\n')
558 return doc
559
560 cmdtable = {
558 cmdtable = {
561 "bookmarks":
559 "bookmarks":
562 (bookmark,
560 (bookmark,
563 [('f', 'force', False, _('force')),
561 [('f', 'force', False, _('force')),
564 ('r', 'rev', '', _('revision'), _('REV')),
562 ('r', 'rev', '', _('revision'), _('REV')),
565 ('d', 'delete', False, _('delete a given bookmark')),
563 ('d', 'delete', False, _('delete a given bookmark')),
566 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
564 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
567 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
565 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
568 }
566 }
569
567
570 colortable = {'bookmarks.current': 'green'}
568 colortable = {'bookmarks.current': 'green'}
@@ -1,634 +1,627 b''
1 # Patch transplanting extension for Mercurial
1 # Patch transplanting extension for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
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 '''command to transplant changesets from another branch
8 '''command to transplant changesets from another branch
9
9
10 This extension allows you to transplant patches from another branch.
10 This extension allows you to transplant patches from another branch.
11
11
12 Transplanted patches are recorded in .hg/transplant/transplants, as a
12 Transplanted patches are recorded in .hg/transplant/transplants, as a
13 map from a changeset hash to its hash in the source repository.
13 map from a changeset hash to its hash in the source repository.
14 '''
14 '''
15
15
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 import os, tempfile
17 import os, tempfile
18 from mercurial import bundlerepo, cmdutil, hg, merge, match
18 from mercurial import bundlerepo, cmdutil, hg, merge, match
19 from mercurial import patch, revlog, util, error
19 from mercurial import patch, revlog, util, error
20 from mercurial import revset, help
20 from mercurial import revset
21
21
22 class transplantentry(object):
22 class transplantentry(object):
23 def __init__(self, lnode, rnode):
23 def __init__(self, lnode, rnode):
24 self.lnode = lnode
24 self.lnode = lnode
25 self.rnode = rnode
25 self.rnode = rnode
26
26
27 class transplants(object):
27 class transplants(object):
28 def __init__(self, path=None, transplantfile=None, opener=None):
28 def __init__(self, path=None, transplantfile=None, opener=None):
29 self.path = path
29 self.path = path
30 self.transplantfile = transplantfile
30 self.transplantfile = transplantfile
31 self.opener = opener
31 self.opener = opener
32
32
33 if not opener:
33 if not opener:
34 self.opener = util.opener(self.path)
34 self.opener = util.opener(self.path)
35 self.transplants = {}
35 self.transplants = {}
36 self.dirty = False
36 self.dirty = False
37 self.read()
37 self.read()
38
38
39 def read(self):
39 def read(self):
40 abspath = os.path.join(self.path, self.transplantfile)
40 abspath = os.path.join(self.path, self.transplantfile)
41 if self.transplantfile and os.path.exists(abspath):
41 if self.transplantfile and os.path.exists(abspath):
42 for line in self.opener(self.transplantfile).read().splitlines():
42 for line in self.opener(self.transplantfile).read().splitlines():
43 lnode, rnode = map(revlog.bin, line.split(':'))
43 lnode, rnode = map(revlog.bin, line.split(':'))
44 list = self.transplants.setdefault(rnode, [])
44 list = self.transplants.setdefault(rnode, [])
45 list.append(transplantentry(lnode, rnode))
45 list.append(transplantentry(lnode, rnode))
46
46
47 def write(self):
47 def write(self):
48 if self.dirty and self.transplantfile:
48 if self.dirty and self.transplantfile:
49 if not os.path.isdir(self.path):
49 if not os.path.isdir(self.path):
50 os.mkdir(self.path)
50 os.mkdir(self.path)
51 fp = self.opener(self.transplantfile, 'w')
51 fp = self.opener(self.transplantfile, 'w')
52 for list in self.transplants.itervalues():
52 for list in self.transplants.itervalues():
53 for t in list:
53 for t in list:
54 l, r = map(revlog.hex, (t.lnode, t.rnode))
54 l, r = map(revlog.hex, (t.lnode, t.rnode))
55 fp.write(l + ':' + r + '\n')
55 fp.write(l + ':' + r + '\n')
56 fp.close()
56 fp.close()
57 self.dirty = False
57 self.dirty = False
58
58
59 def get(self, rnode):
59 def get(self, rnode):
60 return self.transplants.get(rnode) or []
60 return self.transplants.get(rnode) or []
61
61
62 def set(self, lnode, rnode):
62 def set(self, lnode, rnode):
63 list = self.transplants.setdefault(rnode, [])
63 list = self.transplants.setdefault(rnode, [])
64 list.append(transplantentry(lnode, rnode))
64 list.append(transplantentry(lnode, rnode))
65 self.dirty = True
65 self.dirty = True
66
66
67 def remove(self, transplant):
67 def remove(self, transplant):
68 list = self.transplants.get(transplant.rnode)
68 list = self.transplants.get(transplant.rnode)
69 if list:
69 if list:
70 del list[list.index(transplant)]
70 del list[list.index(transplant)]
71 self.dirty = True
71 self.dirty = True
72
72
73 class transplanter(object):
73 class transplanter(object):
74 def __init__(self, ui, repo):
74 def __init__(self, ui, repo):
75 self.ui = ui
75 self.ui = ui
76 self.path = repo.join('transplant')
76 self.path = repo.join('transplant')
77 self.opener = util.opener(self.path)
77 self.opener = util.opener(self.path)
78 self.transplants = transplants(self.path, 'transplants',
78 self.transplants = transplants(self.path, 'transplants',
79 opener=self.opener)
79 opener=self.opener)
80
80
81 def applied(self, repo, node, parent):
81 def applied(self, repo, node, parent):
82 '''returns True if a node is already an ancestor of parent
82 '''returns True if a node is already an ancestor of parent
83 or has already been transplanted'''
83 or has already been transplanted'''
84 if hasnode(repo, node):
84 if hasnode(repo, node):
85 if node in repo.changelog.reachable(parent, stop=node):
85 if node in repo.changelog.reachable(parent, stop=node):
86 return True
86 return True
87 for t in self.transplants.get(node):
87 for t in self.transplants.get(node):
88 # it might have been stripped
88 # it might have been stripped
89 if not hasnode(repo, t.lnode):
89 if not hasnode(repo, t.lnode):
90 self.transplants.remove(t)
90 self.transplants.remove(t)
91 return False
91 return False
92 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
92 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
93 return True
93 return True
94 return False
94 return False
95
95
96 def apply(self, repo, source, revmap, merges, opts={}):
96 def apply(self, repo, source, revmap, merges, opts={}):
97 '''apply the revisions in revmap one by one in revision order'''
97 '''apply the revisions in revmap one by one in revision order'''
98 revs = sorted(revmap)
98 revs = sorted(revmap)
99 p1, p2 = repo.dirstate.parents()
99 p1, p2 = repo.dirstate.parents()
100 pulls = []
100 pulls = []
101 diffopts = patch.diffopts(self.ui, opts)
101 diffopts = patch.diffopts(self.ui, opts)
102 diffopts.git = True
102 diffopts.git = True
103
103
104 lock = wlock = None
104 lock = wlock = None
105 try:
105 try:
106 wlock = repo.wlock()
106 wlock = repo.wlock()
107 lock = repo.lock()
107 lock = repo.lock()
108 for rev in revs:
108 for rev in revs:
109 node = revmap[rev]
109 node = revmap[rev]
110 revstr = '%s:%s' % (rev, revlog.short(node))
110 revstr = '%s:%s' % (rev, revlog.short(node))
111
111
112 if self.applied(repo, node, p1):
112 if self.applied(repo, node, p1):
113 self.ui.warn(_('skipping already applied revision %s\n') %
113 self.ui.warn(_('skipping already applied revision %s\n') %
114 revstr)
114 revstr)
115 continue
115 continue
116
116
117 parents = source.changelog.parents(node)
117 parents = source.changelog.parents(node)
118 if not opts.get('filter'):
118 if not opts.get('filter'):
119 # If the changeset parent is the same as the
119 # If the changeset parent is the same as the
120 # wdir's parent, just pull it.
120 # wdir's parent, just pull it.
121 if parents[0] == p1:
121 if parents[0] == p1:
122 pulls.append(node)
122 pulls.append(node)
123 p1 = node
123 p1 = node
124 continue
124 continue
125 if pulls:
125 if pulls:
126 if source != repo:
126 if source != repo:
127 repo.pull(source, heads=pulls)
127 repo.pull(source, heads=pulls)
128 merge.update(repo, pulls[-1], False, False, None)
128 merge.update(repo, pulls[-1], False, False, None)
129 p1, p2 = repo.dirstate.parents()
129 p1, p2 = repo.dirstate.parents()
130 pulls = []
130 pulls = []
131
131
132 domerge = False
132 domerge = False
133 if node in merges:
133 if node in merges:
134 # pulling all the merge revs at once would mean we
134 # pulling all the merge revs at once would mean we
135 # couldn't transplant after the latest even if
135 # couldn't transplant after the latest even if
136 # transplants before them fail.
136 # transplants before them fail.
137 domerge = True
137 domerge = True
138 if not hasnode(repo, node):
138 if not hasnode(repo, node):
139 repo.pull(source, heads=[node])
139 repo.pull(source, heads=[node])
140
140
141 if parents[1] != revlog.nullid:
141 if parents[1] != revlog.nullid:
142 self.ui.note(_('skipping merge changeset %s:%s\n')
142 self.ui.note(_('skipping merge changeset %s:%s\n')
143 % (rev, revlog.short(node)))
143 % (rev, revlog.short(node)))
144 patchfile = None
144 patchfile = None
145 else:
145 else:
146 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
146 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
147 fp = os.fdopen(fd, 'w')
147 fp = os.fdopen(fd, 'w')
148 gen = patch.diff(source, parents[0], node, opts=diffopts)
148 gen = patch.diff(source, parents[0], node, opts=diffopts)
149 for chunk in gen:
149 for chunk in gen:
150 fp.write(chunk)
150 fp.write(chunk)
151 fp.close()
151 fp.close()
152
152
153 del revmap[rev]
153 del revmap[rev]
154 if patchfile or domerge:
154 if patchfile or domerge:
155 try:
155 try:
156 n = self.applyone(repo, node,
156 n = self.applyone(repo, node,
157 source.changelog.read(node),
157 source.changelog.read(node),
158 patchfile, merge=domerge,
158 patchfile, merge=domerge,
159 log=opts.get('log'),
159 log=opts.get('log'),
160 filter=opts.get('filter'))
160 filter=opts.get('filter'))
161 if n and domerge:
161 if n and domerge:
162 self.ui.status(_('%s merged at %s\n') % (revstr,
162 self.ui.status(_('%s merged at %s\n') % (revstr,
163 revlog.short(n)))
163 revlog.short(n)))
164 elif n:
164 elif n:
165 self.ui.status(_('%s transplanted to %s\n')
165 self.ui.status(_('%s transplanted to %s\n')
166 % (revlog.short(node),
166 % (revlog.short(node),
167 revlog.short(n)))
167 revlog.short(n)))
168 finally:
168 finally:
169 if patchfile:
169 if patchfile:
170 os.unlink(patchfile)
170 os.unlink(patchfile)
171 if pulls:
171 if pulls:
172 repo.pull(source, heads=pulls)
172 repo.pull(source, heads=pulls)
173 merge.update(repo, pulls[-1], False, False, None)
173 merge.update(repo, pulls[-1], False, False, None)
174 finally:
174 finally:
175 self.saveseries(revmap, merges)
175 self.saveseries(revmap, merges)
176 self.transplants.write()
176 self.transplants.write()
177 lock.release()
177 lock.release()
178 wlock.release()
178 wlock.release()
179
179
180 def filter(self, filter, changelog, patchfile):
180 def filter(self, filter, changelog, patchfile):
181 '''arbitrarily rewrite changeset before applying it'''
181 '''arbitrarily rewrite changeset before applying it'''
182
182
183 self.ui.status(_('filtering %s\n') % patchfile)
183 self.ui.status(_('filtering %s\n') % patchfile)
184 user, date, msg = (changelog[1], changelog[2], changelog[4])
184 user, date, msg = (changelog[1], changelog[2], changelog[4])
185
185
186 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
186 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
187 fp = os.fdopen(fd, 'w')
187 fp = os.fdopen(fd, 'w')
188 fp.write("# HG changeset patch\n")
188 fp.write("# HG changeset patch\n")
189 fp.write("# User %s\n" % user)
189 fp.write("# User %s\n" % user)
190 fp.write("# Date %d %d\n" % date)
190 fp.write("# Date %d %d\n" % date)
191 fp.write(msg + '\n')
191 fp.write(msg + '\n')
192 fp.close()
192 fp.close()
193
193
194 try:
194 try:
195 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
195 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
196 util.shellquote(patchfile)),
196 util.shellquote(patchfile)),
197 environ={'HGUSER': changelog[1]},
197 environ={'HGUSER': changelog[1]},
198 onerr=util.Abort, errprefix=_('filter failed'))
198 onerr=util.Abort, errprefix=_('filter failed'))
199 user, date, msg = self.parselog(file(headerfile))[1:4]
199 user, date, msg = self.parselog(file(headerfile))[1:4]
200 finally:
200 finally:
201 os.unlink(headerfile)
201 os.unlink(headerfile)
202
202
203 return (user, date, msg)
203 return (user, date, msg)
204
204
205 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
205 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
206 filter=None):
206 filter=None):
207 '''apply the patch in patchfile to the repository as a transplant'''
207 '''apply the patch in patchfile to the repository as a transplant'''
208 (manifest, user, (time, timezone), files, message) = cl[:5]
208 (manifest, user, (time, timezone), files, message) = cl[:5]
209 date = "%d %d" % (time, timezone)
209 date = "%d %d" % (time, timezone)
210 extra = {'transplant_source': node}
210 extra = {'transplant_source': node}
211 if filter:
211 if filter:
212 (user, date, message) = self.filter(filter, cl, patchfile)
212 (user, date, message) = self.filter(filter, cl, patchfile)
213
213
214 if log:
214 if log:
215 # we don't translate messages inserted into commits
215 # we don't translate messages inserted into commits
216 message += '\n(transplanted from %s)' % revlog.hex(node)
216 message += '\n(transplanted from %s)' % revlog.hex(node)
217
217
218 self.ui.status(_('applying %s\n') % revlog.short(node))
218 self.ui.status(_('applying %s\n') % revlog.short(node))
219 self.ui.note('%s %s\n%s\n' % (user, date, message))
219 self.ui.note('%s %s\n%s\n' % (user, date, message))
220
220
221 if not patchfile and not merge:
221 if not patchfile and not merge:
222 raise util.Abort(_('can only omit patchfile if merging'))
222 raise util.Abort(_('can only omit patchfile if merging'))
223 if patchfile:
223 if patchfile:
224 try:
224 try:
225 files = {}
225 files = {}
226 try:
226 try:
227 patch.patch(patchfile, self.ui, cwd=repo.root,
227 patch.patch(patchfile, self.ui, cwd=repo.root,
228 files=files, eolmode=None)
228 files=files, eolmode=None)
229 if not files:
229 if not files:
230 self.ui.warn(_('%s: empty changeset')
230 self.ui.warn(_('%s: empty changeset')
231 % revlog.hex(node))
231 % revlog.hex(node))
232 return None
232 return None
233 finally:
233 finally:
234 files = cmdutil.updatedir(self.ui, repo, files)
234 files = cmdutil.updatedir(self.ui, repo, files)
235 except Exception, inst:
235 except Exception, inst:
236 seriespath = os.path.join(self.path, 'series')
236 seriespath = os.path.join(self.path, 'series')
237 if os.path.exists(seriespath):
237 if os.path.exists(seriespath):
238 os.unlink(seriespath)
238 os.unlink(seriespath)
239 p1 = repo.dirstate.parents()[0]
239 p1 = repo.dirstate.parents()[0]
240 p2 = node
240 p2 = node
241 self.log(user, date, message, p1, p2, merge=merge)
241 self.log(user, date, message, p1, p2, merge=merge)
242 self.ui.write(str(inst) + '\n')
242 self.ui.write(str(inst) + '\n')
243 raise util.Abort(_('fix up the merge and run '
243 raise util.Abort(_('fix up the merge and run '
244 'hg transplant --continue'))
244 'hg transplant --continue'))
245 else:
245 else:
246 files = None
246 files = None
247 if merge:
247 if merge:
248 p1, p2 = repo.dirstate.parents()
248 p1, p2 = repo.dirstate.parents()
249 repo.dirstate.setparents(p1, node)
249 repo.dirstate.setparents(p1, node)
250 m = match.always(repo.root, '')
250 m = match.always(repo.root, '')
251 else:
251 else:
252 m = match.exact(repo.root, '', files)
252 m = match.exact(repo.root, '', files)
253
253
254 n = repo.commit(message, user, date, extra=extra, match=m)
254 n = repo.commit(message, user, date, extra=extra, match=m)
255 if not n:
255 if not n:
256 # Crash here to prevent an unclear crash later, in
256 # Crash here to prevent an unclear crash later, in
257 # transplants.write(). This can happen if patch.patch()
257 # transplants.write(). This can happen if patch.patch()
258 # does nothing but claims success or if repo.status() fails
258 # does nothing but claims success or if repo.status() fails
259 # to report changes done by patch.patch(). These both
259 # to report changes done by patch.patch(). These both
260 # appear to be bugs in other parts of Mercurial, but dying
260 # appear to be bugs in other parts of Mercurial, but dying
261 # here, as soon as we can detect the problem, is preferable
261 # here, as soon as we can detect the problem, is preferable
262 # to silently dropping changesets on the floor.
262 # to silently dropping changesets on the floor.
263 raise RuntimeError('nothing committed after transplant')
263 raise RuntimeError('nothing committed after transplant')
264 if not merge:
264 if not merge:
265 self.transplants.set(n, node)
265 self.transplants.set(n, node)
266
266
267 return n
267 return n
268
268
269 def resume(self, repo, source, opts=None):
269 def resume(self, repo, source, opts=None):
270 '''recover last transaction and apply remaining changesets'''
270 '''recover last transaction and apply remaining changesets'''
271 if os.path.exists(os.path.join(self.path, 'journal')):
271 if os.path.exists(os.path.join(self.path, 'journal')):
272 n, node = self.recover(repo)
272 n, node = self.recover(repo)
273 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
273 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
274 revlog.short(n)))
274 revlog.short(n)))
275 seriespath = os.path.join(self.path, 'series')
275 seriespath = os.path.join(self.path, 'series')
276 if not os.path.exists(seriespath):
276 if not os.path.exists(seriespath):
277 self.transplants.write()
277 self.transplants.write()
278 return
278 return
279 nodes, merges = self.readseries()
279 nodes, merges = self.readseries()
280 revmap = {}
280 revmap = {}
281 for n in nodes:
281 for n in nodes:
282 revmap[source.changelog.rev(n)] = n
282 revmap[source.changelog.rev(n)] = n
283 os.unlink(seriespath)
283 os.unlink(seriespath)
284
284
285 self.apply(repo, source, revmap, merges, opts)
285 self.apply(repo, source, revmap, merges, opts)
286
286
287 def recover(self, repo):
287 def recover(self, repo):
288 '''commit working directory using journal metadata'''
288 '''commit working directory using journal metadata'''
289 node, user, date, message, parents = self.readlog()
289 node, user, date, message, parents = self.readlog()
290 merge = len(parents) == 2
290 merge = len(parents) == 2
291
291
292 if not user or not date or not message or not parents[0]:
292 if not user or not date or not message or not parents[0]:
293 raise util.Abort(_('transplant log file is corrupt'))
293 raise util.Abort(_('transplant log file is corrupt'))
294
294
295 extra = {'transplant_source': node}
295 extra = {'transplant_source': node}
296 wlock = repo.wlock()
296 wlock = repo.wlock()
297 try:
297 try:
298 p1, p2 = repo.dirstate.parents()
298 p1, p2 = repo.dirstate.parents()
299 if p1 != parents[0]:
299 if p1 != parents[0]:
300 raise util.Abort(
300 raise util.Abort(
301 _('working dir not at transplant parent %s') %
301 _('working dir not at transplant parent %s') %
302 revlog.hex(parents[0]))
302 revlog.hex(parents[0]))
303 if merge:
303 if merge:
304 repo.dirstate.setparents(p1, parents[1])
304 repo.dirstate.setparents(p1, parents[1])
305 n = repo.commit(message, user, date, extra=extra)
305 n = repo.commit(message, user, date, extra=extra)
306 if not n:
306 if not n:
307 raise util.Abort(_('commit failed'))
307 raise util.Abort(_('commit failed'))
308 if not merge:
308 if not merge:
309 self.transplants.set(n, node)
309 self.transplants.set(n, node)
310 self.unlog()
310 self.unlog()
311
311
312 return n, node
312 return n, node
313 finally:
313 finally:
314 wlock.release()
314 wlock.release()
315
315
316 def readseries(self):
316 def readseries(self):
317 nodes = []
317 nodes = []
318 merges = []
318 merges = []
319 cur = nodes
319 cur = nodes
320 for line in self.opener('series').read().splitlines():
320 for line in self.opener('series').read().splitlines():
321 if line.startswith('# Merges'):
321 if line.startswith('# Merges'):
322 cur = merges
322 cur = merges
323 continue
323 continue
324 cur.append(revlog.bin(line))
324 cur.append(revlog.bin(line))
325
325
326 return (nodes, merges)
326 return (nodes, merges)
327
327
328 def saveseries(self, revmap, merges):
328 def saveseries(self, revmap, merges):
329 if not revmap:
329 if not revmap:
330 return
330 return
331
331
332 if not os.path.isdir(self.path):
332 if not os.path.isdir(self.path):
333 os.mkdir(self.path)
333 os.mkdir(self.path)
334 series = self.opener('series', 'w')
334 series = self.opener('series', 'w')
335 for rev in sorted(revmap):
335 for rev in sorted(revmap):
336 series.write(revlog.hex(revmap[rev]) + '\n')
336 series.write(revlog.hex(revmap[rev]) + '\n')
337 if merges:
337 if merges:
338 series.write('# Merges\n')
338 series.write('# Merges\n')
339 for m in merges:
339 for m in merges:
340 series.write(revlog.hex(m) + '\n')
340 series.write(revlog.hex(m) + '\n')
341 series.close()
341 series.close()
342
342
343 def parselog(self, fp):
343 def parselog(self, fp):
344 parents = []
344 parents = []
345 message = []
345 message = []
346 node = revlog.nullid
346 node = revlog.nullid
347 inmsg = False
347 inmsg = False
348 for line in fp.read().splitlines():
348 for line in fp.read().splitlines():
349 if inmsg:
349 if inmsg:
350 message.append(line)
350 message.append(line)
351 elif line.startswith('# User '):
351 elif line.startswith('# User '):
352 user = line[7:]
352 user = line[7:]
353 elif line.startswith('# Date '):
353 elif line.startswith('# Date '):
354 date = line[7:]
354 date = line[7:]
355 elif line.startswith('# Node ID '):
355 elif line.startswith('# Node ID '):
356 node = revlog.bin(line[10:])
356 node = revlog.bin(line[10:])
357 elif line.startswith('# Parent '):
357 elif line.startswith('# Parent '):
358 parents.append(revlog.bin(line[9:]))
358 parents.append(revlog.bin(line[9:]))
359 elif not line.startswith('# '):
359 elif not line.startswith('# '):
360 inmsg = True
360 inmsg = True
361 message.append(line)
361 message.append(line)
362 return (node, user, date, '\n'.join(message), parents)
362 return (node, user, date, '\n'.join(message), parents)
363
363
364 def log(self, user, date, message, p1, p2, merge=False):
364 def log(self, user, date, message, p1, p2, merge=False):
365 '''journal changelog metadata for later recover'''
365 '''journal changelog metadata for later recover'''
366
366
367 if not os.path.isdir(self.path):
367 if not os.path.isdir(self.path):
368 os.mkdir(self.path)
368 os.mkdir(self.path)
369 fp = self.opener('journal', 'w')
369 fp = self.opener('journal', 'w')
370 fp.write('# User %s\n' % user)
370 fp.write('# User %s\n' % user)
371 fp.write('# Date %s\n' % date)
371 fp.write('# Date %s\n' % date)
372 fp.write('# Node ID %s\n' % revlog.hex(p2))
372 fp.write('# Node ID %s\n' % revlog.hex(p2))
373 fp.write('# Parent ' + revlog.hex(p1) + '\n')
373 fp.write('# Parent ' + revlog.hex(p1) + '\n')
374 if merge:
374 if merge:
375 fp.write('# Parent ' + revlog.hex(p2) + '\n')
375 fp.write('# Parent ' + revlog.hex(p2) + '\n')
376 fp.write(message.rstrip() + '\n')
376 fp.write(message.rstrip() + '\n')
377 fp.close()
377 fp.close()
378
378
379 def readlog(self):
379 def readlog(self):
380 return self.parselog(self.opener('journal'))
380 return self.parselog(self.opener('journal'))
381
381
382 def unlog(self):
382 def unlog(self):
383 '''remove changelog journal'''
383 '''remove changelog journal'''
384 absdst = os.path.join(self.path, 'journal')
384 absdst = os.path.join(self.path, 'journal')
385 if os.path.exists(absdst):
385 if os.path.exists(absdst):
386 os.unlink(absdst)
386 os.unlink(absdst)
387
387
388 def transplantfilter(self, repo, source, root):
388 def transplantfilter(self, repo, source, root):
389 def matchfn(node):
389 def matchfn(node):
390 if self.applied(repo, node, root):
390 if self.applied(repo, node, root):
391 return False
391 return False
392 if source.changelog.parents(node)[1] != revlog.nullid:
392 if source.changelog.parents(node)[1] != revlog.nullid:
393 return False
393 return False
394 extra = source.changelog.read(node)[5]
394 extra = source.changelog.read(node)[5]
395 cnode = extra.get('transplant_source')
395 cnode = extra.get('transplant_source')
396 if cnode and self.applied(repo, cnode, root):
396 if cnode and self.applied(repo, cnode, root):
397 return False
397 return False
398 return True
398 return True
399
399
400 return matchfn
400 return matchfn
401
401
402 def hasnode(repo, node):
402 def hasnode(repo, node):
403 try:
403 try:
404 return repo.changelog.rev(node) != None
404 return repo.changelog.rev(node) != None
405 except error.RevlogError:
405 except error.RevlogError:
406 return False
406 return False
407
407
408 def browserevs(ui, repo, nodes, opts):
408 def browserevs(ui, repo, nodes, opts):
409 '''interactively transplant changesets'''
409 '''interactively transplant changesets'''
410 def browsehelp(ui):
410 def browsehelp(ui):
411 ui.write(_('y: transplant this changeset\n'
411 ui.write(_('y: transplant this changeset\n'
412 'n: skip this changeset\n'
412 'n: skip this changeset\n'
413 'm: merge at this changeset\n'
413 'm: merge at this changeset\n'
414 'p: show patch\n'
414 'p: show patch\n'
415 'c: commit selected changesets\n'
415 'c: commit selected changesets\n'
416 'q: cancel transplant\n'
416 'q: cancel transplant\n'
417 '?: show this help\n'))
417 '?: show this help\n'))
418
418
419 displayer = cmdutil.show_changeset(ui, repo, opts)
419 displayer = cmdutil.show_changeset(ui, repo, opts)
420 transplants = []
420 transplants = []
421 merges = []
421 merges = []
422 for node in nodes:
422 for node in nodes:
423 displayer.show(repo[node])
423 displayer.show(repo[node])
424 action = None
424 action = None
425 while not action:
425 while not action:
426 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
426 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
427 if action == '?':
427 if action == '?':
428 browsehelp(ui)
428 browsehelp(ui)
429 action = None
429 action = None
430 elif action == 'p':
430 elif action == 'p':
431 parent = repo.changelog.parents(node)[0]
431 parent = repo.changelog.parents(node)[0]
432 for chunk in patch.diff(repo, parent, node):
432 for chunk in patch.diff(repo, parent, node):
433 ui.write(chunk)
433 ui.write(chunk)
434 action = None
434 action = None
435 elif action not in ('y', 'n', 'm', 'c', 'q'):
435 elif action not in ('y', 'n', 'm', 'c', 'q'):
436 ui.write(_('no such option\n'))
436 ui.write(_('no such option\n'))
437 action = None
437 action = None
438 if action == 'y':
438 if action == 'y':
439 transplants.append(node)
439 transplants.append(node)
440 elif action == 'm':
440 elif action == 'm':
441 merges.append(node)
441 merges.append(node)
442 elif action == 'c':
442 elif action == 'c':
443 break
443 break
444 elif action == 'q':
444 elif action == 'q':
445 transplants = ()
445 transplants = ()
446 merges = ()
446 merges = ()
447 break
447 break
448 displayer.close()
448 displayer.close()
449 return (transplants, merges)
449 return (transplants, merges)
450
450
451 def transplant(ui, repo, *revs, **opts):
451 def transplant(ui, repo, *revs, **opts):
452 '''transplant changesets from another branch
452 '''transplant changesets from another branch
453
453
454 Selected changesets will be applied on top of the current working
454 Selected changesets will be applied on top of the current working
455 directory with the log of the original changeset. If --log is
455 directory with the log of the original changeset. If --log is
456 specified, log messages will have a comment appended of the form::
456 specified, log messages will have a comment appended of the form::
457
457
458 (transplanted from CHANGESETHASH)
458 (transplanted from CHANGESETHASH)
459
459
460 You can rewrite the changelog message with the --filter option.
460 You can rewrite the changelog message with the --filter option.
461 Its argument will be invoked with the current changelog message as
461 Its argument will be invoked with the current changelog message as
462 $1 and the patch as $2.
462 $1 and the patch as $2.
463
463
464 If --source/-s is specified, selects changesets from the named
464 If --source/-s is specified, selects changesets from the named
465 repository. If --branch/-b is specified, selects changesets from
465 repository. If --branch/-b is specified, selects changesets from
466 the branch holding the named revision, up to that revision. If
466 the branch holding the named revision, up to that revision. If
467 --all/-a is specified, all changesets on the branch will be
467 --all/-a is specified, all changesets on the branch will be
468 transplanted, otherwise you will be prompted to select the
468 transplanted, otherwise you will be prompted to select the
469 changesets you want.
469 changesets you want.
470
470
471 :hg:`transplant --branch REVISION --all` will rebase the selected
471 :hg:`transplant --branch REVISION --all` will rebase the selected
472 branch (up to the named revision) onto your current working
472 branch (up to the named revision) onto your current working
473 directory.
473 directory.
474
474
475 You can optionally mark selected transplanted changesets as merge
475 You can optionally mark selected transplanted changesets as merge
476 changesets. You will not be prompted to transplant any ancestors
476 changesets. You will not be prompted to transplant any ancestors
477 of a merged transplant, and you can merge descendants of them
477 of a merged transplant, and you can merge descendants of them
478 normally instead of transplanting them.
478 normally instead of transplanting them.
479
479
480 If no merges or revisions are provided, :hg:`transplant` will
480 If no merges or revisions are provided, :hg:`transplant` will
481 start an interactive changeset browser.
481 start an interactive changeset browser.
482
482
483 If a changeset application fails, you can fix the merge by hand
483 If a changeset application fails, you can fix the merge by hand
484 and then resume where you left off by calling :hg:`transplant
484 and then resume where you left off by calling :hg:`transplant
485 --continue/-c`.
485 --continue/-c`.
486 '''
486 '''
487 def incwalk(repo, incoming, branches, match=util.always):
487 def incwalk(repo, incoming, branches, match=util.always):
488 if not branches:
488 if not branches:
489 branches = None
489 branches = None
490 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
490 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
491 if match(node):
491 if match(node):
492 yield node
492 yield node
493
493
494 def transplantwalk(repo, root, branches, match=util.always):
494 def transplantwalk(repo, root, branches, match=util.always):
495 if not branches:
495 if not branches:
496 branches = repo.heads()
496 branches = repo.heads()
497 ancestors = []
497 ancestors = []
498 for branch in branches:
498 for branch in branches:
499 ancestors.append(repo.changelog.ancestor(root, branch))
499 ancestors.append(repo.changelog.ancestor(root, branch))
500 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
500 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
501 if match(node):
501 if match(node):
502 yield node
502 yield node
503
503
504 def checkopts(opts, revs):
504 def checkopts(opts, revs):
505 if opts.get('continue'):
505 if opts.get('continue'):
506 if opts.get('branch') or opts.get('all') or opts.get('merge'):
506 if opts.get('branch') or opts.get('all') or opts.get('merge'):
507 raise util.Abort(_('--continue is incompatible with '
507 raise util.Abort(_('--continue is incompatible with '
508 'branch, all or merge'))
508 'branch, all or merge'))
509 return
509 return
510 if not (opts.get('source') or revs or
510 if not (opts.get('source') or revs or
511 opts.get('merge') or opts.get('branch')):
511 opts.get('merge') or opts.get('branch')):
512 raise util.Abort(_('no source URL, branch tag or revision '
512 raise util.Abort(_('no source URL, branch tag or revision '
513 'list provided'))
513 'list provided'))
514 if opts.get('all'):
514 if opts.get('all'):
515 if not opts.get('branch'):
515 if not opts.get('branch'):
516 raise util.Abort(_('--all requires a branch revision'))
516 raise util.Abort(_('--all requires a branch revision'))
517 if revs:
517 if revs:
518 raise util.Abort(_('--all is incompatible with a '
518 raise util.Abort(_('--all is incompatible with a '
519 'revision list'))
519 'revision list'))
520
520
521 checkopts(opts, revs)
521 checkopts(opts, revs)
522
522
523 if not opts.get('log'):
523 if not opts.get('log'):
524 opts['log'] = ui.config('transplant', 'log')
524 opts['log'] = ui.config('transplant', 'log')
525 if not opts.get('filter'):
525 if not opts.get('filter'):
526 opts['filter'] = ui.config('transplant', 'filter')
526 opts['filter'] = ui.config('transplant', 'filter')
527
527
528 tp = transplanter(ui, repo)
528 tp = transplanter(ui, repo)
529
529
530 p1, p2 = repo.dirstate.parents()
530 p1, p2 = repo.dirstate.parents()
531 if len(repo) > 0 and p1 == revlog.nullid:
531 if len(repo) > 0 and p1 == revlog.nullid:
532 raise util.Abort(_('no revision checked out'))
532 raise util.Abort(_('no revision checked out'))
533 if not opts.get('continue'):
533 if not opts.get('continue'):
534 if p2 != revlog.nullid:
534 if p2 != revlog.nullid:
535 raise util.Abort(_('outstanding uncommitted merges'))
535 raise util.Abort(_('outstanding uncommitted merges'))
536 m, a, r, d = repo.status()[:4]
536 m, a, r, d = repo.status()[:4]
537 if m or a or r or d:
537 if m or a or r or d:
538 raise util.Abort(_('outstanding local changes'))
538 raise util.Abort(_('outstanding local changes'))
539
539
540 bundle = None
540 bundle = None
541 source = opts.get('source')
541 source = opts.get('source')
542 if source:
542 if source:
543 sourcerepo = ui.expandpath(source)
543 sourcerepo = ui.expandpath(source)
544 source = hg.repository(ui, sourcerepo)
544 source = hg.repository(ui, sourcerepo)
545 source, incoming, bundle = bundlerepo.getremotechanges(ui, repo, source,
545 source, incoming, bundle = bundlerepo.getremotechanges(ui, repo, source,
546 force=True)
546 force=True)
547 else:
547 else:
548 source = repo
548 source = repo
549
549
550 try:
550 try:
551 if opts.get('continue'):
551 if opts.get('continue'):
552 tp.resume(repo, source, opts)
552 tp.resume(repo, source, opts)
553 return
553 return
554
554
555 tf = tp.transplantfilter(repo, source, p1)
555 tf = tp.transplantfilter(repo, source, p1)
556 if opts.get('prune'):
556 if opts.get('prune'):
557 prune = [source.lookup(r)
557 prune = [source.lookup(r)
558 for r in cmdutil.revrange(source, opts.get('prune'))]
558 for r in cmdutil.revrange(source, opts.get('prune'))]
559 matchfn = lambda x: tf(x) and x not in prune
559 matchfn = lambda x: tf(x) and x not in prune
560 else:
560 else:
561 matchfn = tf
561 matchfn = tf
562 branches = map(source.lookup, opts.get('branch', ()))
562 branches = map(source.lookup, opts.get('branch', ()))
563 merges = map(source.lookup, opts.get('merge', ()))
563 merges = map(source.lookup, opts.get('merge', ()))
564 revmap = {}
564 revmap = {}
565 if revs:
565 if revs:
566 for r in cmdutil.revrange(source, revs):
566 for r in cmdutil.revrange(source, revs):
567 revmap[int(r)] = source.lookup(r)
567 revmap[int(r)] = source.lookup(r)
568 elif opts.get('all') or not merges:
568 elif opts.get('all') or not merges:
569 if source != repo:
569 if source != repo:
570 alltransplants = incwalk(source, incoming, branches,
570 alltransplants = incwalk(source, incoming, branches,
571 match=matchfn)
571 match=matchfn)
572 else:
572 else:
573 alltransplants = transplantwalk(source, p1, branches,
573 alltransplants = transplantwalk(source, p1, branches,
574 match=matchfn)
574 match=matchfn)
575 if opts.get('all'):
575 if opts.get('all'):
576 revs = alltransplants
576 revs = alltransplants
577 else:
577 else:
578 revs, newmerges = browserevs(ui, source, alltransplants, opts)
578 revs, newmerges = browserevs(ui, source, alltransplants, opts)
579 merges.extend(newmerges)
579 merges.extend(newmerges)
580 for r in revs:
580 for r in revs:
581 revmap[source.changelog.rev(r)] = r
581 revmap[source.changelog.rev(r)] = r
582 for r in merges:
582 for r in merges:
583 revmap[source.changelog.rev(r)] = r
583 revmap[source.changelog.rev(r)] = r
584
584
585 tp.apply(repo, source, revmap, merges, opts)
585 tp.apply(repo, source, revmap, merges, opts)
586 finally:
586 finally:
587 if bundle:
587 if bundle:
588 source.close()
588 source.close()
589 os.unlink(bundle)
589 os.unlink(bundle)
590
590
591 def revsettransplanted(repo, subset, x):
591 def revsettransplanted(repo, subset, x):
592 """``transplanted(set)``
593 Transplanted changesets in set.
594 """
592 if x:
595 if x:
593 s = revset.getset(repo, subset, x)
596 s = revset.getset(repo, subset, x)
594 else:
597 else:
595 s = subset
598 s = subset
596 cs = set()
599 cs = set()
597 for r in xrange(0, len(repo)):
600 for r in xrange(0, len(repo)):
598 if repo[r].extra().get('transplant_source'):
601 if repo[r].extra().get('transplant_source'):
599 cs.add(r)
602 cs.add(r)
600 return [r for r in s if r in cs]
603 return [r for r in s if r in cs]
601
604
602 def revsetdoc():
605 def extsetup(ui):
603 doc = help.loaddoc('revsets')()
604 doc += _('\nAdded by the transplant extension:\n\n'
605 '``transplanted(set)``\n'
606 ' Transplanted changesets in set.\n')
607 return doc
608
609 def uisetup(ui):
610 'Add the transplanted revset predicate'
611 for i in (i for i, x in enumerate(help.helptable) if x[0] == ['revsets']):
612 help.helptable[i] = (['revsets'], _("Specifying Revision Sets"), revsetdoc)
613 revset.symbols['transplanted'] = revsettransplanted
606 revset.symbols['transplanted'] = revsettransplanted
614
607
615 cmdtable = {
608 cmdtable = {
616 "transplant":
609 "transplant":
617 (transplant,
610 (transplant,
618 [('s', 'source', '',
611 [('s', 'source', '',
619 _('pull patches from REPO'), _('REPO')),
612 _('pull patches from REPO'), _('REPO')),
620 ('b', 'branch', [],
613 ('b', 'branch', [],
621 _('pull patches from branch BRANCH'), _('BRANCH')),
614 _('pull patches from branch BRANCH'), _('BRANCH')),
622 ('a', 'all', None, _('pull all changesets up to BRANCH')),
615 ('a', 'all', None, _('pull all changesets up to BRANCH')),
623 ('p', 'prune', [],
616 ('p', 'prune', [],
624 _('skip over REV'), _('REV')),
617 _('skip over REV'), _('REV')),
625 ('m', 'merge', [],
618 ('m', 'merge', [],
626 _('merge at REV'), _('REV')),
619 _('merge at REV'), _('REV')),
627 ('', 'log', None, _('append transplant info to log message')),
620 ('', 'log', None, _('append transplant info to log message')),
628 ('c', 'continue', None, _('continue last transplant session '
621 ('c', 'continue', None, _('continue last transplant session '
629 'after repair')),
622 'after repair')),
630 ('', 'filter', '',
623 ('', 'filter', '',
631 _('filter changesets through command'), _('CMD'))],
624 _('filter changesets through command'), _('CMD'))],
632 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
625 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
633 '[-m REV] [REV]...'))
626 '[-m REV] [REV]...'))
634 }
627 }
@@ -1,211 +1,213 b''
1 $ echo "[extensions]" >> $HGRCPATH
1 $ echo "[extensions]" >> $HGRCPATH
2 $ echo "bookmarks=" >> $HGRCPATH
2 $ echo "bookmarks=" >> $HGRCPATH
3
3
4 $ hg init
4 $ hg init
5
5
6 no bookmarks
6 no bookmarks
7
7
8 $ hg bookmarks
8 $ hg bookmarks
9 no bookmarks set
9 no bookmarks set
10
10
11 bookmark rev -1
11 bookmark rev -1
12
12
13 $ hg bookmark X
13 $ hg bookmark X
14
14
15 list bookmarks
15 list bookmarks
16
16
17 $ hg bookmarks
17 $ hg bookmarks
18 * X -1:000000000000
18 * X -1:000000000000
19
19
20 list bookmarks with color
20 list bookmarks with color
21
21
22 $ hg --config extensions.color= --config color.mode=ansi \
22 $ hg --config extensions.color= --config color.mode=ansi \
23 > bookmarks --color=always
23 > bookmarks --color=always
24  * X -1:000000000000
24  * X -1:000000000000
25
25
26 $ echo a > a
26 $ echo a > a
27 $ hg add a
27 $ hg add a
28 $ hg commit -m 0
28 $ hg commit -m 0
29
29
30 bookmark X moved to rev 0
30 bookmark X moved to rev 0
31
31
32 $ hg bookmarks
32 $ hg bookmarks
33 * X 0:f7b1eb17ad24
33 * X 0:f7b1eb17ad24
34
34
35 look up bookmark
35 look up bookmark
36
36
37 $ hg log -r X
37 $ hg log -r X
38 changeset: 0:f7b1eb17ad24
38 changeset: 0:f7b1eb17ad24
39 tag: X
39 tag: X
40 tag: tip
40 tag: tip
41 user: test
41 user: test
42 date: Thu Jan 01 00:00:00 1970 +0000
42 date: Thu Jan 01 00:00:00 1970 +0000
43 summary: 0
43 summary: 0
44
44
45
45
46 second bookmark for rev 0
46 second bookmark for rev 0
47
47
48 $ hg bookmark X2
48 $ hg bookmark X2
49
49
50 bookmark rev -1 again
50 bookmark rev -1 again
51
51
52 $ hg bookmark -r null Y
52 $ hg bookmark -r null Y
53
53
54 list bookmarks
54 list bookmarks
55
55
56 $ hg bookmarks
56 $ hg bookmarks
57 * X2 0:f7b1eb17ad24
57 * X2 0:f7b1eb17ad24
58 * X 0:f7b1eb17ad24
58 * X 0:f7b1eb17ad24
59 Y -1:000000000000
59 Y -1:000000000000
60
60
61 $ echo b > b
61 $ echo b > b
62 $ hg add b
62 $ hg add b
63 $ hg commit -m 1
63 $ hg commit -m 1
64
64
65 bookmarks revset
65 bookmarks revset
66
66
67 $ hg log -r 'bookmark()'
67 $ hg log -r 'bookmark()'
68 changeset: 1:925d80f479bb
68 changeset: 1:925d80f479bb
69 tag: X
69 tag: X
70 tag: X2
70 tag: X2
71 tag: tip
71 tag: tip
72 user: test
72 user: test
73 date: Thu Jan 01 00:00:00 1970 +0000
73 date: Thu Jan 01 00:00:00 1970 +0000
74 summary: 1
74 summary: 1
75
75
76 $ hg log -r 'bookmark(Y)'
76 $ hg log -r 'bookmark(Y)'
77 $ hg log -r 'bookmark(X2)'
77 $ hg log -r 'bookmark(X2)'
78 changeset: 1:925d80f479bb
78 changeset: 1:925d80f479bb
79 tag: X
79 tag: X
80 tag: X2
80 tag: X2
81 tag: tip
81 tag: tip
82 user: test
82 user: test
83 date: Thu Jan 01 00:00:00 1970 +0000
83 date: Thu Jan 01 00:00:00 1970 +0000
84 summary: 1
84 summary: 1
85
85
86 $ hg help revsets | grep 'bookmark('
87 "bookmark([name])"
86
88
87 bookmarks X and X2 moved to rev 1, Y at rev -1
89 bookmarks X and X2 moved to rev 1, Y at rev -1
88
90
89 $ hg bookmarks
91 $ hg bookmarks
90 * X2 1:925d80f479bb
92 * X2 1:925d80f479bb
91 * X 1:925d80f479bb
93 * X 1:925d80f479bb
92 Y -1:000000000000
94 Y -1:000000000000
93
95
94 bookmark rev 0 again
96 bookmark rev 0 again
95
97
96 $ hg bookmark -r 0 Z
98 $ hg bookmark -r 0 Z
97
99
98 $ echo c > c
100 $ echo c > c
99 $ hg add c
101 $ hg add c
100 $ hg commit -m 2
102 $ hg commit -m 2
101
103
102 bookmarks X and X2 moved to rev 2, Y at rev -1, Z at rev 0
104 bookmarks X and X2 moved to rev 2, Y at rev -1, Z at rev 0
103
105
104 $ hg bookmarks
106 $ hg bookmarks
105 * X2 2:0316ce92851d
107 * X2 2:0316ce92851d
106 * X 2:0316ce92851d
108 * X 2:0316ce92851d
107 Z 0:f7b1eb17ad24
109 Z 0:f7b1eb17ad24
108 Y -1:000000000000
110 Y -1:000000000000
109
111
110 rename nonexistent bookmark
112 rename nonexistent bookmark
111
113
112 $ hg bookmark -m A B
114 $ hg bookmark -m A B
113 abort: a bookmark of this name does not exist
115 abort: a bookmark of this name does not exist
114 [255]
116 [255]
115
117
116 rename to existent bookmark
118 rename to existent bookmark
117
119
118 $ hg bookmark -m X Y
120 $ hg bookmark -m X Y
119 abort: a bookmark of the same name already exists
121 abort: a bookmark of the same name already exists
120 [255]
122 [255]
121
123
122 force rename to existent bookmark
124 force rename to existent bookmark
123
125
124 $ hg bookmark -f -m X Y
126 $ hg bookmark -f -m X Y
125
127
126 list bookmarks
128 list bookmarks
127
129
128 $ hg bookmark
130 $ hg bookmark
129 * X2 2:0316ce92851d
131 * X2 2:0316ce92851d
130 * Y 2:0316ce92851d
132 * Y 2:0316ce92851d
131 Z 0:f7b1eb17ad24
133 Z 0:f7b1eb17ad24
132
134
133 rename without new name
135 rename without new name
134
136
135 $ hg bookmark -m Y
137 $ hg bookmark -m Y
136 abort: new bookmark name required
138 abort: new bookmark name required
137 [255]
139 [255]
138
140
139 delete without name
141 delete without name
140
142
141 $ hg bookmark -d
143 $ hg bookmark -d
142 abort: bookmark name required
144 abort: bookmark name required
143 [255]
145 [255]
144
146
145 delete nonexistent bookmark
147 delete nonexistent bookmark
146
148
147 $ hg bookmark -d A
149 $ hg bookmark -d A
148 abort: a bookmark of this name does not exist
150 abort: a bookmark of this name does not exist
149 [255]
151 [255]
150
152
151 bookmark name with spaces should be stripped
153 bookmark name with spaces should be stripped
152
154
153 $ hg bookmark ' x y '
155 $ hg bookmark ' x y '
154
156
155 list bookmarks
157 list bookmarks
156
158
157 $ hg bookmarks
159 $ hg bookmarks
158 * X2 2:0316ce92851d
160 * X2 2:0316ce92851d
159 * Y 2:0316ce92851d
161 * Y 2:0316ce92851d
160 Z 0:f7b1eb17ad24
162 Z 0:f7b1eb17ad24
161 * x y 2:0316ce92851d
163 * x y 2:0316ce92851d
162
164
163 look up stripped bookmark name
165 look up stripped bookmark name
164
166
165 $ hg log -r '"x y"'
167 $ hg log -r '"x y"'
166 changeset: 2:0316ce92851d
168 changeset: 2:0316ce92851d
167 tag: X2
169 tag: X2
168 tag: Y
170 tag: Y
169 tag: tip
171 tag: tip
170 tag: x y
172 tag: x y
171 user: test
173 user: test
172 date: Thu Jan 01 00:00:00 1970 +0000
174 date: Thu Jan 01 00:00:00 1970 +0000
173 summary: 2
175 summary: 2
174
176
175
177
176 reject bookmark name with newline
178 reject bookmark name with newline
177
179
178 $ hg bookmark '
180 $ hg bookmark '
179 > '
181 > '
180 abort: bookmark name cannot contain newlines
182 abort: bookmark name cannot contain newlines
181 [255]
183 [255]
182
184
183 bookmark with existing name
185 bookmark with existing name
184
186
185 $ hg bookmark Z
187 $ hg bookmark Z
186 abort: a bookmark of the same name already exists
188 abort: a bookmark of the same name already exists
187 [255]
189 [255]
188
190
189 force bookmark with existing name
191 force bookmark with existing name
190
192
191 $ hg bookmark -f Z
193 $ hg bookmark -f Z
192
194
193 list bookmarks
195 list bookmarks
194
196
195 $ hg bookmark
197 $ hg bookmark
196 * X2 2:0316ce92851d
198 * X2 2:0316ce92851d
197 * Y 2:0316ce92851d
199 * Y 2:0316ce92851d
198 * Z 2:0316ce92851d
200 * Z 2:0316ce92851d
199 * x y 2:0316ce92851d
201 * x y 2:0316ce92851d
200
202
201 revision but no bookmark name
203 revision but no bookmark name
202
204
203 $ hg bookmark -r .
205 $ hg bookmark -r .
204 abort: bookmark name required
206 abort: bookmark name required
205 [255]
207 [255]
206
208
207 bookmark name with whitespace only
209 bookmark name with whitespace only
208
210
209 $ hg bookmark ' '
211 $ hg bookmark ' '
210 abort: bookmark names cannot consist entirely of whitespace
212 abort: bookmark names cannot consist entirely of whitespace
211 [255]
213 [255]
General Comments 0
You need to be logged in to leave comments. Login now