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