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