##// END OF EJS Templates
bookmarks: factor out repository lookup from writing bookmarks file...
Ryan McElroy -
r23469:65e48b8d default
parent child Browse files
Show More
@@ -1,456 +1,460
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 self._writerepo(repo)
83
84 def _writerepo(self, repo):
85 """Factored out for extensibility"""
82 86 if repo._bookmarkcurrent not in self:
83 87 unsetcurrent(repo)
84 88
85 89 wlock = repo.wlock()
86 90 try:
87 91
88 92 file = repo.vfs('bookmarks', 'w', atomictemp=True)
89 93 self._write(file)
90 94 file.close()
91 95
92 96 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
93 97 try:
94 98 repo.svfs.utime('00changelog.i', None)
95 99 except OSError:
96 100 pass
97 101
98 102 finally:
99 103 wlock.release()
100 104
101 105 def _write(self, fp):
102 106 for name, node in self.iteritems():
103 107 fp.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
104 108
105 109 def readcurrent(repo):
106 110 '''Get the current bookmark
107 111
108 112 If we use gittish branches we have a current bookmark that
109 113 we are on. This function returns the name of the bookmark. It
110 114 is stored in .hg/bookmarks.current
111 115 '''
112 116 mark = None
113 117 try:
114 118 file = repo.opener('bookmarks.current')
115 119 except IOError, inst:
116 120 if inst.errno != errno.ENOENT:
117 121 raise
118 122 return None
119 123 try:
120 124 # No readline() in osutil.posixfile, reading everything is cheap
121 125 mark = encoding.tolocal((file.readlines() or [''])[0])
122 126 if mark == '' or mark not in repo._bookmarks:
123 127 mark = None
124 128 finally:
125 129 file.close()
126 130 return mark
127 131
128 132 def setcurrent(repo, mark):
129 133 '''Set the name of the bookmark that we are currently on
130 134
131 135 Set the name of the bookmark that we are on (hg update <bookmark>).
132 136 The name is recorded in .hg/bookmarks.current
133 137 '''
134 138 if mark not in repo._bookmarks:
135 139 raise AssertionError('bookmark %s does not exist!' % mark)
136 140
137 141 current = repo._bookmarkcurrent
138 142 if current == mark:
139 143 return
140 144
141 145 wlock = repo.wlock()
142 146 try:
143 147 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
144 148 file.write(encoding.fromlocal(mark))
145 149 file.close()
146 150 finally:
147 151 wlock.release()
148 152 repo._bookmarkcurrent = mark
149 153
150 154 def unsetcurrent(repo):
151 155 wlock = repo.wlock()
152 156 try:
153 157 try:
154 158 repo.vfs.unlink('bookmarks.current')
155 159 repo._bookmarkcurrent = None
156 160 except OSError, inst:
157 161 if inst.errno != errno.ENOENT:
158 162 raise
159 163 finally:
160 164 wlock.release()
161 165
162 166 def iscurrent(repo, mark=None, parents=None):
163 167 '''Tell whether the current bookmark is also active
164 168
165 169 I.e., the bookmark listed in .hg/bookmarks.current also points to a
166 170 parent of the working directory.
167 171 '''
168 172 if not mark:
169 173 mark = repo._bookmarkcurrent
170 174 if not parents:
171 175 parents = [p.node() for p in repo[None].parents()]
172 176 marks = repo._bookmarks
173 177 return (mark in marks and marks[mark] in parents)
174 178
175 179 def updatecurrentbookmark(repo, oldnode, curbranch):
176 180 try:
177 181 return update(repo, oldnode, repo.branchtip(curbranch))
178 182 except error.RepoLookupError:
179 183 if curbranch == "default": # no default branch!
180 184 return update(repo, oldnode, repo.lookup("tip"))
181 185 else:
182 186 raise util.Abort(_("branch %s not found") % curbranch)
183 187
184 188 def deletedivergent(repo, deletefrom, bm):
185 189 '''Delete divergent versions of bm on nodes in deletefrom.
186 190
187 191 Return True if at least one bookmark was deleted, False otherwise.'''
188 192 deleted = False
189 193 marks = repo._bookmarks
190 194 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
191 195 for mark in divergent:
192 196 if mark == '@' or '@' not in mark:
193 197 # can't be divergent by definition
194 198 continue
195 199 if mark and marks[mark] in deletefrom:
196 200 if mark != bm:
197 201 del marks[mark]
198 202 deleted = True
199 203 return deleted
200 204
201 205 def calculateupdate(ui, repo, checkout):
202 206 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
203 207 check out and where to move the active bookmark from, if needed.'''
204 208 movemarkfrom = None
205 209 if checkout is None:
206 210 curmark = repo._bookmarkcurrent
207 211 if iscurrent(repo):
208 212 movemarkfrom = repo['.'].node()
209 213 elif curmark:
210 214 ui.status(_("updating to active bookmark %s\n") % curmark)
211 215 checkout = curmark
212 216 return (checkout, movemarkfrom)
213 217
214 218 def update(repo, parents, node):
215 219 deletefrom = parents
216 220 marks = repo._bookmarks
217 221 update = False
218 222 cur = repo._bookmarkcurrent
219 223 if not cur:
220 224 return False
221 225
222 226 if marks[cur] in parents:
223 227 new = repo[node]
224 228 divs = [repo[b] for b in marks
225 229 if b.split('@', 1)[0] == cur.split('@', 1)[0]]
226 230 anc = repo.changelog.ancestors([new.rev()])
227 231 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
228 232 if validdest(repo, repo[marks[cur]], new):
229 233 marks[cur] = new.node()
230 234 update = True
231 235
232 236 if deletedivergent(repo, deletefrom, cur):
233 237 update = True
234 238
235 239 if update:
236 240 marks.write()
237 241 return update
238 242
239 243 def listbookmarks(repo):
240 244 # We may try to list bookmarks on a repo type that does not
241 245 # support it (e.g., statichttprepository).
242 246 marks = getattr(repo, '_bookmarks', {})
243 247
244 248 d = {}
245 249 hasnode = repo.changelog.hasnode
246 250 for k, v in marks.iteritems():
247 251 # don't expose local divergent bookmarks
248 252 if hasnode(v) and ('@' not in k or k.endswith('@')):
249 253 d[k] = hex(v)
250 254 return d
251 255
252 256 def pushbookmark(repo, key, old, new):
253 257 w = l = tr = None
254 258 try:
255 259 w = repo.wlock()
256 260 l = repo.lock()
257 261 tr = repo.transaction('bookmarks')
258 262 marks = repo._bookmarks
259 263 existing = hex(marks.get(key, ''))
260 264 if existing != old and existing != new:
261 265 return False
262 266 if new == '':
263 267 del marks[key]
264 268 else:
265 269 if new not in repo:
266 270 return False
267 271 marks[key] = repo[new].node()
268 272 marks.recordchange(tr)
269 273 tr.close()
270 274 return True
271 275 finally:
272 276 lockmod.release(tr, l, w)
273 277
274 278 def compare(repo, srcmarks, dstmarks,
275 279 srchex=None, dsthex=None, targets=None):
276 280 '''Compare bookmarks between srcmarks and dstmarks
277 281
278 282 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
279 283 differ, invalid)", each are list of bookmarks below:
280 284
281 285 :addsrc: added on src side (removed on dst side, perhaps)
282 286 :adddst: added on dst side (removed on src side, perhaps)
283 287 :advsrc: advanced on src side
284 288 :advdst: advanced on dst side
285 289 :diverge: diverge
286 290 :differ: changed, but changeset referred on src is unknown on dst
287 291 :invalid: unknown on both side
288 292 :same: same on both side
289 293
290 294 Each elements of lists in result tuple is tuple "(bookmark name,
291 295 changeset ID on source side, changeset ID on destination
292 296 side)". Each changeset IDs are 40 hexadecimal digit string or
293 297 None.
294 298
295 299 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
296 300 "invalid" list may be unknown for repo.
297 301
298 302 This function expects that "srcmarks" and "dstmarks" return
299 303 changeset ID in 40 hexadecimal digit string for specified
300 304 bookmark. If not so (e.g. bmstore "repo._bookmarks" returning
301 305 binary value), "srchex" or "dsthex" should be specified to convert
302 306 into such form.
303 307
304 308 If "targets" is specified, only bookmarks listed in it are
305 309 examined.
306 310 '''
307 311 if not srchex:
308 312 srchex = lambda x: x
309 313 if not dsthex:
310 314 dsthex = lambda x: x
311 315
312 316 if targets:
313 317 bset = set(targets)
314 318 else:
315 319 srcmarkset = set(srcmarks)
316 320 dstmarkset = set(dstmarks)
317 321 bset = srcmarkset | dstmarkset
318 322
319 323 results = ([], [], [], [], [], [], [], [])
320 324 addsrc = results[0].append
321 325 adddst = results[1].append
322 326 advsrc = results[2].append
323 327 advdst = results[3].append
324 328 diverge = results[4].append
325 329 differ = results[5].append
326 330 invalid = results[6].append
327 331 same = results[7].append
328 332
329 333 for b in sorted(bset):
330 334 if b not in srcmarks:
331 335 if b in dstmarks:
332 336 adddst((b, None, dsthex(dstmarks[b])))
333 337 else:
334 338 invalid((b, None, None))
335 339 elif b not in dstmarks:
336 340 addsrc((b, srchex(srcmarks[b]), None))
337 341 else:
338 342 scid = srchex(srcmarks[b])
339 343 dcid = dsthex(dstmarks[b])
340 344 if scid == dcid:
341 345 same((b, scid, dcid))
342 346 elif scid in repo and dcid in repo:
343 347 sctx = repo[scid]
344 348 dctx = repo[dcid]
345 349 if sctx.rev() < dctx.rev():
346 350 if validdest(repo, sctx, dctx):
347 351 advdst((b, scid, dcid))
348 352 else:
349 353 diverge((b, scid, dcid))
350 354 else:
351 355 if validdest(repo, dctx, sctx):
352 356 advsrc((b, scid, dcid))
353 357 else:
354 358 diverge((b, scid, dcid))
355 359 else:
356 360 # it is too expensive to examine in detail, in this case
357 361 differ((b, scid, dcid))
358 362
359 363 return results
360 364
361 365 def _diverge(ui, b, path, localmarks):
362 366 if b == '@':
363 367 b = ''
364 368 # find a unique @ suffix
365 369 for x in range(1, 100):
366 370 n = '%s@%d' % (b, x)
367 371 if n not in localmarks:
368 372 break
369 373 # try to use an @pathalias suffix
370 374 # if an @pathalias already exists, we overwrite (update) it
371 375 if path.startswith("file:"):
372 376 path = util.url(path).path
373 377 for p, u in ui.configitems("paths"):
374 378 if u.startswith("file:"):
375 379 u = util.url(u).path
376 380 if path == u:
377 381 n = '%s@%s' % (b, p)
378 382 return n
379 383
380 384 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
381 385 ui.debug("checking for updated bookmarks\n")
382 386 localmarks = repo._bookmarks
383 387 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
384 388 ) = compare(repo, remotemarks, localmarks, dsthex=hex)
385 389
386 390 status = ui.status
387 391 warn = ui.warn
388 392 if ui.configbool('ui', 'quietbookmarkmove', False):
389 393 status = warn = ui.debug
390 394
391 395 explicit = set(explicit)
392 396 changed = []
393 397 for b, scid, dcid in addsrc:
394 398 if scid in repo: # add remote bookmarks for changes we already have
395 399 changed.append((b, bin(scid), status,
396 400 _("adding remote bookmark %s\n") % (b)))
397 401 for b, scid, dcid in advsrc:
398 402 changed.append((b, bin(scid), status,
399 403 _("updating bookmark %s\n") % (b)))
400 404 # remove normal movement from explicit set
401 405 explicit.difference_update(d[0] for d in changed)
402 406
403 407 for b, scid, dcid in diverge:
404 408 if b in explicit:
405 409 explicit.discard(b)
406 410 changed.append((b, bin(scid), status,
407 411 _("importing bookmark %s\n") % (b)))
408 412 else:
409 413 db = _diverge(ui, b, path, localmarks)
410 414 changed.append((db, bin(scid), warn,
411 415 _("divergent bookmark %s stored as %s\n")
412 416 % (b, db)))
413 417 for b, scid, dcid in adddst + advdst:
414 418 if b in explicit:
415 419 explicit.discard(b)
416 420 changed.append((b, bin(scid), status,
417 421 _("importing bookmark %s\n") % (b)))
418 422
419 423 if changed:
420 424 tr = trfunc()
421 425 for b, node, writer, msg in sorted(changed):
422 426 localmarks[b] = node
423 427 writer(msg)
424 428 localmarks.recordchange(tr)
425 429
426 430 def diff(ui, dst, src):
427 431 ui.status(_("searching for changed bookmarks\n"))
428 432
429 433 smarks = src.listkeys('bookmarks')
430 434 dmarks = dst.listkeys('bookmarks')
431 435
432 436 diff = sorted(set(smarks) - set(dmarks))
433 437 for k in diff:
434 438 mark = ui.debugflag and smarks[k] or smarks[k][:12]
435 439 ui.write(" %-25s %s\n" % (k, mark))
436 440
437 441 if len(diff) <= 0:
438 442 ui.status(_("no changed bookmarks found\n"))
439 443 return 1
440 444 return 0
441 445
442 446 def validdest(repo, old, new):
443 447 """Is the new bookmark destination a valid update from the old one"""
444 448 repo = repo.unfiltered()
445 449 if old == new:
446 450 # Old == new -> nothing to update.
447 451 return False
448 452 elif not old:
449 453 # old is nullrev, anything is valid.
450 454 # (new != nullrev has been excluded by the previous check)
451 455 return True
452 456 elif repo.obsstore:
453 457 return new.node() in obsolete.foreground(repo, [old.node()])
454 458 else:
455 459 # still an independent clause as it is lazyer (and therefore faster)
456 460 return old.descendant(new)
General Comments 0
You need to be logged in to leave comments. Login now