Line data Source code
1 : // Copyright (C) 2019 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.notedb; 16 : 17 : import static com.google.common.base.Preconditions.checkArgument; 18 : import static com.google.common.base.Preconditions.checkState; 19 : import static java.util.Objects.requireNonNull; 20 : 21 : import com.google.common.collect.ListMultimap; 22 : import com.google.gerrit.common.Nullable; 23 : import com.google.gerrit.entities.Project; 24 : import com.google.gerrit.exceptions.StorageException; 25 : import com.google.gerrit.server.git.GitRepositoryManager; 26 : import com.google.gerrit.server.git.InMemoryInserter; 27 : import com.google.gerrit.server.git.InsertedObject; 28 : import com.google.gerrit.server.update.ChainedReceiveCommands; 29 : import java.io.IOException; 30 : import java.util.Collection; 31 : import java.util.Map; 32 : import java.util.Objects; 33 : import java.util.Optional; 34 : import org.eclipse.jgit.lib.ObjectId; 35 : import org.eclipse.jgit.lib.ObjectInserter; 36 : import org.eclipse.jgit.lib.ObjectReader; 37 : import org.eclipse.jgit.lib.Repository; 38 : import org.eclipse.jgit.revwalk.RevWalk; 39 : import org.eclipse.jgit.transport.ReceiveCommand; 40 : 41 : /** 42 : * Wrapper around {@link Repository} that keeps track of related {@link ObjectInserter}s and other 43 : * objects that are jointly closed when invoking {@link #close}. 44 : */ 45 : class OpenRepo implements AutoCloseable { 46 : final Repository repo; 47 : final RevWalk rw; 48 : final ChainedReceiveCommands cmds; 49 : final ObjectInserter tempIns; 50 : 51 : private final InMemoryInserter inMemIns; 52 : @Nullable private final ObjectInserter finalIns; 53 : private final boolean close; 54 : 55 : /** Returns a {@link OpenRepo} wrapping around an open {@link Repository}. */ 56 : static OpenRepo open(GitRepositoryManager repoManager, Project.NameKey project) 57 : throws IOException { 58 32 : Repository repo = repoManager.openRepository(project); // Closed by OpenRepo#close. 59 32 : ObjectInserter ins = repo.newObjectInserter(); // Closed by OpenRepo#close. 60 32 : ObjectReader reader = ins.newReader(); // Not closed by OpenRepo#close. 61 32 : try (RevWalk rw = new RevWalk(reader)) { // Doesn't escape OpenRepo constructor. 62 32 : return new OpenRepo(repo, rw, ins, new ChainedReceiveCommands(repo), true) { 63 : @Override 64 : public void close() { 65 32 : reader.close(); 66 32 : super.close(); 67 32 : } 68 : }; 69 : } 70 : } 71 : 72 : OpenRepo( 73 : Repository repo, 74 : RevWalk rw, 75 : @Nullable ObjectInserter ins, 76 : ChainedReceiveCommands cmds, 77 110 : boolean close) { 78 110 : ObjectReader reader = rw.getObjectReader(); 79 110 : checkArgument( 80 110 : ins == null || reader.getCreatedFromInserter() == ins, 81 : "expected reader to be created from %s, but was %s", 82 : ins, 83 110 : reader.getCreatedFromInserter()); 84 110 : this.repo = requireNonNull(repo); 85 : 86 110 : this.inMemIns = new InMemoryInserter(rw.getObjectReader()); 87 110 : this.tempIns = inMemIns; 88 : 89 110 : this.rw = new RevWalk(tempIns.newReader()); 90 110 : this.finalIns = ins; 91 110 : this.cmds = requireNonNull(cmds); 92 110 : this.close = close; 93 110 : } 94 : 95 : @Override 96 : public void close() { 97 110 : rw.getObjectReader().close(); 98 110 : rw.close(); 99 110 : if (close) { 100 32 : if (finalIns != null) { 101 32 : finalIns.close(); 102 : } 103 32 : repo.close(); 104 : } 105 110 : } 106 : 107 : void flush() throws IOException { 108 109 : flushToFinalInserter(); 109 109 : finalIns.flush(); 110 109 : } 111 : 112 : void flushToFinalInserter() throws IOException { 113 109 : checkState(finalIns != null); 114 109 : for (InsertedObject obj : inMemIns.getInsertedObjects()) { 115 103 : finalIns.insert(obj.type(), obj.data().toByteArray()); 116 103 : } 117 109 : inMemIns.clear(); 118 109 : } 119 : 120 : private static <U extends AbstractChangeUpdate> boolean allowWrite( 121 : Collection<U> updates, ObjectId old) { 122 103 : if (!old.equals(ObjectId.zeroId())) { 123 90 : return true; 124 : } 125 103 : return updates.iterator().next().allowWriteToNewRef(); 126 : } 127 : 128 : <U extends AbstractChangeUpdate> void addUpdatesNoLimits(ListMultimap<String, U> all) 129 : throws IOException { 130 29 : addUpdates( 131 29 : all, Optional.empty() /* unlimited updates */, Optional.empty() /* unlimited patch sets */); 132 29 : } 133 : 134 : <U extends AbstractChangeUpdate> void addUpdates( 135 : ListMultimap<String, U> all, Optional<Integer> maxUpdates, Optional<Integer> maxPatchSets) 136 : throws IOException { 137 109 : for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) { 138 103 : String refName = e.getKey(); 139 103 : Collection<U> updates = e.getValue(); 140 103 : ObjectId old = cmds.get(refName).orElse(ObjectId.zeroId()); 141 : // Only actually write to the ref if one of the updates explicitly allows 142 : // us to do so, i.e. it is known to represent a new change. This avoids 143 : // writing partial change meta if the change hasn't been backfilled yet. 144 103 : if (!allowWrite(updates, old)) { 145 0 : continue; 146 : } 147 : 148 103 : int updateCount = 0; 149 103 : U first = updates.iterator().next(); 150 103 : if (maxUpdates.isPresent()) { 151 103 : checkState(first.getNotes() != null, "expected ChangeNotes on %s", first); 152 103 : updateCount = first.getNotes().getUpdateCount(); 153 : } 154 : 155 103 : ObjectId curr = old; 156 103 : for (U update : updates) { 157 103 : if (maxPatchSets.isPresent() && update.psId != null) { 158 : // Patch set IDs are assigned consecutively. Patch sets may have been deleted, but the ID 159 : // is still a good estimate and an upper bound. 160 103 : if (update.psId.get() > maxPatchSets.get()) { 161 1 : throw new LimitExceededException( 162 1 : String.format( 163 : "Change %d may not exceed %d patch sets. To continue working on this change, " 164 : + "recreate it with a new Change-Id, then abandon this one.", 165 1 : update.getId().get(), maxPatchSets.get())); 166 : } 167 : } 168 103 : if (update.isRootOnly() && !old.equals(ObjectId.zeroId())) { 169 1 : throw new StorageException("Given ChangeUpdate is only allowed on initial commit"); 170 : } 171 103 : ObjectId next = update.apply(rw, tempIns, curr); 172 103 : if (next == null) { 173 40 : continue; 174 : } 175 103 : if (maxUpdates.isPresent() 176 103 : && !Objects.equals(next, curr) 177 103 : && ++updateCount > maxUpdates.get() 178 2 : && !update.bypassMaxUpdates()) { 179 2 : throw new LimitExceededException( 180 2 : String.format( 181 : "Change %s may not exceed %d updates. It may still be abandoned, submitted and you can add/remove" 182 : + " reviewers to/from the attention-set. To continue working on this change, recreate it with a new" 183 : + " Change-Id, then abandon this one.", 184 2 : update.getId(), maxUpdates.get())); 185 : } 186 103 : curr = next; 187 103 : } 188 103 : if (!old.equals(curr)) { 189 103 : cmds.add(new ReceiveCommand(old, curr, refName)); 190 : } 191 103 : } 192 109 : } 193 : }