Line data Source code
1 : // Copyright (C) 2016 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 static com.google.gerrit.server.project.ProjectCache.illegalState;
18 :
19 : import com.google.common.base.MoreObjects;
20 : import com.google.common.base.Strings;
21 : import com.google.common.collect.Iterables;
22 : import com.google.gerrit.entities.BranchNameKey;
23 : import com.google.gerrit.entities.Change;
24 : import com.google.gerrit.entities.PatchSet;
25 : import com.google.gerrit.entities.Project;
26 : import com.google.gerrit.exceptions.InvalidMergeStrategyException;
27 : import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
28 : import com.google.gerrit.extensions.client.ListChangesOption;
29 : import com.google.gerrit.extensions.common.ChangeInfo;
30 : import com.google.gerrit.extensions.common.MergeInput;
31 : import com.google.gerrit.extensions.common.MergePatchSetInput;
32 : import com.google.gerrit.extensions.restapi.AuthException;
33 : import com.google.gerrit.extensions.restapi.BadRequestException;
34 : import com.google.gerrit.extensions.restapi.MergeConflictException;
35 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
36 : import com.google.gerrit.extensions.restapi.Response;
37 : import com.google.gerrit.extensions.restapi.RestApiException;
38 : import com.google.gerrit.extensions.restapi.RestModifyView;
39 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
40 : import com.google.gerrit.server.ChangeUtil;
41 : import com.google.gerrit.server.CurrentUser;
42 : import com.google.gerrit.server.GerritPersonIdent;
43 : import com.google.gerrit.server.IdentifiedUser;
44 : import com.google.gerrit.server.PatchSetUtil;
45 : import com.google.gerrit.server.change.ChangeFinder;
46 : import com.google.gerrit.server.change.ChangeJson;
47 : import com.google.gerrit.server.change.ChangeResource;
48 : import com.google.gerrit.server.change.NotifyResolver;
49 : import com.google.gerrit.server.change.PatchSetInserter;
50 : import com.google.gerrit.server.git.CodeReviewCommit;
51 : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
52 : import com.google.gerrit.server.git.GitRepositoryManager;
53 : import com.google.gerrit.server.git.MergeUtil;
54 : import com.google.gerrit.server.git.MergeUtilFactory;
55 : import com.google.gerrit.server.notedb.ChangeNotes;
56 : import com.google.gerrit.server.permissions.ChangePermission;
57 : import com.google.gerrit.server.permissions.PermissionBackend;
58 : import com.google.gerrit.server.permissions.PermissionBackendException;
59 : import com.google.gerrit.server.permissions.RefPermission;
60 : import com.google.gerrit.server.project.ProjectCache;
61 : import com.google.gerrit.server.project.ProjectState;
62 : import com.google.gerrit.server.restapi.project.CommitsCollection;
63 : import com.google.gerrit.server.submit.MergeIdenticalTreeException;
64 : import com.google.gerrit.server.update.BatchUpdate;
65 : import com.google.gerrit.server.update.UpdateException;
66 : import com.google.gerrit.server.util.time.TimeUtil;
67 : import com.google.inject.Inject;
68 : import com.google.inject.Provider;
69 : import com.google.inject.Singleton;
70 : import java.io.IOException;
71 : import java.time.Instant;
72 : import java.time.ZoneId;
73 : import java.util.List;
74 : import org.eclipse.jgit.lib.ObjectId;
75 : import org.eclipse.jgit.lib.ObjectInserter;
76 : import org.eclipse.jgit.lib.ObjectReader;
77 : import org.eclipse.jgit.lib.PersonIdent;
78 : import org.eclipse.jgit.lib.Ref;
79 : import org.eclipse.jgit.lib.Repository;
80 : import org.eclipse.jgit.revwalk.RevCommit;
81 : import org.eclipse.jgit.util.ChangeIdUtil;
82 :
83 : @Singleton
84 : public class CreateMergePatchSet implements RestModifyView<ChangeResource, MergePatchSetInput> {
85 : private final BatchUpdate.Factory updateFactory;
86 : private final GitRepositoryManager gitManager;
87 : private final CommitsCollection commits;
88 : private final ZoneId serverZoneId;
89 : private final Provider<CurrentUser> user;
90 : private final ChangeJson.Factory jsonFactory;
91 : private final PatchSetUtil psUtil;
92 : private final MergeUtilFactory mergeUtilFactory;
93 : private final PatchSetInserter.Factory patchSetInserterFactory;
94 : private final ProjectCache projectCache;
95 : private final ChangeFinder changeFinder;
96 : private final PermissionBackend permissionBackend;
97 :
98 : @Inject
99 : CreateMergePatchSet(
100 : BatchUpdate.Factory updateFactory,
101 : GitRepositoryManager gitManager,
102 : CommitsCollection commits,
103 : @GerritPersonIdent PersonIdent myIdent,
104 : Provider<CurrentUser> user,
105 : ChangeJson.Factory json,
106 : PatchSetUtil psUtil,
107 : MergeUtilFactory mergeUtilFactory,
108 : PatchSetInserter.Factory patchSetInserterFactory,
109 : ProjectCache projectCache,
110 : ChangeFinder changeFinder,
111 145 : PermissionBackend permissionBackend) {
112 145 : this.updateFactory = updateFactory;
113 145 : this.gitManager = gitManager;
114 145 : this.commits = commits;
115 145 : this.serverZoneId = myIdent.getZoneId();
116 145 : this.user = user;
117 145 : this.jsonFactory = json;
118 145 : this.psUtil = psUtil;
119 145 : this.mergeUtilFactory = mergeUtilFactory;
120 145 : this.patchSetInserterFactory = patchSetInserterFactory;
121 145 : this.projectCache = projectCache;
122 145 : this.changeFinder = changeFinder;
123 145 : this.permissionBackend = permissionBackend;
124 145 : }
125 :
126 : @Override
127 : public Response<ChangeInfo> apply(ChangeResource rsrc, MergePatchSetInput in)
128 : throws IOException, RestApiException, UpdateException, PermissionBackendException {
129 : // Not allowed to create a new patch set if the current patch set is locked.
130 2 : psUtil.checkPatchSetNotLocked(rsrc.getNotes());
131 :
132 2 : rsrc.permissions().check(ChangePermission.ADD_PATCH_SET);
133 2 : if (in.author != null) {
134 1 : permissionBackend
135 1 : .currentUser()
136 1 : .project(rsrc.getProject())
137 1 : .ref(rsrc.getChange().getDest().branch())
138 1 : .check(RefPermission.FORGE_AUTHOR);
139 : }
140 :
141 2 : ProjectState projectState =
142 2 : projectCache.get(rsrc.getProject()).orElseThrow(illegalState(rsrc.getProject()));
143 2 : projectState.checkStatePermitsWrite();
144 :
145 2 : MergeInput merge = in.merge;
146 2 : if (merge == null || Strings.isNullOrEmpty(merge.source)) {
147 1 : throw new BadRequestException("merge.source must be non-empty");
148 : }
149 1 : if (in.author != null
150 1 : && (Strings.isNullOrEmpty(in.author.email) || Strings.isNullOrEmpty(in.author.name))) {
151 1 : throw new BadRequestException("Author must specify name and email");
152 : }
153 1 : in.baseChange = Strings.nullToEmpty(in.baseChange).trim();
154 :
155 1 : PatchSet ps = psUtil.current(rsrc.getNotes());
156 1 : Change change = rsrc.getChange();
157 1 : Project.NameKey project = change.getProject();
158 1 : BranchNameKey dest = change.getDest();
159 1 : try (Repository git = gitManager.openRepository(project);
160 1 : ObjectInserter oi = git.newObjectInserter();
161 1 : ObjectReader reader = oi.newReader();
162 1 : CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(reader)) {
163 :
164 1 : RevCommit sourceCommit = MergeUtil.resolveCommit(git, rw, merge.source);
165 1 : if (!commits.canRead(projectState, git, sourceCommit)) {
166 0 : throw new ResourceNotFoundException(
167 : "cannot find source commit: " + merge.source + " to merge.");
168 : }
169 :
170 : RevCommit currentPsCommit;
171 1 : List<String> groups = null;
172 1 : if (!in.inheritParent && !in.baseChange.isEmpty()) {
173 1 : PatchSet basePS = findBasePatchSet(in.baseChange);
174 1 : currentPsCommit = rw.parseCommit(basePS.commitId());
175 1 : groups = basePS.groups();
176 1 : } else {
177 1 : currentPsCommit = rw.parseCommit(ps.commitId());
178 : }
179 :
180 1 : Instant now = TimeUtil.now();
181 1 : IdentifiedUser me = user.get().asIdentifiedUser();
182 : PersonIdent author =
183 1 : in.author == null
184 1 : ? me.newCommitterIdent(now, serverZoneId)
185 1 : : new PersonIdent(in.author.name, in.author.email, now, serverZoneId);
186 1 : CodeReviewCommit newCommit =
187 1 : createMergeCommit(
188 : in,
189 : projectState,
190 : dest,
191 : git,
192 : oi,
193 : rw,
194 : currentPsCommit,
195 : sourceCommit,
196 : author,
197 1 : ObjectId.fromString(change.getKey().get().substring(1)));
198 1 : oi.flush();
199 :
200 1 : PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.id());
201 1 : PatchSetInserter psInserter =
202 1 : patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
203 1 : try (BatchUpdate bu = updateFactory.create(project, me, now)) {
204 1 : bu.setRepository(git, rw, oi);
205 1 : bu.setNotify(NotifyResolver.Result.none());
206 1 : psInserter
207 1 : .setMessage(messageForChange(nextPsId, newCommit))
208 1 : .setWorkInProgress(!newCommit.getFilesWithGitConflicts().isEmpty())
209 1 : .setCheckAddPatchSetPermission(false);
210 1 : if (groups != null) {
211 1 : psInserter.setGroups(groups);
212 : }
213 1 : bu.addOp(rsrc.getId(), psInserter);
214 1 : bu.execute();
215 : }
216 :
217 1 : ChangeJson json = jsonFactory.create(ListChangesOption.CURRENT_REVISION);
218 1 : ChangeInfo changeInfo = json.format(psInserter.getChange());
219 1 : changeInfo.containsGitConflicts =
220 1 : !newCommit.getFilesWithGitConflicts().isEmpty() ? true : null;
221 1 : return Response.ok(changeInfo);
222 1 : } catch (InvalidMergeStrategyException | MergeWithConflictsNotSupportedException e) {
223 1 : throw new BadRequestException(e.getMessage());
224 : }
225 : }
226 :
227 : private PatchSet findBasePatchSet(String baseChange)
228 : throws PermissionBackendException, UnprocessableEntityException {
229 1 : List<ChangeNotes> notes = changeFinder.find(baseChange);
230 1 : if (notes.size() != 1) {
231 0 : throw new UnprocessableEntityException("Base change not found: " + baseChange);
232 : }
233 1 : ChangeNotes change = Iterables.getOnlyElement(notes);
234 : try {
235 1 : permissionBackend.currentUser().change(change).check(ChangePermission.READ);
236 1 : } catch (AuthException e) {
237 1 : throw new UnprocessableEntityException("Read not permitted for " + baseChange, e);
238 1 : }
239 1 : return psUtil.current(change);
240 : }
241 :
242 : private CodeReviewCommit createMergeCommit(
243 : MergePatchSetInput in,
244 : ProjectState projectState,
245 : BranchNameKey dest,
246 : Repository git,
247 : ObjectInserter oi,
248 : CodeReviewRevWalk rw,
249 : RevCommit currentPsCommit,
250 : RevCommit sourceCommit,
251 : PersonIdent author,
252 : ObjectId changeId)
253 : throws ResourceNotFoundException, MergeIdenticalTreeException, MergeConflictException,
254 : IOException {
255 :
256 : ObjectId parentCommit;
257 1 : if (in.inheritParent) {
258 : // inherit first parent from previous patch set
259 1 : parentCommit = currentPsCommit.getParent(0);
260 1 : } else if (!in.baseChange.isEmpty()) {
261 1 : parentCommit = currentPsCommit.getId();
262 : } else {
263 : // get the current branch tip of destination branch
264 1 : Ref destRef = git.getRefDatabase().exactRef(dest.branch());
265 1 : if (destRef != null) {
266 1 : parentCommit = destRef.getObjectId();
267 : } else {
268 0 : throw new ResourceNotFoundException("cannot find destination branch");
269 : }
270 : }
271 1 : RevCommit mergeTip = rw.parseCommit(parentCommit);
272 :
273 : String commitMsg;
274 1 : if (Strings.emptyToNull(in.subject) != null) {
275 1 : commitMsg = ChangeIdUtil.insertId(in.subject, changeId);
276 : } else {
277 : // reuse previous patch set commit message
278 1 : commitMsg = currentPsCommit.getFullMessage();
279 : }
280 :
281 1 : String mergeStrategy =
282 1 : MoreObjects.firstNonNull(
283 1 : Strings.emptyToNull(in.merge.strategy),
284 1 : mergeUtilFactory.create(projectState).mergeStrategyName());
285 :
286 1 : return MergeUtil.createMergeCommit(
287 : oi,
288 1 : git.getConfig(),
289 : mergeTip,
290 : sourceCommit,
291 : mergeStrategy,
292 : in.merge.allowConflicts,
293 : author,
294 : commitMsg,
295 : rw);
296 : }
297 :
298 : private static String messageForChange(PatchSet.Id patchSetId, CodeReviewCommit commit) {
299 1 : StringBuilder stringBuilder =
300 1 : new StringBuilder(String.format("Uploaded patch set %s.", patchSetId.get()));
301 :
302 1 : if (!commit.getFilesWithGitConflicts().isEmpty()) {
303 1 : stringBuilder.append("\n\nThe following files contain Git conflicts:\n");
304 1 : commit.getFilesWithGitConflicts().stream()
305 1 : .sorted()
306 1 : .forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n"));
307 : }
308 :
309 1 : return stringBuilder.toString();
310 : }
311 : }
|