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