Line data Source code
1 : // Copyright (C) 2012 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 com.google.common.collect.ImmutableListMultimap; 18 : import com.google.common.collect.Streams; 19 : import com.google.gerrit.common.Nullable; 20 : import com.google.gerrit.entities.Change; 21 : import com.google.gerrit.entities.RefNames; 22 : import com.google.gerrit.extensions.client.ListChangesOption; 23 : import com.google.gerrit.extensions.client.ListOption; 24 : import com.google.gerrit.extensions.common.ChangeInfo; 25 : import com.google.gerrit.extensions.common.PluginDefinedInfo; 26 : import com.google.gerrit.extensions.registration.DynamicSet; 27 : import com.google.gerrit.extensions.restapi.BadRequestException; 28 : import com.google.gerrit.extensions.restapi.PreconditionFailedException; 29 : import com.google.gerrit.extensions.restapi.Response; 30 : import com.google.gerrit.extensions.restapi.RestApiException; 31 : import com.google.gerrit.extensions.restapi.RestReadView; 32 : import com.google.gerrit.server.DynamicOptions; 33 : import com.google.gerrit.server.DynamicOptions.DynamicBean; 34 : import com.google.gerrit.server.change.ChangeJson; 35 : import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory; 36 : import com.google.gerrit.server.change.ChangeResource; 37 : import com.google.gerrit.server.change.PluginDefinedAttributesFactories; 38 : import com.google.gerrit.server.change.RevisionResource; 39 : import com.google.gerrit.server.git.GitRepositoryManager; 40 : import com.google.gerrit.server.notedb.MissingMetaObjectException; 41 : import com.google.gerrit.server.query.change.ChangeData; 42 : import com.google.inject.Inject; 43 : import java.io.IOException; 44 : import java.util.Collection; 45 : import java.util.EnumSet; 46 : import java.util.HashMap; 47 : import java.util.Map; 48 : import org.eclipse.jgit.errors.InvalidObjectIdException; 49 : import org.eclipse.jgit.lib.ObjectId; 50 : import org.eclipse.jgit.lib.Ref; 51 : import org.eclipse.jgit.lib.Repository; 52 : import org.eclipse.jgit.revwalk.RevCommit; 53 : import org.eclipse.jgit.revwalk.RevWalk; 54 : import org.kohsuke.args4j.Option; 55 : 56 : public class GetChange 57 : implements RestReadView<ChangeResource>, 58 : DynamicOptions.BeanReceiver, 59 : DynamicOptions.BeanProvider { 60 : private final ChangeJson.Factory json; 61 : private final DynamicSet<ChangePluginDefinedInfoFactory> pdiFactories; 62 145 : private final EnumSet<ListChangesOption> options = EnumSet.noneOf(ListChangesOption.class); 63 145 : private final Map<String, DynamicBean> dynamicBeans = new HashMap<>(); 64 : private final GitRepositoryManager repoMgr; 65 : 66 : @Option(name = "-o", usage = "Output options") 67 : public void addOption(ListChangesOption o) { 68 145 : options.add(o); 69 145 : } 70 : 71 145 : @Option(name = "--meta", usage = "NoteDb meta SHA1") 72 : String metaRevId = ""; 73 : 74 : public void setMetaRevId(String metaRevId) { 75 1 : this.metaRevId = metaRevId == null ? "" : metaRevId; 76 1 : } 77 : 78 : @Option(name = "-O", usage = "Output option flags, in hex") 79 : void setOptionFlagsHex(String hex) throws BadRequestException { 80 0 : EnumSet<ListChangesOption> optionSet = ListOption.fromHexString(ListChangesOption.class, hex); 81 0 : options.addAll(optionSet); 82 0 : } 83 : 84 : @Inject 85 : GetChange( 86 : ChangeJson.Factory json, 87 : DynamicSet<ChangePluginDefinedInfoFactory> pdiFactories, 88 145 : GitRepositoryManager repoMgr) { 89 145 : this.json = json; 90 145 : this.pdiFactories = pdiFactories; 91 145 : this.repoMgr = repoMgr; 92 145 : } 93 : 94 : @Override 95 : public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) { 96 2 : dynamicBeans.put(plugin, dynamicBean); 97 2 : } 98 : 99 : @Override 100 : public DynamicBean getDynamicBean(String plugin) { 101 2 : return dynamicBeans.get(plugin); 102 : } 103 : 104 : @Override 105 : public Response<ChangeInfo> apply(ChangeResource rsrc) throws RestApiException { 106 : try { 107 67 : Change change = rsrc.getChange(); 108 67 : ObjectId changeMetaRevId = getMetaRevId(change); 109 67 : return Response.withMustRevalidate(newChangeJson().format(change, changeMetaRevId)); 110 0 : } catch (MissingMetaObjectException e) { 111 0 : throw new PreconditionFailedException(e.getMessage()); 112 : } 113 : } 114 : 115 : Response<ChangeInfo> apply(RevisionResource rsrc) { 116 2 : return Response.withMustRevalidate(newChangeJson().format(rsrc)); 117 : } 118 : 119 : @Nullable 120 : private ObjectId getMetaRevId(Change change) throws RestApiException { 121 67 : if (metaRevId.isEmpty()) { 122 67 : return null; 123 : } 124 : 125 : // It might be interesting to also allow {SHA1}^^, so callers can walk back into history 126 : // without having to fetch the entire /meta ref. If we do so, we have to be careful that 127 : // the error messages can't be abused to fetch hidden data. 128 : ObjectId metaRevObjectId; 129 : try { 130 1 : metaRevObjectId = ObjectId.fromString(metaRevId); 131 1 : } catch (InvalidObjectIdException e) { 132 1 : throw new BadRequestException("invalid meta SHA1: " + metaRevId, e); 133 1 : } 134 1 : return verifyMetaId(change, metaRevObjectId); 135 : } 136 : 137 : private ChangeJson newChangeJson() { 138 68 : return json.create(options, this::createPluginDefinedInfos); 139 : } 140 : 141 : private ImmutableListMultimap<Change.Id, PluginDefinedInfo> createPluginDefinedInfos( 142 : Collection<ChangeData> cds) { 143 68 : return PluginDefinedAttributesFactories.createAll( 144 68 : cds, this, Streams.stream(pdiFactories.entries())); 145 : } 146 : 147 : @Nullable 148 : private ObjectId verifyMetaId(Change change, @Nullable ObjectId id) throws RestApiException { 149 1 : if (id == null) { 150 0 : return null; 151 : } 152 : 153 1 : String changeMetaRefName = RefNames.changeMetaRef(change.getId()); 154 1 : try (Repository repo = repoMgr.openRepository(change.getProject()); 155 1 : RevWalk rw = new RevWalk(repo)) { 156 1 : rw.setRetainBody(false); 157 1 : Ref ref = repo.getRefDatabase().exactRef(changeMetaRefName); 158 1 : RevCommit tip = rw.parseCommit(ref.getObjectId()); 159 1 : rw.markStart(tip); 160 1 : for (RevCommit rev : rw) { 161 1 : if (id.equals(rev)) { 162 1 : return id; 163 : } 164 1 : } 165 1 : } catch (IOException e) { 166 0 : throw RestApiException.wrap( 167 : "I/O error while reading meta-ref id=" 168 0 : + id.getName() 169 : + " from change " 170 0 : + change.getChangeId(), 171 : e); 172 1 : } 173 : 174 1 : throw new PreconditionFailedException( 175 1 : id.getName() + " not reachable from " + changeMetaRefName); 176 : } 177 : }