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