##// END OF EJS Templates
bookmarks: add i18n hints to bookmark sync states
Wagner Bruna -
r24832:5947a68f stable
parent child Browse files
Show More
@@ -1,547 +1,556
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 import os
9 9 from mercurial.i18n import _
10 10 from mercurial.node import hex, bin
11 11 from mercurial import encoding, error, util, obsolete, lock as lockmod
12 12 import errno
13 13
14 14 class bmstore(dict):
15 15 """Storage for bookmarks.
16 16
17 17 This object should do all bookmark reads and writes, so that it's
18 18 fairly simple to replace the storage underlying bookmarks without
19 19 having to clone the logic surrounding bookmarks.
20 20
21 21 This particular bmstore implementation stores bookmarks as
22 22 {hash}\s{name}\n (the same format as localtags) in
23 23 .hg/bookmarks. The mapping is stored as {name: nodeid}.
24 24
25 25 This class does NOT handle the "current" bookmark state at this
26 26 time.
27 27 """
28 28
29 29 def __init__(self, repo):
30 30 dict.__init__(self)
31 31 self._repo = repo
32 32 try:
33 33 bkfile = self.getbkfile(repo)
34 34 for line in bkfile:
35 35 line = line.strip()
36 36 if not line:
37 37 continue
38 38 if ' ' not in line:
39 39 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
40 40 % line)
41 41 continue
42 42 sha, refspec = line.split(' ', 1)
43 43 refspec = encoding.tolocal(refspec)
44 44 try:
45 45 self[refspec] = repo.changelog.lookup(sha)
46 46 except LookupError:
47 47 pass
48 48 except IOError, inst:
49 49 if inst.errno != errno.ENOENT:
50 50 raise
51 51
52 52 def getbkfile(self, repo):
53 53 bkfile = None
54 54 if 'HG_PENDING' in os.environ:
55 55 try:
56 56 bkfile = repo.vfs('bookmarks.pending')
57 57 except IOError, inst:
58 58 if inst.errno != errno.ENOENT:
59 59 raise
60 60 if bkfile is None:
61 61 bkfile = repo.vfs('bookmarks')
62 62 return bkfile
63 63
64 64 def recordchange(self, tr):
65 65 """record that bookmarks have been changed in a transaction
66 66
67 67 The transaction is then responsible for updating the file content."""
68 68 tr.addfilegenerator('bookmarks', ('bookmarks',), self._write,
69 69 location='plain')
70 70 tr.hookargs['bookmark_moved'] = '1'
71 71
72 72 def write(self):
73 73 '''Write bookmarks
74 74
75 75 Write the given bookmark => hash dictionary to the .hg/bookmarks file
76 76 in a format equal to those of localtags.
77 77
78 78 We also store a backup of the previous state in undo.bookmarks that
79 79 can be copied back on rollback.
80 80 '''
81 81 repo = self._repo
82 82 self._writerepo(repo)
83 83
84 84 def _writerepo(self, repo):
85 85 """Factored out for extensibility"""
86 86 if repo._bookmarkcurrent not in self:
87 87 unsetcurrent(repo)
88 88
89 89 wlock = repo.wlock()
90 90 try:
91 91
92 92 file = repo.vfs('bookmarks', 'w', atomictemp=True)
93 93 self._write(file)
94 94 file.close()
95 95
96 96 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
97 97 try:
98 98 repo.svfs.utime('00changelog.i', None)
99 99 except OSError:
100 100 pass
101 101
102 102 finally:
103 103 wlock.release()
104 104
105 105 def _write(self, fp):
106 106 for name, node in self.iteritems():
107 107 fp.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
108 108
109 109 def readcurrent(repo):
110 110 '''Get the current bookmark
111 111
112 112 If we use gittish branches we have a current bookmark that
113 113 we are on. This function returns the name of the bookmark. It
114 114 is stored in .hg/bookmarks.current
115 115 '''
116 116 mark = None
117 117 try:
118 118 file = repo.vfs('bookmarks.current')
119 119 except IOError, inst:
120 120 if inst.errno != errno.ENOENT:
121 121 raise
122 122 return None
123 123 try:
124 124 # No readline() in osutil.posixfile, reading everything is cheap
125 125 mark = encoding.tolocal((file.readlines() or [''])[0])
126 126 if mark == '' or mark not in repo._bookmarks:
127 127 mark = None
128 128 finally:
129 129 file.close()
130 130 return mark
131 131
132 132 def setcurrent(repo, mark):
133 133 '''Set the name of the bookmark that we are currently on
134 134
135 135 Set the name of the bookmark that we are on (hg update <bookmark>).
136 136 The name is recorded in .hg/bookmarks.current
137 137 '''
138 138 if mark not in repo._bookmarks:
139 139 raise AssertionError('bookmark %s does not exist!' % mark)
140 140
141 141 current = repo._bookmarkcurrent
142 142 if current == mark:
143 143 return
144 144
145 145 wlock = repo.wlock()
146 146 try:
147 147 file = repo.vfs('bookmarks.current', 'w', atomictemp=True)
148 148 file.write(encoding.fromlocal(mark))
149 149 file.close()
150 150 finally:
151 151 wlock.release()
152 152 repo._bookmarkcurrent = mark
153 153
154 154 def unsetcurrent(repo):
155 155 wlock = repo.wlock()
156 156 try:
157 157 try:
158 158 repo.vfs.unlink('bookmarks.current')
159 159 repo._bookmarkcurrent = None
160 160 except OSError, inst:
161 161 if inst.errno != errno.ENOENT:
162 162 raise
163 163 finally:
164 164 wlock.release()
165 165
166 166 def iscurrent(repo, mark=None, parents=None):
167 167 '''Tell whether the current bookmark is also active
168 168
169 169 I.e., the bookmark listed in .hg/bookmarks.current also points to a
170 170 parent of the working directory.
171 171 '''
172 172 if not mark:
173 173 mark = repo._bookmarkcurrent
174 174 if not parents:
175 175 parents = [p.node() for p in repo[None].parents()]
176 176 marks = repo._bookmarks
177 177 return (mark in marks and marks[mark] in parents)
178 178
179 179 def updatecurrentbookmark(repo, oldnode, curbranch):
180 180 try:
181 181 return update(repo, oldnode, repo.branchtip(curbranch))
182 182 except error.RepoLookupError:
183 183 if curbranch == "default": # no default branch!
184 184 return update(repo, oldnode, repo.lookup("tip"))
185 185 else:
186 186 raise util.Abort(_("branch %s not found") % curbranch)
187 187
188 188 def deletedivergent(repo, deletefrom, bm):
189 189 '''Delete divergent versions of bm on nodes in deletefrom.
190 190
191 191 Return True if at least one bookmark was deleted, False otherwise.'''
192 192 deleted = False
193 193 marks = repo._bookmarks
194 194 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
195 195 for mark in divergent:
196 196 if mark == '@' or '@' not in mark:
197 197 # can't be divergent by definition
198 198 continue
199 199 if mark and marks[mark] in deletefrom:
200 200 if mark != bm:
201 201 del marks[mark]
202 202 deleted = True
203 203 return deleted
204 204
205 205 def calculateupdate(ui, repo, checkout):
206 206 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
207 207 check out and where to move the active bookmark from, if needed.'''
208 208 movemarkfrom = None
209 209 if checkout is None:
210 210 curmark = repo._bookmarkcurrent
211 211 if iscurrent(repo):
212 212 movemarkfrom = repo['.'].node()
213 213 elif curmark:
214 214 ui.status(_("updating to active bookmark %s\n") % curmark)
215 215 checkout = curmark
216 216 return (checkout, movemarkfrom)
217 217
218 218 def update(repo, parents, node):
219 219 deletefrom = parents
220 220 marks = repo._bookmarks
221 221 update = False
222 222 cur = repo._bookmarkcurrent
223 223 if not cur:
224 224 return False
225 225
226 226 if marks[cur] in parents:
227 227 new = repo[node]
228 228 divs = [repo[b] for b in marks
229 229 if b.split('@', 1)[0] == cur.split('@', 1)[0]]
230 230 anc = repo.changelog.ancestors([new.rev()])
231 231 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
232 232 if validdest(repo, repo[marks[cur]], new):
233 233 marks[cur] = new.node()
234 234 update = True
235 235
236 236 if deletedivergent(repo, deletefrom, cur):
237 237 update = True
238 238
239 239 if update:
240 240 marks.write()
241 241 return update
242 242
243 243 def listbookmarks(repo):
244 244 # We may try to list bookmarks on a repo type that does not
245 245 # support it (e.g., statichttprepository).
246 246 marks = getattr(repo, '_bookmarks', {})
247 247
248 248 d = {}
249 249 hasnode = repo.changelog.hasnode
250 250 for k, v in marks.iteritems():
251 251 # don't expose local divergent bookmarks
252 252 if hasnode(v) and ('@' not in k or k.endswith('@')):
253 253 d[k] = hex(v)
254 254 return d
255 255
256 256 def pushbookmark(repo, key, old, new):
257 257 w = l = tr = None
258 258 try:
259 259 w = repo.wlock()
260 260 l = repo.lock()
261 261 tr = repo.transaction('bookmarks')
262 262 marks = repo._bookmarks
263 263 existing = hex(marks.get(key, ''))
264 264 if existing != old and existing != new:
265 265 return False
266 266 if new == '':
267 267 del marks[key]
268 268 else:
269 269 if new not in repo:
270 270 return False
271 271 marks[key] = repo[new].node()
272 272 marks.recordchange(tr)
273 273 tr.close()
274 274 return True
275 275 finally:
276 276 lockmod.release(tr, l, w)
277 277
278 278 def compare(repo, srcmarks, dstmarks,
279 279 srchex=None, dsthex=None, targets=None):
280 280 '''Compare bookmarks between srcmarks and dstmarks
281 281
282 282 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
283 283 differ, invalid)", each are list of bookmarks below:
284 284
285 285 :addsrc: added on src side (removed on dst side, perhaps)
286 286 :adddst: added on dst side (removed on src side, perhaps)
287 287 :advsrc: advanced on src side
288 288 :advdst: advanced on dst side
289 289 :diverge: diverge
290 290 :differ: changed, but changeset referred on src is unknown on dst
291 291 :invalid: unknown on both side
292 292 :same: same on both side
293 293
294 294 Each elements of lists in result tuple is tuple "(bookmark name,
295 295 changeset ID on source side, changeset ID on destination
296 296 side)". Each changeset IDs are 40 hexadecimal digit string or
297 297 None.
298 298
299 299 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
300 300 "invalid" list may be unknown for repo.
301 301
302 302 This function expects that "srcmarks" and "dstmarks" return
303 303 changeset ID in 40 hexadecimal digit string for specified
304 304 bookmark. If not so (e.g. bmstore "repo._bookmarks" returning
305 305 binary value), "srchex" or "dsthex" should be specified to convert
306 306 into such form.
307 307
308 308 If "targets" is specified, only bookmarks listed in it are
309 309 examined.
310 310 '''
311 311 if not srchex:
312 312 srchex = lambda x: x
313 313 if not dsthex:
314 314 dsthex = lambda x: x
315 315
316 316 if targets:
317 317 bset = set(targets)
318 318 else:
319 319 srcmarkset = set(srcmarks)
320 320 dstmarkset = set(dstmarks)
321 321 bset = srcmarkset | dstmarkset
322 322
323 323 results = ([], [], [], [], [], [], [], [])
324 324 addsrc = results[0].append
325 325 adddst = results[1].append
326 326 advsrc = results[2].append
327 327 advdst = results[3].append
328 328 diverge = results[4].append
329 329 differ = results[5].append
330 330 invalid = results[6].append
331 331 same = results[7].append
332 332
333 333 for b in sorted(bset):
334 334 if b not in srcmarks:
335 335 if b in dstmarks:
336 336 adddst((b, None, dsthex(dstmarks[b])))
337 337 else:
338 338 invalid((b, None, None))
339 339 elif b not in dstmarks:
340 340 addsrc((b, srchex(srcmarks[b]), None))
341 341 else:
342 342 scid = srchex(srcmarks[b])
343 343 dcid = dsthex(dstmarks[b])
344 344 if scid == dcid:
345 345 same((b, scid, dcid))
346 346 elif scid in repo and dcid in repo:
347 347 sctx = repo[scid]
348 348 dctx = repo[dcid]
349 349 if sctx.rev() < dctx.rev():
350 350 if validdest(repo, sctx, dctx):
351 351 advdst((b, scid, dcid))
352 352 else:
353 353 diverge((b, scid, dcid))
354 354 else:
355 355 if validdest(repo, dctx, sctx):
356 356 advsrc((b, scid, dcid))
357 357 else:
358 358 diverge((b, scid, dcid))
359 359 else:
360 360 # it is too expensive to examine in detail, in this case
361 361 differ((b, scid, dcid))
362 362
363 363 return results
364 364
365 365 def _diverge(ui, b, path, localmarks, remotenode):
366 366 '''Return appropriate diverged bookmark for specified ``path``
367 367
368 368 This returns None, if it is failed to assign any divergent
369 369 bookmark name.
370 370
371 371 This reuses already existing one with "@number" suffix, if it
372 372 refers ``remotenode``.
373 373 '''
374 374 if b == '@':
375 375 b = ''
376 376 # try to use an @pathalias suffix
377 377 # if an @pathalias already exists, we overwrite (update) it
378 378 if path.startswith("file:"):
379 379 path = util.url(path).path
380 380 for p, u in ui.configitems("paths"):
381 381 if u.startswith("file:"):
382 382 u = util.url(u).path
383 383 if path == u:
384 384 return '%s@%s' % (b, p)
385 385
386 386 # assign a unique "@number" suffix newly
387 387 for x in range(1, 100):
388 388 n = '%s@%d' % (b, x)
389 389 if n not in localmarks or localmarks[n] == remotenode:
390 390 return n
391 391
392 392 return None
393 393
394 394 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
395 395 ui.debug("checking for updated bookmarks\n")
396 396 localmarks = repo._bookmarks
397 397 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
398 398 ) = compare(repo, remotemarks, localmarks, dsthex=hex)
399 399
400 400 status = ui.status
401 401 warn = ui.warn
402 402 if ui.configbool('ui', 'quietbookmarkmove', False):
403 403 status = warn = ui.debug
404 404
405 405 explicit = set(explicit)
406 406 changed = []
407 407 for b, scid, dcid in addsrc:
408 408 if scid in repo: # add remote bookmarks for changes we already have
409 409 changed.append((b, bin(scid), status,
410 410 _("adding remote bookmark %s\n") % (b)))
411 411 for b, scid, dcid in advsrc:
412 412 changed.append((b, bin(scid), status,
413 413 _("updating bookmark %s\n") % (b)))
414 414 # remove normal movement from explicit set
415 415 explicit.difference_update(d[0] for d in changed)
416 416
417 417 for b, scid, dcid in diverge:
418 418 if b in explicit:
419 419 explicit.discard(b)
420 420 changed.append((b, bin(scid), status,
421 421 _("importing bookmark %s\n") % (b)))
422 422 else:
423 423 snode = bin(scid)
424 424 db = _diverge(ui, b, path, localmarks, snode)
425 425 if db:
426 426 changed.append((db, snode, warn,
427 427 _("divergent bookmark %s stored as %s\n") %
428 428 (b, db)))
429 429 else:
430 430 warn(_("warning: failed to assign numbered name "
431 431 "to divergent bookmark %s\n") % (b))
432 432 for b, scid, dcid in adddst + advdst:
433 433 if b in explicit:
434 434 explicit.discard(b)
435 435 changed.append((b, bin(scid), status,
436 436 _("importing bookmark %s\n") % (b)))
437 437
438 438 if changed:
439 439 tr = trfunc()
440 440 for b, node, writer, msg in sorted(changed):
441 441 localmarks[b] = node
442 442 writer(msg)
443 443 localmarks.recordchange(tr)
444 444
445 445 def incoming(ui, repo, other):
446 446 '''Show bookmarks incoming from other to repo
447 447 '''
448 448 ui.status(_("searching for changed bookmarks\n"))
449 449
450 450 r = compare(repo, other.listkeys('bookmarks'), repo._bookmarks,
451 451 dsthex=hex)
452 452 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
453 453
454 454 incomings = []
455 455 if ui.debugflag:
456 456 getid = lambda id: id
457 457 else:
458 458 getid = lambda id: id[:12]
459 459 if ui.verbose:
460 460 def add(b, id, st):
461 461 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
462 462 else:
463 463 def add(b, id, st):
464 464 incomings.append(" %-25s %s\n" % (b, getid(id)))
465 465 for b, scid, dcid in addsrc:
466 # i18n: "added" refers to a bookmark
466 467 add(b, scid, _('added'))
467 468 for b, scid, dcid in advsrc:
469 # i18n: "advanced" refers to a bookmark
468 470 add(b, scid, _('advanced'))
469 471 for b, scid, dcid in diverge:
472 # i18n: "diverged" refers to a bookmark
470 473 add(b, scid, _('diverged'))
471 474 for b, scid, dcid in differ:
475 # i18n: "changed" refers to a bookmark
472 476 add(b, scid, _('changed'))
473 477
474 478 if not incomings:
475 479 ui.status(_("no changed bookmarks found\n"))
476 480 return 1
477 481
478 482 for s in sorted(incomings):
479 483 ui.write(s)
480 484
481 485 return 0
482 486
483 487 def outgoing(ui, repo, other):
484 488 '''Show bookmarks outgoing from repo to other
485 489 '''
486 490 ui.status(_("searching for changed bookmarks\n"))
487 491
488 492 r = compare(repo, repo._bookmarks, other.listkeys('bookmarks'),
489 493 srchex=hex)
490 494 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
491 495
492 496 outgoings = []
493 497 if ui.debugflag:
494 498 getid = lambda id: id
495 499 else:
496 500 getid = lambda id: id[:12]
497 501 if ui.verbose:
498 502 def add(b, id, st):
499 503 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
500 504 else:
501 505 def add(b, id, st):
502 506 outgoings.append(" %-25s %s\n" % (b, getid(id)))
503 507 for b, scid, dcid in addsrc:
508 # i18n: "added refers to a bookmark
504 509 add(b, scid, _('added'))
505 510 for b, scid, dcid in adddst:
511 # i18n: "deleted" refers to a bookmark
506 512 add(b, ' ' * 40, _('deleted'))
507 513 for b, scid, dcid in advsrc:
514 # i18n: "advanced" refers to a bookmark
508 515 add(b, scid, _('advanced'))
509 516 for b, scid, dcid in diverge:
517 # i18n: "diverged" refers to a bookmark
510 518 add(b, scid, _('diverged'))
511 519 for b, scid, dcid in differ:
520 # i18n: "changed" refers to a bookmark
512 521 add(b, scid, _('changed'))
513 522
514 523 if not outgoings:
515 524 ui.status(_("no changed bookmarks found\n"))
516 525 return 1
517 526
518 527 for s in sorted(outgoings):
519 528 ui.write(s)
520 529
521 530 return 0
522 531
523 532 def summary(repo, other):
524 533 '''Compare bookmarks between repo and other for "hg summary" output
525 534
526 535 This returns "(# of incoming, # of outgoing)" tuple.
527 536 '''
528 537 r = compare(repo, other.listkeys('bookmarks'), repo._bookmarks,
529 538 dsthex=hex)
530 539 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
531 540 return (len(addsrc), len(adddst))
532 541
533 542 def validdest(repo, old, new):
534 543 """Is the new bookmark destination a valid update from the old one"""
535 544 repo = repo.unfiltered()
536 545 if old == new:
537 546 # Old == new -> nothing to update.
538 547 return False
539 548 elif not old:
540 549 # old is nullrev, anything is valid.
541 550 # (new != nullrev has been excluded by the previous check)
542 551 return True
543 552 elif repo.obsstore:
544 553 return new.node() in obsolete.foreground(repo, [old.node()])
545 554 else:
546 555 # still an independent clause as it is lazier (and therefore faster)
547 556 return old.descendant(new)
General Comments 0
You need to be logged in to leave comments. Login now