##// END OF EJS Templates
bookmarks: calculateupdate() returns a bookmark, not a rev...
Martin von Zweigbergk -
r37377:e27298bf default
parent child Browse files
Show More
@@ -1,907 +1,907 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 = (mark in self._repo)
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, checkout):
351 def calculateupdate(ui, repo, checkout):
352 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
352 '''Return a tuple (activemark, movemarkfrom) indicating the active bookmark
353 check out and where to move the active bookmark from, if needed.'''
353 and where to move the active bookmark from, if needed.'''
354 movemarkfrom = None
354 movemarkfrom = None
355 if checkout is None:
355 if checkout is None:
356 activemark = repo._activebookmark
356 activemark = repo._activebookmark
357 if isactivewdirparent(repo):
357 if isactivewdirparent(repo):
358 movemarkfrom = repo['.'].node()
358 movemarkfrom = repo['.'].node()
359 elif activemark:
359 elif activemark:
360 ui.status(_("updating to active bookmark %s\n") % activemark)
360 ui.status(_("updating to active bookmark %s\n") % activemark)
361 checkout = activemark
361 checkout = activemark
362 return (checkout, movemarkfrom)
362 return (checkout, movemarkfrom)
363
363
364 def update(repo, parents, node):
364 def update(repo, parents, node):
365 deletefrom = parents
365 deletefrom = parents
366 marks = repo._bookmarks
366 marks = repo._bookmarks
367 active = marks.active
367 active = marks.active
368 if not active:
368 if not active:
369 return False
369 return False
370
370
371 bmchanges = []
371 bmchanges = []
372 if marks[active] in parents:
372 if marks[active] in parents:
373 new = repo[node]
373 new = repo[node]
374 divs = [repo[b] for b in marks
374 divs = [repo[b] for b in marks
375 if b.split('@', 1)[0] == active.split('@', 1)[0]]
375 if b.split('@', 1)[0] == active.split('@', 1)[0]]
376 anc = repo.changelog.ancestors([new.rev()])
376 anc = repo.changelog.ancestors([new.rev()])
377 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
377 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
378 if validdest(repo, repo[marks[active]], new):
378 if validdest(repo, repo[marks[active]], new):
379 bmchanges.append((active, new.node()))
379 bmchanges.append((active, new.node()))
380
380
381 for bm in divergent2delete(repo, deletefrom, active):
381 for bm in divergent2delete(repo, deletefrom, active):
382 bmchanges.append((bm, None))
382 bmchanges.append((bm, None))
383
383
384 if bmchanges:
384 if bmchanges:
385 with repo.lock(), repo.transaction('bookmark') as tr:
385 with repo.lock(), repo.transaction('bookmark') as tr:
386 marks.applychanges(repo, tr, bmchanges)
386 marks.applychanges(repo, tr, bmchanges)
387 return bool(bmchanges)
387 return bool(bmchanges)
388
388
389 def listbinbookmarks(repo):
389 def listbinbookmarks(repo):
390 # We may try to list bookmarks on a repo type that does not
390 # We may try to list bookmarks on a repo type that does not
391 # support it (e.g., statichttprepository).
391 # support it (e.g., statichttprepository).
392 marks = getattr(repo, '_bookmarks', {})
392 marks = getattr(repo, '_bookmarks', {})
393
393
394 hasnode = repo.changelog.hasnode
394 hasnode = repo.changelog.hasnode
395 for k, v in marks.iteritems():
395 for k, v in marks.iteritems():
396 # don't expose local divergent bookmarks
396 # don't expose local divergent bookmarks
397 if hasnode(v) and ('@' not in k or k.endswith('@')):
397 if hasnode(v) and ('@' not in k or k.endswith('@')):
398 yield k, v
398 yield k, v
399
399
400 def listbookmarks(repo):
400 def listbookmarks(repo):
401 d = {}
401 d = {}
402 for book, node in listbinbookmarks(repo):
402 for book, node in listbinbookmarks(repo):
403 d[book] = hex(node)
403 d[book] = hex(node)
404 return d
404 return d
405
405
406 def pushbookmark(repo, key, old, new):
406 def pushbookmark(repo, key, old, new):
407 with repo.wlock(), repo.lock(), repo.transaction('bookmarks') as tr:
407 with repo.wlock(), repo.lock(), repo.transaction('bookmarks') as tr:
408 marks = repo._bookmarks
408 marks = repo._bookmarks
409 existing = hex(marks.get(key, ''))
409 existing = hex(marks.get(key, ''))
410 if existing != old and existing != new:
410 if existing != old and existing != new:
411 return False
411 return False
412 if new == '':
412 if new == '':
413 changes = [(key, None)]
413 changes = [(key, None)]
414 else:
414 else:
415 if new not in repo:
415 if new not in repo:
416 return False
416 return False
417 changes = [(key, repo[new].node())]
417 changes = [(key, repo[new].node())]
418 marks.applychanges(repo, tr, changes)
418 marks.applychanges(repo, tr, changes)
419 return True
419 return True
420
420
421 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
421 def comparebookmarks(repo, srcmarks, dstmarks, targets=None):
422 '''Compare bookmarks between srcmarks and dstmarks
422 '''Compare bookmarks between srcmarks and dstmarks
423
423
424 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
424 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
425 differ, invalid)", each are list of bookmarks below:
425 differ, invalid)", each are list of bookmarks below:
426
426
427 :addsrc: added on src side (removed on dst side, perhaps)
427 :addsrc: added on src side (removed on dst side, perhaps)
428 :adddst: added on dst side (removed on src side, perhaps)
428 :adddst: added on dst side (removed on src side, perhaps)
429 :advsrc: advanced on src side
429 :advsrc: advanced on src side
430 :advdst: advanced on dst side
430 :advdst: advanced on dst side
431 :diverge: diverge
431 :diverge: diverge
432 :differ: changed, but changeset referred on src is unknown on dst
432 :differ: changed, but changeset referred on src is unknown on dst
433 :invalid: unknown on both side
433 :invalid: unknown on both side
434 :same: same on both side
434 :same: same on both side
435
435
436 Each elements of lists in result tuple is tuple "(bookmark name,
436 Each elements of lists in result tuple is tuple "(bookmark name,
437 changeset ID on source side, changeset ID on destination
437 changeset ID on source side, changeset ID on destination
438 side)". Each changeset IDs are 40 hexadecimal digit string or
438 side)". Each changeset IDs are 40 hexadecimal digit string or
439 None.
439 None.
440
440
441 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
441 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
442 "invalid" list may be unknown for repo.
442 "invalid" list may be unknown for repo.
443
443
444 If "targets" is specified, only bookmarks listed in it are
444 If "targets" is specified, only bookmarks listed in it are
445 examined.
445 examined.
446 '''
446 '''
447
447
448 if targets:
448 if targets:
449 bset = set(targets)
449 bset = set(targets)
450 else:
450 else:
451 srcmarkset = set(srcmarks)
451 srcmarkset = set(srcmarks)
452 dstmarkset = set(dstmarks)
452 dstmarkset = set(dstmarks)
453 bset = srcmarkset | dstmarkset
453 bset = srcmarkset | dstmarkset
454
454
455 results = ([], [], [], [], [], [], [], [])
455 results = ([], [], [], [], [], [], [], [])
456 addsrc = results[0].append
456 addsrc = results[0].append
457 adddst = results[1].append
457 adddst = results[1].append
458 advsrc = results[2].append
458 advsrc = results[2].append
459 advdst = results[3].append
459 advdst = results[3].append
460 diverge = results[4].append
460 diverge = results[4].append
461 differ = results[5].append
461 differ = results[5].append
462 invalid = results[6].append
462 invalid = results[6].append
463 same = results[7].append
463 same = results[7].append
464
464
465 for b in sorted(bset):
465 for b in sorted(bset):
466 if b not in srcmarks:
466 if b not in srcmarks:
467 if b in dstmarks:
467 if b in dstmarks:
468 adddst((b, None, dstmarks[b]))
468 adddst((b, None, dstmarks[b]))
469 else:
469 else:
470 invalid((b, None, None))
470 invalid((b, None, None))
471 elif b not in dstmarks:
471 elif b not in dstmarks:
472 addsrc((b, srcmarks[b], None))
472 addsrc((b, srcmarks[b], None))
473 else:
473 else:
474 scid = srcmarks[b]
474 scid = srcmarks[b]
475 dcid = dstmarks[b]
475 dcid = dstmarks[b]
476 if scid == dcid:
476 if scid == dcid:
477 same((b, scid, dcid))
477 same((b, scid, dcid))
478 elif scid in repo and dcid in repo:
478 elif scid in repo and dcid in repo:
479 sctx = repo[scid]
479 sctx = repo[scid]
480 dctx = repo[dcid]
480 dctx = repo[dcid]
481 if sctx.rev() < dctx.rev():
481 if sctx.rev() < dctx.rev():
482 if validdest(repo, sctx, dctx):
482 if validdest(repo, sctx, dctx):
483 advdst((b, scid, dcid))
483 advdst((b, scid, dcid))
484 else:
484 else:
485 diverge((b, scid, dcid))
485 diverge((b, scid, dcid))
486 else:
486 else:
487 if validdest(repo, dctx, sctx):
487 if validdest(repo, dctx, sctx):
488 advsrc((b, scid, dcid))
488 advsrc((b, scid, dcid))
489 else:
489 else:
490 diverge((b, scid, dcid))
490 diverge((b, scid, dcid))
491 else:
491 else:
492 # it is too expensive to examine in detail, in this case
492 # it is too expensive to examine in detail, in this case
493 differ((b, scid, dcid))
493 differ((b, scid, dcid))
494
494
495 return results
495 return results
496
496
497 def _diverge(ui, b, path, localmarks, remotenode):
497 def _diverge(ui, b, path, localmarks, remotenode):
498 '''Return appropriate diverged bookmark for specified ``path``
498 '''Return appropriate diverged bookmark for specified ``path``
499
499
500 This returns None, if it is failed to assign any divergent
500 This returns None, if it is failed to assign any divergent
501 bookmark name.
501 bookmark name.
502
502
503 This reuses already existing one with "@number" suffix, if it
503 This reuses already existing one with "@number" suffix, if it
504 refers ``remotenode``.
504 refers ``remotenode``.
505 '''
505 '''
506 if b == '@':
506 if b == '@':
507 b = ''
507 b = ''
508 # try to use an @pathalias suffix
508 # try to use an @pathalias suffix
509 # if an @pathalias already exists, we overwrite (update) it
509 # if an @pathalias already exists, we overwrite (update) it
510 if path.startswith("file:"):
510 if path.startswith("file:"):
511 path = util.url(path).path
511 path = util.url(path).path
512 for p, u in ui.configitems("paths"):
512 for p, u in ui.configitems("paths"):
513 if u.startswith("file:"):
513 if u.startswith("file:"):
514 u = util.url(u).path
514 u = util.url(u).path
515 if path == u:
515 if path == u:
516 return '%s@%s' % (b, p)
516 return '%s@%s' % (b, p)
517
517
518 # assign a unique "@number" suffix newly
518 # assign a unique "@number" suffix newly
519 for x in range(1, 100):
519 for x in range(1, 100):
520 n = '%s@%d' % (b, x)
520 n = '%s@%d' % (b, x)
521 if n not in localmarks or localmarks[n] == remotenode:
521 if n not in localmarks or localmarks[n] == remotenode:
522 return n
522 return n
523
523
524 return None
524 return None
525
525
526 def unhexlifybookmarks(marks):
526 def unhexlifybookmarks(marks):
527 binremotemarks = {}
527 binremotemarks = {}
528 for name, node in marks.items():
528 for name, node in marks.items():
529 binremotemarks[name] = bin(node)
529 binremotemarks[name] = bin(node)
530 return binremotemarks
530 return binremotemarks
531
531
532 _binaryentry = struct.Struct('>20sH')
532 _binaryentry = struct.Struct('>20sH')
533
533
534 def binaryencode(bookmarks):
534 def binaryencode(bookmarks):
535 """encode a '(bookmark, node)' iterable into a binary stream
535 """encode a '(bookmark, node)' iterable into a binary stream
536
536
537 the binary format is:
537 the binary format is:
538
538
539 <node><bookmark-length><bookmark-name>
539 <node><bookmark-length><bookmark-name>
540
540
541 :node: is a 20 bytes binary node,
541 :node: is a 20 bytes binary node,
542 :bookmark-length: an unsigned short,
542 :bookmark-length: an unsigned short,
543 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
543 :bookmark-name: the name of the bookmark (of length <bookmark-length>)
544
544
545 wdirid (all bits set) will be used as a special value for "missing"
545 wdirid (all bits set) will be used as a special value for "missing"
546 """
546 """
547 binarydata = []
547 binarydata = []
548 for book, node in bookmarks:
548 for book, node in bookmarks:
549 if not node: # None or ''
549 if not node: # None or ''
550 node = wdirid
550 node = wdirid
551 binarydata.append(_binaryentry.pack(node, len(book)))
551 binarydata.append(_binaryentry.pack(node, len(book)))
552 binarydata.append(book)
552 binarydata.append(book)
553 return ''.join(binarydata)
553 return ''.join(binarydata)
554
554
555 def binarydecode(stream):
555 def binarydecode(stream):
556 """decode a binary stream into an '(bookmark, node)' iterable
556 """decode a binary stream into an '(bookmark, node)' iterable
557
557
558 the binary format is:
558 the binary format is:
559
559
560 <node><bookmark-length><bookmark-name>
560 <node><bookmark-length><bookmark-name>
561
561
562 :node: is a 20 bytes binary node,
562 :node: is a 20 bytes binary node,
563 :bookmark-length: an unsigned short,
563 :bookmark-length: an unsigned short,
564 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
564 :bookmark-name: the name of the bookmark (of length <bookmark-length>))
565
565
566 wdirid (all bits set) will be used as a special value for "missing"
566 wdirid (all bits set) will be used as a special value for "missing"
567 """
567 """
568 entrysize = _binaryentry.size
568 entrysize = _binaryentry.size
569 books = []
569 books = []
570 while True:
570 while True:
571 entry = stream.read(entrysize)
571 entry = stream.read(entrysize)
572 if len(entry) < entrysize:
572 if len(entry) < entrysize:
573 if entry:
573 if entry:
574 raise error.Abort(_('bad bookmark stream'))
574 raise error.Abort(_('bad bookmark stream'))
575 break
575 break
576 node, length = _binaryentry.unpack(entry)
576 node, length = _binaryentry.unpack(entry)
577 bookmark = stream.read(length)
577 bookmark = stream.read(length)
578 if len(bookmark) < length:
578 if len(bookmark) < length:
579 if entry:
579 if entry:
580 raise error.Abort(_('bad bookmark stream'))
580 raise error.Abort(_('bad bookmark stream'))
581 if node == wdirid:
581 if node == wdirid:
582 node = None
582 node = None
583 books.append((bookmark, node))
583 books.append((bookmark, node))
584 return books
584 return books
585
585
586 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
586 def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()):
587 ui.debug("checking for updated bookmarks\n")
587 ui.debug("checking for updated bookmarks\n")
588 localmarks = repo._bookmarks
588 localmarks = repo._bookmarks
589 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
589 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same
590 ) = comparebookmarks(repo, remotemarks, localmarks)
590 ) = comparebookmarks(repo, remotemarks, localmarks)
591
591
592 status = ui.status
592 status = ui.status
593 warn = ui.warn
593 warn = ui.warn
594 if ui.configbool('ui', 'quietbookmarkmove'):
594 if ui.configbool('ui', 'quietbookmarkmove'):
595 status = warn = ui.debug
595 status = warn = ui.debug
596
596
597 explicit = set(explicit)
597 explicit = set(explicit)
598 changed = []
598 changed = []
599 for b, scid, dcid in addsrc:
599 for b, scid, dcid in addsrc:
600 if scid in repo: # add remote bookmarks for changes we already have
600 if scid in repo: # add remote bookmarks for changes we already have
601 changed.append((b, scid, status,
601 changed.append((b, scid, status,
602 _("adding remote bookmark %s\n") % (b)))
602 _("adding remote bookmark %s\n") % (b)))
603 elif b in explicit:
603 elif b in explicit:
604 explicit.remove(b)
604 explicit.remove(b)
605 ui.warn(_("remote bookmark %s points to locally missing %s\n")
605 ui.warn(_("remote bookmark %s points to locally missing %s\n")
606 % (b, hex(scid)[:12]))
606 % (b, hex(scid)[:12]))
607
607
608 for b, scid, dcid in advsrc:
608 for b, scid, dcid in advsrc:
609 changed.append((b, scid, status,
609 changed.append((b, scid, status,
610 _("updating bookmark %s\n") % (b)))
610 _("updating bookmark %s\n") % (b)))
611 # remove normal movement from explicit set
611 # remove normal movement from explicit set
612 explicit.difference_update(d[0] for d in changed)
612 explicit.difference_update(d[0] for d in changed)
613
613
614 for b, scid, dcid in diverge:
614 for b, scid, dcid in diverge:
615 if b in explicit:
615 if b in explicit:
616 explicit.discard(b)
616 explicit.discard(b)
617 changed.append((b, scid, status,
617 changed.append((b, scid, status,
618 _("importing bookmark %s\n") % (b)))
618 _("importing bookmark %s\n") % (b)))
619 else:
619 else:
620 db = _diverge(ui, b, path, localmarks, scid)
620 db = _diverge(ui, b, path, localmarks, scid)
621 if db:
621 if db:
622 changed.append((db, scid, warn,
622 changed.append((db, scid, warn,
623 _("divergent bookmark %s stored as %s\n") %
623 _("divergent bookmark %s stored as %s\n") %
624 (b, db)))
624 (b, db)))
625 else:
625 else:
626 warn(_("warning: failed to assign numbered name "
626 warn(_("warning: failed to assign numbered name "
627 "to divergent bookmark %s\n") % (b))
627 "to divergent bookmark %s\n") % (b))
628 for b, scid, dcid in adddst + advdst:
628 for b, scid, dcid in adddst + advdst:
629 if b in explicit:
629 if b in explicit:
630 explicit.discard(b)
630 explicit.discard(b)
631 changed.append((b, scid, status,
631 changed.append((b, scid, status,
632 _("importing bookmark %s\n") % (b)))
632 _("importing bookmark %s\n") % (b)))
633 for b, scid, dcid in differ:
633 for b, scid, dcid in differ:
634 if b in explicit:
634 if b in explicit:
635 explicit.remove(b)
635 explicit.remove(b)
636 ui.warn(_("remote bookmark %s points to locally missing %s\n")
636 ui.warn(_("remote bookmark %s points to locally missing %s\n")
637 % (b, hex(scid)[:12]))
637 % (b, hex(scid)[:12]))
638
638
639 if changed:
639 if changed:
640 tr = trfunc()
640 tr = trfunc()
641 changes = []
641 changes = []
642 for b, node, writer, msg in sorted(changed):
642 for b, node, writer, msg in sorted(changed):
643 changes.append((b, node))
643 changes.append((b, node))
644 writer(msg)
644 writer(msg)
645 localmarks.applychanges(repo, tr, changes)
645 localmarks.applychanges(repo, tr, changes)
646
646
647 def incoming(ui, repo, other):
647 def incoming(ui, repo, other):
648 '''Show bookmarks incoming from other to repo
648 '''Show bookmarks incoming from other to repo
649 '''
649 '''
650 ui.status(_("searching for changed bookmarks\n"))
650 ui.status(_("searching for changed bookmarks\n"))
651
651
652 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
652 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
653 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
653 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
654 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
654 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
655
655
656 incomings = []
656 incomings = []
657 if ui.debugflag:
657 if ui.debugflag:
658 getid = lambda id: id
658 getid = lambda id: id
659 else:
659 else:
660 getid = lambda id: id[:12]
660 getid = lambda id: id[:12]
661 if ui.verbose:
661 if ui.verbose:
662 def add(b, id, st):
662 def add(b, id, st):
663 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
663 incomings.append(" %-25s %s %s\n" % (b, getid(id), st))
664 else:
664 else:
665 def add(b, id, st):
665 def add(b, id, st):
666 incomings.append(" %-25s %s\n" % (b, getid(id)))
666 incomings.append(" %-25s %s\n" % (b, getid(id)))
667 for b, scid, dcid in addsrc:
667 for b, scid, dcid in addsrc:
668 # i18n: "added" refers to a bookmark
668 # i18n: "added" refers to a bookmark
669 add(b, hex(scid), _('added'))
669 add(b, hex(scid), _('added'))
670 for b, scid, dcid in advsrc:
670 for b, scid, dcid in advsrc:
671 # i18n: "advanced" refers to a bookmark
671 # i18n: "advanced" refers to a bookmark
672 add(b, hex(scid), _('advanced'))
672 add(b, hex(scid), _('advanced'))
673 for b, scid, dcid in diverge:
673 for b, scid, dcid in diverge:
674 # i18n: "diverged" refers to a bookmark
674 # i18n: "diverged" refers to a bookmark
675 add(b, hex(scid), _('diverged'))
675 add(b, hex(scid), _('diverged'))
676 for b, scid, dcid in differ:
676 for b, scid, dcid in differ:
677 # i18n: "changed" refers to a bookmark
677 # i18n: "changed" refers to a bookmark
678 add(b, hex(scid), _('changed'))
678 add(b, hex(scid), _('changed'))
679
679
680 if not incomings:
680 if not incomings:
681 ui.status(_("no changed bookmarks found\n"))
681 ui.status(_("no changed bookmarks found\n"))
682 return 1
682 return 1
683
683
684 for s in sorted(incomings):
684 for s in sorted(incomings):
685 ui.write(s)
685 ui.write(s)
686
686
687 return 0
687 return 0
688
688
689 def outgoing(ui, repo, other):
689 def outgoing(ui, repo, other):
690 '''Show bookmarks outgoing from repo to other
690 '''Show bookmarks outgoing from repo to other
691 '''
691 '''
692 ui.status(_("searching for changed bookmarks\n"))
692 ui.status(_("searching for changed bookmarks\n"))
693
693
694 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
694 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
695 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
695 r = comparebookmarks(repo, repo._bookmarks, remotemarks)
696 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
696 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
697
697
698 outgoings = []
698 outgoings = []
699 if ui.debugflag:
699 if ui.debugflag:
700 getid = lambda id: id
700 getid = lambda id: id
701 else:
701 else:
702 getid = lambda id: id[:12]
702 getid = lambda id: id[:12]
703 if ui.verbose:
703 if ui.verbose:
704 def add(b, id, st):
704 def add(b, id, st):
705 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
705 outgoings.append(" %-25s %s %s\n" % (b, getid(id), st))
706 else:
706 else:
707 def add(b, id, st):
707 def add(b, id, st):
708 outgoings.append(" %-25s %s\n" % (b, getid(id)))
708 outgoings.append(" %-25s %s\n" % (b, getid(id)))
709 for b, scid, dcid in addsrc:
709 for b, scid, dcid in addsrc:
710 # i18n: "added refers to a bookmark
710 # i18n: "added refers to a bookmark
711 add(b, hex(scid), _('added'))
711 add(b, hex(scid), _('added'))
712 for b, scid, dcid in adddst:
712 for b, scid, dcid in adddst:
713 # i18n: "deleted" refers to a bookmark
713 # i18n: "deleted" refers to a bookmark
714 add(b, ' ' * 40, _('deleted'))
714 add(b, ' ' * 40, _('deleted'))
715 for b, scid, dcid in advsrc:
715 for b, scid, dcid in advsrc:
716 # i18n: "advanced" refers to a bookmark
716 # i18n: "advanced" refers to a bookmark
717 add(b, hex(scid), _('advanced'))
717 add(b, hex(scid), _('advanced'))
718 for b, scid, dcid in diverge:
718 for b, scid, dcid in diverge:
719 # i18n: "diverged" refers to a bookmark
719 # i18n: "diverged" refers to a bookmark
720 add(b, hex(scid), _('diverged'))
720 add(b, hex(scid), _('diverged'))
721 for b, scid, dcid in differ:
721 for b, scid, dcid in differ:
722 # i18n: "changed" refers to a bookmark
722 # i18n: "changed" refers to a bookmark
723 add(b, hex(scid), _('changed'))
723 add(b, hex(scid), _('changed'))
724
724
725 if not outgoings:
725 if not outgoings:
726 ui.status(_("no changed bookmarks found\n"))
726 ui.status(_("no changed bookmarks found\n"))
727 return 1
727 return 1
728
728
729 for s in sorted(outgoings):
729 for s in sorted(outgoings):
730 ui.write(s)
730 ui.write(s)
731
731
732 return 0
732 return 0
733
733
734 def summary(repo, other):
734 def summary(repo, other):
735 '''Compare bookmarks between repo and other for "hg summary" output
735 '''Compare bookmarks between repo and other for "hg summary" output
736
736
737 This returns "(# of incoming, # of outgoing)" tuple.
737 This returns "(# of incoming, # of outgoing)" tuple.
738 '''
738 '''
739 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
739 remotemarks = unhexlifybookmarks(other.listkeys('bookmarks'))
740 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
740 r = comparebookmarks(repo, remotemarks, repo._bookmarks)
741 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
741 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r
742 return (len(addsrc), len(adddst))
742 return (len(addsrc), len(adddst))
743
743
744 def validdest(repo, old, new):
744 def validdest(repo, old, new):
745 """Is the new bookmark destination a valid update from the old one"""
745 """Is the new bookmark destination a valid update from the old one"""
746 repo = repo.unfiltered()
746 repo = repo.unfiltered()
747 if old == new:
747 if old == new:
748 # Old == new -> nothing to update.
748 # Old == new -> nothing to update.
749 return False
749 return False
750 elif not old:
750 elif not old:
751 # old is nullrev, anything is valid.
751 # old is nullrev, anything is valid.
752 # (new != nullrev has been excluded by the previous check)
752 # (new != nullrev has been excluded by the previous check)
753 return True
753 return True
754 elif repo.obsstore:
754 elif repo.obsstore:
755 return new.node() in obsutil.foreground(repo, [old.node()])
755 return new.node() in obsutil.foreground(repo, [old.node()])
756 else:
756 else:
757 # still an independent clause as it is lazier (and therefore faster)
757 # still an independent clause as it is lazier (and therefore faster)
758 return old.descendant(new)
758 return old.descendant(new)
759
759
760 def checkformat(repo, mark):
760 def checkformat(repo, mark):
761 """return a valid version of a potential bookmark name
761 """return a valid version of a potential bookmark name
762
762
763 Raises an abort error if the bookmark name is not valid.
763 Raises an abort error if the bookmark name is not valid.
764 """
764 """
765 mark = mark.strip()
765 mark = mark.strip()
766 if not mark:
766 if not mark:
767 raise error.Abort(_("bookmark names cannot consist entirely of "
767 raise error.Abort(_("bookmark names cannot consist entirely of "
768 "whitespace"))
768 "whitespace"))
769 scmutil.checknewlabel(repo, mark, 'bookmark')
769 scmutil.checknewlabel(repo, mark, 'bookmark')
770 return mark
770 return mark
771
771
772 def delete(repo, tr, names):
772 def delete(repo, tr, names):
773 """remove a mark from the bookmark store
773 """remove a mark from the bookmark store
774
774
775 Raises an abort error if mark does not exist.
775 Raises an abort error if mark does not exist.
776 """
776 """
777 marks = repo._bookmarks
777 marks = repo._bookmarks
778 changes = []
778 changes = []
779 for mark in names:
779 for mark in names:
780 if mark not in marks:
780 if mark not in marks:
781 raise error.Abort(_("bookmark '%s' does not exist") % mark)
781 raise error.Abort(_("bookmark '%s' does not exist") % mark)
782 if mark == repo._activebookmark:
782 if mark == repo._activebookmark:
783 deactivate(repo)
783 deactivate(repo)
784 changes.append((mark, None))
784 changes.append((mark, None))
785 marks.applychanges(repo, tr, changes)
785 marks.applychanges(repo, tr, changes)
786
786
787 def rename(repo, tr, old, new, force=False, inactive=False):
787 def rename(repo, tr, old, new, force=False, inactive=False):
788 """rename a bookmark from old to new
788 """rename a bookmark from old to new
789
789
790 If force is specified, then the new name can overwrite an existing
790 If force is specified, then the new name can overwrite an existing
791 bookmark.
791 bookmark.
792
792
793 If inactive is specified, then do not activate the new bookmark.
793 If inactive is specified, then do not activate the new bookmark.
794
794
795 Raises an abort error if old is not in the bookmark store.
795 Raises an abort error if old is not in the bookmark store.
796 """
796 """
797 marks = repo._bookmarks
797 marks = repo._bookmarks
798 mark = checkformat(repo, new)
798 mark = checkformat(repo, new)
799 if old not in marks:
799 if old not in marks:
800 raise error.Abort(_("bookmark '%s' does not exist") % old)
800 raise error.Abort(_("bookmark '%s' does not exist") % old)
801 changes = []
801 changes = []
802 for bm in marks.checkconflict(mark, force):
802 for bm in marks.checkconflict(mark, force):
803 changes.append((bm, None))
803 changes.append((bm, None))
804 changes.extend([(mark, marks[old]), (old, None)])
804 changes.extend([(mark, marks[old]), (old, None)])
805 marks.applychanges(repo, tr, changes)
805 marks.applychanges(repo, tr, changes)
806 if repo._activebookmark == old and not inactive:
806 if repo._activebookmark == old and not inactive:
807 activate(repo, mark)
807 activate(repo, mark)
808
808
809 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
809 def addbookmarks(repo, tr, names, rev=None, force=False, inactive=False):
810 """add a list of bookmarks
810 """add a list of bookmarks
811
811
812 If force is specified, then the new name can overwrite an existing
812 If force is specified, then the new name can overwrite an existing
813 bookmark.
813 bookmark.
814
814
815 If inactive is specified, then do not activate any bookmark. Otherwise, the
815 If inactive is specified, then do not activate any bookmark. Otherwise, the
816 first bookmark is activated.
816 first bookmark is activated.
817
817
818 Raises an abort error if old is not in the bookmark store.
818 Raises an abort error if old is not in the bookmark store.
819 """
819 """
820 marks = repo._bookmarks
820 marks = repo._bookmarks
821 cur = repo['.'].node()
821 cur = repo['.'].node()
822 newact = None
822 newact = None
823 changes = []
823 changes = []
824 hiddenrev = None
824 hiddenrev = None
825
825
826 # unhide revs if any
826 # unhide revs if any
827 if rev:
827 if rev:
828 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
828 repo = scmutil.unhidehashlikerevs(repo, [rev], 'nowarn')
829
829
830 for mark in names:
830 for mark in names:
831 mark = checkformat(repo, mark)
831 mark = checkformat(repo, mark)
832 if newact is None:
832 if newact is None:
833 newact = mark
833 newact = mark
834 if inactive and mark == repo._activebookmark:
834 if inactive and mark == repo._activebookmark:
835 deactivate(repo)
835 deactivate(repo)
836 return
836 return
837 tgt = cur
837 tgt = cur
838 if rev:
838 if rev:
839 ctx = scmutil.revsingle(repo, rev)
839 ctx = scmutil.revsingle(repo, rev)
840 if ctx.hidden():
840 if ctx.hidden():
841 hiddenrev = ctx.hex()[:12]
841 hiddenrev = ctx.hex()[:12]
842 tgt = ctx.node()
842 tgt = ctx.node()
843 for bm in marks.checkconflict(mark, force, tgt):
843 for bm in marks.checkconflict(mark, force, tgt):
844 changes.append((bm, None))
844 changes.append((bm, None))
845 changes.append((mark, tgt))
845 changes.append((mark, tgt))
846
846
847 if hiddenrev:
847 if hiddenrev:
848 repo.ui.warn(_("bookmarking hidden changeset %s\n") % hiddenrev)
848 repo.ui.warn(_("bookmarking hidden changeset %s\n") % hiddenrev)
849
849
850 if ctx.obsolete():
850 if ctx.obsolete():
851 msg = obsutil._getfilteredreason(repo, "%s" % hiddenrev, ctx)
851 msg = obsutil._getfilteredreason(repo, "%s" % hiddenrev, ctx)
852 repo.ui.warn("(%s)\n" % msg)
852 repo.ui.warn("(%s)\n" % msg)
853
853
854 marks.applychanges(repo, tr, changes)
854 marks.applychanges(repo, tr, changes)
855 if not inactive and cur == marks[newact] and not rev:
855 if not inactive and cur == marks[newact] and not rev:
856 activate(repo, newact)
856 activate(repo, newact)
857 elif cur != tgt and newact == repo._activebookmark:
857 elif cur != tgt and newact == repo._activebookmark:
858 deactivate(repo)
858 deactivate(repo)
859
859
860 def _printbookmarks(ui, repo, bmarks, **opts):
860 def _printbookmarks(ui, repo, bmarks, **opts):
861 """private method to print bookmarks
861 """private method to print bookmarks
862
862
863 Provides a way for extensions to control how bookmarks are printed (e.g.
863 Provides a way for extensions to control how bookmarks are printed (e.g.
864 prepend or postpend names)
864 prepend or postpend names)
865 """
865 """
866 opts = pycompat.byteskwargs(opts)
866 opts = pycompat.byteskwargs(opts)
867 fm = ui.formatter('bookmarks', opts)
867 fm = ui.formatter('bookmarks', opts)
868 hexfn = fm.hexfunc
868 hexfn = fm.hexfunc
869 if len(bmarks) == 0 and fm.isplain():
869 if len(bmarks) == 0 and fm.isplain():
870 ui.status(_("no bookmarks set\n"))
870 ui.status(_("no bookmarks set\n"))
871 for bmark, (n, prefix, label) in sorted(bmarks.iteritems()):
871 for bmark, (n, prefix, label) in sorted(bmarks.iteritems()):
872 fm.startitem()
872 fm.startitem()
873 if not ui.quiet:
873 if not ui.quiet:
874 fm.plain(' %s ' % prefix, label=label)
874 fm.plain(' %s ' % prefix, label=label)
875 fm.write('bookmark', '%s', bmark, label=label)
875 fm.write('bookmark', '%s', bmark, label=label)
876 pad = " " * (25 - encoding.colwidth(bmark))
876 pad = " " * (25 - encoding.colwidth(bmark))
877 fm.condwrite(not ui.quiet, 'rev node', pad + ' %d:%s',
877 fm.condwrite(not ui.quiet, 'rev node', pad + ' %d:%s',
878 repo.changelog.rev(n), hexfn(n), label=label)
878 repo.changelog.rev(n), hexfn(n), label=label)
879 fm.data(active=(activebookmarklabel in label))
879 fm.data(active=(activebookmarklabel in label))
880 fm.plain('\n')
880 fm.plain('\n')
881 fm.end()
881 fm.end()
882
882
883 def printbookmarks(ui, repo, **opts):
883 def printbookmarks(ui, repo, **opts):
884 """print bookmarks to a formatter
884 """print bookmarks to a formatter
885
885
886 Provides a way for extensions to control how bookmarks are printed.
886 Provides a way for extensions to control how bookmarks are printed.
887 """
887 """
888 marks = repo._bookmarks
888 marks = repo._bookmarks
889 bmarks = {}
889 bmarks = {}
890 for bmark, n in sorted(marks.iteritems()):
890 for bmark, n in sorted(marks.iteritems()):
891 active = repo._activebookmark
891 active = repo._activebookmark
892 if bmark == active:
892 if bmark == active:
893 prefix, label = '*', activebookmarklabel
893 prefix, label = '*', activebookmarklabel
894 else:
894 else:
895 prefix, label = ' ', ''
895 prefix, label = ' ', ''
896
896
897 bmarks[bmark] = (n, prefix, label)
897 bmarks[bmark] = (n, prefix, label)
898 _printbookmarks(ui, repo, bmarks, **opts)
898 _printbookmarks(ui, repo, bmarks, **opts)
899
899
900 def preparehookargs(name, old, new):
900 def preparehookargs(name, old, new):
901 if new is None:
901 if new is None:
902 new = ''
902 new = ''
903 if old is None:
903 if old is None:
904 old = ''
904 old = ''
905 return {'bookmark': name,
905 return {'bookmark': name,
906 'node': hex(new),
906 'node': hex(new),
907 'oldnode': hex(old)}
907 'oldnode': hex(old)}
@@ -1,415 +1,415 b''
1 # destutil.py - Mercurial utility function for command destination
1 # destutil.py - Mercurial utility function for command destination
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com> and other
3 # Copyright Matt Mackall <mpm@selenic.com> and other
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 from .i18n import _
10 from .i18n import _
11 from . import (
11 from . import (
12 bookmarks,
12 bookmarks,
13 error,
13 error,
14 obsutil,
14 obsutil,
15 scmutil,
15 scmutil,
16 stack
16 stack
17 )
17 )
18
18
19 def _destupdateobs(repo, clean):
19 def _destupdateobs(repo, clean):
20 """decide of an update destination from obsolescence markers"""
20 """decide of an update destination from obsolescence markers"""
21 node = None
21 node = None
22 wc = repo[None]
22 wc = repo[None]
23 p1 = wc.p1()
23 p1 = wc.p1()
24 movemark = None
24 movemark = None
25
25
26 if p1.obsolete() and not p1.children():
26 if p1.obsolete() and not p1.children():
27 # allow updating to successors
27 # allow updating to successors
28 successors = obsutil.successorssets(repo, p1.node())
28 successors = obsutil.successorssets(repo, p1.node())
29
29
30 # behavior of certain cases is as follows,
30 # behavior of certain cases is as follows,
31 #
31 #
32 # divergent changesets: update to highest rev, similar to what
32 # divergent changesets: update to highest rev, similar to what
33 # is currently done when there are more than one head
33 # is currently done when there are more than one head
34 # (i.e. 'tip')
34 # (i.e. 'tip')
35 #
35 #
36 # replaced changesets: same as divergent except we know there
36 # replaced changesets: same as divergent except we know there
37 # is no conflict
37 # is no conflict
38 #
38 #
39 # pruned changeset: no update is done; though, we could
39 # pruned changeset: no update is done; though, we could
40 # consider updating to the first non-obsolete parent,
40 # consider updating to the first non-obsolete parent,
41 # similar to what is current done for 'hg prune'
41 # similar to what is current done for 'hg prune'
42
42
43 if successors:
43 if successors:
44 # flatten the list here handles both divergent (len > 1)
44 # flatten the list here handles both divergent (len > 1)
45 # and the usual case (len = 1)
45 # and the usual case (len = 1)
46 successors = [n for sub in successors for n in sub]
46 successors = [n for sub in successors for n in sub]
47
47
48 # get the max revision for the given successors set,
48 # get the max revision for the given successors set,
49 # i.e. the 'tip' of a set
49 # i.e. the 'tip' of a set
50 node = repo.revs('max(%ln)', successors).first()
50 node = repo.revs('max(%ln)', successors).first()
51 if bookmarks.isactivewdirparent(repo):
51 if bookmarks.isactivewdirparent(repo):
52 movemark = repo['.'].node()
52 movemark = repo['.'].node()
53 return node, movemark, None
53 return node, movemark, None
54
54
55 def _destupdatebook(repo, clean):
55 def _destupdatebook(repo, clean):
56 """decide on an update destination from active bookmark"""
56 """decide on an update destination from active bookmark"""
57 # we also move the active bookmark, if any
57 # we also move the active bookmark, if any
58 activemark = None
58 node = None
59 node, movemark = bookmarks.calculateupdate(repo.ui, repo, None)
59 activemark, movemark = bookmarks.calculateupdate(repo.ui, repo, None)
60 if node is not None:
60 if activemark is not None:
61 activemark = node
61 node = repo.lookup(activemark)
62 return node, movemark, activemark
62 return node, movemark, activemark
63
63
64 def _destupdatebranch(repo, clean):
64 def _destupdatebranch(repo, clean):
65 """decide on an update destination from current branch
65 """decide on an update destination from current branch
66
66
67 This ignores closed branch heads.
67 This ignores closed branch heads.
68 """
68 """
69 wc = repo[None]
69 wc = repo[None]
70 movemark = node = None
70 movemark = node = None
71 currentbranch = wc.branch()
71 currentbranch = wc.branch()
72
72
73 if clean:
73 if clean:
74 currentbranch = repo['.'].branch()
74 currentbranch = repo['.'].branch()
75
75
76 if currentbranch in repo.branchmap():
76 if currentbranch in repo.branchmap():
77 heads = repo.branchheads(currentbranch)
77 heads = repo.branchheads(currentbranch)
78 if heads:
78 if heads:
79 node = repo.revs('max(.::(%ln))', heads).first()
79 node = repo.revs('max(.::(%ln))', heads).first()
80 if bookmarks.isactivewdirparent(repo):
80 if bookmarks.isactivewdirparent(repo):
81 movemark = repo['.'].node()
81 movemark = repo['.'].node()
82 elif currentbranch == 'default' and not wc.p1():
82 elif currentbranch == 'default' and not wc.p1():
83 # "null" parent belongs to "default" branch, but it doesn't exist, so
83 # "null" parent belongs to "default" branch, but it doesn't exist, so
84 # update to the tipmost non-closed branch head
84 # update to the tipmost non-closed branch head
85 node = repo.revs('max(head() and not closed())').first()
85 node = repo.revs('max(head() and not closed())').first()
86 else:
86 else:
87 node = repo['.'].node()
87 node = repo['.'].node()
88 return node, movemark, None
88 return node, movemark, None
89
89
90 def _destupdatebranchfallback(repo, clean):
90 def _destupdatebranchfallback(repo, clean):
91 """decide on an update destination from closed heads in current branch"""
91 """decide on an update destination from closed heads in current branch"""
92 wc = repo[None]
92 wc = repo[None]
93 currentbranch = wc.branch()
93 currentbranch = wc.branch()
94 movemark = None
94 movemark = None
95 if currentbranch in repo.branchmap():
95 if currentbranch in repo.branchmap():
96 # here, all descendant branch heads are closed
96 # here, all descendant branch heads are closed
97 heads = repo.branchheads(currentbranch, closed=True)
97 heads = repo.branchheads(currentbranch, closed=True)
98 assert heads, "any branch has at least one head"
98 assert heads, "any branch has at least one head"
99 node = repo.revs('max(.::(%ln))', heads).first()
99 node = repo.revs('max(.::(%ln))', heads).first()
100 assert node is not None, ("any revision has at least "
100 assert node is not None, ("any revision has at least "
101 "one descendant branch head")
101 "one descendant branch head")
102 if bookmarks.isactivewdirparent(repo):
102 if bookmarks.isactivewdirparent(repo):
103 movemark = repo['.'].node()
103 movemark = repo['.'].node()
104 else:
104 else:
105 # here, no "default" branch, and all branches are closed
105 # here, no "default" branch, and all branches are closed
106 node = repo.lookup('tip')
106 node = repo.lookup('tip')
107 assert node is not None, "'tip' exists even in empty repository"
107 assert node is not None, "'tip' exists even in empty repository"
108 return node, movemark, None
108 return node, movemark, None
109
109
110 # order in which each step should be evaluated
110 # order in which each step should be evaluated
111 # steps are run until one finds a destination
111 # steps are run until one finds a destination
112 destupdatesteps = ['evolution', 'bookmark', 'branch', 'branchfallback']
112 destupdatesteps = ['evolution', 'bookmark', 'branch', 'branchfallback']
113 # mapping to ease extension overriding steps.
113 # mapping to ease extension overriding steps.
114 destupdatestepmap = {'evolution': _destupdateobs,
114 destupdatestepmap = {'evolution': _destupdateobs,
115 'bookmark': _destupdatebook,
115 'bookmark': _destupdatebook,
116 'branch': _destupdatebranch,
116 'branch': _destupdatebranch,
117 'branchfallback': _destupdatebranchfallback,
117 'branchfallback': _destupdatebranchfallback,
118 }
118 }
119
119
120 def destupdate(repo, clean=False):
120 def destupdate(repo, clean=False):
121 """destination for bare update operation
121 """destination for bare update operation
122
122
123 return (rev, movemark, activemark)
123 return (rev, movemark, activemark)
124
124
125 - rev: the revision to update to,
125 - rev: the revision to update to,
126 - movemark: node to move the active bookmark from
126 - movemark: node to move the active bookmark from
127 (cf bookmark.calculate update),
127 (cf bookmark.calculate update),
128 - activemark: a bookmark to activate at the end of the update.
128 - activemark: a bookmark to activate at the end of the update.
129 """
129 """
130 node = movemark = activemark = None
130 node = movemark = activemark = None
131
131
132 for step in destupdatesteps:
132 for step in destupdatesteps:
133 node, movemark, activemark = destupdatestepmap[step](repo, clean)
133 node, movemark, activemark = destupdatestepmap[step](repo, clean)
134 if node is not None:
134 if node is not None:
135 break
135 break
136 rev = repo[node].rev()
136 rev = repo[node].rev()
137
137
138 return rev, movemark, activemark
138 return rev, movemark, activemark
139
139
140 msgdestmerge = {
140 msgdestmerge = {
141 # too many matching divergent bookmark
141 # too many matching divergent bookmark
142 'toomanybookmarks':
142 'toomanybookmarks':
143 {'merge':
143 {'merge':
144 (_("multiple matching bookmarks to merge -"
144 (_("multiple matching bookmarks to merge -"
145 " please merge with an explicit rev or bookmark"),
145 " please merge with an explicit rev or bookmark"),
146 _("run 'hg heads' to see all heads")),
146 _("run 'hg heads' to see all heads")),
147 'rebase':
147 'rebase':
148 (_("multiple matching bookmarks to rebase -"
148 (_("multiple matching bookmarks to rebase -"
149 " please rebase to an explicit rev or bookmark"),
149 " please rebase to an explicit rev or bookmark"),
150 _("run 'hg heads' to see all heads")),
150 _("run 'hg heads' to see all heads")),
151 },
151 },
152 # no other matching divergent bookmark
152 # no other matching divergent bookmark
153 'nootherbookmarks':
153 'nootherbookmarks':
154 {'merge':
154 {'merge':
155 (_("no matching bookmark to merge - "
155 (_("no matching bookmark to merge - "
156 "please merge with an explicit rev or bookmark"),
156 "please merge with an explicit rev or bookmark"),
157 _("run 'hg heads' to see all heads")),
157 _("run 'hg heads' to see all heads")),
158 'rebase':
158 'rebase':
159 (_("no matching bookmark to rebase - "
159 (_("no matching bookmark to rebase - "
160 "please rebase to an explicit rev or bookmark"),
160 "please rebase to an explicit rev or bookmark"),
161 _("run 'hg heads' to see all heads")),
161 _("run 'hg heads' to see all heads")),
162 },
162 },
163 # branch have too many unbookmarked heads, no obvious destination
163 # branch have too many unbookmarked heads, no obvious destination
164 'toomanyheads':
164 'toomanyheads':
165 {'merge':
165 {'merge':
166 (_("branch '%s' has %d heads - please merge with an explicit rev"),
166 (_("branch '%s' has %d heads - please merge with an explicit rev"),
167 _("run 'hg heads .' to see heads")),
167 _("run 'hg heads .' to see heads")),
168 'rebase':
168 'rebase':
169 (_("branch '%s' has %d heads - please rebase to an explicit rev"),
169 (_("branch '%s' has %d heads - please rebase to an explicit rev"),
170 _("run 'hg heads .' to see heads")),
170 _("run 'hg heads .' to see heads")),
171 },
171 },
172 # branch have no other unbookmarked heads
172 # branch have no other unbookmarked heads
173 'bookmarkedheads':
173 'bookmarkedheads':
174 {'merge':
174 {'merge':
175 (_("heads are bookmarked - please merge with an explicit rev"),
175 (_("heads are bookmarked - please merge with an explicit rev"),
176 _("run 'hg heads' to see all heads")),
176 _("run 'hg heads' to see all heads")),
177 'rebase':
177 'rebase':
178 (_("heads are bookmarked - please rebase to an explicit rev"),
178 (_("heads are bookmarked - please rebase to an explicit rev"),
179 _("run 'hg heads' to see all heads")),
179 _("run 'hg heads' to see all heads")),
180 },
180 },
181 # branch have just a single heads, but there is other branches
181 # branch have just a single heads, but there is other branches
182 'nootherbranchheads':
182 'nootherbranchheads':
183 {'merge':
183 {'merge':
184 (_("branch '%s' has one head - please merge with an explicit rev"),
184 (_("branch '%s' has one head - please merge with an explicit rev"),
185 _("run 'hg heads' to see all heads")),
185 _("run 'hg heads' to see all heads")),
186 'rebase':
186 'rebase':
187 (_("branch '%s' has one head - please rebase to an explicit rev"),
187 (_("branch '%s' has one head - please rebase to an explicit rev"),
188 _("run 'hg heads' to see all heads")),
188 _("run 'hg heads' to see all heads")),
189 },
189 },
190 # repository have a single head
190 # repository have a single head
191 'nootherheads':
191 'nootherheads':
192 {'merge':
192 {'merge':
193 (_('nothing to merge'),
193 (_('nothing to merge'),
194 None),
194 None),
195 'rebase':
195 'rebase':
196 (_('nothing to rebase'),
196 (_('nothing to rebase'),
197 None),
197 None),
198 },
198 },
199 # repository have a single head and we are not on it
199 # repository have a single head and we are not on it
200 'nootherheadsbehind':
200 'nootherheadsbehind':
201 {'merge':
201 {'merge':
202 (_('nothing to merge'),
202 (_('nothing to merge'),
203 _("use 'hg update' instead")),
203 _("use 'hg update' instead")),
204 'rebase':
204 'rebase':
205 (_('nothing to rebase'),
205 (_('nothing to rebase'),
206 _("use 'hg update' instead")),
206 _("use 'hg update' instead")),
207 },
207 },
208 # We are not on a head
208 # We are not on a head
209 'notatheads':
209 'notatheads':
210 {'merge':
210 {'merge':
211 (_('working directory not at a head revision'),
211 (_('working directory not at a head revision'),
212 _("use 'hg update' or merge with an explicit revision")),
212 _("use 'hg update' or merge with an explicit revision")),
213 'rebase':
213 'rebase':
214 (_('working directory not at a head revision'),
214 (_('working directory not at a head revision'),
215 _("use 'hg update' or rebase to an explicit revision"))
215 _("use 'hg update' or rebase to an explicit revision"))
216 },
216 },
217 'emptysourceset':
217 'emptysourceset':
218 {'merge':
218 {'merge':
219 (_('source set is empty'),
219 (_('source set is empty'),
220 None),
220 None),
221 'rebase':
221 'rebase':
222 (_('source set is empty'),
222 (_('source set is empty'),
223 None),
223 None),
224 },
224 },
225 'multiplebranchessourceset':
225 'multiplebranchessourceset':
226 {'merge':
226 {'merge':
227 (_('source set is rooted in multiple branches'),
227 (_('source set is rooted in multiple branches'),
228 None),
228 None),
229 'rebase':
229 'rebase':
230 (_('rebaseset is rooted in multiple named branches'),
230 (_('rebaseset is rooted in multiple named branches'),
231 _('specify an explicit destination with --dest')),
231 _('specify an explicit destination with --dest')),
232 },
232 },
233 }
233 }
234
234
235 def _destmergebook(repo, action='merge', sourceset=None, destspace=None):
235 def _destmergebook(repo, action='merge', sourceset=None, destspace=None):
236 """find merge destination in the active bookmark case"""
236 """find merge destination in the active bookmark case"""
237 node = None
237 node = None
238 bmheads = bookmarks.headsforactive(repo)
238 bmheads = bookmarks.headsforactive(repo)
239 curhead = repo[repo._activebookmark].node()
239 curhead = repo[repo._activebookmark].node()
240 if len(bmheads) == 2:
240 if len(bmheads) == 2:
241 if curhead == bmheads[0]:
241 if curhead == bmheads[0]:
242 node = bmheads[1]
242 node = bmheads[1]
243 else:
243 else:
244 node = bmheads[0]
244 node = bmheads[0]
245 elif len(bmheads) > 2:
245 elif len(bmheads) > 2:
246 msg, hint = msgdestmerge['toomanybookmarks'][action]
246 msg, hint = msgdestmerge['toomanybookmarks'][action]
247 raise error.ManyMergeDestAbort(msg, hint=hint)
247 raise error.ManyMergeDestAbort(msg, hint=hint)
248 elif len(bmheads) <= 1:
248 elif len(bmheads) <= 1:
249 msg, hint = msgdestmerge['nootherbookmarks'][action]
249 msg, hint = msgdestmerge['nootherbookmarks'][action]
250 raise error.NoMergeDestAbort(msg, hint=hint)
250 raise error.NoMergeDestAbort(msg, hint=hint)
251 assert node is not None
251 assert node is not None
252 return node
252 return node
253
253
254 def _destmergebranch(repo, action='merge', sourceset=None, onheadcheck=True,
254 def _destmergebranch(repo, action='merge', sourceset=None, onheadcheck=True,
255 destspace=None):
255 destspace=None):
256 """find merge destination based on branch heads"""
256 """find merge destination based on branch heads"""
257 node = None
257 node = None
258
258
259 if sourceset is None:
259 if sourceset is None:
260 sourceset = [repo[repo.dirstate.p1()].rev()]
260 sourceset = [repo[repo.dirstate.p1()].rev()]
261 branch = repo.dirstate.branch()
261 branch = repo.dirstate.branch()
262 elif not sourceset:
262 elif not sourceset:
263 msg, hint = msgdestmerge['emptysourceset'][action]
263 msg, hint = msgdestmerge['emptysourceset'][action]
264 raise error.NoMergeDestAbort(msg, hint=hint)
264 raise error.NoMergeDestAbort(msg, hint=hint)
265 else:
265 else:
266 branch = None
266 branch = None
267 for ctx in repo.set('roots(%ld::%ld)', sourceset, sourceset):
267 for ctx in repo.set('roots(%ld::%ld)', sourceset, sourceset):
268 if branch is not None and ctx.branch() != branch:
268 if branch is not None and ctx.branch() != branch:
269 msg, hint = msgdestmerge['multiplebranchessourceset'][action]
269 msg, hint = msgdestmerge['multiplebranchessourceset'][action]
270 raise error.ManyMergeDestAbort(msg, hint=hint)
270 raise error.ManyMergeDestAbort(msg, hint=hint)
271 branch = ctx.branch()
271 branch = ctx.branch()
272
272
273 bheads = repo.branchheads(branch)
273 bheads = repo.branchheads(branch)
274 onhead = repo.revs('%ld and %ln', sourceset, bheads)
274 onhead = repo.revs('%ld and %ln', sourceset, bheads)
275 if onheadcheck and not onhead:
275 if onheadcheck and not onhead:
276 # Case A: working copy if not on a head. (merge only)
276 # Case A: working copy if not on a head. (merge only)
277 #
277 #
278 # This is probably a user mistake We bailout pointing at 'hg update'
278 # This is probably a user mistake We bailout pointing at 'hg update'
279 if len(repo.heads()) <= 1:
279 if len(repo.heads()) <= 1:
280 msg, hint = msgdestmerge['nootherheadsbehind'][action]
280 msg, hint = msgdestmerge['nootherheadsbehind'][action]
281 else:
281 else:
282 msg, hint = msgdestmerge['notatheads'][action]
282 msg, hint = msgdestmerge['notatheads'][action]
283 raise error.Abort(msg, hint=hint)
283 raise error.Abort(msg, hint=hint)
284 # remove heads descendants of source from the set
284 # remove heads descendants of source from the set
285 bheads = list(repo.revs('%ln - (%ld::)', bheads, sourceset))
285 bheads = list(repo.revs('%ln - (%ld::)', bheads, sourceset))
286 # filters out bookmarked heads
286 # filters out bookmarked heads
287 nbhs = list(repo.revs('%ld - bookmark()', bheads))
287 nbhs = list(repo.revs('%ld - bookmark()', bheads))
288
288
289 if destspace is not None:
289 if destspace is not None:
290 # restrict search space
290 # restrict search space
291 # used in the 'hg pull --rebase' case, see issue 5214.
291 # used in the 'hg pull --rebase' case, see issue 5214.
292 nbhs = list(repo.revs('%ld and %ld', destspace, nbhs))
292 nbhs = list(repo.revs('%ld and %ld', destspace, nbhs))
293
293
294 if len(nbhs) > 1:
294 if len(nbhs) > 1:
295 # Case B: There is more than 1 other anonymous heads
295 # Case B: There is more than 1 other anonymous heads
296 #
296 #
297 # This means that there will be more than 1 candidate. This is
297 # This means that there will be more than 1 candidate. This is
298 # ambiguous. We abort asking the user to pick as explicit destination
298 # ambiguous. We abort asking the user to pick as explicit destination
299 # instead.
299 # instead.
300 msg, hint = msgdestmerge['toomanyheads'][action]
300 msg, hint = msgdestmerge['toomanyheads'][action]
301 msg %= (branch, len(bheads) + 1)
301 msg %= (branch, len(bheads) + 1)
302 raise error.ManyMergeDestAbort(msg, hint=hint)
302 raise error.ManyMergeDestAbort(msg, hint=hint)
303 elif not nbhs:
303 elif not nbhs:
304 # Case B: There is no other anonymous heads
304 # Case B: There is no other anonymous heads
305 #
305 #
306 # This means that there is no natural candidate to merge with.
306 # This means that there is no natural candidate to merge with.
307 # We abort, with various messages for various cases.
307 # We abort, with various messages for various cases.
308 if bheads:
308 if bheads:
309 msg, hint = msgdestmerge['bookmarkedheads'][action]
309 msg, hint = msgdestmerge['bookmarkedheads'][action]
310 elif len(repo.heads()) > 1:
310 elif len(repo.heads()) > 1:
311 msg, hint = msgdestmerge['nootherbranchheads'][action]
311 msg, hint = msgdestmerge['nootherbranchheads'][action]
312 msg %= branch
312 msg %= branch
313 elif not onhead:
313 elif not onhead:
314 # if 'onheadcheck == False' (rebase case),
314 # if 'onheadcheck == False' (rebase case),
315 # this was not caught in Case A.
315 # this was not caught in Case A.
316 msg, hint = msgdestmerge['nootherheadsbehind'][action]
316 msg, hint = msgdestmerge['nootherheadsbehind'][action]
317 else:
317 else:
318 msg, hint = msgdestmerge['nootherheads'][action]
318 msg, hint = msgdestmerge['nootherheads'][action]
319 raise error.NoMergeDestAbort(msg, hint=hint)
319 raise error.NoMergeDestAbort(msg, hint=hint)
320 else:
320 else:
321 node = nbhs[0]
321 node = nbhs[0]
322 assert node is not None
322 assert node is not None
323 return node
323 return node
324
324
325 def destmerge(repo, action='merge', sourceset=None, onheadcheck=True,
325 def destmerge(repo, action='merge', sourceset=None, onheadcheck=True,
326 destspace=None):
326 destspace=None):
327 """return the default destination for a merge
327 """return the default destination for a merge
328
328
329 (or raise exception about why it can't pick one)
329 (or raise exception about why it can't pick one)
330
330
331 :action: the action being performed, controls emitted error message
331 :action: the action being performed, controls emitted error message
332 """
332 """
333 # destspace is here to work around issues with `hg pull --rebase` see
333 # destspace is here to work around issues with `hg pull --rebase` see
334 # issue5214 for details
334 # issue5214 for details
335 if repo._activebookmark:
335 if repo._activebookmark:
336 node = _destmergebook(repo, action=action, sourceset=sourceset,
336 node = _destmergebook(repo, action=action, sourceset=sourceset,
337 destspace=destspace)
337 destspace=destspace)
338 else:
338 else:
339 node = _destmergebranch(repo, action=action, sourceset=sourceset,
339 node = _destmergebranch(repo, action=action, sourceset=sourceset,
340 onheadcheck=onheadcheck, destspace=destspace)
340 onheadcheck=onheadcheck, destspace=destspace)
341 return repo[node].rev()
341 return repo[node].rev()
342
342
343 def desthistedit(ui, repo):
343 def desthistedit(ui, repo):
344 """Default base revision to edit for `hg histedit`."""
344 """Default base revision to edit for `hg histedit`."""
345 default = ui.config('histedit', 'defaultrev')
345 default = ui.config('histedit', 'defaultrev')
346
346
347 if default is None:
347 if default is None:
348 revs = stack.getstack(repo)
348 revs = stack.getstack(repo)
349 elif default:
349 elif default:
350 revs = scmutil.revrange(repo, [default])
350 revs = scmutil.revrange(repo, [default])
351
351
352 if revs:
352 if revs:
353 # The revset supplied by the user may not be in ascending order nor
353 # The revset supplied by the user may not be in ascending order nor
354 # take the first revision. So do this manually.
354 # take the first revision. So do this manually.
355 revs.sort()
355 revs.sort()
356 return revs.first()
356 return revs.first()
357
357
358 return None
358 return None
359
359
360 def stackbase(ui, repo):
360 def stackbase(ui, repo):
361 revs = stack.getstack(repo)
361 revs = stack.getstack(repo)
362 return revs.first() if revs else None
362 return revs.first() if revs else None
363
363
364 def _statusotherbook(ui, repo):
364 def _statusotherbook(ui, repo):
365 bmheads = bookmarks.headsforactive(repo)
365 bmheads = bookmarks.headsforactive(repo)
366 curhead = repo[repo._activebookmark].node()
366 curhead = repo[repo._activebookmark].node()
367 if repo.revs('%n and parents()', curhead):
367 if repo.revs('%n and parents()', curhead):
368 # we are on the active bookmark
368 # we are on the active bookmark
369 bmheads = [b for b in bmheads if curhead != b]
369 bmheads = [b for b in bmheads if curhead != b]
370 if bmheads:
370 if bmheads:
371 msg = _('%i other divergent bookmarks for "%s"\n')
371 msg = _('%i other divergent bookmarks for "%s"\n')
372 ui.status(msg % (len(bmheads), repo._activebookmark))
372 ui.status(msg % (len(bmheads), repo._activebookmark))
373
373
374 def _statusotherbranchheads(ui, repo):
374 def _statusotherbranchheads(ui, repo):
375 currentbranch = repo.dirstate.branch()
375 currentbranch = repo.dirstate.branch()
376 allheads = repo.branchheads(currentbranch, closed=True)
376 allheads = repo.branchheads(currentbranch, closed=True)
377 heads = repo.branchheads(currentbranch)
377 heads = repo.branchheads(currentbranch)
378 if repo.revs('%ln and parents()', allheads):
378 if repo.revs('%ln and parents()', allheads):
379 # we are on a head, even though it might be closed
379 # we are on a head, even though it might be closed
380 #
380 #
381 # on closed otherheads
381 # on closed otherheads
382 # ========= ==========
382 # ========= ==========
383 # o 0 all heads for current branch are closed
383 # o 0 all heads for current branch are closed
384 # N only descendant branch heads are closed
384 # N only descendant branch heads are closed
385 # x 0 there is only one non-closed branch head
385 # x 0 there is only one non-closed branch head
386 # N there are some non-closed branch heads
386 # N there are some non-closed branch heads
387 # ========= ==========
387 # ========= ==========
388 otherheads = repo.revs('%ln - parents()', heads)
388 otherheads = repo.revs('%ln - parents()', heads)
389 if repo['.'].closesbranch():
389 if repo['.'].closesbranch():
390 ui.warn(_('no open descendant heads on branch "%s", '
390 ui.warn(_('no open descendant heads on branch "%s", '
391 'updating to a closed head\n') %
391 'updating to a closed head\n') %
392 (currentbranch))
392 (currentbranch))
393 if otherheads:
393 if otherheads:
394 ui.warn(_("(committing will reopen the head, "
394 ui.warn(_("(committing will reopen the head, "
395 "use 'hg heads .' to see %i other heads)\n") %
395 "use 'hg heads .' to see %i other heads)\n") %
396 (len(otherheads)))
396 (len(otherheads)))
397 else:
397 else:
398 ui.warn(_('(committing will reopen branch "%s")\n') %
398 ui.warn(_('(committing will reopen branch "%s")\n') %
399 (currentbranch))
399 (currentbranch))
400 elif otherheads:
400 elif otherheads:
401 curhead = repo['.']
401 curhead = repo['.']
402 ui.status(_('updated to "%s: %s"\n') % (curhead,
402 ui.status(_('updated to "%s: %s"\n') % (curhead,
403 curhead.description().split('\n')[0]))
403 curhead.description().split('\n')[0]))
404 ui.status(_('%i other heads for branch "%s"\n') %
404 ui.status(_('%i other heads for branch "%s"\n') %
405 (len(otherheads), currentbranch))
405 (len(otherheads), currentbranch))
406
406
407 def statusotherdests(ui, repo):
407 def statusotherdests(ui, repo):
408 """Print message about other head"""
408 """Print message about other head"""
409 # XXX we should probably include a hint:
409 # XXX we should probably include a hint:
410 # - about what to do
410 # - about what to do
411 # - how to see such heads
411 # - how to see such heads
412 if repo._activebookmark:
412 if repo._activebookmark:
413 _statusotherbook(ui, repo)
413 _statusotherbook(ui, repo)
414 else:
414 else:
415 _statusotherbranchheads(ui, repo)
415 _statusotherbranchheads(ui, repo)
General Comments 0
You need to be logged in to leave comments. Login now