##// END OF EJS Templates
bookmarks: use vfs.tryread() instead of reimplementing it...
Martin von Zweigbergk -
r42503:2b77183a default
parent child Browse files
Show More
@@ -1,955 +1,939
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 __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import struct
11 import struct
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 bin,
15 bin,
16 hex,
16 hex,
17 short,
17 short,
18 wdirid,
18 wdirid,
19 )
19 )
20 from . import (
20 from . import (
21 encoding,
21 encoding,
22 error,
22 error,
23 obsutil,
23 obsutil,
24 pycompat,
24 pycompat,
25 scmutil,
25 scmutil,
26 txnutil,
26 txnutil,
27 util,
27 util,
28 )
28 )
29
29
30 # label constants
30 # label constants
31 # until 3.5, bookmarks.current was the advertised name, not
31 # until 3.5, bookmarks.current was the advertised name, not
32 # bookmarks.active, so we must use both to avoid breaking old
32 # bookmarks.active, so we must use both to avoid breaking old
33 # custom styles
33 # custom styles
34 activebookmarklabel = 'bookmarks.active bookmarks.current'
34 activebookmarklabel = 'bookmarks.active bookmarks.current'
35
35
36 def _getbkfile(repo):
36 def _getbkfile(repo):
37 """Hook so that extensions that mess with the store can hook bm storage.
37 """Hook so that extensions that mess with the store can hook bm storage.
38
38
39 For core, this just handles wether we should see pending
39 For core, this just handles wether we should see pending
40 bookmarks or the committed ones. Other extensions (like share)
40 bookmarks or the committed ones. Other extensions (like share)
41 may need to tweak this behavior further.
41 may need to tweak this behavior further.
42 """
42 """
43 fp, pending = txnutil.trypending(repo.root, repo.vfs, 'bookmarks')
43 fp, pending = txnutil.trypending(repo.root, repo.vfs, 'bookmarks')
44 return fp
44 return fp
45
45
46 class bmstore(object):
46 class bmstore(object):
47 r"""Storage for bookmarks.
47 r"""Storage for bookmarks.
48
48
49 This object should do all bookmark-related reads and writes, so
49 This object should do all bookmark-related reads and writes, so
50 that it's fairly simple to replace the storage underlying
50 that it's fairly simple to replace the storage underlying
51 bookmarks without having to clone the logic surrounding
51 bookmarks without having to clone the logic surrounding
52 bookmarks. This type also should manage the active bookmark, if
52 bookmarks. This type also should manage the active bookmark, if
53 any.
53 any.
54
54
55 This particular bmstore implementation stores bookmarks as
55 This particular bmstore implementation stores bookmarks as
56 {hash}\s{name}\n (the same format as localtags) in
56 {hash}\s{name}\n (the same format as localtags) in
57 .hg/bookmarks. The mapping is stored as {name: nodeid}.
57 .hg/bookmarks. The mapping is stored as {name: nodeid}.
58 """
58 """
59
59
60 def __init__(self, repo):
60 def __init__(self, repo):
61 self._repo = repo
61 self._repo = repo
62 self._refmap = refmap = {} # refspec: node
62 self._refmap = refmap = {} # refspec: node
63 self._nodemap = nodemap = {} # node: sorted([refspec, ...])
63 self._nodemap = nodemap = {} # node: sorted([refspec, ...])
64 self._clean = True
64 self._clean = True
65 self._aclean = True
65 self._aclean = True
66 nm = repo.changelog.nodemap
66 nm = repo.changelog.nodemap
67 tonode = bin # force local lookup
67 tonode = bin # force local lookup
68 try:
68 try:
69 with _getbkfile(repo) as bkfile:
69 with _getbkfile(repo) as bkfile:
70 for line in bkfile:
70 for line in bkfile:
71 line = line.strip()
71 line = line.strip()
72 if not line:
72 if not line:
73 continue
73 continue
74 try:
74 try:
75 sha, refspec = line.split(' ', 1)
75 sha, refspec = line.split(' ', 1)
76 node = tonode(sha)
76 node = tonode(sha)
77 if node in nm:
77 if node in nm:
78 refspec = encoding.tolocal(refspec)
78 refspec = encoding.tolocal(refspec)
79 refmap[refspec] = node
79 refmap[refspec] = node
80 nrefs = nodemap.get(node)
80 nrefs = nodemap.get(node)
81 if nrefs is None:
81 if nrefs is None:
82 nodemap[node] = [refspec]
82 nodemap[node] = [refspec]
83 else:
83 else:
84 nrefs.append(refspec)
84 nrefs.append(refspec)
85 if nrefs[-2] > refspec:
85 if nrefs[-2] > refspec:
86 # bookmarks weren't sorted before 4.5
86 # bookmarks weren't sorted before 4.5
87 nrefs.sort()
87 nrefs.sort()
88 except (TypeError, ValueError):
88 except (TypeError, ValueError):
89 # TypeError:
89 # TypeError:
90 # - bin(...)
90 # - bin(...)
91 # ValueError:
91 # ValueError:
92 # - node in nm, for non-20-bytes entry
92 # - node in nm, for non-20-bytes entry
93 # - split(...), for string without ' '
93 # - split(...), for string without ' '
94 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
94 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
95 % pycompat.bytestr(line))
95 % pycompat.bytestr(line))
96 except IOError as inst:
96 except IOError as inst:
97 if inst.errno != errno.ENOENT:
97 if inst.errno != errno.ENOENT:
98 raise
98 raise
99 self._active = _readactive(repo, self)
99 self._active = _readactive(repo, self)
100
100
101 @property
101 @property
102 def active(self):
102 def active(self):
103 return self._active
103 return self._active
104
104
105 @active.setter
105 @active.setter
106 def active(self, mark):
106 def active(self, mark):
107 if mark is not None and mark not in self._refmap:
107 if mark is not None and mark not in self._refmap:
108 raise AssertionError('bookmark %s does not exist!' % mark)
108 raise AssertionError('bookmark %s does not exist!' % mark)
109
109
110 self._active = mark
110 self._active = mark
111 self._aclean = False
111 self._aclean = False
112
112
113 def __len__(self):
113 def __len__(self):
114 return len(self._refmap)
114 return len(self._refmap)
115
115
116 def __iter__(self):
116 def __iter__(self):
117 return iter(self._refmap)
117 return iter(self._refmap)
118
118
119 def iteritems(self):
119 def iteritems(self):
120 return self._refmap.iteritems()
120 return self._refmap.iteritems()
121
121
122 def items(self):
122 def items(self):
123 return self._refmap.items()
123 return self._refmap.items()
124
124
125 # TODO: maybe rename to allnames()?
125 # TODO: maybe rename to allnames()?
126 def keys(self):
126 def keys(self):
127 return self._refmap.keys()
127 return self._refmap.keys()
128
128
129 # TODO: maybe rename to allnodes()? but nodes would have to be deduplicated
129 # TODO: maybe rename to allnodes()? but nodes would have to be deduplicated
130 # could be self._nodemap.keys()
130 # could be self._nodemap.keys()
131 def values(self):
131 def values(self):
132 return self._refmap.values()
132 return self._refmap.values()
133
133
134 def __contains__(self, mark):
134 def __contains__(self, mark):
135 return mark in self._refmap
135 return mark in self._refmap
136
136
137 def __getitem__(self, mark):
137 def __getitem__(self, mark):
138 return self._refmap[mark]
138 return self._refmap[mark]
139
139
140 def get(self, mark, default=None):
140 def get(self, mark, default=None):
141 return self._refmap.get(mark, default)
141 return self._refmap.get(mark, default)
142
142
143 def _set(self, mark, node):
143 def _set(self, mark, node):
144 self._clean = False
144 self._clean = False
145 if mark in self._refmap:
145 if mark in self._refmap:
146 self._del(mark)
146 self._del(mark)
147 self._refmap[mark] = node
147 self._refmap[mark] = node
148 nrefs = self._nodemap.get(node)
148 nrefs = self._nodemap.get(node)
149 if nrefs is None:
149 if nrefs is None:
150 self._nodemap[node] = [mark]
150 self._nodemap[node] = [mark]
151 else:
151 else:
152 nrefs.append(mark)
152 nrefs.append(mark)
153 nrefs.sort()
153 nrefs.sort()
154
154
155 def _del(self, mark):
155 def _del(self, mark):
156 self._clean = False
156 self._clean = False
157 node = self._refmap.pop(mark)
157 node = self._refmap.pop(mark)
158 nrefs = self._nodemap[node]
158 nrefs = self._nodemap[node]
159 if len(nrefs) == 1:
159 if len(nrefs) == 1:
160 assert nrefs[0] == mark
160 assert nrefs[0] == mark
161 del self._nodemap[node]
161 del self._nodemap[node]
162 else:
162 else:
163 nrefs.remove(mark)
163 nrefs.remove(mark)
164
164
165 def names(self, node):
165 def names(self, node):
166 """Return a sorted list of bookmarks pointing to the specified node"""
166 """Return a sorted list of bookmarks pointing to the specified node"""
167 return self._nodemap.get(node, [])
167 return self._nodemap.get(node, [])
168
168
169 def changectx(self, mark):
169 def changectx(self, mark):
170 node = self._refmap[mark]
170 node = self._refmap[mark]
171 return self._repo[node]
171 return self._repo[node]
172
172
173 def applychanges(self, repo, tr, changes):
173 def applychanges(self, repo, tr, changes):
174 """Apply a list of changes to bookmarks
174 """Apply a list of changes to bookmarks
175 """
175 """
176 bmchanges = tr.changes.get('bookmarks')
176 bmchanges = tr.changes.get('bookmarks')
177 for name, node in changes:
177 for name, node in changes:
178 old = self._refmap.get(name)
178 old = self._refmap.get(name)
179 if node is None:
179 if node is None:
180 self._del(name)
180 self._del(name)
181 else:
181 else:
182 self._set(name, node)
182 self._set(name, node)
183 if bmchanges is not None:
183 if bmchanges is not None:
184 # if a previous value exist preserve the "initial" value
184 # if a previous value exist preserve the "initial" value
185 previous = bmchanges.get(name)
185 previous = bmchanges.get(name)
186 if previous is not None:
186 if previous is not None:
187 old = previous[0]
187 old = previous[0]
188 bmchanges[name] = (old, node)
188 bmchanges[name] = (old, node)
189 self._recordchange(tr)
189 self._recordchange(tr)
190
190
191 def _recordchange(self, tr):
191 def _recordchange(self, tr):
192 """record that bookmarks have been changed in a transaction
192 """record that bookmarks have been changed in a transaction
193
193
194 The transaction is then responsible for updating the file content."""
194 The transaction is then responsible for updating the file content."""
195 tr.addfilegenerator('bookmarks', ('bookmarks',), self._write,
195 tr.addfilegenerator('bookmarks', ('bookmarks',), self._write,
196 location='plain')
196 location='plain')
197 tr.hookargs['bookmark_moved'] = '1'
197 tr.hookargs['bookmark_moved'] = '1'
198
198
199 def _writerepo(self, repo):
199 def _writerepo(self, repo):
200 """Factored out for extensibility"""
200 """Factored out for extensibility"""
201 rbm = repo._bookmarks
201 rbm = repo._bookmarks
202 if rbm.active not in self._refmap:
202 if rbm.active not in self._refmap:
203 rbm.active = None
203 rbm.active = None
204 rbm._writeactive()
204 rbm._writeactive()
205
205
206 with repo.wlock():
206 with repo.wlock():
207 with repo.vfs('bookmarks', 'w', atomictemp=True,
207 with repo.vfs('bookmarks', 'w', atomictemp=True,
208 checkambig=True) as f:
208 checkambig=True) as f:
209 self._write(f)
209 self._write(f)
210
210
211 def _writeactive(self):
211 def _writeactive(self):
212 if self._aclean:
212 if self._aclean:
213 return
213 return
214 with self._repo.wlock():
214 with self._repo.wlock():
215 if self._active is not None:
215 if self._active is not None:
216 with self._repo.vfs('bookmarks.current', 'w', atomictemp=True,
216 with self._repo.vfs('bookmarks.current', 'w', atomictemp=True,
217 checkambig=True) as f:
217 checkambig=True) as f:
218 f.write(encoding.fromlocal(self._active))
218 f.write(encoding.fromlocal(self._active))
219 else:
219 else:
220 self._repo.vfs.tryunlink('bookmarks.current')
220 self._repo.vfs.tryunlink('bookmarks.current')
221 self._aclean = True
221 self._aclean = True
222
222
223 def _write(self, fp):
223 def _write(self, fp):
224 for name, node in sorted(self._refmap.iteritems()):
224 for name, node in sorted(self._refmap.iteritems()):
225 fp.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
225 fp.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
226 self._clean = True
226 self._clean = True
227 self._repo.invalidatevolatilesets()
227 self._repo.invalidatevolatilesets()
228
228
229 def expandname(self, bname):
229 def expandname(self, bname):
230 if bname == '.':
230 if bname == '.':
231 if self.active:
231 if self.active:
232 return self.active
232 return self.active
233 else:
233 else:
234 raise error.RepoLookupError(_("no active bookmark"))
234 raise error.RepoLookupError(_("no active bookmark"))
235 return bname
235 return bname
236
236
237 def checkconflict(self, mark, force=False, target=None):
237 def checkconflict(self, mark, force=False, target=None):
238 """check repo for a potential clash of mark with an existing bookmark,
238 """check repo for a potential clash of mark with an existing bookmark,
239 branch, or hash
239 branch, or hash
240
240
241 If target is supplied, then check that we are moving the bookmark
241 If target is supplied, then check that we are moving the bookmark
242 forward.
242 forward.
243
243
244 If force is supplied, then forcibly move the bookmark to a new commit
244 If force is supplied, then forcibly move the bookmark to a new commit
245 regardless if it is a move forward.
245 regardless if it is a move forward.
246
246
247 If divergent bookmark are to be deleted, they will be returned as list.
247 If divergent bookmark are to be deleted, they will be returned as list.
248 """
248 """
249 cur = self._repo['.'].node()
249 cur = self._repo['.'].node()
250 if mark in self._refmap and not force:
250 if mark in self._refmap and not force:
251 if target:
251 if target:
252 if self._refmap[mark] == target and target == cur:
252 if self._refmap[mark] == target and target == cur:
253 # re-activating a bookmark
253 # re-activating a bookmark
254 return []
254 return []
255 rev = self._repo[target].rev()
255 rev = self._repo[target].rev()
256 anc = self._repo.changelog.ancestors([rev])
256 anc = self._repo.changelog.ancestors([rev])
257 bmctx = self.changectx(mark)
257 bmctx = self.changectx(mark)
258 divs = [self._refmap[b] for b in self._refmap
258 divs = [self._refmap[b] for b in self._refmap
259 if b.split('@', 1)[0] == mark.split('@', 1)[0]]
259 if b.split('@', 1)[0] == mark.split('@', 1)[0]]
260
260
261 # allow resolving a single divergent bookmark even if moving
261 # allow resolving a single divergent bookmark even if moving
262 # the bookmark across branches when a revision is specified
262 # the bookmark across branches when a revision is specified
263 # that contains a divergent bookmark
263 # that contains a divergent bookmark
264 if bmctx.rev() not in anc and target in divs:
264 if bmctx.rev() not in anc and target in divs:
265 return divergent2delete(self._repo, [target], mark)
265 return divergent2delete(self._repo, [target], mark)
266
266
267 deletefrom = [b for b in divs
267 deletefrom = [b for b in divs
268 if self._repo[b].rev() in anc or b == target]
268 if self._repo[b].rev() in anc or b == target]
269 delbms = divergent2delete(self._repo, deletefrom, mark)
269 delbms = divergent2delete(self._repo, deletefrom, mark)
270 if validdest(self._repo, bmctx, self._repo[target]):
270 if validdest(self._repo, bmctx, self._repo[target]):
271 self._repo.ui.status(
271 self._repo.ui.status(
272 _("moving bookmark '%s' forward from %s\n") %
272 _("moving bookmark '%s' forward from %s\n") %
273 (mark, short(bmctx.node())))
273 (mark, short(bmctx.node())))
274 return delbms
274 return delbms
275 raise error.Abort(_("bookmark '%s' already exists "
275 raise error.Abort(_("bookmark '%s' already exists "
276 "(use -f to force)") % mark)
276 "(use -f to force)") % mark)
277 if ((mark in self._repo.branchmap() or
277 if ((mark in self._repo.branchmap() or
278 mark == self._repo.dirstate.branch()) and not force):
278 mark == self._repo.dirstate.branch()) and not force):
279 raise error.Abort(
279 raise error.Abort(
280 _("a bookmark cannot have the name of an existing branch"))
280 _("a bookmark cannot have the name of an existing branch"))
281 if len(mark) > 3 and not force:
281 if len(mark) > 3 and not force:
282 try:
282 try:
283 shadowhash = scmutil.isrevsymbol(self._repo, mark)
283 shadowhash = scmutil.isrevsymbol(self._repo, mark)
284 except error.LookupError: # ambiguous identifier
284 except error.LookupError: # ambiguous identifier
285 shadowhash = False
285 shadowhash = False
286 if shadowhash:
286 if shadowhash:
287 self._repo.ui.warn(
287 self._repo.ui.warn(
288 _("bookmark %s matches a changeset hash\n"
288 _("bookmark %s matches a changeset hash\n"
289 "(did you leave a -r out of an 'hg bookmark' "
289 "(did you leave a -r out of an 'hg bookmark' "
290 "command?)\n")
290 "command?)\n")
291 % mark)
291 % mark)
292 return []
292 return []
293
293
294 def _readactive(repo, marks):
294 def _readactive(repo, marks):
295 """
295 """
296 Get the active bookmark. We can have an active bookmark that updates
296 Get the active bookmark. We can have an active bookmark that updates
297 itself as we commit. This function returns the name of that bookmark.
297 itself as we commit. This function returns the name of that bookmark.
298 It is stored in .hg/bookmarks.current
298 It is stored in .hg/bookmarks.current
299 """
299 """
300 try:
300 # No readline() in osutil.posixfile, reading everything is
301 file = repo.vfs('bookmarks.current')
301 # cheap.
302 except IOError as inst:
302 content = repo.vfs.tryread('bookmarks.current')
303 if inst.errno != errno.ENOENT:
303 mark = encoding.tolocal((content.splitlines() or [''])[0])
304 raise
304 if mark == '' or mark not in marks:
305 return None
305 mark = None
306 try:
307 # No readline() in osutil.posixfile, reading everything is
308 # cheap.
309 # Note that it's possible for readlines() here to raise
310 # IOError, since we might be reading the active mark over
311 # static-http which only tries to load the file when we try
312 # to read from it.
313 mark = encoding.tolocal((file.readlines() or [''])[0])
314 if mark == '' or mark not in marks:
315 mark = None
316 except IOError as inst:
317 if inst.errno != errno.ENOENT:
318 raise
319 return None
320 finally:
321 file.close()
322 return mark
306 return mark
323
307
324 def activate(repo, mark):
308 def activate(repo, mark):
325 """
309 """
326 Set the given bookmark to be 'active', meaning that this bookmark will
310 Set the given bookmark to be 'active', meaning that this bookmark will
327 follow new commits that are made.
311 follow new commits that are made.
328 The name is recorded in .hg/bookmarks.current
312 The name is recorded in .hg/bookmarks.current
329 """
313 """
330 repo._bookmarks.active = mark
314 repo._bookmarks.active = mark
331 repo._bookmarks._writeactive()
315 repo._bookmarks._writeactive()
332
316
333 def deactivate(repo):
317 def deactivate(repo):
334 """
318 """
335 Unset the active bookmark in this repository.
319 Unset the active bookmark in this repository.
336 """
320 """
337 repo._bookmarks.active = None
321 repo._bookmarks.active = None
338 repo._bookmarks._writeactive()
322 repo._bookmarks._writeactive()
339
323
340 def isactivewdirparent(repo):
324 def isactivewdirparent(repo):
341 """
325 """
342 Tell whether the 'active' bookmark (the one that follows new commits)
326 Tell whether the 'active' bookmark (the one that follows new commits)
343 points to one of the parents of the current working directory (wdir).
327 points to one of the parents of the current working directory (wdir).
344
328
345 While this is normally the case, it can on occasion be false; for example,
329 While this is normally the case, it can on occasion be false; for example,
346 immediately after a pull, the active bookmark can be moved to point
330 immediately after a pull, the active bookmark can be moved to point
347 to a place different than the wdir. This is solved by running `hg update`.
331 to a place different than the wdir. This is solved by running `hg update`.
348 """
332 """
349 mark = repo._activebookmark
333 mark = repo._activebookmark
350 marks = repo._bookmarks
334 marks = repo._bookmarks
351 parents = [p.node() for p in repo[None].parents()]
335 parents = [p.node() for p in repo[None].parents()]
352 return (mark in marks and marks[mark] in parents)
336 return (mark in marks and marks[mark] in parents)
353
337
354 def divergent2delete(repo, deletefrom, bm):
338 def divergent2delete(repo, deletefrom, bm):
355 """find divergent versions of bm on nodes in deletefrom.
339 """find divergent versions of bm on nodes in deletefrom.
356
340
357 the list of bookmark to delete."""
341 the list of bookmark to delete."""
358 todelete = []
342 todelete = []
359 marks = repo._bookmarks
343 marks = repo._bookmarks
360 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
344 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
361 for mark in divergent:
345 for mark in divergent:
362 if mark == '@' or '@' not in mark:
346 if mark == '@' or '@' not in mark:
363 # can't be divergent by definition
347 # can't be divergent by definition
364 continue
348 continue
365 if mark and marks[mark] in deletefrom:
349 if mark and marks[mark] in deletefrom:
366 if mark != bm:
350 if mark != bm:
367 todelete.append(mark)
351 todelete.append(mark)
368 return todelete
352 return todelete
369
353
370 def headsforactive(repo):
354 def headsforactive(repo):
371 """Given a repo with an active bookmark, return divergent bookmark nodes.
355 """Given a repo with an active bookmark, return divergent bookmark nodes.
372
356
373 Args:
357 Args:
374 repo: A repository with an active bookmark.
358 repo: A repository with an active bookmark.
375
359
376 Returns:
360 Returns:
377 A list of binary node ids that is the full list of other
361 A list of binary node ids that is the full list of other
378 revisions with bookmarks divergent from the active bookmark. If
362 revisions with bookmarks divergent from the active bookmark. If
379 there were no divergent bookmarks, then this list will contain
363 there were no divergent bookmarks, then this list will contain
380 only one entry.
364 only one entry.
381 """
365 """
382 if not repo._activebookmark:
366 if not repo._activebookmark:
383 raise ValueError(
367 raise ValueError(
384 'headsforactive() only makes sense with an active bookmark')
368 'headsforactive() only makes sense with an active bookmark')
385 name = repo._activebookmark.split('@', 1)[0]
369 name = repo._activebookmark.split('@', 1)[0]
386 heads = []
370 heads = []
387 for mark, n in repo._bookmarks.iteritems():
371 for mark, n in repo._bookmarks.iteritems():
388 if mark.split('@', 1)[0] == name:
372 if mark.split('@', 1)[0] == name:
389 heads.append(n)
373 heads.append(n)
390 return heads
374 return heads
391
375
392 def calculateupdate(ui, repo):
376 def calculateupdate(ui, repo):
393 '''Return a tuple (activemark, movemarkfrom) indicating the active bookmark
377 '''Return a tuple (activemark, movemarkfrom) indicating the active bookmark
394 and where to move the active bookmark from, if needed.'''
378 and where to move the active bookmark from, if needed.'''
395 checkout, movemarkfrom = None, None
379 checkout, movemarkfrom = None, None
396 activemark = repo._activebookmark
380 activemark = repo._activebookmark
397 if isactivewdirparent(repo):
381 if isactivewdirparent(repo):
398 movemarkfrom = repo['.'].node()
382 movemarkfrom = repo['.'].node()
399 elif activemark:
383 elif activemark:
400 ui.status(_("updating to active bookmark %s\n") % activemark)
384 ui.status(_("updating to active bookmark %s\n") % activemark)
401 checkout = activemark
385 checkout = activemark
402 return (checkout, movemarkfrom)
386 return (checkout, movemarkfrom)
403
387
404 def update(repo, parents, node):
388 def update(repo, parents, node):
405 deletefrom = parents
389 deletefrom = parents
406 marks = repo._bookmarks
390 marks = repo._bookmarks
407 active = marks.active
391 active = marks.active
408 if not active:
392 if not active:
409 return False
393 return False
410
394
411 bmchanges = []
395 bmchanges = []
412 if marks[active] in parents:
396 if marks[active] in parents:
413 new = repo[node]
397 new = repo[node]
414 divs = [marks.changectx(b) for b in marks
398 divs = [marks.changectx(b) for b in marks
415 if b.split('@', 1)[0] == active.split('@', 1)[0]]
399 if b.split('@', 1)[0] == active.split('@', 1)[0]]
416 anc = repo.changelog.ancestors([new.rev()])
400 anc = repo.changelog.ancestors([new.rev()])
417 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
401 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
418 if validdest(repo, marks.changectx(active), new):
402 if validdest(repo, marks.changectx(active), new):
419 bmchanges.append((active, new.node()))
403 bmchanges.append((active, new.node()))
420
404
421 for bm in divergent2delete(repo, deletefrom, active):
405 for bm in divergent2delete(repo, deletefrom, active):
422 bmchanges.append((bm, None))
406 bmchanges.append((bm, None))
423
407
424 if bmchanges:
408 if bmchanges:
425 with repo.lock(), repo.transaction('bookmark') as tr:
409 with repo.lock(), repo.transaction('bookmark') as tr:
426 marks.applychanges(repo, tr, bmchanges)
410 marks.applychanges(repo, tr, bmchanges)
427 return bool(bmchanges)
411 return bool(bmchanges)
428
412
429 def listbinbookmarks(repo):
413 def listbinbookmarks(repo):
430 # We may try to list bookmarks on a repo type that does not
414 # We may try to list bookmarks on a repo type that does not
431 # support it (e.g., statichttprepository).
415 # support it (e.g., statichttprepository).
432 marks = getattr(repo, '_bookmarks', {})
416 marks = getattr(repo, '_bookmarks', {})
433
417
434 hasnode = repo.changelog.hasnode
418 hasnode = repo.changelog.hasnode
435 for k, v in marks.iteritems():
419 for k, v in marks.iteritems():
436 # don't expose local divergent bookmarks
420 # don't expose local divergent bookmarks
437 if hasnode(v) and ('@' not in k or k.endswith('@')):
421 if hasnode(v) and ('@' not in k or k.endswith('@')):
438 yield k, v
422 yield k, v
439
423
440 def listbookmarks(repo):
424 def listbookmarks(repo):
441 d = {}
425 d = {}
442 for book, node in listbinbookmarks(repo):
426 for book, node in listbinbookmarks(repo):
443 d[book] = hex(node)
427 d[book] = hex(node)
444 return d
428 return d
445
429
446 def pushbookmark(repo, key, old, new):
430 def pushbookmark(repo, key, old, new):
447 with repo.wlock(), repo.lock(), repo.transaction('bookmarks') as tr:
431 with repo.wlock(), repo.lock(), repo.transaction('bookmarks') as tr:
448 marks = repo._bookmarks
432 marks = repo._bookmarks
449 existing = hex(marks.get(key, ''))
433 existing = hex(marks.get(key, ''))
450 if existing != old and existing != new:
434 if existing != old and existing != new:
451 return False
435 return False
452 if new == '':
436 if new == '':
453 changes = [(key, None)]
437 changes = [(key, None)]
454 else:
438 else:
455 if new not in repo:
439 if new not in repo:
456 return False
440 return False
457 changes = [(key, repo[new].node())]
441 changes = [(key, repo[new].node())]
458 marks.applychanges(repo, tr, changes)
442 marks.applychanges(repo, tr, changes)
459 return True
443 return True
460
444
461 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
445 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
462 '''Compare bookmarks between srcmarks and dstmarks
446 '''Compare bookmarks between srcmarks and dstmarks
463
447
464 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
448 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
465 differ, invalid)", each are list of bookmarks below:
449 differ, invalid)", each are list of bookmarks below:
466
450
467 :addsrc: added on src side (removed on dst side, perhaps)
451 :addsrc: added on src side (removed on dst side, perhaps)
468 :adddst: added on dst side (removed on src side, perhaps)
452 :adddst: added on dst side (removed on src side, perhaps)
469 :advsrc: advanced on src side
453 :advsrc: advanced on src side
470 :advdst: advanced on dst side
454 :advdst: advanced on dst side
471 :diverge: diverge
455 :diverge: diverge
472 :differ: changed, but changeset referred on src is unknown on dst
456 :differ: changed, but changeset referred on src is unknown on dst
473 :invalid: unknown on both side
457 :invalid: unknown on both side
474 :same: same on both side
458 :same: same on both side
475
459
476 Each elements of lists in result tuple is tuple "(bookmark name,
460 Each elements of lists in result tuple is tuple "(bookmark name,
477 changeset ID on source side, changeset ID on destination
461 changeset ID on source side, changeset ID on destination
478 side)". Each changeset IDs are 40 hexadecimal digit string or
462 side)". Each changeset IDs are 40 hexadecimal digit string or
479 None.
463 None.
480
464
481 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
465 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
482 "invalid" list may be unknown for repo.
466 "invalid" list may be unknown for repo.
483
467
484 If "targets" is specified, only bookmarks listed in it are
468 If "targets" is specified, only bookmarks listed in it are
485 examined.
469 examined.
486 '''
470 '''
487
471
488 if targets:
472 if targets:
489 bset = set(targets)
473 bset = set(targets)
490 else:
474 else:
491 srcmarkset = set(srcmarks)
475 srcmarkset = set(srcmarks)
492 dstmarkset = set(dstmarks)
476 dstmarkset = set(dstmarks)
493 bset = srcmarkset | dstmarkset
477 bset = srcmarkset | dstmarkset
494
478
495 results = ([], [], [], [], [], [], [], [])
479 results = ([], [], [], [], [], [], [], [])
496 addsrc = results[0].append
480 addsrc = results[0].append
497 adddst = results[1].append
481 adddst = results[1].append
498 advsrc = results[2].append
482 advsrc = results[2].append
499 advdst = results[3].append
483 advdst = results[3].append
500 diverge = results[4].append
484 diverge = results[4].append
501 differ = results[5].append
485 differ = results[5].append
502 invalid = results[6].append
486 invalid = results[6].append
503 same = results[7].append
487 same = results[7].append
504
488
505 for b in sorted(bset):
489 for b in sorted(bset):
506 if b not in srcmarks:
490 if b not in srcmarks:
507 if b in dstmarks:
491 if b in dstmarks:
508 adddst((b, None, dstmarks[b]))
492 adddst((b, None, dstmarks[b]))
509 else:
493 else:
510 invalid((b, None, None))
494 invalid((b, None, None))
511 elif b not in dstmarks:
495 elif b not in dstmarks:
512 addsrc((b, srcmarks[b], None))
496 addsrc((b, srcmarks[b], None))
513 else:
497 else:
514 scid = srcmarks[b]
498 scid = srcmarks[b]
515 dcid = dstmarks[b]
499 dcid = dstmarks[b]
516 if scid == dcid:
500 if scid == dcid:
517 same((b, scid, dcid))
501 same((b, scid, dcid))
518 elif scid in repo and dcid in repo:
502 elif scid in repo and dcid in repo:
519 sctx = repo[scid]
503 sctx = repo[scid]
520 dctx = repo[dcid]
504 dctx = repo[dcid]
521 if sctx.rev() < dctx.rev():
505 if sctx.rev() < dctx.rev():
522 if validdest(repo, sctx, dctx):
506 if validdest(repo, sctx, dctx):
523 advdst((b, scid, dcid))
507 advdst((b, scid, dcid))
524 else:
508 else:
525 diverge((b, scid, dcid))
509 diverge((b, scid, dcid))
526 else:
510 else:
527 if validdest(repo, dctx, sctx):
511 if validdest(repo, dctx, sctx):
528 advsrc((b, scid, dcid))
512 advsrc((b, scid, dcid))
529 else:
513 else:
530 diverge((b, scid, dcid))
514 diverge((b, scid, dcid))
531 else:
515 else:
532 # it is too expensive to examine in detail, in this case
516 # it is too expensive to examine in detail, in this case
533 differ((b, scid, dcid))
517 differ((b, scid, dcid))
534
518
535 return results
519 return results
536
520
537 def _diverge(ui, b, path, localmarks, remotenode):
521 def _diverge(ui, b, path, localmarks, remotenode):
538 '''Return appropriate diverged bookmark for specified ``path``
522 '''Return appropriate diverged bookmark for specified ``path``
539
523
540 This returns None, if it is failed to assign any divergent
524 This returns None, if it is failed to assign any divergent
541 bookmark name.
525 bookmark name.
542
526
543 This reuses already existing one with "@number" suffix, if it
527 This reuses already existing one with "@number" suffix, if it
544 refers ``remotenode``.
528 refers ``remotenode``.
545 '''
529 '''
546 if b == '@':
530 if b == '@':
547 b = ''
531 b = ''
548 # try to use an @pathalias suffix
532 # try to use an @pathalias suffix
549 # if an @pathalias already exists, we overwrite (update) it
533 # if an @pathalias already exists, we overwrite (update) it
550 if path.startswith("file:"):
534 if path.startswith("file:"):
551 path = util.url(path).path
535 path = util.url(path).path
552 for p, u in ui.configitems("paths"):
536 for p, u in ui.configitems("paths"):
553 if u.startswith("file:"):
537 if u.startswith("file:"):
554 u = util.url(u).path
538 u = util.url(u).path
555 if path == u:
539 if path == u:
556 return '%s@%s' % (b, p)
540 return '%s@%s' % (b, p)
557
541
558 # assign a unique "@number" suffix newly
542 # assign a unique "@number" suffix newly
559 for x in range(1, 100):
543 for x in range(1, 100):
560 n = '%s@%d' % (b, x)
544 n = '%s@%d' % (b, x)
561 if n not in localmarks or localmarks[n] == remotenode:
545 if n not in localmarks or localmarks[n] == remotenode:
562 return n
546 return n
563
547
564 return None
548 return None
565
549
566 def unhexlifybookmarks(marks):
550 def unhexlifybookmarks(marks):
567 binremotemarks = {}
551 binremotemarks = {}
568 for name, node in marks.items():
552 for name, node in marks.items():
569 binremotemarks[name] = bin(node)
553 binremotemarks[name] = bin(node)
570 return binremotemarks
554 return binremotemarks
571
555
572 _binaryentry = struct.Struct('>20sH')
556 _binaryentry = struct.Struct('>20sH')
573
557
574 def binaryencode(bookmarks):
558 def binaryencode(bookmarks):
575 """encode a '(bookmark, node)' iterable into a binary stream
559 """encode a '(bookmark, node)' iterable into a binary stream
576
560
577 the binary format is:
561 the binary format is:
578
562
579 <node><bookmark-length><bookmark-name>
563 <node><bookmark-length><bookmark-name>
580
564
581 :node: is a 20 bytes binary node,
565 :node: is a 20 bytes binary node,
582 :bookmark-length: an unsigned short,
566 :bookmark-length: an unsigned short,
583 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
567 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
584
568
585 wdirid (all bits set) will be used as a special value for "missing"
569 wdirid (all bits set) will be used as a special value for "missing"
586 """
570 """
587 binarydata = []
571 binarydata = []
588 for book, node in bookmarks:
572 for book, node in bookmarks:
589 if not node: # None or ''
573 if not node: # None or ''
590 node = wdirid
574 node = wdirid
591 binarydata.append(_binaryentry.pack(node, len(book)))
575 binarydata.append(_binaryentry.pack(node, len(book)))
592 binarydata.append(book)
576 binarydata.append(book)
593 return ''.join(binarydata)
577 return ''.join(binarydata)
594
578
595 def binarydecode(stream):
579 def binarydecode(stream):
596 """decode a binary stream into an '(bookmark, node)' iterable
580 """decode a binary stream into an '(bookmark, node)' iterable
597
581
598 the binary format is:
582 the binary format is:
599
583
600 <node><bookmark-length><bookmark-name>
584 <node><bookmark-length><bookmark-name>
601
585
602 :node: is a 20 bytes binary node,
586 :node: is a 20 bytes binary node,
603 :bookmark-length: an unsigned short,
587 :bookmark-length: an unsigned short,
604 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
588 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
605
589
606 wdirid (all bits set) will be used as a special value for "missing"
590 wdirid (all bits set) will be used as a special value for "missing"
607 """
591 """
608 entrysize = _binaryentry.size
592 entrysize = _binaryentry.size
609 books = []
593 books = []
610 while True:
594 while True:
611 entry = stream.read(entrysize)
595 entry = stream.read(entrysize)
612 if len(entry) < entrysize:
596 if len(entry) < entrysize:
613 if entry:
597 if entry:
614 raise error.Abort(_('bad bookmark stream'))
598 raise error.Abort(_('bad bookmark stream'))
615 break
599 break
616 node, length = _binaryentry.unpack(entry)
600 node, length = _binaryentry.unpack(entry)
617 bookmark = stream.read(length)
601 bookmark = stream.read(length)
618 if len(bookmark) < length:
602 if len(bookmark) < length:
619 if entry:
603 if entry:
620 raise error.Abort(_('bad bookmark stream'))
604 raise error.Abort(_('bad bookmark stream'))
621 if node == wdirid:
605 if node == wdirid:
622 node = None
606 node = None
623 books.append((bookmark, node))
607 books.append((bookmark, node))
624 return books
608 return books
625
609
626 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
610 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
627 ui.debug("checking for updated bookmarks\n")
611 ui.debug("checking for updated bookmarks\n")
628 localmarks = repo._bookmarks
612 localmarks = repo._bookmarks
629 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
613 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
630 ) = comparebookmarks(repo, remotemarks, localmarks)
614 ) = comparebookmarks(repo, remotemarks, localmarks)
631
615
632 status = ui.status
616 status = ui.status
633 warn = ui.warn
617 warn = ui.warn
634 if ui.configbool('ui', 'quietbookmarkmove'):
618 if ui.configbool('ui', 'quietbookmarkmove'):
635 status = warn = ui.debug
619 status = warn = ui.debug
636
620
637 explicit = set(explicit)
621 explicit = set(explicit)
638 changed = []
622 changed = []
639 for b, scid, dcid in addsrc:
623 for b, scid, dcid in addsrc:
640 if scid in repo: # add remote bookmarks for changes we already have
624 if scid in repo: # add remote bookmarks for changes we already have
641 changed.append((b, scid, status,
625 changed.append((b, scid, status,
642 _("adding remote bookmark %s\n") % (b)))
626 _("adding remote bookmark %s\n") % (b)))
643 elif b in explicit:
627 elif b in explicit:
644 explicit.remove(b)
628 explicit.remove(b)
645 ui.warn(_("remote bookmark %s points to locally missing %s\n")
629 ui.warn(_("remote bookmark %s points to locally missing %s\n")
646 % (b, hex(scid)[:12]))
630 % (b, hex(scid)[:12]))
647
631
648 for b, scid, dcid in advsrc:
632 for b, scid, dcid in advsrc:
649 changed.append((b, scid, status,
633 changed.append((b, scid, status,
650 _("updating bookmark %s\n") % (b)))
634 _("updating bookmark %s\n") % (b)))
651 # remove normal movement from explicit set
635 # remove normal movement from explicit set
652 explicit.difference_update(d[0] for d in changed)
636 explicit.difference_update(d[0] for d in changed)
653
637
654 for b, scid, dcid in diverge:
638 for b, scid, dcid in diverge:
655 if b in explicit:
639 if b in explicit:
656 explicit.discard(b)
640 explicit.discard(b)
657 changed.append((b, scid, status,
641 changed.append((b, scid, status,
658 _("importing bookmark %s\n") % (b)))
642 _("importing bookmark %s\n") % (b)))
659 else:
643 else:
660 db = _diverge(ui, b, path, localmarks, scid)
644 db = _diverge(ui, b, path, localmarks, scid)
661 if db:
645 if db:
662 changed.append((db, scid, warn,
646 changed.append((db, scid, warn,
663 _("divergent bookmark %s stored as %s\n") %
647 _("divergent bookmark %s stored as %s\n") %
664 (b, db)))
648 (b, db)))
665 else:
649 else:
666 warn(_("warning: failed to assign numbered name "
650 warn(_("warning: failed to assign numbered name "
667 "to divergent bookmark %s\n") % (b))
651 "to divergent bookmark %s\n") % (b))
668 for b, scid, dcid in adddst + advdst:
652 for b, scid, dcid in adddst + advdst:
669 if b in explicit:
653 if b in explicit:
670 explicit.discard(b)
654 explicit.discard(b)
671 changed.append((b, scid, status,
655 changed.append((b, scid, status,
672 _("importing bookmark %s\n") % (b)))
656 _("importing bookmark %s\n") % (b)))
673 for b, scid, dcid in differ:
657 for b, scid, dcid in differ:
674 if b in explicit:
658 if b in explicit:
675 explicit.remove(b)
659 explicit.remove(b)
676 ui.warn(_("remote bookmark %s points to locally missing %s\n")
660 ui.warn(_("remote bookmark %s points to locally missing %s\n")
677 % (b, hex(scid)[:12]))
661 % (b, hex(scid)[:12]))
678
662
679 if changed:
663 if changed:
680 tr = trfunc()
664 tr = trfunc()
681 changes = []
665 changes = []
682 for b, node, writer, msg in sorted(changed):
666 for b, node, writer, msg in sorted(changed):
683 changes.append((b, node))
667 changes.append((b, node))
684 writer(msg)
668 writer(msg)
685 localmarks.applychanges(repo, tr, changes)
669 localmarks.applychanges(repo, tr, changes)
686
670
687 def incoming(ui, repo, peer):
671 def incoming(ui, repo, peer):
688 '''Show bookmarks incoming from other to repo
672 '''Show bookmarks incoming from other to repo
689 '''
673 '''
690 ui.status(_("searching for changed bookmarks\n"))
674 ui.status(_("searching for changed bookmarks\n"))
691
675
692 with peer.commandexecutor() as e:
676 with peer.commandexecutor() as e:
693 remotemarks = unhexlifybookmarks(e.callcommand('listkeys', {
677 remotemarks = unhexlifybookmarks(e.callcommand('listkeys', {
694 'namespace': 'bookmarks',
678 'namespace': 'bookmarks',
695 }).result())
679 }).result())
696
680
697 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
681 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
698 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
682 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
699
683
700 incomings = []
684 incomings = []
701 if ui.debugflag:
685 if ui.debugflag:
702 getid = lambda id: id
686 getid = lambda id: id
703 else:
687 else:
704 getid = lambda id: id[:12]
688 getid = lambda id: id[:12]
705 if ui.verbose:
689 if ui.verbose:
706 def add(b, id, st):
690 def add(b, id, st):
707 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
691 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
708 else:
692 else:
709 def add(b, id, st):
693 def add(b, id, st):
710 incomings.append(" %-25s %s\n" % (b, getid(id)))
694 incomings.append(" %-25s %s\n" % (b, getid(id)))
711 for b, scid, dcid in addsrc:
695 for b, scid, dcid in addsrc:
712 # i18n: "added" refers to a bookmark
696 # i18n: "added" refers to a bookmark
713 add(b, hex(scid), _('added'))
697 add(b, hex(scid), _('added'))
714 for b, scid, dcid in advsrc:
698 for b, scid, dcid in advsrc:
715 # i18n: "advanced" refers to a bookmark
699 # i18n: "advanced" refers to a bookmark
716 add(b, hex(scid), _('advanced'))
700 add(b, hex(scid), _('advanced'))
717 for b, scid, dcid in diverge:
701 for b, scid, dcid in diverge:
718 # i18n: "diverged" refers to a bookmark
702 # i18n: "diverged" refers to a bookmark
719 add(b, hex(scid), _('diverged'))
703 add(b, hex(scid), _('diverged'))
720 for b, scid, dcid in differ:
704 for b, scid, dcid in differ:
721 # i18n: "changed" refers to a bookmark
705 # i18n: "changed" refers to a bookmark
722 add(b, hex(scid), _('changed'))
706 add(b, hex(scid), _('changed'))
723
707
724 if not incomings:
708 if not incomings:
725 ui.status(_("no changed bookmarks found\n"))
709 ui.status(_("no changed bookmarks found\n"))
726 return 1
710 return 1
727
711
728 for s in sorted(incomings):
712 for s in sorted(incomings):
729 ui.write(s)
713 ui.write(s)
730
714
731 return 0
715 return 0
732
716
733 def outgoing(ui, repo, other):
717 def outgoing(ui, repo, other):
734 '''Show bookmarks outgoing from repo to other
718 '''Show bookmarks outgoing from repo to other
735 '''
719 '''
736 ui.status(_("searching for changed bookmarks\n"))
720 ui.status(_("searching for changed bookmarks\n"))
737
721
738 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
722 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
739 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
723 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
740 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
724 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
741
725
742 outgoings = []
726 outgoings = []
743 if ui.debugflag:
727 if ui.debugflag:
744 getid = lambda id: id
728 getid = lambda id: id
745 else:
729 else:
746 getid = lambda id: id[:12]
730 getid = lambda id: id[:12]
747 if ui.verbose:
731 if ui.verbose:
748 def add(b, id, st):
732 def add(b, id, st):
749 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
733 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
750 else:
734 else:
751 def add(b, id, st):
735 def add(b, id, st):
752 outgoings.append(" %-25s %s\n" % (b, getid(id)))
736 outgoings.append(" %-25s %s\n" % (b, getid(id)))
753 for b, scid, dcid in addsrc:
737 for b, scid, dcid in addsrc:
754 # i18n: "added refers to a bookmark
738 # i18n: "added refers to a bookmark
755 add(b, hex(scid), _('added'))
739 add(b, hex(scid), _('added'))
756 for b, scid, dcid in adddst:
740 for b, scid, dcid in adddst:
757 # i18n: "deleted" refers to a bookmark
741 # i18n: "deleted" refers to a bookmark
758 add(b, ' ' * 40, _('deleted'))
742 add(b, ' ' * 40, _('deleted'))
759 for b, scid, dcid in advsrc:
743 for b, scid, dcid in advsrc:
760 # i18n: "advanced" refers to a bookmark
744 # i18n: "advanced" refers to a bookmark
761 add(b, hex(scid), _('advanced'))
745 add(b, hex(scid), _('advanced'))
762 for b, scid, dcid in diverge:
746 for b, scid, dcid in diverge:
763 # i18n: "diverged" refers to a bookmark
747 # i18n: "diverged" refers to a bookmark
764 add(b, hex(scid), _('diverged'))
748 add(b, hex(scid), _('diverged'))
765 for b, scid, dcid in differ:
749 for b, scid, dcid in differ:
766 # i18n: "changed" refers to a bookmark
750 # i18n: "changed" refers to a bookmark
767 add(b, hex(scid), _('changed'))
751 add(b, hex(scid), _('changed'))
768
752
769 if not outgoings:
753 if not outgoings:
770 ui.status(_("no changed bookmarks found\n"))
754 ui.status(_("no changed bookmarks found\n"))
771 return 1
755 return 1
772
756
773 for s in sorted(outgoings):
757 for s in sorted(outgoings):
774 ui.write(s)
758 ui.write(s)
775
759
776 return 0
760 return 0
777
761
778 def summary(repo, peer):
762 def summary(repo, peer):
779 '''Compare bookmarks between repo and other for "hg summary" output
763 '''Compare bookmarks between repo and other for "hg summary" output
780
764
781 This returns "(# of incoming, # of outgoing)" tuple.
765 This returns "(# of incoming, # of outgoing)" tuple.
782 '''
766 '''
783 with peer.commandexecutor() as e:
767 with peer.commandexecutor() as e:
784 remotemarks = unhexlifybookmarks(e.callcommand('listkeys', {
768 remotemarks = unhexlifybookmarks(e.callcommand('listkeys', {
785 'namespace': 'bookmarks',
769 'namespace': 'bookmarks',
786 }).result())
770 }).result())
787
771
788 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
772 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
789 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
773 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
790 return (len(addsrc), len(adddst))
774 return (len(addsrc), len(adddst))
791
775
792 def validdest(repo, old, new):
776 def validdest(repo, old, new):
793 """Is the new bookmark destination a valid update from the old one"""
777 """Is the new bookmark destination a valid update from the old one"""
794 repo = repo.unfiltered()
778 repo = repo.unfiltered()
795 if old == new:
779 if old == new:
796 # Old == new -> nothing to update.
780 # Old == new -> nothing to update.
797 return False
781 return False
798 elif not old:
782 elif not old:
799 # old is nullrev, anything is valid.
783 # old is nullrev, anything is valid.
800 # (new != nullrev has been excluded by the previous check)
784 # (new != nullrev has been excluded by the previous check)
801 return True
785 return True
802 elif repo.obsstore:
786 elif repo.obsstore:
803 return new.node() in obsutil.foreground(repo, [old.node()])
787 return new.node() in obsutil.foreground(repo, [old.node()])
804 else:
788 else:
805 # still an independent clause as it is lazier (and therefore faster)
789 # still an independent clause as it is lazier (and therefore faster)
806 return old.isancestorof(new)
790 return old.isancestorof(new)
807
791
808 def checkformat(repo, mark):
792 def checkformat(repo, mark):
809 """return a valid version of a potential bookmark name
793 """return a valid version of a potential bookmark name
810
794
811 Raises an abort error if the bookmark name is not valid.
795 Raises an abort error if the bookmark name is not valid.
812 """
796 """
813 mark = mark.strip()
797 mark = mark.strip()
814 if not mark:
798 if not mark:
815 raise error.Abort(_("bookmark names cannot consist entirely of "
799 raise error.Abort(_("bookmark names cannot consist entirely of "
816 "whitespace"))
800 "whitespace"))
817 scmutil.checknewlabel(repo, mark, 'bookmark')
801 scmutil.checknewlabel(repo, mark, 'bookmark')
818 return mark
802 return mark
819
803
820 def delete(repo, tr, names):
804 def delete(repo, tr, names):
821 """remove a mark from the bookmark store
805 """remove a mark from the bookmark store
822
806
823 Raises an abort error if mark does not exist.
807 Raises an abort error if mark does not exist.
824 """
808 """
825 marks = repo._bookmarks
809 marks = repo._bookmarks
826 changes = []
810 changes = []
827 for mark in names:
811 for mark in names:
828 if mark not in marks:
812 if mark not in marks:
829 raise error.Abort(_("bookmark '%s' does not exist") % mark)
813 raise error.Abort(_("bookmark '%s' does not exist") % mark)
830 if mark == repo._activebookmark:
814 if mark == repo._activebookmark:
831 deactivate(repo)
815 deactivate(repo)
832 changes.append((mark, None))
816 changes.append((mark, None))
833 marks.applychanges(repo, tr, changes)
817 marks.applychanges(repo, tr, changes)
834
818
835 def rename(repo, tr, old, new, force=False, inactive=False):
819 def rename(repo, tr, old, new, force=False, inactive=False):
836 """rename a bookmark from old to new
820 """rename a bookmark from old to new
837
821
838 If force is specified, then the new name can overwrite an existing
822 If force is specified, then the new name can overwrite an existing
839 bookmark.
823 bookmark.
840
824
841 If inactive is specified, then do not activate the new bookmark.
825 If inactive is specified, then do not activate the new bookmark.
842
826
843 Raises an abort error if old is not in the bookmark store.
827 Raises an abort error if old is not in the bookmark store.
844 """
828 """
845 marks = repo._bookmarks
829 marks = repo._bookmarks
846 mark = checkformat(repo, new)
830 mark = checkformat(repo, new)
847 if old not in marks:
831 if old not in marks:
848 raise error.Abort(_("bookmark '%s' does not exist") % old)
832 raise error.Abort(_("bookmark '%s' does not exist") % old)
849 changes = []
833 changes = []
850 for bm in marks.checkconflict(mark, force):
834 for bm in marks.checkconflict(mark, force):
851 changes.append((bm, None))
835 changes.append((bm, None))
852 changes.extend([(mark, marks[old]), (old, None)])
836 changes.extend([(mark, marks[old]), (old, None)])
853 marks.applychanges(repo, tr, changes)
837 marks.applychanges(repo, tr, changes)
854 if repo._activebookmark == old and not inactive:
838 if repo._activebookmark == old and not inactive:
855 activate(repo, mark)
839 activate(repo, mark)
856
840
857 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
841 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
858 """add a list of bookmarks
842 """add a list of bookmarks
859
843
860 If force is specified, then the new name can overwrite an existing
844 If force is specified, then the new name can overwrite an existing
861 bookmark.
845 bookmark.
862
846
863 If inactive is specified, then do not activate any bookmark. Otherwise, the
847 If inactive is specified, then do not activate any bookmark. Otherwise, the
864 first bookmark is activated.
848 first bookmark is activated.
865
849
866 Raises an abort error if old is not in the bookmark store.
850 Raises an abort error if old is not in the bookmark store.
867 """
851 """
868 marks = repo._bookmarks
852 marks = repo._bookmarks
869 cur = repo['.'].node()
853 cur = repo['.'].node()
870 newact = None
854 newact = None
871 changes = []
855 changes = []
872 hiddenrev = None
856 hiddenrev = None
873
857
874 # unhide revs if any
858 # unhide revs if any
875 if rev:
859 if rev:
876 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
860 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
877
861
878 for mark in names:
862 for mark in names:
879 mark = checkformat(repo, mark)
863 mark = checkformat(repo, mark)
880 if newact is None:
864 if newact is None:
881 newact = mark
865 newact = mark
882 if inactive and mark == repo._activebookmark:
866 if inactive and mark == repo._activebookmark:
883 deactivate(repo)
867 deactivate(repo)
884 return
868 return
885 tgt = cur
869 tgt = cur
886 if rev:
870 if rev:
887 ctx = scmutil.revsingle(repo, rev)
871 ctx = scmutil.revsingle(repo, rev)
888 if ctx.hidden():
872 if ctx.hidden():
889 hiddenrev = ctx.hex()[:12]
873 hiddenrev = ctx.hex()[:12]
890 tgt = ctx.node()
874 tgt = ctx.node()
891 for bm in marks.checkconflict(mark, force, tgt):
875 for bm in marks.checkconflict(mark, force, tgt):
892 changes.append((bm, None))
876 changes.append((bm, None))
893 changes.append((mark, tgt))
877 changes.append((mark, tgt))
894
878
895 if hiddenrev:
879 if hiddenrev:
896 repo.ui.warn(_("bookmarking hidden changeset %s\n") % hiddenrev)
880 repo.ui.warn(_("bookmarking hidden changeset %s\n") % hiddenrev)
897
881
898 if ctx.obsolete():
882 if ctx.obsolete():
899 msg = obsutil._getfilteredreason(repo, "%s" % hiddenrev, ctx)
883 msg = obsutil._getfilteredreason(repo, "%s" % hiddenrev, ctx)
900 repo.ui.warn("(%s)\n" % msg)
884 repo.ui.warn("(%s)\n" % msg)
901
885
902 marks.applychanges(repo, tr, changes)
886 marks.applychanges(repo, tr, changes)
903 if not inactive and cur == marks[newact] and not rev:
887 if not inactive and cur == marks[newact] and not rev:
904 activate(repo, newact)
888 activate(repo, newact)
905 elif cur != tgt and newact == repo._activebookmark:
889 elif cur != tgt and newact == repo._activebookmark:
906 deactivate(repo)
890 deactivate(repo)
907
891
908 def _printbookmarks(ui, repo, fm, bmarks):
892 def _printbookmarks(ui, repo, fm, bmarks):
909 """private method to print bookmarks
893 """private method to print bookmarks
910
894
911 Provides a way for extensions to control how bookmarks are printed (e.g.
895 Provides a way for extensions to control how bookmarks are printed (e.g.
912 prepend or postpend names)
896 prepend or postpend names)
913 """
897 """
914 hexfn = fm.hexfunc
898 hexfn = fm.hexfunc
915 if len(bmarks) == 0 and fm.isplain():
899 if len(bmarks) == 0 and fm.isplain():
916 ui.status(_("no bookmarks set\n"))
900 ui.status(_("no bookmarks set\n"))
917 for bmark, (n, prefix, label) in sorted(bmarks.iteritems()):
901 for bmark, (n, prefix, label) in sorted(bmarks.iteritems()):
918 fm.startitem()
902 fm.startitem()
919 fm.context(repo=repo)
903 fm.context(repo=repo)
920 if not ui.quiet:
904 if not ui.quiet:
921 fm.plain(' %s ' % prefix, label=label)
905 fm.plain(' %s ' % prefix, label=label)
922 fm.write('bookmark', '%s', bmark, label=label)
906 fm.write('bookmark', '%s', bmark, label=label)
923 pad = " " * (25 - encoding.colwidth(bmark))
907 pad = " " * (25 - encoding.colwidth(bmark))
924 fm.condwrite(not ui.quiet, 'rev node', pad + ' %d:%s',
908 fm.condwrite(not ui.quiet, 'rev node', pad + ' %d:%s',
925 repo.changelog.rev(n), hexfn(n), label=label)
909 repo.changelog.rev(n), hexfn(n), label=label)
926 fm.data(active=(activebookmarklabel in label))
910 fm.data(active=(activebookmarklabel in label))
927 fm.plain('\n')
911 fm.plain('\n')
928
912
929 def printbookmarks(ui, repo, fm, names=None):
913 def printbookmarks(ui, repo, fm, names=None):
930 """print bookmarks by the given formatter
914 """print bookmarks by the given formatter
931
915
932 Provides a way for extensions to control how bookmarks are printed.
916 Provides a way for extensions to control how bookmarks are printed.
933 """
917 """
934 marks = repo._bookmarks
918 marks = repo._bookmarks
935 bmarks = {}
919 bmarks = {}
936 for bmark in (names or marks):
920 for bmark in (names or marks):
937 if bmark not in marks:
921 if bmark not in marks:
938 raise error.Abort(_("bookmark '%s' does not exist") % bmark)
922 raise error.Abort(_("bookmark '%s' does not exist") % bmark)
939 active = repo._activebookmark
923 active = repo._activebookmark
940 if bmark == active:
924 if bmark == active:
941 prefix, label = '*', activebookmarklabel
925 prefix, label = '*', activebookmarklabel
942 else:
926 else:
943 prefix, label = ' ', ''
927 prefix, label = ' ', ''
944
928
945 bmarks[bmark] = (marks[bmark], prefix, label)
929 bmarks[bmark] = (marks[bmark], prefix, label)
946 _printbookmarks(ui, repo, fm, bmarks)
930 _printbookmarks(ui, repo, fm, bmarks)
947
931
948 def preparehookargs(name, old, new):
932 def preparehookargs(name, old, new):
949 if new is None:
933 if new is None:
950 new = ''
934 new = ''
951 if old is None:
935 if old is None:
952 old = ''
936 old = ''
953 return {'bookmark': name,
937 return {'bookmark': name,
954 'node': hex(new),
938 'node': hex(new),
955 'oldnode': hex(old)}
939 'oldnode': hex(old)}
General Comments 0
You need to be logged in to leave comments. Login now