All Downloads are FREE. Search and download functionalities are using the official Maven repository.

br.com.objectos.git.GitEngine Maven / Gradle / Ivy

Go to download

ObjectosGit is a pure Java library that provides a limited set of GIT operations.

The newest version!
/*
 * Copyright (C) 2020-2022 Objectos Software LTDA.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package br.com.objectos.git;

import br.com.objectos.collections.list.MutableList;
import br.com.objectos.collections.map.Maps;
import br.com.objectos.collections.map.MutableMap;
import br.com.objectos.collections.set.ImmutableSet;
import br.com.objectos.collections.set.MutableSet;
import br.com.objectos.concurrent.CpuTask;
import br.com.objectos.concurrent.IoWorker;
import br.com.objectos.core.object.Checks;
import br.com.objectos.fs.Directory;
import br.com.objectos.logging.Logger;
import br.com.objectos.logging.NoopLogger;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

/**
 * A state machine providing low-level (plumbing) Git operations.
 *
 * 

* Instead of using this class directly, users might prefer using * {@link StageGitCommand} and {@link GitService} as they provide means to * combine, compose and parallelize git operations. * *

* When using tasks provided by this class, please note that they are not * thread-safe and must be run in a single thread. Read {@link GitTask} * javadoc for more information. * * @since 1 */ public final class GitEngine extends GitInjector { private static final int MASK_CHAR_BUFFER = 1 << 0; private static final int MASK_DEFLATER = 1 << 1; private static final int MASK_FILTER_NON_EXISTING = 1 << 2; private static final int MASK_RESOLVE_REF = 1 << 3; private static final int MASK_STRING_BUILDER = 1 << 4; private Deque arrayDeque; private int bitset; private final int bufferSize; private final Deque byteArrayWriters = Git.newArrayDeque(3); private final Deque byteBuffers = Git.newArrayDeque(3); private CharBuffer charBuffer; private CopyObjects copyObjects; private CpuTask currentCpuTask; private final Map decoderMap = MutableMap.create(); private Deflater deflater; private final Map encoderMap = Maps.newHashMapWithCapacity(2); private FilterNonExisting filterNonExisting; private IdentificationBuilder identificationBuilder; private Inflater inflater; private final Deque intStacks = Git.newArrayDeque(3); private final IoWorker ioWorker; private final Logger logger; private MaterializeEntry materializeEntry; private MessageDigest messageDigest; private final Deque> mutableLists = Git.newArrayDeque(3); private final Deque> mutableSets = Git.newArrayDeque(3); private ObjectReader objectReader; private OpenPackFile openPackFile; private OpenRepository openRepository; private ReadBlob readBlob; private ReadCommit readCommit; private ReadTree readTree; private ResolveRef resolveRef; private StringBuilder stringBuilder; private UpdateRef updateRef; private WriteCommit writeCommit; private WriteTree writeTree; GitEngine(int bufferSize, IoWorker ioWorker, Logger logger) { this.bufferSize = bufferSize; this.ioWorker = ioWorker; this.logger = logger; } /** * Returns an option that sets the buffer size to the specified value. * * @param size * the buffer size * * @return an option that sets the buffer size to the specified value * * @throws IllegalArgumentException * if {@code size < 64} */ public static Option bufferSize(final int size) { Git.checkBufferSize(size); return new Option() { @Override final void acceptBuilder(GitEngineBuilder builder) { builder.setBufferSize(size); } }; } /** * Creates and returns a new git engine instance. * *

* Options are provided by {@code static} methods in this class. The available * options are: * *

* * * * * * * * * * * * * * * * * * * * *
Options provided by the {@code GitEngine} class
Method nameDescriptionDefault value
{@link GitEngine#bufferSize(int) bufferSize}The size in bytes of the buffers used by this engine{@code 4096}
{@link GitEngine#logger(Logger) logger}The logger instance the {@code GitEngine} will use{@link NoopLogger#getInstance()}
* * @param ioWorker * the worker for I/O tasks * @param options * additional configuration options * * @return a new git engine instance */ public static GitEngine create(IoWorker ioWorker, Option... options) { Checks.checkNotNull(ioWorker, "ioWorker == null"); Checks.checkNotNull(options, "options == null"); GitEngineBuilder builder; builder = new GitEngineBuilder(ioWorker); for (int i = 0, length = options.length; i < length; i++) { Option o; o = options[i]; Checks.checkNotNull(o, "options[", i, "] == null"); o.acceptBuilder(builder); } return builder.build(); } /** * Returns an options that sets the logger instance to the specified value. * * @param logger * the logger instance * * @return an option that sets the logger instance to the specified value */ public static Option logger(final Logger logger) { Checks.checkNotNull(logger, "logger == null"); return new Option() { @Override final void acceptBuilder(GitEngineBuilder builder) { builder.setLogger(logger); } }; } /** * Returns a new task for copying a set of objects from one repository to * another. * *

* The task first determines which of the specified objects are not present in * the destination repository. For each of the objects in the resulting * subset, the task tries to read it fully (into memory) from the source * repository and then it tries to write it to the destination repository. If * both the read and write operations succeed it goes to the next object. If, * however, either the read or write operation fails, the task stops. * *

* The task, if successful, returns the set of objects that were actually * copied, i.e., excluding the ones that were already present in the * destination repository before the operation. * * @param source * the source repository containing the objects. Git objects will be * read from this repository. * @param objects * the ids (hash values) of the objects to be copied * @param destination * the destination repository. Git objects will (possibly) be written * to this repository * * @return a new task for copying Git objects * * @since 3 */ public final GitTask> copyObjects( Repository source, ImmutableSet objects, Repository destination) { Checks.checkNotNull(source, "source == null"); Checks.checkNotNull(objects, "objects == null"); Checks.checkNotNull(destination, "destination == null"); return new CopyObjectsTask(this, source, objects, destination); } /** * Returns a new task for materializing a Git tree entry at the specified * target directory. * *

* If the entry denotes a tree object, the operation will attempt to * create a child directory (relative to the specified target directory) * having the same name as the entry name. If the directory already exists, * the operation will simply return the existing directory (wrapped in a * {@code MaterializedEntry} instance). * *

* If the entry denotes a blob object, the operation will attempt to read the * blob contents from the specified repository. If it succeeds reading the * contents, it will write all bytes to a regular file having the same name as * entry name and located at the specified target directory. If the file does * not exist it will be created, if the file already exists it will be * overwritten. * * @param repository * repository to read blob contents from (if necessary) * @param entry * the entry to be materialized * @param target * the target directory * * @return a new task for materializing a Git tree entry * * @since 3 */ public final GitTask materializeEntry( Repository repository, Entry entry, Directory target) { Checks.checkNotNull(repository, "repository == null"); Checks.checkNotNull(entry, "entry == null"); Checks.checkNotNull(target, "target == null"); return new MaterializeEntryTask(this, repository, entry, target); } /** * Returns a new task for opening a Git repository located at the specified * directory. * *

* A {@link Repository} instance is required for most Git operations provided * by this class. The open repository operation succeeds if the * specified directory is an actual Git repository and it fails otherwise. The * operation will also fail if an I/O error occurs. * *

* As of the current release the operation will fail if the specified * directory is not a bare repository. * * @param directory * the directory where a Git repository is located * * @return a new task for opening a repository * * @since 1 */ public final GitTask openRepository(Directory directory) { Checks.checkNotNull(directory, "directory == null"); return new OpenRepositoryTask(this, directory); } /** * Returns a new task for reading a blob object from a repository. * *

* The returned task will fail if an object with the specified hash cannot be * found, if an object with the specified hash exists but is not a blob object * or if there is a parse error. * * @param repository * the git repository from which to read the commit * @param id * the unique hash value of the blob to read * * @return a new task for reading a blob object * * @since 3 */ public final GitTask readBlob(Repository repository, ObjectId id) { Checks.checkNotNull(repository, "repository == null"); Checks.checkNotNull(id, "id == null"); return new ReadBlobTask(this, repository, id); } /** * Returns a new task for reading a commit object from a repository. * *

* The returned task will fail if an object with the specified hash cannot be * found, if an object with the specified hash exists but is not a commit * object or if there is a parse error. * * @param repository * the git repository from which to read the commit * @param id * the unique hash value of the commit to read * * @return a new task for reading a commit object * * @since 2 */ public final GitTask readCommit(Repository repository, ObjectId id) { Checks.checkNotNull(repository, "repository == null"); Checks.checkNotNull(id, "id == null"); return new ReadCommitTask(this, repository, id); } /** * Returns a new task for reading a tree object from a repository. * *

* The returned task will fail if an object with the specified hash cannot be * found, if an object with the specified hash exists but is not a tree * object, if there is a parse error or if an I/O error occurs. * * @param repository * the git repository from which to read the tree * @param id * the unique hash value of the tree to read * * @return a new task for reading a tree object * * @since 2 */ public final GitTask readTree(Repository repository, ObjectId id) { Checks.checkNotNull(repository, "repository == null"); Checks.checkNotNull(id, "id == null"); return new ReadTreeTask(this, repository, id); } /** * Returns a new task for resolving a Git reference. To resolve a Git * reference is to find the object ID associated to it. * * @param repository * the repository for which the reference will be resolved * @param ref * the Git reference to be resolved * * @return a new task for resolving a Git reference * * @since 1 */ public final GitTask resolve(Repository repository, RefName ref) { Checks.checkNotNull(repository, "repository == null"); Checks.checkNotNull(ref, "ref == null"); return new ResolveRefTask(this, repository, ref); } /** * Returns a new task for updating a Git reference. * * @param repository * the repository for which the reference will be updated * @param ref * the Git reference to be updated * @param newValue * the new value for the reference * * @return a new task for updating a Git reference * * @since 3 */ public final GitTask updateRef( Repository repository, RefName ref, ObjectId newValue) { Checks.checkNotNull(repository, "repository == null"); Checks.checkNotNull(ref, "ref == null"); Checks.checkNotNull(newValue, "newValue == null"); return new UpdateRefTask(this, repository, ref, newValue); } /** * Returns a new task for writing a commit object (as a loose object) to a * repository. * *

* The task will first verify if the specified commit have all of the * mandatory properties defined. If not, the task fails. Otherwise it will * assemble the Git commit object and will try to write it as a loose object. * If the loose object already exists the task assumes it is correct and * returns. If the loose object does not exist, the task creates it and the * data is written. * *

* As of the current release the operation will create a loose object even if * the same commit object exists in a pack file. * * @param repository * the git repository where the commit is to be written * @param commit * a {@code MutableCommit} instance containing all the information to * be recorded as a commit object * * @return a new task for writing a commit object * * @since 3 */ public final GitTask writeCommit(Repository repository, MutableCommit commit) { Checks.checkNotNull(repository, "repository == null"); Checks.checkNotNull(commit, "commit == null"); return new WriteCommitTask(this, repository, commit); } /** * Returns a new task for recursively writing tree objects to a repository. * The task is recursive in the sense that, if an entry in the specified tree * is itself a {@link MutableTree} instance, then it will also be written. * *

* As of the current release the operation will create loose objects even if * the same object exists in a pack file. * * @param repository * the git repository where the tree is to be written * @param tree * a {@code MutableTree} to be written * * @return a new task for writing a tree object * * @since 3 */ public final GitTask writeTree(Repository repository, MutableTree tree) { Checks.checkNotNull(repository, "repository == null"); Checks.checkNotNull(tree, "tree == null"); return new WriteTreeTask(this, repository, tree); } @Override @SuppressWarnings("unchecked") final ArrayDeque getArrayDeque() { if (arrayDeque == null) { arrayDeque = new ArrayDeque(); } else { arrayDeque.clear(); } return (ArrayDeque) arrayDeque; } @Override final int getBufferSize() { return bufferSize; } @Override final ByteArrayWriter getByteArrayWriter() { if (byteArrayWriters.isEmpty()) { return new ByteArrayWriter(); } ByteArrayWriter writer; writer = byteArrayWriters.remove(); writer.clear(); return writer; } @Override final ByteBuffer getByteBuffer() { if (byteBuffers.isEmpty()) { return ByteBuffer.allocate(bufferSize); } ByteBuffer buffer; buffer = byteBuffers.remove(); buffer.clear(); return buffer; } @Override final CharBuffer getCharBuffer() { checkBitSet(MASK_CHAR_BUFFER, "CharBuffer"); if (charBuffer == null) { charBuffer = CharBuffer.allocate(bufferSize); } charBuffer.clear(); setBitSet(MASK_CHAR_BUFFER); return charBuffer; } final CopyObjects getCopyObjects() { if (copyObjects == null) { copyObjects = new CopyObjects(this); } return copyObjects; } @Override final CharsetDecoder getDecoder(Charset charset) { DecoderBucket decoder; decoder = decoderMap.get(charset); if (decoder == null) { decoder = new DecoderBucket(charset); decoderMap.put(charset, decoder); } return decoder.get(); } @Override final Deflater getDeflater() { checkBitSet(MASK_DEFLATER, "Deflater"); if (deflater == null) { deflater = new Deflater(); } else { deflater.reset(); } setBitSet(MASK_DEFLATER); return deflater; } @Override final CharsetEncoder getEncoder(Charset charset) { CharsetEncoder encoder; encoder = encoderMap.get(charset); if (encoder == null) { encoder = charset.newEncoder(); encoderMap.put(charset, encoder); } else { encoder.reset(); } return encoder; } @Override final FilterNonExisting getFilterNonExisting() { checkBitSet(MASK_FILTER_NON_EXISTING, "FilterNonExisting"); if (filterNonExisting == null) { filterNonExisting = new FilterNonExisting(this); } setBitSet(MASK_FILTER_NON_EXISTING); return filterNonExisting; } @Override final IdentificationBuilder getIdentificationBuilder() { if (identificationBuilder == null) { identificationBuilder = new IdentificationBuilder(); } else { identificationBuilder.reset(); } return identificationBuilder; } @Override final Inflater getInflater() { if (inflater == null) { inflater = new Inflater(); } else { inflater.reset(); } return inflater; } @Override final IntStack getIntStack() { if (intStacks.isEmpty()) { return new IntStack(); } IntStack stack; stack = intStacks.remove(); stack.clear(); return stack; } @Override final IoWorker getIoWorker() { return ioWorker; } @Override final Logger getLogger() { return logger; } final MaterializeEntry getMaterializeEntry() { if (materializeEntry == null) { materializeEntry = new MaterializeEntry(this); } return materializeEntry; } @Override final MessageDigest getMessageDigest(String algoName) { if (messageDigest == null) { try { messageDigest = MessageDigest.getInstance(Git.SHA1); } catch (NoSuchAlgorithmException e) { throw new UnsupportedOperationException("Implement me: move to Git.createMachine?"); } } return messageDigest; } @Override @SuppressWarnings("unchecked") final MutableList getMutableList() { if (mutableLists.isEmpty()) { return MutableList.create(); } MutableList list; list = mutableLists.remove(); list.clear(); return (MutableList) list; } @SuppressWarnings("unchecked") @Override final MutableSet getMutableSet() { if (mutableSets.isEmpty()) { return MutableSet.create(); } MutableSet set; set = mutableSets.remove(); set.clear(); return (MutableSet) set; } @Override final ObjectReader getObjectReader() { if (objectReader == null) { objectReader = new ObjectReader(this); } return objectReader; } @Override final OpenPackFile getOpenPackFile() { if (openPackFile == null) { openPackFile = new OpenPackFile(this); } return openPackFile; } final OpenRepository getOpenRepository() { if (openRepository == null) { openRepository = new OpenRepository(this); } return openRepository; } @Override final ReadBlob getReadBlob() { if (readBlob == null) { readBlob = new ReadBlob(this); } return readBlob; } final ReadCommit getReadCommit() { if (readCommit == null) { readCommit = new ReadCommit(this); } return readCommit; } final ReadTree getReadTree() { if (readTree == null) { readTree = new ReadTree(this); } return readTree; } @Override final ResolveRef getResolveRef() { checkBitSet(MASK_RESOLVE_REF, "ResolveRef"); if (resolveRef == null) { resolveRef = new ResolveRef(this); } setBitSet(MASK_RESOLVE_REF); return resolveRef; } @Override final StringBuilder getStringBuilder() { checkBitSet(MASK_STRING_BUILDER, "StringBuilder"); if (stringBuilder == null) { stringBuilder = new StringBuilder(); } else { stringBuilder.setLength(0); } setBitSet(MASK_STRING_BUILDER); return stringBuilder; } @Override final UpdateRef getUpdateRef() { if (updateRef == null) { updateRef = new UpdateRef(this); } return updateRef; } final WriteCommit getWriteCommit() { if (writeCommit == null) { writeCommit = new WriteCommit(this); } return writeCommit; } final WriteTree getWriteTree() { if (writeTree == null) { writeTree = new WriteTree(this); } return writeTree; } @Override final void putArrayDeque(ArrayDeque deque) { // noop } @Override final ByteArrayWriter putByteArrayWriter(ByteArrayWriter writer) { if (writer != null) { byteArrayWriters.add(writer); } return null; } @Override final ByteBuffer putByteBuffer(ByteBuffer buffer) { if (buffer != null) { byteBuffers.add(buffer); } return null; } @Override final CharBuffer putCharBuffer(CharBuffer buffer) { if (buffer != null) { unsetBitSet(MASK_CHAR_BUFFER); } return null; } @Override final CharsetDecoder putDecoder(CharsetDecoder decoder) { if (decoder != null) { Charset charset; charset = decoder.charset(); DecoderBucket bucket; bucket = decoderMap.get(charset); Checks.checkState(bucket != null, "Bucket was null for ", charset); bucket.checkedOut = false; } return null; } @Override final Deflater putDeflater(Deflater deflater) { if (deflater != null) { unsetBitSet(MASK_DEFLATER); } return null; } @Override final CharsetEncoder putEncoder(CharsetEncoder encoder) { // noop return null; } @Override final FilterNonExisting putFilterNonExisting(FilterNonExisting task) { if (task != null) { unsetBitSet(MASK_FILTER_NON_EXISTING); } return null; } @Override final Inflater putInflater(Inflater inflater) { // noop return null; } @Override final IntStack putIntStack(IntStack stack) { if (stack != null) { intStacks.add(stack); } return null; } @Override final MessageDigest putMessageDigest(MessageDigest digest) { return null; } @Override @SuppressWarnings("unchecked") final MutableList putMutableList(MutableList list) { if (list != null) { mutableLists.add((MutableList) list); } return null; } @SuppressWarnings("unchecked") @Override final MutableSet putMutableSet(MutableSet set) { if (set != null) { mutableSets.add((MutableSet) set); } return null; } @Override final ObjectReader putObjectReader(ObjectReader reader) { // noop return null; } @Override final ResolveRef putResolveRef(ResolveRef task) { if (task != null) { unsetBitSet(MASK_RESOLVE_REF); } return null; } @Override final StringBuilder putStringBuilder(StringBuilder sb) { if (sb != null) { unsetBitSet(MASK_STRING_BUILDER); } return null; } @Override final boolean tryLock(CpuTask task) { if (currentCpuTask != null) { return false; } else { currentCpuTask = task; return true; } } @Override final void unlock(CpuTask task) { if (currentCpuTask == task) { currentCpuTask = null; } else { // TODO: re-enable // throw new IllegalStateException("running a different task: " + currentCpuTask); } } private void checkBitSet(int mask, String message) { Checks.checkState((bitset & mask) == 0, message, " already checked out"); } private void setBitSet(int mask) { bitset |= mask; } private void unsetBitSet(int mask) { bitset &= ~mask; } /** * A {@code GitEngine} configuration option. * * @since 2 */ public abstract static class Option { Option() {} abstract void acceptBuilder(GitEngineBuilder builder); } private static class DecoderBucket { private boolean checkedOut; private final CharsetDecoder decoder; public DecoderBucket(Charset charset) { decoder = charset.newDecoder(); } final CharsetDecoder get() { Checks.checkState(!checkedOut, decoder.charset(), " decoder already checked out"); checkedOut = true; return decoder.reset(); } } }