using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Handles;
namespace LibGit2Sharp
{
///
/// Provides methods to directly work against the Git object database
/// without involving the index nor the working directory.
///
public class ObjectDatabase : IEnumerable
{
private readonly Repository repo;
private readonly ObjectDatabaseSafeHandle handle;
///
/// Needed for mocking purposes.
///
protected ObjectDatabase()
{ }
internal ObjectDatabase(Repository repo)
{
this.repo = repo;
handle = Proxy.git_repository_odb(repo.Handle);
repo.RegisterForCleanup(handle);
}
#region Implementation of IEnumerable
///
/// Returns an enumerator that iterates through the collection.
///
/// An object that can be used to iterate through the collection.
public virtual IEnumerator GetEnumerator()
{
ICollection oids = Proxy.git_odb_foreach(handle,
ptr => ptr.MarshalAs());
return oids
.Select(gitOid => repo.Lookup(new ObjectId(gitOid)))
.GetEnumerator();
}
///
/// Returns an enumerator that iterates through the collection.
///
/// An object that can be used to iterate through the collection.
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
///
/// Determines if the given object can be found in the object database.
///
/// Identifier of the object being searched for.
/// True if the object has been found; false otherwise.
public virtual bool Contains(ObjectId objectId)
{
Ensure.ArgumentNotNull(objectId, "objectId");
return Proxy.git_odb_exists(handle, objectId);
}
///
/// Retrieves the header of a GitObject from the object database. The header contains the Size
/// and Type of the object. Note that most backends do not support reading only the header
/// of an object, so the whole object will be read and then size would be returned.
///
/// Object Id of the queried object
/// GitObjectMetadata object instance containg object header information
public virtual GitObjectMetadata RetrieveObjectMetadata(ObjectId objectId)
{
Ensure.ArgumentNotNull(objectId, "objectId");
return Proxy.git_odb_read_header(handle, objectId);
}
///
/// Inserts a into the object database, created from the content of a file.
///
/// Path to the file to create the blob from. A relative path is allowed to
/// be passed if the is a standard, non-bare, repository. The path
/// will then be considered as a path relative to the root of the working directory.
/// The created .
public virtual Blob CreateBlob(string path)
{
Ensure.ArgumentNotNullOrEmptyString(path, "path");
if (repo.Info.IsBare && !Path.IsPathRooted(path))
{
throw new InvalidOperationException(
string.Format(CultureInfo.InvariantCulture,
"Cannot create a blob in a bare repository from a relative path ('{0}').", path));
}
ObjectId id = Path.IsPathRooted(path)
? Proxy.git_blob_create_fromdisk(repo.Handle, path)
: Proxy.git_blob_create_fromfile(repo.Handle, path);
return repo.Lookup(id);
}
///
/// Adds the provided backend to the object database with the specified priority.
///
/// If the provided backend implements , the
/// method will be honored and invoked upon the disposal of the repository.
///
///
/// The backend to add
/// The priority at which libgit2 should consult this backend (higher values are consulted first)
public virtual void AddBackend(OdbBackend backend, int priority)
{
Ensure.ArgumentNotNull(backend, "backend");
Ensure.ArgumentConformsTo(priority, s => s > 0, "priority");
Proxy.git_odb_add_backend(handle, backend.GitOdbBackendPointer, priority);
}
private class Processor
{
private readonly Stream stream;
private readonly long? numberOfBytesToConsume;
private int totalNumberOfReadBytes;
public Processor(Stream stream, long? numberOfBytesToConsume)
{
this.stream = stream;
this.numberOfBytesToConsume = numberOfBytesToConsume;
}
public int Provider(IntPtr content, int max_length, IntPtr data)
{
var local = new byte[max_length];
int bytesToRead = max_length;
if (numberOfBytesToConsume.HasValue)
{
long totalRemainingBytesToRead = numberOfBytesToConsume.Value - totalNumberOfReadBytes;
if (totalRemainingBytesToRead < max_length)
{
bytesToRead = totalRemainingBytesToRead > int.MaxValue ? int.MaxValue : (int)totalRemainingBytesToRead;
}
}
if (bytesToRead == 0)
{
return 0;
}
int numberOfReadBytes = stream.Read(local, 0, bytesToRead);
if (numberOfBytesToConsume.HasValue
&& numberOfReadBytes == 0)
{
return (int)GitErrorCode.User;
}
totalNumberOfReadBytes += numberOfReadBytes;
Marshal.Copy(local, 0, content, numberOfReadBytes);
return numberOfReadBytes;
}
}
///
/// Inserts a into the object database, created from the content of a stream.
/// Optionally, git filters will be applied to the content before storing it.
///
/// The stream from which will be read the content of the blob to be created.
/// The created .
public virtual Blob CreateBlob(Stream stream)
{
return CreateBlob(stream, null, null);
}
///
/// Inserts a into the object database, created from the content of a stream.
/// Optionally, git filters will be applied to the content before storing it.
///
/// The stream from which will be read the content of the blob to be created.
/// The hintpath is used to determine what git filters should be applied to the object before it can be placed to the object database.
/// The created .
public virtual Blob CreateBlob(Stream stream, string hintpath)
{
return CreateBlob(stream, hintpath, null);
}
///
/// Inserts a into the object database, created from the content of a stream.
/// Optionally, git filters will be applied to the content before storing it.
///
/// The stream from which will be read the content of the blob to be created.
/// The hintpath is used to determine what git filters should be applied to the object before it can be placed to the object database.
/// The number of bytes to consume from the stream.
/// The created .
public virtual Blob CreateBlob(Stream stream, string hintpath, long numberOfBytesToConsume)
{
return CreateBlob(stream, hintpath, (long?)numberOfBytesToConsume);
}
private Blob CreateBlob(Stream stream, string hintpath, long? numberOfBytesToConsume)
{
Ensure.ArgumentNotNull(stream, "stream");
// there's no need to buffer the file for filtering, so simply use a stream
if (hintpath == null && numberOfBytesToConsume.HasValue)
{
return CreateBlob(stream, numberOfBytesToConsume.Value);
}
if (!stream.CanRead)
{
throw new ArgumentException("The stream cannot be read from.", "stream");
}
var proc = new Processor(stream, numberOfBytesToConsume);
ObjectId id = Proxy.git_blob_create_fromchunks(repo.Handle, hintpath, proc.Provider);
return repo.Lookup(id);
}
///
/// Inserts a into the object database created from the content of the stream.
///
/// The stream from which will be read the content of the blob to be created.
/// Number of bytes to consume from the stream.
/// The created .
public virtual Blob CreateBlob(Stream stream, long numberOfBytesToConsume)
{
Ensure.ArgumentNotNull(stream, "stream");
if (!stream.CanRead)
{
throw new ArgumentException("The stream cannot be read from.", "stream");
}
using (var odbStream = Proxy.git_odb_open_wstream(handle, numberOfBytesToConsume, GitObjectType.Blob))
{
var buffer = new byte[4*1024];
long totalRead = 0;
while (totalRead < numberOfBytesToConsume)
{
long left = numberOfBytesToConsume - totalRead;
int toRead = left < buffer.Length ? (int)left : buffer.Length;
var read = stream.Read(buffer, 0, toRead);
if (read == 0)
{
throw new EndOfStreamException("The stream ended unexpectedly");
}
Proxy.git_odb_stream_write(odbStream, buffer, read);
totalRead += read;
}
var id = Proxy.git_odb_stream_finalize_write(odbStream);
return repo.Lookup(id);
}
}
///
/// Inserts a into the object database, created from a .
///
/// The .
/// The created .
public virtual Tree CreateTree(TreeDefinition treeDefinition)
{
Ensure.ArgumentNotNull(treeDefinition, "treeDefinition");
return treeDefinition.Build(repo);
}
///
/// Inserts a into the object database, created from the .
///
/// It recursively creates tree objects for each of the subtrees stored in the index, but only returns the root tree.
///
///
/// The index must be fully merged.
///
///
/// The .
/// The created . This can be used e.g. to create a .
public virtual Tree CreateTree(Index index)
{
Ensure.ArgumentNotNull(index, "index");
var treeId = Proxy.git_index_write_tree(index.Handle);
return this.repo.Lookup(treeId);
}
///
/// Inserts a into the object database, referencing an existing .
///
/// Prettifing the message includes:
/// * Removing empty lines from the beginning and end.
/// * Removing trailing spaces from every line.
/// * Turning multiple consecutive empty lines between paragraphs into just one empty line.
/// * Ensuring the commit message ends with a newline.
/// * Removing every line starting with "#".
///
///
/// The of who made the change.
/// The of who added the change to the repository.
/// The description of why a change was made to the repository.
/// The of the to be created.
/// The parents of the to be created.
/// True to prettify the message, or false to leave it as is.
/// The created .
public virtual Commit CreateCommit(Signature author, Signature committer, string message, Tree tree, IEnumerable parents, bool prettifyMessage)
{
return CreateCommit(author, committer, message, tree, parents, prettifyMessage, null);
}
///
/// Inserts a into the object database, referencing an existing .
///
/// Prettifing the message includes:
/// * Removing empty lines from the beginning and end.
/// * Removing trailing spaces from every line.
/// * Turning multiple consecutive empty lines between paragraphs into just one empty line.
/// * Ensuring the commit message ends with a newline.
/// * Removing every line starting with the .
///
///
/// The of who made the change.
/// The of who added the change to the repository.
/// The description of why a change was made to the repository.
/// The of the to be created.
/// The parents of the to be created.
/// True to prettify the message, or false to leave it as is.
/// When non null, lines starting with this character will be stripped if prettifyMessage is true.
/// The created .
public virtual Commit CreateCommit(Signature author, Signature committer, string message, Tree tree, IEnumerable parents, bool prettifyMessage, char? commentChar)
{
Ensure.ArgumentNotNull(message, "message");
Ensure.ArgumentDoesNotContainZeroByte(message, "message");
Ensure.ArgumentNotNull(author, "author");
Ensure.ArgumentNotNull(committer, "committer");
Ensure.ArgumentNotNull(tree, "tree");
Ensure.ArgumentNotNull(parents, "parents");
if (prettifyMessage)
{
message = Proxy.git_message_prettify(message, commentChar);
}
GitOid[] parentIds = parents.Select(p => p.Id.Oid).ToArray();
ObjectId commitId = Proxy.git_commit_create(repo.Handle, null, author, committer, message, tree, parentIds);
return repo.Lookup(commitId);
}
///
/// Inserts a into the object database, pointing to a specific .
///
/// The name.
/// The being pointed at.
/// The tagger.
/// The message.
/// The created .
public virtual TagAnnotation CreateTagAnnotation(string name, GitObject target, Signature tagger, string message)
{
Ensure.ArgumentNotNullOrEmptyString(name, "name");
Ensure.ArgumentNotNull(message, "message");
Ensure.ArgumentNotNull(target, "target");
Ensure.ArgumentNotNull(tagger, "tagger");
Ensure.ArgumentDoesNotContainZeroByte(name, "name");
Ensure.ArgumentDoesNotContainZeroByte(message, "message");
string prettifiedMessage = Proxy.git_message_prettify(message, null);
ObjectId tagId = Proxy.git_tag_annotation_create(repo.Handle, name, target, tagger, prettifiedMessage);
return repo.Lookup(tagId);
}
///
/// Archive the given commit.
///
/// The commit.
/// The archiver to use.
public virtual void Archive(Commit commit, ArchiverBase archiver)
{
Ensure.ArgumentNotNull(commit, "commit");
Ensure.ArgumentNotNull(archiver, "archiver");
archiver.OrchestrateArchiving(commit.Tree, commit.Id, commit.Committer.When);
}
///
/// Archive the given tree.
///
/// The tree.
/// The archiver to use.
public virtual void Archive(Tree tree, ArchiverBase archiver)
{
Ensure.ArgumentNotNull(tree, "tree");
Ensure.ArgumentNotNull(archiver, "archiver");
archiver.OrchestrateArchiving(tree, null, DateTimeOffset.UtcNow);
}
///
/// Returns the merge base (best common ancestor) of the given commits
/// and the distance between each of these commits and this base.
///
/// The being used as a reference.
/// The being compared against .
/// A instance of .
public virtual HistoryDivergence CalculateHistoryDivergence(Commit one, Commit another)
{
Ensure.ArgumentNotNull(one, "one");
Ensure.ArgumentNotNull(another, "another");
return new HistoryDivergence(repo, one, another);
}
///
/// Calculates the current shortest abbreviated
/// string representation for a .
///
/// The which identifier should be shortened.
/// A short string representation of the .
public virtual string ShortenObjectId(GitObject gitObject)
{
var shortSha = Proxy.git_object_short_id(repo.Handle, gitObject.Id);
return shortSha;
}
///
/// Calculates the current shortest abbreviated
/// string representation for a .
///
/// The which identifier should be shortened.
/// Minimum length of the shortened representation.
/// A short string representation of the .
public virtual string ShortenObjectId(GitObject gitObject, int minLength)
{
if (minLength <= 0 || minLength > ObjectId.HexSize)
{
throw new ArgumentOutOfRangeException("minLength", minLength,
string.Format("Expected value should be greater than zero and less than or equal to {0}.", ObjectId.HexSize));
}
string shortSha = Proxy.git_object_short_id(repo.Handle, gitObject.Id);
if (minLength <= shortSha.Length)
{
return shortSha;
}
return gitObject.Sha.Substring(0, minLength);
}
///
/// Returns whether merging into
/// would result in merge conflicts.
///
/// The commit wrapping the base tree to merge into.
/// The commit wrapping the tree to merge into .
/// True if the merge does not result in a conflict, false otherwise.
public virtual bool CanMergeWithoutConflict(Commit one, Commit another)
{
Ensure.ArgumentNotNull(one, "one");
Ensure.ArgumentNotNull(another, "another");
var result = repo.ObjectDatabase.MergeCommits(one, another, null);
return (result.Status == MergeTreeStatus.Succeeded);
}
///
/// Find the best possible merge base given two s.
///
/// The first .
/// The second .
/// The merge base or null if none found.
public virtual Commit FindMergeBase(Commit first, Commit second)
{
Ensure.ArgumentNotNull(first, "first");
Ensure.ArgumentNotNull(second, "second");
return FindMergeBase(new[] { first, second }, MergeBaseFindingStrategy.Standard);
}
///
/// Find the best possible merge base given two or more according to the .
///
/// The s for which to find the merge base.
/// The strategy to leverage in order to find the merge base.
/// The merge base or null if none found.
public virtual Commit FindMergeBase(IEnumerable commits, MergeBaseFindingStrategy strategy)
{
Ensure.ArgumentNotNull(commits, "commits");
ObjectId id;
List ids = new List(8);
int count = 0;
foreach (var commit in commits)
{
if (commit == null)
{
throw new ArgumentException("Enumerable contains null at position: " + count.ToString(CultureInfo.InvariantCulture), "commits");
}
ids.Add(commit.Id.Oid);
count++;
}
if (count < 2)
{
throw new ArgumentException("The enumerable must contains at least two commits.", "commits");
}
switch (strategy)
{
case MergeBaseFindingStrategy.Standard:
id = Proxy.git_merge_base_many(repo.Handle, ids.ToArray());
break;
case MergeBaseFindingStrategy.Octopus:
id = Proxy.git_merge_base_octopus(repo.Handle, ids.ToArray());
break;
default:
throw new ArgumentException("", "strategy");
}
return id == null ? null : repo.Lookup(id);
}
///
/// Perform a three-way merge of two commits, looking up their
/// commit ancestor. The returned index will contain the results
/// of the merge and can be examined for conflicts. The returned
/// index must be disposed.
///
/// The first tree
/// The second tree
/// The controlling the merge
/// The containing the merged trees and any conflicts
public virtual MergeTreeResult MergeCommits(Commit ours, Commit theirs, MergeTreeOptions options)
{
Ensure.ArgumentNotNull(ours, "ours");
Ensure.ArgumentNotNull(theirs, "theirs");
options = options ?? new MergeTreeOptions();
var mergeOptions = new GitMergeOpts
{
Version = 1,
MergeFileFavorFlags = options.MergeFileFavor,
MergeTreeFlags = options.FindRenames ? GitMergeTreeFlags.GIT_MERGE_TREE_FIND_RENAMES :
GitMergeTreeFlags.GIT_MERGE_TREE_NORMAL,
RenameThreshold = (uint)options.RenameThreshold,
TargetLimit = (uint)options.TargetLimit,
};
using (var oneHandle = Proxy.git_object_lookup(repo.Handle, ours.Id, GitObjectType.Commit))
using (var twoHandle = Proxy.git_object_lookup(repo.Handle, theirs.Id, GitObjectType.Commit))
using (var indexHandle = Proxy.git_merge_commits(repo.Handle, oneHandle, twoHandle, mergeOptions))
{
MergeTreeResult mergeResult;
if (Proxy.git_index_has_conflicts(indexHandle))
{
List conflicts = new List();
Conflict conflict;
using (ConflictIteratorSafeHandle iterator = Proxy.git_index_conflict_iterator_new(indexHandle))
{
while ((conflict = Proxy.git_index_conflict_next(iterator)) != null)
{
conflicts.Add(conflict);
}
}
mergeResult = new MergeTreeResult(conflicts);
}
else
{
var treeId = Proxy.git_index_write_tree_to(indexHandle, repo.Handle);
mergeResult = new MergeTreeResult(this.repo.Lookup(treeId));
}
return mergeResult;
}
}
}
}