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