using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
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 RepositoryHandle handle;
private readonly Lazy 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 Lazy rebaseOperation;
private readonly Stack toCleanup = new Stack();
private readonly Ignore ignore;
private readonly SubmoduleCollection submodules;
private readonly WorktreeCollection worktrees;
private readonly Lazy pathCase;
[Flags]
private enum RepositoryRequiredParameter
{
None = 0,
Path = 1,
Options = 2,
}
///
/// Initializes a new instance of the class
/// that does not point to an on-disk Git repository. This is
/// suitable only for custom, in-memory Git repositories that are
/// configured with custom object database, reference database and/or
/// configuration backends.
///
public Repository()
: this(null, null, RepositoryRequiredParameter.None)
{
}
///
/// Initializes a new instance of the class.
/// 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.
///
public Repository(string path)
: this(path, null, RepositoryRequiredParameter.Path)
{ }
///
/// Initializes a new instance of the class,
/// providing optional behavioral overrides through the
/// parameter.
/// For a standard repository, may
/// 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) :
this(path, options, RepositoryRequiredParameter.Path | RepositoryRequiredParameter.Options)
{
}
internal Repository(WorktreeHandle worktreeHandle)
{
try
{
handle = Proxy.git_repository_open_from_worktree(worktreeHandle);
RegisterForCleanup(handle);
RegisterForCleanup(worktreeHandle);
isBare = Proxy.git_repository_is_bare(handle);
Func indexBuilder = () => new Index(this);
string configurationGlobalFilePath = null;
string configurationXDGFilePath = null;
string configurationSystemFilePath = null;
if (!isBare)
{
index = new Lazy(() => 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,
null,
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));
rebaseOperation = new Lazy(() => new Rebase(this));
pathCase = new Lazy(() => new PathCase(this));
submodules = new SubmoduleCollection(this);
worktrees = new WorktreeCollection(this);
}
catch
{
CleanupDisposableDependencies();
throw;
}
}
private Repository(string path, RepositoryOptions options, RepositoryRequiredParameter requiredParameter)
{
if ((requiredParameter & RepositoryRequiredParameter.Path) == RepositoryRequiredParameter.Path)
{
Ensure.ArgumentNotNullOrEmptyString(path, "path");
}
if ((requiredParameter & RepositoryRequiredParameter.Options) == RepositoryRequiredParameter.Options)
{
Ensure.ArgumentNotNull(options, "options");
}
try
{
handle = (path != null) ? Proxy.git_repository_open(path) : Proxy.git_repository_new();
RegisterForCleanup(handle);
isBare = Proxy.git_repository_is_bare(handle);
/* TODO: bug in libgit2, update when fixed by
* https://github.com/libgit2/libgit2/pull/2970
*/
if (path == null)
{
isBare = true;
}
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.");
}
if (!isWorkDirNull)
{
isBare = false;
}
if (!isIndexNull)
{
indexBuilder = () => new Index(this, options.IndexPath);
}
if (!isWorkDirNull)
{
Proxy.git_repository_set_workdir(handle, options.WorkingDirectoryPath);
}
if (options.Identity != null)
{
Proxy.git_repository_set_ident(handle, options.Identity.Name, options.Identity.Email);
}
}
if (!isBare)
{
index = new Lazy(() => 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,
null,
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));
rebaseOperation = new Lazy(() => new Rebase(this));
pathCase = new Lazy(() => new PathCase(this));
submodules = new SubmoduleCollection(this);
worktrees = new WorktreeCollection(this);
EagerlyLoadComponentsWithSpecifiedPaths(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.ArgumentNotNull(path, "path");
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
try
{
Proxy.git_repository_open_ext(path, RepositoryOpenFlags.NoSearch, null);
}
catch (RepositoryNotFoundException)
{
return false;
}
return true;
}
private void EagerlyLoadComponentsWithSpecifiedPaths(RepositoryOptions options)
{
if (options == null)
{
return;
}
if (!string.IsNullOrEmpty(options.IndexPath))
{
// Another dirty hack to avoid warnings
if (Index.Count < 0)
{
throw new InvalidOperationException("Unexpected state.");
}
}
}
internal RepositoryHandle 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 != null ? index.Value : null;
}
}
///
/// 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; }
}
///
/// Provides access to rebase functionality for a repository.
///
public Rebase Rebase
{
get
{
return rebaseOperation.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; }
}
///
/// Worktrees in the repository.
///
public WorktreeCollection Worktrees
{
get { return worktrees; }
}
#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.
/// The path to the created repository.
public static string Init(string path)
{
return Init(path, false);
}
///
/// 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)
{
Ensure.ArgumentNotNullOrEmptyString(path, "path");
using (RepositoryHandle 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 (RepositoryHandle 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, string knownPath)
{
Ensure.ArgumentNotNull(id, "id");
using (ObjectHandle obj = Proxy.git_object_lookup(handle, id, type))
{
if (obj == null || obj.IsNull)
{
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 (ObjectHandle 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.Peel(lookUpOptions.HasFlag(LookUpOptions.ThrowWhenCanNotBeDereferencedToACommit));
}
return obj;
}
internal Commit LookupCommit(string committish)
{
return (Commit)Lookup(committish,
GitObjectType.Any,
LookUpOptions.ThrowWhenNoGitObjectHasBeenFound |
LookUpOptions.DereferenceResultToCommit |
LookUpOptions.ThrowWhenCanNotBeDereferencedToACommit);
}
///
/// Lists the Remote Repository References.
///
///
/// Does not require a local Repository. The retrieved
///
/// throws in this case.
///
/// The url to list from.
/// The references in the remote repository.
public static IEnumerable ListRemoteReferences(string url)
{
return ListRemoteReferences(url, null);
}
///
/// Lists the Remote Repository References.
///
///
/// Does not require a local Repository. The retrieved
///
/// throws in this case.
///
/// The url to list from.
/// The used to connect to remote repository.
/// The references in the remote repository.
public static IEnumerable ListRemoteReferences(string url, CredentialsHandler credentialsProvider)
{
Ensure.ArgumentNotNull(url, "url");
using (RepositoryHandle repositoryHandle = Proxy.git_repository_new())
using (RemoteHandle remoteHandle = Proxy.git_remote_create_anonymous(repositoryHandle, url))
{
var gitCallbacks = new GitRemoteCallbacks { version = 1 };
var proxyOptions = new GitProxyOptions { Version = 1 };
if (credentialsProvider != null)
{
var callbacks = new RemoteCallbacks(credentialsProvider);
gitCallbacks = callbacks.GenerateCallbacks();
}
Proxy.git_remote_connect(remoteHandle, GitDirection.Fetch, ref gitCallbacks, ref proxyOptions);
return Proxy.git_remote_ls(null, remoteHandle);
}
}
///
/// 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, or null if no repository was found.
public static string Discover(string startingPath)
{
FilePath discoveredPath = Proxy.git_repository_discover(startingPath);
if (discoveredPath == null)
{
return null;
}
return discoveredPath.Native;
}
///
/// Clone using default options.
///
/// This exception is thrown when there
/// is an error is encountered while recursively cloning submodules. The inner exception
/// will contain the original exception. The initially cloned repository would
/// be reported through the
/// property."
/// Exception thrown when the cancelling
/// the clone of the initial repository."
/// URI for the remote repository
/// Local path to clone into
/// The path to the created repository.
public static string Clone(string sourceUrl, string workdirPath)
{
return Clone(sourceUrl, workdirPath, null);
}
///
/// Clone with specified options.
///
/// This exception is thrown when there
/// is an error is encountered while recursively cloning submodules. The inner exception
/// will contain the original exception. The initially cloned repository would
/// be reported through the
/// property."
/// Exception thrown when the cancelling
/// the clone of the initial repository."
/// 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)
{
Ensure.ArgumentNotNull(sourceUrl, "sourceUrl");
Ensure.ArgumentNotNull(workdirPath, "workdirPath");
options = options ?? new CloneOptions();
// context variable that contains information on the repository that
// we are cloning.
var context = new RepositoryOperationContext(Path.GetFullPath(workdirPath), sourceUrl);
// Notify caller that we are starting to work with the current repository.
bool continueOperation = OnRepositoryOperationStarting(options.RepositoryOperationStarting,
context);
if (!continueOperation)
{
throw new UserCancelledException("Clone cancelled by the user.");
}
using (var checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
using (var fetchOptionsWrapper = new GitFetchOptionsWrapper())
{
var gitCheckoutOptions = checkoutOptionsWrapper.Options;
var gitFetchOptions = fetchOptionsWrapper.Options;
gitFetchOptions.ProxyOptions = new GitProxyOptions { Version = 1 };
gitFetchOptions.RemoteCallbacks = new RemoteCallbacks(options).GenerateCallbacks();
if (options.FetchOptions != null && options.FetchOptions.CustomHeaders != null)
{
gitFetchOptions.CustomHeaders =
GitStrArrayManaged.BuildFrom(options.FetchOptions.CustomHeaders);
}
var cloneOpts = new GitCloneOptions
{
Version = 1,
Bare = options.IsBare ? 1 : 0,
CheckoutOpts = gitCheckoutOptions,
FetchOpts = gitFetchOptions,
};
string clonedRepoPath;
try
{
cloneOpts.CheckoutBranch = StrictUtf8Marshaler.FromManaged(options.BranchName);
using (RepositoryHandle repo = Proxy.git_clone(sourceUrl, workdirPath, ref cloneOpts))
{
clonedRepoPath = Proxy.git_repository_path(repo).Native;
}
}
finally
{
EncodingMarshaler.Cleanup(cloneOpts.CheckoutBranch);
}
// Notify caller that we are done with the current repository.
OnRepositoryOperationCompleted(options.RepositoryOperationCompleted,
context);
// Recursively clone submodules if requested.
try
{
RecursivelyCloneSubmodules(options, clonedRepoPath, 1);
}
catch (Exception ex)
{
throw new RecurseSubmodulesException("The top level repository was cloned, but there was an error cloning its submodules.",
ex,
clonedRepoPath);
}
return clonedRepoPath;
}
}
///
/// Recursively clone submodules if directed to do so by the clone options.
///
/// Options controlling clone behavior.
/// Path of the parent repository.
/// The current depth of the recursion.
private static void RecursivelyCloneSubmodules(CloneOptions options, string repoPath, int recursionDepth)
{
if (options.RecurseSubmodules)
{
List submodules = new List();
using (Repository repo = new Repository(repoPath))
{
SubmoduleUpdateOptions updateOptions = new SubmoduleUpdateOptions()
{
Init = true,
CredentialsProvider = options.CredentialsProvider,
OnCheckoutProgress = options.OnCheckoutProgress,
OnProgress = options.OnProgress,
OnTransferProgress = options.OnTransferProgress,
OnUpdateTips = options.OnUpdateTips,
};
string parentRepoWorkDir = repo.Info.WorkingDirectory;
// Iterate through the submodules (where the submodule is in the index),
// and clone them.
foreach (var sm in repo.Submodules.Where(sm => sm.RetrieveStatus().HasFlag(SubmoduleStatus.InIndex)))
{
string fullSubmodulePath = Path.Combine(parentRepoWorkDir, sm.Path);
// Resolve the URL in the .gitmodule file to the one actually used
// to clone
string resolvedUrl = Proxy.git_submodule_resolve_url(repo.Handle, sm.Url);
var context = new RepositoryOperationContext(fullSubmodulePath,
resolvedUrl,
parentRepoWorkDir,
sm.Name,
recursionDepth);
bool continueOperation = OnRepositoryOperationStarting(options.RepositoryOperationStarting,
context);
if (!continueOperation)
{
throw new UserCancelledException("Recursive clone of submodules was cancelled.");
}
repo.Submodules.Update(sm.Name, updateOptions);
OnRepositoryOperationCompleted(options.RepositoryOperationCompleted,
context);
submodules.Add(Path.Combine(repo.Info.WorkingDirectory, sm.Path));
}
}
// If we are continuing the recursive operation, then
// recurse into nested submodules.
// Check submodules to see if they have their own submodules.
foreach (string submodule in submodules)
{
RecursivelyCloneSubmodules(options, submodule, recursionDepth + 1);
}
}
}
///
/// If a callback has been provided to notify callers that we are
/// either starting to work on a repository.
///
/// The callback to notify change.
/// Context of the repository this operation affects.
/// true to continue the operation, false to cancel.
private static bool OnRepositoryOperationStarting(
RepositoryOperationStarting repositoryChangedCallback,
RepositoryOperationContext context)
{
bool continueOperation = true;
if (repositoryChangedCallback != null)
{
continueOperation = repositoryChangedCallback(context);
}
return continueOperation;
}
private static void OnRepositoryOperationCompleted(
RepositoryOperationCompleted repositoryChangedCallback,
RepositoryOperationContext context)
{
if (repositoryChangedCallback != null)
{
repositoryChangedCallback(context);
}
}
///
/// 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 tree.
///
/// The to checkout.
/// The paths to checkout.
/// Collection of parameters controlling checkout behavior.
public void Checkout(Tree tree, IEnumerable paths, CheckoutOptions options)
{
CheckoutTree(tree, paths != null ? paths.ToList() : null, options);
}
///
/// 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.
public void Reset(ResetMode resetMode, Commit commit)
{
Reset(resetMode, commit, new CheckoutOptions());
}
///
/// Sets 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.
/// Collection of parameters controlling checkout behavior.
public void Reset(ResetMode resetMode, Commit commit, CheckoutOptions opts)
{
Ensure.ArgumentNotNull(commit, "commit");
Ensure.ArgumentNotNull(opts, "opts");
using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(opts))
{
var options = checkoutOptionsWrapper.Options;
Proxy.git_reset(handle, commit.Id, resetMode, ref options);
}
}
///
/// 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");
var listOfPaths = paths.ToList();
// If there are no paths, then there is nothing to do.
if (listOfPaths.Count == 0)
{
return;
}
Commit commit = LookupCommit(committishOrBranchSpec);
CheckoutTree(commit.Tree, listOfPaths, checkoutOptions ?? new CheckoutOptions());
}
///
/// 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_index_write_tree(Index.Handle);
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 (options.AmendPreviousCommit ?
new EmptyCommitException("Amending this commit would produce a commit that is identical to its parent (id = {0})", parents[0].Id) :
new EmptyCommitException("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, reflogMessage);
return;
}
var symRef = (SymbolicReference)reference;
reference = symRef.Target;
if (reference == null)
{
Refs.Add(symRef.TargetIdentifier, commit.Id, 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 unsafe 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 ObjectHandle(null, false), 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;
}
///
/// 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 (AnnotatedCommitHandle annotatedCommitHandle = Proxy.git_annotated_commit_lookup(Handle, commit.Id.Oid))
{
return Merge(new[] { annotatedCommitHandle }, 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 (ReferenceHandle referencePtr = Refs.RetrieveReferencePtr(branch.CanonicalName))
using (AnnotatedCommitHandle annotatedCommitHandle = Proxy.git_annotated_commit_from_ref(Handle, referencePtr))
{
return Merge(new[] { annotatedCommitHandle }, 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 reference that was recently fetched. This will merge
/// the branch on the fetched remote that corresponded to the
/// current local branch when we did the fetch. This is the
/// second step in performing a pull operation (after having
/// performed said fetch).
///
/// 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 MergeFetchedRefs(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)
{
var expectedRef = this.Head.UpstreamBranchCanonicalName;
throw new MergeFetchHeadNotFoundException("The current branch is configured to merge with the reference '{0}' from the remote, but this reference was not fetched.",
expectedRef);
}
AnnotatedCommitHandle[] annotatedCommitHandles = fetchHeads.Select(fetchHead =>
Proxy.git_annotated_commit_from_fetchhead(Handle, fetchHead.RemoteCanonicalName, fetchHead.Url, fetchHead.Target.Id.Oid)).ToArray();
try
{
// Perform the merge.
return Merge(annotatedCommitHandles, merger, options);
}
finally
{
// Cleanup.
foreach (AnnotatedCommitHandle annotatedCommitHandle in annotatedCommitHandles)
{
annotatedCommitHandle.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 ? GitMergeFlag.GIT_MERGE_FIND_RENAMES :
GitMergeFlag.GIT_MERGE_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 = 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 ? GitMergeFlag.GIT_MERGE_FIND_RENAMES :
GitMergeFlag.GIT_MERGE_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.NoFastForward;
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(AnnotatedCommitHandle[] annotatedCommits, Signature merger, MergeOptions options)
{
GitMergeAnalysis mergeAnalysis;
GitMergePreference mergePreference;
Proxy.git_merge_analysis(Handle, annotatedCommits, 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 (annotatedCommits.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(annotatedCommits[0], options);
}
else if (mergeAnalysis.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_NORMAL))
{
mergeResult = NormalMerge(annotatedCommits, merger, options);
}
break;
case FastForwardStrategy.FastForwardOnly:
if (mergeAnalysis.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_FASTFORWARD))
{
if (annotatedCommits.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(annotatedCommits[0], 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.NoFastForward:
if (mergeAnalysis.HasFlag(GitMergeAnalysis.GIT_MERGE_ANALYSIS_NORMAL))
{
mergeResult = NormalMerge(annotatedCommits, merger, options);
}
break;
default:
throw new NotImplementedException(
string.Format(CultureInfo.InvariantCulture, "Unknown fast forward strategy: {0}", fastForwardStrategy));
}
if (mergeResult == null)
{
throw new NotImplementedException(
string.Format(CultureInfo.InvariantCulture, "Unknown merge analysis: {0}", mergeAnalysis));
}
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(AnnotatedCommitHandle[] annotatedCommits, Signature merger, MergeOptions options)
{
MergeResult mergeResult;
GitMergeFlag treeFlags = options.FindRenames ? GitMergeFlag.GIT_MERGE_FIND_RENAMES
: GitMergeFlag.GIT_MERGE_NORMAL;
if (options.FailOnConflict)
{
treeFlags |= GitMergeFlag.GIT_MERGE_FAIL_ON_CONFLICT;
}
if (options.SkipReuc)
{
treeFlags |= GitMergeFlag.GIT_MERGE_SKIP_REUC;
}
var fileFlags = options.IgnoreWhitespaceChange
? GitMergeFileFlag.GIT_MERGE_FILE_IGNORE_WHITESPACE_CHANGE
: GitMergeFileFlag.GIT_MERGE_FILE_DEFAULT;
var mergeOptions = new GitMergeOpts
{
Version = 1,
MergeFileFavorFlags = options.MergeFileFavor,
MergeTreeFlags = treeFlags,
RenameThreshold = (uint)options.RenameThreshold,
TargetLimit = (uint)options.TargetLimit,
FileFlags = fileFlags
};
bool earlyStop;
using (GitCheckoutOptsWrapper checkoutOptionsWrapper = new GitCheckoutOptsWrapper(options))
{
var checkoutOpts = checkoutOptionsWrapper.Options;
Proxy.git_merge(Handle, annotatedCommits, mergeOptions, checkoutOpts, out earlyStop);
}
if (earlyStop)
{
return new MergeResult(MergeStatus.Conflicts);
}
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.
/// Options controlling merge behavior.
/// The of the merge.
private MergeResult FastForwardMerge(AnnotatedCommitHandle annotatedCommit, MergeOptions options)
{
ObjectId id = Proxy.git_annotated_commit_id(annotatedCommit);
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, refLogEntry);
}
else
{
// Update target reference.
Refs.UpdateTarget(reference, fastForwardCommit.Id.Sha, 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 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();
}
///
/// Retrieves the state of a file in the working directory, comparing it against the staging area and the latest commit.
///
/// The relative path within the working directory to the file.
/// A representing the state of the parameter.
public FileStatus RetrieveStatus(string filePath)
{
Ensure.ArgumentNotNullOrEmptyString(filePath, "filePath");
string relativePath = this.BuildRelativePathFrom(filePath);
return Proxy.git_status_file(Handle, relativePath);
}
///
/// Retrieves the state of all files in the working directory, comparing them against the staging area and the latest commit.
///
/// If set, the options that control the status investigation.
/// A holding the state of all the files.
public RepositoryStatus RetrieveStatus(StatusOptions options)
{
ReloadFromDisk();
return new RepositoryStatus(this, options);
}
internal void ReloadFromDisk()
{
Proxy.git_index_read(Index.Handle);
}
internal void AddToIndex(string relativePath)
{
if (!Submodules.TryStage(relativePath, true))
{
Proxy.git_index_add_bypath(Index.Handle, relativePath);
}
}
internal string RemoveFromIndex(string relativePath)
{
Proxy.git_index_remove_bypath(Index.Handle, relativePath);
return relativePath;
}
internal void UpdatePhysicalIndex()
{
Proxy.git_index_write(Index.Handle);
}
///
/// Finds the most recent annotated tag that is reachable from a commit.
///
/// If the tag points to the commit, then only the tag is shown. Otherwise,
/// it suffixes the tag name with the number of additional commits on top
/// of the tagged object and the abbreviated object name of the most recent commit.
///
///
/// Optionally, the parameter allow to tweak the
/// search strategy (considering lightweight tags, or even branches as reference points)
/// and the formatting of the returned identifier.
///
///
/// The commit to be described.
/// Determines how the commit will be described.
/// A descriptive identifier for the commit based on the nearest annotated tag.
public string Describe(Commit commit, DescribeOptions options)
{
Ensure.ArgumentNotNull(commit, "commit");
Ensure.ArgumentNotNull(options, "options");
return Proxy.git_describe_commit(handle, commit.Id, options);
}
///
/// Parse an extended SHA-1 expression and retrieve the object and the reference
/// mentioned in the revision (if any).
///
/// An extended SHA-1 expression for the object to look up
/// The reference mentioned in the revision (if any)
/// The object which the revision resolves to
public void RevParse(string revision, out Reference reference, out GitObject obj)
{
var handles = Proxy.git_revparse_ext(Handle, revision);
if (handles == null)
{
Ensure.GitObjectIsNotNull(null, revision);
}
using (var objH = handles.Item1)
using (var refH = handles.Item2)
{
reference = refH.IsNull ? null : Reference.BuildFromPtr(refH, this);
obj = GitObject.BuildFrom(this, Proxy.git_object_id(objH), Proxy.git_object_type(objH), PathFromRevparseSpec(revision));
}
}
private string DebuggerDisplay
{
get
{
return string.Format(CultureInfo.InvariantCulture,
"{0} = \"{1}\"",
Info.IsBare ? "Gitdir" : "Workdir",
Info.IsBare ? Info.Path : Info.WorkingDirectory);
}
}
}
}