##// 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 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
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 setcurrent(repo, None)
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 current = repo._bookmarkcurrent
110 110 if current == mark:
111 111 return
112 112
113 113 if mark not in repo._bookmarks:
114 114 mark = ''
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 and marks[mark] in deletefrom:
168 168 if mark != bm:
169 169 del marks[mark]
170 170 deleted = True
171 171 return deleted
172 172
173 173 def calculateupdate(ui, repo, checkout):
174 174 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
175 175 check out and where to move the active bookmark from, if needed.'''
176 176 movemarkfrom = None
177 177 if checkout is None:
178 178 curmark = repo._bookmarkcurrent
179 179 if iscurrent(repo):
180 180 movemarkfrom = repo['.'].node()
181 181 elif curmark:
182 182 ui.status(_("updating to active bookmark %s\n") % curmark)
183 183 checkout = curmark
184 184 return (checkout, movemarkfrom)
185 185
186 186 def update(repo, parents, node):
187 187 deletefrom = parents
188 188 marks = repo._bookmarks
189 189 update = False
190 190 cur = repo._bookmarkcurrent
191 191 if not cur:
192 192 return False
193 193
194 194 if marks[cur] in parents:
195 195 old = repo[marks[cur]]
196 196 new = repo[node]
197 197 divs = [repo[b] for b in marks
198 198 if b.split('@', 1)[0] == cur.split('@', 1)[0]]
199 199 anc = repo.changelog.ancestors([new.rev()])
200 200 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
201 201 if old.descendant(new):
202 202 marks[cur] = new.node()
203 203 update = True
204 204
205 205 if deletedivergent(repo, deletefrom, cur):
206 206 update = True
207 207
208 208 if update:
209 209 marks.write()
210 210 return update
211 211
212 212 def listbookmarks(repo):
213 213 # We may try to list bookmarks on a repo type that does not
214 214 # support it (e.g., statichttprepository).
215 215 marks = getattr(repo, '_bookmarks', {})
216 216
217 217 d = {}
218 218 hasnode = repo.changelog.hasnode
219 219 for k, v in marks.iteritems():
220 220 # don't expose local divergent bookmarks
221 221 if hasnode(v) and ('@' not in k or k.endswith('@')):
222 222 d[k] = hex(v)
223 223 return d
224 224
225 225 def pushbookmark(repo, key, old, new):
226 226 w = repo.wlock()
227 227 try:
228 228 marks = repo._bookmarks
229 229 if hex(marks.get(key, '')) != old:
230 230 return False
231 231 if new == '':
232 232 del marks[key]
233 233 else:
234 234 if new not in repo:
235 235 return False
236 236 marks[key] = repo[new].node()
237 237 marks.write()
238 238 return True
239 239 finally:
240 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 328 def updatefromremote(ui, repo, remotemarks, path):
243 329 ui.debug("checking for updated bookmarks\n")
244 330 changed = False
245 331 localmarks = repo._bookmarks
246 332 for k in sorted(remotemarks):
247 333 if k in localmarks:
248 334 nr, nl = remotemarks[k], localmarks[k]
249 335 if nr in repo:
250 336 cr = repo[nr]
251 337 cl = repo[nl]
252 338 if cl.rev() >= cr.rev():
253 339 continue
254 340 if validdest(repo, cl, cr):
255 341 localmarks[k] = cr.node()
256 342 changed = True
257 343 ui.status(_("updating bookmark %s\n") % k)
258 344 else:
259 345 if k == '@':
260 346 kd = ''
261 347 else:
262 348 kd = k
263 349 # find a unique @ suffix
264 350 for x in range(1, 100):
265 351 n = '%s@%d' % (kd, x)
266 352 if n not in localmarks:
267 353 break
268 354 # try to use an @pathalias suffix
269 355 # if an @pathalias already exists, we overwrite (update) it
270 356 for p, u in ui.configitems("paths"):
271 357 if path == u:
272 358 n = '%s@%s' % (kd, p)
273 359
274 360 localmarks[n] = cr.node()
275 361 changed = True
276 362 ui.warn(_("divergent bookmark %s stored as %s\n") % (k, n))
277 363 elif remotemarks[k] in repo:
278 364 # add remote bookmarks for changes we already have
279 365 localmarks[k] = repo[remotemarks[k]].node()
280 366 changed = True
281 367 ui.status(_("adding remote bookmark %s\n") % k)
282 368
283 369 if changed:
284 370 localmarks.write()
285 371
286 372 def diff(ui, dst, src):
287 373 ui.status(_("searching for changed bookmarks\n"))
288 374
289 375 smarks = src.listkeys('bookmarks')
290 376 dmarks = dst.listkeys('bookmarks')
291 377
292 378 diff = sorted(set(smarks) - set(dmarks))
293 379 for k in diff:
294 380 mark = ui.debugflag and smarks[k] or smarks[k][:12]
295 381 ui.write(" %-25s %s\n" % (k, mark))
296 382
297 383 if len(diff) <= 0:
298 384 ui.status(_("no changed bookmarks found\n"))
299 385 return 1
300 386 return 0
301 387
302 388 def validdest(repo, old, new):
303 389 """Is the new bookmark destination a valid update from the old one"""
304 390 repo = repo.unfiltered()
305 391 if old == new:
306 392 # Old == new -> nothing to update.
307 393 return False
308 394 elif not old:
309 395 # old is nullrev, anything is valid.
310 396 # (new != nullrev has been excluded by the previous check)
311 397 return True
312 398 elif repo.obsstore:
313 399 return new.node() in obsolete.foreground(repo, [old.node()])
314 400 else:
315 401 # still an independent clause as it is lazyer (and therefore faster)
316 402 return old.descendant(new)
General Comments 0
You need to be logged in to leave comments. Login now