##// END OF EJS Templates
bookmarks: add revset for referencing bookmarks
Augie Fackler -
r12714:f5178fbc default
parent child Browse files
Show More
@@ -1,549 +1,570 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, 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 import os
35 import os
35
36
36 def write(repo):
37 def write(repo):
37 '''Write bookmarks
38 '''Write bookmarks
38
39
39 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 in a format equal to those of localtags.
41 in a format equal to those of localtags.
41
42
42 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
43 can be copied back on rollback.
44 can be copied back on rollback.
44 '''
45 '''
45 refs = repo._bookmarks
46 refs = repo._bookmarks
46 if os.path.exists(repo.join('bookmarks')):
47 if os.path.exists(repo.join('bookmarks')):
47 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 if repo._bookmarkcurrent not in refs:
49 if repo._bookmarkcurrent not in refs:
49 setcurrent(repo, None)
50 setcurrent(repo, None)
50 wlock = repo.wlock()
51 wlock = repo.wlock()
51 try:
52 try:
52 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 for refspec, node in refs.iteritems():
54 for refspec, node in refs.iteritems():
54 file.write("%s %s\n" % (hex(node), refspec))
55 file.write("%s %s\n" % (hex(node), refspec))
55 file.rename()
56 file.rename()
56
57
57 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
58 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
58 try:
59 try:
59 os.utime(repo.sjoin('00changelog.i'), None)
60 os.utime(repo.sjoin('00changelog.i'), None)
60 except OSError:
61 except OSError:
61 pass
62 pass
62
63
63 finally:
64 finally:
64 wlock.release()
65 wlock.release()
65
66
66 def setcurrent(repo, mark):
67 def setcurrent(repo, mark):
67 '''Set the name of the bookmark that we are currently on
68 '''Set the name of the bookmark that we are currently on
68
69
69 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>).
70 The name is recorded in .hg/bookmarks.current
71 The name is recorded in .hg/bookmarks.current
71 '''
72 '''
72 current = repo._bookmarkcurrent
73 current = repo._bookmarkcurrent
73 if current == mark:
74 if current == mark:
74 return
75 return
75
76
76 refs = repo._bookmarks
77 refs = repo._bookmarks
77
78
78 # 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
79 if (mark and mark not in refs and
80 if (mark and mark not in refs and
80 current and refs[current] == repo.changectx('.').node()):
81 current and refs[current] == repo.changectx('.').node()):
81 return
82 return
82 if mark not in refs:
83 if mark not in refs:
83 mark = ''
84 mark = ''
84 wlock = repo.wlock()
85 wlock = repo.wlock()
85 try:
86 try:
86 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
87 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
87 file.write(mark)
88 file.write(mark)
88 file.rename()
89 file.rename()
89 finally:
90 finally:
90 wlock.release()
91 wlock.release()
91 repo._bookmarkcurrent = mark
92 repo._bookmarkcurrent = mark
92
93
93 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):
94 '''track a line of development with movable markers
95 '''track a line of development with movable markers
95
96
96 Bookmarks are pointers to certain commits that move when
97 Bookmarks are pointers to certain commits that move when
97 committing. Bookmarks are local. They can be renamed, copied and
98 committing. Bookmarks are local. They can be renamed, copied and
98 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
99 :hg:`update` to merge and update respectively to a given bookmark.
100 :hg:`update` to merge and update respectively to a given bookmark.
100
101
101 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
102 directory's parent revision with the given name. If you specify
103 directory's parent revision with the given name. If you specify
103 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),
104 the bookmark is assigned to that revision.
105 the bookmark is assigned to that revision.
105 '''
106 '''
106 hexfn = ui.debugflag and hex or short
107 hexfn = ui.debugflag and hex or short
107 marks = repo._bookmarks
108 marks = repo._bookmarks
108 cur = repo.changectx('.').node()
109 cur = repo.changectx('.').node()
109
110
110 if rename:
111 if rename:
111 if rename not in marks:
112 if rename not in marks:
112 raise util.Abort(_("a bookmark of this name does not exist"))
113 raise util.Abort(_("a bookmark of this name does not exist"))
113 if mark in marks and not force:
114 if mark in marks and not force:
114 raise util.Abort(_("a bookmark of the same name already exists"))
115 raise util.Abort(_("a bookmark of the same name already exists"))
115 if mark is None:
116 if mark is None:
116 raise util.Abort(_("new bookmark name required"))
117 raise util.Abort(_("new bookmark name required"))
117 marks[mark] = marks[rename]
118 marks[mark] = marks[rename]
118 del marks[rename]
119 del marks[rename]
119 if repo._bookmarkcurrent == rename:
120 if repo._bookmarkcurrent == rename:
120 setcurrent(repo, mark)
121 setcurrent(repo, mark)
121 write(repo)
122 write(repo)
122 return
123 return
123
124
124 if delete:
125 if delete:
125 if mark is None:
126 if mark is None:
126 raise util.Abort(_("bookmark name required"))
127 raise util.Abort(_("bookmark name required"))
127 if mark not in marks:
128 if mark not in marks:
128 raise util.Abort(_("a bookmark of this name does not exist"))
129 raise util.Abort(_("a bookmark of this name does not exist"))
129 if mark == repo._bookmarkcurrent:
130 if mark == repo._bookmarkcurrent:
130 setcurrent(repo, None)
131 setcurrent(repo, None)
131 del marks[mark]
132 del marks[mark]
132 write(repo)
133 write(repo)
133 return
134 return
134
135
135 if mark != None:
136 if mark != None:
136 if "\n" in mark:
137 if "\n" in mark:
137 raise util.Abort(_("bookmark name cannot contain newlines"))
138 raise util.Abort(_("bookmark name cannot contain newlines"))
138 mark = mark.strip()
139 mark = mark.strip()
139 if not mark:
140 if not mark:
140 raise util.Abort(_("bookmark names cannot consist entirely of "
141 raise util.Abort(_("bookmark names cannot consist entirely of "
141 "whitespace"))
142 "whitespace"))
142 if mark in marks and not force:
143 if mark in marks and not force:
143 raise util.Abort(_("a bookmark of the same name already exists"))
144 raise util.Abort(_("a bookmark of the same name already exists"))
144 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
145 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
145 and not force):
146 and not force):
146 raise util.Abort(
147 raise util.Abort(
147 _("a bookmark cannot have the name of an existing branch"))
148 _("a bookmark cannot have the name of an existing branch"))
148 if rev:
149 if rev:
149 marks[mark] = repo.lookup(rev)
150 marks[mark] = repo.lookup(rev)
150 else:
151 else:
151 marks[mark] = repo.changectx('.').node()
152 marks[mark] = repo.changectx('.').node()
152 setcurrent(repo, mark)
153 setcurrent(repo, mark)
153 write(repo)
154 write(repo)
154 return
155 return
155
156
156 if mark is None:
157 if mark is None:
157 if rev:
158 if rev:
158 raise util.Abort(_("bookmark name required"))
159 raise util.Abort(_("bookmark name required"))
159 if len(marks) == 0:
160 if len(marks) == 0:
160 ui.status(_("no bookmarks set\n"))
161 ui.status(_("no bookmarks set\n"))
161 else:
162 else:
162 for bmark, n in marks.iteritems():
163 for bmark, n in marks.iteritems():
163 if ui.configbool('bookmarks', 'track.current'):
164 if ui.configbool('bookmarks', 'track.current'):
164 current = repo._bookmarkcurrent
165 current = repo._bookmarkcurrent
165 if bmark == current and n == cur:
166 if bmark == current and n == cur:
166 prefix, label = '*', 'bookmarks.current'
167 prefix, label = '*', 'bookmarks.current'
167 else:
168 else:
168 prefix, label = ' ', ''
169 prefix, label = ' ', ''
169 else:
170 else:
170 if n == cur:
171 if n == cur:
171 prefix, label = '*', 'bookmarks.current'
172 prefix, label = '*', 'bookmarks.current'
172 else:
173 else:
173 prefix, label = ' ', ''
174 prefix, label = ' ', ''
174
175
175 if ui.quiet:
176 if ui.quiet:
176 ui.write("%s\n" % bmark, label=label)
177 ui.write("%s\n" % bmark, label=label)
177 else:
178 else:
178 ui.write(" %s %-25s %d:%s\n" % (
179 ui.write(" %s %-25s %d:%s\n" % (
179 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
180 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
180 label=label)
181 label=label)
181 return
182 return
182
183
183 def _revstostrip(changelog, node):
184 def _revstostrip(changelog, node):
184 srev = changelog.rev(node)
185 srev = changelog.rev(node)
185 tostrip = [srev]
186 tostrip = [srev]
186 saveheads = []
187 saveheads = []
187 for r in xrange(srev, len(changelog)):
188 for r in xrange(srev, len(changelog)):
188 parents = changelog.parentrevs(r)
189 parents = changelog.parentrevs(r)
189 if parents[0] in tostrip or parents[1] in tostrip:
190 if parents[0] in tostrip or parents[1] in tostrip:
190 tostrip.append(r)
191 tostrip.append(r)
191 if parents[1] != nullrev:
192 if parents[1] != nullrev:
192 for p in parents:
193 for p in parents:
193 if p not in tostrip and p > srev:
194 if p not in tostrip and p > srev:
194 saveheads.append(p)
195 saveheads.append(p)
195 return [r for r in tostrip if r not in saveheads]
196 return [r for r in tostrip if r not in saveheads]
196
197
197 def strip(oldstrip, ui, repo, node, backup="all"):
198 def strip(oldstrip, ui, repo, node, backup="all"):
198 """Strip bookmarks if revisions are stripped using
199 """Strip bookmarks if revisions are stripped using
199 the mercurial.strip method. This usually happens during
200 the mercurial.strip method. This usually happens during
200 qpush and qpop"""
201 qpush and qpop"""
201 revisions = _revstostrip(repo.changelog, node)
202 revisions = _revstostrip(repo.changelog, node)
202 marks = repo._bookmarks
203 marks = repo._bookmarks
203 update = []
204 update = []
204 for mark, n in marks.iteritems():
205 for mark, n in marks.iteritems():
205 if repo.changelog.rev(n) in revisions:
206 if repo.changelog.rev(n) in revisions:
206 update.append(mark)
207 update.append(mark)
207 oldstrip(ui, repo, node, backup)
208 oldstrip(ui, repo, node, backup)
208 if len(update) > 0:
209 if len(update) > 0:
209 for m in update:
210 for m in update:
210 marks[m] = repo.changectx('.').node()
211 marks[m] = repo.changectx('.').node()
211 write(repo)
212 write(repo)
212
213
213 def reposetup(ui, repo):
214 def reposetup(ui, repo):
214 if not repo.local():
215 if not repo.local():
215 return
216 return
216
217
217 class bookmark_repo(repo.__class__):
218 class bookmark_repo(repo.__class__):
218
219
219 @util.propertycache
220 @util.propertycache
220 def _bookmarks(self):
221 def _bookmarks(self):
221 '''Parse .hg/bookmarks file and return a dictionary
222 '''Parse .hg/bookmarks file and return a dictionary
222
223
223 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
224 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
224 in the .hg/bookmarks file.
225 in the .hg/bookmarks file.
225 Read the file and return a (name=>nodeid) dictionary
226 Read the file and return a (name=>nodeid) dictionary
226 '''
227 '''
227 self._loadingbookmarks = True
228 self._loadingbookmarks = True
228 try:
229 try:
229 bookmarks = {}
230 bookmarks = {}
230 for line in self.opener('bookmarks'):
231 for line in self.opener('bookmarks'):
231 sha, refspec = line.strip().split(' ', 1)
232 sha, refspec = line.strip().split(' ', 1)
232 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
233 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
233 except:
234 except:
234 pass
235 pass
235 self._loadingbookmarks = False
236 self._loadingbookmarks = False
236 return bookmarks
237 return bookmarks
237
238
238 @util.propertycache
239 @util.propertycache
239 def _bookmarkcurrent(self):
240 def _bookmarkcurrent(self):
240 '''Get the current bookmark
241 '''Get the current bookmark
241
242
242 If we use gittishsh branches we have a current bookmark that
243 If we use gittishsh branches we have a current bookmark that
243 we are on. This function returns the name of the bookmark. It
244 we are on. This function returns the name of the bookmark. It
244 is stored in .hg/bookmarks.current
245 is stored in .hg/bookmarks.current
245 '''
246 '''
246 mark = None
247 mark = None
247 if os.path.exists(self.join('bookmarks.current')):
248 if os.path.exists(self.join('bookmarks.current')):
248 file = self.opener('bookmarks.current')
249 file = self.opener('bookmarks.current')
249 # No readline() in posixfile_nt, reading everything is cheap
250 # No readline() in posixfile_nt, reading everything is cheap
250 mark = (file.readlines() or [''])[0]
251 mark = (file.readlines() or [''])[0]
251 if mark == '':
252 if mark == '':
252 mark = None
253 mark = None
253 file.close()
254 file.close()
254 return mark
255 return mark
255
256
256 def rollback(self, *args):
257 def rollback(self, *args):
257 if os.path.exists(self.join('undo.bookmarks')):
258 if os.path.exists(self.join('undo.bookmarks')):
258 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
259 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
259 return super(bookmark_repo, self).rollback(*args)
260 return super(bookmark_repo, self).rollback(*args)
260
261
261 def lookup(self, key):
262 def lookup(self, key):
262 if not getattr(self, '_loadingbookmarks', False):
263 if not getattr(self, '_loadingbookmarks', False):
263 if key in self._bookmarks:
264 if key in self._bookmarks:
264 key = self._bookmarks[key]
265 key = self._bookmarks[key]
265 return super(bookmark_repo, self).lookup(key)
266 return super(bookmark_repo, self).lookup(key)
266
267
267 def _bookmarksupdate(self, parents, node):
268 def _bookmarksupdate(self, parents, node):
268 marks = self._bookmarks
269 marks = self._bookmarks
269 update = False
270 update = False
270 if ui.configbool('bookmarks', 'track.current'):
271 if ui.configbool('bookmarks', 'track.current'):
271 mark = self._bookmarkcurrent
272 mark = self._bookmarkcurrent
272 if mark and marks[mark] in parents:
273 if mark and marks[mark] in parents:
273 marks[mark] = node
274 marks[mark] = node
274 update = True
275 update = True
275 else:
276 else:
276 for mark, n in marks.items():
277 for mark, n in marks.items():
277 if n in parents:
278 if n in parents:
278 marks[mark] = node
279 marks[mark] = node
279 update = True
280 update = True
280 if update:
281 if update:
281 write(self)
282 write(self)
282
283
283 def commitctx(self, ctx, error=False):
284 def commitctx(self, ctx, error=False):
284 """Add a revision to the repository and
285 """Add a revision to the repository and
285 move the bookmark"""
286 move the bookmark"""
286 wlock = self.wlock() # do both commit and bookmark with lock held
287 wlock = self.wlock() # do both commit and bookmark with lock held
287 try:
288 try:
288 node = super(bookmark_repo, self).commitctx(ctx, error)
289 node = super(bookmark_repo, self).commitctx(ctx, error)
289 if node is None:
290 if node is None:
290 return None
291 return None
291 parents = self.changelog.parents(node)
292 parents = self.changelog.parents(node)
292 if parents[1] == nullid:
293 if parents[1] == nullid:
293 parents = (parents[0],)
294 parents = (parents[0],)
294
295
295 self._bookmarksupdate(parents, node)
296 self._bookmarksupdate(parents, node)
296 return node
297 return node
297 finally:
298 finally:
298 wlock.release()
299 wlock.release()
299
300
300 def pull(self, remote, heads=None, force=False):
301 def pull(self, remote, heads=None, force=False):
301 result = super(bookmark_repo, self).pull(remote, heads, force)
302 result = super(bookmark_repo, self).pull(remote, heads, force)
302
303
303 self.ui.debug("checking for updated bookmarks\n")
304 self.ui.debug("checking for updated bookmarks\n")
304 rb = remote.listkeys('bookmarks')
305 rb = remote.listkeys('bookmarks')
305 changed = False
306 changed = False
306 for k in rb.keys():
307 for k in rb.keys():
307 if k in self._bookmarks:
308 if k in self._bookmarks:
308 nr, nl = rb[k], self._bookmarks[k]
309 nr, nl = rb[k], self._bookmarks[k]
309 if nr in self:
310 if nr in self:
310 cr = self[nr]
311 cr = self[nr]
311 cl = self[nl]
312 cl = self[nl]
312 if cl.rev() >= cr.rev():
313 if cl.rev() >= cr.rev():
313 continue
314 continue
314 if cr in cl.descendants():
315 if cr in cl.descendants():
315 self._bookmarks[k] = cr.node()
316 self._bookmarks[k] = cr.node()
316 changed = True
317 changed = True
317 self.ui.status(_("updating bookmark %s\n") % k)
318 self.ui.status(_("updating bookmark %s\n") % k)
318 else:
319 else:
319 self.ui.warn(_("not updating divergent"
320 self.ui.warn(_("not updating divergent"
320 " bookmark %s\n") % k)
321 " bookmark %s\n") % k)
321 if changed:
322 if changed:
322 write(repo)
323 write(repo)
323
324
324 return result
325 return result
325
326
326 def push(self, remote, force=False, revs=None, newbranch=False):
327 def push(self, remote, force=False, revs=None, newbranch=False):
327 result = super(bookmark_repo, self).push(remote, force, revs,
328 result = super(bookmark_repo, self).push(remote, force, revs,
328 newbranch)
329 newbranch)
329
330
330 self.ui.debug("checking for updated bookmarks\n")
331 self.ui.debug("checking for updated bookmarks\n")
331 rb = remote.listkeys('bookmarks')
332 rb = remote.listkeys('bookmarks')
332 for k in rb.keys():
333 for k in rb.keys():
333 if k in self._bookmarks:
334 if k in self._bookmarks:
334 nr, nl = rb[k], self._bookmarks[k]
335 nr, nl = rb[k], self._bookmarks[k]
335 if nr in self:
336 if nr in self:
336 cr = self[nr]
337 cr = self[nr]
337 cl = self[nl]
338 cl = self[nl]
338 if cl in cr.descendants():
339 if cl in cr.descendants():
339 r = remote.pushkey('bookmarks', k, nr, nl)
340 r = remote.pushkey('bookmarks', k, nr, nl)
340 if r:
341 if r:
341 self.ui.status(_("updating bookmark %s\n") % k)
342 self.ui.status(_("updating bookmark %s\n") % k)
342 else:
343 else:
343 self.ui.warn(_('updating bookmark %s'
344 self.ui.warn(_('updating bookmark %s'
344 ' failed!\n') % k)
345 ' failed!\n') % k)
345
346
346 return result
347 return result
347
348
348 def addchangegroup(self, *args, **kwargs):
349 def addchangegroup(self, *args, **kwargs):
349 parents = self.dirstate.parents()
350 parents = self.dirstate.parents()
350
351
351 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
352 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
352 if result > 1:
353 if result > 1:
353 # We have more heads than before
354 # We have more heads than before
354 return result
355 return result
355 node = self.changelog.tip()
356 node = self.changelog.tip()
356
357
357 self._bookmarksupdate(parents, node)
358 self._bookmarksupdate(parents, node)
358 return result
359 return result
359
360
360 def _findtags(self):
361 def _findtags(self):
361 """Merge bookmarks with normal tags"""
362 """Merge bookmarks with normal tags"""
362 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
363 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
363 if not getattr(self, '_loadingbookmarks', False):
364 if not getattr(self, '_loadingbookmarks', False):
364 tags.update(self._bookmarks)
365 tags.update(self._bookmarks)
365 return (tags, tagtypes)
366 return (tags, tagtypes)
366
367
367 if hasattr(repo, 'invalidate'):
368 if hasattr(repo, 'invalidate'):
368 def invalidate(self):
369 def invalidate(self):
369 super(bookmark_repo, self).invalidate()
370 super(bookmark_repo, self).invalidate()
370 for attr in ('_bookmarks', '_bookmarkcurrent'):
371 for attr in ('_bookmarks', '_bookmarkcurrent'):
371 if attr in self.__dict__:
372 if attr in self.__dict__:
372 delattr(self, attr)
373 delattr(self, attr)
373
374
374 repo.__class__ = bookmark_repo
375 repo.__class__ = bookmark_repo
375
376
376 def listbookmarks(repo):
377 def listbookmarks(repo):
377 # 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
378 # support it (e.g., statichttprepository).
379 # support it (e.g., statichttprepository).
379 if not hasattr(repo, '_bookmarks'):
380 if not hasattr(repo, '_bookmarks'):
380 return {}
381 return {}
381
382
382 d = {}
383 d = {}
383 for k, v in repo._bookmarks.iteritems():
384 for k, v in repo._bookmarks.iteritems():
384 d[k] = hex(v)
385 d[k] = hex(v)
385 return d
386 return d
386
387
387 def pushbookmark(repo, key, old, new):
388 def pushbookmark(repo, key, old, new):
388 w = repo.wlock()
389 w = repo.wlock()
389 try:
390 try:
390 marks = repo._bookmarks
391 marks = repo._bookmarks
391 if hex(marks.get(key, '')) != old:
392 if hex(marks.get(key, '')) != old:
392 return False
393 return False
393 if new == '':
394 if new == '':
394 del marks[key]
395 del marks[key]
395 else:
396 else:
396 if new not in repo:
397 if new not in repo:
397 return False
398 return False
398 marks[key] = repo[new].node()
399 marks[key] = repo[new].node()
399 write(repo)
400 write(repo)
400 return True
401 return True
401 finally:
402 finally:
402 w.release()
403 w.release()
403
404
404 def pull(oldpull, ui, repo, source="default", **opts):
405 def pull(oldpull, ui, repo, source="default", **opts):
405 # translate bookmark args to rev args for actual pull
406 # translate bookmark args to rev args for actual pull
406 if opts.get('bookmark'):
407 if opts.get('bookmark'):
407 # this is an unpleasant hack as pull will do this internally
408 # this is an unpleasant hack as pull will do this internally
408 source, branches = hg.parseurl(ui.expandpath(source),
409 source, branches = hg.parseurl(ui.expandpath(source),
409 opts.get('branch'))
410 opts.get('branch'))
410 other = hg.repository(hg.remoteui(repo, opts), source)
411 other = hg.repository(hg.remoteui(repo, opts), source)
411 rb = other.listkeys('bookmarks')
412 rb = other.listkeys('bookmarks')
412
413
413 for b in opts['bookmark']:
414 for b in opts['bookmark']:
414 if b not in rb:
415 if b not in rb:
415 raise util.Abort(_('remote bookmark %s not found!') % b)
416 raise util.Abort(_('remote bookmark %s not found!') % b)
416 opts.setdefault('rev', []).append(b)
417 opts.setdefault('rev', []).append(b)
417
418
418 result = oldpull(ui, repo, source, **opts)
419 result = oldpull(ui, repo, source, **opts)
419
420
420 # update specified bookmarks
421 # update specified bookmarks
421 if opts.get('bookmark'):
422 if opts.get('bookmark'):
422 for b in opts['bookmark']:
423 for b in opts['bookmark']:
423 # explicit pull overrides local bookmark if any
424 # explicit pull overrides local bookmark if any
424 ui.status(_("importing bookmark %s\n") % b)
425 ui.status(_("importing bookmark %s\n") % b)
425 repo._bookmarks[b] = repo[rb[b]].node()
426 repo._bookmarks[b] = repo[rb[b]].node()
426 write(repo)
427 write(repo)
427
428
428 return result
429 return result
429
430
430 def push(oldpush, ui, repo, dest=None, **opts):
431 def push(oldpush, ui, repo, dest=None, **opts):
431 dopush = True
432 dopush = True
432 if opts.get('bookmark'):
433 if opts.get('bookmark'):
433 dopush = False
434 dopush = False
434 for b in opts['bookmark']:
435 for b in opts['bookmark']:
435 if b in repo._bookmarks:
436 if b in repo._bookmarks:
436 dopush = True
437 dopush = True
437 opts.setdefault('rev', []).append(b)
438 opts.setdefault('rev', []).append(b)
438
439
439 result = 0
440 result = 0
440 if dopush:
441 if dopush:
441 result = oldpush(ui, repo, dest, **opts)
442 result = oldpush(ui, repo, dest, **opts)
442
443
443 if opts.get('bookmark'):
444 if opts.get('bookmark'):
444 # this is an unpleasant hack as push will do this internally
445 # this is an unpleasant hack as push will do this internally
445 dest = ui.expandpath(dest or 'default-push', dest or 'default')
446 dest = ui.expandpath(dest or 'default-push', dest or 'default')
446 dest, branches = hg.parseurl(dest, opts.get('branch'))
447 dest, branches = hg.parseurl(dest, opts.get('branch'))
447 other = hg.repository(hg.remoteui(repo, opts), dest)
448 other = hg.repository(hg.remoteui(repo, opts), dest)
448 rb = other.listkeys('bookmarks')
449 rb = other.listkeys('bookmarks')
449 for b in opts['bookmark']:
450 for b in opts['bookmark']:
450 # explicit push overrides remote bookmark if any
451 # explicit push overrides remote bookmark if any
451 if b in repo._bookmarks:
452 if b in repo._bookmarks:
452 ui.status(_("exporting bookmark %s\n") % b)
453 ui.status(_("exporting bookmark %s\n") % b)
453 new = repo[b].hex()
454 new = repo[b].hex()
454 elif b in rb:
455 elif b in rb:
455 ui.status(_("deleting remote bookmark %s\n") % b)
456 ui.status(_("deleting remote bookmark %s\n") % b)
456 new = '' # delete
457 new = '' # delete
457 else:
458 else:
458 ui.warn(_('bookmark %s does not exist on the local '
459 ui.warn(_('bookmark %s does not exist on the local '
459 'or remote repository!\n') % b)
460 'or remote repository!\n') % b)
460 return 2
461 return 2
461 old = rb.get(b, '')
462 old = rb.get(b, '')
462 r = other.pushkey('bookmarks', b, old, new)
463 r = other.pushkey('bookmarks', b, old, new)
463 if not r:
464 if not r:
464 ui.warn(_('updating bookmark %s failed!\n') % b)
465 ui.warn(_('updating bookmark %s failed!\n') % b)
465 if not result:
466 if not result:
466 result = 2
467 result = 2
467
468
468 return result
469 return result
469
470
470 def diffbookmarks(ui, repo, remote):
471 def diffbookmarks(ui, repo, remote):
471 ui.status(_("searching for changes\n"))
472 ui.status(_("searching for changes\n"))
472
473
473 lmarks = repo.listkeys('bookmarks')
474 lmarks = repo.listkeys('bookmarks')
474 rmarks = remote.listkeys('bookmarks')
475 rmarks = remote.listkeys('bookmarks')
475
476
476 diff = sorted(set(rmarks) - set(lmarks))
477 diff = sorted(set(rmarks) - set(lmarks))
477 for k in diff:
478 for k in diff:
478 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
479 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
479
480
480 if len(diff) <= 0:
481 if len(diff) <= 0:
481 ui.status(_("no changes found\n"))
482 ui.status(_("no changes found\n"))
482 return 1
483 return 1
483 return 0
484 return 0
484
485
485 def incoming(oldincoming, ui, repo, source="default", **opts):
486 def incoming(oldincoming, ui, repo, source="default", **opts):
486 if opts.get('bookmarks'):
487 if opts.get('bookmarks'):
487 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
488 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
488 other = hg.repository(hg.remoteui(repo, opts), source)
489 other = hg.repository(hg.remoteui(repo, opts), source)
489 ui.status(_('comparing with %s\n') % url.hidepassword(source))
490 ui.status(_('comparing with %s\n') % url.hidepassword(source))
490 return diffbookmarks(ui, repo, other)
491 return diffbookmarks(ui, repo, other)
491 else:
492 else:
492 return oldincoming(ui, repo, source, **opts)
493 return oldincoming(ui, repo, source, **opts)
493
494
494 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
495 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
495 if opts.get('bookmarks'):
496 if opts.get('bookmarks'):
496 dest = ui.expandpath(dest or 'default-push', dest or 'default')
497 dest = ui.expandpath(dest or 'default-push', dest or 'default')
497 dest, branches = hg.parseurl(dest, opts.get('branch'))
498 dest, branches = hg.parseurl(dest, opts.get('branch'))
498 other = hg.repository(hg.remoteui(repo, opts), dest)
499 other = hg.repository(hg.remoteui(repo, opts), dest)
499 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
500 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
500 return diffbookmarks(ui, other, repo)
501 return diffbookmarks(ui, other, repo)
501 else:
502 else:
502 return oldoutgoing(ui, repo, dest, **opts)
503 return oldoutgoing(ui, repo, dest, **opts)
503
504
504 def uisetup(ui):
505 def uisetup(ui):
505 extensions.wrapfunction(repair, "strip", strip)
506 extensions.wrapfunction(repair, "strip", strip)
506 if ui.configbool('bookmarks', 'track.current'):
507 if ui.configbool('bookmarks', 'track.current'):
507 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
508 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
508
509
509 entry = extensions.wrapcommand(commands.table, 'pull', pull)
510 entry = extensions.wrapcommand(commands.table, 'pull', pull)
510 entry[1].append(('B', 'bookmark', [],
511 entry[1].append(('B', 'bookmark', [],
511 _("bookmark to import"),
512 _("bookmark to import"),
512 _('BOOKMARK')))
513 _('BOOKMARK')))
513 entry = extensions.wrapcommand(commands.table, 'push', push)
514 entry = extensions.wrapcommand(commands.table, 'push', push)
514 entry[1].append(('B', 'bookmark', [],
515 entry[1].append(('B', 'bookmark', [],
515 _("bookmark to export"),
516 _("bookmark to export"),
516 _('BOOKMARK')))
517 _('BOOKMARK')))
517 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
518 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
518 entry[1].append(('B', 'bookmarks', False,
519 entry[1].append(('B', 'bookmarks', False,
519 _("compare bookmark")))
520 _("compare bookmark")))
520 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
521 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
521 entry[1].append(('B', 'bookmarks', False,
522 entry[1].append(('B', 'bookmarks', False,
522 _("compare bookmark")))
523 _("compare bookmark")))
523
524
524 pushkey.register('bookmarks', pushbookmark, listbookmarks)
525 pushkey.register('bookmarks', pushbookmark, listbookmarks)
525
526
526 def updatecurbookmark(orig, ui, repo, *args, **opts):
527 def updatecurbookmark(orig, ui, repo, *args, **opts):
527 '''Set the current bookmark
528 '''Set the current bookmark
528
529
529 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
530 file.
531 file.
531 '''
532 '''
532 res = orig(ui, repo, *args, **opts)
533 res = orig(ui, repo, *args, **opts)
533 rev = opts['rev']
534 rev = opts['rev']
534 if not rev and len(args) > 0:
535 if not rev and len(args) > 0:
535 rev = args[0]
536 rev = args[0]
536 setcurrent(repo, rev)
537 setcurrent(repo, rev)
537 return res
538 return res
538
539
540 def bmrevset(repo, subset, x):
541 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
542 if args:
543 bm = revset.getstring(args[0],
544 _('the argument to bookmark must be a string'))
545 bmrev = listbookmarks(repo).get(bm, None)
546 if bmrev:
547 bmrev = repo.changelog.rev(bin(bmrev))
548 return [r for r in subset if r == bmrev]
549 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
550 return [r for r in subset if r in bms]
551 revset.symbols['bookmark'] = bmrevset
552
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
539 cmdtable = {
560 cmdtable = {
540 "bookmarks":
561 "bookmarks":
541 (bookmark,
562 (bookmark,
542 [('f', 'force', False, _('force')),
563 [('f', 'force', False, _('force')),
543 ('r', 'rev', '', _('revision'), _('REV')),
564 ('r', 'rev', '', _('revision'), _('REV')),
544 ('d', 'delete', False, _('delete a given bookmark')),
565 ('d', 'delete', False, _('delete a given bookmark')),
545 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
566 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
546 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
567 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
547 }
568 }
548
569
549 colortable = {'bookmarks.current': 'green'}
570 colortable = {'bookmarks.current': 'green'}
@@ -1,189 +1,211 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
66
67 $ hg log -r 'bookmark()'
68 changeset: 1:925d80f479bb
69 tag: X
70 tag: X2
71 tag: tip
72 user: test
73 date: Thu Jan 01 00:00:00 1970 +0000
74 summary: 1
75
76 $ hg log -r 'bookmark(Y)'
77 $ hg log -r 'bookmark(X2)'
78 changeset: 1:925d80f479bb
79 tag: X
80 tag: X2
81 tag: tip
82 user: test
83 date: Thu Jan 01 00:00:00 1970 +0000
84 summary: 1
85
86
65 bookmarks X and X2 moved to rev 1, Y at rev -1
87 bookmarks X and X2 moved to rev 1, Y at rev -1
66
88
67 $ hg bookmarks
89 $ hg bookmarks
68 * X2 1:925d80f479bb
90 * X2 1:925d80f479bb
69 * X 1:925d80f479bb
91 * X 1:925d80f479bb
70 Y -1:000000000000
92 Y -1:000000000000
71
93
72 bookmark rev 0 again
94 bookmark rev 0 again
73
95
74 $ hg bookmark -r 0 Z
96 $ hg bookmark -r 0 Z
75
97
76 $ echo c > c
98 $ echo c > c
77 $ hg add c
99 $ hg add c
78 $ hg commit -m 2
100 $ hg commit -m 2
79
101
80 bookmarks X and X2 moved to rev 2, Y at rev -1, Z at rev 0
102 bookmarks X and X2 moved to rev 2, Y at rev -1, Z at rev 0
81
103
82 $ hg bookmarks
104 $ hg bookmarks
83 * X2 2:0316ce92851d
105 * X2 2:0316ce92851d
84 * X 2:0316ce92851d
106 * X 2:0316ce92851d
85 Z 0:f7b1eb17ad24
107 Z 0:f7b1eb17ad24
86 Y -1:000000000000
108 Y -1:000000000000
87
109
88 rename nonexistent bookmark
110 rename nonexistent bookmark
89
111
90 $ hg bookmark -m A B
112 $ hg bookmark -m A B
91 abort: a bookmark of this name does not exist
113 abort: a bookmark of this name does not exist
92 [255]
114 [255]
93
115
94 rename to existent bookmark
116 rename to existent bookmark
95
117
96 $ hg bookmark -m X Y
118 $ hg bookmark -m X Y
97 abort: a bookmark of the same name already exists
119 abort: a bookmark of the same name already exists
98 [255]
120 [255]
99
121
100 force rename to existent bookmark
122 force rename to existent bookmark
101
123
102 $ hg bookmark -f -m X Y
124 $ hg bookmark -f -m X Y
103
125
104 list bookmarks
126 list bookmarks
105
127
106 $ hg bookmark
128 $ hg bookmark
107 * X2 2:0316ce92851d
129 * X2 2:0316ce92851d
108 * Y 2:0316ce92851d
130 * Y 2:0316ce92851d
109 Z 0:f7b1eb17ad24
131 Z 0:f7b1eb17ad24
110
132
111 rename without new name
133 rename without new name
112
134
113 $ hg bookmark -m Y
135 $ hg bookmark -m Y
114 abort: new bookmark name required
136 abort: new bookmark name required
115 [255]
137 [255]
116
138
117 delete without name
139 delete without name
118
140
119 $ hg bookmark -d
141 $ hg bookmark -d
120 abort: bookmark name required
142 abort: bookmark name required
121 [255]
143 [255]
122
144
123 delete nonexistent bookmark
145 delete nonexistent bookmark
124
146
125 $ hg bookmark -d A
147 $ hg bookmark -d A
126 abort: a bookmark of this name does not exist
148 abort: a bookmark of this name does not exist
127 [255]
149 [255]
128
150
129 bookmark name with spaces should be stripped
151 bookmark name with spaces should be stripped
130
152
131 $ hg bookmark ' x y '
153 $ hg bookmark ' x y '
132
154
133 list bookmarks
155 list bookmarks
134
156
135 $ hg bookmarks
157 $ hg bookmarks
136 * X2 2:0316ce92851d
158 * X2 2:0316ce92851d
137 * Y 2:0316ce92851d
159 * Y 2:0316ce92851d
138 Z 0:f7b1eb17ad24
160 Z 0:f7b1eb17ad24
139 * x y 2:0316ce92851d
161 * x y 2:0316ce92851d
140
162
141 look up stripped bookmark name
163 look up stripped bookmark name
142
164
143 $ hg log -r '"x y"'
165 $ hg log -r '"x y"'
144 changeset: 2:0316ce92851d
166 changeset: 2:0316ce92851d
145 tag: X2
167 tag: X2
146 tag: Y
168 tag: Y
147 tag: tip
169 tag: tip
148 tag: x y
170 tag: x y
149 user: test
171 user: test
150 date: Thu Jan 01 00:00:00 1970 +0000
172 date: Thu Jan 01 00:00:00 1970 +0000
151 summary: 2
173 summary: 2
152
174
153
175
154 reject bookmark name with newline
176 reject bookmark name with newline
155
177
156 $ hg bookmark '
178 $ hg bookmark '
157 > '
179 > '
158 abort: bookmark name cannot contain newlines
180 abort: bookmark name cannot contain newlines
159 [255]
181 [255]
160
182
161 bookmark with existing name
183 bookmark with existing name
162
184
163 $ hg bookmark Z
185 $ hg bookmark Z
164 abort: a bookmark of the same name already exists
186 abort: a bookmark of the same name already exists
165 [255]
187 [255]
166
188
167 force bookmark with existing name
189 force bookmark with existing name
168
190
169 $ hg bookmark -f Z
191 $ hg bookmark -f Z
170
192
171 list bookmarks
193 list bookmarks
172
194
173 $ hg bookmark
195 $ hg bookmark
174 * X2 2:0316ce92851d
196 * X2 2:0316ce92851d
175 * Y 2:0316ce92851d
197 * Y 2:0316ce92851d
176 * Z 2:0316ce92851d
198 * Z 2:0316ce92851d
177 * x y 2:0316ce92851d
199 * x y 2:0316ce92851d
178
200
179 revision but no bookmark name
201 revision but no bookmark name
180
202
181 $ hg bookmark -r .
203 $ hg bookmark -r .
182 abort: bookmark name required
204 abort: bookmark name required
183 [255]
205 [255]
184
206
185 bookmark name with whitespace only
207 bookmark name with whitespace only
186
208
187 $ hg bookmark ' '
209 $ hg bookmark ' '
188 abort: bookmark names cannot consist entirely of whitespace
210 abort: bookmark names cannot consist entirely of whitespace
189 [255]
211 [255]
General Comments 0
You need to be logged in to leave comments. Login now