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.gerrit.server.project.ProjectCache.illegalState;
18 : import static java.nio.charset.StandardCharsets.UTF_8;
19 :
20 : import com.google.common.base.Strings;
21 : import com.google.common.collect.ImmutableList;
22 : import com.google.common.io.ByteStreams;
23 : import com.google.gerrit.common.RawInputUtil;
24 : import com.google.gerrit.entities.Change;
25 : import com.google.gerrit.entities.Patch;
26 : import com.google.gerrit.entities.PatchSet;
27 : import com.google.gerrit.entities.Project;
28 : import com.google.gerrit.extensions.api.changes.FileContentInput;
29 : import com.google.gerrit.extensions.common.DiffWebLinkInfo;
30 : import com.google.gerrit.extensions.common.EditInfo;
31 : import com.google.gerrit.extensions.common.Input;
32 : import com.google.gerrit.extensions.registration.DynamicMap;
33 : import com.google.gerrit.extensions.restapi.AuthException;
34 : import com.google.gerrit.extensions.restapi.BadRequestException;
35 : import com.google.gerrit.extensions.restapi.BinaryResult;
36 : import com.google.gerrit.extensions.restapi.ChildCollection;
37 : import com.google.gerrit.extensions.restapi.DefaultInput;
38 : import com.google.gerrit.extensions.restapi.IdString;
39 : import com.google.gerrit.extensions.restapi.RawInput;
40 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
41 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
42 : import com.google.gerrit.extensions.restapi.Response;
43 : import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
44 : import com.google.gerrit.extensions.restapi.RestCollectionDeleteMissingView;
45 : import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
46 : import com.google.gerrit.extensions.restapi.RestModifyView;
47 : import com.google.gerrit.extensions.restapi.RestReadView;
48 : import com.google.gerrit.extensions.restapi.RestView;
49 : import com.google.gerrit.server.WebLinks;
50 : import com.google.gerrit.server.change.ChangeEditResource;
51 : import com.google.gerrit.server.change.ChangeResource;
52 : import com.google.gerrit.server.change.FileContentUtil;
53 : import com.google.gerrit.server.change.FileInfoJson;
54 : import com.google.gerrit.server.change.RevisionResource;
55 : import com.google.gerrit.server.edit.ChangeEdit;
56 : import com.google.gerrit.server.edit.ChangeEditJson;
57 : import com.google.gerrit.server.edit.ChangeEditModifier;
58 : import com.google.gerrit.server.edit.ChangeEditUtil;
59 : import com.google.gerrit.server.git.GitRepositoryManager;
60 : import com.google.gerrit.server.patch.PatchListNotAvailableException;
61 : import com.google.gerrit.server.permissions.PermissionBackendException;
62 : import com.google.gerrit.server.project.InvalidChangeOperationException;
63 : import com.google.gerrit.server.project.ProjectCache;
64 : import com.google.inject.Inject;
65 : import com.google.inject.Provider;
66 : import com.google.inject.Singleton;
67 : import java.io.IOException;
68 : import java.util.List;
69 : import java.util.Optional;
70 : import java.util.regex.Matcher;
71 : import java.util.regex.Pattern;
72 : import org.eclipse.jgit.lib.Repository;
73 : import org.eclipse.jgit.revwalk.RevCommit;
74 : import org.eclipse.jgit.revwalk.RevWalk;
75 : import org.eclipse.jgit.util.Base64;
76 : import org.kohsuke.args4j.Option;
77 :
78 : @Singleton
79 : public class ChangeEdits implements ChildCollection<ChangeResource, ChangeEditResource> {
80 : private final DynamicMap<RestView<ChangeEditResource>> views;
81 : private final Provider<Detail> detail;
82 : private final ChangeEditUtil editUtil;
83 :
84 : @Inject
85 : ChangeEdits(
86 : DynamicMap<RestView<ChangeEditResource>> views,
87 : Provider<Detail> detail,
88 145 : ChangeEditUtil editUtil) {
89 145 : this.views = views;
90 145 : this.detail = detail;
91 145 : this.editUtil = editUtil;
92 145 : }
93 :
94 : @Override
95 : public DynamicMap<RestView<ChangeEditResource>> views() {
96 2 : return views;
97 : }
98 :
99 : @Override
100 : public RestView<ChangeResource> list() {
101 2 : return detail.get();
102 : }
103 :
104 : @Override
105 : public ChangeEditResource parse(ChangeResource rsrc, IdString id)
106 : throws ResourceNotFoundException, AuthException, IOException {
107 4 : Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
108 4 : if (!edit.isPresent()) {
109 2 : throw new ResourceNotFoundException(id);
110 : }
111 4 : return new ChangeEditResource(rsrc, edit.get(), id.get());
112 : }
113 :
114 : /**
115 : * Create handler that is activated when collection element is accessed but doesn't exist, e. g.
116 : * PUT request with a path was called but change edit wasn't created yet. Change edit is created
117 : * and PUT handler is called.
118 : */
119 : @Singleton
120 : public static class Create
121 : implements RestCollectionCreateView<ChangeResource, ChangeEditResource, FileContentInput> {
122 : private final Put putEdit;
123 :
124 : @Inject
125 138 : Create(Put putEdit) {
126 138 : this.putEdit = putEdit;
127 138 : }
128 :
129 : @Override
130 : public Response<Object> apply(
131 : ChangeResource resource, IdString id, FileContentInput fileContentInput)
132 : throws AuthException, BadRequestException, ResourceConflictException, IOException,
133 : PermissionBackendException {
134 2 : putEdit.apply(resource, id.get(), fileContentInput);
135 2 : return Response.none();
136 : }
137 : }
138 :
139 : @Singleton
140 : public static class DeleteFile
141 : implements RestCollectionDeleteMissingView<ChangeResource, ChangeEditResource, Input> {
142 : private final DeleteContent deleteContent;
143 :
144 : @Inject
145 138 : DeleteFile(DeleteContent deleteContent) {
146 138 : this.deleteContent = deleteContent;
147 138 : }
148 :
149 : @Override
150 : public Response<Object> apply(ChangeResource rsrc, IdString id, Input input)
151 : throws IOException, AuthException, BadRequestException, ResourceConflictException,
152 : PermissionBackendException {
153 2 : return deleteContent.apply(rsrc, id.get());
154 : }
155 : }
156 :
157 : // TODO(davido): Turn the boolean options to ChangeEditOption enum,
158 : // like it's already the case for ListChangesOption/ListGroupsOption
159 : public static class Detail implements RestReadView<ChangeResource> {
160 : private final ChangeEditUtil editUtil;
161 : private final ChangeEditJson editJson;
162 : private final FileInfoJson fileInfoJson;
163 : private final Revisions revisions;
164 :
165 : private String base;
166 : private boolean list;
167 : private boolean downloadCommands;
168 :
169 : @Option(name = "--base", metaVar = "revision-id")
170 : public void setBase(String base) {
171 1 : this.base = base;
172 1 : }
173 :
174 : @Option(name = "--list")
175 : public void setList(boolean list) {
176 1 : this.list = list;
177 1 : }
178 :
179 : @Option(name = "--download-commands")
180 : public void setDownloadCommands(boolean downloadCommands) {
181 1 : this.downloadCommands = downloadCommands;
182 1 : }
183 :
184 : @Inject
185 : Detail(
186 : ChangeEditUtil editUtil,
187 : ChangeEditJson editJson,
188 : FileInfoJson fileInfoJson,
189 9 : Revisions revisions) {
190 9 : this.editJson = editJson;
191 9 : this.editUtil = editUtil;
192 9 : this.fileInfoJson = fileInfoJson;
193 9 : this.revisions = revisions;
194 9 : }
195 :
196 : @Override
197 : public Response<EditInfo> apply(ChangeResource rsrc)
198 : throws AuthException, IOException, ResourceNotFoundException, ResourceConflictException,
199 : PermissionBackendException {
200 9 : Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
201 9 : if (!edit.isPresent()) {
202 4 : return Response.none();
203 : }
204 :
205 9 : EditInfo editInfo = editJson.toEditInfo(edit.get(), downloadCommands);
206 9 : if (list) {
207 1 : PatchSet basePatchSet = null;
208 1 : if (base != null) {
209 1 : RevisionResource baseResource = revisions.parse(rsrc, IdString.fromDecoded(base));
210 1 : basePatchSet = baseResource.getPatchSet();
211 : }
212 : try {
213 1 : editInfo.files =
214 1 : fileInfoJson.getFileInfoMap(
215 1 : rsrc.getChange(), edit.get().getEditCommit(), basePatchSet);
216 0 : } catch (PatchListNotAvailableException e) {
217 0 : throw new ResourceNotFoundException(e.getMessage());
218 1 : }
219 : }
220 9 : return Response.ok(editInfo);
221 : }
222 : }
223 :
224 : /**
225 : * Post to edit collection resource. Two different operations are supported:
226 : *
227 : * <ul>
228 : * <li>Create non existing change edit
229 : * <li>Restore path in existing change edit
230 : * </ul>
231 : *
232 : * The combination of two operations in one request is supported.
233 : */
234 : @Singleton
235 : public static class Post
236 : implements RestCollectionModifyView<ChangeResource, ChangeEditResource, Post.Input> {
237 4 : public static class Input {
238 : public String restorePath;
239 : public String oldPath;
240 : public String newPath;
241 : }
242 :
243 : private final ChangeEditModifier editModifier;
244 : private final GitRepositoryManager repositoryManager;
245 :
246 : @Inject
247 142 : Post(ChangeEditModifier editModifier, GitRepositoryManager repositoryManager) {
248 142 : this.editModifier = editModifier;
249 142 : this.repositoryManager = repositoryManager;
250 142 : }
251 :
252 : @Override
253 : public Response<Object> apply(ChangeResource resource, Post.Input postInput)
254 : throws AuthException, BadRequestException, IOException, ResourceConflictException,
255 : PermissionBackendException {
256 18 : Project.NameKey project = resource.getProject();
257 18 : try (Repository repository = repositoryManager.openRepository(project)) {
258 18 : if (isRestoreFile(postInput)) {
259 1 : editModifier.restoreFile(repository, resource.getNotes(), postInput.restorePath);
260 18 : } else if (isRenameFile(postInput)) {
261 3 : editModifier.renameFile(
262 3 : repository, resource.getNotes(), postInput.oldPath, postInput.newPath);
263 : } else {
264 18 : editModifier.createEdit(repository, resource.getNotes());
265 : }
266 1 : } catch (InvalidChangeOperationException e) {
267 1 : throw new ResourceConflictException(e.getMessage());
268 18 : }
269 18 : return Response.none();
270 : }
271 :
272 : private static boolean isRestoreFile(Post.Input postInput) {
273 18 : return postInput != null && !Strings.isNullOrEmpty(postInput.restorePath);
274 : }
275 :
276 : private static boolean isRenameFile(Post.Input postInput) {
277 18 : return postInput != null
278 4 : && !Strings.isNullOrEmpty(postInput.oldPath)
279 18 : && !Strings.isNullOrEmpty(postInput.newPath);
280 : }
281 : }
282 :
283 : /** Put handler that is activated when PUT request is called on collection element. */
284 : @Singleton
285 : public static class Put implements RestModifyView<ChangeEditResource, FileContentInput> {
286 142 : private static final Pattern BINARY_DATA_PATTERN =
287 142 : Pattern.compile("data:([\\w/.-]*);([\\w]+),(.*)");
288 : private static final String BASE64 = "base64";
289 :
290 : private final ChangeEditModifier editModifier;
291 : private final GitRepositoryManager repositoryManager;
292 : private final EditMessage editMessage;
293 :
294 : @Inject
295 : Put(
296 : ChangeEditModifier editModifier,
297 : GitRepositoryManager repositoryManager,
298 142 : EditMessage editMessage) {
299 142 : this.editModifier = editModifier;
300 142 : this.repositoryManager = repositoryManager;
301 142 : this.editMessage = editMessage;
302 142 : }
303 :
304 : @Override
305 : public Response<Object> apply(ChangeEditResource rsrc, FileContentInput fileContentInput)
306 : throws AuthException, BadRequestException, ResourceConflictException, IOException,
307 : PermissionBackendException {
308 2 : return apply(rsrc.getChangeResource(), rsrc.getPath(), fileContentInput);
309 : }
310 :
311 : public Response<Object> apply(
312 : ChangeResource rsrc, String path, FileContentInput fileContentInput)
313 : throws AuthException, BadRequestException, ResourceConflictException, IOException,
314 : PermissionBackendException {
315 :
316 19 : if (fileContentInput.content == null && fileContentInput.binary_content == null) {
317 1 : throw new BadRequestException("either content or binary_content is required");
318 : }
319 :
320 : RawInput newContent;
321 19 : if (fileContentInput.binary_content != null) {
322 1 : Matcher m = BINARY_DATA_PATTERN.matcher(fileContentInput.binary_content);
323 1 : if (m.matches() && BASE64.equals(m.group(2))) {
324 1 : newContent = RawInputUtil.create(Base64.decode(m.group(3)));
325 : } else {
326 1 : throw new BadRequestException("binary_content must be encoded as base64 data uri");
327 : }
328 1 : } else {
329 19 : newContent = fileContentInput.content;
330 : }
331 :
332 19 : if (Patch.COMMIT_MSG.equals(path) && fileContentInput.binary_content == null) {
333 1 : EditMessage.Input editMessageInput = new EditMessage.Input();
334 1 : editMessageInput.message =
335 1 : new String(ByteStreams.toByteArray(newContent.getInputStream()), UTF_8);
336 1 : return editMessage.apply(rsrc, editMessageInput);
337 : }
338 :
339 19 : if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') {
340 1 : throw new ResourceConflictException("Invalid path: " + path);
341 : }
342 :
343 18 : if (fileContentInput.fileMode != null) {
344 1 : if ((fileContentInput.fileMode != 100644) && (fileContentInput.fileMode != 100755)) {
345 0 : throw new BadRequestException(
346 : "file_mode ("
347 : + fileContentInput.fileMode
348 : + ") was invalid: supported values are 0, 644, or 755.");
349 : }
350 : }
351 :
352 18 : try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
353 18 : editModifier.modifyFile(
354 18 : repository, rsrc.getNotes(), path, newContent, fileContentInput.fileMode);
355 1 : } catch (InvalidChangeOperationException e) {
356 1 : throw new ResourceConflictException(e.getMessage());
357 18 : }
358 18 : return Response.none();
359 : }
360 : }
361 :
362 : /**
363 : * Handler to delete a file.
364 : *
365 : * <p>This deletes the file from the repository completely. This is not the same as reverting or
366 : * restoring a file to its previous contents.
367 : */
368 : @Singleton
369 : public static class DeleteContent implements RestModifyView<ChangeEditResource, Input> {
370 :
371 : private final ChangeEditModifier editModifier;
372 : private final GitRepositoryManager repositoryManager;
373 :
374 : @Inject
375 142 : DeleteContent(ChangeEditModifier editModifier, GitRepositoryManager repositoryManager) {
376 142 : this.editModifier = editModifier;
377 142 : this.repositoryManager = repositoryManager;
378 142 : }
379 :
380 : @Override
381 : public Response<Object> apply(ChangeEditResource rsrc, Input input)
382 : throws AuthException, BadRequestException, ResourceConflictException, IOException,
383 : PermissionBackendException {
384 2 : return apply(rsrc.getChangeResource(), rsrc.getPath());
385 : }
386 :
387 : public Response<Object> apply(ChangeResource rsrc, String filePath)
388 : throws AuthException, BadRequestException, IOException, ResourceConflictException,
389 : PermissionBackendException {
390 7 : try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
391 6 : editModifier.deleteFile(repository, rsrc.getNotes(), filePath);
392 1 : } catch (InvalidChangeOperationException e) {
393 1 : throw new ResourceConflictException(e.getMessage());
394 6 : }
395 6 : return Response.none();
396 : }
397 : }
398 :
399 : public static class Get implements RestReadView<ChangeEditResource> {
400 : private final FileContentUtil fileContentUtil;
401 : private final ProjectCache projectCache;
402 : private final GetMessage getMessage;
403 :
404 : @Option(
405 : name = "--base",
406 : aliases = {"-b"},
407 : usage = "whether to load the content on the base revision instead of the change edit")
408 : private boolean base;
409 :
410 : @Inject
411 4 : Get(FileContentUtil fileContentUtil, ProjectCache projectCache, GetMessage getMessage) {
412 4 : this.fileContentUtil = fileContentUtil;
413 4 : this.projectCache = projectCache;
414 4 : this.getMessage = getMessage;
415 4 : }
416 :
417 : @Override
418 : public Response<BinaryResult> apply(ChangeEditResource rsrc) throws AuthException, IOException {
419 : try {
420 4 : if (Patch.COMMIT_MSG.equals(rsrc.getPath())) {
421 1 : return getMessage.apply(rsrc.getChangeResource());
422 : }
423 :
424 4 : ChangeEdit edit = rsrc.getChangeEdit();
425 4 : Project.NameKey project = rsrc.getChangeResource().getProject();
426 4 : return Response.ok(
427 4 : fileContentUtil.getContent(
428 4 : projectCache.get(project).orElseThrow(illegalState(project)),
429 4 : base ? edit.getBasePatchSet().commitId() : edit.getEditCommit(),
430 4 : rsrc.getPath(),
431 : null));
432 1 : } catch (ResourceNotFoundException | BadRequestException e) {
433 1 : return Response.none();
434 : }
435 : }
436 : }
437 :
438 : @Singleton
439 : public static class GetMeta implements RestReadView<ChangeEditResource> {
440 : private final WebLinks webLinks;
441 :
442 : @Inject
443 138 : GetMeta(WebLinks webLinks) {
444 138 : this.webLinks = webLinks;
445 138 : }
446 :
447 : @Override
448 : public Response<FileInfo> apply(ChangeEditResource rsrc) {
449 1 : FileInfo r = new FileInfo();
450 1 : ChangeEdit edit = rsrc.getChangeEdit();
451 1 : Change change = edit.getChange();
452 1 : ImmutableList<DiffWebLinkInfo> links =
453 1 : webLinks.getDiffLinks(
454 1 : change.getProject().get(),
455 1 : change.getChangeId(),
456 1 : edit.getBasePatchSet().number(),
457 1 : edit.getBasePatchSet().refName(),
458 1 : rsrc.getPath(),
459 : 0,
460 1 : edit.getRefName(),
461 1 : rsrc.getPath());
462 1 : r.webLinks = links.isEmpty() ? null : links;
463 1 : return Response.ok(r);
464 : }
465 :
466 1 : public static class FileInfo {
467 : public List<DiffWebLinkInfo> webLinks;
468 : }
469 : }
470 :
471 : @Singleton
472 : public static class EditMessage implements RestModifyView<ChangeResource, EditMessage.Input> {
473 7 : public static class Input {
474 : @DefaultInput public String message;
475 : }
476 :
477 : private final ChangeEditModifier editModifier;
478 : private final GitRepositoryManager repositoryManager;
479 :
480 : @Inject
481 145 : EditMessage(ChangeEditModifier editModifier, GitRepositoryManager repositoryManager) {
482 145 : this.editModifier = editModifier;
483 145 : this.repositoryManager = repositoryManager;
484 145 : }
485 :
486 : @Override
487 : public Response<Object> apply(ChangeResource rsrc, EditMessage.Input editMessageInput)
488 : throws AuthException, IOException, BadRequestException, ResourceConflictException,
489 : PermissionBackendException {
490 7 : if (editMessageInput == null || Strings.isNullOrEmpty(editMessageInput.message)) {
491 1 : throw new BadRequestException("commit message must be provided");
492 : }
493 :
494 6 : Project.NameKey project = rsrc.getProject();
495 6 : try (Repository repository = repositoryManager.openRepository(project)) {
496 6 : editModifier.modifyMessage(repository, rsrc.getNotes(), editMessageInput.message);
497 1 : } catch (InvalidChangeOperationException e) {
498 1 : throw new ResourceConflictException(e.getMessage());
499 6 : }
500 :
501 6 : return Response.none();
502 : }
503 : }
504 :
505 : public static class GetMessage implements RestReadView<ChangeResource> {
506 : private final GitRepositoryManager repoManager;
507 : private final ChangeEditUtil editUtil;
508 :
509 : @Option(
510 : name = "--base",
511 : aliases = {"-b"},
512 : usage = "whether to load the message on the base revision instead of the change edit")
513 : private boolean base;
514 :
515 : @Inject
516 57 : GetMessage(GitRepositoryManager repoManager, ChangeEditUtil editUtil) {
517 57 : this.repoManager = repoManager;
518 57 : this.editUtil = editUtil;
519 57 : }
520 :
521 : @Override
522 : public Response<BinaryResult> apply(ChangeResource rsrc)
523 : throws AuthException, IOException, ResourceNotFoundException {
524 4 : Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
525 : String msg;
526 4 : if (edit.isPresent()) {
527 4 : if (base) {
528 1 : try (Repository repo = repoManager.openRepository(rsrc.getProject());
529 1 : RevWalk rw = new RevWalk(repo)) {
530 1 : RevCommit commit = rw.parseCommit(edit.get().getBasePatchSet().commitId());
531 1 : msg = commit.getFullMessage();
532 : }
533 : } else {
534 4 : msg = edit.get().getEditCommit().getFullMessage();
535 : }
536 :
537 4 : return Response.ok(
538 4 : BinaryResult.create(msg)
539 4 : .setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
540 4 : .base64());
541 : }
542 1 : throw new ResourceNotFoundException();
543 : }
544 : }
545 : }
|