Line data Source code
1 : // Copyright (C) 2013 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.common.collect.ImmutableListMultimap;
20 : import com.google.common.collect.ImmutableSet;
21 : import com.google.common.collect.Sets;
22 : import com.google.gerrit.common.Nullable;
23 : import com.google.gerrit.entities.BranchNameKey;
24 : import com.google.gerrit.entities.Change;
25 : import com.google.gerrit.entities.PatchSet;
26 : import com.google.gerrit.extensions.api.changes.RebaseInput;
27 : import com.google.gerrit.extensions.client.ListChangesOption;
28 : import com.google.gerrit.extensions.common.ChangeInfo;
29 : import com.google.gerrit.extensions.restapi.AuthException;
30 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
31 : import com.google.gerrit.extensions.restapi.Response;
32 : import com.google.gerrit.extensions.restapi.RestApiException;
33 : import com.google.gerrit.extensions.restapi.RestModifyView;
34 : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
35 : import com.google.gerrit.extensions.webui.UiAction;
36 : import com.google.gerrit.server.ChangeUtil;
37 : import com.google.gerrit.server.PatchSetUtil;
38 : import com.google.gerrit.server.change.ChangeJson;
39 : import com.google.gerrit.server.change.ChangeResource;
40 : import com.google.gerrit.server.change.NotifyResolver;
41 : import com.google.gerrit.server.change.RebaseChangeOp;
42 : import com.google.gerrit.server.change.RebaseUtil;
43 : import com.google.gerrit.server.change.RebaseUtil.Base;
44 : import com.google.gerrit.server.change.RevisionResource;
45 : import com.google.gerrit.server.git.CodeReviewCommit;
46 : import com.google.gerrit.server.git.GitRepositoryManager;
47 : import com.google.gerrit.server.permissions.ChangePermission;
48 : import com.google.gerrit.server.permissions.PermissionBackend;
49 : import com.google.gerrit.server.permissions.PermissionBackendException;
50 : import com.google.gerrit.server.project.NoSuchChangeException;
51 : import com.google.gerrit.server.project.ProjectCache;
52 : import com.google.gerrit.server.update.BatchUpdate;
53 : import com.google.gerrit.server.update.UpdateException;
54 : import com.google.gerrit.server.util.time.TimeUtil;
55 : import com.google.inject.Inject;
56 : import com.google.inject.Singleton;
57 : import java.io.IOException;
58 : import java.util.Map;
59 : import org.eclipse.jgit.lib.ObjectId;
60 : import org.eclipse.jgit.lib.ObjectInserter;
61 : import org.eclipse.jgit.lib.ObjectReader;
62 : import org.eclipse.jgit.lib.Ref;
63 : import org.eclipse.jgit.lib.Repository;
64 : import org.eclipse.jgit.revwalk.RevCommit;
65 : import org.eclipse.jgit.revwalk.RevWalk;
66 :
67 : @Singleton
68 : public class Rebase
69 : implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> {
70 145 : private static final ImmutableSet<ListChangesOption> OPTIONS =
71 145 : Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
72 :
73 : private final BatchUpdate.Factory updateFactory;
74 : private final GitRepositoryManager repoManager;
75 : private final RebaseChangeOp.Factory rebaseFactory;
76 : private final RebaseUtil rebaseUtil;
77 : private final ChangeJson.Factory json;
78 : private final PermissionBackend permissionBackend;
79 : private final ProjectCache projectCache;
80 : private final PatchSetUtil patchSetUtil;
81 :
82 : @Inject
83 : public Rebase(
84 : BatchUpdate.Factory updateFactory,
85 : GitRepositoryManager repoManager,
86 : RebaseChangeOp.Factory rebaseFactory,
87 : RebaseUtil rebaseUtil,
88 : ChangeJson.Factory json,
89 : PermissionBackend permissionBackend,
90 : ProjectCache projectCache,
91 145 : PatchSetUtil patchSetUtil) {
92 145 : this.updateFactory = updateFactory;
93 145 : this.repoManager = repoManager;
94 145 : this.rebaseFactory = rebaseFactory;
95 145 : this.rebaseUtil = rebaseUtil;
96 145 : this.json = json;
97 145 : this.permissionBackend = permissionBackend;
98 145 : this.projectCache = projectCache;
99 145 : this.patchSetUtil = patchSetUtil;
100 145 : }
101 :
102 : @Override
103 : public Response<ChangeInfo> apply(RevisionResource rsrc, RebaseInput input)
104 : throws UpdateException, RestApiException, IOException, PermissionBackendException {
105 : // Not allowed to rebase if the current patch set is locked.
106 12 : patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
107 :
108 12 : rsrc.permissions().check(ChangePermission.REBASE);
109 12 : projectCache
110 12 : .get(rsrc.getProject())
111 12 : .orElseThrow(illegalState(rsrc.getProject()))
112 12 : .checkStatePermitsWrite();
113 :
114 12 : Change change = rsrc.getChange();
115 12 : try (Repository repo = repoManager.openRepository(change.getProject());
116 12 : ObjectInserter oi = repo.newObjectInserter();
117 12 : ObjectReader reader = oi.newReader();
118 12 : RevWalk rw = CodeReviewCommit.newRevWalk(reader);
119 12 : BatchUpdate bu =
120 12 : updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
121 12 : if (!change.isNew()) {
122 2 : throw new ResourceConflictException("change is " + ChangeUtil.status(change));
123 12 : } else if (!hasOneParent(rw, rsrc.getPatchSet())) {
124 0 : throw new ResourceConflictException(
125 : "cannot rebase merge commits or commit with no ancestor");
126 : }
127 12 : RebaseChangeOp rebaseOp =
128 : rebaseFactory
129 11 : .create(rsrc.getNotes(), rsrc.getPatchSet(), findBaseRev(repo, rw, rsrc, input))
130 11 : .setForceContentMerge(true)
131 11 : .setAllowConflicts(input.allowConflicts)
132 11 : .setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions))
133 11 : .setFireRevisionCreated(true);
134 : // TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
135 11 : bu.setNotify(NotifyResolver.Result.none());
136 11 : bu.setRepository(repo, rw, oi);
137 11 : bu.addOp(change.getId(), rebaseOp);
138 11 : bu.execute();
139 :
140 11 : ChangeInfo changeInfo = json.create(OPTIONS).format(change.getProject(), change.getId());
141 11 : changeInfo.containsGitConflicts =
142 11 : !rebaseOp.getRebasedCommit().getFilesWithGitConflicts().isEmpty() ? true : null;
143 11 : return Response.ok(changeInfo);
144 : }
145 : }
146 :
147 : private ObjectId findBaseRev(
148 : Repository repo, RevWalk rw, RevisionResource rsrc, RebaseInput input)
149 : throws RestApiException, IOException, NoSuchChangeException, AuthException,
150 : PermissionBackendException {
151 12 : BranchNameKey destRefKey = rsrc.getChange().getDest();
152 12 : if (input == null || input.base == null) {
153 7 : return rebaseUtil.findBaseRevision(rsrc.getPatchSet(), destRefKey, repo, rw);
154 : }
155 :
156 5 : Change change = rsrc.getChange();
157 5 : String str = input.base.trim();
158 5 : if (str.equals("")) {
159 : // Remove existing dependency to other patch set.
160 1 : Ref destRef = repo.exactRef(destRefKey.branch());
161 1 : if (destRef == null) {
162 0 : throw new ResourceConflictException(
163 0 : "can't rebase onto tip of branch " + destRefKey.branch() + "; branch doesn't exist");
164 : }
165 1 : return destRef.getObjectId();
166 : }
167 :
168 : Base base;
169 : try {
170 5 : base = rebaseUtil.parseBase(rsrc, str);
171 5 : if (base == null) {
172 1 : throw new ResourceConflictException(
173 : "base revision is missing from the destination branch: " + str);
174 : }
175 1 : } catch (NoSuchChangeException e) {
176 1 : throw new UnprocessableEntityException(
177 1 : String.format("Base change not found: %s", input.base), e);
178 5 : }
179 :
180 5 : PatchSet.Id baseId = base.patchSet().id();
181 5 : if (change.getId().equals(baseId.changeId())) {
182 1 : throw new ResourceConflictException("cannot rebase change onto itself");
183 : }
184 :
185 5 : permissionBackend.user(rsrc.getUser()).change(base.notes()).check(ChangePermission.READ);
186 :
187 5 : Change baseChange = base.notes().getChange();
188 5 : if (!baseChange.getProject().equals(change.getProject())) {
189 0 : throw new ResourceConflictException(
190 0 : "base change is in wrong project: " + baseChange.getProject());
191 5 : } else if (!baseChange.getDest().equals(change.getDest())) {
192 1 : throw new ResourceConflictException(
193 1 : "base change is targeting wrong branch: " + baseChange.getDest());
194 5 : } else if (baseChange.isAbandoned()) {
195 1 : throw new ResourceConflictException("base change is abandoned: " + baseChange.getKey());
196 5 : } else if (isMergedInto(rw, rsrc.getPatchSet(), base.patchSet())) {
197 1 : throw new ResourceConflictException(
198 : "base change "
199 1 : + baseChange.getKey()
200 : + " is a descendant of the current change - recursion not allowed");
201 : }
202 5 : return base.patchSet().commitId();
203 : }
204 :
205 : private boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip) throws IOException {
206 5 : ObjectId baseId = base.commitId();
207 5 : ObjectId tipId = tip.commitId();
208 5 : return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
209 : }
210 :
211 : private boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
212 : // Prevent rebase of exotic changes (merge commit, no ancestor).
213 54 : RevCommit c = rw.parseCommit(ps.commitId());
214 54 : return c.getParentCount() == 1;
215 : }
216 :
217 : @Override
218 : public UiAction.Description getDescription(RevisionResource rsrc) throws IOException {
219 57 : UiAction.Description description =
220 : new UiAction.Description()
221 57 : .setLabel("Rebase")
222 57 : .setTitle(
223 : "Rebase onto tip of branch or parent change. Makes you the uploader of this "
224 : + "change which can affect validity of approvals.")
225 57 : .setVisible(false);
226 :
227 57 : Change change = rsrc.getChange();
228 57 : if (!(change.isNew() && rsrc.isCurrent())) {
229 24 : return description;
230 : }
231 52 : if (!projectCache
232 52 : .get(rsrc.getProject())
233 52 : .orElseThrow(illegalState(rsrc.getProject()))
234 52 : .statePermitsWrite()) {
235 0 : return description;
236 : }
237 52 : if (patchSetUtil.isPatchSetLocked(rsrc.getNotes())) {
238 0 : return description;
239 : }
240 :
241 52 : boolean enabled = false;
242 52 : try (Repository repo = repoManager.openRepository(change.getDest().project());
243 52 : RevWalk rw = new RevWalk(repo)) {
244 52 : if (hasOneParent(rw, rsrc.getPatchSet())) {
245 49 : enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
246 : }
247 : }
248 :
249 52 : if (rsrc.permissions().testOrFalse(ChangePermission.REBASE)) {
250 50 : return description.setVisible(true).setEnabled(enabled);
251 : }
252 14 : return description;
253 : }
254 :
255 : private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
256 : @Nullable Map<String, String> validationOptions) {
257 11 : if (validationOptions == null) {
258 11 : return ImmutableListMultimap.of();
259 : }
260 :
261 : ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
262 1 : ImmutableListMultimap.builder();
263 1 : validationOptions
264 1 : .entrySet()
265 1 : .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
266 1 : return validationOptionsBuilder.build();
267 : }
268 :
269 : public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> {
270 : private final PatchSetUtil psUtil;
271 : private final Rebase rebase;
272 :
273 : @Inject
274 91 : CurrentRevision(PatchSetUtil psUtil, Rebase rebase) {
275 91 : this.psUtil = psUtil;
276 91 : this.rebase = rebase;
277 91 : }
278 :
279 : @Override
280 : public Response<ChangeInfo> apply(ChangeResource rsrc, RebaseInput input)
281 : throws RestApiException, UpdateException, IOException, PermissionBackendException {
282 4 : PatchSet ps = psUtil.current(rsrc.getNotes());
283 4 : if (ps == null) {
284 0 : throw new ResourceConflictException("current revision is missing");
285 : }
286 4 : return Response.ok(rebase.apply(new RevisionResource(rsrc, ps), input).value());
287 : }
288 : }
289 : }
|