/*
 * 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.

Push certificates are stored in a special ref refs/meta/push-certs. The filenames in the tree are ref names followed by the special suffix @{cert}, 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
/** * 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.
/** 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.
Params:
  • db – the repository.
/** * Create a new store backed by the given repository. * * @param db * the repository. */
public PushCertificateStore(Repository db) { this.db = db; pending = new ArrayList<>(); }
{@inheritDoc}

Close resources opened by this store.

If get(String) was called, closes the cached object reader created by that method. Does not close the underlying repository.

/** * {@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.

Lazily opens refs/meta/push-certs and reads from the repository as necessary. The state is cached between calls to get; to reread the, call close() first.

Params:
  • refName – the ref name to get the certificate for.
Throws:
  • IOException – if a problem occurred reading the repository.
Returns:last certificate affecting the ref, or null if no cert was recorded for the last update to this ref.
/** * 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.

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.

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 Iterable.iterator(). However, method calls on the returned iterator may fail if save or close is called on the enclosing store during iteration.

Params:
  • refName – the ref name to get certificates for.
Returns:iterable over certificates; must be fully iterated in order to close resources.
/** * 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.

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.

Pending certificates added to this method are not returned by get(String) and getAll(String) until after calling save().

Params:
  • cert – certificate to store.
  • ident – identity for the commit that stores this certificate. Pending certificates are sorted by identity timestamp during save().
/** * 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.

Like 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.

Pending certificates added to this method are not returned by get(String) and getAll(String) until after calling save().

Params:
  • cert – certificate to store.
  • ident – identity for the commit that stores this certificate. Pending certificates are sorted by identity timestamp during save().
  • matching – only store certs for the refs listed in this list whose values match the commands in the cert.
/** * 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.

One commit is created per certificate added with put(PushCertificate, PersonIdent), in order of identity timestamps, and a single ref update is performed.

The pending list is cleared if and only the ref update fails, which allows for easy retries in case of lock failure.

Throws:
  • IOException – if there was an error reading from or writing to the repository.
Returns:the result of attempting to update the ref.
/** * 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.

One commit is created per certificate added with put(PushCertificate, PersonIdent), in order of identity timestamps, all commits are flushed, and a single command is added to the batch.

The cached ref value and pending list are not cleared. If the ref update succeeds, the caller is responsible for calling close() and/or clear().

Params:
  • batch – update to save to.
Throws:
  • IOException – if there was an error reading from or writing to the repository.
Returns:whether a command was added to the batch.
/** * 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 put(PushCertificate, PersonIdent).
/** * 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(); } }