| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.plugins.checks.db; |
| |
| import static com.google.gerrit.plugins.checks.CheckerRef.checksRef; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; |
| |
| import com.google.common.base.Throwables; |
| import com.google.common.collect.Maps; |
| import com.google.gerrit.exceptions.DuplicateKeyException; |
| import com.google.gerrit.git.RefUpdateUtil; |
| import com.google.gerrit.plugins.checks.Check; |
| import com.google.gerrit.plugins.checks.CheckKey; |
| import com.google.gerrit.plugins.checks.CheckUpdate; |
| import com.google.gerrit.plugins.checks.CheckerRef; |
| import com.google.gerrit.plugins.checks.CheckerUuid; |
| import com.google.gerrit.plugins.checks.Checkers; |
| import com.google.gerrit.plugins.checks.ChecksStorageUpdate; |
| import com.google.gerrit.plugins.checks.CombinedCheckStateCache; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.extensions.events.GitReferenceUpdated; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.notedb.ChangeNoteUtil; |
| import com.google.gerrit.server.update.RetryHelper; |
| import com.google.inject.assistedinject.Assisted; |
| import com.google.inject.assistedinject.AssistedInject; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.sql.Timestamp; |
| import java.util.Map; |
| import java.util.Optional; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.CommitBuilder; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectInserter; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.RefUpdate; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.notes.NoteMap; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| |
| public class NoteDbChecksUpdate implements ChecksStorageUpdate { |
| interface Factory { |
| NoteDbChecksUpdate create(IdentifiedUser currentUser); |
| |
| NoteDbChecksUpdate createWithServerIdent(); |
| } |
| |
| private enum Operation { |
| CREATE, |
| UPDATE |
| } |
| |
| private final GitRepositoryManager repoManager; |
| private final PersonIdent personIdent; |
| private final GitReferenceUpdated gitRefUpdated; |
| private final RetryHelper retryHelper; |
| private final ChangeNoteUtil noteUtil; |
| private final Optional<IdentifiedUser> currentUser; |
| private final Checkers checkers; |
| private final CombinedCheckStateCache combinedCheckStateCache; |
| |
| @AssistedInject |
| NoteDbChecksUpdate( |
| GitRepositoryManager repoManager, |
| GitReferenceUpdated gitRefUpdated, |
| RetryHelper retryHelper, |
| ChangeNoteUtil noteUtil, |
| Checkers checkers, |
| CombinedCheckStateCache combinedCheckStateCache, |
| @GerritPersonIdent PersonIdent personIdent) { |
| this( |
| repoManager, |
| gitRefUpdated, |
| retryHelper, |
| noteUtil, |
| checkers, |
| combinedCheckStateCache, |
| personIdent, |
| Optional.empty()); |
| } |
| |
| @AssistedInject |
| NoteDbChecksUpdate( |
| GitRepositoryManager repoManager, |
| GitReferenceUpdated gitRefUpdated, |
| RetryHelper retryHelper, |
| ChangeNoteUtil noteUtil, |
| Checkers checkers, |
| CombinedCheckStateCache combinedCheckStateCache, |
| @GerritPersonIdent PersonIdent personIdent, |
| @Assisted IdentifiedUser currentUser) { |
| this( |
| repoManager, |
| gitRefUpdated, |
| retryHelper, |
| noteUtil, |
| checkers, |
| combinedCheckStateCache, |
| personIdent, |
| Optional.of(currentUser)); |
| } |
| |
| private NoteDbChecksUpdate( |
| GitRepositoryManager repoManager, |
| GitReferenceUpdated gitRefUpdated, |
| RetryHelper retryHelper, |
| ChangeNoteUtil noteUtil, |
| Checkers checkers, |
| CombinedCheckStateCache combinedCheckStateCache, |
| @GerritPersonIdent PersonIdent personIdent, |
| Optional<IdentifiedUser> currentUser) { |
| this.repoManager = repoManager; |
| this.gitRefUpdated = gitRefUpdated; |
| this.retryHelper = retryHelper; |
| this.noteUtil = noteUtil; |
| this.checkers = checkers; |
| this.currentUser = currentUser; |
| this.personIdent = personIdent; |
| this.combinedCheckStateCache = combinedCheckStateCache; |
| } |
| |
| @Override |
| public Check createCheck(CheckKey checkKey, CheckUpdate checkUpdate) |
| throws DuplicateKeyException, IOException { |
| try { |
| return retryHelper |
| .pluginUpdate( |
| "createCheck", () -> upsertCheckInNoteDb(checkKey, checkUpdate, Operation.CREATE)) |
| .call(); |
| } catch (Exception e) { |
| Throwables.throwIfUnchecked(e); |
| Throwables.throwIfInstanceOf(e, DuplicateKeyException.class); |
| Throwables.throwIfInstanceOf(e, IOException.class); |
| throw new IOException(e); |
| } |
| } |
| |
| @Override |
| public Check updateCheck(CheckKey checkKey, CheckUpdate checkUpdate) throws IOException { |
| try { |
| return retryHelper |
| .pluginUpdate( |
| "updateCheck", () -> upsertCheckInNoteDb(checkKey, checkUpdate, Operation.UPDATE)) |
| .call(); |
| } catch (Exception e) { |
| Throwables.throwIfUnchecked(e); |
| Throwables.throwIfInstanceOf(e, IOException.class); |
| throw new IOException(e); |
| } |
| } |
| |
| private Check upsertCheckInNoteDb(CheckKey checkKey, CheckUpdate checkUpdate, Operation operation) |
| throws IOException, ConfigInvalidException, DuplicateKeyException { |
| if (operation == Operation.CREATE) { |
| assertCheckerIsPresent(checkKey.checkerUuid()); |
| } |
| |
| try (Repository repo = repoManager.openRepository(checkKey.repository()); |
| ObjectInserter objectInserter = repo.newObjectInserter(); |
| RevWalk rw = new RevWalk(repo)) { |
| Ref checkRef = repo.getRefDatabase().exactRef(checksRef(checkKey.patchSet().changeId())); |
| ObjectId parent = checkRef == null ? ObjectId.zeroId() : checkRef.getObjectId(); |
| CommitBuilder cb; |
| String message; |
| if (operation == Operation.CREATE) { |
| message = "Insert check " + checkKey.checkerUuid(); |
| cb = commitBuilder(message, parent); |
| } else { |
| message = "Update check " + checkKey.checkerUuid(); |
| cb = commitBuilder(message, parent); |
| } |
| |
| boolean dirty = |
| updateNotesMap(checkKey, checkUpdate, repo, rw, objectInserter, parent, cb, operation); |
| if (!dirty) { |
| // This update is a NoOp, so omit writing a commit with the same tree. |
| return readSingleCheck(checkKey, repo, rw, checkRef.getObjectId()); |
| } |
| |
| ObjectId newCommitId = objectInserter.insert(cb); |
| objectInserter.flush(); |
| |
| String refName = CheckerRef.checksRef(checkKey.patchSet().changeId()); |
| RefUpdate refUpdate = repo.updateRef(refName); |
| refUpdate.setExpectedOldObjectId(parent); |
| refUpdate.setNewObjectId(newCommitId); |
| refUpdate.setRefLogIdent(personIdent); |
| refUpdate.setRefLogMessage(message, false); |
| refUpdate.update(); |
| RefUpdateUtil.checkResult(refUpdate); |
| |
| combinedCheckStateCache.updateIfNecessary(checkKey.repository(), checkKey.patchSet()); |
| gitRefUpdated.fire( |
| checkKey.repository(), refUpdate, currentUser.map(user -> user.state()).orElse(null)); |
| return readSingleCheck(checkKey, repo, rw, newCommitId); |
| } |
| } |
| |
| private void assertCheckerIsPresent(CheckerUuid checkerUuid) |
| throws ConfigInvalidException, IOException { |
| checkers |
| .getChecker(checkerUuid) |
| .orElseThrow(() -> new IOException(String.format("checker %s not found", checkerUuid))); |
| } |
| |
| private boolean updateNotesMap( |
| CheckKey checkKey, |
| CheckUpdate checkUpdate, |
| Repository repo, |
| RevWalk rw, |
| ObjectInserter ins, |
| ObjectId curr, |
| CommitBuilder cb, |
| Operation operation) |
| throws ConfigInvalidException, IOException, DuplicateKeyException { |
| Ref patchSetRef = repo.exactRef(checkKey.patchSet().toRefName()); |
| if (patchSetRef == null) { |
| throw new IOException(String.format("patchset %s not found", checkKey.patchSet())); |
| } |
| ObjectId commitId = patchSetRef.getObjectId(); |
| |
| // Read a fresh copy of the notes map |
| Map<ObjectId, NoteDbCheckMap> newNotes = getRevisionNoteByCommitId(rw, curr); |
| if (!newNotes.containsKey(commitId)) { |
| if (operation == Operation.UPDATE) { |
| throw new IOException(String.format("checker %s not found", checkKey.checkerUuid())); |
| } |
| |
| newNotes.put(commitId, NoteDbCheckMap.empty()); |
| } |
| |
| NoteDbCheckMap checksForRevision = newNotes.get(commitId); |
| if (!checksForRevision.checks.containsKey(checkKey.checkerUuid().get())) { |
| if (operation == Operation.UPDATE) { |
| throw new IOException(String.format("checker %s not found", checkKey.checkerUuid())); |
| } |
| |
| // Create check |
| NoteDbCheck newCheck = NoteDbCheck.createInitialNoteDbCheck(checkUpdate); |
| newCheck.created = Timestamp.from(personIdent.getWhen().toInstant()); |
| newCheck.updated = newCheck.created; |
| checksForRevision.checks.put(checkKey.checkerUuid().get(), newCheck); |
| writeNotesMap(newNotes, cb, ins); |
| return true; |
| } else if (operation == Operation.CREATE) { |
| throw new DuplicateKeyException( |
| String.format("checker %s already exists", checkKey.checkerUuid())); |
| } |
| |
| // Update in place |
| NoteDbCheck modifiedCheck = checksForRevision.checks.get(checkKey.checkerUuid().get()); |
| boolean dirty = modifiedCheck.applyUpdate(checkUpdate); |
| if (!dirty) { |
| return false; |
| } |
| modifiedCheck.updated = Timestamp.from(personIdent.getWhen().toInstant()); |
| |
| writeNotesMap(newNotes, cb, ins); |
| return true; |
| } |
| |
| private void writeNotesMap( |
| Map<ObjectId, NoteDbCheckMap> notesMap, CommitBuilder cb, ObjectInserter ins) |
| throws IOException { |
| CheckRevisionNoteMap output = CheckRevisionNoteMap.emptyMap(); |
| for (Map.Entry<ObjectId, NoteDbCheckMap> e : notesMap.entrySet()) { |
| ObjectId id = e.getKey(); |
| byte[] data = toData(e.getValue()); |
| if (data.length != 0) { |
| ObjectId dataBlob = ins.insert(OBJ_BLOB, data); |
| output.noteMap.set(id, dataBlob); |
| } |
| } |
| cb.setTreeId(output.noteMap.writeTree(ins)); |
| } |
| |
| private Map<ObjectId, NoteDbCheckMap> getRevisionNoteByCommitId(RevWalk rw, ObjectId curr) |
| throws ConfigInvalidException, IOException { |
| CheckRevisionNoteMap existingNotes = getRevisionNoteMap(rw, curr); |
| |
| // Generate a list with all current checks keyed by patch set |
| Map<ObjectId, NoteDbCheckMap> newNotes = |
| Maps.newHashMapWithExpectedSize(existingNotes.revisionNotes.size()); |
| for (Map.Entry<ObjectId, CheckRevisionNote> e : existingNotes.revisionNotes.entrySet()) { |
| newNotes.put(e.getKey(), e.getValue().getOnlyEntity()); |
| } |
| return newNotes; |
| } |
| |
| private CheckRevisionNoteMap getRevisionNoteMap(RevWalk rw, ObjectId curr) |
| throws ConfigInvalidException, IOException { |
| if (curr.equals(ObjectId.zeroId())) { |
| return CheckRevisionNoteMap.emptyMap(); |
| } |
| NoteMap noteMap; |
| if (!curr.equals(ObjectId.zeroId())) { |
| noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr)); |
| } else { |
| noteMap = NoteMap.newEmptyMap(); |
| } |
| return CheckRevisionNoteMap.parseChecks( |
| noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap); |
| } |
| |
| private byte[] toData(NoteDbCheckMap map) throws IOException { |
| if (map.checks.isEmpty()) { |
| return new byte[0]; |
| } |
| |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| try (OutputStreamWriter osw = new OutputStreamWriter(out, UTF_8)) { |
| noteUtil.getChangeNoteJson().getGson().toJson(map, osw); |
| } |
| return out.toByteArray(); |
| } |
| |
| private CommitBuilder commitBuilder(String message, ObjectId parent) { |
| CommitBuilder cb = new CommitBuilder(); |
| if (!parent.equals(ObjectId.zeroId())) { |
| cb.setParentId(parent); |
| } |
| cb.setAuthor(personIdent); |
| cb.setCommitter(personIdent); |
| cb.setMessage(message); |
| return cb; |
| } |
| |
| private Check readSingleCheck(CheckKey checkKey, Repository repo, RevWalk rw, ObjectId tip) |
| throws IOException, ConfigInvalidException { |
| Ref patchSetRef = repo.exactRef(checkKey.patchSet().toRefName()); |
| if (patchSetRef == null) { |
| throw new IllegalStateException("patchset " + checkKey.patchSet() + " not found"); |
| } |
| ObjectId commitId = patchSetRef.getObjectId(); |
| Map<ObjectId, NoteDbCheckMap> newNotes = getRevisionNoteByCommitId(rw, tip); |
| if (!newNotes.containsKey(commitId)) { |
| throw new IllegalStateException("revision " + commitId.name() + " not found"); |
| } |
| Map<String, NoteDbCheck> checks = newNotes.get(commitId).checks; |
| String checkerUuidString = checkKey.checkerUuid().get(); |
| if (!checks.containsKey(checkerUuidString)) { |
| throw new IllegalStateException("checker " + checkerUuidString + " not found"); |
| } |
| return checks.get(checkerUuidString).toCheck(checkKey); |
| } |
| } |