summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/submit/SubmitStrategyListener.java
blob: d1f847bbd781f371c7d86eca35225fc7f6889bd9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.submit;

import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.submit.MergeOp.CommitStatus;
import com.google.gerrit.server.update.BatchUpdateListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.revwalk.RevCommit;

public class SubmitStrategyListener implements BatchUpdateListener {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private final Collection<SubmitStrategy> strategies;
  private final CommitStatus commitStatus;
  private final boolean failAfterRefUpdates;

  public SubmitStrategyListener(
      SubmitInput input, Collection<SubmitStrategy> strategies, CommitStatus commitStatus) {
    this.strategies = strategies;
    this.commitStatus = commitStatus;
    if (input instanceof TestSubmitInput) {
      failAfterRefUpdates = ((TestSubmitInput) input).failAfterRefUpdates;
    } else {
      failAfterRefUpdates = false;
    }
  }

  @Override
  public void afterUpdateRepos() throws ResourceConflictException {
    try {
      markCleanMerges();
      List<Change.Id> alreadyMerged = checkCommitStatus();
      findUnmergedChanges(alreadyMerged);
    } catch (IntegrationException e) {
      throw new ResourceConflictException(e.getMessage(), e);
    }
  }

  @Override
  public void afterUpdateRefs() throws ResourceConflictException {
    if (failAfterRefUpdates) {
      throw new ResourceConflictException("Failing after ref updates");
    }
  }

  private void findUnmergedChanges(List<Change.Id> alreadyMerged)
      throws ResourceConflictException, IntegrationException {
    for (SubmitStrategy strategy : strategies) {
      if (strategy instanceof CherryPick) {
        // Can't do this sanity check for CherryPick since:
        // * CherryPick might have picked a subset of changes
        // * CherryPick might have status SKIPPED_IDENTICAL_TREE
        continue;
      }
      SubmitStrategy.Arguments args = strategy.args;
      Set<Change.Id> unmerged =
          args.mergeUtil.findUnmergedChanges(
              args.commitStatus.getChangeIds(args.destBranch),
              args.rw,
              args.canMergeFlag,
              args.mergeTip.getInitialTip(),
              args.mergeTip.getCurrentTip(),
              alreadyMerged);
      for (Change.Id id : unmerged) {
        commitStatus.problem(id, "internal error: change not reachable from new branch tip");
      }
    }
    commitStatus.maybeFailVerbose();
  }

  private void markCleanMerges() throws IntegrationException {
    for (SubmitStrategy strategy : strategies) {
      SubmitStrategy.Arguments args = strategy.args;
      RevCommit initialTip = args.mergeTip.getInitialTip();
      args.mergeUtil.markCleanMerges(
          args.rw,
          args.canMergeFlag,
          args.mergeTip.getCurrentTip(),
          initialTip == null ? ImmutableSet.<RevCommit>of() : ImmutableSet.of(initialTip));
    }
  }

  private List<Change.Id> checkCommitStatus() throws ResourceConflictException {
    List<Change.Id> alreadyMerged = new ArrayList<>(commitStatus.getChangeIds().size());
    for (Change.Id id : commitStatus.getChangeIds()) {
      CodeReviewCommit commit = commitStatus.get(id);
      CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
      if (s == null) {
        logger.atSevere().log("change %d: change not processed by merge strategy", id.get());
        commitStatus.problem(id, "internal error: change not processed by merge strategy");
        continue;
      }
      if (commit.getStatusMessage().isPresent()) {
        logger.atFine().log(
            "change %d: Status for commit %s is %s. %s",
            id.get(), commit.name(), s, commit.getStatusMessage().get());
      } else {
        logger.atFine().log("change %d: Status for commit %s is %s.", id.get(), commit.name(), s);
      }
      switch (s) {
        case CLEAN_MERGE:
        case CLEAN_REBASE:
        case CLEAN_PICK:
        case SKIPPED_IDENTICAL_TREE:
          break; // Merge strategy accepted this change.

        case ALREADY_MERGED:
          // Already an ancestor of tip.
          alreadyMerged.add(commit.getPatchsetId().getParentKey());
          break;

        case PATH_CONFLICT:
        case REBASE_MERGE_CONFLICT:
        case MANUAL_RECURSIVE_MERGE:
        case CANNOT_CHERRY_PICK_ROOT:
        case CANNOT_REBASE_ROOT:
        case NOT_FAST_FORWARD:
        case EMPTY_COMMIT:
        case MISSING_DEPENDENCY:
          // TODO(dborowitz): Reformat these messages to be more appropriate for
          // short problem descriptions.
          String message = s.getDescription();
          if (commit.getStatusMessage().isPresent()) {
            message += " " + commit.getStatusMessage().get();
          }
          commitStatus.problem(id, CharMatcher.is('\n').collapseFrom(message, ' '));
          break;

        default:
          commitStatus.problem(id, "unspecified merge failure: " + s);
          break;
      }
    }
    commitStatus.maybeFailVerbose();
    return alreadyMerged;
  }

  @Override
  public void afterUpdateChanges() throws ResourceConflictException {
    commitStatus.maybeFail("Error updating status");
  }
}