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