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