summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit
diff options
context:
space:
mode:
authorDave Borowitz <dborowitz@google.com>2015-06-29 18:05:11 -0700
committerDave Borowitz <dborowitz@google.com>2015-07-10 13:16:37 -0700
commitd5a71e9ca3d95330acdd858306c4f75ae0b01e58 (patch)
treeac0290a03ee6fa398644ffa08236df5db1dfa3fe /org.eclipse.jgit
parent217b2a7cc5366491be5317d20f3f3c1b6e3475bf (diff)
downloadjgit-d5a71e9ca3d95330acdd858306c4f75ae0b01e58.tar.gz
jgit-d5a71e9ca3d95330acdd858306c4f75ae0b01e58.zip
Store push certificates in refs/meta/push-certs
Inspired by a proposal from gitolite[1], where we store a file in a tree for each ref name, and the contents of the file is the latest push cert to affect that ref. The main modification from that proposal (other than lacking the out-of-git batching) is to append "@{cert}" to filenames, which allows storing certificates for both refs/foo and refs/foo/bar. Those refnames cannot coexist at the same time in a repository, but we do not want to discard the push certificate responsible for deleting the ref, which we would have to do if refs/foo in the push cert tree changed from a tree to a blob. The "@{cert}" syntax is at least somewhat consistent with gitrevisions(7) wherein @{...} describe operators on ref names. As we cannot (currently) atomically update the push cert ref with the refs that were updated, this operation is inherently racy. Kick the can down the road by pushing this burden on callers. [1] https://github.com/sitaramc/gitolite/blob/cf062b8bb6b21a52f7c5002d33fbc950762c1aa7/contrib/hooks/repo-specific/save-push-signatures Change-Id: Id3eb32416f969fba4b5e4d9c4b47053c564b0ccd
Diffstat (limited to 'org.eclipse.jgit')
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java443
3 files changed, 449 insertions, 0 deletions
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index bc8b8bfc05..0e0b4028b0 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -564,6 +564,9 @@ stashDropMissingReflog=Stash reflog does not contain entry ''{0}''
stashFailed=Stashing local changes did not successfully complete
stashResolveFailed=Reference ''{0}'' does not resolve to stashed commit
statelessRPCRequiresOptionToBeEnabled=stateless RPC requires {0} to be enabled
+storePushCertMultipleRefs=Store push certificate for {0} refs
+storePushCertOneRef=Store push certificate for {0}
+storePushCertReflog=Store push certificate
submoduleExists=Submodule ''{0}'' already exists in the index
submoduleParentRemoteUrlInvalid=Cannot remove segment from remote url ''{0}''
submodulesNotSupported=Submodules are not supported
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 86f277ac39..fdcfb8ecdb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -623,6 +623,9 @@ public class JGitText extends TranslationBundle {
/***/ public String stashFailed;
/***/ public String stashResolveFailed;
/***/ public String statelessRPCRequiresOptionToBeEnabled;
+ /***/ public String storePushCertMultipleRefs;
+ /***/ public String storePushCertOneRef;
+ /***/ public String storePushCertReflog;
/***/ public String submoduleExists;
/***/ public String submodulesNotSupported;
/***/ public String submoduleParentRemoteUrlInvalid;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java
new file mode 100644
index 0000000000..94677f9c4b
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+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.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+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.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 {
+ private PushCertificate cert;
+ private PersonIdent ident;
+
+ private PendingCert(PushCertificate cert, PersonIdent ident) {
+ this.cert = cert;
+ this.ident = ident;
+ }
+ }
+
+ private final Repository db;
+ private final List<PendingCert> pending;
+ private ObjectReader reader;
+ private 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<>();
+ }
+
+ /**
+ * 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.
+ */
+ 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 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(final String refName) {
+ return new Iterable<PushCertificate>() {
+ @Override
+ public Iterator<PushCertificate> iterator() {
+ 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();
+ }
+ };
+ }
+ };
+ }
+
+ private 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());
+ }
+ }
+
+ private 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) {
+ pending.add(new PendingCert(cert, ident));
+ }
+
+ /**
+ * 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 IOException
+ * if there was an error reading from or writing to the
+ * repository.
+ */
+ public RefUpdate.Result save() throws IOException {
+ if (pending.isEmpty()) {
+ return RefUpdate.Result.NO_CHANGE;
+ }
+ 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();
+ RefUpdate.Result result = updateRef(curr);
+ switch (result) {
+ case FAST_FORWARD:
+ case NEW:
+ case NO_CHANGE:
+ pending.clear();
+ break;
+ default:
+ break;
+ }
+ return result;
+ } finally {
+ close();
+ }
+ }
+
+ private static void sortPending(List<PendingCert> pending) {
+ Collections.sort(pending, new Comparator<PendingCert>() {
+ @Override
+ public int compare(PendingCert a, PendingCert b) {
+ return Long.signum(
+ a.ident.getWhen().getTime() - b.ident.getWhen().getTime());
+ }
+ });
+ }
+
+ private DirCache newDirCache() throws IOException {
+ DirCache dc = DirCache.newInCore();
+ if (commit != null) {
+ DirCacheBuilder b = dc.builder();
+ b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, commit.getTree());
+ b.finish();
+ }
+ return dc;
+ }
+
+ private ObjectId saveCert(ObjectInserter inserter, DirCache dc,
+ PendingCert pc, ObjectId curr) throws IOException {
+ DirCacheEditor editor = dc.editor();
+ String certText = pc.cert.toText() + pc.cert.getSignature();
+ final ObjectId certId = inserter.insert(OBJ_BLOB, certText.getBytes(UTF_8));
+ for (ReceiveCommand cmd : pc.cert.getCommands()) {
+ editor.add(new PathEdit(pathName(cmd.getRefName())) {
+ @Override
+ public void apply(DirCacheEntry ent) {
+ ent.setFileMode(FileMode.REGULAR_FILE);
+ ent.setObjectId(certId);
+ }
+ });
+ }
+ 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 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());
+ }
+
+ private 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();
+ }
+}