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