Browse Source

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] cf062b8bb6/contrib/hooks/repo-specific/save-push-signatures

Change-Id: Id3eb32416f969fba4b5e4d9c4b47053c564b0ccd
stable-4.1
Dave Borowitz 10 years ago
parent
commit
d5a71e9ca3
  1. 304
      org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
  2. 3
      org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
  3. 3
      org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
  4. 443
      org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java

304
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 <dborowitz@google.com> 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<PushCertificate> 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 <T> List<T> toList(Iterable<T> it) {
List<T> list = new ArrayList<>();
for (T t : it) {
list.add(t);
}
return list;
}
}

3
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 stashFailed=Stashing local changes did not successfully complete
stashResolveFailed=Reference ''{0}'' does not resolve to stashed commit stashResolveFailed=Reference ''{0}'' does not resolve to stashed commit
statelessRPCRequiresOptionToBeEnabled=stateless RPC requires {0} to be enabled 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 submoduleExists=Submodule ''{0}'' already exists in the index
submoduleParentRemoteUrlInvalid=Cannot remove segment from remote url ''{0}'' submoduleParentRemoteUrlInvalid=Cannot remove segment from remote url ''{0}''
submodulesNotSupported=Submodules are not supported submodulesNotSupported=Submodules are not supported

3
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java

@ -623,6 +623,9 @@ public class JGitText extends TranslationBundle {
/***/ public String stashFailed; /***/ public String stashFailed;
/***/ public String stashResolveFailed; /***/ public String stashResolveFailed;
/***/ public String statelessRPCRequiresOptionToBeEnabled; /***/ public String statelessRPCRequiresOptionToBeEnabled;
/***/ public String storePushCertMultipleRefs;
/***/ public String storePushCertOneRef;
/***/ public String storePushCertReflog;
/***/ public String submoduleExists; /***/ public String submoduleExists;
/***/ public String submodulesNotSupported; /***/ public String submodulesNotSupported;
/***/ public String submoduleParentRemoteUrlInvalid; /***/ public String submoduleParentRemoteUrlInvalid;

443
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();
}
}
Loading…
Cancel
Save