summaryrefslogtreecommitdiff
path: root/bin/git-gpick
diff options
context:
space:
mode:
authorOswald Buddenhagen <oswald.buddenhagen@qt.io>2014-12-15 18:48:18 +0100
committerOswald Buddenhagen <oswald.buddenhagen@gmx.de>2020-03-05 18:07:42 +0000
commitbe42ce32e992eb5d971255842c04683309cc9ec3 (patch)
tree6207839cd903a0fe610a02323059b60e46102033 /bin/git-gpick
parent6f58f6e3371a9a50dacd1db26ca6c1197d3dc1b9 (diff)
downloadqtrepotools-be42ce32e992eb5d971255842c04683309cc9ec3.tar.gz
gpick: fetch pre-cherry-pick PatchSets for MERGED Changes
gerrit creates a new PatchSet for the commit it cherry-picked into the target branch. this commit has the wrong parent, and is excluded from the listing by the negated upstream refs. both consequences would pose problems later on, so make an effort to fetch the PatchSet before the cherry-picked one. Change-Id: I22192202202df2ef734e7d61f995507d50f9ef08 Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@gmx.de>
Diffstat (limited to 'bin/git-gpick')
-rwxr-xr-xbin/git-gpick168
1 files changed, 151 insertions, 17 deletions
diff --git a/bin/git-gpick b/bin/git-gpick
index 93960a7..4a44d8a 100755
--- a/bin/git-gpick
+++ b/bin/git-gpick
@@ -626,41 +626,170 @@ sub map_update_spec($$$)
}
}
+sub add_patchset($$)
+{
+ my ($ginfo, $idxes) = @_;
+
+ my $idx = $$ginfo{pick_idx};
+ $$idxes{$idx} = 1;
+ my $revs = $$ginfo{revs};
+ $$idxes{$idx - 1} = 1
+ if (($idx > 0) && ($idx == $#$revs) && ($$ginfo{status} eq 'MERGED'));
+}
+
# Fetch the latest PatchSets for the specified Changes.
# We don't re-fetch PatchSets which we already have.
-sub fetch_patchsets($)
+sub fetch_patchsets($$)
{
- my ($ginfos) = @_;
+ my ($ginfos, $visits) = @_;
my @refs;
foreach my $ginfo (@$ginfos) {
- my ($revs, $pick_idx) = ($$ginfo{revs}, $$ginfo{pick_idx});
- my $rev = $$revs[$pick_idx];
- my ($rev_id, $rev_ps) = ($$rev{id}, $$rev{ps});
- if (defined($$ginfo{fetched}{$rev_ps})) {
- print "Already have PatchSet $rev_ps for $$ginfo{id}.\n" if ($debug);
- } else {
- push @refs, "+$$rev{ref}:refs/gpush/g$$ginfo{key}_$rev_ps";
+ my %idxes;
+ add_patchset($ginfo, \%idxes);
+ my $revs = $$ginfo{revs};
+ foreach my $idx (keys %idxes) {
+ my $rev = $$revs[$idx];
+ my ($rev_id, $rev_ps) = ($$rev{id}, $$rev{ps});
+ if (defined($$ginfo{fetched}{$rev_ps})) {
+ print "Already have PatchSet $rev_ps for $$ginfo{id}.\n" if ($debug);
+ $$visits{$rev_id} = 1;
+ } else {
+ push @refs, "+$$rev{ref}:refs/gpush/g$$ginfo{key}_$rev_ps";
+ $$visits{$rev_id} = 1;
+ }
}
- my $commit = {
- id => $rev_id,
- parents => $$rev{parents}
- };
- init_commit($commit);
- $$ginfo{pick_commit} = $commit;
}
if (@refs) {
+ # We need to fetch the current upstream to be able to exclude it.
+ # See visit_local_commits() for explanation for fetching all branches.
+ push @refs, "+refs/heads/*:refs/remotes/$remote/*";
print "Fetching PatchSets ...\n" if (!$quiet);
my @gitcmd = ("git", "fetch");
# The git-fetch output is quite noisy and unhelpful here, unless debugging.
push @gitcmd, '-q' if (!$debug);
push @gitcmd, $remote, @refs;
run_process(FWD_OUTPUT, @gitcmd);
+ print "Re-enumerating branches from Gerrit remote ...\n" if ($debug);
+ load_refs("refs/remotes/$remote/");
+ update_excludes();
} else {
print "No PatchSets need fetching.\n" if ($debug);
}
}
+# Verify that the 2nd commit is a cherry-pick of the 1st one.
+sub verify_cherrypick($$)
+{
+ my ($prev, $curr) = @_;
+
+ return 0 if ("@{$$curr{author}}" ne "@{$$prev{author}}");
+
+ my @curr_msg = split(/$/m, $$curr{message});
+ my @prev_msg = split(/$/m, $$prev{message});
+ # Cherry-picking may add various footers to the commit message.
+ # Therefore, we verify that the current message is a strict
+ # superset of the previous one.
+ while (@curr_msg && @prev_msg) {
+ my $line = pop @curr_msg;
+ # If this was an added line, we will re-sync at some point.
+ # If it wasn't, unconsumed lines will remain in @prev_msg.
+ pop @prev_msg if ($line eq $prev_msg[-1]);
+ }
+ return 0 if (@curr_msg || @prev_msg);
+
+ my $tree;
+ my $base = get_1st_parent($curr);
+ if (get_1st_parent($prev) eq $base) {
+ # If the pick didn't rebase, we can just compare the trees.
+ $tree = $$prev{tree};
+ } else {
+ # Otherwise the diff needs rebasing.
+ ($tree, undef) = apply_diff($prev, $base, NUL_STDERR);
+ return 0 if (!defined($tree));
+ }
+ return 0 if ($tree ne $$curr{tree});
+
+ return 1;
+}
+
+sub drop_merged_patchsets($$)
+{
+ my ($ginfos, $cmt_by_id) = @_;
+
+ foreach my $ginfo (@$ginfos) {
+ my $revs = $$ginfo{revs};
+ my $curr_commit = $$cmt_by_id{$$revs[-1]{id}};
+ next if (!$curr_commit); # The Change doesn't need fixup.
+ my $prev_commit = $commit_by_id{$$revs[-2]{id}};
+ wfail("Last-but-one PatchSet of Change $$ginfo{key} is ALSO already upstream?!\n")
+ if (!$prev_commit); # Actually seen in the wild. Qt Gerrit bug ...
+
+ # This can happen for Changes which were not cherry-picked -
+ # for example merges, or commits which were amended and then
+ # direct-pushed.
+ wfail("Last PatchSet of Change $$ginfo{key} is already upstream,"
+ ." and is not a cherry-pick of the previous one.\n")
+ if (!verify_cherrypick($prev_commit, $curr_commit));
+
+ print "Dropping upstreamed last PatchSet of $$ginfo{id}.\n" if ($debug);
+ pop @$revs;
+ }
+}
+
+sub analyze_patchset($)
+{
+ my ($ginfo) = @_;
+
+ my $idx = $$ginfo{pick_idx};
+ my $revs = $$ginfo{revs};
+ $$ginfo{pick_idx} = --$idx
+ if ($idx == @$revs);
+ my $rev = $$revs[$idx];
+ my $rev_id = $$rev{id};
+ my $commit = $commit_by_id{$rev_id};
+ wfail("PatchSet $$rev{ps} of Change $$ginfo{key} is apparently upstream?!\n")
+ if (!$commit);
+ $$ginfo{pick_commit} = $commit;
+}
+
+# Extract additional information from the fetched PatchSets.
+sub analyze_patchsets($)
+{
+ my ($ginfos) = @_;
+
+ print "Analyzing fetched PatchSets ...\n" if ($debug);
+ my @upstream;
+ foreach my $ginfo (@$ginfos) {
+ next if ($$ginfo{status} ne 'MERGED');
+
+ my $revs = $$ginfo{revs};
+ next if ($$ginfo{pick_idx} != $#$revs);
+
+ # Gerrit creates a new PatchSet when cherry-picking into the target
+ # branch. Obviously, this commit is excluded by ^@{upstream}.
+ my $rev_id = $$revs[-1]{id};
+
+ die("Change $$ginfo{key} is already merged, but not excluded by upstream?!\n")
+ if ($debug && $commit_by_id{$rev_id});
+
+ # This can happen for Changes which were not cherry-picked -
+ # for example merges.
+ fail("Change $$ginfo{key} has only one PatchSet, and that is already upstream.\n")
+ if (@$revs == 1);
+
+ push @upstream, $rev_id;
+ }
+ if (@upstream) {
+ my $commits = visit_commits_raw(\@upstream, ['--no-walk']);
+ my %cmt_by_id = map { $$_{id} => $_ } @$commits;
+ with_local_git_index(\&drop_merged_patchsets, $ginfos, \%cmt_by_id);
+ }
+ foreach my $ginfo (@$ginfos) {
+ analyze_patchset($ginfo);
+ }
+}
+
# Download the metadata and PatchSets referenced by the specs.
sub complete_spec_heads($)
{
@@ -668,7 +797,7 @@ sub complete_spec_heads($)
print "Completing commit specs ...\n" if ($debug);
- my %picks;
+ my (%picks, %visits);
foreach my $spec (@$specs) {
my $action = $$spec{action};
if ($action == INSERT) {
@@ -698,7 +827,12 @@ sub complete_spec_heads($)
}
exit(1) if ($any_errors);
- fetch_patchsets(\@fetches) if (@fetches);
+ fetch_patchsets(\@fetches, \%visits) if (@fetches);
+
+ print "Visiting fetched PatchSets ...\n" if ($debug);
+ visit_local_commits([ keys %visits ]);
+
+ analyze_patchsets(\@fetches) if (@fetches);
}
sub check_specs($)