Line data Source code
1 : // Copyright (C) 2020 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.submit;
16 :
17 : import static java.util.Comparator.comparing;
18 : import static java.util.stream.Collectors.toList;
19 :
20 : import com.google.common.flogger.FluentLogger;
21 : import com.google.gerrit.common.Nullable;
22 : import com.google.gerrit.entities.BranchNameKey;
23 : import com.google.gerrit.entities.SubmoduleSubscription;
24 : import com.google.gerrit.exceptions.StorageException;
25 : import com.google.gerrit.server.GerritPersonIdent;
26 : import com.google.gerrit.server.config.GerritServerConfig;
27 : import com.google.gerrit.server.config.VerboseSuperprojectUpdate;
28 : import com.google.gerrit.server.git.CodeReviewCommit;
29 : import com.google.gerrit.server.project.NoSuchProjectException;
30 : import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
31 : import com.google.inject.Inject;
32 : import com.google.inject.Provider;
33 : import com.google.inject.Singleton;
34 : import java.io.IOException;
35 : import java.util.Collection;
36 : import java.util.Iterator;
37 : import java.util.List;
38 : import java.util.Objects;
39 : import java.util.Optional;
40 : import org.apache.commons.lang3.StringUtils;
41 : import org.eclipse.jgit.dircache.DirCache;
42 : import org.eclipse.jgit.dircache.DirCacheBuilder;
43 : import org.eclipse.jgit.dircache.DirCacheEditor;
44 : import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
45 : import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
46 : import org.eclipse.jgit.dircache.DirCacheEntry;
47 : import org.eclipse.jgit.lib.CommitBuilder;
48 : import org.eclipse.jgit.lib.Config;
49 : import org.eclipse.jgit.lib.FileMode;
50 : import org.eclipse.jgit.lib.ObjectId;
51 : import org.eclipse.jgit.lib.PersonIdent;
52 : import org.eclipse.jgit.revwalk.RevCommit;
53 : import org.eclipse.jgit.revwalk.RevWalk;
54 :
55 : /** Create commit or amend existing one updating gitlinks. */
56 : class SubmoduleCommits {
57 :
58 153 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
59 :
60 : private final PersonIdent myIdent;
61 : private final VerboseSuperprojectUpdate verboseSuperProject;
62 : private final MergeOpRepoManager orm;
63 : private final long maxCombinedCommitMessageSize;
64 : private final long maxCommitMessages;
65 69 : private final BranchTips branchTips = new BranchTips();
66 :
67 : @Singleton
68 : public static class Factory {
69 : private final Provider<PersonIdent> serverIdent;
70 : private final Config cfg;
71 :
72 : @Inject
73 142 : Factory(@GerritPersonIdent Provider<PersonIdent> serverIdent, @GerritServerConfig Config cfg) {
74 142 : this.serverIdent = serverIdent;
75 142 : this.cfg = cfg;
76 142 : }
77 :
78 : public SubmoduleCommits create(MergeOpRepoManager orm) {
79 68 : return new SubmoduleCommits(orm, serverIdent.get(), cfg);
80 : }
81 : }
82 :
83 69 : SubmoduleCommits(MergeOpRepoManager orm, PersonIdent myIdent, Config cfg) {
84 69 : this.orm = orm;
85 69 : this.myIdent = myIdent;
86 69 : this.verboseSuperProject =
87 69 : cfg.getEnum("submodule", null, "verboseSuperprojectUpdate", VerboseSuperprojectUpdate.TRUE);
88 69 : this.maxCombinedCommitMessageSize =
89 69 : cfg.getLong("submodule", "maxCombinedCommitMessageSize", 256 << 10);
90 69 : this.maxCommitMessages = cfg.getLong("submodule", "maxCommitMessages", 1000);
91 69 : }
92 :
93 : /**
94 : * Use the commit as tip of the branch
95 : *
96 : * <p>This keeps track of the tip of the branch as the submission progresses.
97 : */
98 : void addBranchTip(BranchNameKey branch, CodeReviewCommit tip) {
99 53 : branchTips.put(branch, tip);
100 53 : }
101 :
102 : /**
103 : * Create a separate gitlink commit
104 : *
105 : * @param subscriber superproject (and branch)
106 : * @param subscriptions subprojects the superproject is subscribed to
107 : * @return a new commit on top of subscriber with gitlinks update to the tips of the subprojects;
108 : * empty if nothing has changed. Subproject tips are read from the cached branched tips
109 : * (defaulting to the mergeOpRepoManager).
110 : */
111 : Optional<CodeReviewCommit> composeGitlinksCommit(
112 : BranchNameKey subscriber, Collection<SubmoduleSubscription> subscriptions)
113 : throws IOException, SubmoduleConflictException {
114 : OpenRepo or;
115 : try {
116 3 : or = orm.getRepo(subscriber.project());
117 0 : } catch (NoSuchProjectException | IOException e) {
118 0 : throw new StorageException("Cannot access superproject", e);
119 3 : }
120 :
121 3 : CodeReviewCommit currentCommit =
122 : branchTips
123 3 : .getTip(subscriber, or)
124 3 : .orElseThrow(
125 : () ->
126 0 : new SubmoduleConflictException(
127 : "The branch was probably deleted from the subscriber repository"));
128 :
129 3 : StringBuilder msgbuf = new StringBuilder();
130 3 : PersonIdent author = null;
131 3 : DirCache dc = readTree(or.getCodeReviewRevWalk(), currentCommit);
132 3 : DirCacheEditor ed = dc.editor();
133 3 : int count = 0;
134 :
135 3 : for (SubmoduleSubscription s : sortByPath(subscriptions)) {
136 3 : if (count > 0) {
137 2 : msgbuf.append("\n\n");
138 : }
139 3 : RevCommit newCommit = updateSubmodule(dc, ed, msgbuf, s);
140 3 : count++;
141 3 : if (newCommit != null) {
142 3 : PersonIdent newCommitAuthor = newCommit.getAuthorIdent();
143 3 : if (author == null) {
144 3 : author = new PersonIdent(newCommitAuthor, myIdent.getWhen());
145 2 : } else if (!author.getName().equals(newCommitAuthor.getName())
146 2 : || !author.getEmailAddress().equals(newCommitAuthor.getEmailAddress())) {
147 1 : author = myIdent;
148 : }
149 : }
150 3 : }
151 3 : ed.finish();
152 3 : ObjectId newTreeId = dc.writeTree(or.ins);
153 :
154 : // Gitlinks are already in the branch, return null
155 3 : if (newTreeId.equals(currentCommit.getTree())) {
156 2 : return Optional.empty();
157 : }
158 3 : CommitBuilder commit = new CommitBuilder();
159 3 : commit.setTreeId(newTreeId);
160 3 : commit.setParentId(currentCommit);
161 3 : StringBuilder commitMsg = new StringBuilder("Update git submodules\n\n");
162 3 : if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
163 3 : commitMsg.append(msgbuf);
164 : }
165 3 : commit.setMessage(commitMsg.toString());
166 3 : commit.setAuthor(author);
167 3 : commit.setCommitter(myIdent);
168 3 : ObjectId id = or.ins.insert(commit);
169 3 : return Optional.of(or.getCodeReviewRevWalk().parseCommit(id));
170 : }
171 :
172 : /** Amend an existing commit with gitlink updates */
173 : CodeReviewCommit amendGitlinksCommit(
174 : BranchNameKey subscriber,
175 : CodeReviewCommit currentCommit,
176 : Collection<SubmoduleSubscription> subscriptions)
177 : throws IOException, SubmoduleConflictException {
178 : OpenRepo or;
179 : try {
180 2 : or = orm.getRepo(subscriber.project());
181 0 : } catch (NoSuchProjectException | IOException e) {
182 0 : throw new StorageException("Cannot access superproject", e);
183 2 : }
184 :
185 2 : StringBuilder msgbuf = new StringBuilder();
186 2 : DirCache dc = readTree(or.rw, currentCommit);
187 2 : DirCacheEditor ed = dc.editor();
188 2 : for (SubmoduleSubscription s : sortByPath(subscriptions)) {
189 2 : updateSubmodule(dc, ed, msgbuf, s);
190 2 : }
191 2 : ed.finish();
192 2 : ObjectId newTreeId = dc.writeTree(or.ins);
193 :
194 : // Gitlinks are already updated, just return the commit
195 2 : if (newTreeId.equals(currentCommit.getTree())) {
196 1 : return currentCommit;
197 : }
198 2 : or.rw.parseBody(currentCommit);
199 2 : CommitBuilder commit = new CommitBuilder();
200 2 : commit.setTreeId(newTreeId);
201 2 : commit.setParentIds(currentCommit.getParents());
202 2 : if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
203 : // TODO(czhen): handle cherrypick footer
204 2 : commit.setMessage(currentCommit.getFullMessage() + "\n\n* submodules:\n" + msgbuf.toString());
205 : } else {
206 0 : commit.setMessage(currentCommit.getFullMessage());
207 : }
208 2 : commit.setAuthor(currentCommit.getAuthorIdent());
209 2 : commit.setCommitter(myIdent);
210 2 : ObjectId id = or.ins.insert(commit);
211 2 : CodeReviewCommit newCommit = or.getCodeReviewRevWalk().parseCommit(id);
212 2 : newCommit.copyFrom(currentCommit);
213 2 : return newCommit;
214 : }
215 :
216 : @Nullable
217 : private RevCommit updateSubmodule(
218 : DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, SubmoduleSubscription s)
219 : throws SubmoduleConflictException, IOException {
220 3 : logger.atFine().log("Updating gitlink for %s", s);
221 : OpenRepo subOr;
222 : try {
223 3 : subOr = orm.getRepo(s.getSubmodule().project());
224 0 : } catch (NoSuchProjectException | IOException e) {
225 0 : throw new StorageException("Cannot access submodule", e);
226 3 : }
227 :
228 3 : DirCacheEntry dce = dc.getEntry(s.getPath());
229 3 : RevCommit oldCommit = null;
230 3 : if (dce != null) {
231 3 : if (!dce.getFileMode().equals(FileMode.GITLINK)) {
232 0 : String errMsg =
233 : "Requested to update gitlink "
234 0 : + s.getPath()
235 : + " in "
236 0 : + s.getSubmodule().project().get()
237 : + " but entry "
238 : + "doesn't have gitlink file mode.";
239 0 : throw new SubmoduleConflictException(errMsg);
240 : }
241 : // Parse the current gitlink entry commit in the subproject repo. This is used to add a
242 : // shortlog for this submodule to the commit message in the superproject.
243 : //
244 : // Even if we don't strictly speaking need that commit message, parsing the commit is a sanity
245 : // check that the old gitlink is a commit that actually exists. If not, then there is an
246 : // inconsistency between the superproject and subproject state, and we don't want to risk
247 : // making things worse by updating the gitlink to something else.
248 : try {
249 3 : oldCommit = subOr.getCodeReviewRevWalk().parseCommit(dce.getObjectId());
250 2 : } catch (IOException e) {
251 : // Broken gitlink; sanity check failed. Warn and continue so the submit operation can
252 : // proceed, it will just skip this gitlink update.
253 2 : logger.atSevere().withCause(e).log("Failed to read commit %s", dce.getObjectId().name());
254 2 : return null;
255 3 : }
256 : }
257 :
258 3 : Optional<CodeReviewCommit> maybeNewCommit = branchTips.getTip(s.getSubmodule(), subOr);
259 3 : if (!maybeNewCommit.isPresent()) {
260 : // For whatever reason, this submodule was not updated as part of this submit batch, but the
261 : // superproject is still subscribed to this branch. Re-read the ref to see if anything has
262 : // changed since the last time the gitlink was updated, and roll that update into the same
263 : // commit as all other submodule updates.
264 0 : ed.add(new DeletePath(s.getPath()));
265 0 : return null;
266 : }
267 :
268 3 : CodeReviewCommit newCommit = maybeNewCommit.get();
269 3 : if (Objects.equals(newCommit, oldCommit)) {
270 : // gitlink have already been updated for this submodule
271 1 : return null;
272 : }
273 3 : ed.add(
274 3 : new PathEdit(s.getPath()) {
275 : @Override
276 : public void apply(DirCacheEntry ent) {
277 3 : ent.setFileMode(FileMode.GITLINK);
278 3 : ent.setObjectId(newCommit.getId());
279 3 : }
280 : });
281 :
282 3 : if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) {
283 3 : createSubmoduleCommitMsg(msgbuf, s, subOr, newCommit, oldCommit);
284 : }
285 3 : subOr.getCodeReviewRevWalk().parseBody(newCommit);
286 3 : return newCommit;
287 : }
288 :
289 : private void createSubmoduleCommitMsg(
290 : StringBuilder msgbuf,
291 : SubmoduleSubscription s,
292 : OpenRepo subOr,
293 : RevCommit newCommit,
294 : RevCommit oldCommit) {
295 3 : msgbuf.append("* Update ");
296 3 : msgbuf.append(s.getPath());
297 3 : msgbuf.append(" from branch '");
298 3 : msgbuf.append(s.getSubmodule().shortName());
299 3 : msgbuf.append("'");
300 3 : msgbuf.append("\n to ");
301 3 : msgbuf.append(newCommit.getName());
302 :
303 : // newly created submodule gitlink, do not append whole history
304 3 : if (oldCommit == null) {
305 2 : return;
306 : }
307 :
308 : try {
309 3 : subOr.rw.resetRetain(subOr.canMergeFlag);
310 3 : subOr.rw.markStart(newCommit);
311 3 : subOr.rw.markUninteresting(oldCommit);
312 3 : int numMessages = 0;
313 3 : for (Iterator<RevCommit> iter = subOr.rw.iterator(); iter.hasNext(); ) {
314 3 : RevCommit c = iter.next();
315 3 : subOr.rw.parseBody(c);
316 :
317 : String message =
318 3 : verboseSuperProject == VerboseSuperprojectUpdate.SUBJECT_ONLY
319 1 : ? c.getShortMessage()
320 3 : : StringUtils.replace(c.getFullMessage(), "\n", "\n ");
321 :
322 3 : String bullet = "\n - ";
323 3 : String ellipsis = "\n\n[...]";
324 3 : int newSize = msgbuf.length() + bullet.length() + message.length();
325 3 : if (++numMessages > maxCommitMessages
326 : || newSize > maxCombinedCommitMessageSize
327 3 : || (iter.hasNext() && (newSize + ellipsis.length()) > maxCombinedCommitMessageSize)) {
328 1 : msgbuf.append(ellipsis);
329 1 : break;
330 : }
331 3 : msgbuf.append(bullet);
332 3 : msgbuf.append(message);
333 3 : }
334 0 : } catch (IOException e) {
335 0 : throw new StorageException(
336 : "Could not perform a revwalk to create superproject commit message", e);
337 3 : }
338 3 : }
339 :
340 : private static DirCache readTree(RevWalk rw, ObjectId base) throws IOException {
341 3 : final DirCache dc = DirCache.newInCore();
342 3 : final DirCacheBuilder b = dc.builder();
343 3 : b.addTree(
344 : new byte[0], // no prefix path
345 : DirCacheEntry.STAGE_0, // standard stage
346 3 : rw.getObjectReader(),
347 3 : rw.parseTree(base));
348 3 : b.finish();
349 3 : return dc;
350 : }
351 :
352 : private static List<SubmoduleSubscription> sortByPath(
353 : Collection<SubmoduleSubscription> subscriptions) {
354 3 : return subscriptions.stream()
355 3 : .sorted(comparing(SubmoduleSubscription::getPath))
356 3 : .collect(toList());
357 : }
358 : }
|