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