Line data Source code
1 : // Copyright (C) 2016 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.change; 16 : 17 : import com.google.common.base.Throwables; 18 : import com.google.common.cache.Cache; 19 : import com.google.common.collect.ImmutableList; 20 : import com.google.common.collect.Sets; 21 : import com.google.common.flogger.FluentLogger; 22 : import com.google.common.primitives.Ints; 23 : import com.google.gerrit.entities.Change; 24 : import com.google.gerrit.entities.Project; 25 : import com.google.gerrit.exceptions.StorageException; 26 : import com.google.gerrit.extensions.restapi.Url; 27 : import com.google.gerrit.git.ObjectIds; 28 : import com.google.gerrit.index.IndexConfig; 29 : import com.google.gerrit.metrics.Counter1; 30 : import com.google.gerrit.metrics.Description; 31 : import com.google.gerrit.metrics.Field; 32 : import com.google.gerrit.metrics.MetricMaker; 33 : import com.google.gerrit.server.cache.CacheModule; 34 : import com.google.gerrit.server.logging.Metadata; 35 : import com.google.gerrit.server.notedb.ChangeNotes; 36 : import com.google.gerrit.server.project.NoSuchChangeException; 37 : import com.google.gerrit.server.query.change.ChangeData; 38 : import com.google.gerrit.server.query.change.InternalChangeQuery; 39 : import com.google.inject.Inject; 40 : import com.google.inject.Module; 41 : import com.google.inject.Provider; 42 : import com.google.inject.Singleton; 43 : import com.google.inject.name.Named; 44 : import java.util.ArrayList; 45 : import java.util.Collections; 46 : import java.util.List; 47 : import java.util.Optional; 48 : import java.util.Set; 49 : import org.eclipse.jgit.errors.RepositoryNotFoundException; 50 : 51 : @Singleton 52 : public class ChangeFinder { 53 152 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 54 : 55 : private static final String CACHE_NAME = "changeid_project"; 56 : 57 : public static Module module() { 58 152 : return new CacheModule() { 59 : @Override 60 : protected void configure() { 61 152 : cache(CACHE_NAME, Change.Id.class, String.class).maximumWeight(1024); 62 152 : } 63 : }; 64 : } 65 : 66 90 : public enum ChangeIdType { 67 90 : ALL, 68 90 : TRIPLET, 69 90 : NUMERIC_ID, 70 90 : I_HASH, 71 90 : PROJECT_NUMERIC_ID, 72 90 : COMMIT_HASH 73 : } 74 : 75 : private final IndexConfig indexConfig; 76 : private final Cache<Change.Id, String> changeIdProjectCache; 77 : private final Provider<InternalChangeQuery> queryProvider; 78 : private final ChangeNotes.Factory changeNotesFactory; 79 : private final Counter1<ChangeIdType> changeIdCounter; 80 : 81 : @Inject 82 : ChangeFinder( 83 : IndexConfig indexConfig, 84 : @Named(CACHE_NAME) Cache<Change.Id, String> changeIdProjectCache, 85 : Provider<InternalChangeQuery> queryProvider, 86 : ChangeNotes.Factory changeNotesFactory, 87 149 : MetricMaker metricMaker) { 88 149 : this.indexConfig = indexConfig; 89 149 : this.changeIdProjectCache = changeIdProjectCache; 90 149 : this.queryProvider = queryProvider; 91 149 : this.changeNotesFactory = changeNotesFactory; 92 149 : this.changeIdCounter = 93 149 : metricMaker.newCounter( 94 : "http/server/rest_api/change_id_type", 95 : new Description("Total number of API calls per identifier type.") 96 149 : .setRate() 97 149 : .setUnit("requests"), 98 149 : Field.ofEnum(ChangeIdType.class, "change_id_type", Metadata.Builder::changeIdType) 99 149 : .description("The type of the change identifier.") 100 149 : .build()); 101 149 : } 102 : 103 : public Optional<ChangeNotes> findOne(String id) { 104 : // Limit the maximum number of results to just 2 items for saving CPU cycles 105 : // in reading change-notes. 106 15 : List<ChangeNotes> ctls = find(id, 2); 107 15 : if (ctls.size() != 1) { 108 1 : return Optional.empty(); 109 : } 110 15 : return Optional.of(ctls.get(0)); 111 : } 112 : 113 : /** 114 : * Find changes matching the given identifier. 115 : * 116 : * @param id change identifier. 117 : * @return possibly-empty list of notes for all matching changes; may or may not be visible. 118 : */ 119 : public List<ChangeNotes> find(String id) { 120 11 : return find(id, 0); 121 : } 122 : 123 : /** 124 : * Find at most N changes matching the given identifier. 125 : * 126 : * @param id change identifier. 127 : * @param queryLimit maximum number of changes to be returned 128 : * @return possibly-empty list of notes for all matching changes; may or may not be visible. 129 : */ 130 : public List<ChangeNotes> find(String id, int queryLimit) { 131 90 : if (id.isEmpty()) { 132 0 : return Collections.emptyList(); 133 : } 134 : 135 90 : int z = id.lastIndexOf('~'); 136 90 : int y = id.lastIndexOf('~', z - 1); 137 90 : if (y < 0 && z > 0) { 138 : // Try project~numericChangeId 139 3 : Integer n = Ints.tryParse(id.substring(z + 1)); 140 3 : if (n != null) { 141 3 : changeIdCounter.increment(ChangeIdType.PROJECT_NUMERIC_ID); 142 3 : return fromProjectNumber(id.substring(0, z), n.intValue()); 143 : } 144 : } 145 : 146 90 : if (y < 0 && z < 0) { 147 : // Try numeric changeId 148 90 : Integer n = Ints.tryParse(id); 149 90 : if (n != null) { 150 55 : changeIdCounter.increment(ChangeIdType.NUMERIC_ID); 151 55 : return find(Change.id(n)); 152 : } 153 : } 154 : 155 : // Use the index to search for changes, but don't return any stored fields, 156 : // to force rereading in case the index is stale. 157 80 : InternalChangeQuery query = queryProvider.get().noFields(); 158 80 : if (queryLimit > 0) { 159 80 : query.setLimit(queryLimit); 160 : } 161 : 162 : // Try commit hash 163 80 : if (id.matches("^([0-9a-fA-F]{" + ObjectIds.ABBREV_STR_LEN + "," + ObjectIds.STR_LEN + "})$")) { 164 2 : changeIdCounter.increment(ChangeIdType.COMMIT_HASH); 165 2 : return asChangeNotes(query.byCommit(id)); 166 : } 167 : 168 79 : if (y > 0 && z > 0) { 169 : // Try change triplet (project~branch~Ihash...) 170 18 : Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id, y, z); 171 18 : if (triplet.isPresent()) { 172 18 : ChangeTriplet t = triplet.get(); 173 18 : changeIdCounter.increment(ChangeIdType.TRIPLET); 174 18 : return asChangeNotes(query.byBranchKey(t.branch(), t.id())); 175 : } 176 : } 177 : 178 : // Try isolated Ihash... format ("Change-Id: Ihash"). 179 79 : List<ChangeNotes> notes = asChangeNotes(query.byKeyPrefix(id)); 180 79 : if (!notes.isEmpty()) { 181 79 : changeIdCounter.increment(ChangeIdType.I_HASH); 182 : } 183 79 : return notes; 184 : } 185 : 186 : private List<ChangeNotes> fromProjectNumber(String project, int changeNumber) { 187 45 : Change.Id cId = Change.id(changeNumber); 188 : try { 189 45 : return ImmutableList.of( 190 45 : changeNotesFactory.createChecked(Project.NameKey.parse(project), cId)); 191 1 : } catch (NoSuchChangeException e) { 192 1 : return Collections.emptyList(); 193 1 : } catch (StorageException e) { 194 : // Distinguish between a RepositoryNotFoundException (project argument invalid) and 195 : // other StorageExceptions (failure in the persistence layer). 196 1 : if (Throwables.getRootCause(e) instanceof RepositoryNotFoundException) { 197 1 : return Collections.emptyList(); 198 : } 199 0 : throw e; 200 : } 201 : } 202 : 203 : public Optional<ChangeNotes> findOne(Change.Id id) { 204 10 : List<ChangeNotes> notes = find(id); 205 10 : if (notes.size() != 1) { 206 1 : return Optional.empty(); 207 : } 208 10 : return Optional.of(notes.get(0)); 209 : } 210 : 211 : public List<ChangeNotes> find(Change.Id id) { 212 65 : String project = changeIdProjectCache.getIfPresent(id); 213 65 : if (project != null) { 214 45 : return fromProjectNumber(project, id.get()); 215 : } 216 : 217 : // Use the index to search for changes, but don't return any stored fields, 218 : // to force rereading in case the index is stale. 219 65 : InternalChangeQuery query = queryProvider.get().noFields(); 220 65 : List<ChangeData> r = query.byLegacyChangeId(id); 221 65 : if (r.size() == 1) { 222 65 : changeIdProjectCache.put(id, Url.encode(r.get(0).project().get())); 223 : } 224 65 : return asChangeNotes(r); 225 : } 226 : 227 : private List<ChangeNotes> asChangeNotes(List<ChangeData> cds) { 228 91 : List<ChangeNotes> notes = new ArrayList<>(cds.size()); 229 91 : if (!indexConfig.separateChangeSubIndexes()) { 230 0 : for (ChangeData cd : cds) { 231 : try { 232 0 : notes.add(cd.notes()); 233 0 : } catch (NoSuchChangeException e) { 234 0 : logger.atWarning().log("Change %s seen in index, but missing in NoteDb", e.getMessage()); 235 0 : } 236 0 : } 237 0 : return notes; 238 : } 239 : 240 : // If an index implementation uses separate non-atomic subindexes, it's possible to temporarily 241 : // observe a change as present in both subindexes, if this search is concurrent with a write. 242 : // Dedup to avoid confusing the caller. We can choose an arbitrary ChangeData instance because 243 : // the index results have no stored fields, so the data is already reloaded. (It's also possible 244 : // that a change might appear in zero subindexes, but there's nothing we can do here to help 245 : // this case.) 246 91 : Set<Change.Id> seen = Sets.newHashSetWithExpectedSize(cds.size()); 247 91 : for (ChangeData cd : cds) { 248 91 : if (seen.add(cd.getId())) { 249 : try { 250 91 : notes.add(cd.notes()); 251 0 : } catch (NoSuchChangeException e) { 252 0 : logger.atWarning().log("Change %s seen in index, but missing in NoteDb", e.getMessage()); 253 91 : } 254 : } 255 91 : } 256 91 : return notes; 257 : } 258 : }