Line data Source code
1 : // Copyright (C) 2017 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.restapi.change; 16 : 17 : import static com.google.gerrit.server.project.ProjectCache.illegalState; 18 : 19 : import com.google.gerrit.entities.BooleanProjectConfig; 20 : import com.google.gerrit.entities.PatchSet; 21 : import com.google.gerrit.extensions.api.changes.NotifyHandling; 22 : import com.google.gerrit.extensions.common.CommitMessageInput; 23 : import com.google.gerrit.extensions.restapi.AuthException; 24 : import com.google.gerrit.extensions.restapi.BadRequestException; 25 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 26 : import com.google.gerrit.extensions.restapi.Response; 27 : import com.google.gerrit.extensions.restapi.RestApiException; 28 : import com.google.gerrit.extensions.restapi.RestModifyView; 29 : import com.google.gerrit.server.ChangeUtil; 30 : import com.google.gerrit.server.CurrentUser; 31 : import com.google.gerrit.server.GerritPersonIdent; 32 : import com.google.gerrit.server.PatchSetUtil; 33 : import com.google.gerrit.server.change.ChangeResource; 34 : import com.google.gerrit.server.change.NotifyResolver; 35 : import com.google.gerrit.server.change.PatchSetInserter; 36 : import com.google.gerrit.server.git.GitRepositoryManager; 37 : import com.google.gerrit.server.notedb.ChangeNotes; 38 : import com.google.gerrit.server.permissions.ChangePermission; 39 : import com.google.gerrit.server.permissions.PermissionBackend; 40 : import com.google.gerrit.server.permissions.PermissionBackendException; 41 : import com.google.gerrit.server.project.ProjectCache; 42 : import com.google.gerrit.server.update.BatchUpdate; 43 : import com.google.gerrit.server.update.UpdateException; 44 : import com.google.gerrit.server.util.CommitMessageUtil; 45 : import com.google.gerrit.server.util.time.TimeUtil; 46 : import com.google.inject.Inject; 47 : import com.google.inject.Provider; 48 : import com.google.inject.Singleton; 49 : import java.io.IOException; 50 : import java.time.Instant; 51 : import java.time.ZoneId; 52 : import org.eclipse.jgit.errors.ConfigInvalidException; 53 : import org.eclipse.jgit.lib.CommitBuilder; 54 : import org.eclipse.jgit.lib.ObjectId; 55 : import org.eclipse.jgit.lib.ObjectInserter; 56 : import org.eclipse.jgit.lib.PersonIdent; 57 : import org.eclipse.jgit.lib.Repository; 58 : import org.eclipse.jgit.revwalk.RevCommit; 59 : import org.eclipse.jgit.revwalk.RevWalk; 60 : 61 : @Singleton 62 : public class PutMessage implements RestModifyView<ChangeResource, CommitMessageInput> { 63 : 64 : private final BatchUpdate.Factory updateFactory; 65 : private final GitRepositoryManager repositoryManager; 66 : private final Provider<CurrentUser> userProvider; 67 : private final ZoneId zoneId; 68 : private final PatchSetInserter.Factory psInserterFactory; 69 : private final PermissionBackend permissionBackend; 70 : private final PatchSetUtil psUtil; 71 : private final NotifyResolver notifyResolver; 72 : private final ProjectCache projectCache; 73 : 74 : @Inject 75 : PutMessage( 76 : BatchUpdate.Factory updateFactory, 77 : GitRepositoryManager repositoryManager, 78 : Provider<CurrentUser> userProvider, 79 : PatchSetInserter.Factory psInserterFactory, 80 : PermissionBackend permissionBackend, 81 : @GerritPersonIdent PersonIdent gerritIdent, 82 : PatchSetUtil psUtil, 83 : NotifyResolver notifyResolver, 84 145 : ProjectCache projectCache) { 85 145 : this.updateFactory = updateFactory; 86 145 : this.repositoryManager = repositoryManager; 87 145 : this.userProvider = userProvider; 88 145 : this.psInserterFactory = psInserterFactory; 89 145 : this.zoneId = gerritIdent.getZoneId(); 90 145 : this.permissionBackend = permissionBackend; 91 145 : this.psUtil = psUtil; 92 145 : this.notifyResolver = notifyResolver; 93 145 : this.projectCache = projectCache; 94 145 : } 95 : 96 : @Override 97 : public Response<String> apply(ChangeResource resource, CommitMessageInput input) 98 : throws IOException, RestApiException, UpdateException, PermissionBackendException, 99 : ConfigInvalidException { 100 4 : PatchSet ps = psUtil.current(resource.getNotes()); 101 4 : if (ps == null) { 102 0 : throw new ResourceConflictException("current revision is missing"); 103 : } 104 : 105 4 : if (input == null) { 106 0 : throw new BadRequestException("input cannot be null"); 107 : } 108 4 : String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message); 109 : 110 4 : ensureCanEditCommitMessage(resource.getNotes()); 111 4 : ChangeUtil.ensureChangeIdIsCorrect( 112 : projectCache 113 4 : .get(resource.getProject()) 114 4 : .orElseThrow(illegalState(resource.getProject())) 115 4 : .is(BooleanProjectConfig.REQUIRE_CHANGE_ID), 116 4 : resource.getChange().getKey().get(), 117 : sanitizedCommitMessage); 118 : 119 4 : try (Repository repository = repositoryManager.openRepository(resource.getProject()); 120 4 : RevWalk revWalk = new RevWalk(repository); 121 4 : ObjectInserter objectInserter = repository.newObjectInserter()) { 122 4 : RevCommit patchSetCommit = revWalk.parseCommit(ps.commitId()); 123 : 124 4 : String currentCommitMessage = patchSetCommit.getFullMessage(); 125 4 : if (input.message.equals(currentCommitMessage)) { 126 1 : throw new ResourceConflictException("new and existing commit message are the same"); 127 : } 128 : 129 4 : Instant ts = TimeUtil.now(); 130 4 : try (BatchUpdate bu = 131 4 : updateFactory.create(resource.getChange().getProject(), userProvider.get(), ts)) { 132 : // Ensure that BatchUpdate will update the same repo 133 4 : bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter); 134 : 135 4 : PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.id()); 136 4 : ObjectId newCommit = 137 4 : createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts); 138 4 : PatchSetInserter inserter = psInserterFactory.create(resource.getNotes(), psId, newCommit); 139 4 : inserter.setMessage( 140 4 : String.format("Patch Set %s: Commit message was updated.", psId.getId())); 141 4 : inserter.setDescription("Edit commit message"); 142 4 : bu.setNotify(resolveNotify(input, resource)); 143 4 : bu.addOp(resource.getChange().getId(), inserter); 144 4 : bu.execute(); 145 : } 146 : } 147 4 : return Response.ok("ok"); 148 : } 149 : 150 : private NotifyResolver.Result resolveNotify(CommitMessageInput input, ChangeResource resource) 151 : throws BadRequestException, ConfigInvalidException, IOException { 152 4 : NotifyHandling notifyHandling = input.notify; 153 4 : if (notifyHandling == null) { 154 : notifyHandling = 155 4 : resource.getChange().isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.ALL; 156 : } 157 4 : return notifyResolver.resolve(notifyHandling, input.notifyDetails); 158 : } 159 : 160 : private ObjectId createCommit( 161 : ObjectInserter objectInserter, 162 : RevCommit basePatchSetCommit, 163 : String commitMessage, 164 : Instant timestamp) 165 : throws IOException { 166 4 : CommitBuilder builder = new CommitBuilder(); 167 4 : builder.setTreeId(basePatchSetCommit.getTree()); 168 4 : builder.setParentIds(basePatchSetCommit.getParents()); 169 4 : builder.setAuthor(basePatchSetCommit.getAuthorIdent()); 170 4 : builder.setCommitter( 171 4 : userProvider.get().asIdentifiedUser().newCommitterIdent(timestamp, zoneId)); 172 4 : builder.setMessage(commitMessage); 173 4 : ObjectId newCommitId = objectInserter.insert(builder); 174 4 : objectInserter.flush(); 175 4 : return newCommitId; 176 : } 177 : 178 : private void ensureCanEditCommitMessage(ChangeNotes changeNotes) 179 : throws AuthException, PermissionBackendException, ResourceConflictException { 180 4 : if (!userProvider.get().isIdentifiedUser()) { 181 0 : throw new AuthException("Authentication required"); 182 : } 183 : 184 : // Not allowed to put message if the current patch set is locked. 185 4 : psUtil.checkPatchSetNotLocked(changeNotes); 186 : try { 187 4 : permissionBackend 188 4 : .user(userProvider.get()) 189 4 : .change(changeNotes) 190 4 : .check(ChangePermission.ADD_PATCH_SET); 191 4 : projectCache 192 4 : .get(changeNotes.getProjectName()) 193 4 : .orElseThrow(illegalState(changeNotes.getProjectName())) 194 4 : .checkStatePermitsWrite(); 195 1 : } catch (AuthException denied) { 196 1 : throw new AuthException("modifying commit message not permitted", denied); 197 4 : } 198 4 : } 199 : }