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.edit;
16 :
17 : import static com.google.common.base.Preconditions.checkArgument;
18 :
19 : import com.google.gerrit.entities.Change;
20 : import com.google.gerrit.entities.PatchSet;
21 : import com.google.gerrit.entities.RefNames;
22 : import com.google.gerrit.exceptions.StorageException;
23 : import com.google.gerrit.extensions.client.ChangeKind;
24 : import com.google.gerrit.extensions.restapi.AuthException;
25 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
26 : import com.google.gerrit.extensions.restapi.RestApiException;
27 : import com.google.gerrit.git.LockFailureException;
28 : import com.google.gerrit.server.ChangeUtil;
29 : import com.google.gerrit.server.CurrentUser;
30 : import com.google.gerrit.server.IdentifiedUser;
31 : import com.google.gerrit.server.PatchSetUtil;
32 : import com.google.gerrit.server.change.ChangeKindCache;
33 : import com.google.gerrit.server.change.NotifyResolver;
34 : import com.google.gerrit.server.change.PatchSetInserter;
35 : import com.google.gerrit.server.git.GitRepositoryManager;
36 : import com.google.gerrit.server.index.change.ChangeIndexer;
37 : import com.google.gerrit.server.notedb.ChangeNotes;
38 : import com.google.gerrit.server.update.BatchUpdate;
39 : import com.google.gerrit.server.update.BatchUpdateOp;
40 : import com.google.gerrit.server.update.RepoContext;
41 : import com.google.gerrit.server.update.UpdateException;
42 : import com.google.gerrit.server.util.time.TimeUtil;
43 : import com.google.inject.Inject;
44 : import com.google.inject.Provider;
45 : import com.google.inject.Singleton;
46 : import java.io.IOException;
47 : import java.util.Optional;
48 : import org.eclipse.jgit.lib.CommitBuilder;
49 : import org.eclipse.jgit.lib.ObjectId;
50 : import org.eclipse.jgit.lib.ObjectInserter;
51 : import org.eclipse.jgit.lib.ObjectReader;
52 : import org.eclipse.jgit.lib.Ref;
53 : import org.eclipse.jgit.lib.RefUpdate;
54 : import org.eclipse.jgit.lib.Repository;
55 : import org.eclipse.jgit.revwalk.RevCommit;
56 : import org.eclipse.jgit.revwalk.RevWalk;
57 :
58 : /**
59 : * Utility functions to manipulate change edits.
60 : *
61 : * <p>This class contains methods to retrieve, publish and delete edits. For changing edits see
62 : * {@link ChangeEditModifier}.
63 : */
64 : @Singleton
65 : public class ChangeEditUtil {
66 : private final GitRepositoryManager gitManager;
67 : private final PatchSetInserter.Factory patchSetInserterFactory;
68 : private final ChangeIndexer indexer;
69 : private final Provider<CurrentUser> userProvider;
70 : private final ChangeKindCache changeKindCache;
71 : private final PatchSetUtil psUtil;
72 :
73 : @Inject
74 : ChangeEditUtil(
75 : GitRepositoryManager gitManager,
76 : PatchSetInserter.Factory patchSetInserterFactory,
77 : ChangeIndexer indexer,
78 : Provider<CurrentUser> userProvider,
79 : ChangeKindCache changeKindCache,
80 145 : PatchSetUtil psUtil) {
81 145 : this.gitManager = gitManager;
82 145 : this.patchSetInserterFactory = patchSetInserterFactory;
83 145 : this.indexer = indexer;
84 145 : this.userProvider = userProvider;
85 145 : this.changeKindCache = changeKindCache;
86 145 : this.psUtil = psUtil;
87 145 : }
88 :
89 : /**
90 : * Retrieve edit for a given change.
91 : *
92 : * <p>At most one change edit can exist per user and change.
93 : *
94 : * @param notes change notes of change to retrieve change edits for.
95 : * @return edit for this change for this user, if present.
96 : * @throws AuthException if this is not a logged-in user.
97 : * @throws IOException if an error occurs.
98 : */
99 : public Optional<ChangeEdit> byChange(ChangeNotes notes) throws AuthException, IOException {
100 24 : return byChange(notes, userProvider.get());
101 : }
102 :
103 : /**
104 : * Retrieve edit for a change and the given user.
105 : *
106 : * <p>At most one change edit can exist per user and change.
107 : *
108 : * @param notes change notes of change to retrieve change edits for.
109 : * @param user user to retrieve edits as.
110 : * @return edit for this change for this user, if present.
111 : * @throws AuthException if this is not a logged-in user.
112 : * @throws IOException if an error occurs.
113 : */
114 : public Optional<ChangeEdit> byChange(ChangeNotes notes, CurrentUser user)
115 : throws AuthException, IOException {
116 28 : if (!user.isIdentifiedUser()) {
117 0 : throw new AuthException("Authentication required");
118 : }
119 28 : IdentifiedUser u = user.asIdentifiedUser();
120 28 : Change change = notes.getChange();
121 28 : try (Repository repo = gitManager.openRepository(change.getProject())) {
122 28 : int n = change.currentPatchSetId().get();
123 28 : String[] refNames = new String[n];
124 28 : for (int i = n; i > 0; i--) {
125 28 : refNames[i - 1] =
126 28 : RefNames.refsEdit(u.getAccountId(), change.getId(), PatchSet.id(change.getId(), i));
127 : }
128 28 : Ref ref = repo.getRefDatabase().firstExactRef(refNames);
129 28 : if (ref == null) {
130 28 : return Optional.empty();
131 : }
132 25 : try (RevWalk rw = new RevWalk(repo)) {
133 25 : RevCommit commit = rw.parseCommit(ref.getObjectId());
134 25 : PatchSet basePs = getBasePatchSet(notes, ref);
135 25 : return Optional.of(new ChangeEdit(change, ref.getName(), commit, basePs));
136 : }
137 28 : }
138 : }
139 :
140 : /**
141 : * Promote change edit to patch set, by squashing the edit into its parent.
142 : *
143 : * @param updateFactory factory for creating updates.
144 : * @param notes the {@code ChangeNotes} of the change to which the change edit belongs
145 : * @param user the current user
146 : * @param edit change edit to publish
147 : * @param notify Notify handling that defines to whom email notifications should be sent after the
148 : * change edit is published.
149 : */
150 : public void publish(
151 : BatchUpdate.Factory updateFactory,
152 : ChangeNotes notes,
153 : CurrentUser user,
154 : ChangeEdit edit,
155 : NotifyResolver.Result notify)
156 : throws IOException, RestApiException, UpdateException {
157 20 : Change change = edit.getChange();
158 20 : try (Repository repo = gitManager.openRepository(change.getProject());
159 20 : ObjectInserter oi = repo.newObjectInserter();
160 20 : ObjectReader reader = oi.newReader();
161 20 : RevWalk rw = new RevWalk(reader)) {
162 20 : PatchSet basePatchSet = edit.getBasePatchSet();
163 19 : if (!basePatchSet.id().equals(change.currentPatchSetId())) {
164 0 : throw new ResourceConflictException("only edit for current patch set can be published");
165 : }
166 :
167 19 : RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
168 19 : PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
169 19 : PatchSetInserter inserter =
170 : patchSetInserterFactory
171 19 : .create(notes, psId, squashed)
172 19 : .setSendEmail(!change.isWorkInProgress());
173 :
174 19 : StringBuilder message =
175 19 : new StringBuilder("Patch Set ").append(inserter.getPatchSetId().get()).append(": ");
176 :
177 : // Previously checked that the base patch set is the current patch set.
178 19 : ObjectId prior = basePatchSet.commitId();
179 19 : ChangeKind kind =
180 19 : changeKindCache.getChangeKind(change.getProject(), rw, repo.getConfig(), prior, squashed);
181 19 : if (kind == ChangeKind.NO_CODE_CHANGE) {
182 6 : message.append("Commit message was updated.");
183 6 : inserter.setDescription("Edit commit message");
184 : } else {
185 17 : message.append("Published edit on patch set ").append(basePatchSet.number()).append(".");
186 : }
187 :
188 19 : try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.now())) {
189 19 : bu.setRepository(repo, rw, oi);
190 19 : bu.setNotify(notify);
191 19 : bu.addOp(change.getId(), inserter.setMessage(message.toString()));
192 19 : bu.addOp(
193 19 : change.getId(),
194 19 : new BatchUpdateOp() {
195 : @Override
196 : public void updateRepo(RepoContext ctx) throws Exception {
197 19 : ctx.addRefUpdate(edit.getEditCommit().copy(), ObjectId.zeroId(), edit.getRefName());
198 19 : }
199 : });
200 19 : bu.execute();
201 : }
202 : }
203 19 : }
204 :
205 : /**
206 : * Delete change edit.
207 : *
208 : * @param edit change edit to delete
209 : */
210 : public void delete(ChangeEdit edit) throws IOException {
211 2 : Change change = edit.getChange();
212 2 : try (Repository repo = gitManager.openRepository(change.getProject())) {
213 2 : deleteRef(repo, edit);
214 : }
215 2 : indexer.index(change);
216 2 : }
217 :
218 : private PatchSet getBasePatchSet(ChangeNotes notes, Ref ref) throws IOException {
219 : try {
220 25 : int pos = ref.getName().lastIndexOf('/');
221 25 : checkArgument(pos > 0, "invalid edit ref: %s", ref.getName());
222 25 : String psId = ref.getName().substring(pos + 1);
223 25 : return psUtil.get(notes, PatchSet.id(notes.getChange().getId(), Integer.parseInt(psId)));
224 0 : } catch (StorageException | NumberFormatException e) {
225 0 : throw new IOException(e);
226 : }
227 : }
228 :
229 : private RevCommit squashEdit(
230 : RevWalk rw, ObjectInserter inserter, RevCommit edit, PatchSet basePatchSet)
231 : throws IOException, ResourceConflictException {
232 20 : RevCommit parent = rw.parseCommit(basePatchSet.commitId());
233 20 : if (parent.getTree().equals(edit.getTree())
234 7 : && edit.getFullMessage().equals(parent.getFullMessage())) {
235 1 : throw new ResourceConflictException("identical tree and message");
236 : }
237 19 : return writeSquashedCommit(rw, inserter, parent, edit);
238 : }
239 :
240 : private static void deleteRef(Repository repo, ChangeEdit edit) throws IOException {
241 2 : String refName = edit.getRefName();
242 2 : RefUpdate ru = repo.updateRef(refName, true);
243 2 : ru.setExpectedOldObjectId(edit.getEditCommit());
244 2 : ru.setForceUpdate(true);
245 2 : RefUpdate.Result result = ru.delete();
246 2 : switch (result) {
247 : case FORCED:
248 : case NEW:
249 : case NO_CHANGE:
250 2 : break;
251 : case LOCK_FAILURE:
252 0 : throw new LockFailureException(String.format("Failed to delete ref %s", refName), ru);
253 : case FAST_FORWARD:
254 : case IO_FAILURE:
255 : case NOT_ATTEMPTED:
256 : case REJECTED:
257 : case REJECTED_CURRENT_BRANCH:
258 : case RENAMED:
259 : case REJECTED_MISSING_OBJECT:
260 : case REJECTED_OTHER_REASON:
261 : default:
262 0 : throw new IOException(String.format("Failed to delete ref %s: %s", refName, result));
263 : }
264 2 : }
265 :
266 : private static RevCommit writeSquashedCommit(
267 : RevWalk rw, ObjectInserter inserter, RevCommit parent, RevCommit edit) throws IOException {
268 19 : CommitBuilder mergeCommit = new CommitBuilder();
269 19 : for (int i = 0; i < parent.getParentCount(); i++) {
270 18 : mergeCommit.addParentId(parent.getParent(i));
271 : }
272 19 : mergeCommit.setAuthor(parent.getAuthorIdent());
273 19 : mergeCommit.setMessage(edit.getFullMessage());
274 19 : mergeCommit.setCommitter(edit.getCommitterIdent());
275 19 : mergeCommit.setTreeId(edit.getTree());
276 :
277 19 : return rw.parseCommit(commit(inserter, mergeCommit));
278 : }
279 :
280 : private static ObjectId commit(ObjectInserter inserter, CommitBuilder mergeCommit)
281 : throws IOException {
282 19 : ObjectId id = inserter.insert(mergeCommit);
283 19 : inserter.flush();
284 19 : return id;
285 : }
286 : }
|