When putting a key/value pair then a GitDbBranch is returned

This commit is contained in:
Paul Campbell 2018-06-12 22:32:06 +01:00
parent 265bd61a11
commit 02b5ba34bc
5 changed files with 357 additions and 90 deletions

View file

@ -38,28 +38,32 @@ public interface GitDB {
/**
* Initialise a new local gitdb.
*
* @param dbDir the path to initialise the local repo in
* @param dbDir the path to initialise the local repo in
* @param userName the user name
* @param userEmailAddress the user email address
* @return a GitDB instance for the created local gitdb
* @throws IOException if there {@code dbDir} is a file or a non-empty directory
*/
static GitDB initLocal(final Path dbDir) throws IOException {
static GitDB initLocal(final Path dbDir, final String userName, final String userEmailAddress) throws IOException {
return new GitDBLocal(
dbDir.toFile()
dbDir.toFile(), userName, userEmailAddress
);
}
/**
* Open an existing local gitdb.
*
* @param dbDir the path to open as a local repo
* @param dbDir the path to open as a local repo
* @param userName the user name
* @param userEmailAddress the user email address
* @return a GitDB instance for the local gitdb
*/
static GitDBLocal openLocal(final Path dbDir) {
static GitDBLocal openLocal(final Path dbDir, final String userName, final String userEmailAddress) {
try {
return Optional.of(Git.open(dbDir.toFile()))
.map(Git::getRepository)
.filter(Repository::isBare)
.map(GitDBLocal::new)
.map(repository -> new GitDBLocal(repository, userName, userEmailAddress))
.orElseThrow(() -> new InvalidRepositoryException("Not a bare repo", dbDir));
} catch (IOException e) {
throw new InvalidRepositoryException("Error opening repository", dbDir, e);

View file

@ -23,8 +23,12 @@ package net.kemitix.gitdb;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
@ -35,27 +39,80 @@ import java.util.Optional;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class GitDBBranch {
private final Ref ref;
private static final String KEY_PREFIX = "key:";
private final Ref branchRef;
private final GitDBRepo gitDBRepo;
private final String userName;
private final String userEmailAddress;
/**
* Create a new instance of GitDBBranch for the Ref.
*
* @param ref the Ref
* @param ref the Ref
* @param gitDBRepo the GitDBRepo
* @param userName the user name
* @param userEmailAddress the user email address
* @return a GitDBBranch
*/
public static GitDBBranch withRef(final Ref ref) {
return new GitDBBranch(ref);
public static GitDBBranch withRef(
final Ref ref,
final GitDBRepo gitDBRepo,
final String userName,
final String userEmailAddress
) {
return new GitDBBranch(ref, gitDBRepo, userName, userEmailAddress);
}
/**
* Lookup a value for the key.
*
* @param key the key to lookup
* @param valueClass the expected class of the value
* @param <T> the Class of the value
* @return an Optional containing the value, if it exists, or empty if not
* @throws IOException if there was an error reading the value
*/
public <T> Optional<T> get(final String key, final Class<T> valueClass) {
return Optional.empty();
public Optional<String> get(final String key) throws IOException {
return gitDBRepo.readValue(branchRef, KEY_PREFIX + key)
.map(String::new);
}
/**
* Put a value into the store for the key.
*
* @param key the key to place the value under
* @param value the value (must be Serializable)
* @return an updated branch containing the new key/value
* @throws IOException if there was an error writing the value
*/
public GitDBBranch put(final String key, final String value) throws IOException {
final ObjectId objectId = insertBlob(value.getBytes(StandardCharsets.UTF_8));
final ObjectId treeId = insertTree(KEY_PREFIX + key, objectId);
final String commitMessage = String.format("Add key [%s] = [%s]", key, value);
final ObjectId commitId = insertCommit(treeId, commitMessage);
return updateBranch(commitId);
}
private ObjectId insertBlob(final byte[] blob) throws IOException {
return gitDBRepo.insertBlob(blob);
}
private ObjectId insertTree(final String key, final ObjectId valueId) throws IOException {
return gitDBRepo.insertTree(branchRef, key, valueId);
}
private ObjectId insertCommit(
final ObjectId treeId,
final String message
) throws IOException {
return gitDBRepo.insertCommit(treeId, message, userName, userEmailAddress, head());
}
private AnyObjectId head() {
return branchRef.getObjectId();
}
private GitDBBranch updateBranch(final ObjectId commitId) throws IOException {
final Ref updatedRef = gitDBRepo.writeHead(branchRef.getName(), commitId);
return GitDBBranch.withRef(updatedRef, gitDBRepo, userName, userEmailAddress);
}
}

View file

@ -42,32 +42,44 @@ import java.util.Optional;
class GitDBLocal implements GitDB {
private final Repository repository;
private final String userName;
private final String userEmailAddress;
/**
* Create a new GitDB instance, while initialising a new git repo.
*
* @param dbDir the path to instantiate the git repo in
* @param dbDir the path to instantiate the git repo in
* @param userName the user name
* @param userEmailAddress the user email address
* @throws IOException if there {@code dbDir} is a file or a non-empty directory
*/
GitDBLocal(final File dbDir) throws IOException {
validateDbDir(dbDir);
this.repository = initRepo(dbDir);
GitDBLocal(
final File dbDir,
final String userName,
final String userEmailAddress
) throws IOException {
this(GitDBLocal.initRepo(validDbDir(dbDir)), userName, userEmailAddress);
}
/**
* Create a new GitDB instance using the Git repo.
*
* @param repository the Git repository
* @param repository the Git repository
* @param userName the user name
* @param userEmailAddress the user email address
*/
GitDBLocal(final Repository repository) {
GitDBLocal(final Repository repository, final String userName, final String userEmailAddress) {
this.repository = repository;
this.userName = userName;
this.userEmailAddress = userEmailAddress;
}
private void validateDbDir(final File dbDir) throws IOException {
private static File validDbDir(final File dbDir) throws IOException {
verifyIsNotAFile(dbDir);
if (dbDir.exists()) {
verifyIsEmpty(dbDir);
}
return dbDir;
}
private static void verifyIsEmpty(final File dbDir) throws IOException {
@ -82,6 +94,12 @@ class GitDBLocal implements GitDB {
}
}
@Override
public Optional<GitDBBranch> branch(final String name) throws IOException {
return Optional.ofNullable(repository.findRef(name))
.map(ref -> GitDBBranch.withRef(ref, GitDBRepo.in(repository), userName, userEmailAddress));
}
private static Repository initRepo(final File dbDir) throws IOException {
dbDir.mkdirs();
final Repository repository = RepositoryCache.FileKey.exact(dbDir, FS.DETECTED).open(false);
@ -91,58 +109,30 @@ class GitDBLocal implements GitDB {
}
private static void createInitialBranchOnMaster(final Repository repository) throws IOException {
// create empty file
final ObjectId objectId = insertAnEmptyBlob(repository);
// create tree
final ObjectId treeId = insertTree(repository, objectId);
// create commit
final ObjectId commitId = insertCommit(repository, treeId);
// create branch
writeBranch(repository, commitId, "master");
final GitDBRepo repo = GitDBRepo.in(repository);
final ObjectId objectId = repo.insertBlob(new byte[0]);
final ObjectId treeId = repo.insertNewTree("isGitDB", objectId);
final ObjectId commitId = repo.insertCommit(
treeId,
"Initialise GitDB v1",
"GitDB",
"pcampbell@kemitix.net",
ObjectId.zeroId());
createBranch(repository, commitId, "master");
}
private static void writeBranch(
private static void createBranch(
final Repository repository,
final ObjectId commitId,
final String branchName
) throws IOException {
final Path branchRefPath =
repository.getDirectory().toPath().resolve("refs/heads/" + branchName).toAbsolutePath();
final Path branchRefPath = repository
.getDirectory()
.toPath()
.resolve("refs/heads/" + branchName)
.toAbsolutePath();
final byte[] commitIdBytes = commitId.name().getBytes(StandardCharsets.UTF_8);
Files.write(branchRefPath, commitIdBytes);
}
private static ObjectId insertCommit(
final Repository repository,
final ObjectId treeId
) throws IOException {
final CommitBuilder commitBuilder = new CommitBuilder();
commitBuilder.setTreeId(treeId);
commitBuilder.setMessage("Initialise GitDB v1");
final PersonIdent ident = new PersonIdent("GitDB", "pcampbell@kemitix.net");
commitBuilder.setAuthor(ident);
commitBuilder.setCommitter(ident);
commitBuilder.setParentId(ObjectId.zeroId());
return repository.getObjectDatabase().newInserter().insert(commitBuilder);
}
private static ObjectId insertTree(
final Repository repository,
final ObjectId objectId
) throws IOException {
final TreeFormatter treeFormatter = new TreeFormatter();
treeFormatter.append("isGitDB", FileMode.REGULAR_FILE, objectId);
return repository.getObjectDatabase().newInserter().insert(treeFormatter);
}
private static ObjectId insertAnEmptyBlob(final Repository repository) throws IOException {
return repository.getObjectDatabase().newInserter().insert(Constants.OBJ_BLOB, new byte[0]);
}
@Override
public Optional<GitDBBranch> branch(final String name) throws IOException {
return Optional.ofNullable(repository.findRef(name))
.map(GitDBBranch::withRef);
}
}

View file

@ -0,0 +1,200 @@
/*
The MIT License (MIT)
Copyright (c) 2018 Paul Campbell
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.kemitix.gitdb;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.eclipse.jgit.lib.*;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
/**
* Wrapper for interacting with the GitDB Repository.
*
* @author Paul Campbell (pcampbell@kemitix.net)
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class GitDBRepo {
private final Repository repository;
/**
* Create a GitDBRepo wrapper for the Repository.
*
* @param repository the repository to wrap
* @return the GitDBRepo wrapper
*/
public static GitDBRepo in(final Repository repository) {
return new GitDBRepo(repository);
}
/**
* Insert a blob into the store, returning its unique id.
*
* @param blob content of the blob
* @return the id of the blob
* @throws IOException the blob could not be stored
*/
ObjectId insertBlob(final byte[] blob) throws IOException {
return repository.getObjectDatabase().newInserter().insert(Constants.OBJ_BLOB, blob);
}
/**
* Insert a new, empty tree into the store, returning its unique id.
*
* @param key the key to insert
* @param valueId id of the value
* @return the id of the inserted tree
* @throws IOException the tree could not be stored
*/
ObjectId insertNewTree(
final String key,
final ObjectId valueId
) throws IOException {
final TreeFormatter treeFormatter = new TreeFormatter();
return writeTree(key, valueId, treeFormatter);
}
/**
* Insert a tree into the store, copying the exiting tree from the branch, returning its new unique id.
*
* @param branchRef the branch to copy the tree from
* @param key the key to insert
* @param valueId id of the value
* @return the id of the inserted tree
* @throws IOException the tree could not be stored
*/
ObjectId insertTree(
final Ref branchRef,
final String key,
final ObjectId valueId
) throws IOException {
return writeTree(key, valueId, treeFormatterForBranch(branchRef));
}
private ObjectId writeTree(
final String key,
final ObjectId valueId,
final TreeFormatter treeFormatter
) throws IOException {
treeFormatter.append(key, FileMode.REGULAR_FILE, valueId);
return repository.getObjectDatabase().newInserter().insert(treeFormatter);
}
/**
* Insert a commit into the store, returning its unique id.
*
* @param treeId id of the tree
* @param message the message
* @param userName the user name
* @param userEmailAddress the user email address
* @param parent the commit to link to as parent
* @return the id of the commit
* @throws IOException the commit could not be stored
*/
ObjectId insertCommit(
final ObjectId treeId,
final String message,
final String userName,
final String userEmailAddress,
final AnyObjectId parent
) throws IOException {
final CommitBuilder commitBuilder = new CommitBuilder();
commitBuilder.setTreeId(treeId);
commitBuilder.setMessage(message);
final PersonIdent ident = new PersonIdent(userName, userEmailAddress);
commitBuilder.setAuthor(ident);
commitBuilder.setCommitter(ident);
commitBuilder.setParentId(parent);
return repository.getObjectDatabase().newInserter().insert(commitBuilder);
}
/**
* Updates the branch to point to the new commit.
*
* @param branchName the branch to update
* @param commitId the commit to point the branch at
* @return the Ref of the updated branch
* @throws IOException if there was an error writing the branch
*/
Ref writeHead(
final String branchName,
final ObjectId commitId
) throws IOException {
final Path branchRefPath = repository
.getDirectory()
.toPath()
.resolve(branchName)
.toAbsolutePath();
final byte[] commitIdBytes = commitId.name().getBytes(StandardCharsets.UTF_8);
Files.write(branchRefPath, commitIdBytes);
return repository.findRef(branchName);
}
/**
* Reads a value from the branch with the given key.
*
* @param branchRef the branch to select from
* @param key the key to get the value for
* @return an Optional containing the value if found, or empty
* @throws IOException if there was an error reading the value
*/
Optional<byte[]> readValue(
final Ref branchRef,
final String key
) throws IOException {
try (TreeWalk treeWalk = getTreeWalk(branchRef)) {
treeWalk.setFilter(PathFilter.create(key));
if (treeWalk.next()) {
return Optional.of(repository.open(treeWalk.getObjectId(0), Constants.OBJ_BLOB).getBytes());
}
}
return Optional.empty();
}
private TreeFormatter treeFormatterForBranch(final Ref branchRef) throws IOException {
final TreeFormatter treeFormatter = new TreeFormatter();
try (TreeWalk treeWalk = getTreeWalk(branchRef)) {
while (treeWalk.next()) {
treeFormatter.append(
treeWalk.getNameString(),
new RevWalk(repository).lookupBlob(treeWalk.getObjectId(0)));
}
}
return treeFormatter;
}
private TreeWalk getTreeWalk(final Ref branchRef) throws IOException {
final TreeWalk treeWalk = new TreeWalk(repository);
treeWalk.addTree(new RevWalk(repository).parseCommit(branchRef.getObjectId()).getTree());
treeWalk.setRecursive(false);
return treeWalk;
}
}

View file

@ -1,11 +1,11 @@
package net.kemitix.gitdb;
import org.assertj.core.api.Assumptions;
import org.assertj.core.api.WithAssertions;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ -19,10 +19,8 @@ import static org.assertj.core.api.Assumptions.assumeThat;
@ExtendWith(MockitoExtension.class)
class GitDBTest implements WithAssertions {
// private final Branch master = Branch.name("master");
// private final Message message = Message.message(UUID.randomUUID().toString());
// private final Key key = Key.name(UUID.randomUUID().toString());
// private final Author author = Author.name("junit", "gitdb@kemitix.net");
private String userName = "user name";
private String userEmailAddress = "user@email.com";
// When initialising a repo in a dir that doesn't exist then a bare repo is created
@Test
@ -30,7 +28,7 @@ class GitDBTest implements WithAssertions {
//given
final Path dir = dirDoesNotExist();
//when
final GitDB gitDB = GitDB.initLocal(dir);
final GitDB gitDB = GitDB.initLocal(dir, userName, userEmailAddress);
//then
assertThat(gitDB).isNotNull();
assertThatIsBareRepo(dir);
@ -57,7 +55,7 @@ class GitDBTest implements WithAssertions {
final Path dir = fileExists();
//then
assertThatExceptionOfType(NotDirectoryException.class)
.isThrownBy(() -> GitDB.initLocal(dir))
.isThrownBy(() -> GitDB.initLocal(dir, userName, userEmailAddress))
.withMessageContaining(dir.toString());
}
@ -73,7 +71,7 @@ class GitDBTest implements WithAssertions {
filesExistIn(dir);
//then
assertThatExceptionOfType(DirectoryNotEmptyException.class)
.isThrownBy(() -> GitDB.initLocal(dir))
.isThrownBy(() -> GitDB.initLocal(dir, userName, userEmailAddress))
.withMessageContaining(dir.toString());
}
@ -91,7 +89,7 @@ class GitDBTest implements WithAssertions {
//given
final Path dir = dirExists();
//when
final GitDB gitDB = GitDB.initLocal(dir);
final GitDB gitDB = GitDB.initLocal(dir, userName, userEmailAddress);
//then
assertThat(gitDB).isNotNull();
assertThatIsBareRepo(dir);
@ -104,7 +102,7 @@ class GitDBTest implements WithAssertions {
final Path dir = dirExists();
//then
assertThatExceptionOfType(InvalidRepositoryException.class)
.isThrownBy(() -> GitDB.openLocal(dir))
.isThrownBy(() -> GitDB.openLocal(dir, userName, userEmailAddress))
.withMessageContaining(dir.toString());
}
@ -115,7 +113,7 @@ class GitDBTest implements WithAssertions {
final Path dir = fileExists();
//then
assertThatExceptionOfType(InvalidRepositoryException.class)
.isThrownBy(() -> GitDB.openLocal(dir))
.isThrownBy(() -> GitDB.openLocal(dir, userName, userEmailAddress))
.withMessageContaining(dir.toString());
}
@ -126,7 +124,7 @@ class GitDBTest implements WithAssertions {
final Path dir = dirDoesNotExist();
//then
assertThatExceptionOfType(InvalidRepositoryException.class)
.isThrownBy(() -> GitDB.openLocal(dir))
.isThrownBy(() -> GitDB.openLocal(dir, userName, userEmailAddress))
.withMessageContaining(dir.toString());
}
@ -137,7 +135,7 @@ class GitDBTest implements WithAssertions {
final Path dir = nonBareRepo();
//then
assertThatExceptionOfType(InvalidRepositoryException.class)
.isThrownBy(() -> GitDB.openLocal(dir))
.isThrownBy(() -> GitDB.openLocal(dir, userName, userEmailAddress))
.withMessageContaining("Invalid GitDB repo")
.withMessageContaining("Not a bare repo")
.withMessageContaining(dir.toString());
@ -155,14 +153,14 @@ class GitDBTest implements WithAssertions {
//given
final Path dir = gitDBRepoPath();
//when
final GitDBLocal gitDB = GitDB.openLocal(dir);
final GitDBLocal gitDB = GitDB.openLocal(dir, userName, userEmailAddress);
//then
assertThat(gitDB).isNotNull();
}
private Path gitDBRepoPath() throws IOException {
final Path dbDir = dirDoesNotExist();
GitDB.initLocal(dbDir);
GitDB.initLocal(dbDir, userName, userEmailAddress);
return dbDir;
}
@ -171,15 +169,15 @@ class GitDBTest implements WithAssertions {
@Test
void selectBranch_whenBranchNotExist_thenEmptyOptional() throws IOException {
//given
final GitDB gitDb = newGitDBRepo(dirDoesNotExist());
final GitDB gitDb = gitDB(dirDoesNotExist());
//when
final Optional<GitDBBranch> branch = gitDb.branch("unknown");
//then
assertThat(branch).isEmpty();
}
private GitDB newGitDBRepo(final Path dbDir) throws IOException {
return GitDB.initLocal(dbDir);
private GitDB gitDB(final Path dbDir) throws IOException {
return GitDB.initLocal(dbDir, userName, userEmailAddress);
}
// When select a valid branch then a GitDbBranch is returned
@ -187,7 +185,7 @@ class GitDBTest implements WithAssertions {
void selectBranch_branchExists_thenReturnBranch() throws IOException {
//given
final Path dbDir = dirDoesNotExist();
final GitDB gitDb = newGitDBRepo(dbDir);
final GitDB gitDb = gitDB(dbDir);
//when
final Optional<GitDBBranch> branch = gitDb.branch("master");
//then
@ -197,19 +195,37 @@ class GitDBTest implements WithAssertions {
// Given a valid GitDbBranch handle
// When getting a key that does not exist then return an empty Optional
@Test
void getKey_whenKeyNotExist_thenReturnEmptyOptional() throws IOException {
void getKey_whenKeyNotExist_thenReturnEmptyOptional() throws IOException, ClassNotFoundException {
//given
final GitDB gitDB = newGitDBRepo(dirDoesNotExist());
final Optional<GitDBBranch> branchOptional = gitDB.branch("master");
assumeThat(branchOptional).isNotEmpty();
final GitDBBranch master = branchOptional.get();
final GitDBBranch branch = gitDBBranch();
//when
final Optional<String> value = master.get("unknown", String.class);
final Optional<String> value = branch.get("unknown");
//then
assertThat(value).isEmpty();
}
private GitDBBranch gitDBBranch() throws IOException {
final GitDB gitDB = gitDB(dirDoesNotExist());
final Optional<GitDBBranch> branchOptional = gitDB.branch("master");
assumeThat(branchOptional).isNotEmpty();
return branchOptional.get();
}
// When putting a key/value pair then a GitDbBranch is returned
@Test
void putValue_thenReturnUpdatedGitDBBranch() throws IOException, ClassNotFoundException {
//given
final GitDBBranch originalBranch = gitDBBranch();
//when
final GitDBBranch updatedBranch = originalBranch.put("key-name", "value");
//then
assertThat(updatedBranch).isNotNull();
assertThat(updatedBranch).isNotSameAs(originalBranch);
final Optional<String> optional = updatedBranch.get("key-name");
assertThat(optional).contains("value");
//assertThat(originalBranch.get("key", String.class)).isEmpty();
}
// When getting a key that does exist then the value is returned inside an Optional
// When removing a key that does not exist then the GitDbBranch is returned
// When removing a key that does exist then a GitDbBranch is returned