Line data Source code
1 : // Copyright (C) 2022 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.restapi.change;
16 :
17 : import com.google.gerrit.entities.BranchNameKey;
18 : import com.google.gerrit.entities.Change;
19 : import com.google.gerrit.entities.PatchSet;
20 : import com.google.gerrit.entities.Project.NameKey;
21 : import com.google.gerrit.extensions.api.changes.ApplyPatchPatchSetInput;
22 : import com.google.gerrit.extensions.client.ListChangesOption;
23 : import com.google.gerrit.extensions.common.ChangeInfo;
24 : import com.google.gerrit.extensions.restapi.PreconditionFailedException;
25 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
26 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
27 : import com.google.gerrit.extensions.restapi.Response;
28 : import com.google.gerrit.extensions.restapi.RestApiException;
29 : import com.google.gerrit.extensions.restapi.RestModifyView;
30 : import com.google.gerrit.server.ChangeUtil;
31 : import com.google.gerrit.server.GerritPersonIdent;
32 : import com.google.gerrit.server.IdentifiedUser;
33 : import com.google.gerrit.server.change.ChangeJson;
34 : import com.google.gerrit.server.change.ChangeResource;
35 : import com.google.gerrit.server.change.PatchSetInserter;
36 : import com.google.gerrit.server.git.CodeReviewCommit;
37 : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
38 : import com.google.gerrit.server.git.CommitUtil;
39 : import com.google.gerrit.server.git.GitRepositoryManager;
40 : import com.google.gerrit.server.notedb.ChangeNotes;
41 : import com.google.gerrit.server.permissions.PermissionBackendException;
42 : import com.google.gerrit.server.project.ContributorAgreementsChecker;
43 : import com.google.gerrit.server.project.InvalidChangeOperationException;
44 : import com.google.gerrit.server.project.NoSuchChangeException;
45 : import com.google.gerrit.server.project.NoSuchProjectException;
46 : import com.google.gerrit.server.query.change.ChangeData;
47 : import com.google.gerrit.server.query.change.InternalChangeQuery;
48 : import com.google.gerrit.server.update.BatchUpdate;
49 : import com.google.gerrit.server.update.UpdateException;
50 : import com.google.gerrit.server.util.CommitMessageUtil;
51 : import com.google.gerrit.server.util.time.TimeUtil;
52 : import com.google.inject.Inject;
53 : import com.google.inject.Provider;
54 : import com.google.inject.Singleton;
55 : import java.io.IOException;
56 : import java.time.Instant;
57 : import java.time.ZoneId;
58 : import org.eclipse.jgit.errors.ConfigInvalidException;
59 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
60 : import org.eclipse.jgit.lib.ObjectId;
61 : import org.eclipse.jgit.lib.ObjectInserter;
62 : import org.eclipse.jgit.lib.ObjectReader;
63 : import org.eclipse.jgit.lib.PersonIdent;
64 : import org.eclipse.jgit.lib.Ref;
65 : import org.eclipse.jgit.lib.Repository;
66 : import org.eclipse.jgit.revwalk.RevCommit;
67 :
68 : @Singleton
69 : public class ApplyPatch implements RestModifyView<ChangeResource, ApplyPatchPatchSetInput> {
70 : private final ChangeJson.Factory jsonFactory;
71 : private final ContributorAgreementsChecker contributorAgreements;
72 : private final Provider<IdentifiedUser> user;
73 : private final GitRepositoryManager gitManager;
74 : private final BatchUpdate.Factory batchUpdateFactory;
75 : private final PatchSetInserter.Factory patchSetInserterFactory;
76 : private final Provider<InternalChangeQuery> queryProvider;
77 : private final ZoneId serverZoneId;
78 :
79 : @Inject
80 : ApplyPatch(
81 : ChangeJson.Factory jsonFactory,
82 : ContributorAgreementsChecker contributorAgreements,
83 : Provider<IdentifiedUser> user,
84 : GitRepositoryManager gitManager,
85 : BatchUpdate.Factory batchUpdateFactory,
86 : PatchSetInserter.Factory patchSetInserterFactory,
87 : Provider<InternalChangeQuery> queryProvider,
88 145 : @GerritPersonIdent PersonIdent myIdent) {
89 145 : this.jsonFactory = jsonFactory;
90 145 : this.contributorAgreements = contributorAgreements;
91 145 : this.user = user;
92 145 : this.gitManager = gitManager;
93 145 : this.batchUpdateFactory = batchUpdateFactory;
94 145 : this.patchSetInserterFactory = patchSetInserterFactory;
95 145 : this.queryProvider = queryProvider;
96 145 : this.serverZoneId = myIdent.getZoneId();
97 145 : }
98 :
99 : @Override
100 : public Response<ChangeInfo> apply(ChangeResource rsrc, ApplyPatchPatchSetInput input)
101 : throws IOException, UpdateException, RestApiException, PermissionBackendException,
102 : ConfigInvalidException, NoSuchProjectException, InvalidChangeOperationException {
103 1 : NameKey project = rsrc.getProject();
104 1 : contributorAgreements.check(project, rsrc.getUser());
105 1 : BranchNameKey destBranch = rsrc.getChange().getDest();
106 :
107 1 : try (Repository repo = gitManager.openRepository(project);
108 : // This inserter and revwalk *must* be passed to any BatchUpdates
109 : // created later on, to ensure the applied commit is flushed
110 : // before patch sets are updated.
111 1 : ObjectInserter oi = repo.newObjectInserter();
112 1 : ObjectReader reader = oi.newReader();
113 1 : CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
114 1 : Ref destRef = repo.getRefDatabase().exactRef(destBranch.branch());
115 1 : if (destRef == null) {
116 0 : throw new ResourceNotFoundException(
117 0 : String.format("Branch %s does not exist.", destBranch.branch()));
118 : }
119 1 : ChangeData destChange = rsrc.getChangeData();
120 1 : if (destChange == null) {
121 0 : throw new PreconditionFailedException(
122 : "patch:apply cannot be called without a destination change.");
123 : }
124 :
125 1 : if (destChange.change().isClosed()) {
126 0 : throw new PreconditionFailedException(
127 0 : String.format(
128 : "patch:apply with Change-Id %s could not update the existing change %d "
129 : + "in destination branch %s of project %s, because the change was closed (%s)",
130 0 : destChange.getId(),
131 0 : destChange.getId().get(),
132 0 : destBranch.branch(),
133 0 : destBranch.project(),
134 0 : destChange.change().getStatus().name()));
135 : }
136 :
137 1 : RevCommit baseCommit =
138 1 : CommitUtil.getBaseCommit(
139 1 : project.get(), queryProvider.get(), revWalk, destRef, input.base);
140 1 : ObjectId treeId = ApplyPatchUtil.applyPatch(repo, oi, input.patch, baseCommit);
141 :
142 1 : Instant now = TimeUtil.now();
143 1 : PersonIdent committerIdent = user.get().newCommitterIdent(now, serverZoneId);
144 : PersonIdent authorIdent =
145 1 : input.author == null
146 1 : ? committerIdent
147 1 : : new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
148 : String commitMessage =
149 1 : CommitMessageUtil.checkAndSanitizeCommitMessage(
150 1 : input.commitMessage != null
151 1 : ? input.commitMessage
152 : : "The following patch was applied:\n>\t"
153 1 : + input.patch.patch.replaceAll("\n", "\n>\t"));
154 :
155 1 : ObjectId appliedCommit =
156 1 : CommitUtil.createCommitWithTree(
157 : oi, authorIdent, committerIdent, baseCommit, commitMessage, treeId);
158 1 : CodeReviewCommit commit = revWalk.parseCommit(appliedCommit);
159 1 : oi.flush();
160 :
161 : Change resultChange;
162 1 : try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
163 1 : bu.setRepository(repo, revWalk, oi);
164 1 : resultChange =
165 1 : insertPatchSet(bu, repo, patchSetInserterFactory, destChange.notes(), commit);
166 0 : } catch (NoSuchChangeException | RepositoryNotFoundException e) {
167 0 : throw new ResourceConflictException(e.getMessage());
168 1 : }
169 1 : ChangeJson json = jsonFactory.create(ListChangesOption.CURRENT_REVISION);
170 1 : ChangeInfo changeInfo = json.format(resultChange);
171 1 : return Response.ok(changeInfo);
172 : }
173 : }
174 :
175 : private static Change insertPatchSet(
176 : BatchUpdate bu,
177 : Repository git,
178 : PatchSetInserter.Factory patchSetInserterFactory,
179 : ChangeNotes destNotes,
180 : CodeReviewCommit commit)
181 : throws IOException, UpdateException, RestApiException {
182 1 : Change destChange = destNotes.getChange();
183 1 : PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
184 1 : PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, commit);
185 1 : inserter.setMessage(buildMessageForPatchSet(psId));
186 1 : bu.addOp(destChange.getId(), inserter);
187 1 : bu.execute();
188 1 : return inserter.getChange();
189 : }
190 :
191 : private static String buildMessageForPatchSet(PatchSet.Id psId) {
192 1 : return new StringBuilder(String.format("Uploaded patch set %s.", psId.get())).toString();
193 : }
194 : }
|