.hgext/hgflow.py

changeset 358
cd4f24b84a3e
parent 357
809a691bfa2e
child 359
b3c85390852b
equal deleted inserted replaced
357:809a691bfa2e 358:cd4f24b84a3e
1 """commands to support generalized Driessen's branching model
2 """
3 # License GPL 2.0
4 #
5 # hgflow.py - Mercurial extension to support generalized Driessen's branching model
6 # Copyright (C) 2011-2014, Yujie Wu and others
7 #
8 # This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2 of the License or any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
12 # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13
14
15
16 import os
17 import sys
18 import copy
19 import difflib
20 import mercurial
21
22 from mercurial import util, extensions, error, config
23 from mercurial.node import short
24 from mercurial.i18n import _
25
26
27
28 ###############################################################################################################################
29 # Let's use 128 char width.
30 # It is silly to stick to the 80-char rule.
31 #
32 #
33 # Terminologies <== Read before you got confused
34 # - branch type
35 # We distinguish the following types of branches: master, develop, feature, release, hotfix, support.
36 # We should assume any branch is of one of these types.
37 #
38 # - stream
39 # The entire set of branches of the same type. Stream is not branch, it is a set. Stream is not a type, it is a set.
40 # Stream is a set of branches.
41 #
42 # - substream
43 # A subset of branches in a stream.
44 #
45 # - trunk
46 # Trunk is a special branch. A stream can optionally have a trunk, but only one trunk at most. For example, master and
47 # develop streams each has a trunk, whereas feature, release, hotfix, and support streams don't.
48 # If a stream has a trunk, all branches in the stream normally should diverge from the trunk and later merge to the trunk
49 # when the branches are closed.
50 # Trunk is a relative concept. A trunk of a stream may be a regular branch of another stream. (The former stream will be
51 # a substream of the latter.)
52 #
53 # - source
54 # Source is an attribute of stream. The source of a stream refers to the parent stream where branches in the current stream
55 # are created from. Most commonly, source of a stream is the stream itself. But this is not always the case, for example,
56 # the sources of release and feature streams are the develop stream.
57 #
58 # - destin
59 # Destin is another attribute of stream. The destin of a stream refers to the stream(s) where branches in the current
60 # stream will merge to. Most commonly, destin of a stream is the stream itself. But this is not always the case, for
61 # example, the destin of release is the develop and the master streams.
62 #
63 # - branch name
64 # Use this term carefully since it is potentially ambiguious.
65 # Try using this term to refer to fullname (see below).
66 #
67 # - fullname
68 # Branch name as recognized by the SCM, e.g., feature/enhance_log.
69 # Prefer this term to 'branch name'.
70 #
71 # - basename
72 # Branch name recognized by flow, but not necessarily by SCM, e.g., enhanced_log (with prefix 'feature/' dropped).
73 #
74 # - name
75 # Use this term carefully since it is potentially ambiguious.
76 # This term should be a synonym of basename (see above). Try using it only as place holders, such as
77 # <hotfix-prefix>/<name>.
78 #
79 # - flow action
80 # Refer to action on a specified stream, e.g., hg flow feature start, where 'start' is an action.
81 #
82 # - flow command
83 # Refer to other actions than those on a stream, e.g., hg flow unshelve, where 'unshelve' is a command.
84 #
85 # - hg command
86 # Refer to command not from flow extension.
87 #
88 # - workflow
89 # Refer to the process of executing a sequence of hg commands.
90 #
91 # - history
92 # Refer to a sequence of hg commands that has been executed.
93 #
94 # Notations
95 # - <stream>
96 # Examples: <feature>, <hotfix>. These denote the corresponding streams. When you refer to a stream, e.g., feature stream,
97 # use '<feature>' (or more verbosely 'feature stream'), instead of '<feature> stream', because '<feature>' already means
98 # stream.
99 #
100 # - <stream> branch
101 # Example: a <feature> branch. This phrase refers a branch in <feature>. Do not use 'a feature branch' to mean a branch in
102 # <feature> because the word 'feature' there should take its usual meaning as in English, which doesn't necessarily mean
103 # the feature stream.
104 #
105 # - `text`
106 # Example, `hg flow feature start <name>`. The text wrapped by the ` (apostrophe) symbols should be a piece of code or
107 # shell command, which could contain placeholders to be replaced by actual values.
108 #
109 # - 'text' or "text"
110 # Refer to an exact string.
111 ###############################################################################################################################
112
113
114
115 VERSION = "0.9.8"
116 CONFIG_BASENAME = ".hgflow"
117 OLD_CONFIG_BASENAME = ".flow"
118 CONFIG_SECTION_BRANCHNAME = "branchname"
119 STRIP_CHARS = '\'"'
120
121
122 colortable = {"flow.error" : "red bold",
123 "flow.warn" : "magenta bold",
124 "flow.note" : "cyan",
125 "flow.help.topic" : "yellow",
126 "flow.help.code" : "green bold",
127 }
128
129
130
131 def _print( ui, *arg, **kwarg ) :
132 """
133 Customized print function
134
135 This function prints messages with the prefix: C{flow: }. Multiple messages can be printed in one call.
136 See I{Example I} below.
137
138 @type ui: C{mercurial.ui}
139 @param ui: Mercurial user interface object
140 @type warning: C{bool}
141 @param warning: If set to true, messages will be written to C{stderr} using the C{ui.warn} function.
142 @type note: C{bool}
143 @param note: If set to true, messages will be written to C{stdout} using the C{ui.note} function. The messages will be
144 visible only when user turns on C{--verbose}.
145 By default, both L{warning} and L{note} are set to false, and messages will be written to C{stdout}.
146 @type prefix: C{None} or C{str}
147 @param prefix: Add a customized prefix before every message. See I{Example II}.
148 @type newline: C{bool}
149 @param newline: If set to false, each message will be written without newline suffix. Default value is true.
150
151 I{Example I}:
152
153 >>> _print( ui, "message1", "message2" )
154 flow: message1
155 flow: message2
156
157 I{Example II}:
158
159 >>> _print( ui, "message1", "message2", prefix = "warning: " )
160 flow: warning: message1
161 flow: warning: message2
162
163 I{Example III}:
164
165 >>> _print( ui, "message1", "message2", inline = False )
166 flow: message1message2
167 """
168 printer = ui.warn if (kwarg.get( "warning" )) else (ui.note if (kwarg.get( "note" )) else ui.write)
169 indent = kwarg.get( "indent", "" )
170 prefix = kwarg.get( "prefix", "" )
171 newline = kwarg.get( "newline" )
172 newline = "\n" if (newline or newline is None) else ""
173 for e in arg :
174 printer( ui.config( "flow", "prefix", "flow: " ).strip( STRIP_CHARS ) + prefix + indent )
175 printer( e + newline, label = kwarg.get( "label", "" ) )
176
177
178
179 def _warn( ui, *arg, **kwarg ) :
180 """
181 Print messages to C{stderr}. Each message will be prefixed with C{flow: warning: }.
182
183 This function is a thin wrapper of L{_print}. See document of the later for usage detail.
184
185 Customized prefix will be appended after C{flow: warning: }.
186
187 I{Example}:
188
189 >>> _warn( ui, "message1", "message2", prefix = "prefix_" )
190 flow: warning: prefix_message1
191 flow: warning: prefix_message2
192 """
193 kwarg["warn" ] = True
194 kwarg["label" ] = kwarg.get( "label", "flow.warn" )
195 kwarg["prefix"] = "warning: " + kwarg.get( "prefix", "" )
196 _print( ui, *arg, **kwarg )
197
198
199
200 def _error( ui, *arg, **kwarg ) :
201 """
202 Print messages to C{stderr}. Each message will be prefixed with C{flow: error: }.
203
204 This function is a thin wrapper of L{_print}. See document of the later for usage detail.
205
206 Customized prefix will be appended after C{flow: error: }.
207
208 I{Example}:
209
210 >>> _error( ui, "message1", "message2", prefix = "prefix_" )
211 flow: error: prefix_message1
212 flow: error: prefix_message2
213 """
214 kwarg["warn" ] = True
215 kwarg["label" ] = kwarg.get( "label", "flow.error" )
216 kwarg["prefix"] = "error: " + kwarg.get( "prefix", "" )
217 _print( ui, *arg, **kwarg )
218
219
220
221 def _note( ui, *arg, **kwarg ) :
222 """
223 Print messages to C{stout}. Each message will be prefixed with C{flow: note: }. The messages will be displayed only when
224 user turns on C{--verbose}. If you want to print message without C{--verbose}, include an argument C{via_quiet = True} in
225 the call to this function.
226
227 This function is a thin wrapper of L{_print}. See document of the later for usage detail.
228
229 Customized prefix will be appended after C{flow: note: }.
230
231 I{Example}:
232
233 >>> _note( ui, "message1", "message2", prefix = "prefix_" )
234 flow: note: prefix_message1
235 flow: note: prefix_message2
236 """
237 if (kwarg.get( "via_quiet")) :
238 kwarg["note"] = not kwarg["via_quiet"]
239 del kwarg["via_quiet"]
240 else :
241 kwarg["note"] = True
242 kwarg["label" ] = kwarg.get( "label", "flow.note" )
243 kwarg["prefix"] = "note: " + kwarg.get( "prefix", "" )
244 _print( ui, *arg, **kwarg )
245
246
247
248 class AbortFlow( Exception ) :
249 """
250 Throw an instance of this exception whenever we have to abort the flow command.
251 """
252 def __init__( self, *arg, **kwarg ) :
253 """
254 Accept one or more error messages in C{str} as the arguments.
255 """
256 Exception.__init__( self, "Aborted hg flow command." )
257 self._msg = arg
258 for k in kwarg :
259 self.__dict__[k] = kwarg[k]
260
261
262
263 def error_message( self ) :
264 """
265 Returns a list of error messages in C{str}.
266 """
267 return self._msg
268
269
270
271 class AbnormalStream( Exception ) :
272 """
273 Throw an instance of this exception if the stream does not belong to any one of C{<master>}, C{<develop>}, C{<feature>},
274 C{<release>}, C{<hotfix>}, and C{<support>}.
275 """
276 def __init__( self, message = "", stream = None ) :
277 """
278 Accept one error message. You can also pass the C{Stream} object, which can be retrieved later via the C{stream}
279 method.
280 """
281 Exception.__init__( self, message )
282 self._stream = stream
283
284
285
286 def stream( self ) :
287 """
288 Return the C{Stream} object.
289 """
290 return self._stream
291
292
293
294 class Commands( object ) :
295 """
296 Wrapper class of C{mercurial.commands} with ability of recording command history.
297
298 I{Example:}
299
300 >>> commands = Commands()
301 >>> commands.commit( ui, repo, ... )
302 >>> commands.update( ui, repo, ... )
303 >>> commands.print_history()
304 flow: note: Hg command history:
305 flow: note: hg commit --message "flow: Closed release 0.7." --close_branch
306 flow: note: hg update default
307 """
308
309 def __init__( self ) :
310 self.ui = None
311 self._cmd = None
312 self._cmd_history = []
313 self._via_quiet = False
314 self._dryrun = False
315 self._common_opts = {}
316 self._opt_mutator = {}
317
318 self.reg_option_mutator( "strip", lambda opts : dict( {"rev" : [],}, **opts ) )
319 self.reg_option_mutator( "graft", lambda opts : dict( {"rev" : [], "continue" : False,}, **opts ) )
320 self.reg_option_mutator( "log", lambda opts : dict( {"date" : None, "user" : None, "rev" : None,}, **opts ) )
321
322
323
324 def __getattr__( self, name ) :
325 """
326 Typical invocation of mercurial commands is in the form: commands.name( ... ).
327 We only need to save the command name here, leaving execution of the command to the L{__call__} function.
328 """
329 if (name[0] != "_") :
330 self._cmd = name
331 return self
332
333
334
335 def __call__( self, ui, repo, *arg, **kwarg ) :
336 """
337 Invoke the mercurial command and save it as a string into the history.
338
339 @raise AbortFlow: Throw exception if the return code of hg command (except C{commit} and C{rebase}) is nonzero.
340 """
341 self.ui = ui
342 cmd_str = "hg " + (self._cmd[:-1] if (self._cmd[-1] == "_") else self._cmd)
343 arg = self._branch2str( arg )
344 kwarg = self._branch2str( kwarg )
345 cmd = self._cmd
346
347 if (cmd[0] == "q") :
348 where = extensions.find( "mq" )
349 cmd = cmd[1:]
350 elif (cmd == "strip" ) : where = extensions.find( "mq" )
351 elif (cmd == "rebase") : where = extensions.find( "rebase" )
352 else : where = mercurial.commands
353
354 kwarg = self._mutate_options( where, self._cmd, kwarg )
355
356 for key, value in sorted( kwarg.items(), reverse = True ) :
357 if (value in [None, "", False]) :
358 continue
359
360 # If the command is `hg commit --message <commit-hint> --force-editor [other-options...]`, we will drop the
361 # `--message <commit-hint> --force-editor` part from the command string because `--force-editor` is not a command
362 # option (it avails only programmatically).
363 if (cmd == "commit" and (key in ["message", "force_editor",]) and "force_editor" in kwarg) :
364 continue
365
366 new_key = ""
367 for e in key :
368 new_key += "-" if (e == "_") else e
369 key = new_key
370 value = [self._add_quotes( e ) for e in value] if (isinstance( value, list )) else self._add_quotes( value )
371
372 if (isinstance( value, bool )) :
373 cmd_str = "%s --%s" % (cmd_str, key,)
374 elif (isinstance( value, list )) :
375 for e in value :
376 cmd_str = "%s --%s %s" % (cmd_str, key, str( e ),)
377 else :
378 cmd_str = "%s --%s %s" % (cmd_str, key, str( value ),)
379 for e in arg :
380 cmd_str = '%s %s ' % (cmd_str, self._add_quotes( str( e ) ),)
381
382 self._cmd_history.append( cmd_str )
383
384 if (self._dryrun) :
385 return
386
387 try :
388 # Ever since 2.8 the "strip" command has been moved out of the "mq" module to a new module of its own. Issue#56
389 if ("strip" == cmd) :
390 from mercurial import __version__
391 if (__version__.version > "2.8") :
392 where = extensions.find( "strip" )
393 cmd = "stripcmd"
394
395 ret = None
396 ret = getattr( where, cmd )( ui, repo, *arg, **kwarg )
397 except Exception, e :
398 raise AbortFlow( "Hg command failed: %s" % cmd_str, "abort: %s\n" % str( e ), traceback = sys.exc_info() )
399
400 if (ret and cmd not in ["commit", "rebase",]) :
401 # We have to make some exceptions, where nonzero return codes don't mean error. Issue#55
402 if ((ret, cmd,) not in [(1, "push",),]) :
403 raise AbortFlow( "Hg command failed: %s" % cmd_str, "abort: Nonzero return code from hg command\n" )
404
405
406
407 def _add_quotes( self, value ) :
408 """
409 If C{value} is a string that contains space, slash, double quote, and parenthesis (viz: '(' or ')'), wrap it with
410 double quotes and properly escape the double-quotes and slashes within, and finally return the modified string.
411 Otherwise, return the value as is.
412 """
413 if (isinstance( value, str ) and (1 in [c in value for c in " ()\\\""])) :
414 new_value = ""
415 for c in value :
416 if (c == "\\") : new_value += "\\\\"
417 elif (c == '"' ) : new_value += "\""
418 else : new_value += c
419 value = '"%s"' % new_value
420 return value
421
422
423
424 def _branch2str( self, value ) :
425 """
426 If C{value} is a C{Branch} object, return its fullname (C{str}); if it is not, return the object itself. Do this
427 recursively if C{value} is a C{tuple}, or C{list}, or C{dict} object.
428 """
429 if (isinstance( value, Branch )) :
430 return value.fullname()
431 if (isinstance( value, (list, tuple,) )) :
432 new_value = []
433 for e in value :
434 new_value.append( self._branch2str( e ) )
435 return new_value
436 if (isinstance( value, dict )) :
437 new_value = {}
438 for k, v in value.items() :
439 new_value[k] = self._branch2str( v )
440 return new_value
441 return value
442
443
444
445 def _filter_common_options( self, where, cmd ) :
446 """
447 If any common options are valid options of the command, return these options.
448
449 @type where: module
450 @param where: Should be `mercurial.commands', or a Mercurial's plugin object.
451 @type cmd : C{str}
452 @param cmd : Mercurial command name
453 """
454 ret = {}
455 if (self._common_opts != {}) :
456 if (cmd[-1] == "_") :
457 cmd = cmd[:-1]
458 junk, table = mercurial.cmdutil.findcmd( cmd, where.table if hasattr( where, "table" ) else where.cmdtable )
459 opts = [e[1] for e in table[1]]
460 for e in self._common_opts :
461 if (e in opts) :
462 ret[e] = self._common_opts[e]
463 return ret
464
465
466
467 def _mutate_options( self, where, cmd, opts ) :
468 """
469 Call the registered command option mutator for the command and return the modified command options.
470
471 @type where: module
472 @param where: Should be `mercurial.commands', or a Mercurial's plugin object.
473 @type cmd : C{str}
474 @param cmd : Mercurial command name
475 @type opts : C{dict}
476 @param opts : Original command options
477
478 @rtype: C{dict}
479 """
480 common_opts = self._filter_common_options( where, cmd )
481 common_opts.update( opts )
482 opts = common_opts
483 mutator = self._opt_mutator.get( cmd )
484 if (mutator) :
485 opts = mutator( opts )
486
487 return opts
488
489
490
491 def use_quiet_channel( self, via_quiet = True ) :
492 """
493 Print the history to the I{quiet} channel, where text will be displayed even when user does not specify the
494 C{--verbose} option.
495
496 @type via_quiet: C{bool}
497 @param via_quiet: To turn off using the "quiet" channel for history printing, you can call this function like:
498 C{use_quiet_channel( False )}.
499 """
500 self._via_quiet = via_quiet
501
502
503
504 def use_verbose_channel( self, via_verbose = True ) :
505 """
506 Print the history to the I{verbose} channel, where text will be display only when user specify the C{--verbose} option.
507
508 @type via_verbose: C{bool}
509 @param via_verbose: To turn off using the "verbose" channel for history printing (you will be using the "quiet"
510 channel instead), you can call this function like: C{use_verbose_channel( False )}.
511 """
512 self._via_quiet = not via_verbose
513
514
515
516 def reg_common_options( self, opts ) :
517 """
518 Register common options.
519
520 @type opts: C{dict}
521 @param opts: Common options. Key = option's flag, value = option's value.
522 """
523 self._common_opts.update( opts )
524
525
526
527 def reg_option_mutator( self, cmd, mutator ) :
528 """
529 Register common options.
530
531 @type cmd : C{str}
532 @param cmd : Mercurial command name
533 @type mutator: Callable object
534 @param mutator: It will take a C{dict} object as its argument and return another C{dict} object that is supposed to be
535 a mutant of the former. The input object is supposed to be the original hg-command options in form of
536 key-value pairs.
537 """
538 self._opt_mutator[cmd] = mutator
539
540
541
542 def dryrun( self, switch = None ) :
543 """
544 Switch the dry-run mode.
545
546 @type switch: C{boolean} or C{None}
547 @param switch: Switch on dry-run mode if C{switch = True}, off if C{switch = False}. If C{switch} is C{None}, just
548 return the current state of dry-run mode.
549 """
550 if (switch is None) :
551 return self._dryrun
552 self._dryrun = switch
553
554
555
556 def print_history( self ) :
557 """
558 Print the command history using the L{_note} function.
559 """
560 if (self.ui) :
561 _note( self.ui, "Hg command history:", via_quiet = self._via_quiet )
562 for e in self._cmd_history :
563 _note( self.ui, e, prefix = " ", via_quiet = self._via_quiet )
564
565
566
567 class Stream( object ) :
568 @staticmethod
569 def gen( ui, repo, name, check = False ) :
570 """
571 Given the name of a stream, return a C{Stream} object.
572 If the name is that of one of the standard streams: master, develop, feature, release, hotfix, and support, return the
573 same object as in C{STREAM}. If not, create and return a new C{stream} object. If the new object is not in the standard
574 streams, an C{AbnormalStream} exception will be thrown. One can catch the exception and call its C{stream} method to
575 get the object.
576
577 @type name : C{str}
578 @param name : Name of the stream. It can be a complex stream name, e.g., "develop/spring:release".
579 @type check: C{boolean}
580 @param check: If true and the stream is not a standard one, the function will check if the trunk of the stream exists
581 or not and (if exists) open or not.
582
583 @raise AbortFlow : When C{check} is true and the trunk of the stream doesn't exist or is closed
584 @raise AbnormalStream: When the stream is not in any of the standard streams
585 """
586 source = None
587 tokens = name.split( ':' )
588 n = len( tokens )
589 if (n == 2) :
590 source, name = tokens[0], tokens[1]
591 if (n > 2 or not name) :
592 raise AbortFlow( "Invalid stream syntax: '%s'" % stream )
593
594 for e in STREAM.values() :
595 if (name == e.name()) :
596 if (source) :
597 stream = copy.copy( e )
598 break
599 else :
600 return e
601 else :
602 rootstream_name = name.split( '/', 1 )[0]
603 is_normalstream = True
604
605 if (rootstream_name in STREAM) :
606 trunk = name.replace( rootstream_name + '/', STREAM[rootstream_name].prefix(), 1 )
607 stream = Stream( ui, repo, name, trunk = trunk )
608 else :
609 stream = Stream( ui, repo, name, trunk = name )
610 is_normalstream = False
611
612 if (check) :
613 try :
614 trunk = stream.trunk()
615 except error.RepoLookupError , e :
616 misspelling = difflib.get_close_matches( stream.name(), STREAM.keys(), 3, 0.7 )
617 note = "Did you mean: %s?" % " or ".join( misspelling ) if (misspelling) else None
618 raise AbortFlow( "Stream not found: %s" % stream, note = note )
619 if (trunk.is_closed()) :
620 raise AbortFlow( "%s has been closed." % stream )
621
622 # It seems we never really mind abnormal streams. So comment this out.
623 #if (not is_normalstream) :
624 # raise AbnormalStream( stream = Stream( ui, repo, name ) )
625
626 if (source) :
627 source = Stream.gen( ui, repo, source, check = True )
628 stream = copy.copy( stream )
629 stream._source = source
630 for i, e in enumerate( stream.destin() ) :
631 if (source in e) :
632 stream._destin[i] = source
633 break
634 else :
635 stream._destin = [source]
636
637 return stream
638
639
640
641 def __init__( self, ui, repo, name, **kwarg ) :
642 """
643 Create a new C{Stream} object.
644
645 @type name : C{str}
646 @param name : Name of the new stream
647 @type trunk : C{str} or C{None}
648 @param trunk : Fullname of the trunk of the stream, or C{None}
649 @type prefix: C{str}
650 @param prefix: Name prefix of branches in this stream. If not specified, it will default to C{trunk + '/'} (if C{trunk}
651 is not C{None}), or C{name + '/'} if (C{trunk} is C{None}).
652 @type source: C{Stream}
653 @param source: Stream where branches in this stream will be created from
654 @type destin: C{list} of C{Stream} objects
655 @param destin: Streams where branches in this stream will merge to when being finished
656 """
657 self.ui = ui
658 self.repo = repo
659
660 self._name = name
661 self._trunk = kwarg.get( "trunk" )
662 self._prefix = kwarg.get( "prefix" )
663 self._source = kwarg.get( "source", self )
664 self._destin = kwarg.get( "destin", [self._source,] )
665 self._tcache = None # Caches `Branch' object of the trunk because construction of a `Branch' object is very slow.
666
667 if (self._prefix is None) :
668 if (self._trunk) :
669 self._prefix = self._trunk + '/'
670 else :
671 self._prefix = self._name + '/'
672
673
674
675 def __str__( self ) :
676 """
677 Return a string: '<stream-name>'.
678 """
679 return "<%s>" % self._name
680
681
682
683 def __cmp__( self, rhs ) :
684 """
685 Compare streams by comparing their names as strings.
686 """
687 lhs = self._name
688 rhs = rhs ._name
689 return -1 if (lhs < rhs) else (1 if (lhs > rhs) else 0)
690
691
692
693 def __contains__( self, stranch ) :
694 """
695 Return true if the C{stanch} is in this stream.
696
697 @type stranch: C{Stream} or C{Branch}
698 @param srranch: Stream or branch which you want to test if it is in this stream
699 """
700 if (isinstance( stranch, Branch )) :
701 if (stranch._fullname == self._trunk) :
702 return True
703 return stranch._fullname.startswith( self.prefix() )
704 elif (isinstance( stranch, Stream )) :
705 return stranch.prefix().startswith( self.prefix() )
706 return str( stranch ).startswith( self.prefix() )
707
708
709
710 def name( self ) :
711 """
712 Return the name of this stream.
713 """
714 return self._name
715
716
717
718 def trunk( self, trace = False ) :
719 """
720 Return the trunk of this stream. If it has no trunk, return C{None} or the trunk of the source stream depending on the
721 C{trace} parameter.
722
723 @type trace: C{boolean}
724 @param trace: If true and this stream has no trunk, return the trunk of the source stream, and do this recursively
725 until a trunk is found. If false and this stream has no trunk, this function will return C{None}.
726
727 @return: A C{Branch} object or C{None}
728 """
729 if (self._tcache) :
730 return self._tcache
731
732 trunk = Branch( self.ui, self.repo, self._trunk ) if (self._trunk) else None
733 if (not trunk and trace) :
734 return self.source().trunk( True )
735 self._tcache = trunk
736 return trunk
737
738
739
740 def prefix( self ) :
741 """
742 Return the branch name prefix of this stream.
743
744 @return: C{str}
745 """
746 return self._prefix
747
748
749
750 def source( self ) :
751 """
752 Return the source stream.
753
754 @return: C{Stream}
755 """
756 return self._source
757
758
759
760 def destin( self ) :
761 """
762 Return a list of streams where branches in this stream will merge to when finished.
763
764 @return: C{Stream}
765 """
766 return self._destin
767
768
769
770 def get_fullname( self, branch_basename ) :
771 """
772 Return the fullname of a branch.
773
774 @type branch_basename: C{str}
775 @param branch_basename: Basename of a branch in this stream
776
777 @return: C{str}
778 """
779 return self._prefix + branch_basename
780
781
782
783 def get_branch( self, branch_basename ) :
784 """
785 Create and return a new C{Branch} object with the given basename.
786
787 @type branch_basename: C{str}
788 @param branch_basename: Basename of a branch in this stream
789
790 @return: C{Branch}
791 """
792 return Branch( self.ui, self.repo, self.get_fullname( branch_basename ) )
793
794
795
796 def branches( self, openclosed = "open" ) :
797 """
798 Return a list of branches in this stream. The list does not include the trunk.
799 The returned list is sorted per branch name.
800
801 @type openclosed: C{str}, must be one of "open", "closed", and "all".
802 @param openclosed: If the value is C{"open"}, return all open branches in this stream; if C{"closed"}, return all
803 closed branches in this stream; if C{"all"}, returns all open and closed branches in this stream.
804 """
805 if (openclosed not in ["open", "closed", "all",]) :
806 raise ValueError( "Invalid value for `openclosed` parameter: %s" % openclosed )
807
808 all_branches = []
809 if (openclosed == "open") :
810 for branch_fullname, heads in self.repo.branchmap().items() :
811 all_branches += [Branch( self.ui, self.repo, head ) for head in heads
812 if (not self.repo[head].extra().get( "close", False ))]
813 elif (openclosed == "closed") :
814 for branch_fullname, heads in self.repo.branchmap().items() :
815 all_branches += [Branch( self.ui, self.repo, head ) for head in heads
816 if (self.repo[head].extra().get( "close", False ))]
817 else :
818 for branch_fullname, heads in self.repo.branchmap().items() :
819 all_branches += [Branch( self.ui, self.repo, head ) for head in heads]
820
821 return sorted( [e for e in all_branches if (e in self)] )
822
823
824
825 class Branch( object ) :
826 def __init__( self, ui, repo, rev = None ) :
827 """
828 Create a C{Branch} object with the given C{rev}.
829 """
830 self.ui = ui
831 self.repo = repo
832 self.ctx = repo[rev] # `repo[rev]' is slow when there are tens of thousands of named branches.
833
834 self._fullname = str( self.ctx.branch() )
835
836
837
838 def __str__( self ) :
839 """
840 Return the fullname of this branch.
841 """
842 return self._fullname
843
844
845
846 def __cmp__( self, rhs ) :
847 """
848 Compare two C{Branch} object by comparing their fullnames.
849 """
850 if (rhs is None) :
851 return 1
852 lhs = self._fullname
853 rhs = rhs ._fullname
854 return -1 if (lhs < rhs) else (1 if (lhs > rhs) else 0)
855
856
857
858 def fullname( self ) :
859 """
860 Return the fullname of this branch.
861 """
862 return self._fullname
863
864
865
866 def basename( self, stream = None, should_quote = False ) :
867 """
868 Return the basename relative to the C{stream}. If C{stream} is C{None}, return the shortest possible basename (will
869 not contain any '/'s).
870 Return the string "trunk" if this branch is the trunk of the C{stream}.
871
872 @type stream: C{Stream} or C{None}
873 @param stream: Stream to which the basename is relative
874 @type should_quote: C{bool}
875 @param should_quote: The returned string will be wrapped with single quotes ' if this parameter's value is true.
876 """
877 if (stream) :
878 if (self._fullname == stream._trunk) :
879 return "trunk"
880 ret = self._fullname[len( stream.prefix() ):]
881 else :
882 ret = self._fullname.rsplit( '/', 1 )[-1]
883 if (should_quote) :
884 ret = "'%s'" % ret
885 return ret
886
887
888
889 def rev_node( self ) :
890 """
891 Return a string showing this branch's head's revision number and short node ID in the format of "<rev>:<node-ID>",
892 e.g., "9:db14bf692069".
893 """
894 return "%s:%s" % (self.ctx.rev(), short( self.ctx.node() ),)
895
896
897
898 def is_closed( self ) :
899 """
900 Return true if this branch is closed; or false if it is open.
901 """
902 extra = self.ctx.extra()
903 try :
904 return extra["close"]
905 except KeyError :
906 return False
907
908
909
910 def is_open( self ) :
911 """
912 Return true if this branch is open; or false if it is closed.
913 """
914 return not self.is_closed()
915
916
917
918 def is_develop_trunk( self ) :
919 """
920 Return true if this branch is the trunk of C{<develop>}.
921 """
922 return STREAM["develop"]._trunk == self._fullname
923
924
925
926 def is_master_trunk( self ) :
927 """
928 Return true if this branch is the trunk of C{<master>}.
929 """
930 return STREAM["master"]._trunk == self._fullname
931
932
933
934 def is_trunk( self, stream ) :
935 """
936 Return true if this branch is the trunk of the C{stream}.
937 """
938 return stream._trunk == self._fullname
939
940
941
942 def stream( self ) :
943 """
944 Return the stream that this branch belongs to.
945 """
946 name = self._fullname
947 for stream in STREAM.values() :
948 if (name == stream._trunk) :
949 return stream
950 if (name.startswith( stream.prefix() )) :
951 name = name.replace( stream.prefix(), stream.name() + '/' )
952 break
953 return Stream.gen( self.ui, self.repo, name.rsplit( '/', 1 )[0] )
954
955
956
957 commands = Commands()
958 STREAM = {} # key = stream name, value = `Stream` object. Will be set by `Flow.__init__`.
959
960
961
962 class Flow( object ) :
963
964 ACTION_NAME = ["start", "finish", "push", "publish", "pull", "list", "log", "abort", "promote", "rebase", "rename",]
965
966 def __init__( self, ui, repo, init = False ) :
967 """
968 Construct a C{Flow} instance that will execute the workflow.
969 Construction will fail if the C{flow} extension has not been initialized for the repository.
970 A warning message will be issued if the repository has uncommitted changes.
971
972 @type init: C{boolean}
973 @param init: If true, a C{Flow} object will be constructed for initialization of hgflow. Such constructed object does
974 not supply all functionalities and is only meant to execute the `hg flow init` command.
975 """
976 self.ui = ui
977 self.repo = repo
978
979 self.autoshelve = False
980 self.warn_uncommitted = True
981 self.msg_prefix = "flow: "
982 self.version_prefix = "v"
983 self.orig_workspace = Branch( ui, repo )
984 self.curr_workspace = self.orig_workspace # May be changed whenever `hg update` command is executed.
985 self.orig_dir = os.getcwd()
986 self._dryrun_shelve = set()
987
988 if (init) : return
989
990 config_fname = os.path.join( self.repo.root, CONFIG_BASENAME )
991 if (os.path.isfile( config_fname )) :
992 cfg = config.config()
993 cfg.read( config_fname )
994 try :
995 master = cfg.get( CONFIG_SECTION_BRANCHNAME, "master" )
996 develop = cfg.get( CONFIG_SECTION_BRANCHNAME, "develop" )
997 feature = cfg.get( CONFIG_SECTION_BRANCHNAME, "feature" )
998 release = cfg.get( CONFIG_SECTION_BRANCHNAME, "release" )
999 hotfix = cfg.get( CONFIG_SECTION_BRANCHNAME, "hotfix" )
1000 support = cfg.get( CONFIG_SECTION_BRANCHNAME, "support" )
1001 except Exception, e :
1002 self._error( str( e ) )
1003 self._error( "Flow has not been initialized properly for this repository." )
1004 self._note ( "You can use command `hg flow init -f` to reinitialize for this repository.", via_quiet = True )
1005 sys.exit( 1 )
1006 else :
1007 old_config_fname = os.path.join( self.repo.root, OLD_CONFIG_BASENAME )
1008 if (os.path.isfile( old_config_fname )) :
1009 cfg = config.config()
1010 cfg.read( old_config_fname )
1011 try :
1012 master = cfg.get( CONFIG_SECTION_BRANCHNAME, "master" )
1013 develop = cfg.get( CONFIG_SECTION_BRANCHNAME, "develop" )
1014 feature = cfg.get( CONFIG_SECTION_BRANCHNAME, "feature" )
1015 release = cfg.get( CONFIG_SECTION_BRANCHNAME, "release" )
1016 hotfix = cfg.get( CONFIG_SECTION_BRANCHNAME, "hotfix" )
1017 support = cfg.get( CONFIG_SECTION_BRANCHNAME, "support" )
1018 except Exception, e :
1019 self._error( str( e ) )
1020 self._error( "Flow has not been initialized properly for this repository." )
1021 self._note ( "You can use command `hg flow init -f` to reinitialize for this repository.",
1022 via_quiet = True )
1023 sys.exit( 1 )
1024 else :
1025 self._error( "Flow has not been initialized for this repository: %s file is missing." % CONFIG_BASENAME )
1026 self._note ( "You can use command `hg flow init` to initialize for this repository.", via_quiet = True )
1027 sys.exit( 1 )
1028
1029 global STREAM
1030 STREAM["master" ] = Stream( ui, repo, "master", trunk = master )
1031 STREAM["develop"] = Stream( ui, repo, "develop", trunk = develop )
1032 STREAM["feature"] = Stream( ui, repo, "feature", prefix = feature, source = STREAM["develop"] )
1033 STREAM["release"] = Stream( ui, repo, "release", prefix = release, source = STREAM["develop"] )
1034 STREAM["hotfix" ] = Stream( ui, repo, "hotfix", prefix = hotfix, source = STREAM["master" ] )
1035 STREAM["support"] = Stream( ui, repo, "support", prefix = support, source = STREAM["master" ], destin = [] )
1036
1037 STREAM["develop"]._destin.append( STREAM["release"] )
1038 STREAM["release"]._destin.append( STREAM["master" ] )
1039 STREAM["hotfix" ]._destin.append( STREAM["develop"] )
1040
1041 if (ui.has_section( "hgflow" )) :
1042 self._warn( "The [hgflow] section in hg configuration file is deprecated." )
1043 self._warn( "Please replace the section name from [hgflow] to [flow]." )
1044 self.autoshelve = ui.configbool( "hgflow", "autoshelve", self.autoshelve )
1045 self.warn_uncommitted = ui.configbool( "hgflow", "warn_uncommitted", self.warn_uncommitted )
1046 if (ui.has_section( "flow" )) :
1047 self.autoshelve = ui.configbool( "flow", "autoshelve", self.autoshelve )
1048 self.warn_uncommitted = ui.configbool( "flow", "warn_uncommitted", self.warn_uncommitted )
1049 self.msg_prefix = ui.config ( "flow", "prefix", self.msg_prefix ).strip( STRIP_CHARS )
1050 self.version_prefix = ui.config ( "flow", "version_prefix", self.version_prefix ).strip( STRIP_CHARS )
1051 if (self._has_uncommitted_changes() and self.warn_uncommitted) :
1052 self._warn( "Your workspace has uncommitted changes." )
1053
1054 # We'd better temporarily change the current directory to the root of the repository at the beginning.
1055 # This is to avoid the problem that the CWD might be gone after switching to a different branch. (Issue#14)
1056 # We will change it back to the original directory when the hgflow command exits.
1057 os.chdir( self.repo.root )
1058 # __init__
1059
1060
1061
1062 def __getattr__( self, name ) :
1063 """
1064 Execute mercurial command of name C{name[1:]}.
1065
1066 @type name: C{str}
1067 @param name: Should be a mercurial command name prefixed with one underscore. For example, to call C{commit} command,
1068 use C{self._commit}.
1069 """
1070 if (name[0] == "_") :
1071 cmd = getattr( commands, name[1:] )
1072 def func( *arg, **kwarg ) :
1073 cmd( self.ui, self.repo, *arg, **kwarg )
1074 return func
1075 raise AttributeError( "%s instance has no attribute '%s'" % (self.__class__, name,) )
1076
1077
1078
1079 def _update( self, rev, *arg, **kwarg ) :
1080 """
1081 Intercept the call to `hg update` command. We need to keep track of the branch of the workspace.
1082
1083 @type rev: C{str} or C{mercurial.changectx}
1084 @param rev: Revision to which the workspace will update
1085 """
1086 try :
1087 old_workspace_ctx = self.curr_workspace.ctx
1088 self.curr_workspace = rev if (isinstance( rev, Branch )) else Branch( self.ui, self.repo, rev )
1089 except error.RepoLookupError, e :
1090 if (commands.dryrun()) :
1091 commands.update( self.ui, self.repo, rev, *arg, **kwarg )
1092 else :
1093 raise e
1094
1095 if (old_workspace_ctx != self.curr_workspace.ctx) :
1096 commands.update( self.ui, self.repo, rev, *arg, **kwarg )
1097
1098
1099
1100 def _print( self, *arg, **kwarg ) :
1101 """
1102 Thin wrapper of the global C{_print} function
1103 """
1104 _print( self.ui, *arg, **kwarg )
1105
1106
1107
1108 def _warn( self, *arg, **kwarg ) :
1109 """
1110 Thin wrapper of the global C{_warn} function
1111 """
1112 _warn( self.ui, *arg, **kwarg )
1113
1114
1115
1116 def _error( self, *arg, **kwarg ) :
1117 """
1118 Thin wrapper of the global C{_error} function
1119 """
1120 _error( self.ui, *arg, **kwarg )
1121
1122
1123
1124 def _note( self, *arg, **kwarg ) :
1125 """
1126 Thin wrapper of the global C{_note} function
1127 """
1128 _note( self.ui, *arg, **kwarg )
1129
1130
1131
1132 def _check_rebase( self ) :
1133 """
1134 Check if 'rebase' extension is activated. If not, raise an 'AbortFlow' exception.
1135
1136 @raise AbortFlow: When 'rebase' extension is not found
1137 """
1138 try :
1139 extensions.find( "rebase" )
1140 except KeyError :
1141 raise AbortFlow( "Cannot rebase without 'rebase' extension." )
1142
1143
1144
1145 def _check_mq( self ) :
1146 """
1147 Check if 'mq' extension is activated. If not, raise an 'AbortFlow' exception.
1148
1149 @raise AbortFlow: When 'mq' extension is not found
1150 """
1151 try :
1152 extensions.find( "mq" )
1153 except KeyError :
1154 raise AbortFlow( "Cannot shelve/unshelve changes without 'mq' extension." )
1155
1156
1157
1158 def _check_strip( self ) :
1159 """
1160 The 'strip' command comes with the 'mq' extension.
1161 Check if 'mq' extension is activated. If not, raise an 'AbortFlow' exception.
1162
1163 @raise AbortFlow: When 'mq' extension is not found
1164 """
1165 try :
1166 extensions.find( "mq" )
1167 except KeyError :
1168 raise AbortFlow( "Cannot use 'strip' command without 'mq' extension." )
1169
1170
1171
1172 def _is_shelved( self, branch ) :
1173 """
1174 Return true if the given branch has been shelved.
1175
1176 @type branch: C{Branch}
1177 @param branch: Branch to test if it has shelved changes
1178 """
1179 shelve_name = "flow/" + branch.fullname() + ".pch"
1180 patch_fname = self.repo.join( "patches/" + shelve_name )
1181 return os.path.isfile( patch_fname )
1182
1183
1184
1185 def _shelve( self, *arg, **kwarg ) :
1186 """
1187 Shelve workspace if C{self.autoshelve} is C{True}.
1188
1189 This function utilizes the C{mq} extension to achieve shelving. Bascially, it calls the following C{mq} commands:
1190 C{hg qnew <patchname> --currentuser --currentdate -m "Shelved changes"}
1191 C{hg qpop}
1192 where <patchname> follows the pattern: flow/<branch_fullname>.pch
1193 The two commands will give us a patch file that later will be used to unshelve the change.
1194 """
1195 if (self.autoshelve or kwarg.get( "force" )) :
1196 if (self._has_uncommitted_changes()) :
1197 shelve_name = "flow/" + self.curr_workspace.fullname() + ".pch"
1198 if (commands.dryrun()) :
1199 # For dry run, adds the name of the shelved item into `self._dryrun_shelve'.
1200 # This is for generating correct dry run history for the unshelving operation.
1201 self._dryrun_shelve.add( shelve_name )
1202 self._check_mq()
1203 self._qnew( shelve_name, currentuser = True, currentdate = True, message = "Shelved changes" )
1204 self._qpop()
1205
1206
1207
1208 def _unshelve( self, basename = None, **kwarg ) :
1209 """
1210 Unshelve the previously shelved changes to the workspace if C{self.autoshelve} is C{True}.
1211
1212 This function needs the C{mq} extension to achieve unshelving. Bascially, it calls the following commands:
1213 C{hg import <patch_filename> --no-commit}
1214 C{hg qdelete <patchname>}
1215 where <patchname> follows the pattern: flow/<branch_fullname>.pch, which was previously created by flow's shelving.
1216
1217 @type basename: C{str}
1218 @param basename: Basename of the path of the shelved patch file. Default is the name of current workspace branch.
1219 """
1220 if (self.autoshelve or kwarg.get( "force" )) :
1221 basename = basename if (basename) else self.curr_workspace.fullname()
1222 shelve_name = "flow/" + basename + ".pch"
1223 patch_fname = self.repo.join( "patches/" + shelve_name )
1224 if (os.path.isfile( patch_fname ) or (shelve_name in self._dryrun_shelve)) :
1225 self._check_mq()
1226 self._import_( patch_fname, no_commit = True, base = "", strip = 1 )
1227 self._qdelete( shelve_name )
1228 if (commands.dryrun()) :
1229 self._dryrun_shelve.discard( shelve_name )
1230
1231
1232
1233 def _has_uncommitted_changes( self ) :
1234 """
1235 Return true if any tracked file is modified, or added, or removed, or deleted.
1236 """
1237 return any( self.repo.status() )
1238
1239
1240
1241 def _branches( self, openclosed = "open" ) :
1242 """
1243 Return a list of branches.
1244
1245 @type openclosed: C{str}, "open", "closed", and "all"
1246 @param openclosed: If C{"open"}, return all open branches; if C{"closed"}, return all closed branches; if C{"all"},
1247 return all branches.
1248 """
1249 if (openclosed not in ["open", "closed", "all",]) :
1250 raise ValueError( "Invalid value for openclosed parameter: %s" % openclosed )
1251
1252 all_branches = []
1253 if (openclosed == "open") :
1254 for branch_fullname, heads in self.repo.branchmap().items() :
1255 all_branches += [Branch( self.ui, self.repo, head ) for head in heads
1256 if (not self.repo[head].extra().get( "close", False ))]
1257 elif (openclosed == "closed") :
1258 for branch_fullname, heads in self.repo.branchmap().items() :
1259 all_branches += [Branch( self.ui, self.repo, head ) for head in heads
1260 if (self.repo[head].extra().get( "close", False ))]
1261 else :
1262 for branch_fullname, heads in self.repo.branchmap().items() :
1263 all_branches += [Branch( self.ui, self.repo, head ) for head in heads]
1264
1265 return all_branches
1266
1267
1268
1269 def _find_branch( self, fullname ) :
1270 """
1271 Try to find a branch of name: C{fullname}. If it exists, return a C{Branch} object of this branch and a boolean value
1272 indicating if it's open (C{True}) or closed (C{False}). If it does not exists, return C{(None, None)}.
1273
1274 @type fullname: C{str}
1275 @param fullname: Fullname of the branch to find
1276 """
1277 try :
1278 branch = Branch( self.ui, self.repo, fullname )
1279 return branch, branch.is_open()
1280 except error.RepoLookupError :
1281 return None, None
1282
1283
1284
1285 def latest_master_tags( self ) :
1286 """
1287 Return the latest tag of C{<master>} branch.
1288 """
1289 trunk = STREAM["master"].trunk()
1290 trunk_fullname = trunk.fullname()
1291 master_context = trunk.ctx
1292 while (master_context) :
1293 tags = master_context.tags()
1294 try :
1295 tags.remove( "tip" )
1296 except ValueError :
1297 pass
1298 if (tags) :
1299 return tags
1300 parents = master_context.parents()
1301 master_context = None
1302 for e in parents :
1303 if (trunk_fullname == e.branch()) :
1304 master_context = e
1305 break
1306 return []
1307
1308
1309
1310 def _create_branch( self, fullname, message, from_branch = None, **kwarg ) :
1311 """
1312 Create a new branch and commit the change.
1313
1314 @type fullname: C{str}
1315 @param fullname: Fullname of the new branch
1316 @type message: C{str}
1317 @param message: Commit message
1318 @type from_branch: C{Branch}
1319 @param from_branch: Parent branch of the new branch
1320 """
1321 if (from_branch and self.curr_workspace != from_branch) :
1322 self._update( from_branch )
1323 self._branch( fullname )
1324 self._commit( message = message, **kwarg )
1325 if (commands.dryrun()) :
1326 # Makes a fake new branch.
1327 self.curr_workspace = Branch( self.ui, self.repo )
1328 self.curr_workspace._fullname = fullname
1329 else :
1330 self.curr_workspace = Branch( self.ui, self.repo, fullname )
1331
1332
1333
1334 def _action_start( self, stream, *arg, **kwarg ) :
1335 """
1336 Conduct the I{start} action for the given stream. A new branch in the stream will be created.
1337
1338 @type stream: C{Stream}
1339 @param stream: Stream where you want to start a new branch
1340 """
1341 try :
1342 basename = arg[1]
1343 except IndexError :
1344 raise AbortFlow( "You must specify a name for the new branch to start." )
1345
1346 rev = kwarg.pop( "rev", None )
1347 msg = kwarg.pop( "message", "" )
1348 dirty = kwarg.pop( "dirty", None )
1349 fullname = stream.get_fullname( basename )
1350 br, is_open = self._find_branch( fullname )
1351 if (br) :
1352 self._error( "A branch named '%s' already exists in %s: '%s'." % (basename, stream, fullname,) )
1353 if (not is_open) :
1354 self._note( "Branch '%s' is currently closed." % fullname, via_quiet = True )
1355 else :
1356 shelvedpatch_basename = self.curr_workspace.fullname()
1357 if (rev is None) :
1358 from_branch = stream.source().trunk()
1359 self._shelve( force = dirty )
1360 self._update( from_branch )
1361 else :
1362 from_branch = Branch( self.ui, self.repo, rev )
1363 if (from_branch._fullname != stream.source()._trunk) :
1364 raise AbortFlow( "Revision %s is not in the source stream of %s." % (rev, stream,) )
1365 self._shelve( force = dirty )
1366 self._update( rev = rev )
1367 if (msg) :
1368 msg = "%s\n" % msg
1369 self._create_branch( fullname, "%s%sCreated branch '%s'." % (msg, self.msg_prefix, fullname,), **kwarg )
1370 if (dirty) :
1371 self._unshelve( shelvedpatch_basename, force = dirty )
1372
1373
1374
1375 def _action_push( self, stream, *arg, **kwarg ) :
1376 """
1377 Conduct the I{push} action for the given stream. The workspace branch will be pushed to the remote repository.
1378
1379 @type stream: C{Stream}
1380 @param stream: Stream where you want to push the workspace branch
1381 """
1382 if (self.curr_workspace in stream) :
1383 self._push( new_branch = True, branch = [self.curr_workspace.fullname(),] )
1384 else :
1385 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (self.curr_workspace, stream,),
1386 "To push a %s branch, you must first update to it." % stream )
1387
1388
1389
1390 def _action_pull( self, stream, *arg, **kwarg ) :
1391 """
1392 Conduct the I{pull} action for the given stream. The workspace branch will be updated with changes pulled from the
1393 remote repository.
1394
1395 @type stream: C{Stream}
1396 @param stream: Stream where you want to pull for the workspace branch
1397 """
1398 try :
1399 branch = stream.get_fullname( arg[1] )
1400 except IndexError :
1401 branch = self.curr_workspace
1402 if (branch not in stream) :
1403 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (branch, stream,),
1404 "To pull a %s branch, you must first update to it." % stream )
1405
1406 self._pull( update = True, branch = [branch,] )
1407
1408
1409
1410 def _action_list( self, stream, *arg, **kwarg ) :
1411 """
1412 Print all open branches in the given stream.
1413
1414 @type stream: C{Stream}
1415 @param stream: Stream of which you want to display open branches
1416 """
1417 # Lists all open branches in this stream.
1418 open_branches = stream.branches()
1419 trunk = stream.trunk()
1420 if (trunk) :
1421 tags = ""
1422 if (stream == STREAM["master"]) :
1423 tags = self.latest_master_tags()
1424 tags = (", latest tags: %s" % ", ".join( tags )) if (tags) else ""
1425 self._print( "%s trunk: %s%s" % (stream, trunk, tags,) )
1426 if (open_branches) :
1427 self._print( "Open %s branches:" % stream )
1428 for e in open_branches :
1429 marker = "#" if (self._is_shelved( e ) ) else ""
1430 marker += "*" if (e == self.curr_workspace) else ""
1431 marker += " %s" % e.rev_node()
1432 self._print( str( e ) + marker, prefix = " " )
1433 else :
1434 self._print( "No open %s branches" % stream )
1435 if (kwarg.get( "closed" )) :
1436 closed_branches = stream.branches( "closed" )
1437 if (closed_branches) :
1438 self._print( "Closed %s branches:" % stream )
1439 closed_branches.sort( lambda x, y : y.ctx.rev() - x.ctx.rev() )
1440 for e in closed_branches :
1441 self.ui.write( "%-31s" % e.basename( stream ), label = "branches.closed" )
1442 self.ui.write( " %18s" % e.rev_node(), label = "log.changeset" )
1443 self.ui.write( " %s\n" % util.datestr( e.ctx.date(), format = "%Y-%m-%d %a %H:%M %1" ),
1444 label = "log.date" )
1445 bn = str( e )
1446 p1 = e.ctx
1447 while (p1.branch() == bn) :
1448 e = p1
1449 p1 = e.p1()
1450 description = e.description()
1451 msg_prefix = ("flow: ", "hgflow: ", "hg flow,", self.msg_prefix or "#@$(&*^$",)
1452 if (not (description.startswith( msg_prefix ))) :
1453 lines = [e.strip() for e in description.split( "\n" )]
1454 self.ui.note( " description: %s\n" % lines[0] )
1455 for line in lines[1:] :
1456 if (not (line.startswith( msg_prefix ))) :
1457 self.ui.note( " %s\n" % lines[0] )
1458 self.ui.note( "\n" )
1459 else :
1460 self._print( "No closed %s branches" % stream )
1461
1462
1463
1464 def _action_log( self, stream, *arg, **kwarg ) :
1465 """
1466 Show revision history of the specified branch.
1467
1468 @type stream: C{Stream},
1469 @param stream: Stream where the specified branch is
1470 """
1471 # User may specify a file with a relative path name. Since CWD has changed to the repository's root dir when the
1472 # `Flow' object was constructed, we need to restore the original dir to get the correct path name of the file.
1473 os.chdir( self.orig_dir )
1474 filenames = kwarg.pop( "file", [] )
1475 onstream = kwarg.pop( "onstream", False )
1476 closed = kwarg.pop( "closed", False )
1477 if (onstream) :
1478 filenames.extend( arg[1:] )
1479 branches = stream.branches( "all" if (closed) else "open" )
1480 if (stream._trunk) :
1481 branches.append( stream._trunk )
1482 else :
1483 # Case 1: hg flow <stream> log <basename>
1484 # - Shows the log of the "<stream>/<basename>" branch.
1485 # Case 2: hg flow <stream> log
1486 # - Case 2a: <stream> does not have a trunk
1487 # - Shows the log of the current workspace, which should be a branch in <stream>.
1488 # - Case 2b: <stream> has a trunk
1489 # - Case 2b1: Current workspace is a branch in <stream>.
1490 # - Shows the log of the current workspace.
1491 # - Case 2b2: Current workspace is not a branch in <stream>.
1492 # - Shows the log of <stream>'s trunk.
1493 # Case 3: hg flow <stream> log <filename>
1494 # - This case can be overriden by Case 1. Namely, if the <filename> happens to be the same as the
1495 # <basename>, the latter will take precedence.
1496 # - Case 3a: The current workspace is in <stream>
1497 # - Show the log of <filename> in the current workspace branch.
1498 # - Case 3b: The current workspace is not in <stream>, and <stream> has a trunk.
1499 # - Show the log of <filename> in <stream>'s trunk.
1500 # - Case 3c: The current workspace is not in <stream>, and <stream> has no trunk.
1501 # - Error
1502 try :
1503 branch = stream.get_branch( arg[1] )
1504 # Case 1
1505 except error.RepoLookupError :
1506 filenames.append( arg[1] )
1507 if (self.curr_workspace in stream) :
1508 # Case 3a
1509 branch = self.curr_workspace
1510 else :
1511 branch = stream.trunk()
1512 if (not branch) :
1513 # Case 3c
1514 raise AbortFlow( "Cannot determine branch in %s. Please be more specific." % stream )
1515 else :
1516 # Case 3b
1517 # Just be clear that we have covered Case 2b2.
1518 pass
1519 except IndexError :
1520 branch = stream.trunk()
1521 if (not branch) :
1522 # Case 2a
1523 branch = self.curr_workspace
1524 if (branch not in stream) :
1525 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (branch, stream,),
1526 "To show log of a %s branch, you must also specify its name." % stream )
1527 elif (self.curr_workspace in stream) :
1528 # Case 2b1
1529 branch = self.curr_workspace
1530 else :
1531 # Case 2b2
1532 # Just be clear that we have covered Case 2b2.
1533 pass
1534 # At this point, `branch` must be existent.
1535 branches = [branch,]
1536
1537 opts = {"branch" : branches,}
1538 opts.update( kwarg )
1539 self._log( *filenames, **opts )
1540
1541
1542
1543 def _action_abort( self, stream, *arg, **kwarg ) :
1544 """
1545 Abort the workspace branch.
1546
1547 @type stream: C{Stream}
1548 @param stream: Stream where the branch which you want to abort is
1549 """
1550 arg = arg[1:]
1551 msg = kwarg.pop( "message", "" )
1552 should_erase = kwarg.pop( "erase", False )
1553 onstream = kwarg.pop( "onstream", False )
1554 curr_workspace = self.curr_workspace
1555 if (msg) :
1556 msg = "%s\n" % msg
1557 if (curr_workspace.is_develop_trunk()) :
1558 raise AbortFlow( "You cannot abort the <develop> trunk." )
1559 if (arg) :
1560 if (len( arg ) > 1 or curr_workspace.basename() != arg[0]) :
1561 raise AbortFlow( "hgflow intentionally forbids aborting a non-workspace branch." )
1562 if (onstream) :
1563 branches = stream.branches()
1564 if (stream == STREAM["develop"]) :
1565 branches.remove( stream.trunk() )
1566 elif (stream._trunk) :
1567 branches.append( stream.trunk() )
1568 for branch in branches :
1569 if (should_erase) :
1570 self._strip( branch, self.repo.revs( "min(branch('%s'))" % branch )[0] )
1571 else :
1572 self._update( branch )
1573 self._commit( close_branch = True, message = "%s%sAborted %s %s." %
1574 (msg, self.msg_prefix, stream, branch.basename( stream, should_quote = True ),) )
1575 if (self.curr_workspace != self.orig_workspace and self._orig_workspace not in branches) :
1576 self._update( self.orig_workspace )
1577 else :
1578 if (curr_workspace.is_trunk( stream )) :
1579 curr_stream = curr_workspace.stream()
1580 raise AbortFlow( "You cannot abort a trunk.",
1581 "To abort '%s' as a branch, use `hg flow %s abort`." % (curr_workspace, curr_stream.name(),)
1582 )
1583 if (curr_workspace not in stream) :
1584 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (curr_workspace, stream,),
1585 "To abort a %s branch, you must first update to it." % stream )
1586 if (should_erase) :
1587 self._strip( curr_workspace, self.repo.revs( "min(branch('%s'))" % curr_workspace )[0] )
1588 else :
1589 self._commit( close_branch = True, message = "%s%sAborted %s '%s'." %
1590 (msg, self.msg_prefix, stream, curr_workspace.basename( stream ),) )
1591 self._update( stream.trunk( trace = True ) )
1592 self._unshelve()
1593
1594
1595
1596 def _action_promote( self, stream, *arg, **kwarg ) :
1597 """
1598 Promote the workspace branch to its destination stream(s). If there are uncommitted changes in the current branch,
1599 they will be automatically shelved before rebasing and unshelved afterwards.
1600
1601 @type stream: C{Stream}
1602 @param stream: Stream where the branch which you want to rebase is
1603 @type rev : C{str}
1604 @param rev : If provided, promote this revision instead of the head. The specified revision must be in the workspace
1605 branch.
1606 """
1607 rev = kwarg.pop( "rev", None )
1608 tag_name = kwarg.pop( "tag", None )
1609 message = kwarg.pop( "message", None )
1610 message = (message + "\n") if (message) else ""
1611 orig_workspace = self.curr_workspace
1612 has_shelved = False
1613
1614 if (orig_workspace not in stream) :
1615 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (orig_workspace, stream,),
1616 "To promote a %s branch, you must first update to it." % stream )
1617
1618 if (rev) :
1619 # Ensures `rev` is in workspace branch.
1620 promoted_branch = Branch( self.ui, self.repo, rev )
1621 promoted_rev = rev
1622 promoted_node = promoted_branch.ctx.node()
1623 if (promoted_branch != orig_workspace) :
1624 raise AbortFlow( "Revision %s is not in workspace branch." % rev )
1625 else :
1626 promoted_branch = orig_workspace
1627 promoted_rev = orig_workspace
1628 promoted_ctx = promoted_branch.ctx
1629 promoted_node = promoted_ctx.node()
1630 # `promoted_node' is `None' if the `promote_ctx' is an instance of `workingctx'.
1631 while (promoted_node is None) :
1632 promoted_ctx = promoted_ctx._parents[0]
1633 promoted_node = promoted_ctx.node()
1634
1635 if (arg[1:]) :
1636 if (not has_shelved) :
1637 self._shelve()
1638 has_shelved = True
1639 for dest in arg[1:] :
1640 self._update( dest )
1641 self._merge ( promoted_rev )
1642 self._commit( message = message + ("%sPromoted %s '%s' (%s) to '%s'." %
1643 (self.msg_prefix, stream, promoted_branch.basename( stream ),
1644 short( promoted_node ), dest,)), **kwarg )
1645 if (tag_name) :
1646 self._tag( tag_name, **kwarg )
1647 else :
1648 destin = [STREAM["master"],] if (STREAM["develop"] == stream) else stream.destin()
1649 for s in destin :
1650 if (s == stream) :
1651 continue
1652 trunk = s.trunk()
1653 if (trunk) :
1654 if (not has_shelved) :
1655 self._shelve()
1656 has_shelved = True
1657 self._update( trunk )
1658 self._merge ( promoted_rev )
1659 self._commit( message = message + ("%sPromoted %s '%s' (%s) to '%s'." %
1660 (self.msg_prefix, stream, promoted_branch.basename( stream ),
1661 short( promoted_node ), trunk,)), **kwarg )
1662 if (tag_name) :
1663 self._tag( tag_name, **kwarg )
1664 else :
1665 self._error( "Cannot determine promote destination." )
1666 return
1667 if (orig_workspace != self.curr_workspace) :
1668 self._update( orig_workspace )
1669 self._unshelve()
1670
1671
1672
1673 def _action_rebase( self, stream, *arg, **kwarg ) :
1674 """
1675 Rebase the workspace branch to its parent branch. If there are uncommitted changes in the current branch, they will be
1676 automatically shelved before rebasing and unshelved afterwards.
1677
1678 @type stream: C{Stream}
1679 @param stream: Stream where the branch which you want to rebase is
1680 @type dest : C{str}
1681 @param dest : If provided, use its value as the destination of rebasing. The value must be a changeset of the parent
1682 branch, otherwise it will trigger an error. If not provided, use the tip of the parent branch as the
1683 destination of rebasing.
1684 """
1685 dest = kwarg.get( "dest" )
1686 onstream = kwarg.pop( "onstream", False )
1687 if (onstream) :
1688 if (not dest) :
1689 dest = stream.source().trunk( trace = True )
1690 branches = stream.branches()
1691 if (stream == STREAM["develop"]) :
1692 branches.remove( stream.trunk() )
1693 elif (stream._trunk) :
1694 branches.append( stream.trunk() )
1695 self._check_rebase()
1696 self._shelve()
1697 for branch in branches :
1698 if (dest != branch) :
1699 self._rebase( base = branch, dest = dest, keepbranches = True )
1700 self._unshelve()
1701 else :
1702 curr_workspace = self.curr_workspace
1703 if (not dest) :
1704 dest = stream.trunk( trace = True )
1705 if (curr_workspace not in stream) :
1706 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (curr_workspace, stream,),
1707 "To rebase a %s branch, you must first update to it." % stream )
1708 if (curr_workspace.is_develop_trunk()) :
1709 raise AbortFlow( "You cannot rebase the <develop> trunk." )
1710 if (dest == curr_workspace) :
1711 self._warn( "No effects from rebasing a branch to itself" )
1712 else :
1713 self._check_rebase()
1714 self._shelve()
1715 self._rebase( base = curr_workspace, dest = dest, keepbranches = True )
1716 self._unshelve()
1717
1718
1719
1720 def _action_rename( self, stream, *arg, **kwarg ) :
1721 """
1722 Rename the workspace branch to a new basename. If there are uncommitted changes in the current branch, they will be
1723 automatically shelved before renaming and unshelved afterwards.
1724 Under the hood this action will create a new branch and copy (or graft) all commits in the workspace branch to the new
1725 branch and then erase the workspace branch.
1726
1727 @type stream: C{Stream}
1728 @param stream: Stream where the branch which you want to rename is
1729 @type to : C{str}
1730 @param to : Its value should be the new basename of the workspace branch.
1731 """
1732 new_branch_name = kwarg.pop( "to", None )
1733 curr_workspace = self.curr_workspace
1734 if (not new_branch_name) :
1735 raise AbortFlow( "Please specify the new base name of this branch via the `-t` option." )
1736 if (curr_workspace not in stream) :
1737 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (curr_workspace, stream,),
1738 "To rename a %s branch, you must first update to it." % stream )
1739 if (curr_workspace.is_trunk( stream )) :
1740 raise AbortFlow( "You cannot rename the trunk of %s." % stream )
1741 if (new_branch_name == curr_workspace.basename( stream )) :
1742 self._warn( "No effects because the supposed new basename turns out to be the same as the current one." )
1743 else :
1744 cfn = curr_workspace.fullname()
1745 brn = "branch(%s)" % cfn
1746 rev = "min(%s)" % brn
1747 ctx = self.repo[self.repo.revs( rev )[0]]
1748 nfn = stream.get_fullname( new_branch_name )
1749 msg = ctx.description()
1750 msg = msg.replace( cfn, nfn )
1751 self._shelve()
1752 self._update( self.repo.revs( "%s^" % rev )[0] )
1753 self._create_branch( nfn, msg, user = ctx.user(), date = util.datestr( ctx.date() ) )
1754 self._graft( curr_workspace, **kwarg )
1755 self._unshelve( cfn )
1756 self._strip( curr_workspace, int( ctx ) )
1757
1758
1759
1760 def _update_workspace( self, stream, branch, verbose = True ) :
1761 """
1762 Update the workspace to the given branch. Shelving and unshelving will be conducted automatically.
1763
1764 @type stream: C{Stream}
1765 @param stream: Stream where the branch which you are updating the workspace to is
1766 @type branch: C{Branch} or C{None}
1767 @param branch: Branch to update the workspace to. No effects if it is C{None}.
1768 """
1769 if (not branch) :
1770 return
1771
1772 if (branch == self.curr_workspace) :
1773 if (verbose) :
1774 self._print( "You are already in %s %s." % (stream, branch.basename( stream, should_quote = True ),) )
1775 else :
1776 self._print( "Update workspace to %s %s." % (stream, branch.basename( stream, should_quote = True ),) )
1777 self._shelve()
1778 self._update( branch )
1779 self._unshelve()
1780 self._print( "Parent of working directory: %s" % branch.rev_node() )
1781
1782
1783
1784 def _action_other( self, stream, *arg, **kwarg ) :
1785 """
1786 If the action is the name of a branch in the given stream, we will update workspace to that branch; otherwise, the
1787 action is considered as an error.
1788
1789 @type stream: C{Stream}
1790 @param stream: Stream where the branch that we will switch to is
1791 """
1792 try :
1793 name = arg[0]
1794 branch = stream.get_branch( name )
1795 if (branch.is_closed()) :
1796 self._warn( "%s '%s' has been closed." % (stream, name,) )
1797 self._update_workspace( stream, branch )
1798 except error.RepoLookupError :
1799 misspelling = difflib.get_close_matches( name, Flow.ACTION_NAME, 3, 0.7 )
1800 note = ("Did you mean: %s?" % " or ".join( misspelling )) if (misspelling ) else None
1801 note = ("Did you mean: finish or abort?") if ("close" == name) else note
1802 if (stream != STREAM["master"]) :
1803 note = ("If you meant to create a new branch called '%s' in %s" % (name, stream,),
1804 "try command:", " hg flow %s start %s" % (stream.name(), name,),) if (not note) else note
1805 raise AbortFlow( "Invalid action or unknown branch in %s: '%s'" % (stream, name,), note = note )
1806
1807
1808
1809 def _commit_change( self, opt, commit_hint, is_erasing = False ) :
1810 """
1811 Commit the changes in the workspace.
1812 Note that this method can potentially mutate C{opt}. Specifically, it will delete the C{commit} and C{message} keys if
1813 they present in C{opt}.
1814
1815 @type opt: C{dict}
1816 @param opt: Option dictionary. Recognizable keys are C{commit} and C{message}. The value of C{commit} should be a
1817 boolean, indicating whether or not to perform committing. The value of C{message} should be a string, which
1818 will be used as the commit message. It is OK for both of the options to be missing. But it would trigger
1819 an error if C{message} is given without C{commit} set to true. There is no special treatment on other
1820 keys, and they will be passed to the C{hg commit} command as is.
1821
1822 @rtype : C{bool}
1823 @return: Return `True' if committing was successfully done, or `False' if it was not.
1824 """
1825 if (opt.get( "commit" )) :
1826 del opt["commit"]
1827 msg = opt.get( "message" )
1828 if (msg is None) :
1829 opt["force_editor"] = True
1830 opt["message"] = "\n\nHG: flow: %s" % commit_hint
1831 self._commit( **opt )
1832 del opt["message"]
1833 if (msg is None) :
1834 del opt["force_editor"]
1835 return True
1836 elif (opt.get( "message" )) :
1837 if (is_erasing) :
1838 del opt["message"]
1839 else :
1840 raise AbortFlow( "Cannot use the specified commit message.", "Did you forget to specify the -c option?" )
1841 return False
1842
1843
1844
1845 def _action_finish( self, stream, *arg, **kwarg ) :
1846 """
1847 Finish a branch in the given stream. The current workspace must be in the branch to be finished, otherwise an error
1848 will be triggered. The default behavior of finish action is the following:
1849 1. close the branch.
1850 2. merge the branch to the C{destin} streams.
1851
1852 @type stream: C{Stream}
1853 @param stream: Stream where the branch that we will finish is
1854 """
1855 try :
1856 tag_name = arg[1]
1857 self._warn( "You just specified the <tag-name> using the deprecated syntax:" )
1858 self._warn( " hg flow <stream> finish <tag-name> [<options>]" )
1859 self._warn( "Try using the new syntax to do that in the future: hg flow <stream> finish -t <tag-name>" )
1860 self._warn( "Note that hgflow intentionally forbids finishing a non-workspace branch." )
1861 except IndexError :
1862 tag_name = None
1863
1864 message = kwarg.get( "message", None )
1865 tag_name = kwarg.pop( "tag", tag_name )
1866 onstream = kwarg.pop( "onstream", False )
1867 should_erase = kwarg.pop( "erase", False )
1868 curr_workspace = self.curr_workspace
1869 curr_stream = curr_workspace.stream()
1870 name = curr_workspace.basename( stream, should_quote = True )
1871 tag_name_orig = tag_name
1872 tag_name = tag_name if (tag_name) else (self.version_prefix + name[1:-1])
1873 develop_stream = STREAM["develop"]
1874
1875 if (should_erase) :
1876 if (onstream ) : raise AbortFlow( "'--erase' cannot be used together with '--onstream'." )
1877 if (message is None) : raise AbortFlow( "'--message' is required when '--erase' is used." )
1878 self._check_strip()
1879
1880 if (onstream) :
1881 if (stream in [develop_stream, STREAM["support"], STREAM["hotfix"], STREAM["release"],]) :
1882 raise AbortFlow( "You cannot finish %s." % stream )
1883 branches = stream.branches()
1884 if (stream._trunk) :
1885 substream = Stream.gen( self.ui, self.repo, stream.name() )
1886 for branch in branches :
1887 self._update( branch )
1888 self._action_finish( substream, *arg, **kwarg )
1889 self._update( stream.trunk() )
1890 self._action_finish( stream, *arg, **kwarg )
1891 else :
1892 for branch in branches :
1893 self._update( branch )
1894 self._action_finish( stream, *arg, **kwarg )
1895 return
1896
1897 if (curr_workspace.is_develop_trunk()) :
1898 raise AbortFlow( "You cannot finish the <develop> trunk." )
1899 elif (curr_workspace not in stream) :
1900 raise AbortFlow( "Your workspace is '%s' branch, which is not in %s." % (curr_workspace, stream,),
1901 "To finish a %s branch, you must first update to it." % stream )
1902
1903 # Merges the workspace to its `destin` streams.
1904 destin_with_trunk = []
1905 destin_without_trunk = []
1906 final_branch = None
1907 for s in stream.destin() :
1908 trunk = s.trunk()
1909 if (trunk == curr_workspace) :
1910 pass
1911 elif (trunk) :
1912 destin_with_trunk.append( s )
1913 else :
1914 destin_without_trunk.append( s )
1915
1916 if (should_erase) :
1917 if (len( destin_with_trunk + destin_without_trunk ) > 1) :
1918 raise AbortFlow( "'--erase' cannot be applied to branches with multiple merge destinations." )
1919
1920 # Commits changes (if any) in the current branch.
1921 is_commit_done = self._commit_change( kwarg, "Finishing '%s' branch" % curr_workspace, should_erase )
1922
1923 # If the commit was done successfully, we don't check against uncommitted changes.
1924 # This is particularly needed for dry run.
1925 if (not is_commit_done and self._has_uncommitted_changes()) :
1926 raise AbortFlow( "Cannot finish '%s' branch because it has uncommitted changes." % curr_workspace )
1927
1928 # For destin streams without trunks, we need to create a branch in each of these destin streams.
1929 # Each newly created branch will be from the current branch and named after the pattern:
1930 # <stream-prefix>/<current-branch-basename>. Afterwards, the current branch will be closed.
1931 # Note that there is no need to merge the current branch because the new branch is created from it.
1932 for s in destin_without_trunk :
1933 trunk = s.trunk()
1934 so_name = "" if ("trunk" == curr_workspace.basename( stream )) else ("/" + curr_workspace.basename( stream ))
1935 so = Stream ( self.ui, self.repo, stream.name() + so_name, trunk = curr_workspace.fullname() )
1936 so = Stream.gen( self.ui, self.repo, "%s:%s" % (so.name(), s.name(),), check = True )
1937 basename = curr_workspace.basename()
1938 self.action( so, "start", basename )
1939 final_branch = s.get_fullname( basename )
1940
1941 if (destin_with_trunk or destin_without_trunk) :
1942 # If either list is not empty.
1943 self._update( curr_workspace )
1944 self._commit( close_branch = True, message = "%sClosed %s %s." % (self.msg_prefix, stream, name,), **kwarg )
1945 else :
1946 # If both lists are empty.
1947 if (stream == STREAM["support"]) :
1948 self._update( curr_workspace )
1949 self._commit( close_branch = True, message = "%sClosed %s %s." % (self.msg_prefix, stream, name,), **kwarg )
1950 final_branch = STREAM["master"].trunk()
1951 else :
1952 self._print( "All open branches in %s are finished and merged to its trunk." % stream )
1953
1954 if (tag_name_orig and (STREAM["master"] not in destin_with_trunk)) :
1955 self._warn( "You specified a tag name, but it has effect only when the workspace branch is merged to <master>." )
1956
1957 for s in destin_with_trunk :
1958 trunk = s.trunk()
1959 self._update( trunk )
1960 self._merge ( curr_workspace )
1961 self._commit( message = "%sMerged %s %s to %s ('%s')." % (self.msg_prefix, stream, name, s, trunk,), **kwarg )
1962 if (s == STREAM["master"]) :
1963 self._tag( tag_name, force = True )
1964 elif (s in develop_stream and s is not develop_stream) :
1965 tr_stream = trunk.stream()
1966 for ss in tr_stream.destin() :
1967 if (ss == develop_stream) :
1968 dvtrunk = develop_stream.trunk()
1969 tr_name = trunk.basename( ss )
1970 self._update( dvtrunk )
1971 self._merge ( trunk )
1972 self._commit( message = "%sMerged <develop/%s:%s> %s to %s ('%s')." %
1973 (self.msg_prefix, tr_name, stream.name(), name, ss, dvtrunk,), **kwarg )
1974 if (final_branch) :
1975 self._update( final_branch )
1976 if (should_erase) :
1977 rev = "p1(.)"
1978 rev = mercurial.scmutil.revsingle( self.repo, rev ).rev()
1979 self._update( "tip" )
1980 self._update( rev )
1981 self._revert( rev = "-1", all = True )
1982 self._strip ( curr_workspace, self.repo.revs( "min(branch('%s'))" % curr_workspace )[0] )
1983 self._commit( message = message, **kwarg )
1984 self._unshelve()
1985
1986
1987
1988 def _execute_action( self, stream, *arg, **kwarg ) :
1989 """
1990 Execute an action on the given stream. If no action is specified, the action will default to I{list}
1991 (see L{_action_list}). The default behavior of an action is defined by the C{_action_*} methods. Custom action behavior
1992 can be given through the C{action_func} parameter.
1993
1994 @type stream: C{Stream}
1995 @param stream: Stream where we will execute the action
1996 @type action_func: C{dict}
1997 @param action_func: Custom action methods. Key (C{str}) is action name, and value is a function that define the
1998 behavior of the custom action.
1999 """
2000 try :
2001 action = arg[0]
2002 except IndexError :
2003 action = "list"
2004
2005 action_func = {
2006 "start" : self._action_start,
2007 "finish" : self._action_finish,
2008 "push" : self._action_push,
2009 "publish" : self._action_push,
2010 "pull" : self._action_pull,
2011 "list" : self._action_list,
2012 "log" : self._action_log,
2013 "abort" : self._action_abort,
2014 "promote" : self._action_promote,
2015 "rebase" : self._action_rebase,
2016 "rename" : self._action_rename,
2017 "other" : self._action_other,
2018 }
2019
2020 custom_action_func = kwarg.pop( "action_func", {} )
2021 action_func.update( custom_action_func )
2022
2023 return action_func.get( action, self._action_other )( stream, *arg, **kwarg )
2024
2025
2026
2027 def action( self, stream, *arg, **kwarg ) :
2028 """
2029 Execute action on the stream.
2030
2031 @type stream: C{Stream}
2032 @param stream: Stream where we will execute the action
2033 """
2034 if (len( arg ) > 0) :
2035 action = arg[0]
2036 if (stream == STREAM["master"]) :
2037 if (action in ["start", "finish", "abort", "rebase",]) :
2038 raise AbortFlow( "Invalid action for <master>" )
2039 else :
2040 trunk = stream.trunk()
2041 self._update_workspace( stream, trunk, verbose = False )
2042 self._execute_action( stream, *arg, **kwarg )
2043
2044
2045
2046 def print_version( self, *arg, **kwarg ) :
2047 """
2048 Print flow's version and then quit.
2049 """
2050 self._print( "version %s" % VERSION )
2051
2052
2053
2054 def unshelve( self, *arg, **kwarg ) :
2055 """
2056 Unshelve the previously shelved changes.
2057 """
2058 self.autoshelve = True
2059 self._unshelve( *arg, **kwarg )
2060
2061
2062
2063 def print_open_branches( self, *arg, **kwarg ) :
2064 """
2065 Print open branches in each stream.
2066
2067 The currently active branch will be marked with a * symbol. Branches where there are shelved changes will be marked
2068 with a # symbol.
2069 """
2070 self._print( "Currently open branches:" )
2071 curr_workspace = self.curr_workspace
2072 stream_names = ["master", "develop", "feature", "release", "hotfix", "support",]
2073 all_branches = self._branches()
2074 name_branches = {}
2075 for branch in all_branches :
2076 name_branches.setdefault( branch.fullname(), [] ).append( branch )
2077 name_branches = sorted( name_branches.items() )
2078 for sn in stream_names :
2079 stream = STREAM[sn]
2080 trunk = stream.trunk()
2081 open_branches_in_stream = []
2082 for name, heads in name_branches :
2083 e = heads[0]
2084 if (e in stream) :
2085 open_branches_in_stream.append( e )
2086 if (trunk is None and not open_branches_in_stream) :
2087 continue
2088 self._print( "%-9s: " % stream, newline = False )
2089 if (trunk) :
2090 marker = "#" if (self._is_shelved( trunk )) else ""
2091 marker += "*" if (trunk == curr_workspace ) else ""
2092 self.ui.write( "%s%s " % (trunk, marker,) )
2093 if (trunk in open_branches_in_stream) :
2094 # We need this check because the `trunk' could be closed. See Issue#34.
2095 open_branches_in_stream.remove( trunk )
2096 if (open_branches_in_stream) :
2097 for e in open_branches_in_stream :
2098 marker = "#" if (self._is_shelved( e )) else ""
2099 marker += "*" if (e == curr_workspace ) else ""
2100 self.ui.write( "%s%s " % (e, marker,) )
2101 self.ui.write( "\n" )
2102 if (sum( [len( heads ) - 1 for name, heads in name_branches] )) :
2103 self._print( "\n", newline = False )
2104 self._print( "Multihead branches:" )
2105 for name, heads in name_branches :
2106 if (len( heads ) > 1) :
2107 self._print( " %s" % name )
2108 for head in heads :
2109 self._print( " %s" % head.rev_node() )
2110
2111
2112
2113 def init( self, *arg, **kwarg ) :
2114 """
2115 Initialize flow.
2116 """
2117 config_fname = os.path.join( self.repo.root, CONFIG_BASENAME )
2118 master_stream = "default"
2119 hotfix_stream = "hotfix/"
2120 develop_stream = "develop"
2121 feature_stream = "feature/"
2122 release_stream = "release/"
2123 support_stream = "support/"
2124 has_goodconfig = False
2125
2126 # Fetches existing condition
2127 if (os.path.isfile( config_fname )) :
2128 self._print( "Flow was already initialized for workspace:" )
2129 cfg = config.config()
2130 cfg.read( config_fname )
2131 SECTION = CONFIG_SECTION_BRANCHNAME
2132 try :
2133 master_stream = cfg.get( SECTION, "master" )
2134 develop_stream = cfg.get( SECTION, "develop" )
2135 feature_stream = cfg.get( SECTION, "feature" )
2136 release_stream = cfg.get( SECTION, "release" )
2137 hotfix_stream = cfg.get( SECTION, "hotfix" )
2138 support_stream = cfg.get( SECTION, "support" )
2139 has_goodconfig = True
2140 except ConfigParser.NoSectionError :
2141 self._error( "Section [%s] not found in configuration file: %s" % (SECTION, config_fname,) )
2142 self._error( "Your configuration file is probably in old format or corrupt." )
2143 except ConfigParser.NoOptionError, e :
2144 self._error( "%s" % e )
2145 self._error( "Your configuration file is probably corrupt." )
2146
2147 if (has_goodconfig) :
2148 self._print( "Repository-specific configuration:" )
2149 self._print( "<master> trunk: '%s'" % master_stream, prefix = " " )
2150 self._print( "<develop> trunk: '%s'" % develop_stream, prefix = " " )
2151 self._print( "<feature> branch prefix: '%s'" % feature_stream, prefix = " " )
2152 self._print( "<release> branch prefix: '%s'" % release_stream, prefix = " " )
2153 self._print( "<hotfix> branch prefix: '%s'" % hotfix_stream, prefix = " " )
2154 self._print( "<support> branch prefix: '%s'" % support_stream, prefix = " " )
2155
2156 autoshelve = None
2157 if (self.ui.has_section( "hgflow" ) or self.ui.has_section( "flow" )) :
2158 self._print( "Global configuration:" )
2159 autoshelve = self.ui.configbool( "hgflow", "autoshelve" )
2160 if (self.ui.has_section( "flow" )) :
2161 autoshelve = self.ui.configbool( "flow", "autoshelve" )
2162 if (not (autoshelve is None)) :
2163 self._print( "autoshelve: %s" % ("on" if (autoshelve) else "off"), prefix = " " )
2164
2165 # Shall we continue if there already exists a configuration file?
2166 if (has_goodconfig and not kwarg.get( "force" )) :
2167 return
2168
2169 print
2170 mq = None
2171 try :
2172 mq = extensions.find( "mq" )
2173 except KeyError :
2174 self._warn( "The 'mq' extension is deactivated. You cannot use some features of flow." )
2175 print
2176
2177 workspace = self.curr_workspace
2178 branches = self._branches()
2179 if (len( branches ) > 1) :
2180 self._warn( "You have the following open branches. Will initialize flow for all of them." )
2181 for branch in branches :
2182 if (branch == workspace) :
2183 self._warn( " " + branch.fullname() + " (active)" )
2184 else :
2185 self._warn( " %s" % branch.fullname() )
2186 print
2187
2188 # 'status' method returns a 7-member tuple:
2189 # 0 modified, 1 added, 2 removed, 3 deleted, 4 unknown(?), 5 ignored, and 6 clean
2190 orig_repo_status = self.repo.status()[:4]
2191 for e in orig_repo_status :
2192 try :
2193 e.remove( CONFIG_BASENAME )
2194 except ValueError :
2195 pass
2196
2197 if (any( orig_repo_status )) :
2198 if (len( branches ) > 1 and not mq) :
2199 raise AbortFlow( "Your workspace has uncommitted changes. Cannot initialize flow for all",
2200 " open branches. You can either commit the changes or install the 'mq'",
2201 " extension, and then try again." )
2202
2203 def get_input( stream_name, default ) :
2204 while (True) :
2205 answer = self.ui.prompt( "Branch name for %s stream: [%s]" % (stream_name, default,), default = default )
2206 if (answer.find( ':' ) > -1) :
2207 self._error( "Illegal symbol ':' in branch name" )
2208 else :
2209 return answer
2210
2211 if (not kwarg.get( "default" )) :
2212 master_stream = get_input( "master", master_stream )
2213 develop_stream = get_input( "develop", develop_stream )
2214 feature_stream = get_input( "feature", feature_stream )
2215 release_stream = get_input( "release", release_stream )
2216 hotfix_stream = get_input( "hotfix", hotfix_stream )
2217 support_stream = get_input( "support", support_stream )
2218
2219 if (autoshelve is None) :
2220 self._print( """
2221 When you switch to another branch, flow can automatically shelve uncommitted
2222 changes in workpace right before switching. Later when you switch back, flow can
2223 automatically unshelve the changes to the workspace. This functionality is
2224 called autoshelve. You need the 'mq' extension to use it.""" )
2225 answer = self.ui.prompt( "Do you want to turn it on? [Yes] ", default = "y" )
2226 answer = True if (answer.lower() in ["yes", "y", "",]) else False
2227 if (answer) :
2228 self._print( """
2229 Here is what you need to do:
2230 To turn it on for only this repository, edit your <repository-root>/.hg/hgrc
2231 file by adding the following lines:
2232 [flow]
2233 autoshelve = true
2234 You can turn it on for all of your repositories by doing the same edition to
2235 your $HOME/.hgrc file. To turn it off, just edit the corresponding file and
2236 replace 'true' with 'false'.
2237 """ )
2238 self.ui.prompt( _("Press Enter to continue initialization...") )
2239
2240 # Creates configuration.
2241 cfg_contents = ["[%s]" % CONFIG_SECTION_BRANCHNAME,
2242 "master = %s" % master_stream,
2243 "develop = %s" % develop_stream,
2244 "feature = %s" % feature_stream,
2245 "release = %s" % release_stream,
2246 "hotfix = %s" % hotfix_stream,
2247 "support = %s" % support_stream,]
2248 def write_config() :
2249 # Writes the configuration in the current branch.
2250 if (not commands.dryrun()) :
2251 with open( config_fname, "w" ) as fh :
2252 print >> fh, "\n".join( cfg_contents )
2253 repo_status = self.repo.status( unknown = True )
2254 if (CONFIG_BASENAME in repo_status[0]) :
2255 self._commit( config_fname, message = "flow initialization: Modified configuration file." )
2256 elif (CONFIG_BASENAME in repo_status[4]) :
2257 self._add ( config_fname )
2258 self._commit( config_fname, message = "flow initialization: Added configuration file." )
2259
2260 write_config()
2261
2262 master_trunk, is_open = self._find_branch( master_stream )
2263 if (master_trunk and not is_open) :
2264 self._warn( "Branch \"%s\" is currently closed." % master_stream )
2265 self._warn( "Will reopen and use it as <master> trunk." )
2266 branches.append( master_trunk )
2267
2268 develop_trunk, is_open = self._find_branch( develop_stream )
2269 if (develop_trunk and not is_open) :
2270 self._warn( "Branch \"%s\" is currently closed." % develop_stream )
2271 self._warn( "Will reopen and use it as <develop> trunk." )
2272 branches.append( develop_trunk )
2273
2274 # Writes the configuration in all the other branches.
2275 self.autoshelve = True
2276 self._shelve()
2277
2278 if (len( branches ) > 1) :
2279 for branch in branches :
2280 if (branch == workspace) : continue
2281 self._update( branch )
2282 write_config()
2283 self._update( workspace )
2284
2285 # Creates 'master' and 'develop' streams if they don't yet exist.
2286 if (master_trunk is None) :
2287 self._create_branch( master_stream, "flow initialization: Created <master> trunk: %s." % master_stream )
2288 if (develop_trunk is None) :
2289 self._create_branch( develop_stream, "flow initialization: Created <develop> trunk: %s." % develop_stream )
2290
2291 self._update( workspace )
2292 self._unshelve()
2293
2294
2295
2296 def upgrade( self, *arg, **kwarg ) :
2297 """
2298 Upgrade older version to the latest version.
2299 """
2300 self._print( "Upgrade flow's configuration file from v0.9.4 (or older) to v0.9.5 (or latter)." )
2301 self._print( "Renaming file '%s' to '%s' in all open branches..." % (OLD_CONFIG_BASENAME, CONFIG_BASENAME,) )
2302 config_fname = os.path.join( self.repo.root, CONFIG_BASENAME )
2303 old_config_fname = os.path.join( self.repo.root, OLD_CONFIG_BASENAME )
2304 workspace = self.curr_workspace
2305 for branch in self._branches() :
2306 self._print( " Branch '%s'..." % branch )
2307 self._update( branch )
2308 if (os.path.isfile( old_config_fname )) :
2309 self._rename( old_config_fname, config_fname, force = True )
2310 self._commit( message = "flow upgrade: Renamed flow's configuration file from '%s' to '%s'." %
2311 (OLD_CONFIG_BASENAME, CONFIG_BASENAME,) )
2312 self._update( workspace )
2313 self._print( "Upgrading done" )
2314
2315
2316
2317 def flow_cmd( ui, repo, cmd = None, *arg, **kwarg ) :
2318 """Flow is a Mercurial extension to support the generalized Driessen's branching model.
2319
2320 actions:
2321
2322 - start Open a new branch in the stream.
2323 - finish Close workspace branch and merge it to destination stream(s).
2324 - push Push workspace branch to the remote repository.
2325 - publish Same as `push`
2326 - pull Pull from the remote repository and update workspace branch.
2327 - list List all open branches in the stream.
2328 - log Show revision history of branch.
2329 - promote Merge workspace to other branches. (not closing any branches.)
2330 - rebase Rebase workspace branch to its parent branch.
2331 - rename Rename workspace branch to a new basename.
2332 - abort Abort branch. Close branch without merging.
2333
2334 If no action is specified by user, the action will default to `list`. If a
2335 branch name (instead of action) is given after the stream name, Flow will
2336 switch the current workspace to the branch.
2337
2338 commands:
2339
2340 - init Initialize flow.
2341 - unshelve Unshelve the previously shelved changes for workspace branch.
2342 - upgrade Upgrade the configuration file to v0.9.5 or later.
2343 - help Show help for a specific topic. Example: `hg flow help @help`
2344 - version Show flow's version number.
2345 """
2346 # Supresses bookmarks, otherwise if the name of a bookmark happens to be the same as a named branch, hg will use the
2347 # bookmark's revision.
2348 repo._bookmarks = {}
2349
2350 flow = Flow( ui, repo, cmd in ["init", "upgrade", "help",] )
2351 func = {
2352 "init" : flow.init,
2353 "upgrade" : flow.upgrade,
2354 "unshelve" : flow.unshelve,
2355 "help" : Help( ui, repo ).print_help,
2356 "version" : flow.print_version,
2357 None : flow.print_open_branches,
2358 }
2359
2360 commands.use_quiet_channel( kwarg.get( "history" ) )
2361 commands.dryrun ( kwarg.get( "dry_run" ) )
2362
2363 if (kwarg.get( "dry_run" )) :
2364 _print( ui, "This is a dry run." )
2365 commands.use_quiet_channel( True )
2366
2367 # Registers common options (such as "user").
2368 common_opts = {}
2369 for e in ["user",] :
2370 v = kwarg.get( e )
2371 if (v) :
2372 common_opts[e] = v
2373 commands.reg_common_options( common_opts )
2374
2375 # - Up to this point, `cmd' is a name of command or stream, or `None'.
2376 # - We assign `stream' to be a stream name (or `None') and `cmd' to be a name of command or action.
2377 # - When `arg' is a 0-tuple, `cmd' should be "list" as the default action. We use `arg + ("list",)' to ensure we can get
2378 # the first element.
2379 stream, cmd = (None, cmd) if (cmd in func) else (cmd, (arg + ("list",))[0] )
2380
2381 try :
2382 # Constructs a `Stream' objects.
2383 # This will also check the validity of the part of user's input that is supposed to specify a stream.
2384 if (isinstance( stream, str )) :
2385 stream = Stream.gen( ui, repo, stream, check = True )
2386
2387 # Checks the options for all commands and actions.
2388 kwarg = _getopt( ui, cmd, kwarg )
2389 stamp = kwarg.pop( "stamp", None )
2390 if (stamp) :
2391 def stamp_commit_message( opts ) :
2392 msg = opts["message"]
2393 if (0 > msg.lower().find( stamp.lower() )) :
2394 msg += " %s" % stamp
2395 opts["message"] = msg
2396 return opts
2397 commands.reg_option_mutator( "commit", stamp_commit_message )
2398
2399 func = func.get( cmd, lambda *arg, **kwarg : flow.action( stream, *arg, **kwarg ) )
2400 func( *arg, **kwarg )
2401 except AbortFlow, e :
2402 errmsg = e.error_message()
2403 _error( ui, *errmsg )
2404 if (getattr( e, "note", None )) :
2405 _note( ui, *((e.note,) if (isinstance( e.note, str )) else e.note), via_quiet = True )
2406 elif (errmsg[0].startswith( "Stream not found" )) :
2407 misspelling = difflib.get_close_matches( stream, ["init", "upgrade", "unshelve", "help", "version",], 3, 0.7 )
2408 note = ("Did you mean the command: %s?" % " or ".join( misspelling )) if (misspelling ) else None
2409 note = ("Did you mean the command: init?") if ("install" == stream) else note
2410 note = ("Did you mean the command: upgrade?") if ("update" == stream) else note
2411 if (note) :
2412 _note( ui, note, via_quiet = True )
2413
2414 if (ui.tracebackflag) :
2415 if (hasattr( e, "traceback" )) :
2416 ei = e.traceback
2417 sys.excepthook( ei[0], ei[1], ei[2] )
2418 print
2419 ei = sys.exc_info()
2420 sys.excepthook( ei[0], ei[1], ei[2] )
2421
2422 commands.print_history()
2423
2424 try :
2425 os.chdir( flow.orig_dir )
2426 except :
2427 _print( ui, "The original dir is gone in file system (probably due to updating branch)." )
2428 _print( ui, "You are now in the root dir of the repository." )
2429
2430
2431
2432 # On Windows, a topic should be wrapped with quotes.
2433 if ("nt" == os.name) :
2434 flow_cmd.__doc__ = flow_cmd.__doc__.replace( "help @help", 'help "@help"' )
2435
2436
2437
2438 class Help( object ) :
2439 """
2440 Online help system
2441 We define all help topics within this class.
2442 We support text effects on help message. See C{colortable} for predefined effects as C{flow.help.*}. To make it easy to use
2443 text effects, we invented a primitive markdown syntax. For now, we support only the C{flow.help.code}, which will be
2444 applied to text wrapped with '{{{' and '}}}'.
2445 """
2446
2447 SHORT_USAGE = """
2448 flow: a Mercurial workflow extension
2449
2450 Usage: {{{hg flow {<stream> [<action> [<arg>...]] | <command>} [<option>...]}}}
2451
2452 """ + flow_cmd.__doc__
2453
2454 TOPIC = {
2455 "@deprecated" : """
2456 The following item has been deprecated in this release and will be removed in
2457 the future:
2458 * [hgflow] The '[hgflow]' section name in hg's configuration file has been
2459 renamed to '[flow]'.
2460 * Syntax: hg flow <stream> finish {{{<tag-name>}}}
2461 Replacement syntax: hg flow <stream> finish {{{-t <tag-name>}}}
2462 Any positional arguments for `finish` will be considered as
2463 error.
2464 """,
2465
2466 "@examples" : """
2467 {{{> hg flow}}}
2468 flow: Currently open branches:
2469 flow: <master> : default
2470 flow: <develop>: develop develop/0.9#
2471 flow: <feature>: feature/help*
2472 # Show open branches in all streams. The '*' marker indicates the branch which
2473 # the workspace is in, and the '#' marker indicates there are shelved changes
2474 # in the branch.
2475
2476 {{{> hg flow feature finish --history}}}
2477 # Finish the current <feature> branch, and print the history of primitive hg
2478 # commands used by the workflow.
2479
2480 {{{> hg flow develop/0.9:feature start new_v0.9_feature}}}
2481 # Start a new feature branch from the 'develop/0.9' branch.
2482
2483 {{{> hg flow develop/0.9:feature finish --verbose}}}
2484 flow: note: Hg command history:
2485 flow: note: hg commit --message "flow: Closed <feature> 'help'." --close-branch
2486 flow: note: hg update develop/0.9
2487 flow: note: hg merge feature/help
2488 flow: note: hg commit --message "flow: Merged <feature> 'help' to <develop/0.9> ('develop/0.9')."
2489 flow: note: hg update develop
2490 flow: note: hg merge develop/0.9
2491 flow: note: hg commit --message "flow: Merged <develop/0.9:feature> 'help' to <develop> ('develop')."
2492 # Finish the workspace <feature> branch, merging it to 'develop/0.9', which is
2493 # in turn merged to <develop>'s trunk.
2494 """,
2495
2496 "@master" : """
2497 Master stream contains 1 and only 1 branch that has only and all production
2498 revisions (i.e., official releases). New revisions in <master> are created when
2499 a <release> or <hotfix> branch merges into <master>.
2500 The following actions can be applied to <master>: push, publish, pull, list,
2501 and log.
2502 """,
2503
2504 "@develop" : """
2505 Develop stream contains all changes made for future releases. <release> and
2506 <feature> branches are started from <develop> and will be merged to <develop>
2507 when finished. Since version 0.9, user can create branches in <develop>. A
2508 <develop> branch can be used as the source branch to start <release> and
2509 <feature> branches.
2510 """,
2511
2512 "@feature" : """
2513 Feature stream contains branches where new features for future releases are
2514 developed. Branches in <feature> are created from either <develop> or an
2515 existing <feature> branch.
2516 All actions can be applied to <feature> branches. When a <feature> branch is
2517 finished, it will normally be merged into <develop>.
2518 """,
2519
2520 "@release" : """
2521 Release stream contains branches of release candidates. Code in <release> branch
2522 will usually be tested and bug-fixed. Once a <release> branch is graduated from
2523 the testing and bug-fixing process, it will be merged to both <master> and
2524 <develop>.
2525 """,
2526
2527 "@hotfix" : """
2528 Hotfix stream contains branches for fixing bugs in <master>. <hotfix> branches
2529 are started from <master> and once they are finished will be merged to both
2530 <master> and <develop>.
2531 """,
2532
2533 "@support" : """
2534 Support stream contains branches for supporting a previous release. <support>
2535 branches are started from <master> and will never be merged to anywhere. When
2536 finished, they will be simply closed.
2537 """,
2538
2539 "@start" : """
2540 Start a new branch in stream. <feature> and <release> branches are started from
2541 <develop>. <hotfix> and <support> branches are started from <master>.
2542
2543 syntax:
2544 {{{hg flow <stream> start <name> [<option>...]}}}
2545
2546 options:
2547 -r --rev REV Revision to start a new branch from.
2548 -m --message TEXT Record TEXT as commit message when opening new branch.
2549 -p --stamp TEXT Append TEXT to all commit messages.
2550 -d --date DATE Record the specified DATE as commit date.
2551 -u --user USER Use specified USER as committer.
2552 --dirty Start a new branch from current dirty workspace branch and
2553 move all uncommitted changes to the new branch.
2554
2555 The new branch is named after <stream-prefix>/<name>.
2556 """,
2557
2558 "@finish" : """
2559 Finishing a branch in stream means to close the branch and merge the branch to
2560 destination stream(s). <feature> branches will be merged to <develop>, and
2561 <release> and <hotfix> branches will be merged to both <develop> and <master>.
2562 <support> branches will not be merged to anywhere, and they will only be closed.
2563 Note that merging to a non-trunk <develop> branch will cause the <develop>
2564 branch to be merged into the <develop> trunk.
2565
2566 syntax:
2567 {{{hg flow <stream> finish [<option>...]}}}
2568
2569 The workspace branch will be finished. Hgflow intentionally forbids finishing
2570 a branch other than the workspace one, which forces user to update to and
2571 check the branch before finishing it.
2572
2573 The workspace branch must be in the specified <stream>. When the workspace
2574 branch is merged into <master>, a new tag will be added to the corresponding
2575 snapshot in the <master> trunk. User can use the '-t' option to specify the tag
2576 name; if not specified, the tag name will be derived automatically from the
2577 name of the workspace branch by replacing the stream prefix with the
2578 `version_prefix`. The '-t' option has no effect if the workspace branch is not
2579 merged into <master>.
2580
2581 options:
2582 -c --commit Commit changes before closing the branch.
2583 -m --message TEXT Record TEXT as commit message.
2584 -p --stamp TEXT Append TEXT to all commit messages.
2585 -t --tag NAME Tag the snapshot in the <master> trunk with NAME.
2586 -d --date DATE Record the specified DATE as commit date.
2587 -u --user USER Use specified USER as committer.
2588 -e --erase Erase branch after it is merged successfully.
2589
2590 N.B.: RE. '--erase': A branch cannot be erased if it has been previously merged
2591 to other branches, creating nodes that are not erased together with the branch.
2592 """,
2593
2594 "@push" : """
2595 Push the workspace branch to the remote repository.
2596
2597 syntax:
2598 {{{hg flow <stream> push}}}
2599
2600 alternative syntax:
2601 {{{hg flow <stream> publish}}}
2602 The two syntaxes are completely equivalent.
2603
2604 The workspace branch must be in <stream>.
2605 """,
2606
2607 "@publish" : """
2608 Push the workspace branch to the remote repository.
2609
2610 syntax:
2611 {{{hg flow <stream> publish}}}
2612
2613 alternative syntax:
2614 {{{hg flow <stream> push}}}
2615 The two syntaxes are completely equivalent.
2616
2617 The workspace branch must be in <stream>.
2618 """,
2619
2620 "@pull" : """
2621 Pull a branch named after <stream-prefix>/<name> from the remote repository and
2622 update the workspace. If <name> is not specified, it defaults to the workspace
2623 branch.
2624
2625 syntax:
2626 {{{hg flow <stream> pull [<name>]}}}
2627
2628 The pulled branch must be in <stream>.
2629 """,
2630
2631 "@list" : """
2632 List all open branches in <stream>.
2633
2634 syntax:
2635 {{{hg flow <stream> list}}}
2636
2637 alternative syntax:
2638 {{{hg flow <stream>}}}
2639 If <stream> has trunk (e.g., <develop> and <master>), this syntax will update
2640 the workspace to the trunk besides listing all open branches in <stream>. If
2641 <stream> does not have trunk (e.g., <feature>, <release>, <hotfix>, and
2642 <support>), this syntax is completely equivalent to the other one (i.e., only
2643 list all open branches in the stream).
2644
2645 option:
2646 -c --closed Show open and closed branches in <stream>.
2647
2648 example:
2649 {{{> hg flow hotfix list}}}
2650 flow: Open <hotfix> branches:
2651 flow: hotfix/0.9.6#
2652 flow: hotfix/0.9.6/init_-d_option*
2653 # List all currently open branches in <hotfix>. The '*' marker indicates the
2654 # branch which the workspace is in, and the '#' marker indicates that there are
2655 # shelved changes for the branch.
2656 """,
2657
2658 "@log" : """
2659 Show revision history of the specified branch, which must be in <stream>.
2660 syntax:
2661 {{{hg flow <stream> log [<basename>]}}}
2662 where <basename> is of the branch name, e.g., if a branch's name is
2663 'feature/colored_help', its basename relative to <feature> (assuming the
2664 branch name prefix is 'feature/') is 'colored_help'.
2665 If <basename> is missing, it will default to the workspace branch.
2666
2667 Show revision history of a single file in the workspace branch.
2668 syntax:
2669 {{{hg flow <stream> log <filename>}}}
2670 If <filename> happens to be the same as the basename of a branch in <stream>,
2671 it will be recognized as the basename.
2672
2673 alternative syntax:
2674 {{{hg flow <stream> log -F <filename>}}}
2675 Use this syntax to avoid the potential ambiguity with the prior syntax. Also,
2676 you can specify multiple file names to show revision history of these files.
2677
2678 Show revision history of specified files in a designated branch.
2679 syntax:
2680 {{{hg flow <stream> log <basename> -F <filename>}}}
2681
2682 options:
2683 -F --file FILE [+] File to show history of.
2684 -d --date DATE Show revisions matching date spec.
2685 -u --user USER Show revisions committed by USER.
2686 -k --keyword TEXT Do case-insensitive search for a given text.
2687 -p --patch Show patch.
2688 -g --git Use git extended diff format to show patch.
2689 -l --limit VALUE Limit number of changesets displayed.
2690 -c --closed Show closed branches when used together with -s option.
2691
2692 [+] marked option can be specified multiple times.
2693 """,
2694
2695 "@abort" : """
2696 Aborting the workspace branch can be done in two ways:
2697 1. The default way is simply marking the branch as closed so that it will not
2698 show up when you list alive branches, but all changesets in the branch
2699 remain in the repository and you cannot reuse the branch's name for a
2700 different branch.
2701 2. The other way is erasing the branch, in other words, completely deleting the
2702 branch and all changesets in it from the repository. This way is
2703 devastating, but you can clear unneeded changesets and reuse the branch's
2704 name. To abort a branch in this way, you just add the {{{-e}}} option.
2705 N.B.: A branch cannot be erased if you have previously merged it to other
2706 branches that remain in the repository.
2707
2708 syntax:
2709 {{{hg flow <stream> abort [-m <TEXT>] [-e]}}}
2710
2711 options:
2712 -m --message TEXT Record TEXT as commit message when closing branch.
2713 -p --stamp TEXT Append TEXT to all commit messages.
2714 -e --erase Abort branch and erase it.
2715 """,
2716
2717 "@promote" : """
2718 Merge the workspace branch to destination branches. The destination branches,
2719 if omitted, will default to the trunk of the destination stream. The destination
2720 streams of the basic streams are listed as follows:
2721
2722 stream destination
2723 ------------+-----------------------
2724 <feature> <develop>
2725 <develop> <master>
2726 <release> <develop> & <master>
2727 <hotfix> <develop> & <master>
2728 <master> n/a
2729 <support> n/a
2730 natural stream-trunk
2731
2732 syntax:
2733 {{{hg flow <stream> promote [<destination-branch-full-name>...] [<option>...]}}}
2734
2735 The workspace branch must be in <stream>. If the `-r` option is omitted, its
2736 value will default to the head of the workspace branch.
2737
2738 options:
2739 -r --rev REV Revision to promote to other branches.
2740 -m --message TEXT Record TEXT as commit message when promoting branch.
2741 -p --stamp TEXT Append TEXT to all commit messages.
2742 -t --tag NAME Tag the merging changeset with NAME
2743 -d --date DATE Record the specified DATE as commit date.
2744 -u --user USER Use specified USER as committer.
2745
2746 examples:
2747 {{{> hg flow develop promote -t v0.2.0}}}
2748 # Immediately release <develop> trunk's tip into <master>, bypassing <release>.
2749 # What this command exactly does is to promote the <develop> trunk into
2750 # <master> (<master> is <develop>'s default promotion destination, so you don't
2751 # have to spell it out in the command), and then label the <master> snapshot
2752 # with "v0.2.0".
2753 """,
2754
2755 "@rebase" : """
2756 Rebase the workspace branch to the specified revision.
2757
2758 syntax:
2759 {{{hg flow <stream> rebase [-d <rev>]}}}
2760
2761 option:
2762 -p --stamp TEXT Append TEXT to all commit messages.
2763
2764 The workspace branch must be in <stream>. If the destination revision is not
2765 specified, it will default to the source branch of the workspace branch.
2766 """,
2767
2768 "@version" : """
2769 Show version of the flow extension.
2770
2771 syntax:
2772 {{{hg flow version}}}
2773 """,
2774
2775 "@rename" : """
2776 Rename the workspace branch to a new basename. Under the hood, this action
2777 will create a new branch with the new basename and copy/graft the commits in
2778 the workspace branch to the new branch, and then erase the workspace branch.
2779 All the user, date, and commit-message information will be copied to the
2780 new branch.
2781
2782 N.B.: The workspace branch should be a simple linear branch. This means:
2783 (1) It has not merged to other branches;
2784 (2) It has no subbranches;
2785 (3) No other branches has merged to this branch.
2786
2787 syntax:
2788 {{{hg flow <stream> rename [-t <basename>]}}}
2789
2790 option:
2791 -t --to NAME Rename the basename of the workspace branch to NAME.
2792
2793 The workspace branch must be in <stream>.
2794 """,
2795
2796 "@version" : """
2797 Show version of the flow extension.
2798
2799 syntax:
2800 {{{hg flow version}}}
2801 """,
2802
2803 "@init" : """
2804 Initialize the flow extension for the repository. The configuration file:
2805 {{{.hgflow}}} will be written in the root dir of the repository. The file will be
2806 tracked by hg. If you have multiple open branches, the file should be present
2807 and synchronized in all of them -- init command will do this for you
2808 automatically.
2809
2810 syntax:
2811 {{{hg flow init [<option>...]}}}
2812
2813 options:
2814 -f --force Force reinitializing flow.
2815 -u --user USER Use specified USER as committer.
2816 -p --stamp TEXT Append TEXT to all commit messages.
2817 """,
2818
2819 "@upgrade" : """
2820 Upgrade the configuration file from v0.9.4 (or older) to v0.9.5 or later.
2821
2822 syntax:
2823 {{{hg flow upgrade}}}
2824
2825 options:
2826 -u --user USER Use specified USER as committer.
2827 -p --stamp TEXT Append TEXT to all commit messages.
2828 """,
2829
2830 "@unshelve" : """
2831 Unshelve previously shelved changes by hgflow. Sometimes, unshelving is not
2832 automatically executed because workflow is terminated prematurelly. In such
2833 situations, you can always use the unshelve command to manually restore the
2834 shelved changes.
2835
2836 syntax:
2837 {{{hg flow unshelve}}}
2838 """,
2839
2840 "@terms" : """
2841 Concepts:
2842 - stream
2843 The entire set of branches of the same type. Stream is not branch, it is a
2844 set of branches. In general, a stream can contain any number (including zero)
2845 of branches. The master stream is, however, special in that it contains 1 and
2846 only 1 branch. The develop stream contains at least one branch.
2847
2848 - basic streams
2849 Refers to the master, develop, feature, release, hotfix, and support streams
2850 as predefined in Driessen's model.
2851
2852 - natural streams
2853 Refers to a set of branches that diverged from and will merge into the same
2854 branch. The set of branches plus the branch that they diverged from form a
2855 natural stream. The branch that all the other branches in the same natural
2856 stream diverged from and will merge into is the trunk of the natural stream.
2857
2858 - trunk
2859 Trunk is a special branch. A stream can optionally have a trunk, but only one
2860 trunk at most. For example, master and develop streams each has a trunk,
2861 whereas feature, release, hotfix, and support streams don't. And all natural
2862 streams each has a trunk. If a stream has a trunk, all branches in the stream
2863 normally should diverge from the trunk and later merge to the trunk when they
2864 are finished.
2865 Trunk is a relative concept. A trunk of a stream may be a regular branch of
2866 another stream. (The former stream will be called a substream of the latter.)
2867
2868 - source
2869 Source is an attribute of stream. The source of a stream refers to the stream
2870 where branches in the current stream are created from. A stream's source can
2871 be the stream itself. But this is not always the case, for example,
2872 the sources of release and feature streams are the develop stream.
2873
2874 - destin
2875 Destin is another attribute of stream. The destin of a stream refers to the
2876 stream(s) where branches in the current stream will merge to. A stream's
2877 destin can be the stream itself. But this is not always the case,
2878 for example, the destin of release is the develop and the master streams.
2879
2880 - fullname
2881 Branch name as recognized by the SCM, e.g., feature/enhance_log.
2882
2883 - basename
2884 Branch name recognized by flow, but not necessarily by SCM, e.g.,
2885 enhanced_log (with prefix 'feature/' dropped).
2886
2887 - flow action
2888 Refer to action on a specified stream, e.g., hg flow feature start, where
2889 'start' is an action.
2890
2891 - flow command
2892 Commands don't act on a stream, e.g., hg flow unshelve, where 'unshelve'
2893 is a command.
2894
2895 - hg command
2896 Refer to commands not from flow extension.
2897
2898 - workflow
2899 Refer to the process of executing a sequence of hg commands.
2900
2901 - history
2902 Refer to a sequence of executed hg commands.
2903
2904 Notations
2905 - <stream>
2906 Examples: <feature>, <hotfix>. These denote the corresponding streams. When
2907 you refer to a stream, e.g., feature stream, use '<feature>' (or more
2908 verbosely 'feature stream'), instead of '<feature> stream', because
2909 '<feature>' already means stream.
2910
2911 - <stream> branch
2912 Example: a <feature> branch. This phrase refers a branch in <feature>. Do not
2913 use 'a feature branch' to mean a branch in <feature> because the word
2914 'feature' there should take its usual meaning as in English, which doesn't
2915 necessarily mean the feature stream.
2916 """,
2917
2918 "@help" : """
2919 Show online help and then quit. An argument can be optionally given after the
2920 'help' command to specify a particular help topic. Detailed online help is
2921 available for the following topics:
2922 @all - Show detailed help for all supported topics.
2923 @<stream> - Show help about a particular stream, e.g., {{{@feature}}}, {{{@master}}}.
2924 @<action> - Show help about an action, e.g., {{{@finish,}}} {{{@log}}}.
2925 @<command> - Show help about a command, e.g., {{{@help}}}, {{{@unshelve}}}.
2926 @terms - Show explanations of terminologies used in hgflow.
2927 @examples - Show a few command examples.
2928 @deprecated - Show a list of deprecated features.%s""" % \
2929 ("\nOn Windows platform, a topic should be wrapped with quotes, e.g., {{{\"@finish\"}}}." if ("nt" == os.name) else ""),
2930 }
2931
2932 def __init__( self, ui, repo ) :
2933 self.ui = ui
2934 self.repo = repo
2935
2936
2937
2938 def _print( self, s ) :
2939 """
2940 Print text with predefined effects.
2941 @type s: C{str}
2942 @param s: String to be printed
2943 """
2944 import re
2945 code_pattern = re.compile( "{{{.*?}}}" )
2946 last_span = (0, 0,)
2947 for match in code_pattern.finditer( s ) :
2948 span = match.span()
2949 self.ui.write( s[last_span[1]:span[0]] )
2950 self.ui.write( s[span[0] + 3:span[1] - 3], label = "flow.help.code" )
2951 last_span = span
2952 self.ui.write( s[last_span[1]:] )
2953
2954
2955
2956 def print_help( self, topic = None, *arg, **opts ) :
2957 """
2958 Print help information.
2959
2960 @type topic : C{str} or C{None}
2961 @param topic : Help topic
2962 """
2963 if (topic is None) :
2964 self._print( self.SHORT_USAGE )
2965 elif (topic == "@all") :
2966 doc = self.TOPIC.items()
2967 doc.sort()
2968 for t, help in doc :
2969 self.ui.write( "%s" % t, label = "flow.help.topic" )
2970 self._print( "%s\n" % help )
2971 else :
2972 try :
2973 help_content = self.TOPIC[topic]
2974 self.ui.write( "%s" % topic, label = "flow.help.topic" )
2975 self._print( "%s\n" % help_content )
2976 except KeyError :
2977 _error( self.ui, "Unknown topic: %s" % topic )
2978 if (("@" + topic) in self.TOPIC or topic == "all") :
2979 _error( self.ui, "Did you mean '@%s'?" % topic )
2980 _print( self.ui, """Supported topics are the following:
2981 @all - Show detailed help for all supported topics.
2982 @<stream> - Show help about a particular stream, e.g., @feature, @master.
2983 @<action> - Show help about an action, e.g., @finish, @log.
2984 @<command> - Show help about a command, e.g., @help, @unshelve.
2985 @terms - Show explanations of terminologies used in hgflow.
2986 @examples - Show a few command examples.
2987 @deprecated - Show a list of deprecated features.
2988 """ )
2989
2990
2991
2992 OPT_FILTER = {
2993 "init" : ("force", "user", "stamp", "default",),
2994 "upgrade" : ("user", "stamp",),
2995 "start" : ("rev", "message", "stamp", "date", "user", "dirty",),
2996 "finish" : ("commit", "message", "stamp", "tag", "date", "user", "erase", "onstream",),
2997 "list" : ("closed",),
2998 "log" : ("file", "date", "user", "keyword", "patch", "git", "limit", "graph", "closed", "onstream",),
2999 "abort" : ("erase", "message", "stamp", "onstream",),
3000 "promote" : ("rev", "message", "stamp", "tag", "date", "user", "onstream",),
3001 "rebase" : ("dest", "onstream", "stamp",),
3002 "rename" : ("to",),
3003 }
3004
3005 OPT_CONFLICT = {
3006 "dest" : ("-d", '', ), # (short-form-of-option, default-value,)
3007 "date" : ("-d", '', ),
3008 "default" : ("-d", False,),
3009 "closed" : ("-c", False,),
3010 "commit" : ("-c", False,),
3011 "stamp" : ("-p", '' ),
3012 "patch" : ("-p", False ),
3013 "tag" : ("-t", '' ),
3014 "to" : ("-t", '' ),
3015 }
3016
3017 def _getopt( ui, key, opt ) :
3018 """
3019 Return user-specified options.
3020
3021 We cannot separate options for different subcommands because of the design of the C{cmdtable}. So ambiguity exists for some
3022 options. For example, the C{-d} option, it means C{dest} for C{rebase} and C{date} for C{finish}. For either of the two
3023 actions, the value of the C{-d} option could be saved in C{dest} or C{date}. In general, we don't know which one.
3024
3025 We have to do a bit of parsing to resolve potential ambiguity. This function is here for that purpose. C{opt} is the raw
3026 option C{dict} from C{hg}. We will reparse it a bit for a particular command or action given by C{key}. The function
3027 returns a C{dict} that contains the option's name and its value.
3028 N.B.:
3029 (1) If the value of an option evaluates to false, the option will be absent in the returned C{dict} object.
3030 (2) This function will mutate and return C{opt}.
3031
3032 @type ui: C{mercurial.ui}
3033 @param ui: Mercurial user interface object
3034 @type key: C{str}
3035 @param key: Command or action for which you are getting the options
3036 @type opt: C{dict}
3037 @param opt: Raw options
3038
3039 @raise AbortFlow: AbortFlow exception will be raised if there is option error.
3040 """
3041 ret = {}
3042 rec_short = [] # A list of recoginized short options
3043 for e in OPT_FILTER.get( key, [] ) :
3044 if (opt.get( e )) :
3045 ret[e] = opt[e]
3046 elif (e in OPT_CONFLICT) :
3047 short_opt, default_value = OPT_CONFLICT[e]
3048 argv = sys.argv
3049 if (short_opt in argv) :
3050 rec_short.append( short_opt )
3051 if (isinstance( default_value, str )) :
3052 index = argv.index( short_opt )
3053 try :
3054 ret[e] = argv[index + 1]
3055 except IndexError :
3056 raise AbortFlow( "Value not found for %s option." % short_opt )
3057 else :
3058 ret[e] = not default_value
3059
3060 bad_opt = [e for e in opt if (e not in (["history", "dry_run"] + ret.keys()) and opt[e])]
3061 bad_opt = [e for e in bad_opt if (e in sys.argv) or (OPT_CONFLICT.get( e, [0,] )[0] not in rec_short)]
3062
3063 if (bad_opt) :
3064 bad_opt = [e.replace( "_", "-" ) for e in bad_opt]
3065 if (key is None) :
3066 raise AbortFlow( "Unrecognized option%s for `hg flow`: %s." %
3067 ("" if (len( bad_opt ) == 1) else "s", "--" + (", --".join( bad_opt )),),
3068 note = "`hg flow` should take no options." )
3069 elif (key in Flow.ACTION_NAME) :
3070 raise AbortFlow( "Unrecognized option%s for `%s`: %s." %
3071 ("" if (len( bad_opt ) == 1) else "s", key, "--" + (", --".join( bad_opt )),),
3072 note = "Execute `hg flow help @%s` to see available options for `%s`." % (key, key,) )
3073 else :
3074 raise AbortFlow( "Unrecognized option%s: %s." %
3075 ("" if (len( bad_opt ) == 1) else "s", "--" + (", --".join( bad_opt )),) )
3076
3077 return ret
3078
3079
3080
3081 cmdtable = {
3082 "flow" :
3083 (flow_cmd,
3084 [("", "history", False, _("Print history of hg commands used in this workflow."), ),
3085 ("", "dry-run", None, _("Do not perform actions, just print history."), ),
3086 ("", "dirty", False, _("Start a new branch from a dirty workspace, and move all"
3087 " uncommitted changes to the new branch. [start]"), ),
3088 ("c", "closed", False, _("Show normal and closed branches in stream. [list, log]"), ),
3089 ("c", "commit", False, _("Commit changes before closing the branch. [finish]"), ),
3090 ("d", "default", False, _("Initialize flow with default configuration. [init]"), ),
3091 ("d", "date", '', _("Record the specified date as commit date. [start, finish, promote]"), _('DATE'),),
3092 ("d", "date", '', _("Show revisions matching date spec. [log]"), _('DATE'),),
3093 ("d", "dest", '', _("Destination changeset of rebasing. [rebase]"), _('REV' ),),
3094 ("e", "erase", False, _("Erase branch after it is merged or aborted successfully. [finish, abort]"), ),
3095 ("F", "file", [], _("File to show history of. [log]"), _('FILE'),),
3096 ("f", "force", False, _("Force reinitializing flow. [init]"), ),
3097 ("g", "git", False, _("Use git extended diff format to show patch. [log]"), ),
3098 ("k", "keyword", '', _("Do case-insensitive search for a given text. [log]"), _('TEXT'),),
3099 ("l", "limit", '', _("Limit number of changesets displayed. [log]"), ),
3100 ("m", "message", '', _("Record TEXT as commit message. [start, finish, promote, abort]"), _('TEXT'),),
3101 ("p", "stamp", '', _("Append TEXT to all commit messages. [init, upgrade, start, finish,"
3102 " promote, rebase, abort]"), _('TEXT'),),
3103 ("p", "patch", False, _("Show patch. [log]"), ),
3104 ("r", "rev", '', _("Revision to start a new branch from. [start]"), _('REV'), ),
3105 ("r", "rev", '', _("Revision to promote to other branches. [promote]"), _('REV'), ),
3106 ("s", "onstream", False, _("Act on stream. [finish, rebase, log, abort]"), ),
3107 ("t", "tag", '', _("Tag the merging changeset with NAME. [promote]"), _('NAME'),),
3108 ("t", "tag", '', _("Tag the <master> trunk with NAME after merging. [finish]"), _('NAME'),),
3109 ("t", "to", '', _("Rename the branch to NAME. [rename]"), _('NAME'),),
3110 ("u", "user", '', _("Use specified user as committer. [init, upgrade, start, finish,"
3111 " promote]"), _('USER'),),
3112 ("u", "user", '', _("Show revisions committed by specified user. [log]"), _('USER'),),
3113 ],
3114 "hg flow {<stream> [<action> [<arg>]] | <command>} [<option>...]",
3115 ),
3116 }

mercurial