##// END OF EJS Templates
bookmarks: check @pathalias suffix before available @number for efficiency...
FUJIWARA Katsunori -
r24354:194e1e3e default
parent child Browse files
Show More
@@ -1,474 +1,474 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 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):
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 if b == '@':
372 372 b = ''
373 # find a unique @ suffix
374 for x in range(1, 100):
375 n = '%s@%d' % (b, x)
376 if n not in localmarks:
377 break
378 else:
379 n = None
380 373 # try to use an @pathalias suffix
381 374 # if an @pathalias already exists, we overwrite (update) it
382 375 if path.startswith("file:"):
383 376 path = util.url(path).path
384 377 for p, u in ui.configitems("paths"):
385 378 if u.startswith("file:"):
386 379 u = util.url(u).path
387 380 if path == u:
388 n = '%s@%s' % (b, p)
389 return n
381 return '%s@%s' % (b, p)
382
383 # assign a unique "@number" suffix newly
384 for x in range(1, 100):
385 n = '%s@%d' % (b, x)
386 if n not in localmarks:
387 return n
388
389 return None
390 390
391 391 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
392 392 ui.debug("checking for updated bookmarks\n")
393 393 localmarks = repo._bookmarks
394 394 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
395 395 ) = compare(repo, remotemarks, localmarks, dsthex=hex)
396 396
397 397 status = ui.status
398 398 warn = ui.warn
399 399 if ui.configbool('ui', 'quietbookmarkmove', False):
400 400 status = warn = ui.debug
401 401
402 402 explicit = set(explicit)
403 403 changed = []
404 404 for b, scid, dcid in addsrc:
405 405 if scid in repo: # add remote bookmarks for changes we already have
406 406 changed.append((b, bin(scid), status,
407 407 _("adding remote bookmark %s\n") % (b)))
408 408 for b, scid, dcid in advsrc:
409 409 changed.append((b, bin(scid), status,
410 410 _("updating bookmark %s\n") % (b)))
411 411 # remove normal movement from explicit set
412 412 explicit.difference_update(d[0] for d in changed)
413 413
414 414 for b, scid, dcid in diverge:
415 415 if b in explicit:
416 416 explicit.discard(b)
417 417 changed.append((b, bin(scid), status,
418 418 _("importing bookmark %s\n") % (b)))
419 419 else:
420 420 db = _diverge(ui, b, path, localmarks)
421 421 if db:
422 422 changed.append((db, bin(scid), warn,
423 423 _("divergent bookmark %s stored as %s\n") %
424 424 (b, db)))
425 425 else:
426 426 warn(_("warning: failed to assign numbered name "
427 427 "to divergent bookmark %s\n") % (b))
428 428 for b, scid, dcid in adddst + advdst:
429 429 if b in explicit:
430 430 explicit.discard(b)
431 431 changed.append((b, bin(scid), status,
432 432 _("importing bookmark %s\n") % (b)))
433 433
434 434 if changed:
435 435 tr = trfunc()
436 436 for b, node, writer, msg in sorted(changed):
437 437 localmarks[b] = node
438 438 writer(msg)
439 439 localmarks.recordchange(tr)
440 440
441 441 def diff(ui, dst, src):
442 442 ui.status(_("searching for changed bookmarks\n"))
443 443
444 444 smarks = src.listkeys('bookmarks')
445 445 dmarks = dst.listkeys('bookmarks')
446 446
447 447 diff = sorted(set(smarks) - set(dmarks))
448 448 for k in diff:
449 449 if ui.debugflag:
450 450 mark = smarks[k]
451 451 else:
452 452 mark = smarks[k][:12]
453 453 ui.write(" %-25s %s\n" % (k, mark))
454 454
455 455 if len(diff) <= 0:
456 456 ui.status(_("no changed bookmarks found\n"))
457 457 return 1
458 458 return 0
459 459
460 460 def validdest(repo, old, new):
461 461 """Is the new bookmark destination a valid update from the old one"""
462 462 repo = repo.unfiltered()
463 463 if old == new:
464 464 # Old == new -> nothing to update.
465 465 return False
466 466 elif not old:
467 467 # old is nullrev, anything is valid.
468 468 # (new != nullrev has been excluded by the previous check)
469 469 return True
470 470 elif repo.obsstore:
471 471 return new.node() in obsolete.foreground(repo, [old.node()])
472 472 else:
473 473 # still an independent clause as it is lazier (and therefore faster)
474 474 return old.descendant(new)
General Comments 0
You need to be logged in to leave comments. Login now