##// END OF EJS Templates
destutil: ensure we offer 'hg update' hint when not at head in all cases...
Pierre-Yves David -
r28161:3324345a default
parent child Browse files
Show More
@@ -1,339 +1,344
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 obsolete,
14 obsolete,
15 )
15 )
16
16
17 def _destupdatevalidate(repo, rev, clean, check):
17 def _destupdatevalidate(repo, rev, clean, check):
18 """validate that the destination comply to various rules
18 """validate that the destination comply to various rules
19
19
20 This exists as its own function to help wrapping from extensions."""
20 This exists as its own function to help wrapping from extensions."""
21 wc = repo[None]
21 wc = repo[None]
22 p1 = wc.p1()
22 p1 = wc.p1()
23 if not clean:
23 if not clean:
24 # Check that the update is linear.
24 # Check that the update is linear.
25 #
25 #
26 # Mercurial do not allow update-merge for non linear pattern
26 # Mercurial do not allow update-merge for non linear pattern
27 # (that would be technically possible but was considered too confusing
27 # (that would be technically possible but was considered too confusing
28 # for user a long time ago)
28 # for user a long time ago)
29 #
29 #
30 # See mercurial.merge.update for details
30 # See mercurial.merge.update for details
31 if p1.rev() not in repo.changelog.ancestors([rev], inclusive=True):
31 if p1.rev() not in repo.changelog.ancestors([rev], inclusive=True):
32 dirty = wc.dirty(missing=True)
32 dirty = wc.dirty(missing=True)
33 foreground = obsolete.foreground(repo, [p1.node()])
33 foreground = obsolete.foreground(repo, [p1.node()])
34 if not repo[rev].node() in foreground:
34 if not repo[rev].node() in foreground:
35 if dirty:
35 if dirty:
36 msg = _("uncommitted changes")
36 msg = _("uncommitted changes")
37 hint = _("commit and merge, or update --clean to"
37 hint = _("commit and merge, or update --clean to"
38 " discard changes")
38 " discard changes")
39 raise error.UpdateAbort(msg, hint=hint)
39 raise error.UpdateAbort(msg, hint=hint)
40 elif not check: # destination is not a descendant.
40 elif not check: # destination is not a descendant.
41 msg = _("not a linear update")
41 msg = _("not a linear update")
42 hint = _("merge or update --check to force update")
42 hint = _("merge or update --check to force update")
43 raise error.UpdateAbort(msg, hint=hint)
43 raise error.UpdateAbort(msg, hint=hint)
44
44
45 def _destupdateobs(repo, clean, check):
45 def _destupdateobs(repo, clean, check):
46 """decide of an update destination from obsolescence markers"""
46 """decide of an update destination from obsolescence markers"""
47 node = None
47 node = None
48 wc = repo[None]
48 wc = repo[None]
49 p1 = wc.p1()
49 p1 = wc.p1()
50 movemark = None
50 movemark = None
51
51
52 if p1.obsolete() and not p1.children():
52 if p1.obsolete() and not p1.children():
53 # allow updating to successors
53 # allow updating to successors
54 successors = obsolete.successorssets(repo, p1.node())
54 successors = obsolete.successorssets(repo, p1.node())
55
55
56 # behavior of certain cases is as follows,
56 # behavior of certain cases is as follows,
57 #
57 #
58 # divergent changesets: update to highest rev, similar to what
58 # divergent changesets: update to highest rev, similar to what
59 # is currently done when there are more than one head
59 # is currently done when there are more than one head
60 # (i.e. 'tip')
60 # (i.e. 'tip')
61 #
61 #
62 # replaced changesets: same as divergent except we know there
62 # replaced changesets: same as divergent except we know there
63 # is no conflict
63 # is no conflict
64 #
64 #
65 # pruned changeset: no update is done; though, we could
65 # pruned changeset: no update is done; though, we could
66 # consider updating to the first non-obsolete parent,
66 # consider updating to the first non-obsolete parent,
67 # similar to what is current done for 'hg prune'
67 # similar to what is current done for 'hg prune'
68
68
69 if successors:
69 if successors:
70 # flatten the list here handles both divergent (len > 1)
70 # flatten the list here handles both divergent (len > 1)
71 # and the usual case (len = 1)
71 # and the usual case (len = 1)
72 successors = [n for sub in successors for n in sub]
72 successors = [n for sub in successors for n in sub]
73
73
74 # get the max revision for the given successors set,
74 # get the max revision for the given successors set,
75 # i.e. the 'tip' of a set
75 # i.e. the 'tip' of a set
76 node = repo.revs('max(%ln)', successors).first()
76 node = repo.revs('max(%ln)', successors).first()
77 if bookmarks.isactivewdirparent(repo):
77 if bookmarks.isactivewdirparent(repo):
78 movemark = repo['.'].node()
78 movemark = repo['.'].node()
79 return node, movemark, None
79 return node, movemark, None
80
80
81 def _destupdatebook(repo, clean, check):
81 def _destupdatebook(repo, clean, check):
82 """decide on an update destination from active bookmark"""
82 """decide on an update destination from active bookmark"""
83 # we also move the active bookmark, if any
83 # we also move the active bookmark, if any
84 activemark = None
84 activemark = None
85 node, movemark = bookmarks.calculateupdate(repo.ui, repo, None)
85 node, movemark = bookmarks.calculateupdate(repo.ui, repo, None)
86 if node is not None:
86 if node is not None:
87 activemark = node
87 activemark = node
88 return node, movemark, activemark
88 return node, movemark, activemark
89
89
90 def _destupdatebranch(repo, clean, check):
90 def _destupdatebranch(repo, clean, check):
91 """decide on an update destination from current branch"""
91 """decide on an update destination from current branch"""
92 wc = repo[None]
92 wc = repo[None]
93 movemark = node = None
93 movemark = node = None
94 try:
94 try:
95 node = repo.revs('max(.::(head() and branch(%s)))'
95 node = repo.revs('max(.::(head() and branch(%s)))'
96 , wc.branch()).first()
96 , wc.branch()).first()
97 if bookmarks.isactivewdirparent(repo):
97 if bookmarks.isactivewdirparent(repo):
98 movemark = repo['.'].node()
98 movemark = repo['.'].node()
99 except error.RepoLookupError:
99 except error.RepoLookupError:
100 if wc.branch() == 'default': # no default branch!
100 if wc.branch() == 'default': # no default branch!
101 node = repo.lookup('tip') # update to tip
101 node = repo.lookup('tip') # update to tip
102 else:
102 else:
103 raise error.Abort(_("branch %s not found") % wc.branch())
103 raise error.Abort(_("branch %s not found") % wc.branch())
104 return node, movemark, None
104 return node, movemark, None
105
105
106 # order in which each step should be evalutated
106 # order in which each step should be evalutated
107 # steps are run until one finds a destination
107 # steps are run until one finds a destination
108 destupdatesteps = ['evolution', 'bookmark', 'branch']
108 destupdatesteps = ['evolution', 'bookmark', 'branch']
109 # mapping to ease extension overriding steps.
109 # mapping to ease extension overriding steps.
110 destupdatestepmap = {'evolution': _destupdateobs,
110 destupdatestepmap = {'evolution': _destupdateobs,
111 'bookmark': _destupdatebook,
111 'bookmark': _destupdatebook,
112 'branch': _destupdatebranch,
112 'branch': _destupdatebranch,
113 }
113 }
114
114
115 def destupdate(repo, clean=False, check=False):
115 def destupdate(repo, clean=False, check=False):
116 """destination for bare update operation
116 """destination for bare update operation
117
117
118 return (rev, movemark, activemark)
118 return (rev, movemark, activemark)
119
119
120 - rev: the revision to update to,
120 - rev: the revision to update to,
121 - movemark: node to move the active bookmark from
121 - movemark: node to move the active bookmark from
122 (cf bookmark.calculate update),
122 (cf bookmark.calculate update),
123 - activemark: a bookmark to activate at the end of the update.
123 - activemark: a bookmark to activate at the end of the update.
124 """
124 """
125 node = movemark = activemark = None
125 node = movemark = activemark = None
126
126
127 for step in destupdatesteps:
127 for step in destupdatesteps:
128 node, movemark, activemark = destupdatestepmap[step](repo, clean, check)
128 node, movemark, activemark = destupdatestepmap[step](repo, clean, check)
129 if node is not None:
129 if node is not None:
130 break
130 break
131 rev = repo[node].rev()
131 rev = repo[node].rev()
132
132
133 _destupdatevalidate(repo, rev, clean, check)
133 _destupdatevalidate(repo, rev, clean, check)
134
134
135 return rev, movemark, activemark
135 return rev, movemark, activemark
136
136
137 msgdestmerge = {
137 msgdestmerge = {
138 # too many matching divergent bookmark
138 # too many matching divergent bookmark
139 'toomanybookmarks':
139 'toomanybookmarks':
140 {'merge':
140 {'merge':
141 (_("multiple matching bookmarks to merge -"
141 (_("multiple matching bookmarks to merge -"
142 " please merge with an explicit rev or bookmark"),
142 " please merge with an explicit rev or bookmark"),
143 _("run 'hg heads' to see all heads")),
143 _("run 'hg heads' to see all heads")),
144 },
144 },
145 # no other matching divergent bookmark
145 # no other matching divergent bookmark
146 'nootherbookmarks':
146 'nootherbookmarks':
147 {'merge':
147 {'merge':
148 (_("no matching bookmark to merge - "
148 (_("no matching bookmark to merge - "
149 "please merge with an explicit rev or bookmark"),
149 "please merge with an explicit rev or bookmark"),
150 _("run 'hg heads' to see all heads")),
150 _("run 'hg heads' to see all heads")),
151 },
151 },
152 # branch have too many unbookmarked heads, no obvious destination
152 # branch have too many unbookmarked heads, no obvious destination
153 'toomanyheads':
153 'toomanyheads':
154 {'merge':
154 {'merge':
155 (_("branch '%s' has %d heads - please merge with an explicit rev"),
155 (_("branch '%s' has %d heads - please merge with an explicit rev"),
156 _("run 'hg heads .' to see heads")),
156 _("run 'hg heads .' to see heads")),
157 },
157 },
158 # branch have no other unbookmarked heads
158 # branch have no other unbookmarked heads
159 'bookmarkedheads':
159 'bookmarkedheads':
160 {'merge':
160 {'merge':
161 (_("heads are bookmarked - please merge with an explicit rev"),
161 (_("heads are bookmarked - please merge with an explicit rev"),
162 _("run 'hg heads' to see all heads")),
162 _("run 'hg heads' to see all heads")),
163 },
163 },
164 # branch have just a single heads, but there is other branches
164 # branch have just a single heads, but there is other branches
165 'nootherbranchheads':
165 'nootherbranchheads':
166 {'merge':
166 {'merge':
167 (_("branch '%s' has one head - please merge with an explicit rev"),
167 (_("branch '%s' has one head - please merge with an explicit rev"),
168 _("run 'hg heads' to see all heads")),
168 _("run 'hg heads' to see all heads")),
169 },
169 },
170 # repository have a single head
170 # repository have a single head
171 'nootherheads':
171 'nootherheads':
172 {'merge':
172 {'merge':
173 (_('nothing to merge'),
173 (_('nothing to merge'),
174 None),
174 None),
175 },
175 },
176 # repository have a single head and we are not on it
176 # repository have a single head and we are not on it
177 'nootherheadsbehind':
177 'nootherheadsbehind':
178 {'merge':
178 {'merge':
179 (_('nothing to merge'),
179 (_('nothing to merge'),
180 _("use 'hg update' instead")),
180 _("use 'hg update' instead")),
181 },
181 },
182 # We are not on a head
182 # We are not on a head
183 'notatheads':
183 'notatheads':
184 {'merge':
184 {'merge':
185 (_('working directory not at a head revision'),
185 (_('working directory not at a head revision'),
186 _("use 'hg update' or merge with an explicit revision"))
186 _("use 'hg update' or merge with an explicit revision"))
187 },
187 },
188 'emptysourceset':
188 'emptysourceset':
189 {'merge':
189 {'merge':
190 (_('source set is empty'),
190 (_('source set is empty'),
191 None)
191 None)
192 },
192 },
193 'multiplebranchessourceset':
193 'multiplebranchessourceset':
194 {'merge':
194 {'merge':
195 (_('source set is rooted in multiple branches'),
195 (_('source set is rooted in multiple branches'),
196 None)
196 None)
197 },
197 },
198 }
198 }
199
199
200 def _destmergebook(repo, action='merge', sourceset=None):
200 def _destmergebook(repo, action='merge', sourceset=None):
201 """find merge destination in the active bookmark case"""
201 """find merge destination in the active bookmark case"""
202 node = None
202 node = None
203 bmheads = repo.bookmarkheads(repo._activebookmark)
203 bmheads = repo.bookmarkheads(repo._activebookmark)
204 curhead = repo[repo._activebookmark].node()
204 curhead = repo[repo._activebookmark].node()
205 if len(bmheads) == 2:
205 if len(bmheads) == 2:
206 if curhead == bmheads[0]:
206 if curhead == bmheads[0]:
207 node = bmheads[1]
207 node = bmheads[1]
208 else:
208 else:
209 node = bmheads[0]
209 node = bmheads[0]
210 elif len(bmheads) > 2:
210 elif len(bmheads) > 2:
211 msg, hint = msgdestmerge['toomanybookmarks'][action]
211 msg, hint = msgdestmerge['toomanybookmarks'][action]
212 raise error.ManyMergeDestAbort(msg, hint=hint)
212 raise error.ManyMergeDestAbort(msg, hint=hint)
213 elif len(bmheads) <= 1:
213 elif len(bmheads) <= 1:
214 msg, hint = msgdestmerge['nootherbookmarks'][action]
214 msg, hint = msgdestmerge['nootherbookmarks'][action]
215 raise error.NoMergeDestAbort(msg, hint=hint)
215 raise error.NoMergeDestAbort(msg, hint=hint)
216 assert node is not None
216 assert node is not None
217 return node
217 return node
218
218
219 def _destmergebranch(repo, action='merge', sourceset=None, onheadcheck=True):
219 def _destmergebranch(repo, action='merge', sourceset=None, onheadcheck=True):
220 """find merge destination based on branch heads"""
220 """find merge destination based on branch heads"""
221 node = None
221 node = None
222
222
223 if sourceset is None:
223 if sourceset is None:
224 sourceset = [repo[repo.dirstate.p1()].rev()]
224 sourceset = [repo[repo.dirstate.p1()].rev()]
225 branch = repo.dirstate.branch()
225 branch = repo.dirstate.branch()
226 elif not sourceset:
226 elif not sourceset:
227 msg, hint = msgdestmerge['emptysourceset'][action]
227 msg, hint = msgdestmerge['emptysourceset'][action]
228 raise error.NoMergeDestAbort(msg, hint=hint)
228 raise error.NoMergeDestAbort(msg, hint=hint)
229 else:
229 else:
230 branch = None
230 branch = None
231 for ctx in repo.set('roots(%ld::%ld)', sourceset, sourceset):
231 for ctx in repo.set('roots(%ld::%ld)', sourceset, sourceset):
232 if branch is not None and ctx.branch() != branch:
232 if branch is not None and ctx.branch() != branch:
233 msg, hint = msgdestmerge['multiplebranchessourceset'][action]
233 msg, hint = msgdestmerge['multiplebranchessourceset'][action]
234 raise error.ManyMergeDestAbort(msg, hint=hint)
234 raise error.ManyMergeDestAbort(msg, hint=hint)
235 branch = ctx.branch()
235 branch = ctx.branch()
236
236
237 bheads = repo.branchheads(branch)
237 bheads = repo.branchheads(branch)
238 if onheadcheck and not repo.revs('%ld and %ln', sourceset, bheads):
238 onhead = repo.revs('%ld and %ln', sourceset, bheads)
239 if onheadcheck and not onhead:
239 # Case A: working copy if not on a head. (merge only)
240 # Case A: working copy if not on a head. (merge only)
240 #
241 #
241 # This is probably a user mistake We bailout pointing at 'hg update'
242 # This is probably a user mistake We bailout pointing at 'hg update'
242 if len(repo.heads()) <= 1:
243 if len(repo.heads()) <= 1:
243 msg, hint = msgdestmerge['nootherheadsbehind'][action]
244 msg, hint = msgdestmerge['nootherheadsbehind'][action]
244 else:
245 else:
245 msg, hint = msgdestmerge['notatheads'][action]
246 msg, hint = msgdestmerge['notatheads'][action]
246 raise error.Abort(msg, hint=hint)
247 raise error.Abort(msg, hint=hint)
247 # remove heads descendants of source from the set
248 # remove heads descendants of source from the set
248 bheads = list(repo.revs('%ln - (%ld::)', bheads, sourceset))
249 bheads = list(repo.revs('%ln - (%ld::)', bheads, sourceset))
249 # filters out bookmarked heads
250 # filters out bookmarked heads
250 nbhs = list(repo.revs('%ld - bookmark()', bheads))
251 nbhs = list(repo.revs('%ld - bookmark()', bheads))
251 if len(nbhs) > 1:
252 if len(nbhs) > 1:
252 # Case B: There is more than 1 other anonymous heads
253 # Case B: There is more than 1 other anonymous heads
253 #
254 #
254 # This means that there will be more than 1 candidate. This is
255 # This means that there will be more than 1 candidate. This is
255 # ambiguous. We abort asking the user to pick as explicit destination
256 # ambiguous. We abort asking the user to pick as explicit destination
256 # instead.
257 # instead.
257 msg, hint = msgdestmerge['toomanyheads'][action]
258 msg, hint = msgdestmerge['toomanyheads'][action]
258 msg %= (branch, len(bheads) + 1)
259 msg %= (branch, len(bheads) + 1)
259 raise error.ManyMergeDestAbort(msg, hint=hint)
260 raise error.ManyMergeDestAbort(msg, hint=hint)
260 elif not nbhs:
261 elif not nbhs:
261 # Case B: There is no other anonymous heads
262 # Case B: There is no other anonymous heads
262 #
263 #
263 # This means that there is no natural candidate to merge with.
264 # This means that there is no natural candidate to merge with.
264 # We abort, with various messages for various cases.
265 # We abort, with various messages for various cases.
265 if bheads:
266 if bheads:
266 msg, hint = msgdestmerge['bookmarkedheads'][action]
267 msg, hint = msgdestmerge['bookmarkedheads'][action]
267 elif len(repo.heads()) > 1:
268 elif len(repo.heads()) > 1:
268 msg, hint = msgdestmerge['nootherbranchheads'][action]
269 msg, hint = msgdestmerge['nootherbranchheads'][action]
269 msg %= branch
270 msg %= branch
271 elif not onhead:
272 # if 'onheadcheck == False' (rebase case),
273 # this was not caught in Case A.
274 msg, hint = msgdestmerge['nootherheadsbehind'][action]
270 else:
275 else:
271 msg, hint = msgdestmerge['nootherheads'][action]
276 msg, hint = msgdestmerge['nootherheads'][action]
272 raise error.NoMergeDestAbort(msg, hint=hint)
277 raise error.NoMergeDestAbort(msg, hint=hint)
273 else:
278 else:
274 node = nbhs[0]
279 node = nbhs[0]
275 assert node is not None
280 assert node is not None
276 return node
281 return node
277
282
278 def destmerge(repo, action='merge', sourceset=None, onheadcheck=True):
283 def destmerge(repo, action='merge', sourceset=None, onheadcheck=True):
279 """return the default destination for a merge
284 """return the default destination for a merge
280
285
281 (or raise exception about why it can't pick one)
286 (or raise exception about why it can't pick one)
282
287
283 :action: the action being performed, controls emitted error message
288 :action: the action being performed, controls emitted error message
284 """
289 """
285 if repo._activebookmark:
290 if repo._activebookmark:
286 node = _destmergebook(repo, action=action, sourceset=sourceset)
291 node = _destmergebook(repo, action=action, sourceset=sourceset)
287 else:
292 else:
288 node = _destmergebranch(repo, action=action, sourceset=sourceset,
293 node = _destmergebranch(repo, action=action, sourceset=sourceset,
289 onheadcheck=onheadcheck)
294 onheadcheck=onheadcheck)
290 return repo[node].rev()
295 return repo[node].rev()
291
296
292 histeditdefaultrevset = 'reverse(only(.) and not public() and not ::merge())'
297 histeditdefaultrevset = 'reverse(only(.) and not public() and not ::merge())'
293
298
294 def desthistedit(ui, repo):
299 def desthistedit(ui, repo):
295 """Default base revision to edit for `hg histedit`."""
300 """Default base revision to edit for `hg histedit`."""
296 # Avoid cycle: scmutil -> revset -> destutil
301 # Avoid cycle: scmutil -> revset -> destutil
297 from . import scmutil
302 from . import scmutil
298
303
299 default = ui.config('histedit', 'defaultrev', histeditdefaultrevset)
304 default = ui.config('histedit', 'defaultrev', histeditdefaultrevset)
300 if default:
305 if default:
301 revs = scmutil.revrange(repo, [default])
306 revs = scmutil.revrange(repo, [default])
302 if revs:
307 if revs:
303 # The revset supplied by the user may not be in ascending order nor
308 # The revset supplied by the user may not be in ascending order nor
304 # take the first revision. So do this manually.
309 # take the first revision. So do this manually.
305 revs.sort()
310 revs.sort()
306 return revs.first()
311 return revs.first()
307
312
308 return None
313 return None
309
314
310 def _statusotherbook(ui, repo):
315 def _statusotherbook(ui, repo):
311 bmheads = repo.bookmarkheads(repo._activebookmark)
316 bmheads = repo.bookmarkheads(repo._activebookmark)
312 curhead = repo[repo._activebookmark].node()
317 curhead = repo[repo._activebookmark].node()
313 if repo.revs('%n and parents()', curhead):
318 if repo.revs('%n and parents()', curhead):
314 # we are on the active bookmark
319 # we are on the active bookmark
315 bmheads = [b for b in bmheads if curhead != b]
320 bmheads = [b for b in bmheads if curhead != b]
316 if bmheads:
321 if bmheads:
317 msg = _('%i other divergent bookmarks for "%s"\n')
322 msg = _('%i other divergent bookmarks for "%s"\n')
318 ui.status(msg % (len(bmheads), repo._activebookmark))
323 ui.status(msg % (len(bmheads), repo._activebookmark))
319
324
320 def _statusotherbranchheads(ui, repo):
325 def _statusotherbranchheads(ui, repo):
321 currentbranch = repo.dirstate.branch()
326 currentbranch = repo.dirstate.branch()
322 heads = repo.branchheads(currentbranch)
327 heads = repo.branchheads(currentbranch)
323 l = len(heads)
328 l = len(heads)
324 if repo.revs('%ln and parents()', heads):
329 if repo.revs('%ln and parents()', heads):
325 # we are on a head
330 # we are on a head
326 heads = repo.revs('%ln - parents()', heads)
331 heads = repo.revs('%ln - parents()', heads)
327 if heads and l != len(heads):
332 if heads and l != len(heads):
328 ui.status(_('%i other heads for branch "%s"\n') %
333 ui.status(_('%i other heads for branch "%s"\n') %
329 (len(heads), currentbranch))
334 (len(heads), currentbranch))
330
335
331 def statusotherdests(ui, repo):
336 def statusotherdests(ui, repo):
332 """Print message about other head"""
337 """Print message about other head"""
333 # XXX we should probably include a hint:
338 # XXX we should probably include a hint:
334 # - about what to do
339 # - about what to do
335 # - how to see such heads
340 # - how to see such heads
336 if repo._activebookmark:
341 if repo._activebookmark:
337 _statusotherbook(ui, repo)
342 _statusotherbook(ui, repo)
338 else:
343 else:
339 _statusotherbranchheads(ui, repo)
344 _statusotherbranchheads(ui, repo)
General Comments 0
You need to be logged in to leave comments. Login now