##// END OF EJS Templates
bookmark: make use of output labeling
Brodie Rao -
r10820:da809085 default
parent child Browse files
Show More
@@ -1,334 +1,341 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. hg
15 It is possible to use bookmark names in every revision lookup (e.g. hg
16 merge, hg update).
16 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 .hgrc::
21 your .hgrc::
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, hex, short
33 from mercurial import util, commands, repair, extensions
33 from mercurial import util, commands, repair, extensions
34 import os
34 import os
35
35
36 def write(repo):
36 def write(repo):
37 '''Write bookmarks
37 '''Write bookmarks
38
38
39 Write the given bookmark => hash dictionary to the .hg/bookmarks file
39 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 in a format equal to those of localtags.
40 in a format equal to those of localtags.
41
41
42 We also store a backup of the previous state in undo.bookmarks that
42 We also store a backup of the previous state in undo.bookmarks that
43 can be copied back on rollback.
43 can be copied back on rollback.
44 '''
44 '''
45 refs = repo._bookmarks
45 refs = repo._bookmarks
46 if os.path.exists(repo.join('bookmarks')):
46 if os.path.exists(repo.join('bookmarks')):
47 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
47 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 if repo._bookmarkcurrent not in refs:
48 if repo._bookmarkcurrent not in refs:
49 setcurrent(repo, None)
49 setcurrent(repo, None)
50 wlock = repo.wlock()
50 wlock = repo.wlock()
51 try:
51 try:
52 file = repo.opener('bookmarks', 'w', atomictemp=True)
52 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 for refspec, node in refs.iteritems():
53 for refspec, node in refs.iteritems():
54 file.write("%s %s\n" % (hex(node), refspec))
54 file.write("%s %s\n" % (hex(node), refspec))
55 file.rename()
55 file.rename()
56 finally:
56 finally:
57 wlock.release()
57 wlock.release()
58
58
59 def setcurrent(repo, mark):
59 def setcurrent(repo, mark):
60 '''Set the name of the bookmark that we are currently on
60 '''Set the name of the bookmark that we are currently on
61
61
62 Set the name of the bookmark that we are on (hg update <bookmark>).
62 Set the name of the bookmark that we are on (hg update <bookmark>).
63 The name is recorded in .hg/bookmarks.current
63 The name is recorded in .hg/bookmarks.current
64 '''
64 '''
65 current = repo._bookmarkcurrent
65 current = repo._bookmarkcurrent
66 if current == mark:
66 if current == mark:
67 return
67 return
68
68
69 refs = repo._bookmarks
69 refs = repo._bookmarks
70
70
71 # do not update if we do update to a rev equal to the current bookmark
71 # do not update if we do update to a rev equal to the current bookmark
72 if (mark and mark not in refs and
72 if (mark and mark not in refs and
73 current and refs[current] == repo.changectx('.').node()):
73 current and refs[current] == repo.changectx('.').node()):
74 return
74 return
75 if mark not in refs:
75 if mark not in refs:
76 mark = ''
76 mark = ''
77 wlock = repo.wlock()
77 wlock = repo.wlock()
78 try:
78 try:
79 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
79 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
80 file.write(mark)
80 file.write(mark)
81 file.rename()
81 file.rename()
82 finally:
82 finally:
83 wlock.release()
83 wlock.release()
84 repo._bookmarkcurrent = mark
84 repo._bookmarkcurrent = mark
85
85
86 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
86 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
87 '''track a line of development with movable markers
87 '''track a line of development with movable markers
88
88
89 Bookmarks are pointers to certain commits that move when
89 Bookmarks are pointers to certain commits that move when
90 committing. Bookmarks are local. They can be renamed, copied and
90 committing. Bookmarks are local. They can be renamed, copied and
91 deleted. It is possible to use bookmark names in 'hg merge' and
91 deleted. It is possible to use bookmark names in 'hg merge' and
92 'hg update' to merge and update respectively to a given bookmark.
92 'hg update' to merge and update respectively to a given bookmark.
93
93
94 You can use 'hg bookmark NAME' to set a bookmark on the working
94 You can use 'hg bookmark NAME' to set a bookmark on the working
95 directory's parent revision with the given name. If you specify
95 directory's parent revision with the given name. If you specify
96 a revision using -r REV (where REV may be an existing bookmark),
96 a revision using -r REV (where REV may be an existing bookmark),
97 the bookmark is assigned to that revision.
97 the bookmark is assigned to that revision.
98 '''
98 '''
99 hexfn = ui.debugflag and hex or short
99 hexfn = ui.debugflag and hex or short
100 marks = repo._bookmarks
100 marks = repo._bookmarks
101 cur = repo.changectx('.').node()
101 cur = repo.changectx('.').node()
102
102
103 if rename:
103 if rename:
104 if rename not in marks:
104 if rename not in marks:
105 raise util.Abort(_("a bookmark of this name does not exist"))
105 raise util.Abort(_("a bookmark of this name does not exist"))
106 if mark in marks and not force:
106 if mark in marks and not force:
107 raise util.Abort(_("a bookmark of the same name already exists"))
107 raise util.Abort(_("a bookmark of the same name already exists"))
108 if mark is None:
108 if mark is None:
109 raise util.Abort(_("new bookmark name required"))
109 raise util.Abort(_("new bookmark name required"))
110 marks[mark] = marks[rename]
110 marks[mark] = marks[rename]
111 del marks[rename]
111 del marks[rename]
112 if repo._bookmarkcurrent == rename:
112 if repo._bookmarkcurrent == rename:
113 setcurrent(repo, mark)
113 setcurrent(repo, mark)
114 write(repo)
114 write(repo)
115 return
115 return
116
116
117 if delete:
117 if delete:
118 if mark is None:
118 if mark is None:
119 raise util.Abort(_("bookmark name required"))
119 raise util.Abort(_("bookmark name required"))
120 if mark not in marks:
120 if mark not in marks:
121 raise util.Abort(_("a bookmark of this name does not exist"))
121 raise util.Abort(_("a bookmark of this name does not exist"))
122 if mark == repo._bookmarkcurrent:
122 if mark == repo._bookmarkcurrent:
123 setcurrent(repo, None)
123 setcurrent(repo, None)
124 del marks[mark]
124 del marks[mark]
125 write(repo)
125 write(repo)
126 return
126 return
127
127
128 if mark != None:
128 if mark != None:
129 if "\n" in mark:
129 if "\n" in mark:
130 raise util.Abort(_("bookmark name cannot contain newlines"))
130 raise util.Abort(_("bookmark name cannot contain newlines"))
131 mark = mark.strip()
131 mark = mark.strip()
132 if mark in marks and not force:
132 if mark in marks and not force:
133 raise util.Abort(_("a bookmark of the same name already exists"))
133 raise util.Abort(_("a bookmark of the same name already exists"))
134 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
134 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
135 and not force):
135 and not force):
136 raise util.Abort(
136 raise util.Abort(
137 _("a bookmark cannot have the name of an existing branch"))
137 _("a bookmark cannot have the name of an existing branch"))
138 if rev:
138 if rev:
139 marks[mark] = repo.lookup(rev)
139 marks[mark] = repo.lookup(rev)
140 else:
140 else:
141 marks[mark] = repo.changectx('.').node()
141 marks[mark] = repo.changectx('.').node()
142 setcurrent(repo, mark)
142 setcurrent(repo, mark)
143 write(repo)
143 write(repo)
144 return
144 return
145
145
146 if mark is None:
146 if mark is None:
147 if rev:
147 if rev:
148 raise util.Abort(_("bookmark name required"))
148 raise util.Abort(_("bookmark name required"))
149 if len(marks) == 0:
149 if len(marks) == 0:
150 ui.status(_("no bookmarks set\n"))
150 ui.status(_("no bookmarks set\n"))
151 else:
151 else:
152 for bmark, n in marks.iteritems():
152 for bmark, n in marks.iteritems():
153 if ui.configbool('bookmarks', 'track.current'):
153 if ui.configbool('bookmarks', 'track.current'):
154 current = repo._bookmarkcurrent
154 current = repo._bookmarkcurrent
155 prefix = (bmark == current and n == cur) and '*' or ' '
155 if bmark == current and n == cur:
156 prefix, label = '*', 'bookmarks.current'
157 else:
158 prefix, label = ' ', ''
156 else:
159 else:
157 prefix = (n == cur) and '*' or ' '
160 if n == cur:
161 prefix, label = '*', 'bookmarks.current'
162 else:
163 prefix, label = ' ', ''
158
164
159 if ui.quiet:
165 if ui.quiet:
160 ui.write("%s\n" % bmark)
166 ui.write("%s\n" % bmark, label=label)
161 else:
167 else:
162 ui.write(" %s %-25s %d:%s\n" % (
168 ui.write(" %s %-25s %d:%s\n" % (
163 prefix, bmark, repo.changelog.rev(n), hexfn(n)))
169 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
170 label=label)
164 return
171 return
165
172
166 def _revstostrip(changelog, node):
173 def _revstostrip(changelog, node):
167 srev = changelog.rev(node)
174 srev = changelog.rev(node)
168 tostrip = [srev]
175 tostrip = [srev]
169 saveheads = []
176 saveheads = []
170 for r in xrange(srev, len(changelog)):
177 for r in xrange(srev, len(changelog)):
171 parents = changelog.parentrevs(r)
178 parents = changelog.parentrevs(r)
172 if parents[0] in tostrip or parents[1] in tostrip:
179 if parents[0] in tostrip or parents[1] in tostrip:
173 tostrip.append(r)
180 tostrip.append(r)
174 if parents[1] != nullrev:
181 if parents[1] != nullrev:
175 for p in parents:
182 for p in parents:
176 if p not in tostrip and p > srev:
183 if p not in tostrip and p > srev:
177 saveheads.append(p)
184 saveheads.append(p)
178 return [r for r in tostrip if r not in saveheads]
185 return [r for r in tostrip if r not in saveheads]
179
186
180 def strip(oldstrip, ui, repo, node, backup="all"):
187 def strip(oldstrip, ui, repo, node, backup="all"):
181 """Strip bookmarks if revisions are stripped using
188 """Strip bookmarks if revisions are stripped using
182 the mercurial.strip method. This usually happens during
189 the mercurial.strip method. This usually happens during
183 qpush and qpop"""
190 qpush and qpop"""
184 revisions = _revstostrip(repo.changelog, node)
191 revisions = _revstostrip(repo.changelog, node)
185 marks = repo._bookmarks
192 marks = repo._bookmarks
186 update = []
193 update = []
187 for mark, n in marks.iteritems():
194 for mark, n in marks.iteritems():
188 if repo.changelog.rev(n) in revisions:
195 if repo.changelog.rev(n) in revisions:
189 update.append(mark)
196 update.append(mark)
190 oldstrip(ui, repo, node, backup)
197 oldstrip(ui, repo, node, backup)
191 if len(update) > 0:
198 if len(update) > 0:
192 for m in update:
199 for m in update:
193 marks[m] = repo.changectx('.').node()
200 marks[m] = repo.changectx('.').node()
194 write(repo)
201 write(repo)
195
202
196 def reposetup(ui, repo):
203 def reposetup(ui, repo):
197 if not repo.local():
204 if not repo.local():
198 return
205 return
199
206
200 class bookmark_repo(repo.__class__):
207 class bookmark_repo(repo.__class__):
201
208
202 @util.propertycache
209 @util.propertycache
203 def _bookmarks(self):
210 def _bookmarks(self):
204 '''Parse .hg/bookmarks file and return a dictionary
211 '''Parse .hg/bookmarks file and return a dictionary
205
212
206 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
213 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
207 in the .hg/bookmarks file. They are read returned as a dictionary
214 in the .hg/bookmarks file. They are read returned as a dictionary
208 with name => hash values.
215 with name => hash values.
209 '''
216 '''
210 try:
217 try:
211 bookmarks = {}
218 bookmarks = {}
212 for line in self.opener('bookmarks'):
219 for line in self.opener('bookmarks'):
213 sha, refspec = line.strip().split(' ', 1)
220 sha, refspec = line.strip().split(' ', 1)
214 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
221 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
215 except:
222 except:
216 pass
223 pass
217 return bookmarks
224 return bookmarks
218
225
219 @util.propertycache
226 @util.propertycache
220 def _bookmarkcurrent(self):
227 def _bookmarkcurrent(self):
221 '''Get the current bookmark
228 '''Get the current bookmark
222
229
223 If we use gittishsh branches we have a current bookmark that
230 If we use gittishsh branches we have a current bookmark that
224 we are on. This function returns the name of the bookmark. It
231 we are on. This function returns the name of the bookmark. It
225 is stored in .hg/bookmarks.current
232 is stored in .hg/bookmarks.current
226 '''
233 '''
227 mark = None
234 mark = None
228 if os.path.exists(self.join('bookmarks.current')):
235 if os.path.exists(self.join('bookmarks.current')):
229 file = self.opener('bookmarks.current')
236 file = self.opener('bookmarks.current')
230 # No readline() in posixfile_nt, reading everything is cheap
237 # No readline() in posixfile_nt, reading everything is cheap
231 mark = (file.readlines() or [''])[0]
238 mark = (file.readlines() or [''])[0]
232 if mark == '':
239 if mark == '':
233 mark = None
240 mark = None
234 file.close()
241 file.close()
235 return mark
242 return mark
236
243
237 def rollback(self):
244 def rollback(self):
238 if os.path.exists(self.join('undo.bookmarks')):
245 if os.path.exists(self.join('undo.bookmarks')):
239 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
246 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
240 return super(bookmark_repo, self).rollback()
247 return super(bookmark_repo, self).rollback()
241
248
242 def lookup(self, key):
249 def lookup(self, key):
243 if key in self._bookmarks:
250 if key in self._bookmarks:
244 key = self._bookmarks[key]
251 key = self._bookmarks[key]
245 return super(bookmark_repo, self).lookup(key)
252 return super(bookmark_repo, self).lookup(key)
246
253
247 def _bookmarksupdate(self, parents, node):
254 def _bookmarksupdate(self, parents, node):
248 marks = self._bookmarks
255 marks = self._bookmarks
249 update = False
256 update = False
250 if ui.configbool('bookmarks', 'track.current'):
257 if ui.configbool('bookmarks', 'track.current'):
251 mark = self._bookmarkcurrent
258 mark = self._bookmarkcurrent
252 if mark and marks[mark] in parents:
259 if mark and marks[mark] in parents:
253 marks[mark] = node
260 marks[mark] = node
254 update = True
261 update = True
255 else:
262 else:
256 for mark, n in marks.items():
263 for mark, n in marks.items():
257 if n in parents:
264 if n in parents:
258 marks[mark] = node
265 marks[mark] = node
259 update = True
266 update = True
260 if update:
267 if update:
261 write(self)
268 write(self)
262
269
263 def commitctx(self, ctx, error=False):
270 def commitctx(self, ctx, error=False):
264 """Add a revision to the repository and
271 """Add a revision to the repository and
265 move the bookmark"""
272 move the bookmark"""
266 wlock = self.wlock() # do both commit and bookmark with lock held
273 wlock = self.wlock() # do both commit and bookmark with lock held
267 try:
274 try:
268 node = super(bookmark_repo, self).commitctx(ctx, error)
275 node = super(bookmark_repo, self).commitctx(ctx, error)
269 if node is None:
276 if node is None:
270 return None
277 return None
271 parents = self.changelog.parents(node)
278 parents = self.changelog.parents(node)
272 if parents[1] == nullid:
279 if parents[1] == nullid:
273 parents = (parents[0],)
280 parents = (parents[0],)
274
281
275 self._bookmarksupdate(parents, node)
282 self._bookmarksupdate(parents, node)
276 return node
283 return node
277 finally:
284 finally:
278 wlock.release()
285 wlock.release()
279
286
280 def addchangegroup(self, source, srctype, url, emptyok=False):
287 def addchangegroup(self, source, srctype, url, emptyok=False):
281 parents = self.dirstate.parents()
288 parents = self.dirstate.parents()
282
289
283 result = super(bookmark_repo, self).addchangegroup(
290 result = super(bookmark_repo, self).addchangegroup(
284 source, srctype, url, emptyok)
291 source, srctype, url, emptyok)
285 if result > 1:
292 if result > 1:
286 # We have more heads than before
293 # We have more heads than before
287 return result
294 return result
288 node = self.changelog.tip()
295 node = self.changelog.tip()
289
296
290 self._bookmarksupdate(parents, node)
297 self._bookmarksupdate(parents, node)
291 return result
298 return result
292
299
293 def _findtags(self):
300 def _findtags(self):
294 """Merge bookmarks with normal tags"""
301 """Merge bookmarks with normal tags"""
295 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
302 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
296 tags.update(self._bookmarks)
303 tags.update(self._bookmarks)
297 return (tags, tagtypes)
304 return (tags, tagtypes)
298
305
299 if hasattr(repo, 'invalidate'):
306 if hasattr(repo, 'invalidate'):
300 def invalidate(self):
307 def invalidate(self):
301 super(bookmark_repo, self).invalidate()
308 super(bookmark_repo, self).invalidate()
302 for attr in ('_bookmarks', '_bookmarkcurrent'):
309 for attr in ('_bookmarks', '_bookmarkcurrent'):
303 if attr in self.__dict__:
310 if attr in self.__dict__:
304 delattr(repo, attr)
311 delattr(repo, attr)
305
312
306 repo.__class__ = bookmark_repo
313 repo.__class__ = bookmark_repo
307
314
308 def uisetup(ui):
315 def uisetup(ui):
309 extensions.wrapfunction(repair, "strip", strip)
316 extensions.wrapfunction(repair, "strip", strip)
310 if ui.configbool('bookmarks', 'track.current'):
317 if ui.configbool('bookmarks', 'track.current'):
311 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
318 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
312
319
313 def updatecurbookmark(orig, ui, repo, *args, **opts):
320 def updatecurbookmark(orig, ui, repo, *args, **opts):
314 '''Set the current bookmark
321 '''Set the current bookmark
315
322
316 If the user updates to a bookmark we update the .hg/bookmarks.current
323 If the user updates to a bookmark we update the .hg/bookmarks.current
317 file.
324 file.
318 '''
325 '''
319 res = orig(ui, repo, *args, **opts)
326 res = orig(ui, repo, *args, **opts)
320 rev = opts['rev']
327 rev = opts['rev']
321 if not rev and len(args) > 0:
328 if not rev and len(args) > 0:
322 rev = args[0]
329 rev = args[0]
323 setcurrent(repo, rev)
330 setcurrent(repo, rev)
324 return res
331 return res
325
332
326 cmdtable = {
333 cmdtable = {
327 "bookmarks":
334 "bookmarks":
328 (bookmark,
335 (bookmark,
329 [('f', 'force', False, _('force')),
336 [('f', 'force', False, _('force')),
330 ('r', 'rev', '', _('revision')),
337 ('r', 'rev', '', _('revision')),
331 ('d', 'delete', False, _('delete a given bookmark')),
338 ('d', 'delete', False, _('delete a given bookmark')),
332 ('m', 'rename', '', _('rename a given bookmark'))],
339 ('m', 'rename', '', _('rename a given bookmark'))],
333 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
340 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
334 }
341 }
General Comments 0
You need to be logged in to leave comments. Login now