##// END OF EJS Templates
bookmarks: move update into core
Matt Mackall -
r13352:f9cd37fc default
parent child Browse files
Show More
@@ -1,492 +1,475 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 of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''track a line of development with movable markers
9 9
10 10 Bookmarks are local movable markers to changesets. Every bookmark
11 11 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 shifts to the new changeset.
14 14
15 15 It is possible to use bookmark names in every revision lookup (e.g.
16 16 :hg:`merge`, :hg:`update`).
17 17
18 18 By default, when several bookmarks point to the same changeset, they
19 19 will all move forward together. It is possible to obtain a more
20 20 git-like experience by adding the following configuration option to
21 21 your configuration file::
22 22
23 23 [bookmarks]
24 24 track.current = True
25 25
26 26 This will cause Mercurial to track the bookmark that you are currently
27 27 using, and only update it. This is similar to git's approach to
28 28 branching.
29 29 '''
30 30
31 31 from mercurial.i18n import _
32 32 from mercurial.node import nullid, nullrev, bin, hex, short
33 33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
34 34 from mercurial import revset, encoding
35 35 from mercurial import bookmarks
36 36 import os
37 37
38 38 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
39 39 '''track a line of development with movable markers
40 40
41 41 Bookmarks are pointers to certain commits that move when
42 42 committing. Bookmarks are local. They can be renamed, copied and
43 43 deleted. It is possible to use bookmark names in :hg:`merge` and
44 44 :hg:`update` to merge and update respectively to a given bookmark.
45 45
46 46 You can use :hg:`bookmark NAME` to set a bookmark on the working
47 47 directory's parent revision with the given name. If you specify
48 48 a revision using -r REV (where REV may be an existing bookmark),
49 49 the bookmark is assigned to that revision.
50 50
51 51 Bookmarks can be pushed and pulled between repositories (see :hg:`help
52 52 push` and :hg:`help pull`). This requires the bookmark extension to be
53 53 enabled for both the local and remote repositories.
54 54 '''
55 55 hexfn = ui.debugflag and hex or short
56 56 marks = repo._bookmarks
57 57 cur = repo.changectx('.').node()
58 58
59 59 if rename:
60 60 if rename not in marks:
61 61 raise util.Abort(_("a bookmark of this name does not exist"))
62 62 if mark in marks and not force:
63 63 raise util.Abort(_("a bookmark of the same name already exists"))
64 64 if mark is None:
65 65 raise util.Abort(_("new bookmark name required"))
66 66 marks[mark] = marks[rename]
67 67 del marks[rename]
68 68 if repo._bookmarkcurrent == rename:
69 69 bookmarks.setcurrent(repo, mark)
70 70 bookmarks.write(repo)
71 71 return
72 72
73 73 if delete:
74 74 if mark is None:
75 75 raise util.Abort(_("bookmark name required"))
76 76 if mark not in marks:
77 77 raise util.Abort(_("a bookmark of this name does not exist"))
78 78 if mark == repo._bookmarkcurrent:
79 79 bookmarks.setcurrent(repo, None)
80 80 del marks[mark]
81 81 bookmarks.write(repo)
82 82 return
83 83
84 84 if mark is not None:
85 85 if "\n" in mark:
86 86 raise util.Abort(_("bookmark name cannot contain newlines"))
87 87 mark = mark.strip()
88 88 if not mark:
89 89 raise util.Abort(_("bookmark names cannot consist entirely of "
90 90 "whitespace"))
91 91 if mark in marks and not force:
92 92 raise util.Abort(_("a bookmark of the same name already exists"))
93 93 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
94 94 and not force):
95 95 raise util.Abort(
96 96 _("a bookmark cannot have the name of an existing branch"))
97 97 if rev:
98 98 marks[mark] = repo.lookup(rev)
99 99 else:
100 100 marks[mark] = repo.changectx('.').node()
101 101 bookmarks.setcurrent(repo, mark)
102 102 bookmarks.write(repo)
103 103 return
104 104
105 105 if mark is None:
106 106 if rev:
107 107 raise util.Abort(_("bookmark name required"))
108 108 if len(marks) == 0:
109 109 ui.status(_("no bookmarks set\n"))
110 110 else:
111 111 for bmark, n in marks.iteritems():
112 112 if ui.configbool('bookmarks', 'track.current'):
113 113 current = repo._bookmarkcurrent
114 114 if bmark == current and n == cur:
115 115 prefix, label = '*', 'bookmarks.current'
116 116 else:
117 117 prefix, label = ' ', ''
118 118 else:
119 119 if n == cur:
120 120 prefix, label = '*', 'bookmarks.current'
121 121 else:
122 122 prefix, label = ' ', ''
123 123
124 124 if ui.quiet:
125 125 ui.write("%s\n" % bmark, label=label)
126 126 else:
127 127 ui.write(" %s %-25s %d:%s\n" % (
128 128 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
129 129 label=label)
130 130 return
131 131
132 132 def _revstostrip(changelog, node):
133 133 srev = changelog.rev(node)
134 134 tostrip = [srev]
135 135 saveheads = []
136 136 for r in xrange(srev, len(changelog)):
137 137 parents = changelog.parentrevs(r)
138 138 if parents[0] in tostrip or parents[1] in tostrip:
139 139 tostrip.append(r)
140 140 if parents[1] != nullrev:
141 141 for p in parents:
142 142 if p not in tostrip and p > srev:
143 143 saveheads.append(p)
144 144 return [r for r in tostrip if r not in saveheads]
145 145
146 146 def strip(oldstrip, ui, repo, node, backup="all"):
147 147 """Strip bookmarks if revisions are stripped using
148 148 the mercurial.strip method. This usually happens during
149 149 qpush and qpop"""
150 150 revisions = _revstostrip(repo.changelog, node)
151 151 marks = repo._bookmarks
152 152 update = []
153 153 for mark, n in marks.iteritems():
154 154 if repo.changelog.rev(n) in revisions:
155 155 update.append(mark)
156 156 oldstrip(ui, repo, node, backup)
157 157 if len(update) > 0:
158 158 for m in update:
159 159 marks[m] = repo.changectx('.').node()
160 160 bookmarks.write(repo)
161 161
162 162 def reposetup(ui, repo):
163 163 if not repo.local():
164 164 return
165 165
166 166 class bookmark_repo(repo.__class__):
167
168 167 @util.propertycache
169 168 def _bookmarks(self):
170 169 return bookmarks.read(self)
171 170
172 171 @util.propertycache
173 172 def _bookmarkcurrent(self):
174 173 return bookmarks.readcurrent(self)
175 174
176 175 def rollback(self, dryrun=False):
177 176 if os.path.exists(self.join('undo.bookmarks')):
178 177 if not dryrun:
179 178 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
180 179 elif not os.path.exists(self.sjoin("undo")):
181 180 # avoid "no rollback information available" message
182 181 return 0
183 182 return super(bookmark_repo, self).rollback(dryrun)
184 183
185 184 def lookup(self, key):
186 185 if key in self._bookmarks:
187 186 key = self._bookmarks[key]
188 187 return super(bookmark_repo, self).lookup(key)
189 188
190 def _bookmarksupdate(self, parents, node):
191 marks = self._bookmarks
192 update = False
193 if ui.configbool('bookmarks', 'track.current'):
194 mark = self._bookmarkcurrent
195 if mark and marks[mark] in parents:
196 marks[mark] = node
197 update = True
198 else:
199 for mark, n in marks.items():
200 if n in parents:
201 marks[mark] = node
202 update = True
203 if update:
204 bookmarks.write(self)
205
206 189 def commitctx(self, ctx, error=False):
207 190 """Add a revision to the repository and
208 191 move the bookmark"""
209 192 wlock = self.wlock() # do both commit and bookmark with lock held
210 193 try:
211 194 node = super(bookmark_repo, self).commitctx(ctx, error)
212 195 if node is None:
213 196 return None
214 197 parents = self.changelog.parents(node)
215 198 if parents[1] == nullid:
216 199 parents = (parents[0],)
217 200
218 self._bookmarksupdate(parents, node)
201 bookmarks.update(self, parents, node)
219 202 return node
220 203 finally:
221 204 wlock.release()
222 205
223 206 def pull(self, remote, heads=None, force=False):
224 207 result = super(bookmark_repo, self).pull(remote, heads, force)
225 208
226 209 self.ui.debug("checking for updated bookmarks\n")
227 210 rb = remote.listkeys('bookmarks')
228 211 changed = False
229 212 for k in rb.keys():
230 213 if k in self._bookmarks:
231 214 nr, nl = rb[k], self._bookmarks[k]
232 215 if nr in self:
233 216 cr = self[nr]
234 217 cl = self[nl]
235 218 if cl.rev() >= cr.rev():
236 219 continue
237 220 if cr in cl.descendants():
238 221 self._bookmarks[k] = cr.node()
239 222 changed = True
240 223 self.ui.status(_("updating bookmark %s\n") % k)
241 224 else:
242 225 self.ui.warn(_("not updating divergent"
243 226 " bookmark %s\n") % k)
244 227 if changed:
245 228 bookmarks.write(repo)
246 229
247 230 return result
248 231
249 232 def push(self, remote, force=False, revs=None, newbranch=False):
250 233 result = super(bookmark_repo, self).push(remote, force, revs,
251 234 newbranch)
252 235
253 236 self.ui.debug("checking for updated bookmarks\n")
254 237 rb = remote.listkeys('bookmarks')
255 238 for k in rb.keys():
256 239 if k in self._bookmarks:
257 240 nr, nl = rb[k], hex(self._bookmarks[k])
258 241 if nr in self:
259 242 cr = self[nr]
260 243 cl = self[nl]
261 244 if cl in cr.descendants():
262 245 r = remote.pushkey('bookmarks', k, nr, nl)
263 246 if r:
264 247 self.ui.status(_("updating bookmark %s\n") % k)
265 248 else:
266 249 self.ui.warn(_('updating bookmark %s'
267 250 ' failed!\n') % k)
268 251
269 252 return result
270 253
271 254 def addchangegroup(self, *args, **kwargs):
272 255 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
273 256 if result > 1:
274 257 # We have more heads than before
275 258 return result
276 259 node = self.changelog.tip()
277 260 parents = self.dirstate.parents()
278 self._bookmarksupdate(parents, node)
261 bookmarks.update(self, parents, node)
279 262 return result
280 263
281 264 def _findtags(self):
282 265 """Merge bookmarks with normal tags"""
283 266 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
284 267 tags.update(self._bookmarks)
285 268 return (tags, tagtypes)
286 269
287 270 if hasattr(repo, 'invalidate'):
288 271 def invalidate(self):
289 272 super(bookmark_repo, self).invalidate()
290 273 for attr in ('_bookmarks', '_bookmarkcurrent'):
291 274 if attr in self.__dict__:
292 275 delattr(self, attr)
293 276
294 277 repo.__class__ = bookmark_repo
295 278
296 279 def listbookmarks(repo):
297 280 # We may try to list bookmarks on a repo type that does not
298 281 # support it (e.g., statichttprepository).
299 282 if not hasattr(repo, '_bookmarks'):
300 283 return {}
301 284
302 285 d = {}
303 286 for k, v in repo._bookmarks.iteritems():
304 287 d[k] = hex(v)
305 288 return d
306 289
307 290 def pushbookmark(repo, key, old, new):
308 291 w = repo.wlock()
309 292 try:
310 293 marks = repo._bookmarks
311 294 if hex(marks.get(key, '')) != old:
312 295 return False
313 296 if new == '':
314 297 del marks[key]
315 298 else:
316 299 if new not in repo:
317 300 return False
318 301 marks[key] = repo[new].node()
319 302 bookmarks.write(repo)
320 303 return True
321 304 finally:
322 305 w.release()
323 306
324 307 def pull(oldpull, ui, repo, source="default", **opts):
325 308 # translate bookmark args to rev args for actual pull
326 309 if opts.get('bookmark'):
327 310 # this is an unpleasant hack as pull will do this internally
328 311 source, branches = hg.parseurl(ui.expandpath(source),
329 312 opts.get('branch'))
330 313 other = hg.repository(hg.remoteui(repo, opts), source)
331 314 rb = other.listkeys('bookmarks')
332 315
333 316 for b in opts['bookmark']:
334 317 if b not in rb:
335 318 raise util.Abort(_('remote bookmark %s not found!') % b)
336 319 opts.setdefault('rev', []).append(b)
337 320
338 321 result = oldpull(ui, repo, source, **opts)
339 322
340 323 # update specified bookmarks
341 324 if opts.get('bookmark'):
342 325 for b in opts['bookmark']:
343 326 # explicit pull overrides local bookmark if any
344 327 ui.status(_("importing bookmark %s\n") % b)
345 328 repo._bookmarks[b] = repo[rb[b]].node()
346 329 bookmarks.write(repo)
347 330
348 331 return result
349 332
350 333 def push(oldpush, ui, repo, dest=None, **opts):
351 334 dopush = True
352 335 if opts.get('bookmark'):
353 336 dopush = False
354 337 for b in opts['bookmark']:
355 338 if b in repo._bookmarks:
356 339 dopush = True
357 340 opts.setdefault('rev', []).append(b)
358 341
359 342 result = 0
360 343 if dopush:
361 344 result = oldpush(ui, repo, dest, **opts)
362 345
363 346 if opts.get('bookmark'):
364 347 # this is an unpleasant hack as push will do this internally
365 348 dest = ui.expandpath(dest or 'default-push', dest or 'default')
366 349 dest, branches = hg.parseurl(dest, opts.get('branch'))
367 350 other = hg.repository(hg.remoteui(repo, opts), dest)
368 351 rb = other.listkeys('bookmarks')
369 352 for b in opts['bookmark']:
370 353 # explicit push overrides remote bookmark if any
371 354 if b in repo._bookmarks:
372 355 ui.status(_("exporting bookmark %s\n") % b)
373 356 new = repo[b].hex()
374 357 elif b in rb:
375 358 ui.status(_("deleting remote bookmark %s\n") % b)
376 359 new = '' # delete
377 360 else:
378 361 ui.warn(_('bookmark %s does not exist on the local '
379 362 'or remote repository!\n') % b)
380 363 return 2
381 364 old = rb.get(b, '')
382 365 r = other.pushkey('bookmarks', b, old, new)
383 366 if not r:
384 367 ui.warn(_('updating bookmark %s failed!\n') % b)
385 368 if not result:
386 369 result = 2
387 370
388 371 return result
389 372
390 373 def diffbookmarks(ui, repo, remote):
391 374 ui.status(_("searching for changed bookmarks\n"))
392 375
393 376 lmarks = repo.listkeys('bookmarks')
394 377 rmarks = remote.listkeys('bookmarks')
395 378
396 379 diff = sorted(set(rmarks) - set(lmarks))
397 380 for k in diff:
398 381 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
399 382
400 383 if len(diff) <= 0:
401 384 ui.status(_("no changed bookmarks found\n"))
402 385 return 1
403 386 return 0
404 387
405 388 def incoming(oldincoming, ui, repo, source="default", **opts):
406 389 if opts.get('bookmarks'):
407 390 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
408 391 other = hg.repository(hg.remoteui(repo, opts), source)
409 392 ui.status(_('comparing with %s\n') % url.hidepassword(source))
410 393 return diffbookmarks(ui, repo, other)
411 394 else:
412 395 return oldincoming(ui, repo, source, **opts)
413 396
414 397 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
415 398 if opts.get('bookmarks'):
416 399 dest = ui.expandpath(dest or 'default-push', dest or 'default')
417 400 dest, branches = hg.parseurl(dest, opts.get('branch'))
418 401 other = hg.repository(hg.remoteui(repo, opts), dest)
419 402 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
420 403 return diffbookmarks(ui, other, repo)
421 404 else:
422 405 return oldoutgoing(ui, repo, dest, **opts)
423 406
424 407 def uisetup(ui):
425 408 extensions.wrapfunction(repair, "strip", strip)
426 409 if ui.configbool('bookmarks', 'track.current'):
427 410 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
428 411
429 412 entry = extensions.wrapcommand(commands.table, 'pull', pull)
430 413 entry[1].append(('B', 'bookmark', [],
431 414 _("bookmark to import"),
432 415 _('BOOKMARK')))
433 416 entry = extensions.wrapcommand(commands.table, 'push', push)
434 417 entry[1].append(('B', 'bookmark', [],
435 418 _("bookmark to export"),
436 419 _('BOOKMARK')))
437 420 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
438 421 entry[1].append(('B', 'bookmarks', False,
439 422 _("compare bookmark")))
440 423 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
441 424 entry[1].append(('B', 'bookmarks', False,
442 425 _("compare bookmark")))
443 426
444 427 pushkey.register('bookmarks', pushbookmark, listbookmarks)
445 428
446 429 def updatecurbookmark(orig, ui, repo, *args, **opts):
447 430 '''Set the current bookmark
448 431
449 432 If the user updates to a bookmark we update the .hg/bookmarks.current
450 433 file.
451 434 '''
452 435 res = orig(ui, repo, *args, **opts)
453 436 rev = opts['rev']
454 437 if not rev and len(args) > 0:
455 438 rev = args[0]
456 439 bookmarks.setcurrent(repo, rev)
457 440 return res
458 441
459 442 def bmrevset(repo, subset, x):
460 443 """``bookmark([name])``
461 444 The named bookmark or all bookmarks.
462 445 """
463 446 # i18n: "bookmark" is a keyword
464 447 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
465 448 if args:
466 449 bm = revset.getstring(args[0],
467 450 # i18n: "bookmark" is a keyword
468 451 _('the argument to bookmark must be a string'))
469 452 bmrev = listbookmarks(repo).get(bm, None)
470 453 if bmrev:
471 454 bmrev = repo.changelog.rev(bin(bmrev))
472 455 return [r for r in subset if r == bmrev]
473 456 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
474 457 return [r for r in subset if r in bms]
475 458
476 459 def extsetup(ui):
477 460 revset.symbols['bookmark'] = bmrevset
478 461
479 462 cmdtable = {
480 463 "bookmarks":
481 464 (bookmark,
482 465 [('f', 'force', False, _('force')),
483 466 ('r', 'rev', '', _('revision'), _('REV')),
484 467 ('d', 'delete', False, _('delete a given bookmark')),
485 468 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
486 469 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
487 470 }
488 471
489 472 colortable = {'bookmarks.current': 'green'}
490 473
491 474 # tell hggettext to extract docstrings from these functions:
492 475 i18nfunctions = [bmrevset]
@@ -1,107 +1,123 b''
1 1 # Mercurial bookmark support code
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 of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial.node import nullid, nullrev, bin, hex, short
10 10 from mercurial import encoding
11 11 import os
12 12
13 13 def read(repo):
14 14 '''Parse .hg/bookmarks file and return a dictionary
15 15
16 16 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
17 17 in the .hg/bookmarks file.
18 18 Read the file and return a (name=>nodeid) dictionary
19 19 '''
20 20 try:
21 21 bookmarks = {}
22 22 for line in repo.opener('bookmarks'):
23 23 sha, refspec = line.strip().split(' ', 1)
24 24 refspec = encoding.tolocal(refspec)
25 25 bookmarks[refspec] = repo.changelog.lookup(sha)
26 26 except:
27 27 pass
28 28 return bookmarks
29 29
30 30 def readcurrent(repo):
31 31 '''Get the current bookmark
32 32
33 33 If we use gittishsh branches we have a current bookmark that
34 34 we are on. This function returns the name of the bookmark. It
35 35 is stored in .hg/bookmarks.current
36 36 '''
37 37 mark = None
38 38 if os.path.exists(repo.join('bookmarks.current')):
39 39 file = repo.opener('bookmarks.current')
40 40 # No readline() in posixfile_nt, reading everything is cheap
41 41 mark = (file.readlines() or [''])[0]
42 42 if mark == '':
43 43 mark = None
44 44 file.close()
45 45 return mark
46 46
47 47 def write(repo):
48 48 '''Write bookmarks
49 49
50 50 Write the given bookmark => hash dictionary to the .hg/bookmarks file
51 51 in a format equal to those of localtags.
52 52
53 53 We also store a backup of the previous state in undo.bookmarks that
54 54 can be copied back on rollback.
55 55 '''
56 56 refs = repo._bookmarks
57 57
58 58 try:
59 59 bms = repo.opener('bookmarks').read()
60 60 except IOError:
61 61 bms = ''
62 62 repo.opener('undo.bookmarks', 'w').write(bms)
63 63
64 64 if repo._bookmarkcurrent not in refs:
65 65 setcurrent(repo, None)
66 66 wlock = repo.wlock()
67 67 try:
68 68 file = repo.opener('bookmarks', 'w', atomictemp=True)
69 69 for refspec, node in refs.iteritems():
70 70 file.write("%s %s\n" % (hex(node), encoding.fromlocal(refspec)))
71 71 file.rename()
72 72
73 73 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
74 74 try:
75 75 os.utime(repo.sjoin('00changelog.i'), None)
76 76 except OSError:
77 77 pass
78 78
79 79 finally:
80 80 wlock.release()
81 81
82 82 def setcurrent(repo, mark):
83 83 '''Set the name of the bookmark that we are currently on
84 84
85 85 Set the name of the bookmark that we are on (hg update <bookmark>).
86 86 The name is recorded in .hg/bookmarks.current
87 87 '''
88 88 current = repo._bookmarkcurrent
89 89 if current == mark:
90 90 return
91 91
92 92 refs = repo._bookmarks
93 93
94 94 # do not update if we do update to a rev equal to the current bookmark
95 95 if (mark and mark not in refs and
96 96 current and refs[current] == repo.changectx('.').node()):
97 97 return
98 98 if mark not in refs:
99 99 mark = ''
100 100 wlock = repo.wlock()
101 101 try:
102 102 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
103 103 file.write(mark)
104 104 file.rename()
105 105 finally:
106 106 wlock.release()
107 107 repo._bookmarkcurrent = mark
108
109 def update(repo, parents, node):
110 marks = repo._bookmarks
111 update = False
112 if repo.ui.configbool('bookmarks', 'track.current'):
113 mark = repo._bookmarkcurrent
114 if mark and marks[mark] in parents:
115 marks[mark] = node
116 update = True
117 else:
118 for mark, n in marks.items():
119 if n in parents:
120 marks[mark] = node
121 update = True
122 if update:
123 write(repo)
General Comments 0
You need to be logged in to leave comments. Login now