PushCertificateStore.java
- /*
- * Copyright (C) 2015, Google Inc. and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.transport;
- import static java.nio.charset.StandardCharsets.UTF_8;
- import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
- import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
- import static org.eclipse.jgit.lib.FileMode.TYPE_FILE;
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.io.Reader;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Map;
- import java.util.NoSuchElementException;
- import org.eclipse.jgit.dircache.DirCache;
- import org.eclipse.jgit.dircache.DirCacheEditor;
- import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
- import org.eclipse.jgit.dircache.DirCacheEntry;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.lib.BatchRefUpdate;
- import org.eclipse.jgit.lib.CommitBuilder;
- import org.eclipse.jgit.lib.Constants;
- import org.eclipse.jgit.lib.FileMode;
- import org.eclipse.jgit.lib.ObjectId;
- import org.eclipse.jgit.lib.ObjectInserter;
- import org.eclipse.jgit.lib.ObjectLoader;
- import org.eclipse.jgit.lib.ObjectReader;
- 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.revwalk.RevCommit;
- import org.eclipse.jgit.revwalk.RevWalk;
- import org.eclipse.jgit.treewalk.TreeWalk;
- import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
- import org.eclipse.jgit.treewalk.filter.PathFilter;
- import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
- import org.eclipse.jgit.treewalk.filter.TreeFilter;
- /**
- * Storage for recorded push certificates.
- * <p>
- * Push certificates are stored in a special ref {@code refs/meta/push-certs}.
- * The filenames in the tree are ref names followed by the special suffix
- * <code>@{cert}</code>, and the contents are the latest push cert affecting
- * that ref. The special suffix allows storing certificates for both refs/foo
- * and refs/foo/bar in case those both existed at some point.
- *
- * @since 4.1
- */
- public class PushCertificateStore implements AutoCloseable {
- /** Ref name storing push certificates. */
- static final String REF_NAME =
- Constants.R_REFS + "meta/push-certs"; //$NON-NLS-1$
- private static class PendingCert {
- PushCertificate cert;
- PersonIdent ident;
- Collection<ReceiveCommand> matching;
- PendingCert(PushCertificate cert, PersonIdent ident,
- Collection<ReceiveCommand> matching) {
- this.cert = cert;
- this.ident = ident;
- this.matching = matching;
- }
- }
- private final Repository db;
- private final List<PendingCert> pending;
- ObjectReader reader;
- RevCommit commit;
- /**
- * Create a new store backed by the given repository.
- *
- * @param db
- * the repository.
- */
- public PushCertificateStore(Repository db) {
- this.db = db;
- pending = new ArrayList<>();
- }
- /**
- * {@inheritDoc}
- * <p>
- * Close resources opened by this store.
- * <p>
- * If {@link #get(String)} was called, closes the cached object reader
- * created by that method. Does not close the underlying repository.
- */
- @Override
- public void close() {
- if (reader != null) {
- reader.close();
- reader = null;
- commit = null;
- }
- }
- /**
- * Get latest push certificate associated with a ref.
- * <p>
- * Lazily opens {@code refs/meta/push-certs} and reads from the repository as
- * necessary. The state is cached between calls to {@code get}; to reread the,
- * call {@link #close()} first.
- *
- * @param refName
- * the ref name to get the certificate for.
- * @return last certificate affecting the ref, or null if no cert was recorded
- * for the last update to this ref.
- * @throws java.io.IOException
- * if a problem occurred reading the repository.
- */
- public PushCertificate get(String refName) throws IOException {
- if (reader == null) {
- load();
- }
- try (TreeWalk tw = newTreeWalk(refName)) {
- return read(tw);
- }
- }
- /**
- * Iterate over all push certificates affecting a ref.
- * <p>
- * Only includes push certificates actually stored in the tree; see class
- * Javadoc for conditions where this might not include all push certs ever
- * seen for this ref.
- * <p>
- * The returned iterable may be iterated multiple times, and push certs will
- * be re-read from the current state of the store on each call to {@link
- * Iterable#iterator()}. However, method calls on the returned iterator may
- * fail if {@code save} or {@code close} is called on the enclosing store
- * during iteration.
- *
- * @param refName
- * the ref name to get certificates for.
- * @return iterable over certificates; must be fully iterated in order to
- * close resources.
- */
- public Iterable<PushCertificate> getAll(String refName) {
- return () -> new Iterator<PushCertificate>() {
- private final String path = pathName(refName);
- private PushCertificate next;
- private RevWalk rw;
- {
- try {
- if (reader == null) {
- load();
- }
- if (commit != null) {
- rw = new RevWalk(reader);
- rw.setTreeFilter(AndTreeFilter.create(
- PathFilterGroup.create(Collections
- .singleton(PathFilter.create(path))),
- TreeFilter.ANY_DIFF));
- rw.setRewriteParents(false);
- rw.markStart(rw.parseCommit(commit));
- } else {
- rw = null;
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- @Override
- public boolean hasNext() {
- try {
- if (next == null) {
- if (rw == null) {
- return false;
- }
- try {
- RevCommit c = rw.next();
- if (c != null) {
- try (TreeWalk tw = TreeWalk.forPath(
- rw.getObjectReader(), path,
- c.getTree())) {
- next = read(tw);
- }
- } else {
- next = null;
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- return next != null;
- } finally {
- if (next == null && rw != null) {
- rw.close();
- rw = null;
- }
- }
- }
- @Override
- public PushCertificate next() {
- hasNext();
- PushCertificate n = next;
- if (n == null) {
- throw new NoSuchElementException();
- }
- next = null;
- return n;
- }
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
- };
- }
- void load() throws IOException {
- close();
- reader = db.newObjectReader();
- Ref ref = db.getRefDatabase().exactRef(REF_NAME);
- if (ref == null) {
- // No ref, same as empty.
- return;
- }
- try (RevWalk rw = new RevWalk(reader)) {
- commit = rw.parseCommit(ref.getObjectId());
- }
- }
- static PushCertificate read(TreeWalk tw) throws IOException {
- if (tw == null || (tw.getRawMode(0) & TYPE_FILE) != TYPE_FILE) {
- return null;
- }
- ObjectLoader loader =
- tw.getObjectReader().open(tw.getObjectId(0), OBJ_BLOB);
- try (InputStream in = loader.openStream();
- Reader r = new BufferedReader(
- new InputStreamReader(in, UTF_8))) {
- return PushCertificateParser.fromReader(r);
- }
- }
- /**
- * Put a certificate to be saved to the store.
- * <p>
- * Writes the contents of this certificate for each ref mentioned. It is up
- * to the caller to ensure this certificate accurately represents the state
- * of the ref.
- * <p>
- * Pending certificates added to this method are not returned by
- * {@link #get(String)} and {@link #getAll(String)} until after calling
- * {@link #save()}.
- *
- * @param cert
- * certificate to store.
- * @param ident
- * identity for the commit that stores this certificate. Pending
- * certificates are sorted by identity timestamp during
- * {@link #save()}.
- */
- public void put(PushCertificate cert, PersonIdent ident) {
- put(cert, ident, null);
- }
- /**
- * Put a certificate to be saved to the store, matching a set of commands.
- * <p>
- * Like {@link #put(PushCertificate, PersonIdent)}, except a value is only
- * stored for a push certificate if there is a corresponding command in the
- * list that exactly matches the old/new values mentioned in the push
- * certificate.
- * <p>
- * Pending certificates added to this method are not returned by
- * {@link #get(String)} and {@link #getAll(String)} until after calling
- * {@link #save()}.
- *
- * @param cert
- * certificate to store.
- * @param ident
- * identity for the commit that stores this certificate. Pending
- * certificates are sorted by identity timestamp during
- * {@link #save()}.
- * @param matching
- * only store certs for the refs listed in this list whose values
- * match the commands in the cert.
- */
- public void put(PushCertificate cert, PersonIdent ident,
- Collection<ReceiveCommand> matching) {
- pending.add(new PendingCert(cert, ident, matching));
- }
- /**
- * Save pending certificates to the store.
- * <p>
- * One commit is created per certificate added with
- * {@link #put(PushCertificate, PersonIdent)}, in order of identity
- * timestamps, and a single ref update is performed.
- * <p>
- * The pending list is cleared if and only the ref update fails, which
- * allows for easy retries in case of lock failure.
- *
- * @return the result of attempting to update the ref.
- * @throws java.io.IOException
- * if there was an error reading from or writing to the
- * repository.
- */
- public RefUpdate.Result save() throws IOException {
- ObjectId newId = write();
- if (newId == null) {
- return RefUpdate.Result.NO_CHANGE;
- }
- try (ObjectInserter inserter = db.newObjectInserter()) {
- RefUpdate.Result result = updateRef(newId);
- switch (result) {
- case FAST_FORWARD:
- case NEW:
- case NO_CHANGE:
- pending.clear();
- break;
- default:
- break;
- }
- return result;
- } finally {
- close();
- }
- }
- /**
- * Save pending certificates to the store in an existing batch ref update.
- * <p>
- * One commit is created per certificate added with
- * {@link #put(PushCertificate, PersonIdent)}, in order of identity
- * timestamps, all commits are flushed, and a single command is added to the
- * batch.
- * <p>
- * The cached ref value and pending list are <em>not</em> cleared. If the
- * ref update succeeds, the caller is responsible for calling
- * {@link #close()} and/or {@link #clear()}.
- *
- * @param batch
- * update to save to.
- * @return whether a command was added to the batch.
- * @throws java.io.IOException
- * if there was an error reading from or writing to the
- * repository.
- */
- public boolean save(BatchRefUpdate batch) throws IOException {
- ObjectId newId = write();
- if (newId == null || newId.equals(commit)) {
- return false;
- }
- batch.addCommand(new ReceiveCommand(
- commit != null ? commit : ObjectId.zeroId(), newId, REF_NAME));
- return true;
- }
- /**
- * Clear pending certificates added with {@link #put(PushCertificate,
- * PersonIdent)}.
- */
- public void clear() {
- pending.clear();
- }
- private ObjectId write() throws IOException {
- if (pending.isEmpty()) {
- return null;
- }
- if (reader == null) {
- load();
- }
- sortPending(pending);
- ObjectId curr = commit;
- DirCache dc = newDirCache();
- try (ObjectInserter inserter = db.newObjectInserter()) {
- for (PendingCert pc : pending) {
- curr = saveCert(inserter, dc, pc, curr);
- }
- inserter.flush();
- return curr;
- }
- }
- private static void sortPending(List<PendingCert> pending) {
- Collections.sort(pending, (PendingCert a, PendingCert b) -> Long.signum(
- a.ident.getWhen().getTime() - b.ident.getWhen().getTime()));
- }
- private DirCache newDirCache() throws IOException {
- if (commit != null) {
- return DirCache.read(reader, commit.getTree());
- }
- return DirCache.newInCore();
- }
- private ObjectId saveCert(ObjectInserter inserter, DirCache dc,
- PendingCert pc, ObjectId curr) throws IOException {
- Map<String, ReceiveCommand> byRef;
- if (pc.matching != null) {
- byRef = new HashMap<>();
- for (ReceiveCommand cmd : pc.matching) {
- if (byRef.put(cmd.getRefName(), cmd) != null) {
- throw new IllegalStateException();
- }
- }
- } else {
- byRef = null;
- }
- DirCacheEditor editor = dc.editor();
- String certText = pc.cert.toText() + pc.cert.getSignature();
- final ObjectId certId = inserter.insert(OBJ_BLOB, certText.getBytes(UTF_8));
- boolean any = false;
- for (ReceiveCommand cmd : pc.cert.getCommands()) {
- if (byRef != null && !commandsEqual(cmd, byRef.get(cmd.getRefName()))) {
- continue;
- }
- any = true;
- editor.add(new PathEdit(pathName(cmd.getRefName())) {
- @Override
- public void apply(DirCacheEntry ent) {
- ent.setFileMode(FileMode.REGULAR_FILE);
- ent.setObjectId(certId);
- }
- });
- }
- if (!any) {
- return curr;
- }
- editor.finish();
- CommitBuilder cb = new CommitBuilder();
- cb.setAuthor(pc.ident);
- cb.setCommitter(pc.ident);
- cb.setTreeId(dc.writeTree(inserter));
- if (curr != null) {
- cb.setParentId(curr);
- } else {
- cb.setParentIds(Collections.<ObjectId> emptyList());
- }
- cb.setMessage(buildMessage(pc.cert));
- return inserter.insert(OBJ_COMMIT, cb.build());
- }
- private static boolean commandsEqual(ReceiveCommand c1, ReceiveCommand c2) {
- if (c1 == null || c2 == null) {
- return c1 == c2;
- }
- return c1.getRefName().equals(c2.getRefName())
- && c1.getOldId().equals(c2.getOldId())
- && c1.getNewId().equals(c2.getNewId());
- }
- private RefUpdate.Result updateRef(ObjectId newId) throws IOException {
- RefUpdate ru = db.updateRef(REF_NAME);
- ru.setExpectedOldObjectId(commit != null ? commit : ObjectId.zeroId());
- ru.setNewObjectId(newId);
- ru.setRefLogIdent(pending.get(pending.size() - 1).ident);
- ru.setRefLogMessage(JGitText.get().storePushCertReflog, false);
- try (RevWalk rw = new RevWalk(reader)) {
- return ru.update(rw);
- }
- }
- private TreeWalk newTreeWalk(String refName) throws IOException {
- if (commit == null) {
- return null;
- }
- return TreeWalk.forPath(reader, pathName(refName), commit.getTree());
- }
- static String pathName(String refName) {
- return refName + "@{cert}"; //$NON-NLS-1$
- }
- private static String buildMessage(PushCertificate cert) {
- StringBuilder sb = new StringBuilder();
- if (cert.getCommands().size() == 1) {
- sb.append(MessageFormat.format(
- JGitText.get().storePushCertOneRef,
- cert.getCommands().get(0).getRefName()));
- } else {
- sb.append(MessageFormat.format(
- JGitText.get().storePushCertMultipleRefs,
- Integer.valueOf(cert.getCommands().size())));
- }
- return sb.append('\n').toString();
- }
- }