##// END OF EJS Templates
bookmarks: refactor in preparation for next commit...
Valentin Gatien-Baron -
r44853:02750005 default
parent child Browse files
Show More
@@ -1,1053 +1,1057
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):
467 return b'@' in b and not b.endswith(b'@')
468
469
466 def listbinbookmarks(repo):
470 def listbinbookmarks(repo):
467 # 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
468 # support it (e.g., statichttprepository).
472 # support it (e.g., statichttprepository).
469 marks = getattr(repo, '_bookmarks', {})
473 marks = getattr(repo, '_bookmarks', {})
470
474
471 hasnode = repo.changelog.hasnode
475 hasnode = repo.changelog.hasnode
472 for k, v in pycompat.iteritems(marks):
476 for k, v in pycompat.iteritems(marks):
473 # don't expose local divergent bookmarks
477 # don't expose local divergent bookmarks
474 if hasnode(v) and (b'@' not in k or k.endswith(b'@')):
478 if hasnode(v) and not isdivergent(k):
475 yield k, v
479 yield k, v
476
480
477
481
478 def listbookmarks(repo):
482 def listbookmarks(repo):
479 d = {}
483 d = {}
480 for book, node in listbinbookmarks(repo):
484 for book, node in listbinbookmarks(repo):
481 d[book] = hex(node)
485 d[book] = hex(node)
482 return d
486 return d
483
487
484
488
485 def pushbookmark(repo, key, old, new):
489 def pushbookmark(repo, key, old, new):
486 if bookmarksinstore(repo):
490 if bookmarksinstore(repo):
487 wlock = util.nullcontextmanager()
491 wlock = util.nullcontextmanager()
488 else:
492 else:
489 wlock = repo.wlock()
493 wlock = repo.wlock()
490 with wlock, repo.lock(), repo.transaction(b'bookmarks') as tr:
494 with wlock, repo.lock(), repo.transaction(b'bookmarks') as tr:
491 marks = repo._bookmarks
495 marks = repo._bookmarks
492 existing = hex(marks.get(key, b''))
496 existing = hex(marks.get(key, b''))
493 if existing != old and existing != new:
497 if existing != old and existing != new:
494 return False
498 return False
495 if new == b'':
499 if new == b'':
496 changes = [(key, None)]
500 changes = [(key, None)]
497 else:
501 else:
498 if new not in repo:
502 if new not in repo:
499 return False
503 return False
500 changes = [(key, repo[new].node())]
504 changes = [(key, repo[new].node())]
501 marks.applychanges(repo, tr, changes)
505 marks.applychanges(repo, tr, changes)
502 return True
506 return True
503
507
504
508
505 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
509 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
506 '''Compare bookmarks between srcmarks and dstmarks
510 '''Compare bookmarks between srcmarks and dstmarks
507
511
508 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
512 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
509 differ, invalid)", each are list of bookmarks below:
513 differ, invalid)", each are list of bookmarks below:
510
514
511 :addsrc: added on src side (removed on dst side, perhaps)
515 :addsrc: added on src side (removed on dst side, perhaps)
512 :adddst: added on dst side (removed on src side, perhaps)
516 :adddst: added on dst side (removed on src side, perhaps)
513 :advsrc: advanced on src side
517 :advsrc: advanced on src side
514 :advdst: advanced on dst side
518 :advdst: advanced on dst side
515 :diverge: diverge
519 :diverge: diverge
516 :differ: changed, but changeset referred on src is unknown on dst
520 :differ: changed, but changeset referred on src is unknown on dst
517 :invalid: unknown on both side
521 :invalid: unknown on both side
518 :same: same on both side
522 :same: same on both side
519
523
520 Each elements of lists in result tuple is tuple "(bookmark name,
524 Each elements of lists in result tuple is tuple "(bookmark name,
521 changeset ID on source side, changeset ID on destination
525 changeset ID on source side, changeset ID on destination
522 side)". Each changeset ID is a binary node or None.
526 side)". Each changeset ID is a binary node or None.
523
527
524 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
528 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
525 "invalid" list may be unknown for repo.
529 "invalid" list may be unknown for repo.
526
530
527 If "targets" is specified, only bookmarks listed in it are
531 If "targets" is specified, only bookmarks listed in it are
528 examined.
532 examined.
529 '''
533 '''
530
534
531 if targets:
535 if targets:
532 bset = set(targets)
536 bset = set(targets)
533 else:
537 else:
534 srcmarkset = set(srcmarks)
538 srcmarkset = set(srcmarks)
535 dstmarkset = set(dstmarks)
539 dstmarkset = set(dstmarks)
536 bset = srcmarkset | dstmarkset
540 bset = srcmarkset | dstmarkset
537
541
538 results = ([], [], [], [], [], [], [], [])
542 results = ([], [], [], [], [], [], [], [])
539 addsrc = results[0].append
543 addsrc = results[0].append
540 adddst = results[1].append
544 adddst = results[1].append
541 advsrc = results[2].append
545 advsrc = results[2].append
542 advdst = results[3].append
546 advdst = results[3].append
543 diverge = results[4].append
547 diverge = results[4].append
544 differ = results[5].append
548 differ = results[5].append
545 invalid = results[6].append
549 invalid = results[6].append
546 same = results[7].append
550 same = results[7].append
547
551
548 for b in sorted(bset):
552 for b in sorted(bset):
549 if b not in srcmarks:
553 if b not in srcmarks:
550 if b in dstmarks:
554 if b in dstmarks:
551 adddst((b, None, dstmarks[b]))
555 adddst((b, None, dstmarks[b]))
552 else:
556 else:
553 invalid((b, None, None))
557 invalid((b, None, None))
554 elif b not in dstmarks:
558 elif b not in dstmarks:
555 addsrc((b, srcmarks[b], None))
559 addsrc((b, srcmarks[b], None))
556 else:
560 else:
557 scid = srcmarks[b]
561 scid = srcmarks[b]
558 dcid = dstmarks[b]
562 dcid = dstmarks[b]
559 if scid == dcid:
563 if scid == dcid:
560 same((b, scid, dcid))
564 same((b, scid, dcid))
561 elif scid in repo and dcid in repo:
565 elif scid in repo and dcid in repo:
562 sctx = repo[scid]
566 sctx = repo[scid]
563 dctx = repo[dcid]
567 dctx = repo[dcid]
564 if sctx.rev() < dctx.rev():
568 if sctx.rev() < dctx.rev():
565 if validdest(repo, sctx, dctx):
569 if validdest(repo, sctx, dctx):
566 advdst((b, scid, dcid))
570 advdst((b, scid, dcid))
567 else:
571 else:
568 diverge((b, scid, dcid))
572 diverge((b, scid, dcid))
569 else:
573 else:
570 if validdest(repo, dctx, sctx):
574 if validdest(repo, dctx, sctx):
571 advsrc((b, scid, dcid))
575 advsrc((b, scid, dcid))
572 else:
576 else:
573 diverge((b, scid, dcid))
577 diverge((b, scid, dcid))
574 else:
578 else:
575 # it is too expensive to examine in detail, in this case
579 # it is too expensive to examine in detail, in this case
576 differ((b, scid, dcid))
580 differ((b, scid, dcid))
577
581
578 return results
582 return results
579
583
580
584
581 def _diverge(ui, b, path, localmarks, remotenode):
585 def _diverge(ui, b, path, localmarks, remotenode):
582 '''Return appropriate diverged bookmark for specified ``path``
586 '''Return appropriate diverged bookmark for specified ``path``
583
587
584 This returns None, if it is failed to assign any divergent
588 This returns None, if it is failed to assign any divergent
585 bookmark name.
589 bookmark name.
586
590
587 This reuses already existing one with "@number" suffix, if it
591 This reuses already existing one with "@number" suffix, if it
588 refers ``remotenode``.
592 refers ``remotenode``.
589 '''
593 '''
590 if b == b'@':
594 if b == b'@':
591 b = b''
595 b = b''
592 # try to use an @pathalias suffix
596 # try to use an @pathalias suffix
593 # if an @pathalias already exists, we overwrite (update) it
597 # if an @pathalias already exists, we overwrite (update) it
594 if path.startswith(b"file:"):
598 if path.startswith(b"file:"):
595 path = util.url(path).path
599 path = util.url(path).path
596 for p, u in ui.configitems(b"paths"):
600 for p, u in ui.configitems(b"paths"):
597 if u.startswith(b"file:"):
601 if u.startswith(b"file:"):
598 u = util.url(u).path
602 u = util.url(u).path
599 if path == u:
603 if path == u:
600 return b'%s@%s' % (b, p)
604 return b'%s@%s' % (b, p)
601
605
602 # assign a unique "@number" suffix newly
606 # assign a unique "@number" suffix newly
603 for x in range(1, 100):
607 for x in range(1, 100):
604 n = b'%s@%d' % (b, x)
608 n = b'%s@%d' % (b, x)
605 if n not in localmarks or localmarks[n] == remotenode:
609 if n not in localmarks or localmarks[n] == remotenode:
606 return n
610 return n
607
611
608 return None
612 return None
609
613
610
614
611 def unhexlifybookmarks(marks):
615 def unhexlifybookmarks(marks):
612 binremotemarks = {}
616 binremotemarks = {}
613 for name, node in marks.items():
617 for name, node in marks.items():
614 binremotemarks[name] = bin(node)
618 binremotemarks[name] = bin(node)
615 return binremotemarks
619 return binremotemarks
616
620
617
621
618 _binaryentry = struct.Struct(b'>20sH')
622 _binaryentry = struct.Struct(b'>20sH')
619
623
620
624
621 def binaryencode(bookmarks):
625 def binaryencode(bookmarks):
622 """encode a '(bookmark, node)' iterable into a binary stream
626 """encode a '(bookmark, node)' iterable into a binary stream
623
627
624 the binary format is:
628 the binary format is:
625
629
626 <node><bookmark-length><bookmark-name>
630 <node><bookmark-length><bookmark-name>
627
631
628 :node: is a 20 bytes binary node,
632 :node: is a 20 bytes binary node,
629 :bookmark-length: an unsigned short,
633 :bookmark-length: an unsigned short,
630 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
634 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
631
635
632 wdirid (all bits set) will be used as a special value for "missing"
636 wdirid (all bits set) will be used as a special value for "missing"
633 """
637 """
634 binarydata = []
638 binarydata = []
635 for book, node in bookmarks:
639 for book, node in bookmarks:
636 if not node: # None or ''
640 if not node: # None or ''
637 node = wdirid
641 node = wdirid
638 binarydata.append(_binaryentry.pack(node, len(book)))
642 binarydata.append(_binaryentry.pack(node, len(book)))
639 binarydata.append(book)
643 binarydata.append(book)
640 return b''.join(binarydata)
644 return b''.join(binarydata)
641
645
642
646
643 def binarydecode(stream):
647 def binarydecode(stream):
644 """decode a binary stream into an '(bookmark, node)' iterable
648 """decode a binary stream into an '(bookmark, node)' iterable
645
649
646 the binary format is:
650 the binary format is:
647
651
648 <node><bookmark-length><bookmark-name>
652 <node><bookmark-length><bookmark-name>
649
653
650 :node: is a 20 bytes binary node,
654 :node: is a 20 bytes binary node,
651 :bookmark-length: an unsigned short,
655 :bookmark-length: an unsigned short,
652 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
656 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
653
657
654 wdirid (all bits set) will be used as a special value for "missing"
658 wdirid (all bits set) will be used as a special value for "missing"
655 """
659 """
656 entrysize = _binaryentry.size
660 entrysize = _binaryentry.size
657 books = []
661 books = []
658 while True:
662 while True:
659 entry = stream.read(entrysize)
663 entry = stream.read(entrysize)
660 if len(entry) < entrysize:
664 if len(entry) < entrysize:
661 if entry:
665 if entry:
662 raise error.Abort(_(b'bad bookmark stream'))
666 raise error.Abort(_(b'bad bookmark stream'))
663 break
667 break
664 node, length = _binaryentry.unpack(entry)
668 node, length = _binaryentry.unpack(entry)
665 bookmark = stream.read(length)
669 bookmark = stream.read(length)
666 if len(bookmark) < length:
670 if len(bookmark) < length:
667 if entry:
671 if entry:
668 raise error.Abort(_(b'bad bookmark stream'))
672 raise error.Abort(_(b'bad bookmark stream'))
669 if node == wdirid:
673 if node == wdirid:
670 node = None
674 node = None
671 books.append((bookmark, node))
675 books.append((bookmark, node))
672 return books
676 return books
673
677
674
678
675 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
679 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
676 ui.debug(b"checking for updated bookmarks\n")
680 ui.debug(b"checking for updated bookmarks\n")
677 localmarks = repo._bookmarks
681 localmarks = repo._bookmarks
678 (
682 (
679 addsrc,
683 addsrc,
680 adddst,
684 adddst,
681 advsrc,
685 advsrc,
682 advdst,
686 advdst,
683 diverge,
687 diverge,
684 differ,
688 differ,
685 invalid,
689 invalid,
686 same,
690 same,
687 ) = comparebookmarks(repo, remotemarks, localmarks)
691 ) = comparebookmarks(repo, remotemarks, localmarks)
688
692
689 status = ui.status
693 status = ui.status
690 warn = ui.warn
694 warn = ui.warn
691 if ui.configbool(b'ui', b'quietbookmarkmove'):
695 if ui.configbool(b'ui', b'quietbookmarkmove'):
692 status = warn = ui.debug
696 status = warn = ui.debug
693
697
694 explicit = set(explicit)
698 explicit = set(explicit)
695 changed = []
699 changed = []
696 for b, scid, dcid in addsrc:
700 for b, scid, dcid in addsrc:
697 if scid in repo: # add remote bookmarks for changes we already have
701 if scid in repo: # add remote bookmarks for changes we already have
698 changed.append(
702 changed.append(
699 (b, scid, status, _(b"adding remote bookmark %s\n") % b)
703 (b, scid, status, _(b"adding remote bookmark %s\n") % b)
700 )
704 )
701 elif b in explicit:
705 elif b in explicit:
702 explicit.remove(b)
706 explicit.remove(b)
703 ui.warn(
707 ui.warn(
704 _(b"remote bookmark %s points to locally missing %s\n")
708 _(b"remote bookmark %s points to locally missing %s\n")
705 % (b, hex(scid)[:12])
709 % (b, hex(scid)[:12])
706 )
710 )
707
711
708 for b, scid, dcid in advsrc:
712 for b, scid, dcid in advsrc:
709 changed.append((b, scid, status, _(b"updating bookmark %s\n") % b))
713 changed.append((b, scid, status, _(b"updating bookmark %s\n") % b))
710 # remove normal movement from explicit set
714 # remove normal movement from explicit set
711 explicit.difference_update(d[0] for d in changed)
715 explicit.difference_update(d[0] for d in changed)
712
716
713 for b, scid, dcid in diverge:
717 for b, scid, dcid in diverge:
714 if b in explicit:
718 if b in explicit:
715 explicit.discard(b)
719 explicit.discard(b)
716 changed.append((b, scid, status, _(b"importing bookmark %s\n") % b))
720 changed.append((b, scid, status, _(b"importing bookmark %s\n") % b))
717 else:
721 else:
718 db = _diverge(ui, b, path, localmarks, scid)
722 db = _diverge(ui, b, path, localmarks, scid)
719 if db:
723 if db:
720 changed.append(
724 changed.append(
721 (
725 (
722 db,
726 db,
723 scid,
727 scid,
724 warn,
728 warn,
725 _(b"divergent bookmark %s stored as %s\n") % (b, db),
729 _(b"divergent bookmark %s stored as %s\n") % (b, db),
726 )
730 )
727 )
731 )
728 else:
732 else:
729 warn(
733 warn(
730 _(
734 _(
731 b"warning: failed to assign numbered name "
735 b"warning: failed to assign numbered name "
732 b"to divergent bookmark %s\n"
736 b"to divergent bookmark %s\n"
733 )
737 )
734 % b
738 % b
735 )
739 )
736 for b, scid, dcid in adddst + advdst:
740 for b, scid, dcid in adddst + advdst:
737 if b in explicit:
741 if b in explicit:
738 explicit.discard(b)
742 explicit.discard(b)
739 changed.append((b, scid, status, _(b"importing bookmark %s\n") % b))
743 changed.append((b, scid, status, _(b"importing bookmark %s\n") % b))
740 for b, scid, dcid in differ:
744 for b, scid, dcid in differ:
741 if b in explicit:
745 if b in explicit:
742 explicit.remove(b)
746 explicit.remove(b)
743 ui.warn(
747 ui.warn(
744 _(b"remote bookmark %s points to locally missing %s\n")
748 _(b"remote bookmark %s points to locally missing %s\n")
745 % (b, hex(scid)[:12])
749 % (b, hex(scid)[:12])
746 )
750 )
747
751
748 if changed:
752 if changed:
749 tr = trfunc()
753 tr = trfunc()
750 changes = []
754 changes = []
751 for b, node, writer, msg in sorted(changed):
755 for b, node, writer, msg in sorted(changed):
752 changes.append((b, node))
756 changes.append((b, node))
753 writer(msg)
757 writer(msg)
754 localmarks.applychanges(repo, tr, changes)
758 localmarks.applychanges(repo, tr, changes)
755
759
756
760
757 def incoming(ui, repo, peer):
761 def incoming(ui, repo, peer):
758 '''Show bookmarks incoming from other to repo
762 '''Show bookmarks incoming from other to repo
759 '''
763 '''
760 ui.status(_(b"searching for changed bookmarks\n"))
764 ui.status(_(b"searching for changed bookmarks\n"))
761
765
762 with peer.commandexecutor() as e:
766 with peer.commandexecutor() as e:
763 remotemarks = unhexlifybookmarks(
767 remotemarks = unhexlifybookmarks(
764 e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result()
768 e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result()
765 )
769 )
766
770
767 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
771 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
768 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
772 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
769
773
770 incomings = []
774 incomings = []
771 if ui.debugflag:
775 if ui.debugflag:
772 getid = lambda id: id
776 getid = lambda id: id
773 else:
777 else:
774 getid = lambda id: id[:12]
778 getid = lambda id: id[:12]
775 if ui.verbose:
779 if ui.verbose:
776
780
777 def add(b, id, st):
781 def add(b, id, st):
778 incomings.append(b" %-25s %s %s\n" % (b, getid(id), st))
782 incomings.append(b" %-25s %s %s\n" % (b, getid(id), st))
779
783
780 else:
784 else:
781
785
782 def add(b, id, st):
786 def add(b, id, st):
783 incomings.append(b" %-25s %s\n" % (b, getid(id)))
787 incomings.append(b" %-25s %s\n" % (b, getid(id)))
784
788
785 for b, scid, dcid in addsrc:
789 for b, scid, dcid in addsrc:
786 # i18n: "added" refers to a bookmark
790 # i18n: "added" refers to a bookmark
787 add(b, hex(scid), _(b'added'))
791 add(b, hex(scid), _(b'added'))
788 for b, scid, dcid in advsrc:
792 for b, scid, dcid in advsrc:
789 # i18n: "advanced" refers to a bookmark
793 # i18n: "advanced" refers to a bookmark
790 add(b, hex(scid), _(b'advanced'))
794 add(b, hex(scid), _(b'advanced'))
791 for b, scid, dcid in diverge:
795 for b, scid, dcid in diverge:
792 # i18n: "diverged" refers to a bookmark
796 # i18n: "diverged" refers to a bookmark
793 add(b, hex(scid), _(b'diverged'))
797 add(b, hex(scid), _(b'diverged'))
794 for b, scid, dcid in differ:
798 for b, scid, dcid in differ:
795 # i18n: "changed" refers to a bookmark
799 # i18n: "changed" refers to a bookmark
796 add(b, hex(scid), _(b'changed'))
800 add(b, hex(scid), _(b'changed'))
797
801
798 if not incomings:
802 if not incomings:
799 ui.status(_(b"no changed bookmarks found\n"))
803 ui.status(_(b"no changed bookmarks found\n"))
800 return 1
804 return 1
801
805
802 for s in sorted(incomings):
806 for s in sorted(incomings):
803 ui.write(s)
807 ui.write(s)
804
808
805 return 0
809 return 0
806
810
807
811
808 def outgoing(ui, repo, other):
812 def outgoing(ui, repo, other):
809 '''Show bookmarks outgoing from repo to other
813 '''Show bookmarks outgoing from repo to other
810 '''
814 '''
811 ui.status(_(b"searching for changed bookmarks\n"))
815 ui.status(_(b"searching for changed bookmarks\n"))
812
816
813 remotemarks = unhexlifybookmarks(other.listkeys(b'bookmarks'))
817 remotemarks = unhexlifybookmarks(other.listkeys(b'bookmarks'))
814 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
818 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
815 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
819 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
816
820
817 outgoings = []
821 outgoings = []
818 if ui.debugflag:
822 if ui.debugflag:
819 getid = lambda id: id
823 getid = lambda id: id
820 else:
824 else:
821 getid = lambda id: id[:12]
825 getid = lambda id: id[:12]
822 if ui.verbose:
826 if ui.verbose:
823
827
824 def add(b, id, st):
828 def add(b, id, st):
825 outgoings.append(b" %-25s %s %s\n" % (b, getid(id), st))
829 outgoings.append(b" %-25s %s %s\n" % (b, getid(id), st))
826
830
827 else:
831 else:
828
832
829 def add(b, id, st):
833 def add(b, id, st):
830 outgoings.append(b" %-25s %s\n" % (b, getid(id)))
834 outgoings.append(b" %-25s %s\n" % (b, getid(id)))
831
835
832 for b, scid, dcid in addsrc:
836 for b, scid, dcid in addsrc:
833 # i18n: "added refers to a bookmark
837 # i18n: "added refers to a bookmark
834 add(b, hex(scid), _(b'added'))
838 add(b, hex(scid), _(b'added'))
835 for b, scid, dcid in adddst:
839 for b, scid, dcid in adddst:
836 # i18n: "deleted" refers to a bookmark
840 # i18n: "deleted" refers to a bookmark
837 add(b, b' ' * 40, _(b'deleted'))
841 add(b, b' ' * 40, _(b'deleted'))
838 for b, scid, dcid in advsrc:
842 for b, scid, dcid in advsrc:
839 # i18n: "advanced" refers to a bookmark
843 # i18n: "advanced" refers to a bookmark
840 add(b, hex(scid), _(b'advanced'))
844 add(b, hex(scid), _(b'advanced'))
841 for b, scid, dcid in diverge:
845 for b, scid, dcid in diverge:
842 # i18n: "diverged" refers to a bookmark
846 # i18n: "diverged" refers to a bookmark
843 add(b, hex(scid), _(b'diverged'))
847 add(b, hex(scid), _(b'diverged'))
844 for b, scid, dcid in differ:
848 for b, scid, dcid in differ:
845 # i18n: "changed" refers to a bookmark
849 # i18n: "changed" refers to a bookmark
846 add(b, hex(scid), _(b'changed'))
850 add(b, hex(scid), _(b'changed'))
847
851
848 if not outgoings:
852 if not outgoings:
849 ui.status(_(b"no changed bookmarks found\n"))
853 ui.status(_(b"no changed bookmarks found\n"))
850 return 1
854 return 1
851
855
852 for s in sorted(outgoings):
856 for s in sorted(outgoings):
853 ui.write(s)
857 ui.write(s)
854
858
855 return 0
859 return 0
856
860
857
861
858 def summary(repo, peer):
862 def summary(repo, peer):
859 '''Compare bookmarks between repo and other for "hg summary" output
863 '''Compare bookmarks between repo and other for "hg summary" output
860
864
861 This returns "(# of incoming, # of outgoing)" tuple.
865 This returns "(# of incoming, # of outgoing)" tuple.
862 '''
866 '''
863 with peer.commandexecutor() as e:
867 with peer.commandexecutor() as e:
864 remotemarks = unhexlifybookmarks(
868 remotemarks = unhexlifybookmarks(
865 e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result()
869 e.callcommand(b'listkeys', {b'namespace': b'bookmarks',}).result()
866 )
870 )
867
871
868 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
872 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
869 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
873 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
870 return (len(addsrc), len(adddst))
874 return (len(addsrc), len(adddst))
871
875
872
876
873 def validdest(repo, old, new):
877 def validdest(repo, old, new):
874 """Is the new bookmark destination a valid update from the old one"""
878 """Is the new bookmark destination a valid update from the old one"""
875 repo = repo.unfiltered()
879 repo = repo.unfiltered()
876 if old == new:
880 if old == new:
877 # Old == new -> nothing to update.
881 # Old == new -> nothing to update.
878 return False
882 return False
879 elif not old:
883 elif not old:
880 # old is nullrev, anything is valid.
884 # old is nullrev, anything is valid.
881 # (new != nullrev has been excluded by the previous check)
885 # (new != nullrev has been excluded by the previous check)
882 return True
886 return True
883 elif repo.obsstore:
887 elif repo.obsstore:
884 return new.node() in obsutil.foreground(repo, [old.node()])
888 return new.node() in obsutil.foreground(repo, [old.node()])
885 else:
889 else:
886 # still an independent clause as it is lazier (and therefore faster)
890 # still an independent clause as it is lazier (and therefore faster)
887 return old.isancestorof(new)
891 return old.isancestorof(new)
888
892
889
893
890 def checkformat(repo, mark):
894 def checkformat(repo, mark):
891 """return a valid version of a potential bookmark name
895 """return a valid version of a potential bookmark name
892
896
893 Raises an abort error if the bookmark name is not valid.
897 Raises an abort error if the bookmark name is not valid.
894 """
898 """
895 mark = mark.strip()
899 mark = mark.strip()
896 if not mark:
900 if not mark:
897 raise error.Abort(
901 raise error.Abort(
898 _(b"bookmark names cannot consist entirely of whitespace")
902 _(b"bookmark names cannot consist entirely of whitespace")
899 )
903 )
900 scmutil.checknewlabel(repo, mark, b'bookmark')
904 scmutil.checknewlabel(repo, mark, b'bookmark')
901 return mark
905 return mark
902
906
903
907
904 def delete(repo, tr, names):
908 def delete(repo, tr, names):
905 """remove a mark from the bookmark store
909 """remove a mark from the bookmark store
906
910
907 Raises an abort error if mark does not exist.
911 Raises an abort error if mark does not exist.
908 """
912 """
909 marks = repo._bookmarks
913 marks = repo._bookmarks
910 changes = []
914 changes = []
911 for mark in names:
915 for mark in names:
912 if mark not in marks:
916 if mark not in marks:
913 raise error.Abort(_(b"bookmark '%s' does not exist") % mark)
917 raise error.Abort(_(b"bookmark '%s' does not exist") % mark)
914 if mark == repo._activebookmark:
918 if mark == repo._activebookmark:
915 deactivate(repo)
919 deactivate(repo)
916 changes.append((mark, None))
920 changes.append((mark, None))
917 marks.applychanges(repo, tr, changes)
921 marks.applychanges(repo, tr, changes)
918
922
919
923
920 def rename(repo, tr, old, new, force=False, inactive=False):
924 def rename(repo, tr, old, new, force=False, inactive=False):
921 """rename a bookmark from old to new
925 """rename a bookmark from old to new
922
926
923 If force is specified, then the new name can overwrite an existing
927 If force is specified, then the new name can overwrite an existing
924 bookmark.
928 bookmark.
925
929
926 If inactive is specified, then do not activate the new bookmark.
930 If inactive is specified, then do not activate the new bookmark.
927
931
928 Raises an abort error if old is not in the bookmark store.
932 Raises an abort error if old is not in the bookmark store.
929 """
933 """
930 marks = repo._bookmarks
934 marks = repo._bookmarks
931 mark = checkformat(repo, new)
935 mark = checkformat(repo, new)
932 if old not in marks:
936 if old not in marks:
933 raise error.Abort(_(b"bookmark '%s' does not exist") % old)
937 raise error.Abort(_(b"bookmark '%s' does not exist") % old)
934 changes = []
938 changes = []
935 for bm in marks.checkconflict(mark, force):
939 for bm in marks.checkconflict(mark, force):
936 changes.append((bm, None))
940 changes.append((bm, None))
937 changes.extend([(mark, marks[old]), (old, None)])
941 changes.extend([(mark, marks[old]), (old, None)])
938 marks.applychanges(repo, tr, changes)
942 marks.applychanges(repo, tr, changes)
939 if repo._activebookmark == old and not inactive:
943 if repo._activebookmark == old and not inactive:
940 activate(repo, mark)
944 activate(repo, mark)
941
945
942
946
943 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
947 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
944 """add a list of bookmarks
948 """add a list of bookmarks
945
949
946 If force is specified, then the new name can overwrite an existing
950 If force is specified, then the new name can overwrite an existing
947 bookmark.
951 bookmark.
948
952
949 If inactive is specified, then do not activate any bookmark. Otherwise, the
953 If inactive is specified, then do not activate any bookmark. Otherwise, the
950 first bookmark is activated.
954 first bookmark is activated.
951
955
952 Raises an abort error if old is not in the bookmark store.
956 Raises an abort error if old is not in the bookmark store.
953 """
957 """
954 marks = repo._bookmarks
958 marks = repo._bookmarks
955 cur = repo[b'.'].node()
959 cur = repo[b'.'].node()
956 newact = None
960 newact = None
957 changes = []
961 changes = []
958
962
959 # unhide revs if any
963 # unhide revs if any
960 if rev:
964 if rev:
961 repo = scmutil.unhidehashlikerevs(repo, [rev], b'nowarn')
965 repo = scmutil.unhidehashlikerevs(repo, [rev], b'nowarn')
962
966
963 ctx = scmutil.revsingle(repo, rev, None)
967 ctx = scmutil.revsingle(repo, rev, None)
964 # bookmarking wdir means creating a bookmark on p1 and activating it
968 # bookmarking wdir means creating a bookmark on p1 and activating it
965 activatenew = not inactive and ctx.rev() is None
969 activatenew = not inactive and ctx.rev() is None
966 if ctx.node() is None:
970 if ctx.node() is None:
967 ctx = ctx.p1()
971 ctx = ctx.p1()
968 tgt = ctx.node()
972 tgt = ctx.node()
969 assert tgt
973 assert tgt
970
974
971 for mark in names:
975 for mark in names:
972 mark = checkformat(repo, mark)
976 mark = checkformat(repo, mark)
973 if newact is None:
977 if newact is None:
974 newact = mark
978 newact = mark
975 if inactive and mark == repo._activebookmark:
979 if inactive and mark == repo._activebookmark:
976 deactivate(repo)
980 deactivate(repo)
977 continue
981 continue
978 for bm in marks.checkconflict(mark, force, tgt):
982 for bm in marks.checkconflict(mark, force, tgt):
979 changes.append((bm, None))
983 changes.append((bm, None))
980 changes.append((mark, tgt))
984 changes.append((mark, tgt))
981
985
982 # nothing changed but for the one deactivated above
986 # nothing changed but for the one deactivated above
983 if not changes:
987 if not changes:
984 return
988 return
985
989
986 if ctx.hidden():
990 if ctx.hidden():
987 repo.ui.warn(_(b"bookmarking hidden changeset %s\n") % ctx.hex()[:12])
991 repo.ui.warn(_(b"bookmarking hidden changeset %s\n") % ctx.hex()[:12])
988
992
989 if ctx.obsolete():
993 if ctx.obsolete():
990 msg = obsutil._getfilteredreason(repo, ctx.hex()[:12], ctx)
994 msg = obsutil._getfilteredreason(repo, ctx.hex()[:12], ctx)
991 repo.ui.warn(b"(%s)\n" % msg)
995 repo.ui.warn(b"(%s)\n" % msg)
992
996
993 marks.applychanges(repo, tr, changes)
997 marks.applychanges(repo, tr, changes)
994 if activatenew and cur == marks[newact]:
998 if activatenew and cur == marks[newact]:
995 activate(repo, newact)
999 activate(repo, newact)
996 elif cur != tgt and newact == repo._activebookmark:
1000 elif cur != tgt and newact == repo._activebookmark:
997 deactivate(repo)
1001 deactivate(repo)
998
1002
999
1003
1000 def _printbookmarks(ui, repo, fm, bmarks):
1004 def _printbookmarks(ui, repo, fm, bmarks):
1001 """private method to print bookmarks
1005 """private method to print bookmarks
1002
1006
1003 Provides a way for extensions to control how bookmarks are printed (e.g.
1007 Provides a way for extensions to control how bookmarks are printed (e.g.
1004 prepend or postpend names)
1008 prepend or postpend names)
1005 """
1009 """
1006 hexfn = fm.hexfunc
1010 hexfn = fm.hexfunc
1007 if len(bmarks) == 0 and fm.isplain():
1011 if len(bmarks) == 0 and fm.isplain():
1008 ui.status(_(b"no bookmarks set\n"))
1012 ui.status(_(b"no bookmarks set\n"))
1009 for bmark, (n, prefix, label) in sorted(pycompat.iteritems(bmarks)):
1013 for bmark, (n, prefix, label) in sorted(pycompat.iteritems(bmarks)):
1010 fm.startitem()
1014 fm.startitem()
1011 fm.context(repo=repo)
1015 fm.context(repo=repo)
1012 if not ui.quiet:
1016 if not ui.quiet:
1013 fm.plain(b' %s ' % prefix, label=label)
1017 fm.plain(b' %s ' % prefix, label=label)
1014 fm.write(b'bookmark', b'%s', bmark, label=label)
1018 fm.write(b'bookmark', b'%s', bmark, label=label)
1015 pad = b" " * (25 - encoding.colwidth(bmark))
1019 pad = b" " * (25 - encoding.colwidth(bmark))
1016 fm.condwrite(
1020 fm.condwrite(
1017 not ui.quiet,
1021 not ui.quiet,
1018 b'rev node',
1022 b'rev node',
1019 pad + b' %d:%s',
1023 pad + b' %d:%s',
1020 repo.changelog.rev(n),
1024 repo.changelog.rev(n),
1021 hexfn(n),
1025 hexfn(n),
1022 label=label,
1026 label=label,
1023 )
1027 )
1024 fm.data(active=(activebookmarklabel in label))
1028 fm.data(active=(activebookmarklabel in label))
1025 fm.plain(b'\n')
1029 fm.plain(b'\n')
1026
1030
1027
1031
1028 def printbookmarks(ui, repo, fm, names=None):
1032 def printbookmarks(ui, repo, fm, names=None):
1029 """print bookmarks by the given formatter
1033 """print bookmarks by the given formatter
1030
1034
1031 Provides a way for extensions to control how bookmarks are printed.
1035 Provides a way for extensions to control how bookmarks are printed.
1032 """
1036 """
1033 marks = repo._bookmarks
1037 marks = repo._bookmarks
1034 bmarks = {}
1038 bmarks = {}
1035 for bmark in names or marks:
1039 for bmark in names or marks:
1036 if bmark not in marks:
1040 if bmark not in marks:
1037 raise error.Abort(_(b"bookmark '%s' does not exist") % bmark)
1041 raise error.Abort(_(b"bookmark '%s' does not exist") % bmark)
1038 active = repo._activebookmark
1042 active = repo._activebookmark
1039 if bmark == active:
1043 if bmark == active:
1040 prefix, label = b'*', activebookmarklabel
1044 prefix, label = b'*', activebookmarklabel
1041 else:
1045 else:
1042 prefix, label = b' ', b''
1046 prefix, label = b' ', b''
1043
1047
1044 bmarks[bmark] = (marks[bmark], prefix, label)
1048 bmarks[bmark] = (marks[bmark], prefix, label)
1045 _printbookmarks(ui, repo, fm, bmarks)
1049 _printbookmarks(ui, repo, fm, bmarks)
1046
1050
1047
1051
1048 def preparehookargs(name, old, new):
1052 def preparehookargs(name, old, new):
1049 if new is None:
1053 if new is None:
1050 new = b''
1054 new = b''
1051 if old is None:
1055 if old is None:
1052 old = b''
1056 old = b''
1053 return {b'bookmark': name, b'node': hex(new), b'oldnode': hex(old)}
1057 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