1 # Mercurial extension to provide the 'hg bookmark' command
3 # Copyright 2008 David Soria Parra <dsp@php.net>
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
8 '''track a line of development with movable markers
10 Bookmarks are local movable markers to changesets. Every bookmark
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
13 bookmark shifts to the new changeset.
15 It is possible to use bookmark names in every revision lookup (e.g. hg
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
20 git-like experience by adding the following configuration option to
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
31 from mercurial.i18n import _
32 from mercurial.node import nullid, nullrev, hex, short
33 from mercurial import util, commands, localrepo, repair, extensions
37 '''Parse .hg/bookmarks file and return a dictionary
39 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
40 in the .hg/bookmarks file. They are read by the parse() method and
41 returned as a dictionary with name => hash values.
43 The parsed dictionary is cached until a write() operation is done.
47 return repo._bookmarks
49 for line in repo.opener('bookmarks'):
50 sha, refspec = line.strip().split(' ', 1)
51 repo._bookmarks[refspec] = repo.lookup(sha)
54 return repo._bookmarks
56 def write(repo, refs):
59 Write the given bookmark => hash dictionary to the .hg/bookmarks file
60 in a format equal to those of localtags.
62 We also store a backup of the previous state in undo.bookmarks that
63 can be copied back on rollback.
65 if os.path.exists(repo.join('bookmarks')):
66 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
67 if current(repo) not in refs:
68 setcurrent(repo, None)
71 file = repo.opener('bookmarks', 'w', atomictemp=True)
72 for refspec, node in refs.iteritems():
73 file.write("%s %s\n" % (hex(node), refspec))
79 '''Get the current bookmark
81 If we use gittishsh branches we have a current bookmark that
82 we are on. This function returns the name of the bookmark. It
83 is stored in .hg/bookmarks.current
85 if repo._bookmarkcurrent:
86 return repo._bookmarkcurrent
88 if os.path.exists(repo.join('bookmarks.current')):
89 file = repo.opener('bookmarks.current')
90 # No readline() in posixfile_nt, reading everything is cheap
91 mark = (file.readlines() or [''])[0]
95 repo._bookmarkcurrent = mark
98 def setcurrent(repo, mark):
99 '''Set the name of the bookmark that we are currently on
101 Set the name of the bookmark that we are on (hg update <bookmark>).
102 The name is recorded in .hg/bookmarks.current
104 if current(repo) == mark:
109 # do not update if we do update to a rev equal to the current bookmark
110 if (mark and mark not in refs and
111 current(repo) and refs[current(repo)] == repo.changectx('.').node()):
117 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
122 repo._bookmarkcurrent = mark
124 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
125 '''track a line of development with movable markers
127 Bookmarks are pointers to certain commits that move when
128 committing. Bookmarks are local. They can be renamed, copied and
129 deleted. It is possible to use bookmark names in 'hg merge' and
130 'hg update' to merge and update respectively to a given bookmark.
132 You can use 'hg bookmark NAME' to set a bookmark on the working
133 directory's parent revision with the given name. If you specify
134 a revision using -r REV (where REV may be an existing bookmark),
135 the bookmark is assigned to that revision.
137 hexfn = ui.debugflag and hex or short
139 cur = repo.changectx('.').node()
142 if rename not in marks:
143 raise util.Abort(_("a bookmark of this name does not exist"))
144 if mark in marks and not force:
145 raise util.Abort(_("a bookmark of the same name already exists"))
147 raise util.Abort(_("new bookmark name required"))
148 marks[mark] = marks[rename]
150 if current(repo) == rename:
151 setcurrent(repo, mark)
157 raise util.Abort(_("bookmark name required"))
158 if mark not in marks:
159 raise util.Abort(_("a bookmark of this name does not exist"))
160 if mark == current(repo):
161 setcurrent(repo, None)
168 raise util.Abort(_("bookmark name cannot contain newlines"))
170 if mark in marks and not force:
171 raise util.Abort(_("a bookmark of the same name already exists"))
172 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
175 _("a bookmark cannot have the name of an existing branch"))
177 marks[mark] = repo.lookup(rev)
179 marks[mark] = repo.changectx('.').node()
180 setcurrent(repo, mark)
186 raise util.Abort(_("bookmark name required"))
188 ui.status("no bookmarks set\n")
190 for bmark, n in marks.iteritems():
191 if ui.configbool('bookmarks', 'track.current'):
192 prefix = (bmark == current(repo) and n == cur) and '*' or ' '
194 prefix = (n == cur) and '*' or ' '
196 ui.write(" %s %-25s %d:%s\n" % (
197 prefix, bmark, repo.changelog.rev(n), hexfn(n)))
200 def _revstostrip(changelog, node):
201 srev = changelog.rev(node)
204 for r in xrange(srev, len(changelog)):
205 parents = changelog.parentrevs(r)
206 if parents[0] in tostrip or parents[1] in tostrip:
208 if parents[1] != nullrev:
210 if p not in tostrip and p > srev:
212 return [r for r in tostrip if r not in saveheads]
214 def strip(oldstrip, ui, repo, node, backup="all"):
215 """Strip bookmarks if revisions are stripped using
216 the mercurial.strip method. This usually happens during
218 revisions = _revstostrip(repo.changelog, node)
221 for mark, n in marks.iteritems():
222 if repo.changelog.rev(n) in revisions:
224 oldstrip(ui, repo, node, backup)
227 marks[m] = repo.changectx('.').node()
230 def reposetup(ui, repo):
231 if not isinstance(repo, localrepo.localrepository):
234 # init a bookmark cache as otherwise we would get a infinite reading
236 repo._bookmarks = None
237 repo._bookmarkcurrent = None
239 class bookmark_repo(repo.__class__):
241 if os.path.exists(self.join('undo.bookmarks')):
242 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
243 return super(bookmark_repo, self).rollback()
245 def lookup(self, key):
246 if self._bookmarks is None:
247 self._bookmarks = parse(self)
248 if key in self._bookmarks:
249 key = self._bookmarks[key]
250 return super(bookmark_repo, self).lookup(key)
252 def commitctx(self, ctx, error=False):
253 """Add a revision to the repository and
255 wlock = self.wlock() # do both commit and bookmark with lock held
257 node = super(bookmark_repo, self).commitctx(ctx, error)
260 parents = self.changelog.parents(node)
261 if parents[1] == nullid:
262 parents = (parents[0],)
265 if ui.configbool('bookmarks', 'track.current'):
267 if mark and marks[mark] in parents:
271 for mark, n in marks.items():
281 def addchangegroup(self, source, srctype, url, emptyok=False):
282 parents = self.dirstate.parents()
284 result = super(bookmark_repo, self).addchangegroup(
285 source, srctype, url, emptyok)
287 # We have more heads than before
289 node = self.changelog.tip()
292 if ui.configbool('bookmarks', 'track.current'):
294 if mark and marks[mark] in parents:
298 for mark, n in marks.items():
307 """Merge bookmarks with normal tags"""
308 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
309 tags.update(parse(self))
310 return (tags, tagtypes)
312 repo.__class__ = bookmark_repo
315 extensions.wrapfunction(repair, "strip", strip)
316 if ui.configbool('bookmarks', 'track.current'):
317 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
319 def updatecurbookmark(orig, ui, repo, *args, **opts):
320 '''Set the current bookmark
322 If the user updates to a bookmark we update the .hg/bookmarks.current
325 res = orig(ui, repo, *args, **opts)
327 if not rev and len(args) > 0:
329 setcurrent(repo, rev)
335 [('f', 'force', False, _('force')),
336 ('r', 'rev', '', _('revision')),
337 ('d', 'delete', False, _('delete a given bookmark')),
338 ('m', 'rename', '', _('rename a given bookmark'))],
339 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),