##// END OF EJS Templates
show: implement "stack" view...
Gregory Szorc -
r33194:c5a07a3a default
parent child Browse files
Show More
@@ -0,0 +1,220 b''
1 $ cat >> $HGRCPATH << EOF
2 > [extensions]
3 > show =
4 > EOF
5
6 $ hg init repo0
7 $ cd repo0
8
9 Empty repo / no checkout results in error
10
11 $ hg show stack
12 abort: stack view only available when there is a working directory
13 [255]
14
15 Stack displays single draft changeset as root revision
16
17 $ echo 0 > foo
18 $ hg -q commit -A -m 'commit 0'
19 $ hg show stack
20 @ 9f171 commit 0
21
22 Stack displays multiple draft changesets
23
24 $ echo 1 > foo
25 $ hg commit -m 'commit 1'
26 $ echo 2 > foo
27 $ hg commit -m 'commit 2'
28 $ echo 3 > foo
29 $ hg commit -m 'commit 3'
30 $ echo 4 > foo
31 $ hg commit -m 'commit 4'
32 $ hg show stack
33 @ 2737b commit 4
34 o d1a69 commit 3
35 o 128c8 commit 2
36 o 181cc commit 1
37 o 9f171 commit 0
38
39 Public parent of draft base is displayed, separated from stack
40
41 $ hg phase --public -r 0
42 $ hg show stack
43 @ 2737b commit 4
44 o d1a69 commit 3
45 o 128c8 commit 2
46 o 181cc commit 1
47 / (stack base)
48 o 9f171 commit 0
49
50 $ hg phase --public -r 1
51 $ hg show stack
52 @ 2737b commit 4
53 o d1a69 commit 3
54 o 128c8 commit 2
55 / (stack base)
56 o 181cc commit 1
57
58 Draft descendants are shown
59
60 $ hg -q up 2
61 $ hg show stack
62 o 2737b commit 4
63 o d1a69 commit 3
64 @ 128c8 commit 2
65 / (stack base)
66 o 181cc commit 1
67
68 $ hg -q up 3
69 $ hg show stack
70 o 2737b commit 4
71 @ d1a69 commit 3
72 o 128c8 commit 2
73 / (stack base)
74 o 181cc commit 1
75
76 working dir on public changeset should display special message
77
78 $ hg -q up 1
79 $ hg show stack
80 (empty stack; working directory is a published changeset)
81
82 Branch point in descendants displayed at top of graph
83
84 $ hg -q up 3
85 $ echo b > foo
86 $ hg commit -m 'commit 5 (new dag branch)'
87 created new head
88 $ hg -q up 2
89 $ hg show stack
90 \ / (multiple children)
91 |
92 o d1a69 commit 3
93 @ 128c8 commit 2
94 / (stack base)
95 o 181cc commit 1
96
97 $ cd ..
98
99 Base is stopped at merges
100
101 $ hg init merge-base
102 $ cd merge-base
103 $ echo 0 > foo
104 $ hg -q commit -A -m initial
105 $ echo h1 > foo
106 $ hg commit -m 'head 1'
107 $ hg -q up 0
108 $ echo h2 > foo
109 $ hg -q commit -m 'head 2'
110 $ hg phase --public -r 0:tip
111 $ hg -q up 1
112 $ hg merge -t :local 2
113 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
114 (branch merge, don't forget to commit)
115 $ hg commit -m 'merge heads'
116
117 TODO doesn't yet handle case where wdir is a draft merge
118
119 $ hg show stack
120 @ 8ee90 merge heads
121 / (stack base)
122 o 59478 head 1
123
124 $ echo d1 > foo
125 $ hg commit -m 'draft 1'
126 $ echo d2 > foo
127 $ hg commit -m 'draft 2'
128
129 $ hg show stack
130 @ 430d5 draft 2
131 o 787b1 draft 1
132 / (stack base)
133 o 8ee90 merge heads
134
135 $ cd ..
136
137 Now move on to stacks when there are more commits after the base branchpoint
138
139 $ hg init public-rebase
140 $ cd public-rebase
141 $ echo 0 > foo
142 $ hg -q commit -A -m 'base'
143 $ hg phase --public -r .
144 $ echo d1 > foo
145 $ hg commit -m 'draft 1'
146 $ echo d2 > foo
147 $ hg commit -m 'draft 2'
148 $ hg -q up 0
149 $ echo 1 > foo
150 $ hg commit -m 'new 1'
151 created new head
152 $ echo 2 > foo
153 $ hg commit -m 'new 2'
154 $ hg -q up 2
155
156 Newer draft heads don't impact output
157
158 $ hg show stack
159 @ eaffc draft 2
160 o 2b218 draft 1
161 / (stack base)
162 o b66bb base
163
164 Newer public heads are rendered
165
166 $ hg phase --public -r '::tip'
167
168 $ hg show stack
169 o baa4b new 2
170 / (2 commits ahead)
171 :
172 : (stack head)
173 : @ eaffc draft 2
174 : o 2b218 draft 1
175 :/ (stack base)
176 o b66bb base
177
178 If rebase is available, we show a hint how to rebase to that head
179
180 $ hg --config extensions.rebase= show stack
181 o baa4b new 2
182 / (2 commits ahead; hg rebase --source 2b218 --dest baa4b)
183 :
184 : (stack head)
185 : @ eaffc draft 2
186 : o 2b218 draft 1
187 :/ (stack base)
188 o b66bb base
189
190 Similar tests but for multiple heads
191
192 $ hg -q up 0
193 $ echo h2 > foo
194 $ hg -q commit -m 'new head 2'
195 $ hg phase --public -r .
196 $ hg -q up 2
197
198 $ hg show stack
199 o baa4b new 2
200 / (2 commits ahead)
201 : o 9a848 new head 2
202 :/ (1 commits ahead)
203 :
204 : (stack head)
205 : @ eaffc draft 2
206 : o 2b218 draft 1
207 :/ (stack base)
208 o b66bb base
209
210 $ hg --config extensions.rebase= show stack
211 o baa4b new 2
212 / (2 commits ahead; hg rebase --source 2b218 --dest baa4b)
213 : o 9a848 new head 2
214 :/ (1 commits ahead; hg rebase --source 2b218 --dest 9a848)
215 :
216 : (stack head)
217 : @ eaffc draft 2
218 : o 2b218 draft 1
219 :/ (stack base)
220 o b66bb base
@@ -1,270 +1,432 b''
1 1 # show.py - Extension implementing `hg show`
2 2 #
3 3 # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """unified command to show various repository information (EXPERIMENTAL)
9 9
10 10 This extension provides the :hg:`show` command, which provides a central
11 11 command for displaying commonly-accessed repository data and views of that
12 12 data.
13 13
14 14 The following config options can influence operation.
15 15
16 16 ``commands``
17 17 ------------
18 18
19 19 ``show.aliasprefix``
20 20 List of strings that will register aliases for views. e.g. ``s`` will
21 21 effectively set config options ``alias.s<view> = show <view>`` for all
22 22 views. i.e. `hg swork` would execute `hg show work`.
23 23
24 24 Aliases that would conflict with existing registrations will not be
25 25 performed.
26 26 """
27 27
28 28 from __future__ import absolute_import
29 29
30 30 from mercurial.i18n import _
31 31 from mercurial.node import nullrev
32 32 from mercurial import (
33 33 cmdutil,
34 34 commands,
35 destutil,
35 36 error,
36 37 formatter,
37 38 graphmod,
39 phases,
38 40 pycompat,
39 41 registrar,
40 42 revset,
41 43 revsetlang,
42 44 )
43 45
44 46 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
45 47 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 48 # be specifying the version(s) of Mercurial they are tested with, or
47 49 # leave the attribute unspecified.
48 50 testedwith = 'ships-with-hg-core'
49 51
50 52 cmdtable = {}
51 53 command = registrar.command(cmdtable)
52 54 revsetpredicate = registrar.revsetpredicate()
53 55
54 56 class showcmdfunc(registrar._funcregistrarbase):
55 57 """Register a function to be invoked for an `hg show <thing>`."""
56 58
57 59 # Used by _formatdoc().
58 60 _docformat = '%s -- %s'
59 61
60 62 def _extrasetup(self, name, func, fmtopic=None, csettopic=None):
61 63 """Called with decorator arguments to register a show view.
62 64
63 65 ``name`` is the sub-command name.
64 66
65 67 ``func`` is the function being decorated.
66 68
67 69 ``fmtopic`` is the topic in the style that will be rendered for
68 70 this view.
69 71
70 72 ``csettopic`` is the topic in the style to be used for a changeset
71 73 printer.
72 74
73 75 If ``fmtopic`` is specified, the view function will receive a
74 76 formatter instance. If ``csettopic`` is specified, the view
75 77 function will receive a changeset printer.
76 78 """
77 79 func._fmtopic = fmtopic
78 80 func._csettopic = csettopic
79 81
80 82 showview = showcmdfunc()
81 83
82 84 @command('show', [
83 85 # TODO: Switch this template flag to use cmdutil.formatteropts if
84 86 # 'hg show' becomes stable before --template/-T is stable. For now,
85 87 # we are putting it here without the '(EXPERIMENTAL)' flag because it
86 88 # is an important part of the 'hg show' user experience and the entire
87 89 # 'hg show' experience is experimental.
88 90 ('T', 'template', '', ('display with template'), _('TEMPLATE')),
89 91 ], _('VIEW'))
90 92 def show(ui, repo, view=None, template=None):
91 93 """show various repository information
92 94
93 95 A requested view of repository data is displayed.
94 96
95 97 If no view is requested, the list of available views is shown and the
96 98 command aborts.
97 99
98 100 .. note::
99 101
100 102 There are no backwards compatibility guarantees for the output of this
101 103 command. Output may change in any future Mercurial release.
102 104
103 105 Consumers wanting stable command output should specify a template via
104 106 ``-T/--template``.
105 107
106 108 List of available views:
107 109 """
108 110 if ui.plain() and not template:
109 111 hint = _('invoke with -T/--template to control output format')
110 112 raise error.Abort(_('must specify a template in plain mode'), hint=hint)
111 113
112 114 views = showview._table
113 115
114 116 if not view:
115 117 ui.pager('show')
116 118 # TODO consider using formatter here so available views can be
117 119 # rendered to custom format.
118 120 ui.write(_('available views:\n'))
119 121 ui.write('\n')
120 122
121 123 for name, func in sorted(views.items()):
122 124 ui.write(('%s\n') % func.__doc__)
123 125
124 126 ui.write('\n')
125 127 raise error.Abort(_('no view requested'),
126 128 hint=_('use "hg show VIEW" to choose a view'))
127 129
128 130 # TODO use same logic as dispatch to perform prefix matching.
129 131 if view not in views:
130 132 raise error.Abort(_('unknown view: %s') % view,
131 133 hint=_('run "hg show" to see available views'))
132 134
133 135 template = template or 'show'
134 136
135 137 fn = views[view]
136 138 ui.pager('show')
137 139
138 140 if fn._fmtopic:
139 141 fmtopic = 'show%s' % fn._fmtopic
140 142 with ui.formatter(fmtopic, {'template': template}) as fm:
141 143 return fn(ui, repo, fm)
142 144 elif fn._csettopic:
143 145 ref = 'show%s' % fn._csettopic
144 146 spec = formatter.lookuptemplate(ui, ref, template)
145 147 displayer = cmdutil.changeset_templater(ui, repo, spec, buffered=True)
146 148 return fn(ui, repo, displayer)
147 149 else:
148 150 return fn(ui, repo)
149 151
150 152 @showview('bookmarks', fmtopic='bookmarks')
151 153 def showbookmarks(ui, repo, fm):
152 154 """bookmarks and their associated changeset"""
153 155 marks = repo._bookmarks
154 156 if not len(marks):
155 157 # This is a bit hacky. Ideally, templates would have a way to
156 158 # specify an empty output, but we shouldn't corrupt JSON while
157 159 # waiting for this functionality.
158 160 if not isinstance(fm, formatter.jsonformatter):
159 161 ui.write(_('(no bookmarks set)\n'))
160 162 return
161 163
162 164 active = repo._activebookmark
163 165 longestname = max(len(b) for b in marks)
164 166 # TODO consider exposing longest shortest(node).
165 167
166 168 for bm, node in sorted(marks.items()):
167 169 fm.startitem()
168 170 fm.context(ctx=repo[node])
169 171 fm.write('bookmark', '%s', bm)
170 172 fm.write('node', fm.hexfunc(node), fm.hexfunc(node))
171 173 fm.data(active=bm == active,
172 174 longestbookmarklen=longestname)
173 175
176 @showview('stack', csettopic='stack')
177 def showstack(ui, repo, displayer):
178 """current line of work"""
179 wdirctx = repo['.']
180 if wdirctx.rev() == nullrev:
181 raise error.Abort(_('stack view only available when there is a '
182 'working directory'))
183
184 if wdirctx.phase() == phases.public:
185 ui.write(_('(empty stack; working directory is a published '
186 'changeset)\n'))
187 return
188
189 # TODO extract "find stack" into a function to facilitate
190 # customization and reuse.
191
192 baserev = destutil.stackbase(ui, repo)
193 basectx = None
194
195 if baserev is None:
196 baserev = wdirctx.rev()
197 stackrevs = {wdirctx.rev()}
198 else:
199 stackrevs = set(repo.revs('%d::.', baserev))
200
201 ctx = repo[baserev]
202 if ctx.p1().rev() != nullrev:
203 basectx = ctx.p1()
204
205 # And relevant descendants.
206 branchpointattip = False
207 cl = repo.changelog
208
209 for rev in cl.descendants([wdirctx.rev()]):
210 ctx = repo[rev]
211
212 # Will only happen if . is public.
213 if ctx.phase() == phases.public:
214 break
215
216 stackrevs.add(ctx.rev())
217
218 if len(ctx.children()) > 1:
219 branchpointattip = True
220 break
221
222 stackrevs = list(reversed(sorted(stackrevs)))
223
224 # Find likely target heads for the current stack. These are likely
225 # merge or rebase targets.
226 if basectx:
227 # TODO make this customizable?
228 newheads = set(repo.revs('heads(%d::) - %ld - not public()',
229 basectx.rev(), stackrevs))
230 else:
231 newheads = set()
232
233 try:
234 cmdutil.findcmd('rebase', commands.table)
235 haverebase = True
236 except error.UnknownCommand:
237 haverebase = False
238
239 # TODO use templating.
240 # TODO consider using graphmod. But it may not be necessary given
241 # our simplicity and the customizations required.
242 # TODO use proper graph symbols from graphmod
243
244 shortesttmpl = formatter.maketemplater(ui, '{shortest(node, 5)}')
245 def shortest(ctx):
246 return shortesttmpl.render({'ctx': ctx, 'node': ctx.hex()})
247
248 # We write out new heads to aid in DAG awareness and to help with decision
249 # making on how the stack should be reconciled with commits made since the
250 # branch point.
251 if newheads:
252 # Calculate distance from base so we can render the count and so we can
253 # sort display order by commit distance.
254 revdistance = {}
255 for head in newheads:
256 # There is some redundancy in DAG traversal here and therefore
257 # room to optimize.
258 ancestors = cl.ancestors([head], stoprev=basectx.rev())
259 revdistance[head] = len(list(ancestors))
260
261 sourcectx = repo[stackrevs[-1]]
262
263 sortedheads = sorted(newheads, key=lambda x: revdistance[x],
264 reverse=True)
265
266 for i, rev in enumerate(sortedheads):
267 ctx = repo[rev]
268
269 if i:
270 ui.write(': ')
271 else:
272 ui.write(' ')
273
274 ui.write(('o '))
275 displayer.show(ctx)
276 displayer.flush(ctx)
277 ui.write('\n')
278
279 if i:
280 ui.write(':/')
281 else:
282 ui.write(' /')
283
284 ui.write(' (')
285 ui.write(_('%d commits ahead') % revdistance[rev],
286 label='stack.commitdistance')
287
288 if haverebase:
289 # TODO may be able to omit --source in some scenarios
290 ui.write('; ')
291 ui.write(('hg rebase --source %s --dest %s' % (
292 shortest(sourcectx), shortest(ctx))),
293 label='stack.rebasehint')
294
295 ui.write(')\n')
296
297 ui.write(':\n: ')
298 ui.write(_('(stack head)\n'), label='stack.label')
299
300 if branchpointattip:
301 ui.write(' \\ / ')
302 ui.write(_('(multiple children)\n'), label='stack.label')
303 ui.write(' |\n')
304
305 for rev in stackrevs:
306 ctx = repo[rev]
307 symbol = '@' if rev == wdirctx.rev() else 'o'
308
309 if newheads:
310 ui.write(': ')
311 else:
312 ui.write(' ')
313
314 ui.write(symbol, ' ')
315 displayer.show(ctx)
316 displayer.flush(ctx)
317 ui.write('\n')
318
319 # TODO display histedit hint?
320
321 if basectx:
322 # Vertically and horizontally separate stack base from parent
323 # to reinforce stack boundary.
324 if newheads:
325 ui.write(':/ ')
326 else:
327 ui.write(' / ')
328
329 ui.write(_('(stack base)'), '\n', label='stack.label')
330 ui.write(('o '))
331
332 displayer.show(basectx)
333 displayer.flush(basectx)
334 ui.write('\n')
335
174 336 @revsetpredicate('_underway([commitage[, headage]])')
175 337 def underwayrevset(repo, subset, x):
176 338 args = revset.getargsdict(x, 'underway', 'commitage headage')
177 339 if 'commitage' not in args:
178 340 args['commitage'] = None
179 341 if 'headage' not in args:
180 342 args['headage'] = None
181 343
182 344 # We assume callers of this revset add a topographical sort on the
183 345 # result. This means there is no benefit to making the revset lazy
184 346 # since the topographical sort needs to consume all revs.
185 347 #
186 348 # With this in mind, we build up the set manually instead of constructing
187 349 # a complex revset. This enables faster execution.
188 350
189 351 # Mutable changesets (non-public) are the most important changesets
190 352 # to return. ``not public()`` will also pull in obsolete changesets if
191 353 # there is a non-obsolete changeset with obsolete ancestors. This is
192 354 # why we exclude obsolete changesets from this query.
193 355 rs = 'not public() and not obsolete()'
194 356 rsargs = []
195 357 if args['commitage']:
196 358 rs += ' and date(%s)'
197 359 rsargs.append(revsetlang.getstring(args['commitage'],
198 360 _('commitage requires a string')))
199 361
200 362 mutable = repo.revs(rs, *rsargs)
201 363 relevant = revset.baseset(mutable)
202 364
203 365 # Add parents of mutable changesets to provide context.
204 366 relevant += repo.revs('parents(%ld)', mutable)
205 367
206 368 # We also pull in (public) heads if they a) aren't closing a branch
207 369 # b) are recent.
208 370 rs = 'head() and not closed()'
209 371 rsargs = []
210 372 if args['headage']:
211 373 rs += ' and date(%s)'
212 374 rsargs.append(revsetlang.getstring(args['headage'],
213 375 _('headage requires a string')))
214 376
215 377 relevant += repo.revs(rs, *rsargs)
216 378
217 379 # Add working directory parent.
218 380 wdirrev = repo['.'].rev()
219 381 if wdirrev != nullrev:
220 382 relevant += revset.baseset({wdirrev})
221 383
222 384 return subset & relevant
223 385
224 386 @showview('work', csettopic='work')
225 387 def showwork(ui, repo, displayer):
226 388 """changesets that aren't finished"""
227 389 # TODO support date-based limiting when calling revset.
228 390 revs = repo.revs('sort(_underway(), topo)')
229 391
230 392 revdag = graphmod.dagwalker(repo, revs)
231 393
232 394 ui.setconfig('experimental', 'graphshorten', True)
233 395 cmdutil.displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges)
234 396
235 397 def extsetup(ui):
236 398 # Alias `hg <prefix><view>` to `hg show <view>`.
237 399 for prefix in ui.configlist('commands', 'show.aliasprefix'):
238 400 for view in showview._table:
239 401 name = '%s%s' % (prefix, view)
240 402
241 403 choice, allcommands = cmdutil.findpossible(name, commands.table,
242 404 strict=True)
243 405
244 406 # This alias is already a command name. Don't set it.
245 407 if name in choice:
246 408 continue
247 409
248 410 # Same for aliases.
249 411 if ui.config('alias', name):
250 412 continue
251 413
252 414 ui.setconfig('alias', name, 'show %s' % view, source='show')
253 415
254 416 # Adjust the docstring of the show command so it shows all registered views.
255 417 # This is a bit hacky because it runs at the end of module load. When moved
256 418 # into core or when another extension wants to provide a view, we'll need
257 419 # to do this more robustly.
258 420 # TODO make this more robust.
259 421 def _updatedocstring():
260 422 longest = max(map(len, showview._table.keys()))
261 423 entries = []
262 424 for key in sorted(showview._table.keys()):
263 425 entries.append(pycompat.sysstr(' %s %s' % (
264 426 key.ljust(longest), showview._table[key]._origdoc)))
265 427
266 428 cmdtable['show'][0].__doc__ = pycompat.sysstr('%s\n\n%s\n ') % (
267 429 cmdtable['show'][0].__doc__.rstrip(),
268 430 pycompat.sysstr('\n\n').join(entries))
269 431
270 432 _updatedocstring()
@@ -1,408 +1,414 b''
1 1 # destutil.py - Mercurial utility function for command destination
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com> and other
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from . import (
12 12 bookmarks,
13 13 error,
14 14 obsutil,
15 15 scmutil,
16 16 )
17 17
18 18 def _destupdateobs(repo, clean):
19 19 """decide of an update destination from obsolescence markers"""
20 20 node = None
21 21 wc = repo[None]
22 22 p1 = wc.p1()
23 23 movemark = None
24 24
25 25 if p1.obsolete() and not p1.children():
26 26 # allow updating to successors
27 27 successors = obsutil.successorssets(repo, p1.node())
28 28
29 29 # behavior of certain cases is as follows,
30 30 #
31 31 # divergent changesets: update to highest rev, similar to what
32 32 # is currently done when there are more than one head
33 33 # (i.e. 'tip')
34 34 #
35 35 # replaced changesets: same as divergent except we know there
36 36 # is no conflict
37 37 #
38 38 # pruned changeset: no update is done; though, we could
39 39 # consider updating to the first non-obsolete parent,
40 40 # similar to what is current done for 'hg prune'
41 41
42 42 if successors:
43 43 # flatten the list here handles both divergent (len > 1)
44 44 # and the usual case (len = 1)
45 45 successors = [n for sub in successors for n in sub]
46 46
47 47 # get the max revision for the given successors set,
48 48 # i.e. the 'tip' of a set
49 49 node = repo.revs('max(%ln)', successors).first()
50 50 if bookmarks.isactivewdirparent(repo):
51 51 movemark = repo['.'].node()
52 52 return node, movemark, None
53 53
54 54 def _destupdatebook(repo, clean):
55 55 """decide on an update destination from active bookmark"""
56 56 # we also move the active bookmark, if any
57 57 activemark = None
58 58 node, movemark = bookmarks.calculateupdate(repo.ui, repo, None)
59 59 if node is not None:
60 60 activemark = node
61 61 return node, movemark, activemark
62 62
63 63 def _destupdatebranch(repo, clean):
64 64 """decide on an update destination from current branch
65 65
66 66 This ignores closed branch heads.
67 67 """
68 68 wc = repo[None]
69 69 movemark = node = None
70 70 currentbranch = wc.branch()
71 71
72 72 if clean:
73 73 currentbranch = repo['.'].branch()
74 74
75 75 if currentbranch in repo.branchmap():
76 76 heads = repo.branchheads(currentbranch)
77 77 if heads:
78 78 node = repo.revs('max(.::(%ln))', heads).first()
79 79 if bookmarks.isactivewdirparent(repo):
80 80 movemark = repo['.'].node()
81 81 elif currentbranch == 'default' and not wc.p1():
82 82 # "null" parent belongs to "default" branch, but it doesn't exist, so
83 83 # update to the tipmost non-closed branch head
84 84 node = repo.revs('max(head() and not closed())').first()
85 85 else:
86 86 node = repo['.'].node()
87 87 return node, movemark, None
88 88
89 89 def _destupdatebranchfallback(repo, clean):
90 90 """decide on an update destination from closed heads in current branch"""
91 91 wc = repo[None]
92 92 currentbranch = wc.branch()
93 93 movemark = None
94 94 if currentbranch in repo.branchmap():
95 95 # here, all descendant branch heads are closed
96 96 heads = repo.branchheads(currentbranch, closed=True)
97 97 assert heads, "any branch has at least one head"
98 98 node = repo.revs('max(.::(%ln))', heads).first()
99 99 assert node is not None, ("any revision has at least "
100 100 "one descendant branch head")
101 101 if bookmarks.isactivewdirparent(repo):
102 102 movemark = repo['.'].node()
103 103 else:
104 104 # here, no "default" branch, and all branches are closed
105 105 node = repo.lookup('tip')
106 106 assert node is not None, "'tip' exists even in empty repository"
107 107 return node, movemark, None
108 108
109 109 # order in which each step should be evaluated
110 110 # steps are run until one finds a destination
111 111 destupdatesteps = ['evolution', 'bookmark', 'branch', 'branchfallback']
112 112 # mapping to ease extension overriding steps.
113 113 destupdatestepmap = {'evolution': _destupdateobs,
114 114 'bookmark': _destupdatebook,
115 115 'branch': _destupdatebranch,
116 116 'branchfallback': _destupdatebranchfallback,
117 117 }
118 118
119 119 def destupdate(repo, clean=False):
120 120 """destination for bare update operation
121 121
122 122 return (rev, movemark, activemark)
123 123
124 124 - rev: the revision to update to,
125 125 - movemark: node to move the active bookmark from
126 126 (cf bookmark.calculate update),
127 127 - activemark: a bookmark to activate at the end of the update.
128 128 """
129 129 node = movemark = activemark = None
130 130
131 131 for step in destupdatesteps:
132 132 node, movemark, activemark = destupdatestepmap[step](repo, clean)
133 133 if node is not None:
134 134 break
135 135 rev = repo[node].rev()
136 136
137 137 return rev, movemark, activemark
138 138
139 139 msgdestmerge = {
140 140 # too many matching divergent bookmark
141 141 'toomanybookmarks':
142 142 {'merge':
143 143 (_("multiple matching bookmarks to merge -"
144 144 " please merge with an explicit rev or bookmark"),
145 145 _("run 'hg heads' to see all heads")),
146 146 'rebase':
147 147 (_("multiple matching bookmarks to rebase -"
148 148 " please rebase to an explicit rev or bookmark"),
149 149 _("run 'hg heads' to see all heads")),
150 150 },
151 151 # no other matching divergent bookmark
152 152 'nootherbookmarks':
153 153 {'merge':
154 154 (_("no matching bookmark to merge - "
155 155 "please merge with an explicit rev or bookmark"),
156 156 _("run 'hg heads' to see all heads")),
157 157 'rebase':
158 158 (_("no matching bookmark to rebase - "
159 159 "please rebase to an explicit rev or bookmark"),
160 160 _("run 'hg heads' to see all heads")),
161 161 },
162 162 # branch have too many unbookmarked heads, no obvious destination
163 163 'toomanyheads':
164 164 {'merge':
165 165 (_("branch '%s' has %d heads - please merge with an explicit rev"),
166 166 _("run 'hg heads .' to see heads")),
167 167 'rebase':
168 168 (_("branch '%s' has %d heads - please rebase to an explicit rev"),
169 169 _("run 'hg heads .' to see heads")),
170 170 },
171 171 # branch have no other unbookmarked heads
172 172 'bookmarkedheads':
173 173 {'merge':
174 174 (_("heads are bookmarked - please merge with an explicit rev"),
175 175 _("run 'hg heads' to see all heads")),
176 176 'rebase':
177 177 (_("heads are bookmarked - please rebase to an explicit rev"),
178 178 _("run 'hg heads' to see all heads")),
179 179 },
180 180 # branch have just a single heads, but there is other branches
181 181 'nootherbranchheads':
182 182 {'merge':
183 183 (_("branch '%s' has one head - please merge with an explicit rev"),
184 184 _("run 'hg heads' to see all heads")),
185 185 'rebase':
186 186 (_("branch '%s' has one head - please rebase to an explicit rev"),
187 187 _("run 'hg heads' to see all heads")),
188 188 },
189 189 # repository have a single head
190 190 'nootherheads':
191 191 {'merge':
192 192 (_('nothing to merge'),
193 193 None),
194 194 'rebase':
195 195 (_('nothing to rebase'),
196 196 None),
197 197 },
198 198 # repository have a single head and we are not on it
199 199 'nootherheadsbehind':
200 200 {'merge':
201 201 (_('nothing to merge'),
202 202 _("use 'hg update' instead")),
203 203 'rebase':
204 204 (_('nothing to rebase'),
205 205 _("use 'hg update' instead")),
206 206 },
207 207 # We are not on a head
208 208 'notatheads':
209 209 {'merge':
210 210 (_('working directory not at a head revision'),
211 211 _("use 'hg update' or merge with an explicit revision")),
212 212 'rebase':
213 213 (_('working directory not at a head revision'),
214 214 _("use 'hg update' or rebase to an explicit revision"))
215 215 },
216 216 'emptysourceset':
217 217 {'merge':
218 218 (_('source set is empty'),
219 219 None),
220 220 'rebase':
221 221 (_('source set is empty'),
222 222 None),
223 223 },
224 224 'multiplebranchessourceset':
225 225 {'merge':
226 226 (_('source set is rooted in multiple branches'),
227 227 None),
228 228 'rebase':
229 229 (_('rebaseset is rooted in multiple named branches'),
230 230 _('specify an explicit destination with --dest')),
231 231 },
232 232 }
233 233
234 234 def _destmergebook(repo, action='merge', sourceset=None, destspace=None):
235 235 """find merge destination in the active bookmark case"""
236 236 node = None
237 237 bmheads = bookmarks.headsforactive(repo)
238 238 curhead = repo[repo._activebookmark].node()
239 239 if len(bmheads) == 2:
240 240 if curhead == bmheads[0]:
241 241 node = bmheads[1]
242 242 else:
243 243 node = bmheads[0]
244 244 elif len(bmheads) > 2:
245 245 msg, hint = msgdestmerge['toomanybookmarks'][action]
246 246 raise error.ManyMergeDestAbort(msg, hint=hint)
247 247 elif len(bmheads) <= 1:
248 248 msg, hint = msgdestmerge['nootherbookmarks'][action]
249 249 raise error.NoMergeDestAbort(msg, hint=hint)
250 250 assert node is not None
251 251 return node
252 252
253 253 def _destmergebranch(repo, action='merge', sourceset=None, onheadcheck=True,
254 254 destspace=None):
255 255 """find merge destination based on branch heads"""
256 256 node = None
257 257
258 258 if sourceset is None:
259 259 sourceset = [repo[repo.dirstate.p1()].rev()]
260 260 branch = repo.dirstate.branch()
261 261 elif not sourceset:
262 262 msg, hint = msgdestmerge['emptysourceset'][action]
263 263 raise error.NoMergeDestAbort(msg, hint=hint)
264 264 else:
265 265 branch = None
266 266 for ctx in repo.set('roots(%ld::%ld)', sourceset, sourceset):
267 267 if branch is not None and ctx.branch() != branch:
268 268 msg, hint = msgdestmerge['multiplebranchessourceset'][action]
269 269 raise error.ManyMergeDestAbort(msg, hint=hint)
270 270 branch = ctx.branch()
271 271
272 272 bheads = repo.branchheads(branch)
273 273 onhead = repo.revs('%ld and %ln', sourceset, bheads)
274 274 if onheadcheck and not onhead:
275 275 # Case A: working copy if not on a head. (merge only)
276 276 #
277 277 # This is probably a user mistake We bailout pointing at 'hg update'
278 278 if len(repo.heads()) <= 1:
279 279 msg, hint = msgdestmerge['nootherheadsbehind'][action]
280 280 else:
281 281 msg, hint = msgdestmerge['notatheads'][action]
282 282 raise error.Abort(msg, hint=hint)
283 283 # remove heads descendants of source from the set
284 284 bheads = list(repo.revs('%ln - (%ld::)', bheads, sourceset))
285 285 # filters out bookmarked heads
286 286 nbhs = list(repo.revs('%ld - bookmark()', bheads))
287 287
288 288 if destspace is not None:
289 289 # restrict search space
290 290 # used in the 'hg pull --rebase' case, see issue 5214.
291 291 nbhs = list(repo.revs('%ld and %ld', destspace, nbhs))
292 292
293 293 if len(nbhs) > 1:
294 294 # Case B: There is more than 1 other anonymous heads
295 295 #
296 296 # This means that there will be more than 1 candidate. This is
297 297 # ambiguous. We abort asking the user to pick as explicit destination
298 298 # instead.
299 299 msg, hint = msgdestmerge['toomanyheads'][action]
300 300 msg %= (branch, len(bheads) + 1)
301 301 raise error.ManyMergeDestAbort(msg, hint=hint)
302 302 elif not nbhs:
303 303 # Case B: There is no other anonymous heads
304 304 #
305 305 # This means that there is no natural candidate to merge with.
306 306 # We abort, with various messages for various cases.
307 307 if bheads:
308 308 msg, hint = msgdestmerge['bookmarkedheads'][action]
309 309 elif len(repo.heads()) > 1:
310 310 msg, hint = msgdestmerge['nootherbranchheads'][action]
311 311 msg %= branch
312 312 elif not onhead:
313 313 # if 'onheadcheck == False' (rebase case),
314 314 # this was not caught in Case A.
315 315 msg, hint = msgdestmerge['nootherheadsbehind'][action]
316 316 else:
317 317 msg, hint = msgdestmerge['nootherheads'][action]
318 318 raise error.NoMergeDestAbort(msg, hint=hint)
319 319 else:
320 320 node = nbhs[0]
321 321 assert node is not None
322 322 return node
323 323
324 324 def destmerge(repo, action='merge', sourceset=None, onheadcheck=True,
325 325 destspace=None):
326 326 """return the default destination for a merge
327 327
328 328 (or raise exception about why it can't pick one)
329 329
330 330 :action: the action being performed, controls emitted error message
331 331 """
332 332 # destspace is here to work around issues with `hg pull --rebase` see
333 333 # issue5214 for details
334 334 if repo._activebookmark:
335 335 node = _destmergebook(repo, action=action, sourceset=sourceset,
336 336 destspace=destspace)
337 337 else:
338 338 node = _destmergebranch(repo, action=action, sourceset=sourceset,
339 339 onheadcheck=onheadcheck, destspace=destspace)
340 340 return repo[node].rev()
341 341
342 342 histeditdefaultrevset = 'reverse(only(.) and not public() and not ::merge())'
343 343
344 344 def desthistedit(ui, repo):
345 345 """Default base revision to edit for `hg histedit`."""
346 346 default = ui.config('histedit', 'defaultrev', histeditdefaultrevset)
347 347 if default:
348 348 revs = scmutil.revrange(repo, [default])
349 349 if revs:
350 350 # The revset supplied by the user may not be in ascending order nor
351 351 # take the first revision. So do this manually.
352 352 revs.sort()
353 353 return revs.first()
354 354
355 355 return None
356 356
357 def stackbase(ui, repo):
358 # The histedit default base stops at public changesets, branchpoints,
359 # and merges, which is exactly what we want for a stack.
360 revs = scmutil.revrange(repo, [histeditdefaultrevset])
361 return revs.last() if revs else None
362
357 363 def _statusotherbook(ui, repo):
358 364 bmheads = bookmarks.headsforactive(repo)
359 365 curhead = repo[repo._activebookmark].node()
360 366 if repo.revs('%n and parents()', curhead):
361 367 # we are on the active bookmark
362 368 bmheads = [b for b in bmheads if curhead != b]
363 369 if bmheads:
364 370 msg = _('%i other divergent bookmarks for "%s"\n')
365 371 ui.status(msg % (len(bmheads), repo._activebookmark))
366 372
367 373 def _statusotherbranchheads(ui, repo):
368 374 currentbranch = repo.dirstate.branch()
369 375 allheads = repo.branchheads(currentbranch, closed=True)
370 376 heads = repo.branchheads(currentbranch)
371 377 if repo.revs('%ln and parents()', allheads):
372 378 # we are on a head, even though it might be closed
373 379 #
374 380 # on closed otherheads
375 381 # ========= ==========
376 382 # o 0 all heads for current branch are closed
377 383 # N only descendant branch heads are closed
378 384 # x 0 there is only one non-closed branch head
379 385 # N there are some non-closed branch heads
380 386 # ========= ==========
381 387 otherheads = repo.revs('%ln - parents()', heads)
382 388 if repo['.'].closesbranch():
383 389 ui.warn(_('no open descendant heads on branch "%s", '
384 390 'updating to a closed head\n') %
385 391 (currentbranch))
386 392 if otherheads:
387 393 ui.warn(_("(committing will reopen the head, "
388 394 "use 'hg heads .' to see %i other heads)\n") %
389 395 (len(otherheads)))
390 396 else:
391 397 ui.warn(_('(committing will reopen branch "%s")\n') %
392 398 (currentbranch))
393 399 elif otherheads:
394 400 curhead = repo['.']
395 401 ui.status(_('updated to "%s: %s"\n') % (curhead,
396 402 curhead.description().split('\n')[0]))
397 403 ui.status(_('%i other heads for branch "%s"\n') %
398 404 (len(otherheads), currentbranch))
399 405
400 406 def statusotherdests(ui, repo):
401 407 """Print message about other head"""
402 408 # XXX we should probably include a hint:
403 409 # - about what to do
404 410 # - how to see such heads
405 411 if repo._activebookmark:
406 412 _statusotherbook(ui, repo)
407 413 else:
408 414 _statusotherbranchheads(ui, repo)
@@ -1,18 +1,19 b''
1 1 # TODO there are a few deficiencies in this file:
2 2 # * The "namespace" of the labels needs to be worked out. We currently
3 3 # piggyback on existing values so color works.
4 4 # * Obsolescence isn't considered for node labels. See _cset_labels in
5 5 # map-cmdline.default.
6 6 showbookmarks = '{if(active, "*", " ")} {pad(bookmark, longestbookmarklen + 4)}{shortest(node, 5)}\n'
7 7
8 8 showwork = '{cset_shortnode}{namespaces % cset_namespace} {cset_shortdesc}'
9 showstack = '{showwork}'
9 10
10 11 cset_shortnode = '{label("log.changeset changeset.{phase}", shortest(node, 5))}'
11 12
12 13 # Treat branch and tags specially so we don't display "default" or "tip"
13 14 cset_namespace = '{ifeq(namespace, "branches", names_branches, ifeq(namespace, "tags", names_tags, names_others))}'
14 15 names_branches = '{ifeq(branch, "default", "", " ({label('log.{colorname}', branch)})")}'
15 16 names_tags = '{if(names % "{ifeq(name, 'tip', '', name)}", " ({label('log.{colorname}', join(names % "{ifeq(name, 'tip', '', name)}", ' '))})")}'
16 17 names_others = '{if(names, " ({label('log.{colorname}', join(names, ' '))})")}'
17 18
18 19 cset_shortdesc = '{label("log.description", desc|firstline)}'
@@ -1,168 +1,171 b''
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [extensions]
3 3 > show =
4 4 > EOF
5 5
6 6 No arguments shows available views
7 7
8 8 $ hg init empty
9 9 $ cd empty
10 10 $ hg show
11 11 available views:
12 12
13 13 bookmarks -- bookmarks and their associated changeset
14 stack -- current line of work
14 15 work -- changesets that aren't finished
15 16
16 17 abort: no view requested
17 18 (use "hg show VIEW" to choose a view)
18 19 [255]
19 20
20 21 `hg help show` prints available views
21 22
22 23 $ hg help show
23 24 hg show VIEW
24 25
25 26 show various repository information
26 27
27 28 A requested view of repository data is displayed.
28 29
29 30 If no view is requested, the list of available views is shown and the
30 31 command aborts.
31 32
32 33 Note:
33 34 There are no backwards compatibility guarantees for the output of this
34 35 command. Output may change in any future Mercurial release.
35 36
36 37 Consumers wanting stable command output should specify a template via
37 38 "-T/--template".
38 39
39 40 List of available views:
40 41
41 42 bookmarks bookmarks and their associated changeset
42 43
44 stack current line of work
45
43 46 work changesets that aren't finished
44 47
45 48 (use 'hg help -e show' to show help for the show extension)
46 49
47 50 options:
48 51
49 52 -T --template TEMPLATE display with template
50 53
51 54 (some details hidden, use --verbose to show complete help)
52 55
53 56 Unknown view prints error
54 57
55 58 $ hg show badview
56 59 abort: unknown view: badview
57 60 (run "hg show" to see available views)
58 61 [255]
59 62
60 63 HGPLAIN results in abort
61 64
62 65 $ HGPLAIN=1 hg show bookmarks
63 66 abort: must specify a template in plain mode
64 67 (invoke with -T/--template to control output format)
65 68 [255]
66 69
67 70 But not if a template is specified
68 71
69 72 $ HGPLAIN=1 hg show bookmarks -T '{bookmark}\n'
70 73 (no bookmarks set)
71 74
72 75 $ cd ..
73 76
74 77 bookmarks view with no bookmarks prints empty message
75 78
76 79 $ hg init books
77 80 $ cd books
78 81 $ touch f0
79 82 $ hg -q commit -A -m initial
80 83
81 84 $ hg show bookmarks
82 85 (no bookmarks set)
83 86
84 87 bookmarks view shows bookmarks in an aligned table
85 88
86 89 $ echo book1 > f0
87 90 $ hg commit -m 'commit for book1'
88 91 $ echo book2 > f0
89 92 $ hg commit -m 'commit for book2'
90 93
91 94 $ hg bookmark -r 1 book1
92 95 $ hg bookmark a-longer-bookmark
93 96
94 97 $ hg show bookmarks
95 98 * a-longer-bookmark 7b570
96 99 book1 b757f
97 100
98 101 A custom bookmarks template works
99 102
100 103 $ hg show bookmarks -T '{node} {bookmark} {active}\n'
101 104 7b5709ab64cbc34da9b4367b64afff47f2c4ee83 a-longer-bookmark True
102 105 b757f780b8ffd71267c6ccb32e0882d9d32a8cc0 book1 False
103 106
104 107 bookmarks JSON works
105 108
106 109 $ hg show bookmarks -T json
107 110 [
108 111 {
109 112 "active": true,
110 113 "bookmark": "a-longer-bookmark",
111 114 "longestbookmarklen": 17,
112 115 "node": "7b5709ab64cbc34da9b4367b64afff47f2c4ee83"
113 116 },
114 117 {
115 118 "active": false,
116 119 "bookmark": "book1",
117 120 "longestbookmarklen": 17,
118 121 "node": "b757f780b8ffd71267c6ccb32e0882d9d32a8cc0"
119 122 }
120 123 ]
121 124
122 125 JSON works with no bookmarks
123 126
124 127 $ hg book -d a-longer-bookmark
125 128 $ hg book -d book1
126 129 $ hg show bookmarks -T json
127 130 [
128 131 ]
129 132
130 133 commands.show.aliasprefix aliases values to `show <view>`
131 134
132 135 $ hg --config commands.show.aliasprefix=s sbookmarks
133 136 (no bookmarks set)
134 137
135 138 $ hg --config commands.show.aliasprefix=sh shwork
136 139 @ 7b570 commit for book2
137 140 o b757f commit for book1
138 141 o ba592 initial
139 142
140 143 $ hg --config commands.show.aliasprefix='s sh' swork
141 144 @ 7b570 commit for book2
142 145 o b757f commit for book1
143 146 o ba592 initial
144 147
145 148 $ hg --config commands.show.aliasprefix='s sh' shwork
146 149 @ 7b570 commit for book2
147 150 o b757f commit for book1
148 151 o ba592 initial
149 152
150 153 The aliases don't appear in `hg config`
151 154
152 155 $ hg --config commands.show.aliasprefix=s config alias
153 156 [1]
154 157
155 158 Doesn't overwrite existing alias
156 159
157 160 $ hg --config alias.swork='log -r .' --config commands.show.aliasprefix=s swork
158 161 changeset: 2:7b5709ab64cb
159 162 tag: tip
160 163 user: test
161 164 date: Thu Jan 01 00:00:00 1970 +0000
162 165 summary: commit for book2
163 166
164 167
165 168 $ hg --config alias.swork='log -r .' --config commands.show.aliasprefix=s config alias
166 169 alias.swork=log -r .
167 170
168 171 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now