Line data Source code
1 : // Copyright (C) 2014 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.common.base.MoreObjects.firstNonNull;
18 : import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
19 :
20 : import com.google.common.base.Joiner;
21 : import com.google.common.base.Strings;
22 : import com.google.common.collect.ImmutableListMultimap;
23 : import com.google.common.collect.Iterables;
24 : import com.google.common.flogger.FluentLogger;
25 : import com.google.gerrit.common.Nullable;
26 : import com.google.gerrit.entities.BooleanProjectConfig;
27 : import com.google.gerrit.entities.BranchNameKey;
28 : import com.google.gerrit.entities.Change;
29 : import com.google.gerrit.entities.PatchSet;
30 : import com.google.gerrit.entities.Project;
31 : import com.google.gerrit.entities.RefNames;
32 : import com.google.gerrit.exceptions.InvalidMergeStrategyException;
33 : import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException;
34 : import com.google.gerrit.extensions.api.accounts.AccountInput;
35 : import com.google.gerrit.extensions.api.changes.NotifyHandling;
36 : import com.google.gerrit.extensions.client.ChangeStatus;
37 : import com.google.gerrit.extensions.client.SubmitType;
38 : import com.google.gerrit.extensions.common.ChangeInfo;
39 : import com.google.gerrit.extensions.common.ChangeInput;
40 : import com.google.gerrit.extensions.common.MergeInput;
41 : import com.google.gerrit.extensions.restapi.AuthException;
42 : import com.google.gerrit.extensions.restapi.BadRequestException;
43 : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
44 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
45 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
46 : import com.google.gerrit.extensions.restapi.Response;
47 : import com.google.gerrit.extensions.restapi.RestApiException;
48 : import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
49 : import com.google.gerrit.extensions.restapi.TopLevelResource;
50 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
51 : import com.google.gerrit.server.CurrentUser;
52 : import com.google.gerrit.server.GerritPersonIdent;
53 : import com.google.gerrit.server.IdentifiedUser;
54 : import com.google.gerrit.server.PatchSetUtil;
55 : import com.google.gerrit.server.change.ChangeFinder;
56 : import com.google.gerrit.server.change.ChangeInserter;
57 : import com.google.gerrit.server.change.ChangeJson;
58 : import com.google.gerrit.server.change.ChangeResource;
59 : import com.google.gerrit.server.change.NotifyResolver;
60 : import com.google.gerrit.server.config.AnonymousCowardName;
61 : import com.google.gerrit.server.config.GerritServerConfig;
62 : import com.google.gerrit.server.git.CodeReviewCommit;
63 : import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
64 : import com.google.gerrit.server.git.CommitUtil;
65 : import com.google.gerrit.server.git.GitRepositoryManager;
66 : import com.google.gerrit.server.git.MergeUtil;
67 : import com.google.gerrit.server.git.MergeUtilFactory;
68 : import com.google.gerrit.server.notedb.ChangeNotes;
69 : import com.google.gerrit.server.notedb.Sequences;
70 : import com.google.gerrit.server.permissions.ChangePermission;
71 : import com.google.gerrit.server.permissions.PermissionBackend;
72 : import com.google.gerrit.server.permissions.PermissionBackendException;
73 : import com.google.gerrit.server.permissions.RefPermission;
74 : import com.google.gerrit.server.project.ContributorAgreementsChecker;
75 : import com.google.gerrit.server.project.InvalidChangeOperationException;
76 : import com.google.gerrit.server.project.ProjectResource;
77 : import com.google.gerrit.server.project.ProjectState;
78 : import com.google.gerrit.server.query.change.InternalChangeQuery;
79 : import com.google.gerrit.server.restapi.project.CommitsCollection;
80 : import com.google.gerrit.server.restapi.project.ProjectsCollection;
81 : import com.google.gerrit.server.update.BatchUpdate;
82 : import com.google.gerrit.server.update.UpdateException;
83 : import com.google.gerrit.server.util.CommitMessageUtil;
84 : import com.google.gerrit.server.util.time.TimeUtil;
85 : import com.google.inject.Inject;
86 : import com.google.inject.Provider;
87 : import com.google.inject.Singleton;
88 : import java.io.IOException;
89 : import java.time.Instant;
90 : import java.time.ZoneId;
91 : import java.util.Collections;
92 : import java.util.List;
93 : import java.util.Optional;
94 : import org.eclipse.jgit.errors.ConfigInvalidException;
95 : import org.eclipse.jgit.errors.InvalidObjectIdException;
96 : import org.eclipse.jgit.errors.MissingObjectException;
97 : import org.eclipse.jgit.errors.NoMergeBaseException;
98 : import org.eclipse.jgit.lib.Config;
99 : import org.eclipse.jgit.lib.Constants;
100 : import org.eclipse.jgit.lib.ObjectId;
101 : import org.eclipse.jgit.lib.ObjectInserter;
102 : import org.eclipse.jgit.lib.ObjectReader;
103 : import org.eclipse.jgit.lib.PersonIdent;
104 : import org.eclipse.jgit.lib.Ref;
105 : import org.eclipse.jgit.lib.Repository;
106 : import org.eclipse.jgit.lib.TreeFormatter;
107 : import org.eclipse.jgit.revwalk.RevCommit;
108 : import org.eclipse.jgit.revwalk.RevWalk;
109 : import org.eclipse.jgit.util.ChangeIdUtil;
110 :
111 : @Singleton
112 : public class CreateChange
113 : implements RestCollectionModifyView<TopLevelResource, ChangeResource, ChangeInput> {
114 149 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
115 :
116 : private final BatchUpdate.Factory updateFactory;
117 : private final String anonymousCowardName;
118 : private final GitRepositoryManager gitManager;
119 : private final Sequences seq;
120 : private final ZoneId serverZoneId;
121 : private final PermissionBackend permissionBackend;
122 : private final Provider<CurrentUser> user;
123 : private final ProjectsCollection projectsCollection;
124 : private final CommitsCollection commits;
125 : private final ChangeInserter.Factory changeInserterFactory;
126 : private final ChangeJson.Factory jsonFactory;
127 : private final ChangeFinder changeFinder;
128 : private final Provider<InternalChangeQuery> queryProvider;
129 : private final PatchSetUtil psUtil;
130 : private final MergeUtilFactory mergeUtilFactory;
131 : private final SubmitType submitType;
132 : private final NotifyResolver notifyResolver;
133 : private final ContributorAgreementsChecker contributorAgreements;
134 : private final boolean disablePrivateChanges;
135 :
136 : @Inject
137 : CreateChange(
138 : BatchUpdate.Factory updateFactory,
139 : @AnonymousCowardName String anonymousCowardName,
140 : GitRepositoryManager gitManager,
141 : Sequences seq,
142 : @GerritPersonIdent PersonIdent myIdent,
143 : PermissionBackend permissionBackend,
144 : Provider<CurrentUser> user,
145 : ProjectsCollection projectsCollection,
146 : CommitsCollection commits,
147 : ChangeInserter.Factory changeInserterFactory,
148 : ChangeJson.Factory json,
149 : ChangeFinder changeFinder,
150 : Provider<InternalChangeQuery> queryProvider,
151 : PatchSetUtil psUtil,
152 : @GerritServerConfig Config config,
153 : MergeUtilFactory mergeUtilFactory,
154 : NotifyResolver notifyResolver,
155 149 : ContributorAgreementsChecker contributorAgreements) {
156 149 : this.updateFactory = updateFactory;
157 149 : this.anonymousCowardName = anonymousCowardName;
158 149 : this.gitManager = gitManager;
159 149 : this.seq = seq;
160 149 : this.serverZoneId = myIdent.getZoneId();
161 149 : this.permissionBackend = permissionBackend;
162 149 : this.user = user;
163 149 : this.projectsCollection = projectsCollection;
164 149 : this.commits = commits;
165 149 : this.changeInserterFactory = changeInserterFactory;
166 149 : this.jsonFactory = json;
167 149 : this.changeFinder = changeFinder;
168 149 : this.queryProvider = queryProvider;
169 149 : this.psUtil = psUtil;
170 149 : this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
171 149 : this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
172 149 : this.mergeUtilFactory = mergeUtilFactory;
173 149 : this.notifyResolver = notifyResolver;
174 149 : this.contributorAgreements = contributorAgreements;
175 149 : }
176 :
177 : @Override
178 : public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInput input)
179 : throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
180 : PermissionBackendException, ConfigInvalidException {
181 31 : if (Strings.isNullOrEmpty(input.project)) {
182 1 : throw new BadRequestException("project must be non-empty");
183 : }
184 :
185 31 : return execute(updateFactory, input, projectsCollection.parse(input.project));
186 : }
187 :
188 : /** Creates the changes in the given project. This is public for reuse in the project API. */
189 : public Response<ChangeInfo> execute(
190 : BatchUpdate.Factory updateFactory, ChangeInput input, ProjectResource projectResource)
191 : throws IOException, RestApiException, UpdateException, PermissionBackendException,
192 : ConfigInvalidException {
193 32 : if (!user.get().isIdentifiedUser()) {
194 1 : throw new AuthException("Authentication required");
195 : }
196 :
197 32 : ProjectState projectState = projectResource.getProjectState();
198 32 : projectState.checkStatePermitsWrite();
199 :
200 32 : IdentifiedUser me = user.get().asIdentifiedUser();
201 32 : checkAndSanitizeChangeInput(input, me);
202 :
203 32 : Project.NameKey project = projectResource.getNameKey();
204 32 : contributorAgreements.check(project, user.get());
205 :
206 32 : checkRequiredPermissions(project, input.branch, input.author);
207 :
208 32 : ChangeInfo newChange = createNewChange(input, me, projectState, updateFactory);
209 32 : return Response.created(newChange);
210 : }
211 :
212 : /**
213 : * Checks and sanitizes the user input, e.g. check whether the input is legal; clean the input so
214 : * that it meets the requirement for creating a change; set a field based on the global configs,
215 : * etc.
216 : *
217 : * @param input the {@code ChangeInput} from the request. Note this method modify the {@code
218 : * ChangeInput} object so that it can be reused directly by follow-up code.
219 : * @param me the user who sent the current request to create a change.
220 : * @throws BadRequestException if the input is not legal.
221 : */
222 : private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
223 : throws RestApiException, PermissionBackendException, IOException {
224 32 : if (Strings.isNullOrEmpty(input.branch)) {
225 2 : throw new BadRequestException("branch must be non-empty");
226 : }
227 32 : input.branch = RefNames.fullName(input.branch);
228 32 : if (!isBranchAllowed(input.branch)) {
229 1 : throw new BadRequestException(
230 : "Cannot create a change on ref "
231 : + input.branch
232 : + ". Gerrit internal refs and refs/tags/* are not allowed.");
233 : }
234 :
235 32 : String subject = Strings.nullToEmpty(input.subject);
236 32 : subject = subject.replaceAll("(?m)^#.*$\n?", "").trim();
237 32 : if (subject.isEmpty()) {
238 1 : throw new BadRequestException("commit message must be non-empty");
239 : }
240 32 : input.subject = subject;
241 :
242 32 : Optional<String> changeId = getChangeIdFromMessage(input.subject);
243 32 : if (changeId.isPresent()) {
244 1 : if (!queryProvider
245 1 : .get()
246 1 : .setLimit(1)
247 1 : .byBranchKey(
248 1 : BranchNameKey.create(input.project, input.branch), Change.key(changeId.get()))
249 1 : .isEmpty()) {
250 1 : throw new ResourceConflictException(
251 1 : String.format(
252 1 : "A change with Change-Id %s already exists for this branch.", changeId.get()));
253 : }
254 : }
255 :
256 32 : if (input.topic != null) {
257 1 : input.topic = Strings.emptyToNull(input.topic.trim());
258 : }
259 :
260 32 : if (input.status != null && input.status != ChangeStatus.NEW) {
261 1 : throw new BadRequestException("unsupported change status");
262 : }
263 :
264 32 : if (input.baseChange != null && input.baseCommit != null) {
265 0 : throw new BadRequestException("only provide one of base_change or base_commit");
266 : }
267 :
268 32 : ProjectResource projectResource = projectsCollection.parse(input.project);
269 : // Checks whether the change to be created should be a private change.
270 32 : boolean privateByDefault =
271 32 : projectResource.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
272 32 : boolean isPrivate = input.isPrivate == null ? privateByDefault : input.isPrivate;
273 32 : if (isPrivate && disablePrivateChanges) {
274 2 : throw new MethodNotAllowedException("private changes are disabled");
275 : }
276 32 : input.isPrivate = isPrivate;
277 :
278 32 : ProjectState projectState = projectResource.getProjectState();
279 :
280 32 : if (input.workInProgress == null) {
281 32 : if (projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)) {
282 2 : input.workInProgress = true;
283 : } else {
284 32 : input.workInProgress =
285 32 : firstNonNull(me.state().generalPreferences().workInProgressByDefault, false);
286 : }
287 : }
288 :
289 32 : if (input.merge != null) {
290 2 : if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
291 2 : || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
292 0 : throw new BadRequestException("Submit type: " + submitType + " is not supported");
293 : }
294 : }
295 :
296 32 : if (input.merge != null && input.patch != null) {
297 1 : throw new BadRequestException("Only one of `merge` and `patch` arguments can be set.");
298 : }
299 :
300 32 : if (input.author != null
301 1 : && (Strings.isNullOrEmpty(input.author.email)
302 1 : || Strings.isNullOrEmpty(input.author.name))) {
303 1 : throw new BadRequestException("Author must specify name and email");
304 : }
305 32 : }
306 :
307 : /** Changes are allowed to be created on any ref that is not Gerrit internal or a tag ref. */
308 : private boolean isBranchAllowed(String branch) {
309 32 : return !RefNames.isGerritRef(branch) && !branch.startsWith(RefNames.REFS_TAGS);
310 : }
311 :
312 : private void checkRequiredPermissions(
313 : Project.NameKey project, String refName, @Nullable AccountInput author)
314 : throws ResourceNotFoundException, AuthException, PermissionBackendException {
315 32 : PermissionBackend.ForRef forRef = permissionBackend.currentUser().project(project).ref(refName);
316 32 : if (!forRef.test(RefPermission.READ)) {
317 1 : throw new ResourceNotFoundException(String.format("ref %s not found", refName));
318 : }
319 32 : forRef.check(RefPermission.CREATE_CHANGE);
320 32 : if (author != null) {
321 1 : forRef.check(RefPermission.FORGE_AUTHOR);
322 : }
323 32 : }
324 :
325 : private ChangeInfo createNewChange(
326 : ChangeInput input,
327 : IdentifiedUser me,
328 : ProjectState projectState,
329 : BatchUpdate.Factory updateFactory)
330 : throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
331 : UpdateException {
332 32 : logger.atFine().log(
333 : "Creating new change for target branch %s in project %s"
334 : + " (new branch = %s, base change = %s, base commit = %s)",
335 32 : input.branch, projectState.getName(), input.newBranch, input.baseChange, input.baseCommit);
336 :
337 32 : try (Repository git = gitManager.openRepository(projectState.getNameKey());
338 32 : ObjectInserter oi = git.newObjectInserter();
339 32 : ObjectReader reader = oi.newReader();
340 32 : CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(reader)) {
341 32 : PatchSet basePatchSet = null;
342 32 : List<String> groups = Collections.emptyList();
343 :
344 32 : if (input.baseChange != null) {
345 1 : ChangeNotes baseChange = getBaseChange(input.baseChange);
346 1 : basePatchSet = psUtil.current(baseChange);
347 1 : groups = basePatchSet.groups();
348 1 : logger.atFine().log("base patch set = %s (groups = %s)", basePatchSet.id(), groups);
349 : }
350 :
351 32 : ObjectId parentCommit =
352 32 : getParentCommit(
353 : git, rw, input.branch, input.newBranch, basePatchSet, input.baseCommit, input.merge);
354 32 : logger.atFine().log(
355 32 : "parent commit = %s", parentCommit != null ? parentCommit.name() : "NULL");
356 :
357 32 : RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit);
358 :
359 32 : Instant now = TimeUtil.now();
360 :
361 32 : PersonIdent committer = me.newCommitterIdent(now, serverZoneId);
362 : PersonIdent author =
363 32 : input.author == null
364 32 : ? committer
365 32 : : new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
366 :
367 32 : String commitMessage = getCommitMessage(input.subject, me);
368 :
369 : CodeReviewCommit c;
370 32 : if (input.merge != null) {
371 : // create a merge commit
372 2 : c =
373 2 : newMergeCommit(
374 : git, oi, rw, projectState, mergeTip, input.merge, author, committer, commitMessage);
375 2 : if (!c.getFilesWithGitConflicts().isEmpty()) {
376 1 : logger.atFine().log(
377 : "merge commit has conflicts in the following files: %s",
378 1 : c.getFilesWithGitConflicts());
379 : }
380 31 : } else if (input.patch != null) {
381 : // create a commit with the given patch.
382 1 : if (mergeTip == null) {
383 1 : throw new BadRequestException("Cannot apply patch on top of an empty tree.");
384 : }
385 1 : ObjectId treeId = ApplyPatchUtil.applyPatch(git, oi, input.patch, mergeTip);
386 1 : c =
387 1 : rw.parseCommit(
388 1 : CommitUtil.createCommitWithTree(
389 : oi, author, committer, mergeTip, commitMessage, treeId));
390 1 : } else {
391 : // create an empty commit.
392 31 : c = createEmptyCommit(oi, rw, author, committer, mergeTip, commitMessage);
393 : }
394 : // Flush inserter so that commit becomes visible to validators
395 32 : oi.flush();
396 :
397 32 : Change.Id changeId = Change.id(seq.nextChangeId());
398 32 : ChangeInserter ins = changeInserterFactory.create(changeId, c, input.branch);
399 32 : ins.setMessage(messageForNewChange(ins.getPatchSetId(), c));
400 32 : ins.setTopic(input.topic);
401 32 : ins.setPrivate(input.isPrivate);
402 32 : ins.setWorkInProgress(input.workInProgress || !c.getFilesWithGitConflicts().isEmpty());
403 32 : ins.setGroups(groups);
404 :
405 32 : if (input.validationOptions != null) {
406 : ImmutableListMultimap.Builder<String, String> validationOptions =
407 1 : ImmutableListMultimap.builder();
408 1 : input
409 : .validationOptions
410 1 : .entrySet()
411 1 : .forEach(e -> validationOptions.put(e.getKey(), e.getValue()));
412 1 : ins.setValidationOptions(validationOptions.build());
413 : }
414 :
415 32 : try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
416 32 : bu.setRepository(git, rw, oi);
417 32 : bu.setNotify(
418 32 : notifyResolver.resolve(
419 32 : firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
420 32 : bu.insertChange(ins);
421 32 : bu.execute();
422 : }
423 32 : ChangeInfo changeInfo = jsonFactory.noOptions().format(ins.getChange());
424 32 : changeInfo.containsGitConflicts = !c.getFilesWithGitConflicts().isEmpty() ? true : null;
425 32 : return changeInfo;
426 1 : } catch (InvalidMergeStrategyException | MergeWithConflictsNotSupportedException e) {
427 1 : throw new BadRequestException(e.getMessage());
428 : }
429 : }
430 :
431 : private ChangeNotes getBaseChange(String baseChange)
432 : throws UnprocessableEntityException, PermissionBackendException {
433 1 : List<ChangeNotes> notes = changeFinder.find(baseChange);
434 1 : if (notes.size() != 1) {
435 1 : throw new UnprocessableEntityException("Base change not found: " + baseChange);
436 : }
437 1 : ChangeNotes change = Iterables.getOnlyElement(notes);
438 : try {
439 1 : permissionBackend.currentUser().change(change).check(ChangePermission.READ);
440 0 : } catch (AuthException e) {
441 0 : throw new UnprocessableEntityException("Read not permitted for " + baseChange, e);
442 1 : }
443 :
444 1 : return change;
445 : }
446 :
447 : @Nullable
448 : private ObjectId getParentCommit(
449 : Repository repo,
450 : RevWalk revWalk,
451 : String inputBranch,
452 : @Nullable Boolean newBranch,
453 : @Nullable PatchSet basePatchSet,
454 : @Nullable String baseCommit,
455 : @Nullable MergeInput mergeInput)
456 : throws BadRequestException, IOException, UnprocessableEntityException,
457 : ResourceConflictException {
458 32 : if (basePatchSet != null) {
459 1 : return basePatchSet.commitId();
460 : }
461 :
462 32 : Ref destRef = repo.getRefDatabase().exactRef(inputBranch);
463 : ObjectId parentCommit;
464 32 : if (baseCommit != null) {
465 : try {
466 1 : parentCommit = ObjectId.fromString(baseCommit);
467 1 : } catch (InvalidObjectIdException e) {
468 1 : throw new UnprocessableEntityException(
469 1 : String.format("Base %s doesn't represent a valid SHA-1", baseCommit), e);
470 1 : }
471 :
472 : RevCommit parentRevCommit;
473 : try {
474 1 : parentRevCommit = revWalk.parseCommit(parentCommit);
475 1 : } catch (MissingObjectException e) {
476 1 : throw new UnprocessableEntityException(
477 1 : String.format("Base %s doesn't exist", baseCommit), e);
478 1 : }
479 :
480 1 : if (destRef == null) {
481 1 : throw new BadRequestException("Destination branch does not exist");
482 : }
483 1 : RevCommit destRefRevCommit = revWalk.parseCommit(destRef.getObjectId());
484 :
485 1 : if (!revWalk.isMergedInto(parentRevCommit, destRefRevCommit)) {
486 1 : throw new BadRequestException(
487 1 : String.format("Commit %s doesn't exist on ref %s", baseCommit, inputBranch));
488 : }
489 1 : } else {
490 32 : if (destRef != null) {
491 27 : if (Boolean.TRUE.equals(newBranch)) {
492 1 : throw new ResourceConflictException(
493 1 : String.format("Branch %s already exists.", inputBranch));
494 : }
495 27 : parentCommit = destRef.getObjectId();
496 : } else {
497 7 : if (Boolean.TRUE.equals(newBranch)) {
498 7 : if (mergeInput != null) {
499 1 : throw new BadRequestException("Cannot create merge: destination branch does not exist");
500 : }
501 7 : parentCommit = null;
502 : } else {
503 1 : throw new BadRequestException("Destination branch does not exist");
504 : }
505 : }
506 : }
507 :
508 32 : return parentCommit;
509 : }
510 :
511 : private Optional<String> getChangeIdFromMessage(String subject) {
512 32 : int indexOfChangeId = ChangeIdUtil.indexOfChangeId(subject, "\n");
513 32 : if (indexOfChangeId == -1) {
514 32 : return Optional.empty();
515 : }
516 1 : return Optional.of(
517 1 : subject.substring(
518 : indexOfChangeId + 11 /* "Change-Id: "*/,
519 : indexOfChangeId + 12 /* "Change-Id: I" */ + Constants.OBJECT_ID_STRING_LENGTH));
520 : }
521 :
522 : private String getCommitMessage(String subject, IdentifiedUser me) {
523 : // Add a Change-Id line if there isn't already one
524 32 : String commitMessage = subject;
525 32 : if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
526 32 : ObjectId id = CommitMessageUtil.generateChangeId();
527 32 : commitMessage = ChangeIdUtil.insertId(commitMessage, id);
528 : }
529 :
530 32 : if (Boolean.TRUE.equals(me.state().generalPreferences().signedOffBy)) {
531 1 : commitMessage =
532 1 : Joiner.on("\n")
533 1 : .join(
534 1 : commitMessage.trim(),
535 1 : String.format(
536 : "%s%s",
537 1 : SIGNED_OFF_BY_TAG, me.state().account().getNameEmail(anonymousCowardName)));
538 : }
539 :
540 32 : return commitMessage;
541 : }
542 :
543 : private static CodeReviewCommit createEmptyCommit(
544 : ObjectInserter oi,
545 : CodeReviewRevWalk rw,
546 : PersonIdent authorIdent,
547 : PersonIdent committerIdent,
548 : RevCommit mergeTip,
549 : String commitMessage)
550 : throws IOException {
551 31 : logger.atFine().log("Creating empty commit");
552 31 : ObjectId treeID = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree().getId();
553 31 : return rw.parseCommit(
554 31 : CommitUtil.createCommitWithTree(
555 : oi, authorIdent, committerIdent, mergeTip, commitMessage, treeID));
556 : }
557 :
558 : private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
559 7 : return inserter.insert(new TreeFormatter());
560 : }
561 :
562 : private CodeReviewCommit newMergeCommit(
563 : Repository repo,
564 : ObjectInserter oi,
565 : CodeReviewRevWalk rw,
566 : ProjectState projectState,
567 : RevCommit mergeTip,
568 : MergeInput merge,
569 : PersonIdent authorIdent,
570 : PersonIdent committerIdent,
571 : String commitMessage)
572 : throws RestApiException, IOException {
573 2 : logger.atFine().log(
574 : "Creating merge commit: source = %s, strategy = %s, allowConflicts = %s",
575 2 : merge.source, merge.strategy, merge.allowConflicts);
576 :
577 2 : if (Strings.isNullOrEmpty(merge.source)) {
578 0 : throw new BadRequestException("merge.source must be non-empty");
579 : }
580 :
581 2 : RevCommit sourceCommit = MergeUtil.resolveCommit(repo, rw, merge.source);
582 2 : if (merge.sourceBranch != null) {
583 1 : Ref ref = repo.findRef(merge.sourceBranch);
584 1 : logger.atFine().log("checking visibility for branch %s", merge.sourceBranch);
585 1 : if (ref == null || !commits.canRead(projectState, repo, sourceCommit, ref)) {
586 1 : throw new BadRequestException("do not have read permission for: " + merge.source);
587 : }
588 2 : } else if (!commits.canRead(projectState, repo, sourceCommit)) {
589 0 : throw new BadRequestException("do not have read permission for: " + merge.source);
590 : }
591 :
592 2 : MergeUtil mergeUtil = mergeUtilFactory.create(projectState);
593 : // default merge strategy from project settings
594 2 : String mergeStrategy =
595 2 : firstNonNull(Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName());
596 2 : logger.atFine().log("merge strategy = %s", mergeStrategy);
597 :
598 : try {
599 2 : return MergeUtil.createMergeCommit(
600 : oi,
601 2 : repo.getConfig(),
602 : mergeTip,
603 : sourceCommit,
604 : mergeStrategy,
605 : merge.allowConflicts,
606 : authorIdent,
607 : committerIdent,
608 : commitMessage,
609 : rw);
610 1 : } catch (NoMergeBaseException e) {
611 1 : throw new ResourceConflictException(
612 1 : String.format("Cannot create merge commit: %s", e.getMessage()), e);
613 : }
614 : }
615 :
616 : private static String messageForNewChange(PatchSet.Id patchSetId, CodeReviewCommit commit) {
617 32 : StringBuilder stringBuilder =
618 32 : new StringBuilder(String.format("Uploaded patch set %s.", patchSetId.get()));
619 :
620 32 : if (!commit.getFilesWithGitConflicts().isEmpty()) {
621 1 : stringBuilder.append("\n\nThe following files contain Git conflicts:\n");
622 1 : commit.getFilesWithGitConflicts().stream()
623 1 : .sorted()
624 1 : .forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n"));
625 : }
626 :
627 32 : return stringBuilder.toString();
628 : }
629 : }
|