summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorOswald Buddenhagen <oswald.buddenhagen@qt.io>2014-06-20 17:37:50 +0200
committerOswald Buddenhagen <oswald.buddenhagen@gmx.de>2020-04-01 18:22:13 +0000
commit379601a70ea747dff2dcdcf168b707b82aa70377 (patch)
tree494490013584fa473844ad6d23247a7eaafef488 /bin
parent131f1a82e3d88b978c6b777a82e8d5b2d441e800 (diff)
downloadqtrepotools-379601a70ea747dff2dcdcf168b707b82aa70377.tar.gz
gpush/gpick: add smart series grouping mode
information from previous pushes is used to deduce the changes which are part of a series identified by a single change. the original implementation of this feature would automatically capture leading loose Changes in front of an existing series, but that turned out to be a nuisance in practice, because it would often lead to accidental inclusion of unrelated Changes that were inserted before the series and forgotten about. we now simply ignore them; a subsequent change will deal with them. also, the original implementation used traversal of the actually pushed commits to reconstruct the series. that approach was dropped in favor of using dedicated meta data, because the latter is much simpler and also faster. Change-Id: Iedb5bd8ca825bcc679b65e524a1c3d76116b58dd Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@gmx.de>
Diffstat (limited to 'bin')
-rwxr-xr-xbin/git-gpick250
-rwxr-xr-xbin/git-gpush90
-rw-r--r--bin/git_gpush.pm70
3 files changed, 378 insertions, 32 deletions
diff --git a/bin/git-gpick b/bin/git-gpick
index 80afa92..44e4516 100755
--- a/bin/git-gpick
+++ b/bin/git-gpick
@@ -57,10 +57,12 @@ Description:
Local commits may be specified as either SHA1s or Gerrit Change-Ids,
possibly abbreviated. Git rev-spec suffixes like '~2' are allowed; if
only a suffix is specified, it is understood to be relative to 'HEAD'.
- It is also possible to specify ranges, either as <base>..<tip> or as
- <tip>:<count>. An empty <tip> means 'HEAD'. Similarly, an empty <base>
- means the merge base with the upstream branch; this also works in the
- parent specification.
+
+ Every Change specification denotes a range, either explicitly when
+ written as <base>..<tip> or <tip>:<count>, or implicitly as the tip
+ of a series (as determined by its previous push). An empty <tip>
+ means 'HEAD'. Similarly, an empty <base> means the merge base with
+ the upstream branch; this also works in the parent specification.
This program uses the most recent PatchSets which are not newer than
the reference date, which defaults to 'now'. See the git-rev-parse
@@ -174,17 +176,21 @@ Console Output:
command line are shown in parentheses.
Examples:
- git gpick ~1
+ git gpick ~1:1
Replace the last-but-one local commit with the latest PatchSet from
Gerrit.
+ git gpick ~1
+ Replace the series identified by the last-but-one local commit with
+ the latest PatchSets from Gerrit.
+
git gpick +I21f8ef385d1793757149dfa5cc69e4e907cb1c04
Add a series from Gerrit on top of the local branch.
git gpick +Idc2d0ac4f7d95a5f3bad24e82114e23ada79a542{yesterday}
The same, using the most recent PatchSets as of one day ago.
- git gpick /HEAD \@ ~1:2\@ +I1ad95db7c99018e92d2b2556e4789951b51b2fff:1
+ git gpick /HEAD:1 \@ ~1:2\@ +I1ad95db7c99018e92d2b2556e4789951b51b2fff:1
Drop the top commit, move the next two commits to the start of the
local branch and replace them with the latest PatchSets, and add
another Change right on top of them. The other commits in the local
@@ -194,7 +200,7 @@ Examples:
Bootstrap git-gpush before using it the first time with pre-existing
pending Changes.
- git gpick --check HEAD
+ git gpick --check :1
Check whether a new PatchSet was pushed to the Change corresponding
with the HEAD commit.
@@ -359,6 +365,187 @@ sub get_changes()
return [];
}
+# Deduce series grouping from picked Changes.
+# The grouping on Gerrit is considered authoritative, which may result
+# in previously assigned local series being split or joined. This
+# affects even Changes which are not being updated, as long as they
+# are part of a series which any Change of is being updated; this is
+# done to avoid that partial picks tear apart series. Locally assigned
+# Changes which are not claimed by an applied remote series remain
+# assigned to their previous series, unless they lie between Changes
+# which are claimed by separate remote series.
+#
+# The algorithm sequentially interates through all Changes passed to it.
+# The "prefix" state prepresents a local series which may still overlap
+# with a remote series. The "suffix" state represents a local series
+# which extends beyond an overlap with a remote series. A "hold" state
+# is entered from the respective "plain" state when the local series
+# runs into the start of a remote series which does not overlap a local
+# series (yet). The state table can be viewed at
+# https://docs.google.com/spreadsheets/d/1z2BkqggoYvAe3guPrhPf96Y7lx8JJqB9iUmBx1bISgs/edit?usp=sharing
+
+use constant {
+ DSS_DEFAULT => 0,
+ DSS_PREFIX => 1,
+ DSS_PREFIX_HOLD => 2,
+ DSS_SUFFIX => 3,
+ DSS_SUFFIX_HOLD => 4
+};
+
+use constant {
+ DSC_LCL_NONE => 0, # Neither current nor previous Change have gid.
+ DSC_LCL_START => 1, # Current Change has gid while previous didn't.
+ DSC_LCL_RESUME => 2, # Same, but the gid equals the last seen one.
+ DSC_LCL_CONT => 3, # Current and previous Change have same gid.
+ DSC_LCL_SWITCH => 4, # Current and previous Change have different gids.
+ DSC_LCL_END => 5, # Current Change has no gid while previous did.
+ DSC_RMT_NONE => 0x00,
+ DSC_RMT_START => 0x10,
+ DSC_RMT_RESUME => 0x20,
+ DSC_RMT_CONT => 0x30,
+ DSC_RMT_SWITCH => 0x40,
+ DSC_RMT_END => 0x50
+};
+
+sub deduce_series($)
+{
+ my ($changes) = @_;
+
+ my %groups; # { group-id => [ change, ... ] }
+ my $state = DSS_DEFAULT;
+ my @backlog;
+
+ local *drop = sub {
+ my ($sts) = @_;
+ print "... dropping backlog.\n" if ($debug);
+ @backlog = ();
+ $state = $sts // DSS_DEFAULT;
+ };
+
+ local *flush = sub {
+ my ($grp, $sts) = @_;
+ print "... committing backlog to $grp.\n" if ($debug);
+ push @{$groups{$grp}}, @backlog;
+ @backlog = ();
+ $state = $sts // DSS_DEFAULT;
+ };
+
+ print "Deducing series from picked Changes ...\n" if ($debug);
+ my ($good_lcl_grp, $good_rmt_grp) = (0, ""); # The last non-null value.
+ my ($prev_lcl_grp, $prev_rmt_grp) = (0, ""); # From the immediately preceding Change.
+ foreach my $change (@$changes, undef) {
+ my ($curr_lcl_grp, $curr_rmt_grp) = (0, "");
+ if ($change) {
+ $curr_lcl_grp = $$change{grp} // 0;
+ my $ginfo = $$change{gerrit};
+ # Ungrouped Changes which also are not being picked are
+ # completely ignored.
+ next if (!$curr_lcl_grp && !$ginfo);
+ $curr_rmt_grp = $$ginfo{pick_commit}{pgrp} if ($ginfo);
+ } else {
+ # When we run out of Changes, we still synthetize a pair of
+ # commands, so a pending backlog is flushed if necessary.
+ }
+ my $lcl_cmd = $curr_lcl_grp
+ ? $prev_lcl_grp
+ ? ($curr_lcl_grp == $prev_lcl_grp) ? DSC_LCL_CONT : DSC_LCL_SWITCH
+ : ($curr_lcl_grp == $good_lcl_grp) ? DSC_LCL_RESUME : DSC_LCL_START
+ : $prev_lcl_grp ? DSC_LCL_END : DSC_LCL_NONE;
+ my $rmt_cmd = length($curr_rmt_grp)
+ ? length($prev_rmt_grp)
+ ? ($curr_rmt_grp eq $prev_rmt_grp) ? DSC_RMT_CONT : DSC_RMT_SWITCH
+ : ($curr_rmt_grp eq $good_rmt_grp) ? DSC_RMT_RESUME : DSC_RMT_START
+ : length($prev_rmt_grp) ? DSC_RMT_END : DSC_RMT_NONE;
+ printf("sts=%d lcl=%d rmt=%d chg=%s\n",
+ $state, $lcl_cmd, $rmt_cmd >> 4, $change ? $$change{id} : "<end>")
+ if ($debug);
+ my $cmd = $lcl_cmd | $rmt_cmd;
+ if ($state == DSS_PREFIX) {
+ if ($cmd == (DSC_LCL_CONT | DSC_RMT_START) ||
+ $cmd == (DSC_LCL_CONT | DSC_RMT_RESUME)) {
+ flush($curr_rmt_grp);
+ } elsif ($cmd == (DSC_LCL_SWITCH | DSC_RMT_START) ||
+ $cmd == (DSC_LCL_SWITCH | DSC_RMT_RESUME) ||
+ $cmd == (DSC_LCL_END | DSC_RMT_NONE)) {
+ drop();
+ } elsif ($cmd == (DSC_LCL_SWITCH | DSC_RMT_NONE)) {
+ drop(DSS_PREFIX);
+ } elsif ($cmd == (DSC_LCL_END | DSC_RMT_START) ||
+ $cmd == (DSC_LCL_END | DSC_RMT_RESUME)) {
+ $state = DSS_PREFIX_HOLD;
+ }
+ } elsif ($state == DSS_PREFIX_HOLD) {
+ if ($cmd == (DSC_LCL_RESUME | DSC_RMT_CONT)) {
+ flush($curr_rmt_grp);
+ } elsif ($cmd == (DSC_LCL_NONE | DSC_RMT_SWITCH) ||
+ $cmd == (DSC_LCL_NONE | DSC_RMT_END) ||
+ $cmd == (DSC_LCL_START | DSC_RMT_CONT) ||
+ $cmd == (DSC_LCL_START | DSC_RMT_SWITCH) ||
+ $cmd == (DSC_LCL_RESUME | DSC_RMT_SWITCH)) {
+ drop();
+ } elsif ($cmd == (DSC_LCL_START | DSC_RMT_END)) {
+ drop(DSS_PREFIX);
+ } elsif ($cmd == (DSC_LCL_RESUME | DSC_RMT_END)) {
+ $state = DSS_PREFIX;
+ }
+ } elsif ($state == DSS_SUFFIX) {
+ if ($cmd == (DSC_LCL_CONT | DSC_RMT_RESUME) ||
+ $cmd == (DSC_LCL_SWITCH | DSC_RMT_START) ||
+ $cmd == (DSC_LCL_SWITCH | DSC_RMT_RESUME) ||
+ $cmd == (DSC_LCL_END | DSC_RMT_NONE) ||
+ $cmd == (DSC_LCL_END | DSC_RMT_RESUME)) {
+ flush($good_rmt_grp);
+ } elsif ($cmd == (DSC_LCL_SWITCH | DSC_RMT_NONE)) {
+ flush($good_rmt_grp, DSS_PREFIX);
+ } elsif ($cmd == (DSC_LCL_CONT | DSC_RMT_START)) {
+ drop();
+ } elsif ($cmd == (DSC_LCL_END | DSC_RMT_START)) {
+ $state = DSS_SUFFIX_HOLD;
+ }
+ } elsif ($state == DSS_SUFFIX_HOLD) {
+ if ($cmd == (DSC_LCL_NONE | DSC_RMT_SWITCH) ||
+ $cmd == (DSC_LCL_NONE | DSC_RMT_END) ||
+ $cmd == (DSC_LCL_START | DSC_RMT_CONT) ||
+ $cmd == (DSC_LCL_START | DSC_RMT_SWITCH) ||
+ $cmd == (DSC_LCL_RESUME | DSC_RMT_SWITCH)) {
+ flush($good_rmt_grp);
+ } elsif ($cmd == (DSC_LCL_START | DSC_RMT_END)
+ || $cmd == (DSC_LCL_RESUME | DSC_RMT_END)) {
+ flush($good_rmt_grp, DSS_PREFIX);
+ } elsif ($cmd == (DSC_LCL_RESUME | DSC_RMT_CONT)) {
+ drop();
+ }
+ } else { # DSS_DEFAULT
+ if ($cmd == (DSC_LCL_START | DSC_RMT_NONE) ||
+ $cmd == (DSC_LCL_START | DSC_RMT_END) ||
+ $cmd == (DSC_LCL_SWITCH | DSC_RMT_END)) {
+ $state = DSS_PREFIX;
+ } elsif ($cmd == (DSC_LCL_RESUME | DSC_RMT_END) ||
+ $cmd == (DSC_LCL_CONT | DSC_RMT_END)) {
+ $state = DSS_SUFFIX;
+ }
+ }
+
+ last if (!$change);
+
+ if (length($curr_rmt_grp)) {
+ print "... comitting Change to $curr_rmt_grp.\n" if ($debug);
+ push @{$groups{$curr_rmt_grp}}, $change;
+ } else {
+ print "... backlogging Change.\n" if ($debug);
+ push @backlog, $change;
+ }
+
+ ($prev_lcl_grp, $prev_rmt_grp) = ($curr_lcl_grp, $curr_rmt_grp);
+ $good_lcl_grp = $curr_lcl_grp if ($curr_lcl_grp);
+ $good_rmt_grp = $curr_rmt_grp if (length($curr_rmt_grp));
+ }
+
+ foreach my $group (values %groups) {
+ assign_series($group);
+ }
+}
+
sub is_closed_status($)
{
my ($sts) = @_;
@@ -541,8 +728,12 @@ sub resolve_specs($)
my $base = parse_local_rev($raw_base, SPEC_BASE);
$range = changes_from_commits(get_commits_base($base, $tip, $raw_base, $raw_tip));
} else {
+ my $gid;
my $pivot = $commit_by_id{$tip}{change};
- $range = [ $pivot ];
+ ($range, $gid) = do_determine_series($pivot);
+ wfail("Spec $$spec{orig} points at loose Change(s)."
+ ." Please specify an exact range.\n")
+ if (!defined($gid));
}
wfail("Range $$spec{orig} is empty.\n") if (!@$range);
foreach my $change (@$range) {
@@ -1262,6 +1453,9 @@ sub advance_remote_series($$@)
# Assemble a remote series from its tip commit. On the way record required
# Changes that we didn't query yet, and fail the operation if there are any.
+# Additionally, determine the remote group id for the series, which is
+# needed for deduce_series(); this is the reason why we always traverse down
+# to the bottom of the series even if we need only a part of it.
sub assemble_remote_series($$$$$$$)
{
my ($ginfo, $base, $stamp, $bmap, $seen, $missing, $fails) = @_;
@@ -1272,6 +1466,13 @@ sub assemble_remote_series($$$$$$$)
my ($series, $anchor) = assemble_series(
$$ginfo{id}, $sha1, $rbase, $seen, \&advance_remote_series,
$stamp, $bmap, $missing, $fails);
+ if (!$$fails) { # Only an optimization.
+ # The first Change in the series identifies it. If we got
+ # cut off, get the ID from the already seen parents.
+ my $pgrp = $anchor ? $$anchor{pgrp} : $$series[0]{changeid};
+ #print "Assigning remote group $pgrp.\n" if ($debug);
+ $$_{pgrp} = $pgrp foreach (@$series);
+ }
return ($series, $anchor);
}
@@ -1595,16 +1796,14 @@ sub complete_spec_tails($)
my (%bmap, %pmap);
foreach my $change (@{$$spec{range}}) {
my $changeid = $$change{id};
- next if ($ignore_struct);
my $base = $$change{base};
$bmap{$changeid} = $base if (defined($base));
- next if ($force_struct);
+ next if ($ignore_struct || $force_struct);
my $pushed = $$change{pushed};
$pmap{$changeid} = $pushed if (defined($pushed));
}
- next if ($ignore_struct);
$$spec{bmap} = \%bmap;
- next if ($force_struct);
+ next if ($ignore_struct || $force_struct);
$$spec{pmap} = \%pmap;
}
@@ -1628,8 +1827,6 @@ sub complete_spec_tails($)
if ($action == INSERT) {
resolve_insertion_spec($spec, \%picks);
} elsif ($action == UPDATE) {
- next if ($ignore_struct);
-
# This is a local spec. We need to obtain the corresponding remote
# series. It might have a different structure including a different
# tip, so just try to collect all Changes belonging to it and sort
@@ -1638,7 +1835,9 @@ sub complete_spec_tails($)
# dependencies. The workaround is specifying these Changes manually.
complete_remote_series($spec, \%picks);
- next if ($force_struct);
+ # Note that we cannot skip the above even in --ignore-struct mode,
+ # as it is necessary for deduce_series().
+ next if ($ignore_struct || $force_struct);
# Prepare reconstruction of the series' last push from this repo.
complete_pushed_series($spec, \%pushes);
@@ -1805,12 +2004,20 @@ sub finalize_specs($)
}
}
-sub prepare_specs()
+sub prepare_specs($)
{
+ my ($raw_changes) = @_;
+
my $specs = parse_specs(\@commit_specs);
resolve_specs($specs);
complete_spec_heads($specs);
complete_spec_tails($specs);
+
+ # In --check mode we skip the post-adjust series deduction,
+ # as the adjusted Change list is not committed. Instead, do
+ # it before any adjustments are made.
+ deduce_series($raw_changes) if ($check);
+
check_specs($specs);
finalize_specs($specs);
return $specs;
@@ -2346,7 +2553,7 @@ sub do_adjust_changes($)
my ($need_force_del, $need_force_repl, $can_merge);
my ($any_missing, $any_crossed, $any_merged);
- my (@commits, @reports);
+ my (@commits, @changes, @reports);
foreach my $pair (@$pairs) {
my ($action, $change) = @$pair;
my $ginfo = $$change{gerrit};
@@ -2431,6 +2638,7 @@ sub do_adjust_changes($)
$commit = $rmt_commit;
}
push @commits, $commit;
+ push @changes, $change;
if ($upd & UPD_PUSHED) {
$$change{pushed} = $$rmt_commit{id};
# Make the downloaded commit recyclable. This won't help in many
@@ -2508,6 +2716,12 @@ sub do_adjust_changes($)
print "\n" if ($any_msg);
+ # All Changes which are grouped as an effect of this gpick run get
+ # a new gid assigned. A side effect of this is that series which
+ # got fragmented due to moving around Changes locally may be
+ # permanently split, which seems quite reasonable.
+ deduce_series(\@changes) if (!$check);
+
return \@commits;
}
@@ -2695,7 +2909,7 @@ load_state(1);
determine_local_branch();
my $raw_commits = get_changes();
my $raw_changes = changes_from_commits($raw_commits);
-my $specs = prepare_specs();
+my $specs = prepare_specs($raw_changes);
my $pairs = apply_specs($specs, $raw_changes);
my $commits = adjust_changes($pairs);
rewrite_changes($raw_commits, $commits) if (!$check);
diff --git a/bin/git-gpush b/bin/git-gpush
index 012a111..0600c71 100755
--- a/bin/git-gpush
+++ b/bin/git-gpush
@@ -67,6 +67,13 @@ Description:
they default to '\@{upstream}' and 'HEAD', respectively.
If 'from' is not specified, 'HEAD' is used.
+ When pushing a series for the first time, the exact range of commits
+ needs to be specified. Subsequent pushes of the same series need only
+ the tip. Loose Changes in the middle of the series will be automatically
+ captured by it.
+ It is possible to regroup series any time by specifying new exact
+ ranges.
+
Note that this program can be used in the middle of an interactive
rebase, to push out the amended commits instantly.
@@ -191,6 +198,12 @@ Examples:
Push the range HEAD~4..HEAD~1 for branch 5.4, rebased on top of
commit 85af7f4538b.
+ git gpush Iedb5bd8ca8:3
+ ...
+ git gpush Iedb5bd8ca8
+ Push the Change Iedb5bd8ca8 and the two Changes in front of it.
+ Subsequently, re-push the Changes, specifying only the tip.
+
Copyright:
Copyright (C) 2017 The Qt Company Ltd.
Copyright (C) 2014 Intel Corporation.
@@ -320,17 +333,22 @@ sub parse_arguments(@)
}
}
+ my $series_specific =
+ $commit_count || $from_base
+ || defined($ref_to) || defined($topic);
my $push_specific =
@reviewers || @CCs || $force || $force_branch
- || defined($remote) || defined($ref_to) || defined($topic);
+ || defined($remote);
if ($list_only) {
fail("--list/--list-online is incompatible with --quiet/--verbose.\n")
if ($quiet || ($verbose && !$debug));
+ fail("--list/--list-online is incompatible with series-specific options.\n")
+ if ($series_specific);
fail("--list/--list-online is incompatible with push-modifying options.\n")
if ($push_specific || (!$list_online && $minimal_override));
- fail("--list is incompatible with base-modifying options.\n")
- if (defined($ref_base) && !$list_online);
+ fail("--list/--list-online is incompatible with base-modifying options.\n")
+ if (defined($ref_base));
}
}
@@ -389,6 +407,10 @@ sub caption_group($)
my ($group) = @_;
my $changes = $$group{changes};
+ if (!$$group{gid}) {
+ return (sprintf("Set of %d loose Change(s):", int(@$changes)),
+ $changes);
+ }
my $to = $$group{branch};
my $tos = defined($to) ? " for $to" : "";
my $tpc = $$group{topic};
@@ -626,30 +648,60 @@ sub initialize_get_changes()
return $commit;
}
-sub finalize_get_changes($)
+sub finalize_get_changes($$)
{
- my ($changes) = @_;
+ my ($changes, $gid) = @_;
- my %group = (changes => $changes);
+ my %group = (changes => $changes, gid => $gid);
return \%group;
}
+sub define_series($)
+{
+ my ($commits) = @_;
+
+ # A new group key is used if a series is being (re-)assigned;
+ # for simplicity, we do not bother preserving keys.
+ # Stealing Changes from existing series could dissolve these series
+ # entirely. This implementation does not do this for simplicity's
+ # sake, thus losing some expressiveness - but that might have been
+ # more annoying than helpful anyway.
+ return (changes_from_commits($commits), $next_group++);
+}
+
+sub determine_series($)
+{
+ my ($pivot_id) = @_;
+
+ my $pivot = $commit_by_id{$pivot_id}{change};
+ my ($changes, $gid) = do_determine_series($pivot);
+ if (!$gid && @$changes && !$list_only) {
+ my @reports;
+ report_fixed(\@reports, "Attempted to push ".int(@$changes)." loose Change(s):\n");
+ report_local_changes(\@reports, $changes);
+ report_fixed(\@reports, "Please specify exact ranges.\n");
+ fail_formatted(\@reports);
+ }
+ # The group key is preserved even when the series gains new Changes.
+ return ($changes, $gid);
+}
+
# Get the list of local commits to push.
sub get_changes()
{
my $tip = initialize_get_changes();
# Assemble the series of Changes to push from the pool.
- my $changes;
+ my ($changes, $gid);
if (defined($from_base)) {
my $base = parse_local_rev($from_base, SPEC_BASE);
- $changes = changes_from_commits(get_commits_base($base, $tip, $from_base, $from));
+ ($changes, $gid) = define_series(get_commits_base($base, $tip, $from_base, $from));
} elsif ($commit_count) {
- $changes = changes_from_commits(get_commits_count($tip, $commit_count, $from));
+ ($changes, $gid) = define_series(get_commits_count($tip, $commit_count, $from));
} else {
- $changes = changes_from_commits(get_commits_free($tip));
+ ($changes, $gid) = determine_series($tip);
}
fail("Specified commit range is empty.\n") if (!@$changes);
- my $group = finalize_get_changes($changes);
+ my $group = finalize_get_changes($changes, $gid);
return $group;
}
@@ -1345,6 +1397,11 @@ sub annotate_changes($)
foreach my $change (@{$$group{changes}}) {
my @attribs;
+ # Changes can be loose only when listing, as
+ # otherwise they are currently being assigned, and showing
+ # them as still loose would be weird.
+ my $loose = $list_only && !defined($$change{grp});
+ push @attribs, 'LOOSE' if ($loose);
# Changes in the 'modified' state (that is, the ones for which pushing
# actually has an effect) are annotated, while 'unmodified' ones are not.
# This behavior has been chosen after much deliberation following the
@@ -1353,8 +1410,13 @@ sub annotate_changes($)
# thus potentially confusing - but as having no modified changes at all
# leads to an additional message, the less noisy output (assuming that
# most Changes are usually not modified) seems most sensible.
+ # For loose Changes the most sensible default assumption is that they
+ # are 'new'; deviations occur for example when cherry-picking was done
+ # without gpick, or pushing was done without (or with an older) gpush.
my $freshness = $$change{freshness};
- if (defined($freshness) && ($freshness ne UNMODIFIED) && ($freshness ne KNOWN)) {
+ if (defined($freshness)
+ && ($loose ? ($freshness ne NEW)
+ : ($freshness ne UNMODIFIED) && ($freshness ne KNOWN))) {
$freshness = "PS$$change{patchset}/$freshness"
if ($freshness eq OUTDATED || $freshness eq FORCE);
push @attribs, $freshness;
@@ -1555,10 +1617,12 @@ sub update_state($)
{
my ($group) = @_;
- my ($branch, $tpc, $base) = ($$group{branch}, $$group{topic}, $$group{base});
+ my ($gid, $branch, $tpc, $base) =
+ ($$group{gid}, $$group{branch}, $$group{topic}, $$group{base});
# Setting an empty topic clears the previous topic from the server.
$tpc = undef if (defined($tpc) && !length($tpc));
foreach my $change (@{$$group{changes}}) {
+ $$change{grp} = $gid;
my $sha1 = $$change{final}{id};
if (($$change{pushed} // "") ne $sha1) {
$$change{pushed} = $sha1;
diff --git a/bin/git_gpush.pm b/bin/git_gpush.pm
index f597291..ea4721e 100644
--- a/bin/git_gpush.pm
+++ b/bin/git_gpush.pm
@@ -639,6 +639,7 @@ our %gerrit_infos_by_id;
# - key: Sequence number. This runs independently from Gerrit, so
# we can enumerate Changes which were never pushed, and to make
# it possible to re-associate local Changes with remote ones.
+# - grp: Group (series) sequence number.
# - id: Gerrit Change-Id.
# - src: Local branch name, or "-" if Change is on a detached HEAD.
# - tgt: Target branch name.
@@ -655,6 +656,8 @@ our %change_by_key; # { sequence-number => change-object }
# Same, indexed by Gerrit Change-Id. A Change can exist on multiple branches.
our %changes_by_id; # { gerrit-id => [ change-object, ... ] }
+our $next_group = 10000;
+
our $last_gc = 0;
my $state_lines;
@@ -695,7 +698,7 @@ sub save_state(;$$)
print "Saving ".($new ? "new " : "")."state".($dry ? " [DRY]" : "")." ...\n" if ($debug);
my (@lines, @updates);
- my @fkeys = ('key', 'id', 'src', 'tgt', 'topic', 'base');
+ my @fkeys = ('key', 'grp', 'id', 'src', 'tgt', 'topic', 'base');
my @rkeys = ('pushed', 'orig');
if ($new) {
push @lines, "verify $new", "updater $state_updater";
@@ -704,6 +707,7 @@ sub save_state(;$$)
}
push @lines,
"next_key $next_key",
+ "next_group $next_group",
"last_gc $last_gc",
"";
foreach my $key (sort keys %change_by_key) {
@@ -826,6 +830,8 @@ sub load_state_file(;$)
} elsif ($inhdr) {
if ($1 eq "next_key") {
$next_key = int($2);
+ } elsif ($1 eq "next_group") {
+ $next_group = int($2);
} elsif ($1 eq "last_gc") {
$last_gc = int($2);
} elsif ($new && ($1 eq "verify")) {
@@ -1211,11 +1217,14 @@ sub analyze_local_branch($)
# ... 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;
$$change{local} = $commit;
$$change{index} = $idx++;
+ $$change{parent} = $prev;
+ $prev = $change;
}
return 1;
@@ -1368,6 +1377,65 @@ sub parse_local_rev($$)
return _parse_local_rev_id_only($rev, $scope);
}
+################
+# smart series #
+################
+
+sub assign_series($)
+{
+ my ($changes) = @_;
+
+ my $gid = $next_group++;
+ $$_{grp} = $gid foreach (@$changes);
+}
+
+# Deduce a series from a single commit.
+# Merges are treated in --first-parent mode.
+sub do_determine_series($)
+{
+ my ($change) = @_;
+
+ print "Deducing series from $$change{id}\n" if ($debug);
+ my (@prospects, @changes);
+ my $group_key;
+ while (1) {
+ my $gid = $$change{grp};
+ if (!defined($gid)) {
+ print "Prospectively capturing loose $$change{id}\n" if ($debug);
+ unshift @prospects, $change;
+ } else {
+ if (@changes) {
+ # We already have a proto-series.
+ # Check whether the new candidate is part of it.
+ if ($gid != $group_key) {
+ # Miss; end of series.
+ print "Breaking off at foreign bound $$change{id}\n" if ($debug);
+ last;
+ }
+ # Hit; add the Change to the series.
+ print "Adding bound $$change{id} and ".int(@prospects)." prospect(s)\n"
+ if ($debug);
+ } elsif (!@prospects) {
+ # The specified tip Change is bound.
+ print "Adding bound $$change{id} at tip\n"
+ if ($debug);
+ # This Change determines the series.
+ $group_key = $gid;
+ } else {
+ # Stop when encountering a bound Change after only loose ones.
+ print "Breaking off at bound $$change{id} after only loose\n" if ($debug);
+ last;
+ }
+ unshift @changes, $change, @prospects;
+ @prospects = ();
+ }
+ $change = $$change{parent};
+ last if (!$change);
+ }
+ return (\@prospects, undef) if (!defined($group_key));
+ return (\@changes, $group_key);
+}
+
###################
# commit creation #
###################