using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using LibGit2Sharp.Core;
using LibGit2Sharp.Core.Handles;
using LibGit2Sharp.Handlers;
namespace LibGit2Sharp
{
///
/// A Repository is the primary interface into a git repository
///
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public sealed class Repository : IRepository
{
private readonly bool isBare;
private readonly BranchCollection branches;
private readonly CommitLog commits;
private readonly Lazy config;
private readonly RepositorySafeHandle handle;
private readonly Index index;
private readonly ReferenceCollection refs;
private readonly TagCollection tags;
private readonly StashCollection stashes;
private readonly Lazy info;
private readonly Diff diff;
private readonly NoteCollection notes;
private readonly Lazy odb;
private readonly Lazy network;
private readonly Stack toCleanup = new Stack();
private readonly Ignore ignore;
private readonly SubmoduleCollection submodules;
private readonly Lazy pathCase;
///
/// Initializes a new instance of the class, providing ooptional behavioral overrides through parameter.
/// For a standard repository, should either point to the ".git" folder or to the working directory. For a bare repository, should directly point to the repository folder.
///
///
/// The path to the git repository to open, can be either the path to the git directory (for non-bare repositories this
/// would be the ".git" folder inside the working directory) or the path to the working directory.
///
///
/// Overrides to the way a repository is opened.
///
public Repository(string path, RepositoryOptions options = null)
{
Ensure.ArgumentNotNullOrEmptyString(path, "path");
try
{
handle = Proxy.git_repository_open(path);
RegisterForCleanup(handle);
isBare = Proxy.git_repository_is_bare(handle);
Func indexBuilder = () => new Index(this);
string configurationGlobalFilePath = null;
string configurationXDGFilePath = null;
string configurationSystemFilePath = null;
if (options != null)
{
bool isWorkDirNull = string.IsNullOrEmpty(options.WorkingDirectoryPath);
bool isIndexNull = string.IsNullOrEmpty(options.IndexPath);
if (isBare && (isWorkDirNull ^ isIndexNull))
{
throw new ArgumentException(
"When overriding the opening of a bare repository, both RepositoryOptions.WorkingDirectoryPath an RepositoryOptions.IndexPath have to be provided.");
}
isBare = false;
if (!isIndexNull)
{
indexBuilder = () => new Index(this, options.IndexPath);
}
if (!isWorkDirNull)
{
Proxy.git_repository_set_workdir(handle, options.WorkingDirectoryPath);
}
configurationGlobalFilePath = options.GlobalConfigurationLocation;
configurationXDGFilePath = options.XdgConfigurationLocation;
configurationSystemFilePath = options.SystemConfigurationLocation;
}
if (!isBare)
{
index = indexBuilder();
}
commits = new CommitLog(this);
refs = new ReferenceCollection(this);
branches = new BranchCollection(this);
tags = new TagCollection(this);
stashes = new StashCollection(this);
info = new Lazy(() => new RepositoryInformation(this, isBare));
config =
new Lazy(
() =>
RegisterForCleanup(new Configuration(this, configurationGlobalFilePath, configurationXDGFilePath,
configurationSystemFilePath)));
odb = new Lazy(() => new ObjectDatabase(this));
diff = new Diff(this);
notes = new NoteCollection(this);
ignore = new Ignore(this);
network = new Lazy(() => new Network(this));
pathCase = new Lazy(() => new PathCase(this));
submodules = new SubmoduleCollection(this);
EagerlyLoadTheConfigIfAnyPathHaveBeenPassed(options);
}
catch
{
CleanupDisposableDependencies();
throw;
}
}
///
/// Check if parameter leads to a valid git repository.
///
///
/// The path to the git repository to check, can be either the path to the git directory (for non-bare repositories this
/// would be the ".git" folder inside the working directory) or the path to the working directory.
///
/// True if a repository can be resolved through this path; false otherwise
static public bool IsValid(string path)
{
Ensure.ArgumentNotNullOrEmptyString(path, "path");
try
{
Proxy.git_repository_open_ext(path, RepositoryOpenFlags.NoSearch, null);
}
catch (RepositoryNotFoundException)
{
return false;
}
return true;
}
private void EagerlyLoadTheConfigIfAnyPathHaveBeenPassed(RepositoryOptions options)
{
if (options == null)
{
return;
}
if (options.GlobalConfigurationLocation == null &&
options.XdgConfigurationLocation == null &&
options.SystemConfigurationLocation == null)
{
return;
}
// Dirty hack to force the eager load of the configuration
// without Resharper pestering about useless code
if (!Config.HasConfig(ConfigurationLevel.Local))
{
throw new InvalidOperationException("Unexpected state.");
}
}
internal RepositorySafeHandle Handle
{
get { return handle; }
}
///
/// Shortcut to return the branch pointed to by HEAD
///
public Branch Head
{
get
{
Reference reference = Refs.Head;
if (reference == null)
{
throw new LibGit2SharpException("Corrupt repository. The 'HEAD' reference is missing.");
}
if (reference is SymbolicReference)
{
return new Branch(this, reference);
}
return new DetachedHead(this, reference);
}
}
///
/// Provides access to the configuration settings for this repository.
///
public Configuration Config
{
get { return config.Value; }
}
///
/// Gets the index.
///
public Index Index
{
get
{
if (isBare)
{
throw new BareRepositoryException("Index is not available in a bare repository.");
}
return index;
}
}
///
/// Manipulate the currently ignored files.
///
public Ignore Ignore
{
get
{
return ignore;
}
}
///
/// Provides access to network functionality for a repository.
///
public Network Network
{
get
{
return network.Value;
}
}
///
/// Gets the database.
///
public ObjectDatabase ObjectDatabase
{
get
{
return odb.Value;
}
}
///
/// Lookup and enumerate references in the repository.
///
public ReferenceCollection Refs
{
get { return refs; }
}
///
/// Lookup and enumerate commits in the repository.
/// Iterating this collection directly starts walking from the HEAD.
///
public IQueryableCommitLog Commits
{
get { return commits; }
}
///
/// Lookup and enumerate branches in the repository.
///
public BranchCollection Branches
{
get { return branches; }
}
///
/// Lookup and enumerate tags in the repository.
///
public TagCollection Tags
{
get { return tags; }
}
///
/// Lookup and enumerate stashes in the repository.
///
public StashCollection Stashes
{
get { return stashes; }
}
///
/// Provides high level information about this repository.
///
public RepositoryInformation Info
{
get { return info.Value; }
}
///
/// Provides access to diffing functionalities to show changes between the working tree and the index or a tree, changes between the index and a tree, changes between two trees, or changes between two files on disk.
///
public Diff Diff
{
get { return diff; }
}
///
/// Lookup notes in the repository.
///
public NoteCollection Notes
{
get { return notes; }
}
///
/// Submodules in the repository.
///
public SubmoduleCollection Submodules
{
get { return submodules; }
}
#region IDisposable Members
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
Dispose(true);
}
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
private void Dispose(bool disposing)
{
CleanupDisposableDependencies();
}
#endregion
///
/// Initialize a repository at the specified .
///
/// The path to the working folder when initializing a standard ".git" repository. Otherwise, when initializing a bare repository, the path to the expected location of this later.
/// true to initialize a bare repository. False otherwise, to initialize a standard ".git" repository.
/// The path to the created repository.
public static string Init(string path, bool isBare = false)
{
Ensure.ArgumentNotNullOrEmptyString(path, "path");
using (RepositorySafeHandle repo = Proxy.git_repository_init_ext(null, path, isBare))
{
FilePath repoPath = Proxy.git_repository_path(repo);
return repoPath.Native;
}
}
///
/// Initialize a repository by explictly setting the path to both the working directory and the git directory.
///
/// The path to the working directory.
/// The path to the git repository to be created.
/// The path to the created repository.
public static string Init(string workingDirectoryPath, string gitDirectoryPath)
{
Ensure.ArgumentNotNullOrEmptyString(workingDirectoryPath, "workingDirectoryPath");
Ensure.ArgumentNotNullOrEmptyString(gitDirectoryPath, "gitDirectoryPath");
// When being passed a relative workdir path, libgit2 will evaluate it from the
// path to the repository. We pass a fully rooted path in order for the LibGit2Sharp caller
// to pass a path relatively to his current directory.
string wd = Path.GetFullPath(workingDirectoryPath);
// TODO: Shouldn't we ensure that the working folder isn't under the gitDir?
using (RepositorySafeHandle repo = Proxy.git_repository_init_ext(wd, gitDirectoryPath, false))
{
FilePath repoPath = Proxy.git_repository_path(repo);
return repoPath.Native;
}
}
///
/// Try to lookup an object by its . If no matching object is found, null will be returned.
///
/// The id to lookup.
/// The or null if it was not found.
public GitObject Lookup(ObjectId id)
{
return LookupInternal(id, GitObjectType.Any, null);
}
///
/// Try to lookup an object by its sha or a reference canonical name. If no matching object is found, null will be returned.
///
/// A revparse spec for the object to lookup.
/// The or null if it was not found.
public GitObject Lookup(string objectish)
{
return Lookup(objectish, GitObjectType.Any, LookUpOptions.None);
}
///
/// Try to lookup an object by its and . If no matching object is found, null will be returned.
///
/// The id to lookup.
/// The kind of GitObject being looked up
/// The or null if it was not found.
public GitObject Lookup(ObjectId id, ObjectType type)
{
return LookupInternal(id, type.ToGitObjectType(), null);
}
///
/// Try to lookup an object by its sha or a reference canonical name and . If no matching object is found, null will be returned.
///
/// A revparse spec for the object to lookup.
/// The kind of being looked up
/// The or null if it was not found.
public GitObject Lookup(string objectish, ObjectType type)
{
return Lookup(objectish, type.ToGitObjectType(), LookUpOptions.None);
}
internal GitObject LookupInternal(ObjectId id, GitObjectType type, FilePath knownPath)
{
Ensure.ArgumentNotNull(id, "id");
using (GitObjectSafeHandle obj = Proxy.git_object_lookup(handle, id, type))
{
if (obj == null)
{
return null;
}
return GitObject.BuildFrom(this, id, Proxy.git_object_type(obj), knownPath);
}
}
private static string PathFromRevparseSpec(string spec)
{
if (spec.StartsWith(":/", StringComparison.Ordinal))
{
return null;
}
if (Regex.IsMatch(spec, @"^:.*:"))
{
return null;
}
var m = Regex.Match(spec, @"[^@^ ]*:(.*)");
return (m.Groups.Count > 1) ? m.Groups[1].Value : null;
}
internal GitObject Lookup(string objectish, GitObjectType type, LookUpOptions lookUpOptions)
{
Ensure.ArgumentNotNullOrEmptyString(objectish, "objectish");
GitObject obj;
using (GitObjectSafeHandle sh = Proxy.git_revparse_single(handle, objectish))
{
if (sh == null)
{
if (lookUpOptions.HasFlag(LookUpOptions.ThrowWhenNoGitObjectHasBeenFound))
{
Ensure.GitObjectIsNotNull(null, objectish);
}
return null;
}
GitObjectType objType = Proxy.git_object_type(sh);
if (type != GitObjectType.Any && objType != type)
{
return null;
}
obj = GitObject.BuildFrom(this, Proxy.git_object_id(sh), objType, PathFromRevparseSpec(objectish));
}
if (lookUpOptions.HasFlag(LookUpOptions.DereferenceResultToCommit))
{
return obj.DereferenceToCommit(
lookUpOptions.HasFlag(LookUpOptions.ThrowWhenCanNotBeDereferencedToACommit));
}
return obj;
}
internal Commit LookupCommit(string committish)
{
return (Commit)Lookup(committish, GitObjectType.Any,
LookUpOptions.ThrowWhenNoGitObjectHasBeenFound |
LookUpOptions.DereferenceResultToCommit |
LookUpOptions.ThrowWhenCanNotBeDereferencedToACommit);
}
///
/// Probe for a git repository.
/// The lookup start from and walk upward parent directories if nothing has been found.
///
/// The base path where the lookup starts.
/// The path to the git repository.
public static string Discover(string startingPath)
{
FilePath discoveredPath = Proxy.git_repository_discover(startingPath);
if (discoveredPath == null)
{
return null;
}
return discoveredPath.Native;
}
///
/// Clone with specified options.
///
/// URI for the remote repository
/// Local path to clone into
/// controlling clone behavior
/// The path to the created repository.
public static string Clone(string sourceUrl, string workdirPath,
CloneOptions options = null)
{
options = options ?? new CloneOptions();
using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
{
var gitCheckoutOptions = checkoutOptionsWrapper.Options;
var remoteCallbacks = new RemoteCallbacks(null, options.OnTransferProgress, null, options.CredentialsProvider);
var gitRemoteCallbacks = remoteCallbacks.GenerateCallbacks();
var cloneOpts = new GitCloneOptions
{
Version = 1,
Bare = options.IsBare ? 1 : 0,
CheckoutOpts = gitCheckoutOptions,
RemoteCallbacks = gitRemoteCallbacks,
};
FilePath repoPath;
using (RepositorySafeHandle repo = Proxy.git_clone(sourceUrl, workdirPath, ref cloneOpts))
{
repoPath = Proxy.git_repository_path(repo);
}
return repoPath.Native;
}
}
///
/// Find where each line of a file originated.
///
/// Path of the file to blame.
/// Specifies optional parameters; if null, the defaults are used.
/// The blame for the file.
public BlameHunkCollection Blame(string path, BlameOptions options)
{
return new BlameHunkCollection(this, Handle, path, options ?? new BlameOptions());
}
///
/// Checkout the specified , reference or SHA.
///
/// If the committishOrBranchSpec parameter resolves to a branch name, then the checked out HEAD will
/// will point to the branch. Otherwise, the HEAD will be detached, pointing at the commit sha.
///
///
/// A revparse spec for the commit or branch to checkout.
/// controlling checkout behavior.
/// Identity for use when updating the reflog.
/// The that was checked out.
public Branch Checkout(string committishOrBranchSpec, CheckoutOptions options, Signature signature)
{
Ensure.ArgumentNotNullOrEmptyString(committishOrBranchSpec, "committishOrBranchSpec");
Ensure.ArgumentNotNull(options, "options");
var handles = Proxy.git_revparse_ext(Handle, committishOrBranchSpec);
if (handles == null)
{
Ensure.GitObjectIsNotNull(null, committishOrBranchSpec);
}
var objH = handles.Item1;
var refH = handles.Item2;
GitObject obj;
try
{
if (!refH.IsInvalid)
{
var reference = Reference.BuildFromPtr(refH, this);
if (reference.IsLocalBranch())
{
Branch branch = Branches[reference.CanonicalName];
return Checkout(branch, options, signature);
}
}
obj = GitObject.BuildFrom(this, Proxy.git_object_id(objH), Proxy.git_object_type(objH),
PathFromRevparseSpec(committishOrBranchSpec));
}
finally
{
objH.Dispose();
refH.Dispose();
}
Commit commit = obj.DereferenceToCommit(true);
Checkout(commit.Tree, options, commit.Id.Sha, committishOrBranchSpec, signature);
return Head;
}
///
/// Checkout the tip commit of the specified object. If this commit is the
/// current tip of the branch, will checkout the named branch. Otherwise, will checkout the tip commit
/// as a detached HEAD.
///
/// The to check out.
/// controlling checkout behavior.
/// Identity for use when updating the reflog.
/// The that was checked out.
public Branch Checkout(Branch branch, CheckoutOptions options, Signature signature)
{
Ensure.ArgumentNotNull(branch, "branch");
Ensure.ArgumentNotNull(options, "options");
// Make sure this is not an unborn branch.
if (branch.Tip == null)
{
throw new UnbornBranchException(
string.Format(CultureInfo.InvariantCulture,
"The tip of branch '{0}' is null. There's nothing to checkout.", branch.Name));
}
if (!branch.IsRemote && !(branch is DetachedHead) &&
string.Equals(Refs[branch.CanonicalName].TargetIdentifier, branch.Tip.Id.Sha,
StringComparison.OrdinalIgnoreCase))
{
Checkout(branch.Tip.Tree, options, branch.CanonicalName, branch.Name, signature);
}
else
{
Checkout(branch.Tip.Tree, options, branch.Tip.Id.Sha, branch.Name, signature);
}
return Head;
}
///
/// Checkout the specified .
///
/// Will detach the HEAD and make it point to this commit sha.
///
///
/// The to check out.
/// controlling checkout behavior.
/// Identity for use when updating the reflog.
/// The that was checked out.
public Branch Checkout(Commit commit, CheckoutOptions options, Signature signature)
{
Ensure.ArgumentNotNull(commit, "commit");
Ensure.ArgumentNotNull(options, "options");
Checkout(commit.Tree, options, commit.Id.Sha, commit.Id.Sha, signature);
return Head;
}
///
/// Internal implementation of Checkout that expects the ID of the checkout target
/// to already be in the form of a canonical branch name or a commit ID.
///
/// The to checkout.
/// controlling checkout behavior.
/// Target for the new HEAD.
/// The spec which will be written as target in the reflog.
/// Identity for use when updating the reflog.
private void Checkout(
Tree tree,
CheckoutOptions checkoutOptions,
string headTarget, string refLogHeadSpec, Signature signature)
{
var previousHeadName = Info.IsHeadDetached ? Head.Tip.Sha : Head.Name;
CheckoutTree(tree, null, checkoutOptions);
Refs.UpdateTarget("HEAD", headTarget, signature,
string.Format(
CultureInfo.InvariantCulture,
"checkout: moving from {0} to {1}", previousHeadName, refLogHeadSpec));
}
///
/// Checkout the specified tree.
///
/// The to checkout.
/// The paths to checkout.
/// Collection of parameters controlling checkout behavior.
private void CheckoutTree(
Tree tree,
IList paths,
IConvertableToGitCheckoutOpts opts)
{
using(GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(opts, ToFilePaths(paths)))
{
var options = checkoutOptionsWrapper.Options;
Proxy.git_checkout_tree(Handle, tree.Id, ref options);
}
}
///
/// Sets the current to the specified commit and optionally resets the and
/// the content of the working tree to match.
///
/// Flavor of reset operation to perform.
/// The target commit object.
/// Identity for use when updating the reflog.
/// Message to use when updating the reflog.
public void Reset(ResetMode resetMode, Commit commit, Signature signature, string logMessage)
{
Ensure.ArgumentNotNull(commit, "commit");
if (logMessage == null)
{
logMessage = string.Format(
CultureInfo.InvariantCulture,
"reset: moving to {0}", commit.Sha);
}
Proxy.git_reset(handle, commit.Id, resetMode, signature.OrDefault(Config), logMessage);
}
///
/// Updates specifed paths in the index and working directory with the versions from the specified branch, reference, or SHA.
///
/// This method does not switch branches or update the current repository HEAD.
///
///
/// A revparse spec for the commit or branch to checkout paths from.
/// The paths to checkout. Will throw if null is passed in. Passing an empty enumeration results in nothing being checked out.
/// Collection of parameters controlling checkout behavior.
public void CheckoutPaths(string committishOrBranchSpec, IEnumerable paths, CheckoutOptions checkoutOptions)
{
Ensure.ArgumentNotNullOrEmptyString(committishOrBranchSpec, "committishOrBranchSpec");
Ensure.ArgumentNotNull(paths, "paths");
// If there are no paths, then there is nothing to do.
if (!paths.Any())
{
return;
}
Commit commit = LookupCommit(committishOrBranchSpec);
CheckoutTree(commit.Tree, paths.ToList(), checkoutOptions ?? new CheckoutOptions());
}
///
/// Replaces entries in the with entries from the specified commit.
///
/// The target commit object.
/// The list of paths (either files or directories) that should be considered.
///
/// If set, the passed will be treated as explicit paths.
/// Use these options to determine how unmatched explicit paths should be handled.
///
public void Reset(Commit commit, IEnumerable paths, ExplicitPathsOptions explicitPathsOptions)
{
if (Info.IsBare)
{
throw new BareRepositoryException("Reset is not allowed in a bare repository");
}
Ensure.ArgumentNotNull(commit, "commit");
var changes = Diff.Compare(commit.Tree, DiffTargets.Index, paths, explicitPathsOptions, new CompareOptions { Similarity = SimilarityOptions.None });
Index.Reset(changes);
}
///
/// Stores the content of the as a new into the repository.
/// The tip of the will be used as the parent of this new Commit.
/// Once the commit is created, the will move forward to point at it.
///
/// The description of why a change was made to the repository.
/// The of who made the change.
/// The of who added the change to the repository.
/// The that specify the commit behavior.
/// The generated .
public Commit Commit(string message, Signature author, Signature committer, CommitOptions options)
{
if (options == null)
{
options = new CommitOptions();
}
bool isHeadOrphaned = Info.IsHeadUnborn;
if (options.AmendPreviousCommit && isHeadOrphaned)
{
throw new UnbornBranchException("Can not amend anything. The Head doesn't point at any commit.");
}
var treeId = Proxy.git_tree_create_fromindex(Index);
var tree = this.Lookup(treeId);
var parents = RetrieveParentsOfTheCommitBeingCreated(options.AmendPreviousCommit).ToList();
if (parents.Count == 1 && !options.AllowEmptyCommit)
{
var treesame = parents[0].Tree.Id.Equals(treeId);
var amendMergeCommit = options.AmendPreviousCommit && !isHeadOrphaned && Head.Tip.Parents.Count() > 1;
if (treesame && !amendMergeCommit)
{
throw new EmptyCommitException(
options.AmendPreviousCommit ?
String.Format(CultureInfo.InvariantCulture,
"Amending this commit would produce a commit that is identical to its parent (id = {0})", parents[0].Id) :
"No changes; nothing to commit.");
}
}
Commit result = ObjectDatabase.CreateCommit(author, committer, message, tree, parents, options.PrettifyMessage, options.CommentaryChar);
Proxy.git_repository_state_cleanup(handle);
var logMessage = BuildCommitLogMessage(result, options.AmendPreviousCommit, isHeadOrphaned, parents.Count > 1);
UpdateHeadAndTerminalReference(result, logMessage);
return result;
}
private static string BuildCommitLogMessage(Commit commit, bool amendPreviousCommit, bool isHeadOrphaned, bool isMergeCommit)
{
string kind = string.Empty;
if (isHeadOrphaned)
{
kind = " (initial)";
}
else if (amendPreviousCommit)
{
kind = " (amend)";
}
else if (isMergeCommit)
{
kind = " (merge)";
}
return string.Format(CultureInfo.InvariantCulture, "commit{0}: {1}", kind, commit.MessageShort);
}
private void UpdateHeadAndTerminalReference(Commit commit, string reflogMessage)
{
Reference reference = Refs.Head;
while (true) //TODO: Implement max nesting level
{
if (reference is DirectReference)
{
Refs.UpdateTarget(reference, commit.Id, commit.Committer, reflogMessage);
return;
}
var symRef = (SymbolicReference) reference;
reference = symRef.Target;
if (reference == null)
{
Refs.Add(symRef.TargetIdentifier, commit.Id, commit.Committer, reflogMessage);
return;
}
}
}
private IEnumerable RetrieveParentsOfTheCommitBeingCreated(bool amendPreviousCommit)
{
if (amendPreviousCommit)
{
return Head.Tip.Parents;
}
if (Info.IsHeadUnborn)
{
return Enumerable.Empty();
}
var parents = new List { Head.Tip };
if (Info.CurrentOperation == CurrentOperation.Merge)
{
parents.AddRange(MergeHeads.Select(mh => mh.Tip));
}
return parents;
}
///
/// Clean the working tree by removing files that are not under version control.
///
public void RemoveUntrackedFiles()
{
var options = new GitCheckoutOpts
{
version = 1,
checkout_strategy = CheckoutStrategy.GIT_CHECKOUT_REMOVE_UNTRACKED
| CheckoutStrategy.GIT_CHECKOUT_ALLOW_CONFLICTS,
};
Proxy.git_checkout_index(Handle, new NullGitObjectSafeHandle(), ref options);
}
private void CleanupDisposableDependencies()
{
while (toCleanup.Count > 0)
{
toCleanup.Pop().SafeDispose();
}
}
internal T RegisterForCleanup(T disposable) where T : IDisposable
{
toCleanup.Push(disposable);
return disposable;
}
///
/// Gets the current LibGit2Sharp version.
///
/// The format of the version number is as follows:
/// Major.Minor.Patch-LibGit2Sharp_abbrev_hash-libgit2_abbrev_hash (x86|amd64 - features)
///
///
[Obsolete("This property will be removed in the next release. Use GlobalSettings.Version instead.")]
public static string Version
{
get { return GlobalSettings.Version.ToString(); }
}
///
/// Merges changes from commit into the branch pointed at by HEAD.
///
/// The commit to merge into the branch pointed at by HEAD.
/// The of who is performing the merge.
/// Specifies optional parameters controlling merge behavior; if null, the defaults are used.
/// The of the merge.
public MergeResult Merge(Commit commit, Signature merger, MergeOptions options)
{
Ensure.ArgumentNotNull(commit, "commit");
Ensure.ArgumentNotNull(merger, "merger");
options = options ?? new MergeOptions();
using (GitMergeHeadHandle mergeHeadHandle = Proxy.git_merge_head_from_id(Handle, commit.Id.Oid))
{
return Merge(new[] { mergeHeadHandle }, merger, options);
}
}
///
/// Merges changes from branch into the branch pointed at by HEAD.
///
/// The branch to merge into the branch pointed at by HEAD.
/// The of who is performing the merge.
/// Specifies optional parameters controlling merge behavior; if null, the defaults are used.
/// The of the merge.
public MergeResult Merge(Branch branch, Signature merger, MergeOptions options)
{
Ensure.ArgumentNotNull(branch, "branch");
Ensure.ArgumentNotNull(merger, "merger");
options = options ?? new MergeOptions();
using (ReferenceSafeHandle referencePtr = Refs.RetrieveReferencePtr(branch.CanonicalName))
using (GitMergeHeadHandle mergeHeadHandle = Proxy.git_merge_head_from_ref(Handle, referencePtr))
{
return Merge(new[] { mergeHeadHandle }, merger, options);
}
}
///
/// Merges changes from the commit into the branch pointed at by HEAD.
///
/// The commit to merge into the branch pointed at by HEAD.
/// The of who is performing the merge.
/// Specifies optional parameters controlling merge behavior; if null, the defaults are used.
/// The of the merge.
public MergeResult Merge(string committish, Signature merger, MergeOptions options)
{
Ensure.ArgumentNotNull(committish, "committish");
Ensure.ArgumentNotNull(merger, "merger");
options = options ?? new MergeOptions();
Commit commit = LookupCommit(committish);
return Merge(commit, merger, options);
}
///
/// Merge the current fetch heads into the branch pointed at by HEAD.
///
/// The of who is performing the merge.
/// Specifies optional parameters controlling merge behavior; if null, the defaults are used.
/// The of the merge.
internal MergeResult MergeFetchHeads(Signature merger, MergeOptions options)
{
Ensure.ArgumentNotNull(merger, "merger");
options = options ?? new MergeOptions();
// The current FetchHeads that are marked for merging.
FetchHead[] fetchHeads = Network.FetchHeads.Where(fetchHead => fetchHead.ForMerge).ToArray();
if (fetchHeads.Length == 0)
{
throw new LibGit2SharpException("Remote ref to merge from was not fetched.");
}
GitMergeHeadHandle[] mergeHeadHandles = fetchHeads.Select(fetchHead =>
Proxy.git_merge_head_from_fetchhead(Handle, fetchHead.RemoteCanonicalName, fetchHead.Url, fetchHead.Target.Id.Oid)).ToArray();
try
{
// Perform the merge.
return Merge(mergeHeadHandles, merger, options);
}
finally
{
// Cleanup.
foreach (GitMergeHeadHandle mergeHeadHandle in mergeHeadHandles)
{
mergeHeadHandle.Dispose();
}
}
}
///
/// Revert the specified commit.
///
/// If the revert is successful but there are no changes to commit,
/// then the will be .
/// If the revert is successful and there are changes to revert, then
/// the will be .
/// If the revert resulted in conflicts, then the
/// will be .
///
///
/// The to revert.
/// The of who is performing the revert.
/// controlling revert behavior.
/// The result of the revert.
public RevertResult Revert(Commit commit, Signature reverter, RevertOptions options)
{
Ensure.ArgumentNotNull(commit, "commit");
Ensure.ArgumentNotNull(reverter, "reverter");
if (Info.IsHeadUnborn)
{
throw new UnbornBranchException("Can not revert the commit. The Head doesn't point at a commit.");
}
options = options ?? new RevertOptions();
RevertResult result = null;
using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
{
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,
};
GitRevertOpts gitRevertOpts = new GitRevertOpts()
{
Mainline = (uint)options.Mainline,
MergeOpts = mergeOptions,
CheckoutOpts = checkoutOptionsWrapper.Options,
};
Proxy.git_revert(handle, commit.Id.Oid, gitRevertOpts);
if (Index.IsFullyMerged)
{
Commit revertCommit = null;
// Check if the revert generated any changes
// and set the revert status accordingly
bool anythingToRevert = Index.RetrieveStatus(
new StatusOptions()
{
DetectRenamesInIndex = false,
Show = StatusShowOption.IndexOnly
}).Any();
RevertStatus revertStatus = anythingToRevert ?
RevertStatus.Reverted : RevertStatus.NothingToRevert;
if (options.CommitOnSuccess)
{
if (!anythingToRevert)
{
// If there were no changes to revert, and we are
// asked to commit the changes, then cleanup
// the repository state (following command line behavior).
Proxy.git_repository_state_cleanup(handle);
}
else
{
revertCommit = this.Commit(
Info.Message,
author: reverter,
committer: reverter,
options: null);
}
}
result = new RevertResult(revertStatus, revertCommit);
}
else
{
result = new RevertResult(RevertStatus.Conflicts);
}
}
return result;
}
///
/// Cherry-picks the specified commit.
///
/// The to cherry-pick.
/// The of who is performing the cherry pick.
/// controlling cherry pick behavior.
/// The result of the cherry pick.
public CherryPickResult CherryPick(Commit commit, Signature committer, CherryPickOptions options)
{
Ensure.ArgumentNotNull(commit, "commit");
Ensure.ArgumentNotNull(committer, "committer");
options = options ?? new CherryPickOptions();
CherryPickResult result = null;
using (var checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
{
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,
};
var gitCherryPickOpts = new GitCherryPickOptions()
{
Mainline = (uint)options.Mainline,
MergeOpts = mergeOptions,
CheckoutOpts = checkoutOptionsWrapper.Options,
};
Proxy.git_cherrypick(handle, commit.Id.Oid, gitCherryPickOpts);
if (Index.IsFullyMerged)
{
Commit cherryPickCommit = null;
if (options.CommitOnSuccess)
{
cherryPickCommit = this.Commit(Info.Message, commit.Author, committer, null);
}
result = new CherryPickResult(CherryPickStatus.CherryPicked, cherryPickCommit);
}
else
{
result = new CherryPickResult(CherryPickStatus.Conflicts);
}
}
return result;
}
private FastForwardStrategy FastForwardStrategyFromMergePreference(GitMergePreference preference)
{
switch (preference)
{
case GitMergePreference.GIT_MERGE_PREFERENCE_NONE:
return FastForwardStrategy.Default;
case GitMergePreference.GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY:
return FastForwardStrategy.FastForwardOnly;
case GitMergePreference.GIT_MERGE_PREFERENCE_NO_FASTFORWARD:
return FastForwardStrategy.NoFastFoward;
default:
throw new InvalidOperationException(String.Format("Unknown merge preference: {0}", preference));
}
}
///
/// Internal implementation of merge.
///
/// Merge heads to operate on.
/// The of who is performing the merge.
/// Specifies optional parameters controlling merge behavior; if null, the defaults are used.
/// The of the merge.
private MergeResult Merge(GitMergeHeadHandle[] mergeHeads, Signature merger, MergeOptions options)
{
GitMergeAnalysis mergeAnalysis;
GitMergePreference mergePreference;
Proxy.git_merge_analysis(Handle, mergeHeads, out mergeAnalysis, out mergePreference);
MergeResult mergeResult = null;
if ((mergeAnalysis & GitMergeAnalysis.GIT_MERGE_ANALYSIS_UP_TO_DATE) == GitMergeAnalysis.GIT_MERGE_ANALYSIS_UP_TO_DATE)
{
return new MergeResult(MergeStatus.UpToDate);
}
FastForwardStrategy fastForwardStrategy = (options.FastForwardStrategy != FastForwardStrategy.Default) ?
options.FastForwardStrategy : FastForwardStrategyFromMergePreference(mergePreference);
switch(fastForwardStrategy)
{
case FastForwardStrategy.Default:
if (mergeAnalysis.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_FASTFORWARD))
{
if (mergeHeads.Length != 1)
{
// We should not reach this code unless there is a bug somewhere.
throw new LibGit2SharpException("Unable to perform Fast-Forward merge with mith multiple merge heads.");
}
mergeResult = FastForwardMerge(mergeHeads[0], merger, options);
}
else if (mergeAnalysis.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_NORMAL))
{
mergeResult = NormalMerge(mergeHeads, merger, options);
}
break;
case FastForwardStrategy.FastForwardOnly:
if (mergeAnalysis.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_FASTFORWARD))
{
if (mergeHeads.Length != 1)
{
// We should not reach this code unless there is a bug somewhere.
throw new LibGit2SharpException("Unable to perform Fast-Forward merge with mith multiple merge heads.");
}
mergeResult = FastForwardMerge(mergeHeads[0], merger, options);
}
else
{
// TODO: Maybe this condition should rather be indicated through the merge result
// instead of throwing an exception.
throw new NonFastForwardException("Cannot perform fast-forward merge.");
}
break;
case FastForwardStrategy.NoFastFoward:
if (mergeAnalysis.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_NORMAL))
{
mergeResult = NormalMerge(mergeHeads, merger, options);
}
break;
default:
throw new NotImplementedException(
string.Format(CultureInfo.InvariantCulture, "Unknown fast forward strategy: {0}", mergeAnalysis));
}
if (mergeResult == null)
{
throw new NotImplementedException(
string.Format(CultureInfo.InvariantCulture, "Unknown merge analysis: {0}", options.FastForwardStrategy));
}
return mergeResult;
}
///
/// Perform a normal merge (i.e. a non-fast-forward merge).
///
/// The merge head handles to merge.
/// The of who is performing the merge.
/// Specifies optional parameters controlling merge behavior; if null, the defaults are used.
/// The of the merge.
private MergeResult NormalMerge(GitMergeHeadHandle[] mergeHeads, Signature merger, MergeOptions options)
{
MergeResult mergeResult;
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 (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
{
var checkoutOpts = checkoutOptionsWrapper.Options;
Proxy.git_merge(Handle, mergeHeads, mergeOptions, checkoutOpts);
}
if (Index.IsFullyMerged)
{
Commit mergeCommit = null;
if (options.CommitOnSuccess)
{
// Commit the merge
mergeCommit = Commit(Info.Message, author: merger, committer: merger, options: null);
}
mergeResult = new MergeResult(MergeStatus.NonFastForward, mergeCommit);
}
else
{
mergeResult = new MergeResult(MergeStatus.Conflicts);
}
return mergeResult;
}
///
/// Perform a fast-forward merge.
///
/// The merge head handle to fast-forward merge.
/// The of who is performing the merge.
/// Options controlling merge behavior.
/// The of the merge.
private MergeResult FastForwardMerge(GitMergeHeadHandle mergeHead, Signature merger, MergeOptions options)
{
ObjectId id = Proxy.git_merge_head_id(mergeHead);
Commit fastForwardCommit = (Commit) Lookup(id, ObjectType.Commit);
Ensure.GitObjectIsNotNull(fastForwardCommit, id.Sha);
CheckoutTree(fastForwardCommit.Tree, null, new FastForwardCheckoutOptionsAdapter(options));
var reference = Refs.Head.ResolveToDirectReference();
// TODO: This reflog entry could be more specific
string refLogEntry = string.Format(
CultureInfo.InvariantCulture, "merge {0}: Fast-forward", fastForwardCommit.Sha);
if (reference == null)
{
// Reference does not exist, create it.
Refs.Add(Refs.Head.TargetIdentifier, fastForwardCommit.Id, merger, refLogEntry);
}
else
{
// Update target reference.
Refs.UpdateTarget(reference, fastForwardCommit.Id.Sha, merger, refLogEntry);
}
return new MergeResult(MergeStatus.FastForward, fastForwardCommit);
}
///
/// Gets the references to the tips that are currently being merged.
///
internal IEnumerable MergeHeads
{
get
{
int i = 0;
return Proxy.git_repository_mergehead_foreach(Handle,
commitId => new MergeHead(this, commitId, i++));
}
}
internal StringComparer PathComparer
{
get { return pathCase.Value.Comparer; }
}
internal bool PathStartsWith(string path, string value)
{
return pathCase.Value.StartsWith(path, value);
}
internal FilePath[] ToFilePaths(IEnumerable paths)
{
if (paths == null)
{
return null;
}
var filePaths = new List();
foreach (string path in paths)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("At least one provided path is either null or empty.", "paths");
}
filePaths.Add(this.BuildRelativePathFrom(path));
}
if (filePaths.Count == 0)
{
throw new ArgumentException("No path has been provided.", "paths");
}
return filePaths.ToArray();
}
private string DebuggerDisplay
{
get
{
return string.Format(CultureInfo.InvariantCulture,
"{0} = \"{1}\"",
Info.IsBare ? "Gitdir" : "Workdir",
Info.IsBare ? Info.Path : Info.WorkingDirectory);
}
}
}
}