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