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