Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.query.change;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 : import static com.google.common.flogger.LazyArgs.lazy;
19 : import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
20 : import static java.util.concurrent.TimeUnit.MINUTES;
21 :
22 : import com.google.common.flogger.FluentLogger;
23 : import com.google.common.util.concurrent.UncheckedExecutionException;
24 : import com.google.gerrit.entities.BooleanProjectConfig;
25 : import com.google.gerrit.entities.BranchNameKey;
26 : import com.google.gerrit.entities.Change;
27 : import com.google.gerrit.entities.Project;
28 : import com.google.gerrit.entities.SubmitTypeRecord;
29 : import com.google.gerrit.exceptions.StorageException;
30 : import com.google.gerrit.index.query.PostFilterPredicate;
31 : import com.google.gerrit.index.query.Predicate;
32 : import com.google.gerrit.index.query.QueryParseException;
33 : import com.google.gerrit.server.git.CodeReviewCommit;
34 : import com.google.gerrit.server.project.NoSuchProjectException;
35 : import com.google.gerrit.server.project.ProjectCache;
36 : import com.google.gerrit.server.project.ProjectState;
37 : import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
38 : import com.google.gerrit.server.submit.SubmitDryRun;
39 : import java.io.IOException;
40 : import java.util.ArrayList;
41 : import java.util.HashSet;
42 : import java.util.List;
43 : import java.util.Set;
44 : import java.util.concurrent.Callable;
45 : import java.util.concurrent.ExecutionException;
46 : import org.eclipse.jgit.lib.ObjectId;
47 : import org.eclipse.jgit.lib.Repository;
48 : import org.eclipse.jgit.revwalk.RevCommit;
49 : import org.eclipse.jgit.revwalk.RevWalk;
50 :
51 : public class ConflictsPredicate {
52 4 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
53 :
54 : // UI code may depend on this string, so use caution when changing.
55 : protected static final String TOO_MANY_FILES = "too many files to find conflicts";
56 :
57 : private ConflictsPredicate() {}
58 :
59 : public static Predicate<ChangeData> create(Arguments args, String value, Change c)
60 : throws QueryParseException {
61 : ChangeData cd;
62 : List<String> files;
63 : try {
64 4 : cd = args.changeDataFactory.create(c);
65 4 : files = cd.currentFilePaths();
66 0 : } catch (StorageException e) {
67 0 : warnWithOccasionalStackTrace(
68 : e,
69 : "Error constructing conflicts predicates for change %s in %s",
70 0 : c.getId(),
71 0 : c.getProject());
72 0 : return ChangeIndexPredicate.none();
73 4 : }
74 :
75 4 : if (3 + files.size() > args.indexConfig.maxTerms()) {
76 : // Short-circuit with a nice error message if we exceed the index
77 : // backend's term limit. This assumes that "conflicts:foo" is the entire
78 : // query; if there are more terms in the input, we might not
79 : // short-circuit here, which will result in a more generic error message
80 : // later on in the query parsing.
81 0 : throw new QueryParseException(TOO_MANY_FILES);
82 : }
83 :
84 4 : List<Predicate<ChangeData>> filePredicates = new ArrayList<>(files.size());
85 4 : for (String file : files) {
86 4 : filePredicates.add(ChangePredicates.path(file));
87 4 : }
88 :
89 4 : List<Predicate<ChangeData>> and = new ArrayList<>(5);
90 4 : and.add(ChangePredicates.project(c.getProject()));
91 4 : and.add(ChangePredicates.ref(c.getDest().branch()));
92 4 : and.add(Predicate.not(ChangePredicates.idStr(c.getId())));
93 4 : and.add(Predicate.or(filePredicates));
94 :
95 4 : ChangeDataCache changeDataCache = new ChangeDataCache(cd, args.projectCache);
96 4 : and.add(new CheckConflict(value, args, c, changeDataCache));
97 4 : return Predicate.and(and);
98 : }
99 :
100 : private static final class CheckConflict extends PostFilterPredicate<ChangeData> {
101 : private final Arguments args;
102 : private final BranchNameKey dest;
103 : private final ChangeDataCache changeDataCache;
104 :
105 : CheckConflict(String value, Arguments args, Change c, ChangeDataCache changeDataCache) {
106 4 : super(ChangeQueryBuilder.FIELD_CONFLICTS, value);
107 4 : this.args = args;
108 4 : this.dest = c.getDest();
109 4 : this.changeDataCache = changeDataCache;
110 4 : }
111 :
112 : @Override
113 : public boolean match(ChangeData object) {
114 4 : Change.Id id = object.getId();
115 4 : Project.NameKey otherProject = null;
116 4 : ObjectId other = null;
117 : try {
118 4 : Change otherChange = object.change();
119 4 : if (otherChange == null || !otherChange.getDest().equals(dest)) {
120 0 : return false;
121 : }
122 4 : otherProject = otherChange.getProject();
123 :
124 4 : SubmitTypeRecord str = object.submitTypeRecord();
125 4 : if (!str.isOk()) {
126 0 : return false;
127 : }
128 :
129 : ProjectState projectState;
130 : try {
131 4 : projectState = changeDataCache.getProjectState();
132 0 : } catch (NoSuchProjectException e) {
133 0 : return false;
134 4 : }
135 :
136 4 : other = object.currentPatchSet().commitId();
137 4 : ConflictKey conflictsKey =
138 4 : ConflictKey.create(
139 4 : changeDataCache.getTestAgainst(),
140 : other,
141 : str.type,
142 4 : projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
143 4 : return args.conflictsCache.get(conflictsKey, new Loader(object, changeDataCache, args));
144 0 : } catch (StorageException | ExecutionException | UncheckedExecutionException e) {
145 0 : ObjectId finalOther = other;
146 0 : warnWithOccasionalStackTrace(
147 : e,
148 : "Merge failure checking conflicts of change %s in %s (%s): %s",
149 : id,
150 0 : firstNonNull(otherProject, "unknown project"),
151 0 : lazy(() -> finalOther != null ? finalOther.name() : "unknown commit"),
152 0 : e.getMessage());
153 0 : return false;
154 : }
155 : }
156 :
157 : @Override
158 : public int getCost() {
159 4 : return 5;
160 : }
161 : }
162 :
163 : static class ChangeDataCache {
164 : private final ChangeData cd;
165 : private final ProjectCache projectCache;
166 :
167 : private ObjectId testAgainst;
168 : private ProjectState projectState;
169 : private Set<ObjectId> alreadyAccepted;
170 :
171 4 : ChangeDataCache(ChangeData cd, ProjectCache projectCache) {
172 4 : this.cd = cd;
173 4 : this.projectCache = projectCache;
174 4 : }
175 :
176 : ObjectId getTestAgainst() {
177 4 : if (testAgainst == null) {
178 4 : testAgainst = cd.currentPatchSet().commitId();
179 : }
180 4 : return testAgainst;
181 : }
182 :
183 : ProjectState getProjectState() throws NoSuchProjectException {
184 4 : if (projectState == null) {
185 4 : projectState = projectCache.get(cd.project()).orElseThrow(noSuchProject(cd.project()));
186 : }
187 4 : return projectState;
188 : }
189 :
190 : Set<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
191 4 : if (alreadyAccepted == null) {
192 4 : alreadyAccepted = SubmitDryRun.getAlreadyAccepted(repo);
193 : }
194 4 : return alreadyAccepted;
195 : }
196 : }
197 :
198 : private static void warnWithOccasionalStackTrace(Throwable cause, String format, Object... args) {
199 0 : logger.atWarning().logVarargs(format, args);
200 0 : logger
201 0 : .atWarning()
202 0 : .withCause(cause)
203 0 : .atMostEvery(1, MINUTES)
204 0 : .logVarargs("(Re-logging with stack trace) " + format, args);
205 0 : }
206 :
207 : private static class Loader implements Callable<Boolean> {
208 : private final ChangeData changeData;
209 : private final ConflictsPredicate.ChangeDataCache changeDataCache;
210 : private final ChangeQueryBuilder.Arguments args;
211 :
212 : private Loader(
213 : ChangeData changeData,
214 : ConflictsPredicate.ChangeDataCache changeDataCache,
215 4 : ChangeQueryBuilder.Arguments args) {
216 4 : this.changeData = changeData;
217 4 : this.changeDataCache = changeDataCache;
218 4 : this.args = args;
219 4 : }
220 :
221 : @Override
222 : public Boolean call() throws Exception {
223 4 : Change otherChange = changeData.change();
224 4 : ObjectId other = changeData.currentPatchSet().commitId();
225 4 : try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
226 4 : CodeReviewCommit.CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
227 4 : return !args.submitDryRun.run(
228 : null,
229 4 : changeData.submitTypeRecord().type,
230 : repo,
231 : rw,
232 4 : otherChange.getDest(),
233 4 : changeDataCache.getTestAgainst(),
234 : other,
235 4 : getAlreadyAccepted(repo, rw));
236 0 : } catch (NoSuchProjectException | IOException e) {
237 0 : warnWithOccasionalStackTrace(
238 : e,
239 : "Failure when loading conflicts of change %s in %s (%s): %s",
240 0 : lazy(changeData::getId),
241 0 : lazy(() -> firstNonNull(otherChange.getProject(), "unknown project")),
242 0 : lazy(() -> other != null ? other.name() : "unknown commit"),
243 0 : e.getMessage());
244 0 : return false;
245 : }
246 : }
247 :
248 : private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw) {
249 : try {
250 4 : Set<RevCommit> accepted = new HashSet<>();
251 4 : SubmitDryRun.addCommits(changeDataCache.getAlreadyAccepted(repo), rw, accepted);
252 4 : ObjectId tip = changeDataCache.getTestAgainst();
253 4 : if (tip != null) {
254 4 : accepted.add(rw.parseCommit(tip));
255 : }
256 4 : return accepted;
257 0 : } catch (StorageException | IOException e) {
258 0 : throw new StorageException("Failed to determine already accepted commits.", e);
259 : }
260 : }
261 : }
262 : }
|