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