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