Line data Source code
1 : // Copyright (C) 2015 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.common.collect.ImmutableList.toImmutableList; 18 : import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES; 19 : import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.TOPIC_CLOSURE; 20 : import static java.util.Collections.reverseOrder; 21 : 22 : import com.google.common.collect.ImmutableList; 23 : import com.google.common.flogger.FluentLogger; 24 : import com.google.gerrit.entities.Change; 25 : import com.google.gerrit.exceptions.StorageException; 26 : import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo; 27 : import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption; 28 : import com.google.gerrit.extensions.client.ListChangesOption; 29 : import com.google.gerrit.extensions.restapi.AuthException; 30 : import com.google.gerrit.extensions.restapi.BadRequestException; 31 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 32 : import com.google.gerrit.extensions.restapi.Response; 33 : import com.google.gerrit.extensions.restapi.RestReadView; 34 : import com.google.gerrit.server.change.ChangeJson; 35 : import com.google.gerrit.server.change.ChangeResource; 36 : import com.google.gerrit.server.change.WalkSorter; 37 : import com.google.gerrit.server.change.WalkSorter.PatchSetData; 38 : import com.google.gerrit.server.permissions.PermissionBackendException; 39 : import com.google.gerrit.server.query.change.ChangeData; 40 : import com.google.gerrit.server.query.change.InternalChangeQuery; 41 : import com.google.gerrit.server.submit.ChangeSet; 42 : import com.google.gerrit.server.submit.MergeSuperSet; 43 : import com.google.inject.Inject; 44 : import com.google.inject.Provider; 45 : import java.io.IOException; 46 : import java.util.Collections; 47 : import java.util.Comparator; 48 : import java.util.EnumSet; 49 : import java.util.List; 50 : import java.util.Set; 51 : import org.kohsuke.args4j.Option; 52 : 53 : public class SubmittedTogether implements RestReadView<ChangeResource> { 54 57 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 55 : 56 57 : private final EnumSet<SubmittedTogetherOption> options = 57 57 : EnumSet.noneOf(SubmittedTogetherOption.class); 58 : 59 57 : private final EnumSet<ListChangesOption> jsonOpt = 60 57 : EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.SUBMITTABLE); 61 : 62 57 : private static final Comparator<ChangeData> COMPARATOR = 63 57 : Comparator.comparing(ChangeData::project) 64 57 : .thenComparing(cd -> cd.getId().get(), reverseOrder()); 65 : 66 : private final ChangeJson.Factory json; 67 : private final Provider<InternalChangeQuery> queryProvider; 68 : private final Provider<MergeSuperSet> mergeSuperSet; 69 : private final Provider<WalkSorter> sorter; 70 : 71 : @Option(name = "-o", usage = "Output options") 72 : void addOption(String option) { 73 0 : for (ListChangesOption o : ListChangesOption.values()) { 74 0 : if (o.name().equalsIgnoreCase(option)) { 75 0 : jsonOpt.add(o); 76 0 : return; 77 : } 78 : } 79 : 80 0 : for (SubmittedTogetherOption o : SubmittedTogetherOption.values()) { 81 0 : if (o.name().equalsIgnoreCase(option)) { 82 0 : options.add(o); 83 0 : return; 84 : } 85 : } 86 : 87 0 : throw new IllegalArgumentException("option not recognized: " + option); 88 : } 89 : 90 : @Inject 91 : SubmittedTogether( 92 : ChangeJson.Factory json, 93 : Provider<InternalChangeQuery> queryProvider, 94 : Provider<MergeSuperSet> mergeSuperSet, 95 57 : Provider<WalkSorter> sorter) { 96 57 : this.json = json; 97 57 : this.queryProvider = queryProvider; 98 57 : this.mergeSuperSet = mergeSuperSet; 99 57 : this.sorter = sorter; 100 57 : } 101 : 102 : public SubmittedTogether addListChangesOption(Set<ListChangesOption> o) { 103 10 : jsonOpt.addAll(o); 104 10 : return this; 105 : } 106 : 107 : public SubmittedTogether addSubmittedTogetherOption(Set<SubmittedTogetherOption> o) { 108 10 : options.addAll(o); 109 10 : return this; 110 : } 111 : 112 : @Override 113 : public Response<Object> apply(ChangeResource resource) 114 : throws AuthException, BadRequestException, ResourceConflictException, IOException, 115 : PermissionBackendException { 116 1 : SubmittedTogetherInfo info = applyInfo(resource); 117 1 : if (options.isEmpty()) { 118 1 : return Response.ok(info.changes); 119 : } 120 0 : return Response.ok(info); 121 : } 122 : 123 : public SubmittedTogetherInfo applyInfo(ChangeResource resource) 124 : throws AuthException, IOException, PermissionBackendException { 125 11 : Change c = resource.getChange(); 126 : try { 127 : List<ChangeData> cds; 128 : int hidden; 129 : 130 11 : if (c.isNew()) { 131 10 : ChangeSet cs = 132 : mergeSuperSet 133 10 : .get() 134 10 : .completeChangeSet(c, resource.getUser(), options.contains(TOPIC_CLOSURE)); 135 10 : cds = ensureRequiredDataIsLoaded(cs.changes().asList()); 136 10 : hidden = cs.nonVisibleChanges().size(); 137 11 : } else if (c.isMerged()) { 138 8 : cds = queryProvider.get().bySubmissionId(c.getSubmissionId()); 139 8 : hidden = 0; 140 : } else { 141 0 : cds = Collections.emptyList(); 142 0 : hidden = 0; 143 : } 144 : 145 11 : if (hidden != 0 && !options.contains(NON_VISIBLE_CHANGES)) { 146 0 : throw new AuthException("change would be submitted with a change that you cannot see"); 147 : } 148 : 149 11 : cds = sort(cds, hidden); 150 11 : SubmittedTogetherInfo info = new SubmittedTogetherInfo(); 151 11 : info.changes = json.create(jsonOpt).format(cds); 152 11 : info.nonVisibleChanges = hidden; 153 11 : return info; 154 0 : } catch (StorageException | IOException e) { 155 0 : logger.atSevere().withCause(e).log("Error on getting a ChangeSet"); 156 0 : throw e; 157 : } 158 : } 159 : 160 : private ImmutableList<ChangeData> sort(List<ChangeData> cds, int hidden) throws IOException { 161 11 : if (cds.size() <= 1 && hidden == 0) { 162 : // Skip sorting for singleton lists, to avoid WalkSorter opening the 163 : // repo just to fill out the commit field in PatchSetData. 164 8 : return ImmutableList.of(); 165 : } 166 : 167 10 : long numProjectsDistinct = cds.stream().map(ChangeData::project).distinct().count(); 168 10 : long numProjects = cds.stream().map(ChangeData::project).count(); 169 : 170 10 : if (numProjects == numProjectsDistinct || numProjectsDistinct > 5) { 171 : // We either have only a single change per project which means that WalkSorter won't make a 172 : // difference compared to our index-backed sort, or we are looking at more than 5 projects 173 : // which would make WalkSorter too expensive for this call. 174 1 : return cds.stream().sorted(COMPARATOR).collect(toImmutableList()); 175 : } 176 : 177 : // Perform more expensive walk-sort. 178 10 : ImmutableList.Builder<ChangeData> sorted = ImmutableList.builderWithExpectedSize(cds.size()); 179 10 : for (PatchSetData psd : sorter.get().sort(cds)) { 180 10 : sorted.add(psd.data()); 181 10 : } 182 10 : return sorted.build(); 183 : } 184 : 185 : private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds) { 186 : // TODO(hiesel): Instead of calling these manually, either implement a helper that brings a 187 : // database-backed change on-par with an index-backed change in terms of the populated fields in 188 : // ChangeData or check if any of the ChangeDatas was loaded from the database and allow 189 : // lazyloading if so. 190 10 : for (ChangeData cd : cds) { 191 10 : cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT); 192 10 : cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_STRICT); 193 10 : cd.currentPatchSet(); 194 10 : } 195 10 : return cds; 196 : } 197 : }