diff options
author | Oswald Buddenhagen <oswald.buddenhagen@qt.io> | 2017-08-03 16:27:42 +0200 |
---|---|---|
committer | Oswald Buddenhagen <oswald.buddenhagen@gmx.de> | 2020-04-01 18:39:24 +0000 |
commit | 03c8458dd00a594a6087ceaa16f3c297de6f200a (patch) | |
tree | 0f167c1635202ea11c972a0a88049b43034e33de /bin | |
parent | b215b694ee349918c0357f477fe969361b8cbd01 (diff) | |
download | qtrepotools-03c8458dd00a594a6087ceaa16f3c297de6f200a.tar.gz |
gpush/gpick: implement local branch tracking
this helps dealing with cherry-picks between local branches.
Change-Id: Ie3f7567639cc05e03257cd5bac133714e5c4f068
Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@gmx.de>
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/git-gpick | 50 | ||||
-rwxr-xr-x | bin/git-gpush | 77 | ||||
-rw-r--r-- | bin/git_gpush.pm | 539 |
3 files changed, 623 insertions, 43 deletions
diff --git a/bin/git-gpick b/bin/git-gpick index 91f213f..cc9cd78 100755 --- a/bin/git-gpick +++ b/bin/git-gpick @@ -146,6 +146,14 @@ Options: to Gerrit. In case of ambiguity, the default is the upstram branch of the current local branch. + --move/--copy/--hide <args> + Deal with cherry-picks of Changes between local branches. + This works the same as documented for git-gpush, except that the + range may also be a plus-sign prefixed addition specification. + Unlike local ranges, these DO get picked without being specified + redundantly, because otherwise there would be nothing to operate + on in the first place. + -n, --dry-run Do everything except actually replacing any commits and updating state. @@ -287,6 +295,8 @@ sub parse_arguments(@) } elsif ($arg eq "-?" || $arg eq "--?" || $arg eq "-h" || $arg eq "--help") { usage(); exit 0; + } elsif (parse_source_option($arg, 1, @_)) { + # Nothing } elsif ($arg !~ /^-/) { push @commit_specs, $arg; } else { @@ -1549,17 +1559,20 @@ sub resolve_insertion_spec($$) $$spec{changes} = \%result; } -sub changify_remote_series($) +sub changify_remote_series($$) { - my ($series) = @_; + my ($series, $reference) = @_; + my $fails; my @result; foreach my $commit (@$series) { - my $change = change_for_id($$commit{changeid}, CREATE); + my $change = source_map_assign($commit, $reference); + $fails = 1 if (!$change); + next if ($fails); $$change{gerrit} = $gerrit_info_by_sha1{$$commit{id}}; push @result, $change; } - return \@result; + return $fails ? undef : \@result; } sub do_sort_series($$$$$); @@ -1740,7 +1753,7 @@ sub finalize_remote_series($$$) my $sorted = sort_series($name, $range, $commits, 1); fail("Series $name is empty after filtering.\n") if (!@$sorted && !$check); - return changify_remote_series($sorted); + return $sorted; } # Download the metadata and PatchSets referenced by the specs. @@ -1894,6 +1907,25 @@ sub complete_spec_tails($) } } +sub changify_remote_specs($) +{ + my ($specs) = @_; + + while (1) { + foreach my $spec (@$specs) { + my $commits = $$spec{new_range_commits}; + next if (!$commits); + my $changes = changify_remote_series($commits, $$spec{new_range_reference}); + if ($changes) { + $$spec{new_range} = $changes; + delete $$spec{new_range_commits}; + } + } + last if (!source_map_traverse()); + } + source_map_finish(); +} + sub check_specs($) { my ($specs) = @_; @@ -1905,8 +1937,10 @@ sub check_specs($) # the step may be obsoleted by the spec being matched up. next if ($$spec{action} != INSERT); - $$spec{new_range} = finalize_remote_series($$spec{orig}, [], $$spec{changes}); + $$spec{new_range_commits} = finalize_remote_series($$spec{orig}, [], $$spec{changes}); + $$spec{new_range_reference} = $$spec{tip}; } + changify_remote_specs($specs); print "Checking remote commit specs for collisions ...\n" if ($debug); my @new_specs; @@ -2065,9 +2099,11 @@ sub finalize_specs($) next if ($$spec{action} != UPDATE); next if ($$spec{new_range}); - $$spec{new_range} = + $$spec{new_range_commits} = finalize_remote_series("$$spec{orig}/remote", $$spec{range}, $$spec{changes}); + $$spec{new_range_reference} = $$spec{range}[-1]; } + changify_remote_specs($specs); return if ($force_struct); diff --git a/bin/git-gpush b/bin/git-gpush index f84d520..2215b02 100755 --- a/bin/git-gpush +++ b/bin/git-gpush @@ -144,6 +144,42 @@ Options: Reset the base of a previously pushed series to the local branch's base. Typically used after a conflicted pull. + --move {new|<range>}[/<from>] + After cherry-picking Changes to the current branch, mark the picks + as the _only_ source for subsequent pushes of these Changes. + The Changes on the current branch inherit all persistent properties, + while those on the source branch are hidden, in case they were not + dropped to start with. + <from> must be supplied only if the source branch is ambiguous. + This option is necessary only when a move cannot be unambiguously + inferred. + + --copy {new|<range>} + After cherry-picking Changes to the current branch, mark the picks + as an _additional_ source for subsequent pushes of these Changes. + The Changes on the source branch are left alone, while those on the + current branch start out with a clean slate (which implies a + different default target branch). + When a range is specified, the Changes are also --group'd. + + --hide {new|<range>} + After cherry-picking Changes to the current branch, mark the picks + as an _unacceptable_ source for subsequent pushes of these Changes. + When a range is specified, the Changes are also --group'd. + + The range can be specified as <base>..<tip> (either end can be left + off) or <tip>:<count>. When using 'new' instead of a range, all + Changes which were previously not seen on the current branch are + selected, while actual ranges select Changes regardless of whether + they were already seen, which makes it possible to revise previous + decisions about the authoritative source for pushes. + --move, --copy, and --hide can be specified multiple times. The + ranges may not overlap, but will override a single also specified + operation which uses 'new'. + Note that the ranges supplied to these options do NOT imply these + Changes getting pushed; to do so, specify them a second time as a + regular <from> argument. + -l, --list Report all Changes that would be pushed, then quit. This is a purely off-line operation. @@ -342,6 +378,8 @@ sub parse_arguments(@) } elsif ($arg eq "-?" || $arg eq "--?" || $arg eq "-h" || $arg eq "--help") { usage(); exit 0; + } elsif (parse_source_option($arg, 0, @_)) { + # Nothing } elsif ($arg =~ /^\+(.+)/) { push @reviewers, split(/,/, lookup_alias($1)); } elsif ($arg =~ /^\=(.+)/) { @@ -498,11 +536,15 @@ sub caption_group($) my $tpc = $$group{topic}; my $tpcs = length($tpc) ? ", topic '$tpc'" : ""; my ($pfx, $rmt) = $list_only - ? ($$group{exclude_mix} - ? "Partially excluded series of" - : $$group{exclude} - ? "Excluded series of" - : "Series of", + ? ($$group{hide_mix} + ? "Partially hidden series of" + : $$group{hide} + ? "Hidden series of" + : $$group{exclude_mix} + ? "Partially excluded series of" + : $$group{exclude} + ? "Excluded series of" + : "Series of", ($list_online && length($tos)) ? " on '$remote'" : "") : $group_only ? ("Grouping", "") @@ -780,6 +822,8 @@ sub finalize_get_changes($$) my ($changes, $gid) = @_; my %group = (changes => $changes, gid => $gid); + aggregate_bool_property(\%group, "hide", "HIDDEN", "hiding", + "Use --move/--copy/--hide to make it consistent."); return \%group; } @@ -842,6 +886,20 @@ sub get_changes() } fail("Specified commit range is empty.\n") if (!@$changes); my $group = finalize_get_changes($changes, $gid); + # Note that grouping/excluding hidden series is also rejected, + # as there is just no point to it - --copy and --hide imply + # regrouping anyway, and --move uses the grouping of the source, + # so any grouping done while the Changes are hidden will be lost. + if ($$group{hide}) { + my @reports; + report_flowed(\@reports, + "Refusing to operate hidden series of ".int(@$changes)." Change(s):"); + report_local_changes(\@reports, $changes); + report_fixed(\@reports, + "Operate it from the previously chosen source branch,\n", + "or use --move/--copy to unhide it.\n"); + fail_formatted(\@reports); + } return $group; } @@ -858,7 +916,8 @@ sub get_all_changes() my $group = finalize_get_changes($changes, $gid); aggregate_bool_property($group, "exclude", "EXCLUDED", "exclusion", "Please gpush it separately or use --group/--exclude" - ." to make it consistent."); + ." to make it consistent.") + if (!$$group{hide}); unshift @groups, $group; $have_loose++ if (!$gid); last if (!$change); @@ -1580,9 +1639,11 @@ sub annotate_changes($) { my ($group) = @_; - my $excl_mix = $list_only && $$group{exclude_mix}; + my $hide_mix = $list_only && $$group{hide_mix}; + my $excl_mix = !$hide_mix && $list_only && $$group{exclude_mix}; foreach my $change (@{$$group{changes}}) { my @attribs; + push @attribs, 'HIDDEN' if ($hide_mix && $$change{hide}); push @attribs, 'EXCLUDED' if ($excl_mix && $$change{exclude}); # Changes can be loose only when pushing all or listing, as # otherwise they are currently being assigned, and showing @@ -1897,7 +1958,7 @@ sub execute_pushing() } fail_push($all_groups, "Cannot push all with any free-standing loose Changes.\n"); } - $groups = [ grep { !$$_{exclude} } @$all_groups ]; + $groups = [ grep { !$$_{hide} && !$$_{exclude} } @$all_groups ]; } else { $groups = $all_groups = [ get_changes() ]; } diff --git a/bin/git_gpush.pm b/bin/git_gpush.pm index 252925a..ce1a106 100644 --- a/bin/git_gpush.pm +++ b/bin/git_gpush.pm @@ -653,6 +653,8 @@ our %gerrit_infos_by_id; # attributes, used by --group mode. # - exclude: Flag indicating whether the Change is excluded from # push --all mode. +# - hide: Flag indicating whether the Change is excluded from all +# pushes. my $next_key = 10000; # All known Gerrit Changes for the current repository. @@ -703,7 +705,7 @@ sub save_state(;$$) print "Saving ".($new ? "new " : "")."state".($dry ? " [DRY]" : "")." ...\n" if ($debug); my (@lines, @updates); my @fkeys = ('key', 'grp', 'id', 'src', 'tgt', 'topic', 'base', - 'ntgt', 'ntopic', 'nbase', 'exclude'); + 'ntgt', 'ntopic', 'nbase', 'exclude', 'hide'); my @rkeys = ('pushed', 'orig'); if ($new) { push @lines, "verify $new", "updater $state_updater"; @@ -787,31 +789,6 @@ sub _init_change($$) $next_key++; } -use constant { - CREATE => 1 -}; - -# Get a Change object for a given Id on the _local_ branch. If no such -# object exists, create a new one if requested, otherwise return undef. -sub change_for_id($;$) -{ - my ($changeid, $create) = @_; - - my $br = $local_branch // "-"; - my $chgs = $changes_by_id{$changeid}; - if ($chgs) { - foreach my $chg (@$chgs) { - return $chg if ($$chg{src} eq $br); - } - } - if ($create) { - my %chg = (src => $br); - _init_change(\%chg, $changeid); - return \%chg; - } - return undef; -} - sub load_state_file(;$) { my ($new) = @_; @@ -1187,6 +1164,11 @@ our $local_tip; # Mapping of Change-Ids to commits on the local branch. my %changeid2local; # { change-id => SHA1 } +sub _source_map_prepare(); +sub source_map_assign($$); +sub source_map_traverse(); +sub _source_map_finish_initial(); + sub analyze_local_branch($) { my ($tip) = @_; @@ -1222,14 +1204,25 @@ sub analyze_local_branch($) $local_base = $$commits[0]{parents}[0]; + # This needs to happen early, for parse_local_rev(), which + # _source_map_prepare() calls. $changeid2local{$$_{changeid}} = $$_{id} foreach (@$commits); + # ... then map them to Change objects ... + _source_map_prepare(); + while (1) { + foreach my $commit (@$commits) { + source_map_assign($commit, undef); + } + last if (!source_map_traverse()); + } + _source_map_finish_initial(); + # ... and then add them to the set of local Changes. my $idx = 0; my $prev; foreach my $commit (@$commits) { - my $change = change_for_id($$commit{changeid}, CREATE); - $$commit{change} = $change; + my $change = $$commit{change}; $$change{local} = $commit; $$change{index} = $idx++; $$change{parent} = $prev; @@ -1552,6 +1545,496 @@ sub create_commit($$$$$) # branch tracking # ################### +# This tracks the local branch each Change lives on, following cherry-picks +# and purges as much as possible. + +# Format a single branch name for display. +sub _format_branch($) +{ + my ($branch) = @_; + + return ($branch ne '-') ? "'$branch'" : '<detached HEAD>'; +} + +# Format the source branch names from an array of Changes, +# using Oxford English grammar. +sub _format_branches_raw($) +{ + my ($changes) = @_; + + my @branches = sort map { _format_branch($$_{src}) } @$changes; + my $str = "$branches[0]"; + if (@branches > 1) { + if (@branches > 2) { + $str .= ", $branches[$_]" for (1 .. @branches - 2); + $str .= ","; + } + $str .= " and $branches[-1]"; + } + return $str; +} + +# As above, but with quantifiers to signify that we mean all branches. +sub _format_branches(@) +{ + my ($changes) = @_; + + my $str = _format_branches_raw($changes); + return $str if (@$changes == 1); + return "both $str" if (@$changes == 2); + return "all of $str"; +} + +# Format the message about the outcome of a Change assignment attempt. +sub _format_result($$$@) +{ + my ($commit, $new, $fmt, @args) = @_; + + my $lbr = $local_branch // "-"; + return sprintf("%s\n %son %s $fmt.\n", format_commit($commit), + $new ? "newly " : "", _format_branch($lbr), @args); +} + +use constant { + _SRC_NOOP => 0, + _SRC_MOVE => 1, + _SRC_COPY => 2, + _SRC_HIDE => 3 +}; + +my @sm_options; +my $sm_option_new; +my %sm_option_by_id; +my %sm_wanted; +my %sm_present; +my $sm_printed = 0; +my $sm_changed = 0; +my $sm_failed = 0; + +# Parse a single command line option relating to source branch tracking. +# $arg is the currently processed argument, while $args are the remaining +# arguments on the command line. If $rmt_ok is true, plus-prefixed remote +# specifications are accepted as well; note that these are not removed +# from the command line, unlike local ones. +sub parse_source_option($$\@) +{ + my ($arg, $rmt_ok, $args) = @_; + + my $action; + if ($arg eq "--move") { + $action = _SRC_MOVE; + } elsif ($arg eq "--copy") { + $action = _SRC_COPY; + } elsif ($arg eq "--hide") { + $action = _SRC_HIDE; + } else { + return undef; + } + fail("$arg needs an argument.\n") + if (!@$args || ($$args[0] =~ /^-/)); + my $orig = shift @$args; + my $tip = $orig; + my ($base, $count); + my $branch = $1 if ($tip =~ s,/(.*)$,,); + my $rmt_id; + if ($rmt_ok && $tip =~ /^\+(\w+)/) { + $rmt_id = $1; + unshift @$args, $tip; + } else { + $base = $1 if ($tip =~ s,^(.*)\.\.,,); + $count = $1 if ($tip =~ s,:(.*)$,,); + $tip = undef if ($tip eq "new"); + fail("Specifying a commit count and a range base are mutually exclusive.\n") + if (defined($base) && defined($count)); + if (defined($base) || defined($count)) { + wfail("Specifying a commit count or range base is incompatible with range 'new'.\n") + if (!defined($tip)); + } else { + wfail("Automatic ranges are not supported with $arg." + ." Use $tip:1 if you actually meant a single Change.\n") + if (defined($tip)); + } + } + fail("$arg does not support specifying a source branch.\n") + if (defined($branch) && ($action != _SRC_MOVE)); + push @sm_options, { + action => $action, + orig => $orig, + rmt_id => $rmt_id, + base => defined($base) ? length($base) ? $base : '@{u}' : undef, + tip => defined($tip) ? length($tip) ? $tip : 'HEAD' : undef, + count => $count, + branch => $branch + }; + return 1; +} + +# Do final sanity checking on the source branch tracking related commands, +# expand the supplied ranges into series of commits, and create a reverse +# mapping of commits to command objects. +sub _source_map_prepare() +{ + my $br = $local_branch // "-"; + foreach my $option (@sm_options) { + my $sbr = $$option{branch}; + wfail("Source and target branch are both '$br' in attempt to move Changes.\n") + if (defined($sbr) && ($sbr eq $br)); + + my $rmt_id = $$option{rmt_id}; + if (defined($rmt_id)) { + $sm_option_by_id{$rmt_id} = $option; + next; + } + + my $raw_tip = $$option{tip}; + if (defined($raw_tip)) { + my $commits; + my $tip = parse_local_rev($raw_tip, SPEC_TIP); + my $raw_base = $$option{base}; + if (defined($raw_base)) { + my $base = parse_local_rev($raw_base, SPEC_BASE); + $commits = get_commits_base($base, $tip, $raw_base, $raw_tip); + } else { + my $count = $$option{count}; + $commits = get_commits_count($tip, $count, $raw_tip); + } + foreach my $commit (@$commits) { + my $sha1 = $$commit{id}; + my $old_option = $sm_option_by_id{$sha1}; + wfail("Range $$option{orig} intersects $$old_option{orig} (at $sha1).\n") + if ($old_option); + $sm_option_by_id{$sha1} = $option; + } + $$option{commits} = $commits; + next; + } + + wfail("Only one of --move, --copy, and --hide may be specified with 'new'.\n") + if (defined($sm_option_new)); + $sm_option_new = $option; + } +} + +# Schedule the removal a single Change object from the state database. +sub _obliterate_change($$) +{ + my ($change, $changes) = @_; + + print "Obliterating $$change{key} ($$change{id}) from $$change{src}.\n" if ($debug); + @$changes = grep { $_ != $change } @$changes; # From %changes_by_id + $$change{garbage} = 1; # Mark it for %change_by_key traversal +} + +# Visit the requested non-current local branches with the purpose of +# determining which Change objects still correspond with actual commits. +sub source_map_traverse() +{ + # It would be tempting to always query all local branches in + # advance, to save the extra git call. However, this would also + # collect *really* old branches, and even though they are short, + # calculating the exclusion takes hundreds of milliseconds, + # which is slower than an extra git call even on Windows. + # Additionally, we need to traverse down to the push base of the + # previously pushed Changes (in case they were upstreamed already), + # and this can also have a significant cost if the corresponding + # local branch advanced a lot since a Change was pushed last time, + # so it's better to do that selectively. + + return if (!%sm_wanted); + + # This loop is likely to run only once per call, so don't bother + # coalescing the resulting visit_*() calls. + foreach my $br (keys %sm_wanted) { + print "Investigating other local branch $br ...\n" if ($debug); + my %present; + my @changes = grep { $$_{src} eq $br } values %change_by_key; + my (@missing, $utips); + my $tip = $local_refs{$br}; + if (defined($tip)) { + visit_local_commits([ $tip ]); + my $commits = get_commits_free($tip); + print "Still present on the branch:\n".format_commits($commits) if ($debug); + if (@$commits) { + %present = map { $$_{changeid} => 1 } @$commits; + + # Prepare garbage collection. + @missing = grep { !defined($present{$$_{id}}) } @changes; + $utips = [ get_1st_parent($$commits[0]) ]; + } else { + @missing = @changes; + $utips = [ $tip ]; + } + } else { + print "Branch disappeared.\n" if ($debug); + # The branch was deleted, so all its Changes are missing. + @missing = @changes; + # For the upstream check, we fall back to the branches the Changes + # were targeting. This is worse than using the actual upstream, as + # the target may be outdated and the Change actually went to another + # branch. Also, it's potentially more work. + my $urefs = $remote_refs{$upstream_remote}; + if ($urefs) { + my %utiph = map { $_ => 1 } grep { $_ } map { $$urefs{$$_{tgt}} } @changes; + $utips = [ keys %utiph ]; + } + } + $sm_present{$br} = \%present; + + # We may need to garbage-collect the branch. + # Failure to do this would affect cherry-picks of Changes that were + # upstreamed - neither do we want to need --copy for Changes that are + # actually gone, nor do we want them to be detected as moves (and thus + # inherit the now incorrect target branch). + if (@missing) { + # Record the push base of each Change - if a Change is actually + # in our upstream, it will be so between its push base (which + # cannot be younger than the branch's tip at that time) and the + # branch's merge base with upstream. + my %bases = map { $_ => 1 } grep { $_ } map { $$_{base} } @missing; + if (%bases) { + print "Visiting upstream of other local branch $br ...\n" if ($debug); + my $urevs = visit_upstream_commits($utips, [ keys %bases ]); + foreach my $change (@missing) { + my $changeid = $$change{id}; + _obliterate_change($change, $changes_by_id{$changeid}) + if (defined($$urevs{$changeid})); + } + } + } + } + %sm_wanted = (); + return 1; +} + +# Determine which of the Change objects in $changes still refer to +# actual commits. +sub _find_candidate_sources($$$) +{ + my ($changes, $vanished, $persisting) = @_; + + return 0 if (!$changes); + + my $lbr = $local_branch // "-"; + my $retry = 0; + foreach my $chg (@$changes) { + # Hidden Changes are supposed to be skipped over, so it + # would be backwards to use them as sources for moves. + next if ($$chg{hide}); + + my $obr = $$chg{src}; + # The current branch is obviously not a sensible source. + next if ($obr eq $lbr); + + # Note that detached HEADs "vanish" after switching to an actual + # branch, thereby freeing all Changes assigned to them, sans the + # garbage-collected ones. + my $present = $sm_present{$obr}; + if (!$present) { + # Ensure that the branch is visited and garbage-collected. + $sm_wanted{$obr} = 1; + $retry = 1; + } elsif (defined($$present{$$chg{id}})) { + print "$$chg{id} persists on $obr.\n" if ($debug); + push @$persisting, $chg; + } else { + print "$$chg{id} vanished from $obr.\n" if ($debug); + $$chg{vanished} = 1; + push @$vanished, $chg; + } + } + return $retry; +} + +sub source_map_assign($$) +{ + my ($commit, $reference) = @_; + + my $change = $$commit{change}; + return $change if ($change); + + my $lbr = $local_branch // "-"; + + my $changeid = $$commit{changeid}; + my $changes = $changes_by_id{$changeid}; + my %change_by_branch; + if ($changes) { + foreach my $chg (@$changes) { + my $br = $$chg{src}; + $change_by_branch{$br} = $chg; + } + } + $change = $change_by_branch{$lbr}; + my $new = !$change; + + my $option = $sm_option_by_id{$reference // $$commit{id}} // $sm_option_new; + my $new_only = $option && !defined($$option{tip}); + my $action = $option ? $$option{action} : _SRC_NOOP; + if ($action == _SRC_COPY || $action == _SRC_HIDE) { + # Note: We don't bother finding Changes which could be recycled - + # gpull will clean up. + # We don't complain about no-ops, as they don't hurt. + my $label = ($action == _SRC_HIDE) ? "hidden" : "copied"; + if ($new || !$new_only) { + if ($new) { + $change = {}; + _init_change($change, $changeid); + } + $$change{hide} = ($action == _SRC_HIDE) ? 1 : undef; + wout(_format_result($commit, $new, "marked as $label")) + if (!$quiet); + goto PRINTED; + } + print "Already have $$change{key} ($changeid) on $lbr; not $label.\n" + if ($debug); + goto FOUND; + } + if ($new || ($action == _SRC_MOVE && !$new_only)) { + my $schange; + my $sbr = ($action == _SRC_MOVE) ? $$option{branch} : undef; + if (defined($sbr)) { + $schange = $change_by_branch{$sbr}; + if (!$schange) { + werr("Change $changeid does not exist on '$sbr'; cannot move.\n"); + goto FAIL; + } + if ($$schange{hide}) { + werr("Change $changeid is hidden on '$sbr'; cannot move.\n"); + goto FAIL; + } + } else { + my (@vanished, @persisting); + if (_find_candidate_sources($changes, \@vanished, \@persisting)) { + print "Need to come back for $changeid.\n" if ($debug); + return undef; + } + if ($new) { + if (@vanished) { + if (@vanished > 1) { + werr(_format_result( + $commit, 1, "was previously on %s.\n" + ." Use --move with a source, or use --copy/--hide", + _format_branches(\@vanished))); + goto FAIL; + } + $change = pop @vanished; + # Note: this is a post-fact notice, so if the user failed to use + # --copy/--hide in advance, they'll need to fix it laboriously. + # This seems acceptable, as moving is the much more common case. + wout(_format_result( + $commit, 1, "was previously on %s. Inferring move", + _format_branch($$change{src}))) + if (!$quiet); + goto PRINTED; + } + if (!@persisting) { + # This is the common case: an entirely new Change. + $change = {}; + _init_change($change, $changeid); + goto CHANGED; + } + if ($action != _SRC_MOVE) { + werr(_format_result( + $commit, 1, "exists also on %s.\n" + ." Prune unneeded copies, or use --move/--copy/--hide", + _format_branches_raw(\@persisting))); + goto FAIL; + } + } else { # implies $action == _SRC_MOVE + if (!$$change{hide}) { + # Attempts to move over active Changes are most likely mistakes. + werr(_format_result( + $commit, 0, "is not hidden.\n" + ." Pass a source to --move if you really mean it")); + goto FAIL; + } + if (@vanished) { + if (@vanished > 1) { + werr(_format_result( + $commit, 0, "was previously on %s.\n" + ." Pass a source to --move to disambiguate", + _format_branches(\@vanished))); + goto FAIL; + } + $schange = pop @vanished; + } elsif (!@persisting) { + werr(_format_result( + $commit, 0, "exists on no other branch. Use --copy to unhide it")); + goto FAIL; + } + } + if (!$schange) { + if (@persisting > 1) { + werr(_format_result( + $commit, $new, "exists also on %s.\n" + ." Prune unneeded copies, pass a source to --move," + ." or use --copy/--hide", + _format_branches_raw(\@persisting))); + goto FAIL; + } + $schange = pop @persisting; + } + $sbr = $$schange{src}; + } # !defined($sbr) + if ($change) { + # If the slot on the current branch is busy, we need to free it first. + # Note that this might be a hidden entry from a previous --move/--hide. + _obliterate_change($change, $changes); + } + if (!$$schange{vanished}) { + # If the Change still does or might exist on the source branch, we + # need to create a hidden entry for it, so it does not appear to be + # new next time around. + my %chg = (src => $sbr, grp => $$schange{grp}, hide => 1); + _init_change(\%chg, $changeid); + print "... and hidden on $sbr.\n" if ($debug); + } + wout(_format_result($commit, $new, "moved from %s", _format_branch($sbr))) + if (!$quiet); + $change = $schange; + goto PRINTED; + } + print "Already have $$change{key} ($changeid) on $lbr; leaving alone.\n" + if ($debug); + goto FOUND; + + PRINTED: + $sm_printed = 1 if (!$quiet); + CHANGED: + $$change{src} = $lbr; + $sm_changed = 1; + FOUND: + $$commit{change} = $change; + return $change; + + FAIL: + $sm_failed = 1; + return undef; +} + +sub source_map_finish() +{ + exit(1) if ($sm_failed); + print "\n" if ($sm_printed); # Delimit from remaining output. + $sm_printed = 0; +} + +sub _source_map_finish_initial() +{ + foreach my $option (@sm_options) { + # Only --copy & --hide imply grouping. + my $action = $$option{action}; + next if ($action != _SRC_COPY && $action != _SRC_HIDE); + + my $commits = $$option{commits}; + next if (!$commits); + assign_series(changes_from_commits($commits)); + $sm_changed = 1; + } + + source_map_finish(); + save_state() if ($sm_changed); +} + # Update the target branches of local Changes according to the data # from a Gerrit query. # Each SHA1 is assigned to exactly one PatchSet, which in turn is |