From d5a71e9ca3d95330acdd858306c4f75ae0b01e58 Mon Sep 17 00:00:00 2001 From: Dave Borowitz Date: Mon, 29 Jun 2015 18:05:11 -0700 Subject: [PATCH] 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 --- .../transport/PushCertificateStoreTest.java | 304 ++++++++++++ .../eclipse/jgit/internal/JGitText.properties | 3 + .../org/eclipse/jgit/internal/JGitText.java | 3 + .../jgit/transport/PushCertificateStore.java | 443 ++++++++++++++++++ 4 files changed, 753 insertions(+) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java new file mode 100644 index 000000000..d2faaa774 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java @@ -0,0 +1,304 @@ +/* + * 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 org.eclipse.jgit.lib.ObjectId.zeroId; +import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD; +import static org.eclipse.jgit.lib.RefUpdate.Result.LOCK_FAILURE; +import static org.eclipse.jgit.lib.RefUpdate.Result.NEW; +import static org.eclipse.jgit.lib.RefUpdate.Result.NO_CHANGE; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.junit.Before; +import org.junit.Test; + +public class PushCertificateStoreTest { + private static final ObjectId ID1 = + ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + + private static final ObjectId ID2 = + ObjectId.fromString("badc0ffebadc0ffebadc0ffebadc0ffebadc0ffe"); + + private static PushCertificate newCert(String... updateLines) { + StringBuilder cert = new StringBuilder( + "certificate version 0.1\n" + + "pusher Dave Borowitz 1433954361 -0700\n" + + "pushee git://localhost/repo.git\n" + + "nonce 1433954361-bde756572d665bba81d8\n" + + "\n"); + for (String updateLine : updateLines) { + cert.append(updateLine).append('\n'); + } + cert.append( + "-----BEGIN PGP SIGNATURE-----\n" + + "DUMMY/SIGNATURE\n" + + "-----END PGP SIGNATURE-----\n"); + try { + return PushCertificateParser.fromReader(new InputStreamReader( + new ByteArrayInputStream(Constants.encode(cert.toString())))); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + + private static String command(ObjectId oldId, ObjectId newId, String ref) { + return oldId.name() + " " + newId.name() + " " + ref; + } + + private AtomicInteger ts = new AtomicInteger(1433954361); + private InMemoryRepository repo; + private PushCertificateStore store; + + @Before + public void setUp() throws Exception { + repo = new InMemoryRepository(new DfsRepositoryDescription("repo")); + store = newStore(); + } + + @Test + public void missingRef() throws Exception { + assertCerts("refs/heads/master"); + } + + @Test + public void saveNoChange() throws Exception { + assertEquals(NO_CHANGE, store.save()); + } + + @Test + public void saveOneCertOnOneRef() throws Exception { + PersonIdent ident = newIdent(); + PushCertificate addMaster = newCert( + command(zeroId(), ID1, "refs/heads/master")); + store.put(addMaster, ident); + assertEquals(NEW, store.save()); + assertCerts("refs/heads/master", addMaster); + assertCerts("refs/heads/branch"); + + try (RevWalk rw = new RevWalk(repo)) { + RevCommit c = rw.parseCommit(repo.resolve(PushCertificateStore.REF_NAME)); + rw.parseBody(c); + assertEquals("Store push certificate for refs/heads/master\n", + c.getFullMessage()); + assertEquals(ident, c.getAuthorIdent()); + assertEquals(ident, c.getCommitterIdent()); + } + } + + @Test + public void saveTwoCertsOnSameRefInTwoUpdates() throws Exception { + PushCertificate addMaster = newCert( + command(zeroId(), ID1, "refs/heads/master")); + store.put(addMaster, newIdent()); + assertEquals(NEW, store.save()); + PushCertificate updateMaster = newCert( + command(ID1, ID2, "refs/heads/master")); + store.put(updateMaster, newIdent()); + assertEquals(FAST_FORWARD, store.save()); + assertCerts("refs/heads/master", updateMaster, addMaster); + } + + @Test + public void saveTwoCertsOnSameRefInOneUpdate() throws Exception { + PersonIdent ident1 = newIdent(); + PersonIdent ident2 = newIdent(); + PushCertificate updateMaster = newCert( + command(ID1, ID2, "refs/heads/master")); + store.put(updateMaster, ident2); + PushCertificate addMaster = newCert( + command(zeroId(), ID1, "refs/heads/master")); + store.put(addMaster, ident1); + assertEquals(NEW, store.save()); + assertCerts("refs/heads/master", updateMaster, addMaster); + } + + @Test + public void saveTwoCertsOnDifferentRefsInOneUpdate() throws Exception { + PersonIdent ident1 = newIdent(); + PersonIdent ident3 = newIdent(); + PushCertificate addBranch = newCert( + command(zeroId(), ID1, "refs/heads/branch")); + store.put(addBranch, ident3); + PushCertificate addMaster = newCert( + command(zeroId(), ID1, "refs/heads/master")); + store.put(addMaster, ident1); + assertEquals(NEW, store.save()); + assertCerts("refs/heads/master", addMaster); + assertCerts("refs/heads/branch", addBranch); + } + + @Test + public void saveTwoCertsOnDifferentRefsInTwoUpdates() throws Exception { + PushCertificate addMaster = newCert( + command(zeroId(), ID1, "refs/heads/master")); + store.put(addMaster, newIdent()); + assertEquals(NEW, store.save()); + PushCertificate addBranch = newCert( + command(zeroId(), ID1, "refs/heads/branch")); + store.put(addBranch, newIdent()); + assertEquals(FAST_FORWARD, store.save()); + assertCerts("refs/heads/master", addMaster); + assertCerts("refs/heads/branch", addBranch); + } + + @Test + public void saveOneCertOnMultipleRefs() throws Exception { + PersonIdent ident = newIdent(); + PushCertificate addMasterAndBranch = newCert( + command(zeroId(), ID1, "refs/heads/branch"), + command(zeroId(), ID2, "refs/heads/master")); + store.put(addMasterAndBranch, ident); + assertEquals(NEW, store.save()); + assertCerts("refs/heads/master", addMasterAndBranch); + assertCerts("refs/heads/branch", addMasterAndBranch); + + try (RevWalk rw = new RevWalk(repo)) { + RevCommit c = rw.parseCommit(repo.resolve(PushCertificateStore.REF_NAME)); + rw.parseBody(c); + assertEquals("Store push certificate for 2 refs\n", c.getFullMessage()); + assertEquals(ident, c.getAuthorIdent()); + assertEquals(ident, c.getCommitterIdent()); + } + } + + @Test + public void changeRefFileToDirectory() throws Exception { + PushCertificate deleteRefsHeads = newCert( + command(ID1, zeroId(), "refs/heads")); + store.put(deleteRefsHeads, newIdent()); + PushCertificate addMaster = newCert( + command(zeroId(), ID1, "refs/heads/master")); + store.put(addMaster, newIdent()); + assertEquals(NEW, store.save()); + assertCerts("refs/heads", deleteRefsHeads); + assertCerts("refs/heads/master", addMaster); + } + + @Test + public void getBeforeSaveDoesNotIncludePending() throws Exception { + PushCertificate addMaster = newCert( + command(zeroId(), ID1, "refs/heads/master")); + store.put(addMaster, newIdent()); + assertEquals(NEW, store.save()); + + PushCertificate updateMaster = newCert( + command(ID1, ID2, "refs/heads/master")); + store.put(updateMaster, newIdent()); + + assertCerts("refs/heads/master", addMaster); + assertEquals(FAST_FORWARD, store.save()); + assertCerts("refs/heads/master", updateMaster, addMaster); + } + + @Test + public void lockFailure() throws Exception { + PushCertificateStore store1 = store; + PushCertificateStore store2 = newStore(); + store2.get("refs/heads/master"); + + PushCertificate addMaster = newCert( + command(zeroId(), ID1, "refs/heads/master")); + store1.put(addMaster, newIdent()); + assertEquals(NEW, store1.save()); + + PushCertificate addBranch = newCert( + command(zeroId(), ID2, "refs/heads/branch")); + store2.put(addBranch, newIdent()); + + assertEquals(LOCK_FAILURE, store2.save()); + // Reread ref after lock failure. + assertCerts(store2, "refs/heads/master", addMaster); + assertCerts(store2, "refs/heads/branch"); + + assertEquals(FAST_FORWARD, store2.save()); + assertCerts(store2, "refs/heads/master", addMaster); + assertCerts(store2, "refs/heads/branch", addBranch); + } + + private PersonIdent newIdent() { + return new PersonIdent( + "A U. Thor", "author@example.com", ts.getAndIncrement(), 0); + } + + private PushCertificateStore newStore() { + return new PushCertificateStore(repo); + } + + private void assertCerts(String refName, PushCertificate... expected) + throws Exception { + assertCerts(store, refName, expected); + assertCerts(newStore(), refName, expected); + } + + private static void assertCerts(PushCertificateStore store, String refName, + PushCertificate... expected) throws Exception { + List ex = Arrays.asList(expected); + PushCertificate first = !ex.isEmpty() ? ex.get(0) : null; + assertEquals(first, store.get(refName)); + assertEquals(ex, toList(store.getAll(refName))); + } + + private static List toList(Iterable it) { + List list = new ArrayList<>(); + for (T t : it) { + list.add(t); + } + return list; + } +} 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 bc8b8bfc0..0e0b4028b 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 86f277ac3..fdcfb8ecd 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 000000000..94677f9c4 --- /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. + *

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

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

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

+ * 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 {@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 getAll(final String refName) { + return new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + 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. + *

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

+ * One commit is created per certificate added with {@link + * #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. + * + * @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 pending) { + Collections.sort(pending, new Comparator() { + @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. 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(); + } +}