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; } } } }