| // Copyright (C) 2014 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server.restapi.change; |
| |
| import static com.google.common.base.MoreObjects.firstNonNull; |
| import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION; |
| import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableListMultimap; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.UsedAt; |
| import com.google.gerrit.entities.BooleanProjectConfig; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.entities.converter.ChangeInputProtoConverter; |
| import com.google.gerrit.exceptions.InvalidMergeStrategyException; |
| import com.google.gerrit.exceptions.MergeWithConflictsNotSupportedException; |
| import com.google.gerrit.extensions.api.accounts.AccountInput; |
| import com.google.gerrit.extensions.api.changes.ApplyPatchInput; |
| import com.google.gerrit.extensions.api.changes.NotifyHandling; |
| import com.google.gerrit.extensions.client.ChangeStatus; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.client.SubmitType; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.ChangeInput; |
| import com.google.gerrit.extensions.common.MergeInput; |
| import com.google.gerrit.extensions.restapi.AuthException; |
| import com.google.gerrit.extensions.restapi.BadRequestException; |
| import com.google.gerrit.extensions.restapi.MethodNotAllowedException; |
| import com.google.gerrit.extensions.restapi.ResourceConflictException; |
| import com.google.gerrit.extensions.restapi.ResourceNotFoundException; |
| import com.google.gerrit.extensions.restapi.Response; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.extensions.restapi.RestCollectionModifyView; |
| import com.google.gerrit.extensions.restapi.TopLevelResource; |
| import com.google.gerrit.extensions.restapi.UnprocessableEntityException; |
| import com.google.gerrit.proto.Entities; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.PatchSetUtil; |
| import com.google.gerrit.server.Sequences; |
| import com.google.gerrit.server.change.ChangeFinder; |
| import com.google.gerrit.server.change.ChangeInserter; |
| import com.google.gerrit.server.change.ChangeJson; |
| import com.google.gerrit.server.change.ChangeResource; |
| import com.google.gerrit.server.change.NotifyResolver; |
| import com.google.gerrit.server.config.AnonymousCowardName; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.git.CodeReviewCommit; |
| import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; |
| import com.google.gerrit.server.git.CommitUtil; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.git.MergeUtil; |
| import com.google.gerrit.server.git.MergeUtilFactory; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.permissions.ChangePermission; |
| import com.google.gerrit.server.permissions.PermissionBackend; |
| import com.google.gerrit.server.permissions.PermissionBackendException; |
| import com.google.gerrit.server.permissions.RefPermission; |
| import com.google.gerrit.server.project.ContributorAgreementsChecker; |
| import com.google.gerrit.server.project.InvalidChangeOperationException; |
| import com.google.gerrit.server.project.ProjectResource; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.query.change.InternalChangeQuery; |
| import com.google.gerrit.server.restapi.project.CommitsCollection; |
| import com.google.gerrit.server.restapi.project.ProjectsCollection; |
| import com.google.gerrit.server.update.BatchUpdate; |
| import com.google.gerrit.server.update.UpdateException; |
| import com.google.gerrit.server.update.context.RefUpdateContext; |
| import com.google.gerrit.server.util.CommitMessageUtil; |
| import com.google.gerrit.server.util.time.TimeUtil; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.time.Instant; |
| import java.time.ZoneId; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| import org.eclipse.jgit.annotations.NonNull; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.errors.InvalidObjectIdException; |
| import org.eclipse.jgit.errors.MissingObjectException; |
| import org.eclipse.jgit.errors.NoMergeBaseException; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.ObjectReader; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.TreeFormatter; |
| import org.eclipse.jgit.patch.PatchApplier; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.eclipse.jgit.util.ChangeIdUtil; |
| |
| @Singleton |
| public class CreateChange |
| implements RestCollectionModifyView<TopLevelResource, ChangeResource, ChangeInput> { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| private static final ChangeInputProtoConverter CHANGE_INPUT_PROTO_CONVERTER = |
| ChangeInputProtoConverter.INSTANCE; |
| |
| private final BatchUpdate.Factory updateFactory; |
| private final String anonymousCowardName; |
| private final GitRepositoryManager gitManager; |
| private final Sequences seq; |
| private final ZoneId serverZoneId; |
| private final PermissionBackend permissionBackend; |
| private final Provider<CurrentUser> user; |
| private final ProjectsCollection projectsCollection; |
| private final CommitsCollection commits; |
| private final ChangeInserter.Factory changeInserterFactory; |
| private final ChangeJson.Factory jsonFactory; |
| private final ChangeFinder changeFinder; |
| private final Provider<InternalChangeQuery> queryProvider; |
| private final PatchSetUtil psUtil; |
| private final MergeUtilFactory mergeUtilFactory; |
| private final SubmitType submitType; |
| private final NotifyResolver notifyResolver; |
| private final ContributorAgreementsChecker contributorAgreements; |
| private final boolean disablePrivateChanges; |
| |
| @Inject |
| CreateChange( |
| BatchUpdate.Factory updateFactory, |
| @AnonymousCowardName String anonymousCowardName, |
| GitRepositoryManager gitManager, |
| Sequences seq, |
| @GerritPersonIdent PersonIdent myIdent, |
| PermissionBackend permissionBackend, |
| Provider<CurrentUser> user, |
| ProjectsCollection projectsCollection, |
| CommitsCollection commits, |
| ChangeInserter.Factory changeInserterFactory, |
| ChangeJson.Factory json, |
| ChangeFinder changeFinder, |
| Provider<InternalChangeQuery> queryProvider, |
| PatchSetUtil psUtil, |
| @GerritServerConfig Config config, |
| MergeUtilFactory mergeUtilFactory, |
| NotifyResolver notifyResolver, |
| ContributorAgreementsChecker contributorAgreements) { |
| this.updateFactory = updateFactory; |
| this.anonymousCowardName = anonymousCowardName; |
| this.gitManager = gitManager; |
| this.seq = seq; |
| this.serverZoneId = myIdent.getZoneId(); |
| this.permissionBackend = permissionBackend; |
| this.user = user; |
| this.projectsCollection = projectsCollection; |
| this.commits = commits; |
| this.changeInserterFactory = changeInserterFactory; |
| this.jsonFactory = json; |
| this.changeFinder = changeFinder; |
| this.queryProvider = queryProvider; |
| this.psUtil = psUtil; |
| this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY); |
| this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false); |
| this.mergeUtilFactory = mergeUtilFactory; |
| this.notifyResolver = notifyResolver; |
| this.contributorAgreements = contributorAgreements; |
| } |
| |
| @Override |
| public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInput input) |
| throws IOException, InvalidChangeOperationException, RestApiException, UpdateException, |
| PermissionBackendException, ConfigInvalidException { |
| if (Strings.isNullOrEmpty(input.project)) { |
| throw new BadRequestException("project must be non-empty"); |
| } |
| |
| return execute(updateFactory, input, projectsCollection.parse(input.project)); |
| } |
| |
| @UsedAt(UsedAt.Project.GOOGLE) |
| @FunctionalInterface |
| public interface CommitTreeSupplier { |
| @NonNull |
| ObjectId get(Repository repo, ObjectInserter oi, ChangeInput input, RevCommit mergeTip) |
| throws IOException, RestApiException; |
| } |
| |
| /** |
| * Creates the changes in the given project, using the proto representation of ChangeInput - |
| * {@link com.google.gerrit.proto.Entities.ChangeInput}. |
| */ |
| @UsedAt(UsedAt.Project.GOOGLE) |
| public Response<ChangeInfo> execute( |
| BatchUpdate.Factory updateFactory, |
| Entities.ChangeInput input, |
| CommitTreeSupplier commitTreeSupplier) |
| throws IOException, RestApiException, UpdateException, PermissionBackendException, |
| ConfigInvalidException { |
| return execute( |
| updateFactory, |
| CHANGE_INPUT_PROTO_CONVERTER.fromProto(input), |
| projectsCollection.parse(input.getProject()), |
| Optional.of(commitTreeSupplier)); |
| } |
| |
| /** |
| * Creates the changes in the given project, using the java-class representation of ChangeInput - |
| * {@link com.google.gerrit.extensions.common.ChangeInput}. This is public for reuse in the |
| * project API. |
| */ |
| public Response<ChangeInfo> execute( |
| BatchUpdate.Factory updateFactory, ChangeInput input, ProjectResource projectResource) |
| throws IOException, RestApiException, UpdateException, PermissionBackendException, |
| ConfigInvalidException { |
| return execute(updateFactory, input, projectResource, Optional.empty()); |
| } |
| |
| private Response<ChangeInfo> execute( |
| BatchUpdate.Factory updateFactory, |
| ChangeInput input, |
| ProjectResource projectResource, |
| Optional<CommitTreeSupplier> commitTreeSupplier) |
| throws IOException, RestApiException, UpdateException, PermissionBackendException, |
| ConfigInvalidException { |
| if (!user.get().isIdentifiedUser()) { |
| throw new AuthException("Authentication required"); |
| } |
| |
| ProjectState projectState = projectResource.getProjectState(); |
| projectState.checkStatePermitsWrite(); |
| |
| IdentifiedUser me = user.get().asIdentifiedUser(); |
| checkAndSanitizeChangeInput(input, me, commitTreeSupplier); |
| |
| Project.NameKey project = projectResource.getNameKey(); |
| contributorAgreements.check(project, user.get()); |
| |
| checkRequiredPermissions(project, input.branch, input.author); |
| |
| ChangeInfo newChange = |
| createNewChange(input, me, projectState, updateFactory, commitTreeSupplier); |
| return Response.created(newChange); |
| } |
| |
| /** |
| * Checks and sanitizes the user input, e.g. check whether the input is legal; clean the input so |
| * that it meets the requirement for creating a change; set a field based on the global configs, |
| * etc. |
| * |
| * @param input the {@code ChangeInput} from the request. Note this method modify the {@code |
| * ChangeInput} object so that it can be reused directly by follow-up code. |
| * @param me the user who sent the current request to create a change. |
| * @throws BadRequestException if the input is not legal. |
| */ |
| private void checkAndSanitizeChangeInput( |
| ChangeInput input, IdentifiedUser me, Optional<CommitTreeSupplier> commitTreeSupplier) |
| throws RestApiException, PermissionBackendException, IOException { |
| if (Strings.isNullOrEmpty(input.branch)) { |
| throw new BadRequestException("branch must be non-empty"); |
| } |
| input.branch = RefNames.fullName(input.branch); |
| if (!isBranchAllowed(input.branch)) { |
| throw new BadRequestException( |
| "Cannot create a change on ref " |
| + input.branch |
| + ". Gerrit internal refs and refs/tags/* are not allowed."); |
| } |
| |
| String subject = Strings.nullToEmpty(input.subject); |
| subject = subject.replaceAll("(?m)^#.*$\n?", "").trim(); |
| if (subject.isEmpty()) { |
| throw new BadRequestException("commit message must be non-empty"); |
| } |
| input.subject = subject; |
| |
| Optional<String> changeId = getChangeIdFromMessage(input.subject); |
| if (changeId.isPresent()) { |
| if (!queryProvider |
| .get() |
| .setLimit(1) |
| .byBranchKey( |
| BranchNameKey.create(input.project, input.branch), Change.key(changeId.get())) |
| .isEmpty()) { |
| throw new ResourceConflictException( |
| String.format( |
| "A change with Change-Id %s already exists for this branch.", changeId.get())); |
| } |
| } |
| |
| if (input.topic != null) { |
| input.topic = Strings.emptyToNull(input.topic.trim()); |
| } |
| |
| if (input.status != null && input.status != ChangeStatus.NEW) { |
| throw new BadRequestException("unsupported change status"); |
| } |
| |
| if (input.baseChange != null && input.baseCommit != null) { |
| throw new BadRequestException("only provide one of base_change or base_commit"); |
| } |
| |
| ProjectResource projectResource = projectsCollection.parse(input.project); |
| // Checks whether the change to be created should be a private change. |
| boolean privateByDefault = |
| projectResource.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT); |
| boolean isPrivate = input.isPrivate == null ? privateByDefault : input.isPrivate; |
| if (isPrivate && disablePrivateChanges) { |
| throw new MethodNotAllowedException("private changes are disabled"); |
| } |
| input.isPrivate = isPrivate; |
| |
| ProjectState projectState = projectResource.getProjectState(); |
| |
| if (input.workInProgress == null) { |
| if (projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)) { |
| input.workInProgress = true; |
| } else { |
| input.workInProgress = |
| firstNonNull(me.state().generalPreferences().workInProgressByDefault, false); |
| } |
| } |
| |
| if (input.merge != null) { |
| if (!(submitType.equals(SubmitType.MERGE_ALWAYS) |
| || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) { |
| throw new BadRequestException("Submit type: " + submitType + " is not supported"); |
| } |
| } |
| |
| if (input.merge != null && input.patch != null) { |
| throw new BadRequestException("Only one of `merge` and `patch` arguments can be set."); |
| } |
| |
| if ((input.merge != null || input.patch != null) && commitTreeSupplier.isPresent()) { |
| throw new BadRequestException( |
| "`CommitTreeSupplier` cannot be provided along with `merge` or `patch` arguments"); |
| } |
| |
| if (input.author != null |
| && (Strings.isNullOrEmpty(input.author.email) |
| || Strings.isNullOrEmpty(input.author.name))) { |
| throw new BadRequestException("Author must specify name and email"); |
| } |
| } |
| |
| /** Changes are allowed to be created on any ref that is not Gerrit internal or a tag ref. */ |
| private boolean isBranchAllowed(String branch) { |
| return !RefNames.isGerritRef(branch) && !branch.startsWith(RefNames.REFS_TAGS); |
| } |
| |
| private void checkRequiredPermissions( |
| Project.NameKey project, String refName, @Nullable AccountInput author) |
| throws ResourceNotFoundException, AuthException, PermissionBackendException { |
| PermissionBackend.ForRef forRef = permissionBackend.currentUser().project(project).ref(refName); |
| if (!forRef.test(RefPermission.READ)) { |
| throw new ResourceNotFoundException(String.format("ref %s not found", refName)); |
| } |
| forRef.check(RefPermission.CREATE_CHANGE); |
| if (author != null) { |
| forRef.check(RefPermission.FORGE_AUTHOR); |
| } |
| } |
| |
| private ChangeInfo createNewChange( |
| ChangeInput input, |
| IdentifiedUser me, |
| ProjectState projectState, |
| BatchUpdate.Factory updateFactory, |
| Optional<CommitTreeSupplier> commitTreeSupplier) |
| throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException, |
| UpdateException { |
| try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) { |
| logger.atFine().log( |
| "Creating new change for target branch %s in project %s" |
| + " (new branch = %s, base change = %s, base commit = %s)", |
| input.branch, |
| projectState.getName(), |
| input.newBranch, |
| input.baseChange, |
| input.baseCommit); |
| |
| try (Repository git = gitManager.openRepository(projectState.getNameKey()); |
| ObjectInserter oi = git.newObjectInserter(); |
| ObjectReader reader = oi.newReader(); |
| CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(reader)) { |
| PatchSet basePatchSet = null; |
| List<String> groups = Collections.emptyList(); |
| |
| if (input.baseChange != null) { |
| ChangeNotes baseChange = getBaseChange(input.baseChange); |
| basePatchSet = psUtil.current(baseChange); |
| groups = basePatchSet.groups(); |
| logger.atFine().log("base patch set = %s (groups = %s)", basePatchSet.id(), groups); |
| } |
| |
| ObjectId parentCommit = |
| getParentCommit( |
| git, |
| rw, |
| input.branch, |
| input.newBranch, |
| basePatchSet, |
| input.baseCommit, |
| input.merge); |
| logger.atFine().log( |
| "parent commit = %s", parentCommit != null ? parentCommit.name() : "NULL"); |
| |
| RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit); |
| |
| Instant now = TimeUtil.now(); |
| |
| PersonIdent committer = me.newCommitterIdent(now, serverZoneId); |
| PersonIdent author = |
| input.author == null |
| ? committer |
| : new PersonIdent(input.author.name, input.author.email, now, serverZoneId); |
| |
| String commitMessage = getCommitMessage(input.subject, me); |
| |
| CodeReviewCommit c; |
| if (input.merge != null) { |
| // create a merge commit |
| c = |
| newMergeCommit( |
| git, |
| oi, |
| rw, |
| projectState, |
| mergeTip, |
| input.merge, |
| author, |
| committer, |
| commitMessage); |
| if (!c.getFilesWithGitConflicts().isEmpty()) { |
| logger.atFine().log( |
| "merge commit has conflicts in the following files: %s", |
| c.getFilesWithGitConflicts()); |
| } |
| } else if (input.patch != null) { |
| // create a commit with the given patch. |
| c = |
| createCommitWithPatch( |
| git, reader, oi, rw, mergeTip, input.patch, input.subject, author, committer, me); |
| } else if (commitTreeSupplier.isPresent()) { |
| c = |
| createCommitWithSuppliedTree( |
| git, |
| oi, |
| rw, |
| mergeTip, |
| input, |
| commitTreeSupplier.get(), |
| author, |
| committer, |
| commitMessage); |
| |
| } else { |
| // create an empty commit. |
| c = createEmptyCommit(oi, rw, author, committer, mergeTip, commitMessage); |
| } |
| // Flush inserter so that commit becomes visible to validators |
| logger.atFine().log("flushing inserter %s", oi); |
| oi.flush(); |
| |
| Change.Id changeId = Change.id(seq.nextChangeId()); |
| ChangeInserter ins = changeInserterFactory.create(changeId, c, input.branch); |
| ins.setMessage(messageForNewChange(ins.getPatchSetId(), c)); |
| ins.setTopic(input.topic); |
| ins.setPrivate(input.isPrivate); |
| ins.setWorkInProgress(input.workInProgress || !c.getFilesWithGitConflicts().isEmpty()); |
| ins.setGroups(groups); |
| |
| if (input.validationOptions != null) { |
| ImmutableListMultimap.Builder<String, String> validationOptions = |
| ImmutableListMultimap.builder(); |
| input |
| .validationOptions |
| .entrySet() |
| .forEach(e -> validationOptions.put(e.getKey(), e.getValue())); |
| ins.setValidationOptions(validationOptions.build()); |
| } |
| |
| if (input.customKeyedValues != null) { |
| ImmutableMap.Builder<String, String> customKeyedValues = ImmutableMap.builder(); |
| input |
| .customKeyedValues |
| .entrySet() |
| .forEach(e -> customKeyedValues.put(e.getKey(), e.getValue())); |
| ins.setCustomKeyedValues(customKeyedValues.build()); |
| } |
| |
| try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) { |
| bu.setRepository(git, rw, oi); |
| bu.setNotify( |
| notifyResolver.resolve( |
| firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails)); |
| bu.insertChange(ins); |
| bu.execute(); |
| } |
| List<ListChangesOption> opts = input.responseFormatOptions; |
| if (opts == null) { |
| opts = ImmutableList.of(); |
| } |
| ChangeInfo changeInfo = jsonFactory.create(opts).format(ins.getChange()); |
| changeInfo.containsGitConflicts = !c.getFilesWithGitConflicts().isEmpty() ? true : null; |
| return changeInfo; |
| } catch (InvalidMergeStrategyException | MergeWithConflictsNotSupportedException e) { |
| throw new BadRequestException(e.getMessage()); |
| } |
| } |
| } |
| |
| private ChangeNotes getBaseChange(String baseChange) |
| throws UnprocessableEntityException, PermissionBackendException { |
| List<ChangeNotes> notes = changeFinder.find(baseChange); |
| if (notes.size() != 1) { |
| throw new UnprocessableEntityException("Base change not found: " + baseChange); |
| } |
| ChangeNotes change = Iterables.getOnlyElement(notes); |
| try { |
| permissionBackend.currentUser().change(change).check(ChangePermission.READ); |
| } catch (AuthException e) { |
| throw new UnprocessableEntityException("Read not permitted for " + baseChange, e); |
| } |
| |
| return change; |
| } |
| |
| @Nullable |
| private ObjectId getParentCommit( |
| Repository repo, |
| RevWalk revWalk, |
| String inputBranch, |
| @Nullable Boolean newBranch, |
| @Nullable PatchSet basePatchSet, |
| @Nullable String baseCommit, |
| @Nullable MergeInput mergeInput) |
| throws BadRequestException, IOException, UnprocessableEntityException, |
| ResourceConflictException { |
| if (basePatchSet != null) { |
| return basePatchSet.commitId(); |
| } |
| |
| Ref destRef = repo.getRefDatabase().exactRef(inputBranch); |
| ObjectId parentCommit; |
| if (baseCommit != null) { |
| try { |
| parentCommit = ObjectId.fromString(baseCommit); |
| } catch (InvalidObjectIdException e) { |
| throw new UnprocessableEntityException( |
| String.format("Base %s doesn't represent a valid SHA-1", baseCommit), e); |
| } |
| |
| RevCommit parentRevCommit; |
| try { |
| parentRevCommit = revWalk.parseCommit(parentCommit); |
| } catch (MissingObjectException e) { |
| throw new UnprocessableEntityException( |
| String.format("Base %s doesn't exist", baseCommit), e); |
| } |
| |
| if (destRef == null) { |
| throw new BadRequestException("Destination branch does not exist"); |
| } |
| RevCommit destRefRevCommit = revWalk.parseCommit(destRef.getObjectId()); |
| |
| if (!revWalk.isMergedInto(parentRevCommit, destRefRevCommit)) { |
| throw new BadRequestException( |
| String.format("Commit %s doesn't exist on ref %s", baseCommit, inputBranch)); |
| } |
| } else { |
| if (destRef != null) { |
| if (Boolean.TRUE.equals(newBranch)) { |
| throw new ResourceConflictException( |
| String.format("Branch %s already exists.", inputBranch)); |
| } |
| parentCommit = destRef.getObjectId(); |
| } else { |
| if (Boolean.TRUE.equals(newBranch)) { |
| if (mergeInput != null) { |
| throw new BadRequestException("Cannot create merge: destination branch does not exist"); |
| } |
| parentCommit = null; |
| } else { |
| throw new BadRequestException( |
| String.format("Destination branch does not exist %s", inputBranch)); |
| } |
| } |
| } |
| |
| return parentCommit; |
| } |
| |
| private Optional<String> getChangeIdFromMessage(String subject) { |
| int indexOfChangeId = ChangeIdUtil.indexOfChangeId(subject, "\n"); |
| if (indexOfChangeId == -1) { |
| return Optional.empty(); |
| } |
| return Optional.of( |
| subject.substring( |
| indexOfChangeId + 11 /* "Change-Id: "*/, |
| indexOfChangeId + 12 /* "Change-Id: I" */ + Constants.OBJECT_ID_STRING_LENGTH)); |
| } |
| |
| private String getCommitMessage(String subject, IdentifiedUser me) { |
| // Add a Change-Id line if there isn't already one |
| String commitMessage = subject; |
| if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) { |
| ObjectId id = CommitMessageUtil.generateChangeId(); |
| commitMessage = ChangeIdUtil.insertId(commitMessage, id); |
| } |
| |
| if (Boolean.TRUE.equals(me.state().generalPreferences().signedOffBy)) { |
| commitMessage = |
| Joiner.on("\n") |
| .join( |
| commitMessage.trim(), |
| String.format( |
| "%s%s", |
| SIGNED_OFF_BY_TAG, me.state().account().getNameEmail(anonymousCowardName))); |
| } |
| |
| return commitMessage; |
| } |
| |
| private static CodeReviewCommit createEmptyCommit( |
| ObjectInserter oi, |
| CodeReviewRevWalk rw, |
| PersonIdent authorIdent, |
| PersonIdent committerIdent, |
| @Nullable RevCommit mergeTip, |
| String commitMessage) |
| throws IOException { |
| logger.atFine().log("Creating empty commit (mergeTip = %s)", mergeTip); |
| ObjectId treeId = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree().getId(); |
| logger.atFine().log("Tree ID of empty commit: %s", treeId.name()); |
| List<RevCommit> parents = mergeTip == null ? ImmutableList.of() : ImmutableList.of(mergeTip); |
| return rw.parseCommit( |
| CommitUtil.createCommitWithTree( |
| oi, authorIdent, committerIdent, parents, commitMessage, treeId)); |
| } |
| |
| private CodeReviewCommit createCommitWithPatch( |
| Repository repo, |
| ObjectReader reader, |
| ObjectInserter oi, |
| CodeReviewRevWalk rw, |
| RevCommit mergeTip, |
| ApplyPatchInput patch, |
| String subject, |
| PersonIdent authorIdent, |
| PersonIdent committerIdent, |
| IdentifiedUser me) |
| throws IOException, RestApiException { |
| if (mergeTip == null) { |
| throw new BadRequestException("Cannot apply patch on top of an empty tree."); |
| } |
| PatchApplier.Result applyResult = ApplyPatchUtil.applyPatch(repo, oi, patch, mergeTip); |
| ObjectId treeId = applyResult.getTreeId(); |
| logger.atFine().log("tree ID after applying patch: %s", treeId.name()); |
| String appliedPatchCommitMessage = |
| getCommitMessage( |
| ApplyPatchUtil.buildCommitMessage( |
| subject, |
| ImmutableList.of(), |
| patch.patch, |
| ApplyPatchUtil.getResultPatch(repo, reader, mergeTip, rw.lookupTree(treeId)), |
| applyResult.getErrors()), |
| me); |
| return rw.parseCommit( |
| CommitUtil.createCommitWithTree( |
| oi, |
| authorIdent, |
| committerIdent, |
| ImmutableList.of(mergeTip), |
| appliedPatchCommitMessage, |
| treeId)); |
| } |
| |
| private static CodeReviewCommit createCommitWithSuppliedTree( |
| Repository repo, |
| ObjectInserter oi, |
| CodeReviewRevWalk rw, |
| RevCommit mergeTip, |
| ChangeInput input, |
| CommitTreeSupplier commitTreeSupplier, |
| PersonIdent authorIdent, |
| PersonIdent committerIdent, |
| String commitMessage) |
| throws IOException, RestApiException { |
| if (mergeTip == null) { |
| throw new BadRequestException("`CommitTreeSupplier` cannot be used on top of an empty tree."); |
| } |
| ObjectId treeId = commitTreeSupplier.get(repo, oi, input, mergeTip); |
| return rw.parseCommit( |
| CommitUtil.createCommitWithTree( |
| oi, authorIdent, committerIdent, ImmutableList.of(mergeTip), commitMessage, treeId)); |
| } |
| |
| private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException { |
| return inserter.insert(new TreeFormatter()); |
| } |
| |
| private CodeReviewCommit newMergeCommit( |
| Repository repo, |
| ObjectInserter oi, |
| CodeReviewRevWalk rw, |
| ProjectState projectState, |
| RevCommit mergeTip, |
| MergeInput merge, |
| PersonIdent authorIdent, |
| PersonIdent committerIdent, |
| String commitMessage) |
| throws RestApiException, IOException { |
| logger.atFine().log( |
| "Creating merge commit: source = %s, strategy = %s, allowConflicts = %s", |
| merge.source, merge.strategy, merge.allowConflicts); |
| |
| if (Strings.isNullOrEmpty(merge.source)) { |
| throw new BadRequestException("merge.source must be non-empty"); |
| } |
| |
| RevCommit sourceCommit = MergeUtil.resolveCommit(repo, rw, merge.source); |
| if (merge.sourceBranch != null) { |
| Ref ref = repo.findRef(merge.sourceBranch); |
| logger.atFine().log("checking visibility for branch %s", merge.sourceBranch); |
| if (ref == null || !commits.canRead(projectState, repo, sourceCommit, ref)) { |
| throw new BadRequestException("do not have read permission for: " + merge.source); |
| } |
| } else if (!commits.canRead(projectState, repo, sourceCommit)) { |
| throw new BadRequestException("do not have read permission for: " + merge.source); |
| } |
| |
| MergeUtil mergeUtil = mergeUtilFactory.create(projectState); |
| // default merge strategy from project settings |
| String mergeStrategy = |
| firstNonNull(Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName()); |
| logger.atFine().log("merge strategy = %s", mergeStrategy); |
| |
| try { |
| CodeReviewCommit mergeCommit = |
| MergeUtil.createMergeCommit( |
| oi, |
| repo.getConfig(), |
| mergeTip, |
| sourceCommit, |
| mergeStrategy, |
| merge.allowConflicts, |
| authorIdent, |
| committerIdent, |
| commitMessage, |
| rw); |
| logger.atFine().log("tree ID of merge commit: %s", mergeCommit.getTree().getId().name()); |
| return mergeCommit; |
| } catch (NoMergeBaseException e) { |
| throw new ResourceConflictException( |
| String.format("Cannot create merge commit: %s", e.getMessage()), e); |
| } |
| } |
| |
| private static String messageForNewChange(PatchSet.Id patchSetId, CodeReviewCommit commit) { |
| StringBuilder stringBuilder = |
| new StringBuilder(String.format("Uploaded patch set %s.", patchSetId.get())); |
| |
| if (!commit.getFilesWithGitConflicts().isEmpty()) { |
| stringBuilder.append("\n\nThe following files contain Git conflicts:\n"); |
| commit.getFilesWithGitConflicts().stream() |
| .sorted() |
| .forEach(filePath -> stringBuilder.append("* ").append(filePath).append("\n")); |
| } |
| |
| return stringBuilder.toString(); |
| } |
| } |