##// END OF EJS Templates
bookmarks: avoid deleting primary bookmarks on rebase...
Matt Mackall -
r21843:92666a86 stable
parent child Browse files
Show More
@@ -1,426 +1,429
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 if mark == '@' or '@' not in mark:
168 # can't be divergent by definition
169 continue
167 170 if mark and marks[mark] in deletefrom:
168 171 if mark != bm:
169 172 del marks[mark]
170 173 deleted = True
171 174 return deleted
172 175
173 176 def calculateupdate(ui, repo, checkout):
174 177 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
175 178 check out and where to move the active bookmark from, if needed.'''
176 179 movemarkfrom = None
177 180 if checkout is None:
178 181 curmark = repo._bookmarkcurrent
179 182 if iscurrent(repo):
180 183 movemarkfrom = repo['.'].node()
181 184 elif curmark:
182 185 ui.status(_("updating to active bookmark %s\n") % curmark)
183 186 checkout = curmark
184 187 return (checkout, movemarkfrom)
185 188
186 189 def update(repo, parents, node):
187 190 deletefrom = parents
188 191 marks = repo._bookmarks
189 192 update = False
190 193 cur = repo._bookmarkcurrent
191 194 if not cur:
192 195 return False
193 196
194 197 if marks[cur] in parents:
195 198 new = repo[node]
196 199 divs = [repo[b] for b in marks
197 200 if b.split('@', 1)[0] == cur.split('@', 1)[0]]
198 201 anc = repo.changelog.ancestors([new.rev()])
199 202 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
200 203 if validdest(repo, repo[marks[cur]], new):
201 204 marks[cur] = new.node()
202 205 update = True
203 206
204 207 if deletedivergent(repo, deletefrom, cur):
205 208 update = True
206 209
207 210 if update:
208 211 marks.write()
209 212 return update
210 213
211 214 def listbookmarks(repo):
212 215 # We may try to list bookmarks on a repo type that does not
213 216 # support it (e.g., statichttprepository).
214 217 marks = getattr(repo, '_bookmarks', {})
215 218
216 219 d = {}
217 220 hasnode = repo.changelog.hasnode
218 221 for k, v in marks.iteritems():
219 222 # don't expose local divergent bookmarks
220 223 if hasnode(v) and ('@' not in k or k.endswith('@')):
221 224 d[k] = hex(v)
222 225 return d
223 226
224 227 def pushbookmark(repo, key, old, new):
225 228 w = repo.wlock()
226 229 try:
227 230 marks = repo._bookmarks
228 231 if hex(marks.get(key, '')) != old:
229 232 return False
230 233 if new == '':
231 234 del marks[key]
232 235 else:
233 236 if new not in repo:
234 237 return False
235 238 marks[key] = repo[new].node()
236 239 marks.write()
237 240 return True
238 241 finally:
239 242 w.release()
240 243
241 244 def compare(repo, srcmarks, dstmarks,
242 245 srchex=None, dsthex=None, targets=None):
243 246 '''Compare bookmarks between srcmarks and dstmarks
244 247
245 248 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
246 249 differ, invalid)", each are list of bookmarks below:
247 250
248 251 :addsrc: added on src side (removed on dst side, perhaps)
249 252 :adddst: added on dst side (removed on src side, perhaps)
250 253 :advsrc: advanced on src side
251 254 :advdst: advanced on dst side
252 255 :diverge: diverge
253 256 :differ: changed, but changeset referred on src is unknown on dst
254 257 :invalid: unknown on both side
255 258
256 259 Each elements of lists in result tuple is tuple "(bookmark name,
257 260 changeset ID on source side, changeset ID on destination
258 261 side)". Each changeset IDs are 40 hexadecimal digit string or
259 262 None.
260 263
261 264 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
262 265 "invalid" list may be unknown for repo.
263 266
264 267 This function expects that "srcmarks" and "dstmarks" return
265 268 changeset ID in 40 hexadecimal digit string for specified
266 269 bookmark. If not so (e.g. bmstore "repo._bookmarks" returning
267 270 binary value), "srchex" or "dsthex" should be specified to convert
268 271 into such form.
269 272
270 273 If "targets" is specified, only bookmarks listed in it are
271 274 examined.
272 275 '''
273 276 if not srchex:
274 277 srchex = lambda x: x
275 278 if not dsthex:
276 279 dsthex = lambda x: x
277 280
278 281 if targets:
279 282 bset = set(targets)
280 283 else:
281 284 srcmarkset = set(srcmarks)
282 285 dstmarkset = set(dstmarks)
283 286 bset = srcmarkset ^ dstmarkset
284 287 for b in srcmarkset & dstmarkset:
285 288 if srchex(srcmarks[b]) != dsthex(dstmarks[b]):
286 289 bset.add(b)
287 290
288 291 results = ([], [], [], [], [], [], [])
289 292 addsrc = results[0].append
290 293 adddst = results[1].append
291 294 advsrc = results[2].append
292 295 advdst = results[3].append
293 296 diverge = results[4].append
294 297 differ = results[5].append
295 298 invalid = results[6].append
296 299
297 300 for b in sorted(bset):
298 301 if b not in srcmarks:
299 302 if b in dstmarks:
300 303 adddst((b, None, dsthex(dstmarks[b])))
301 304 else:
302 305 invalid((b, None, None))
303 306 elif b not in dstmarks:
304 307 addsrc((b, srchex(srcmarks[b]), None))
305 308 else:
306 309 scid = srchex(srcmarks[b])
307 310 dcid = dsthex(dstmarks[b])
308 311 if scid in repo and dcid in repo:
309 312 sctx = repo[scid]
310 313 dctx = repo[dcid]
311 314 if sctx.rev() < dctx.rev():
312 315 if validdest(repo, sctx, dctx):
313 316 advdst((b, scid, dcid))
314 317 else:
315 318 diverge((b, scid, dcid))
316 319 else:
317 320 if validdest(repo, dctx, sctx):
318 321 advsrc((b, scid, dcid))
319 322 else:
320 323 diverge((b, scid, dcid))
321 324 else:
322 325 # it is too expensive to examine in detail, in this case
323 326 differ((b, scid, dcid))
324 327
325 328 return results
326 329
327 330 def _diverge(ui, b, path, localmarks):
328 331 if b == '@':
329 332 b = ''
330 333 # find a unique @ suffix
331 334 for x in range(1, 100):
332 335 n = '%s@%d' % (b, x)
333 336 if n not in localmarks:
334 337 break
335 338 # try to use an @pathalias suffix
336 339 # if an @pathalias already exists, we overwrite (update) it
337 340 for p, u in ui.configitems("paths"):
338 341 if path == u:
339 342 n = '%s@%s' % (b, p)
340 343 return n
341 344
342 345 def updatefromremote(ui, repo, remotemarks, path):
343 346 ui.debug("checking for updated bookmarks\n")
344 347 localmarks = repo._bookmarks
345 348 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
346 349 ) = compare(repo, remotemarks, localmarks, dsthex=hex)
347 350
348 351 changed = []
349 352 for b, scid, dcid in addsrc:
350 353 if scid in repo: # add remote bookmarks for changes we already have
351 354 changed.append((b, bin(scid), ui.status,
352 355 _("adding remote bookmark %s\n") % (b)))
353 356 for b, scid, dcid in advsrc:
354 357 changed.append((b, bin(scid), ui.status,
355 358 _("updating bookmark %s\n") % (b)))
356 359 for b, scid, dcid in diverge:
357 360 db = _diverge(ui, b, path, localmarks)
358 361 changed.append((db, bin(scid), ui.warn,
359 362 _("divergent bookmark %s stored as %s\n") % (b, db)))
360 363 if changed:
361 364 for b, node, writer, msg in sorted(changed):
362 365 localmarks[b] = node
363 366 writer(msg)
364 367 localmarks.write()
365 368
366 369 def pushtoremote(ui, repo, remote, targets):
367 370 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
368 371 ) = compare(repo, repo._bookmarks, remote.listkeys('bookmarks'),
369 372 srchex=hex, targets=targets)
370 373 if invalid:
371 374 b, scid, dcid = invalid[0]
372 375 ui.warn(_('bookmark %s does not exist on the local '
373 376 'or remote repository!\n') % b)
374 377 return 2
375 378
376 379 def push(b, old, new):
377 380 r = remote.pushkey('bookmarks', b, old, new)
378 381 if not r:
379 382 ui.warn(_('updating bookmark %s failed!\n') % b)
380 383 return 1
381 384 return 0
382 385 failed = 0
383 386 for b, scid, dcid in sorted(addsrc + advsrc + advdst + diverge + differ):
384 387 ui.status(_("exporting bookmark %s\n") % b)
385 388 if dcid is None:
386 389 dcid = ''
387 390 failed += push(b, dcid, scid)
388 391 for b, scid, dcid in adddst:
389 392 # treat as "deleted locally"
390 393 ui.status(_("deleting remote bookmark %s\n") % b)
391 394 failed += push(b, dcid, '')
392 395
393 396 if failed:
394 397 return 1
395 398
396 399 def diff(ui, dst, src):
397 400 ui.status(_("searching for changed bookmarks\n"))
398 401
399 402 smarks = src.listkeys('bookmarks')
400 403 dmarks = dst.listkeys('bookmarks')
401 404
402 405 diff = sorted(set(smarks) - set(dmarks))
403 406 for k in diff:
404 407 mark = ui.debugflag and smarks[k] or smarks[k][:12]
405 408 ui.write(" %-25s %s\n" % (k, mark))
406 409
407 410 if len(diff) <= 0:
408 411 ui.status(_("no changed bookmarks found\n"))
409 412 return 1
410 413 return 0
411 414
412 415 def validdest(repo, old, new):
413 416 """Is the new bookmark destination a valid update from the old one"""
414 417 repo = repo.unfiltered()
415 418 if old == new:
416 419 # Old == new -> nothing to update.
417 420 return False
418 421 elif not old:
419 422 # old is nullrev, anything is valid.
420 423 # (new != nullrev has been excluded by the previous check)
421 424 return True
422 425 elif repo.obsstore:
423 426 return new.node() in obsolete.foreground(repo, [old.node()])
424 427 else:
425 428 # still an independent clause as it is lazyer (and therefore faster)
426 429 return old.descendant(new)
General Comments 0
You need to be logged in to leave comments. Login now