LCOV - code coverage report
Current view: top level - server/restapi/change - CreateChange.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 246 252 97.6 %
Date: 2022-11-19 15:00:39 Functions: 18 18 100.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750