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