LCOV - code coverage report
Current view: top level - server - StarredChangesUtil.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 134 198 67.7 %
Date: 2022-11-19 15:00:39 Functions: 22 29 75.9 %

          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;
      16             : 
      17             : import static java.nio.charset.StandardCharsets.UTF_8;
      18             : import static java.util.Objects.requireNonNull;
      19             : import static java.util.stream.Collectors.joining;
      20             : import static java.util.stream.Collectors.toSet;
      21             : 
      22             : import com.google.auto.value.AutoValue;
      23             : import com.google.common.base.CharMatcher;
      24             : import com.google.common.base.Joiner;
      25             : import com.google.common.base.Splitter;
      26             : import com.google.common.collect.ImmutableListMultimap;
      27             : import com.google.common.collect.ImmutableMap;
      28             : import com.google.common.collect.ImmutableSet;
      29             : import com.google.common.collect.ImmutableSortedSet;
      30             : import com.google.common.flogger.FluentLogger;
      31             : import com.google.common.primitives.Ints;
      32             : import com.google.gerrit.common.Nullable;
      33             : import com.google.gerrit.entities.Account;
      34             : import com.google.gerrit.entities.Change;
      35             : import com.google.gerrit.entities.RefNames;
      36             : import com.google.gerrit.exceptions.StorageException;
      37             : import com.google.gerrit.git.GitUpdateFailureException;
      38             : import com.google.gerrit.git.LockFailureException;
      39             : import com.google.gerrit.server.config.AllUsersName;
      40             : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
      41             : import com.google.gerrit.server.git.GitRepositoryManager;
      42             : import com.google.gerrit.server.index.change.ChangeField;
      43             : import com.google.gerrit.server.logging.Metadata;
      44             : import com.google.gerrit.server.logging.TraceContext;
      45             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      46             : import com.google.gerrit.server.project.NoSuchChangeException;
      47             : import com.google.gerrit.server.query.change.ChangeData;
      48             : import com.google.gerrit.server.query.change.InternalChangeQuery;
      49             : import com.google.inject.Inject;
      50             : import com.google.inject.Provider;
      51             : import com.google.inject.Singleton;
      52             : import java.io.IOException;
      53             : import java.util.Collection;
      54             : import java.util.Collections;
      55             : import java.util.List;
      56             : import java.util.NavigableSet;
      57             : import java.util.Set;
      58             : import java.util.TreeSet;
      59             : import org.eclipse.jgit.lib.BatchRefUpdate;
      60             : import org.eclipse.jgit.lib.Constants;
      61             : import org.eclipse.jgit.lib.NullProgressMonitor;
      62             : import org.eclipse.jgit.lib.ObjectId;
      63             : import org.eclipse.jgit.lib.ObjectInserter;
      64             : import org.eclipse.jgit.lib.ObjectLoader;
      65             : import org.eclipse.jgit.lib.ObjectReader;
      66             : import org.eclipse.jgit.lib.PersonIdent;
      67             : import org.eclipse.jgit.lib.Ref;
      68             : import org.eclipse.jgit.lib.RefDatabase;
      69             : import org.eclipse.jgit.lib.RefUpdate;
      70             : import org.eclipse.jgit.lib.Repository;
      71             : import org.eclipse.jgit.revwalk.RevWalk;
      72             : import org.eclipse.jgit.transport.ReceiveCommand;
      73             : 
      74             : @Singleton
      75             : public class StarredChangesUtil {
      76         150 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      77             : 
      78             :   @AutoValue
      79           0 :   public abstract static class StarField {
      80             :     private static final String SEPARATOR = ":";
      81             : 
      82             :     @Nullable
      83             :     public static StarField parse(String s) {
      84           0 :       int p = s.indexOf(SEPARATOR);
      85           0 :       if (p >= 0) {
      86           0 :         Integer id = Ints.tryParse(s.substring(0, p));
      87           0 :         if (id == null) {
      88           0 :           return null;
      89             :         }
      90           0 :         Account.Id accountId = Account.id(id);
      91           0 :         String label = s.substring(p + 1);
      92           0 :         return create(accountId, label);
      93             :       }
      94           0 :       return null;
      95             :     }
      96             : 
      97             :     public static StarField create(Account.Id accountId, String label) {
      98           0 :       return new AutoValue_StarredChangesUtil_StarField(accountId, label);
      99             :     }
     100             : 
     101             :     public abstract Account.Id accountId();
     102             : 
     103             :     public abstract String label();
     104             : 
     105             :     @Override
     106             :     public final String toString() {
     107           0 :       return accountId() + SEPARATOR + label();
     108             :     }
     109             :   }
     110             : 
     111          10 :   public enum Operation {
     112          10 :     ADD,
     113          10 :     REMOVE
     114             :   }
     115             : 
     116             :   @AutoValue
     117         103 :   public abstract static class StarRef {
     118         103 :     private static final StarRef MISSING =
     119         103 :         new AutoValue_StarredChangesUtil_StarRef(null, Collections.emptyNavigableSet());
     120             : 
     121             :     private static StarRef create(Ref ref, Iterable<String> labels) {
     122           8 :       return new AutoValue_StarredChangesUtil_StarRef(
     123           8 :           requireNonNull(ref), ImmutableSortedSet.copyOf(labels));
     124             :     }
     125             : 
     126             :     @Nullable
     127             :     public abstract Ref ref();
     128             : 
     129             :     public abstract NavigableSet<String> labels();
     130             : 
     131             :     public ObjectId objectId() {
     132          10 :       return ref() != null ? ref().getObjectId() : ObjectId.zeroId();
     133             :     }
     134             :   }
     135             : 
     136             :   public static class IllegalLabelException extends Exception {
     137             :     private static final long serialVersionUID = 1L;
     138             : 
     139             :     IllegalLabelException(String message) {
     140           0 :       super(message);
     141           0 :     }
     142             :   }
     143             : 
     144             :   public static class InvalidLabelsException extends IllegalLabelException {
     145             :     private static final long serialVersionUID = 1L;
     146             : 
     147             :     InvalidLabelsException(Set<String> invalidLabels) {
     148           0 :       super(String.format("invalid labels: %s", Joiner.on(", ").join(invalidLabels)));
     149           0 :     }
     150             :   }
     151             : 
     152             :   public static class MutuallyExclusiveLabelsException extends IllegalLabelException {
     153             :     private static final long serialVersionUID = 1L;
     154             : 
     155             :     MutuallyExclusiveLabelsException(String label1, String label2) {
     156           0 :       super(
     157           0 :           String.format(
     158             :               "The labels %s and %s are mutually exclusive. Only one of them can be set.",
     159             :               label1, label2));
     160           0 :     }
     161             :   }
     162             : 
     163             :   public static final String DEFAULT_LABEL = "star";
     164             : 
     165             :   private final GitRepositoryManager repoManager;
     166             :   private final GitReferenceUpdated gitRefUpdated;
     167             :   private final AllUsersName allUsers;
     168             :   private final Provider<PersonIdent> serverIdent;
     169             :   private final Provider<InternalChangeQuery> queryProvider;
     170             : 
     171             :   @Inject
     172             :   StarredChangesUtil(
     173             :       GitRepositoryManager repoManager,
     174             :       GitReferenceUpdated gitRefUpdated,
     175             :       AllUsersName allUsers,
     176             :       @GerritPersonIdent Provider<PersonIdent> serverIdent,
     177         150 :       Provider<InternalChangeQuery> queryProvider) {
     178         150 :     this.repoManager = repoManager;
     179         150 :     this.gitRefUpdated = gitRefUpdated;
     180         150 :     this.allUsers = allUsers;
     181         150 :     this.serverIdent = serverIdent;
     182         150 :     this.queryProvider = queryProvider;
     183         150 :   }
     184             : 
     185             :   public NavigableSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
     186         103 :     try (Repository repo = repoManager.openRepository(allUsers)) {
     187         103 :       return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)).labels();
     188           0 :     } catch (IOException e) {
     189           0 :       throw new StorageException(
     190           0 :           String.format(
     191             :               "Reading stars from change %d for account %d failed",
     192           0 :               changeId.get(), accountId.get()),
     193             :           e);
     194             :     }
     195             :   }
     196             : 
     197             :   public void star(Account.Id accountId, Change.Id changeId, Operation op)
     198             :       throws IllegalLabelException {
     199          10 :     try (Repository repo = repoManager.openRepository(allUsers)) {
     200          10 :       String refName = RefNames.refsStarredChanges(changeId, accountId);
     201          10 :       StarRef old = readLabels(repo, refName);
     202             : 
     203          10 :       NavigableSet<String> labels = new TreeSet<>(old.labels());
     204          10 :       switch (op) {
     205             :         case ADD:
     206          10 :           labels.add(DEFAULT_LABEL);
     207          10 :           break;
     208             :         case REMOVE:
     209           3 :           labels.remove(DEFAULT_LABEL);
     210             :           break;
     211             :       }
     212             : 
     213          10 :       if (labels.isEmpty()) {
     214           3 :         deleteRef(repo, refName, old.objectId());
     215             :       } else {
     216          10 :         updateLabels(repo, refName, old.objectId(), labels);
     217             :       }
     218           0 :     } catch (IOException e) {
     219           0 :       throw new StorageException(
     220           0 :           String.format("Star change %d for account %d failed", changeId.get(), accountId.get()),
     221             :           e);
     222          10 :     }
     223          10 :   }
     224             : 
     225             :   /**
     226             :    * Unstar the given change for all users.
     227             :    *
     228             :    * <p>Intended for use only when we're about to delete a change. For that reason, the change is
     229             :    * not reindexed.
     230             :    *
     231             :    * @param changeId change ID.
     232             :    * @throws IOException if an error occurred.
     233             :    */
     234             :   public void unstarAllForChangeDeletion(Change.Id changeId) throws IOException {
     235          11 :     try (Repository repo = repoManager.openRepository(allUsers);
     236          11 :         RevWalk rw = new RevWalk(repo)) {
     237          11 :       BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
     238          11 :       batchUpdate.setAllowNonFastForwards(true);
     239          11 :       batchUpdate.setRefLogIdent(serverIdent.get());
     240          11 :       batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
     241          11 :       for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
     242           0 :         String refName = RefNames.refsStarredChanges(changeId, accountId);
     243           0 :         Ref ref = repo.getRefDatabase().exactRef(refName);
     244           0 :         if (ref != null) {
     245           0 :           batchUpdate.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), refName));
     246             :         }
     247           0 :       }
     248          11 :       batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
     249          11 :       for (ReceiveCommand command : batchUpdate.getCommands()) {
     250           0 :         if (command.getResult() != ReceiveCommand.Result.OK) {
     251           0 :           String message =
     252           0 :               String.format(
     253             :                   "Unstar change %d failed, ref %s could not be deleted: %s",
     254           0 :                   changeId.get(), command.getRefName(), command.getResult());
     255           0 :           if (command.getResult() == ReceiveCommand.Result.LOCK_FAILURE) {
     256           0 :             throw new LockFailureException(message, batchUpdate);
     257             :           }
     258           0 :           throw new GitUpdateFailureException(message, batchUpdate);
     259             :         }
     260           0 :       }
     261             :     }
     262          11 :   }
     263             : 
     264             :   public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
     265         103 :     try (Repository repo = repoManager.openRepository(allUsers)) {
     266         103 :       ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
     267         103 :       for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) {
     268           1 :         Integer id = Ints.tryParse(refPart);
     269           1 :         if (id == null) {
     270           0 :           continue;
     271             :         }
     272           1 :         Account.Id accountId = Account.id(id);
     273           1 :         builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
     274           1 :       }
     275         103 :       return builder.build();
     276           0 :     } catch (IOException e) {
     277           0 :       throw new StorageException(
     278           0 :           String.format("Get accounts that starred change %d failed", changeId.get()), e);
     279             :     }
     280             :   }
     281             : 
     282             :   public ImmutableSet<Change.Id> byAccountId(Account.Id accountId, String label) {
     283           4 :     try (Repository repo = repoManager.openRepository(allUsers)) {
     284           4 :       ImmutableSet.Builder<Change.Id> builder = ImmutableSet.builder();
     285           4 :       for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_STARRED_CHANGES)) {
     286           4 :         Account.Id currentAccountId = Account.Id.fromRef(ref.getName());
     287             :         // Skip all refs that don't correspond with accountId.
     288           4 :         if (currentAccountId == null || !currentAccountId.equals(accountId)) {
     289           4 :           continue;
     290             :         }
     291             :         // Skip all refs that don't contain the required label.
     292           4 :         StarRef starRef = readLabels(repo, ref.getName());
     293           4 :         if (!starRef.labels().contains(label)) {
     294           0 :           continue;
     295             :         }
     296             : 
     297             :         // Skip invalid change ids.
     298           4 :         Change.Id changeId = Change.Id.fromAllUsersRef(ref.getName());
     299           4 :         if (changeId == null) {
     300           0 :           continue;
     301             :         }
     302           4 :         builder.add(changeId);
     303           4 :       }
     304           4 :       return builder.build();
     305           0 :     } catch (IOException e) {
     306           0 :       throw new StorageException(
     307           0 :           String.format("Get starred changes for account %d failed", accountId.get()), e);
     308             :     }
     309             :   }
     310             : 
     311             :   public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
     312          11 :     List<ChangeData> changeData =
     313             :         queryProvider
     314          11 :             .get()
     315          11 :             .setRequestedFields(ChangeField.ID, ChangeField.STAR)
     316          11 :             .byLegacyChangeId(changeId);
     317          11 :     if (changeData.size() != 1) {
     318           0 :       throw new NoSuchChangeException(changeId);
     319             :     }
     320          11 :     return changeData.get(0).stars();
     321             :   }
     322             : 
     323             :   private static Set<String> getRefNames(Repository repo, String prefix) throws IOException {
     324         103 :     RefDatabase refDb = repo.getRefDatabase();
     325         103 :     return refDb.getRefsByPrefix(prefix).stream()
     326         103 :         .map(r -> r.getName().substring(prefix.length()))
     327         103 :         .collect(toSet());
     328             :   }
     329             : 
     330             :   public ObjectId getObjectId(Account.Id accountId, Change.Id changeId) {
     331           5 :     try (Repository repo = repoManager.openRepository(allUsers)) {
     332           5 :       Ref ref = repo.exactRef(RefNames.refsStarredChanges(changeId, accountId));
     333           5 :       return ref != null ? ref.getObjectId() : ObjectId.zeroId();
     334           0 :     } catch (IOException e) {
     335           0 :       logger.atSevere().withCause(e).log(
     336             :           "Getting star object ID for account %d on change %d failed",
     337           0 :           accountId.get(), changeId.get());
     338           0 :       return ObjectId.zeroId();
     339             :     }
     340             :   }
     341             : 
     342             :   public static StarRef readLabels(Repository repo, String refName) throws IOException {
     343         103 :     try (TraceTimer traceTimer =
     344         103 :         TraceContext.newTimer(
     345         103 :             "Read star labels", Metadata.builder().noteDbRefName(refName).build())) {
     346         103 :       Ref ref = repo.exactRef(refName);
     347         103 :       return readLabels(repo, ref);
     348             :     }
     349             :   }
     350             : 
     351             :   public static StarRef readLabels(Repository repo, Ref ref) throws IOException {
     352         103 :     if (ref == null) {
     353         103 :       return StarRef.MISSING;
     354             :     }
     355           8 :     try (TraceTimer traceTimer =
     356           8 :             TraceContext.newTimer(
     357           8 :                 String.format("Read star labels from %s (without ref lookup)", ref.getName()));
     358           8 :         ObjectReader reader = repo.newObjectReader()) {
     359           8 :       ObjectLoader obj = reader.open(ref.getObjectId(), Constants.OBJ_BLOB);
     360           8 :       return StarRef.create(
     361             :           ref,
     362           8 :           Splitter.on(CharMatcher.whitespace())
     363           8 :               .omitEmptyStrings()
     364           8 :               .split(new String(obj.getCachedBytes(Integer.MAX_VALUE), UTF_8)));
     365             :     }
     366             :   }
     367             : 
     368             :   public static ObjectId writeLabels(Repository repo, Collection<String> labels)
     369             :       throws IOException, InvalidLabelsException {
     370          10 :     validateLabels(labels);
     371          10 :     try (ObjectInserter oi = repo.newObjectInserter()) {
     372          10 :       ObjectId id =
     373          10 :           oi.insert(
     374             :               Constants.OBJ_BLOB,
     375          10 :               labels.stream().sorted().distinct().collect(joining("\n")).getBytes(UTF_8));
     376          10 :       oi.flush();
     377          10 :       return id;
     378             :     }
     379             :   }
     380             : 
     381             :   private static void validateLabels(Collection<String> labels) throws InvalidLabelsException {
     382          10 :     if (labels == null) {
     383           0 :       return;
     384             :     }
     385             : 
     386          10 :     NavigableSet<String> invalidLabels = new TreeSet<>();
     387          10 :     for (String label : labels) {
     388          10 :       if (CharMatcher.whitespace().matchesAnyOf(label)) {
     389           0 :         invalidLabels.add(label);
     390             :       }
     391          10 :     }
     392          10 :     if (!invalidLabels.isEmpty()) {
     393           0 :       throw new InvalidLabelsException(invalidLabels);
     394             :     }
     395          10 :   }
     396             : 
     397             :   private void updateLabels(
     398             :       Repository repo, String refName, ObjectId oldObjectId, Collection<String> labels)
     399             :       throws IOException, InvalidLabelsException {
     400          10 :     try (TraceTimer traceTimer =
     401          10 :             TraceContext.newTimer(
     402             :                 "Update star labels",
     403          10 :                 Metadata.builder().noteDbRefName(refName).resourceCount(labels.size()).build());
     404          10 :         RevWalk rw = new RevWalk(repo)) {
     405          10 :       RefUpdate u = repo.updateRef(refName);
     406          10 :       u.setExpectedOldObjectId(oldObjectId);
     407          10 :       u.setForceUpdate(true);
     408          10 :       u.setNewObjectId(writeLabels(repo, labels));
     409          10 :       u.setRefLogIdent(serverIdent.get());
     410          10 :       u.setRefLogMessage("Update star labels", true);
     411          10 :       RefUpdate.Result result = u.update(rw);
     412          10 :       switch (result) {
     413             :         case NEW:
     414             :         case FORCED:
     415             :         case NO_CHANGE:
     416             :         case FAST_FORWARD:
     417          10 :           gitRefUpdated.fire(allUsers, u, null);
     418          10 :           return;
     419             :         case LOCK_FAILURE:
     420           0 :           throw new LockFailureException(
     421           0 :               String.format("Update star labels on ref %s failed", refName), u);
     422             :         case IO_FAILURE:
     423             :         case NOT_ATTEMPTED:
     424             :         case REJECTED:
     425             :         case REJECTED_CURRENT_BRANCH:
     426             :         case RENAMED:
     427             :         case REJECTED_MISSING_OBJECT:
     428             :         case REJECTED_OTHER_REASON:
     429             :         default:
     430           0 :           throw new StorageException(
     431           0 :               String.format("Update star labels on ref %s failed: %s", refName, result.name()));
     432             :       }
     433             :     }
     434             :   }
     435             : 
     436             :   private void deleteRef(Repository repo, String refName, ObjectId oldObjectId) throws IOException {
     437           3 :     if (ObjectId.zeroId().equals(oldObjectId)) {
     438             :       // ref doesn't exist
     439           0 :       return;
     440             :     }
     441             : 
     442           3 :     try (TraceTimer traceTimer =
     443           3 :         TraceContext.newTimer(
     444           3 :             "Delete star labels", Metadata.builder().noteDbRefName(refName).build())) {
     445           3 :       RefUpdate u = repo.updateRef(refName);
     446           3 :       u.setForceUpdate(true);
     447           3 :       u.setExpectedOldObjectId(oldObjectId);
     448           3 :       u.setRefLogIdent(serverIdent.get());
     449           3 :       u.setRefLogMessage("Unstar change", true);
     450           3 :       RefUpdate.Result result = u.delete();
     451           3 :       switch (result) {
     452             :         case FORCED:
     453           3 :           gitRefUpdated.fire(allUsers, u, null);
     454           3 :           return;
     455             :         case LOCK_FAILURE:
     456           0 :           throw new LockFailureException(String.format("Delete star ref %s failed", refName), u);
     457             :         case NEW:
     458             :         case NO_CHANGE:
     459             :         case FAST_FORWARD:
     460             :         case IO_FAILURE:
     461             :         case NOT_ATTEMPTED:
     462             :         case REJECTED:
     463             :         case REJECTED_CURRENT_BRANCH:
     464             :         case RENAMED:
     465             :         case REJECTED_MISSING_OBJECT:
     466             :         case REJECTED_OTHER_REASON:
     467             :         default:
     468           0 :           throw new StorageException(
     469           0 :               String.format("Delete star ref %s failed: %s", refName, result.name()));
     470             :       }
     471             :     }
     472             :   }
     473             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750