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.Compat;
using LibGit2Sharp.Core.Handles;
using LibGit2Sharp.Handlers;
namespace LibGit2Sharp
{
///
/// A Repository is the primary interface into a git repository
///
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public 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 static readonly Lazy versionRetriever = new Lazy(RetrieveVersion);
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;
}
}
///
/// Gets the conflicts that exist.
///
[Obsolete("This property will be removed in the next release. Please use Index.Conflicts instead.")]
public ConflictCollection Conflicts
{
get
{
return Index.Conflicts;
}
}
///
/// 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);
GC.SuppressFinalize(this);
}
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
protected virtual 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.
/// Overrides to the way a repository is opened.
/// a new instance of the class. The client code is responsible for calling on this instance.
public static Repository Init(string path, bool isBare = false, RepositoryOptions options = null)
{
Ensure.ArgumentNotNullOrEmptyString(path, "path");
using (RepositorySafeHandle repo = Proxy.git_repository_init(path, isBare))
{
FilePath repoPath = Proxy.git_repository_path(repo);
return new Repository(repoPath.Native, options);
}
}
///
/// 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");
GitObjectSafeHandle obj = null;
try
{
obj = Proxy.git_object_lookup(handle, id, type);
if (obj == null)
{
return null;
}
return GitObject.BuildFrom(this, id, Proxy.git_object_type(obj), knownPath);
}
finally
{
obj.SafeDispose();
}
}
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, "commitOrBranchSpec");
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;
}
///
/// Lookup a commit by its SHA or name, or throw if a commit is not found.
///
/// A revparse spec for the commit.
/// The commit.
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
/// True will result in a bare clone, false a full clone.
/// If true, the origin's HEAD will be checked out. This only applies
/// to non-bare repositories.
/// Handler for network transfer and indexing progress information
/// Handler for checkout progress information
/// Overrides to the way a repository is opened.
/// Credentials to use for user/pass authentication
///
public static Repository Clone(string sourceUrl, string workdirPath,
bool bare = false,
bool checkout = true,
TransferProgressHandler onTransferProgress = null,
CheckoutProgressHandler onCheckoutProgress = null,
RepositoryOptions options = null,
Credentials credentials = null)
{
var cloneOpts = new GitCloneOptions
{
Bare = bare ? 1 : 0,
TransferProgressCallback = TransferCallbacks.GenerateCallback(onTransferProgress),
CheckoutOpts =
{
version = 1,
progress_cb =
CheckoutCallbacks.GenerateCheckoutCallbacks(onCheckoutProgress),
checkout_strategy = checkout
? CheckoutStrategy.GIT_CHECKOUT_SAFE_CREATE
: CheckoutStrategy.GIT_CHECKOUT_NONE
},
};
if (credentials != null)
{
cloneOpts.CredAcquireCallback =
(out IntPtr cred, IntPtr url, IntPtr username_from_url, uint types, IntPtr payload) =>
NativeMethods.git_cred_userpass_plaintext_new(out cred, credentials.Username, credentials.Password);
}
using(Proxy.git_clone(sourceUrl, workdirPath, cloneOpts)) {}
// To be safe, make sure the credential callback is kept until
// alive until at least this point.
GC.KeepAlive(cloneOpts.CredAcquireCallback);
return new Repository(workdirPath, options);
}
///
/// Checkout the specified , reference or SHA.
///
/// A revparse spec for the commit or branch to checkout.
/// controlling checkout behavior.
/// that checkout progress is reported through.
/// The that was checked out.
public Branch Checkout(string committishOrBranchSpec, CheckoutOptions checkoutOptions, CheckoutProgressHandler onCheckoutProgress)
{
Ensure.ArgumentNotNullOrEmptyString(committishOrBranchSpec, "committishOrBranchSpec");
Branch branch = TryResolveBranch(committishOrBranchSpec);
if (branch != null)
{
return Checkout(branch, checkoutOptions, onCheckoutProgress);
}
var previousHeadName = Info.IsHeadDetached ? Head.Tip.Sha : Head.Name;
Commit commit = LookupCommit(committishOrBranchSpec);
CheckoutTree(commit.Tree, checkoutOptions, onCheckoutProgress);
// Update HEAD.
Refs.UpdateTarget("HEAD", commit.Id.Sha);
if (committishOrBranchSpec != "HEAD")
{
LogCheckout(previousHeadName, commit.Id, committishOrBranchSpec);
}
return Head;
}
private Branch TryResolveBranch(string committishOrBranchSpec)
{
if (committishOrBranchSpec == "HEAD")
{
return Head;
}
try
{
return Branches[committishOrBranchSpec];
}
catch (InvalidSpecificationException)
{
return null;
}
}
///
/// 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.
/// that checkout progress is reported through.
/// The that was checked out.
public Branch Checkout(Branch branch, CheckoutOptions checkoutOptions, CheckoutProgressHandler onCheckoutProgress)
{
Ensure.ArgumentNotNull(branch, "branch");
// Make sure this is not an unborn branch.
if (branch.Tip == null)
{
throw new OrphanedHeadException(
string.Format(CultureInfo.InvariantCulture,
"The tip of branch '{0}' is null. There's nothing to checkout.", branch.Name));
}
var branchIsCurrentRepositoryHead = branch.IsCurrentRepositoryHead;
var previousHeadName = Info.IsHeadDetached ? Head.Tip.Sha : Head.Name;
CheckoutTree(branch.Tip.Tree, checkoutOptions, onCheckoutProgress);
// Update HEAD.
if (!branch.IsRemote && !(branch is DetachedHead) &&
string.Equals(Refs[branch.CanonicalName].TargetIdentifier, branch.Tip.Id.Sha,
StringComparison.OrdinalIgnoreCase))
{
Refs.UpdateTarget("HEAD", branch.CanonicalName);
}
else
{
Refs.UpdateTarget("HEAD", branch.Tip.Id.Sha);
}
if (!branchIsCurrentRepositoryHead)
{
LogCheckout(previousHeadName, branch);
}
return Head;
}
private void LogCheckout(string previousHeadName, Branch newHead)
{
LogCheckout(previousHeadName, newHead.Tip.Id, newHead.Name);
}
private void LogCheckout(string previousHeadName, ObjectId newHeadTip, string newHeadSpec)
{
// Compute reflog message
string reflogMessage = string.Format("checkout: moving from {0} to {1}", previousHeadName, newHeadSpec);
// Log checkout
Refs.Log(Refs.Head).Append(newHeadTip, reflogMessage);
}
///
/// 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.
/// that checkout progress is reported through.
private void CheckoutTree(Tree tree, CheckoutOptions checkoutOptions, CheckoutProgressHandler onCheckoutProgress)
{
GitCheckoutOpts options = new GitCheckoutOpts
{
version = 1,
checkout_strategy = CheckoutStrategy.GIT_CHECKOUT_SAFE,
progress_cb = CheckoutCallbacks.GenerateCheckoutCallbacks(onCheckoutProgress)
};
if (checkoutOptions.HasFlag(CheckoutOptions.Force))
{
options.checkout_strategy = CheckoutStrategy.GIT_CHECKOUT_FORCE;
}
Proxy.git_checkout_tree(this.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(ResetOptions resetOptions, Commit commit)
{
Ensure.ArgumentNotNull(commit, "commit");
Proxy.git_reset(handle, commit.Id, resetOptions);
Refs.Log(Refs.Head).Append(commit.Id, string.Format("reset: moving to {0}", commit.Sha));
}
///
/// 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 = null, ExplicitPathsOptions explicitPathsOptions = null)
{
if (Info.IsBare)
{
throw new BareRepositoryException("Reset is not allowed in a bare repository");
}
Ensure.ArgumentNotNull(commit, "commit");
TreeChanges changes = Diff.Compare(commit.Tree, DiffTargets.Index, paths, explicitPathsOptions);
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.
/// True to amend the current pointed at by , false otherwise.
/// The generated .
public Commit Commit(string message, Signature author, Signature committer, bool amendPreviousCommit = false)
{
bool isHeadOrphaned = Info.IsHeadOrphaned;
if (amendPreviousCommit && isHeadOrphaned)
{
throw new OrphanedHeadException("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(amendPreviousCommit);
Commit result = ObjectDatabase.CreateCommit(message, author, committer, tree, parents, "HEAD");
Proxy.git_repository_merge_cleanup(handle);
// Insert reflog entry
LogCommit(result, amendPreviousCommit, isHeadOrphaned, parents.Count() > 1);
return result;
}
private void LogCommit(Commit commit, bool amendPreviousCommit, bool isHeadOrphaned, bool isMergeCommit)
{
// Compute reflog message
string reflogMessage = "commit";
if (isHeadOrphaned)
{
reflogMessage += " (initial)";
}
else if (amendPreviousCommit)
{
reflogMessage += " (amend)";
}
else if (isMergeCommit)
{
reflogMessage += " (merge)";
}
reflogMessage = string.Format("{0}: {1}", reflogMessage, commit.MessageShort);
var headRef = Refs.Head;
// in case HEAD targets a symbolic reference, log commit on the targeted direct reference
if (headRef is SymbolicReference)
{
Refs.Log(headRef.ResolveToDirectReference()).Append(commit.Id, reflogMessage, commit.Committer);
}
// Log commit on HEAD
Refs.Log(headRef).Append(commit.Id, reflogMessage, commit.Committer);
}
private IEnumerable RetrieveParentsOfTheCommitBeingCreated(bool amendPreviousCommit)
{
if (amendPreviousCommit)
{
return Head.Tip.Parents;
}
if (Info.IsHeadOrphaned)
{
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 virtual 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)
///
///
public static string Version
{
get { return versionRetriever.Value; }
}
private static string RetrieveVersion()
{
Assembly assembly = typeof(Repository).Assembly;
Version version = assembly.GetName().Version;
string libgit2Hash = ReadContentFromResource(assembly, "libgit2_hash.txt");
string libgit2sharpHash = ReadContentFromResource(assembly, "libgit2sharp_hash.txt");
return string.Format(
CultureInfo.InvariantCulture,
"{0}-{1}-{2} ({3})",
version.ToString(3),
libgit2sharpHash.Substring(0, 7),
libgit2Hash.Substring(0, 7),
NativeMethods.ProcessorArchitecture
);
}
private static string ReadContentFromResource(Assembly assembly, string partialResourceName)
{
string name = string.Format(CultureInfo.InvariantCulture, "LibGit2Sharp.{0}", partialResourceName);
using (var sr = new StreamReader(assembly.GetManifestResourceStream(name)))
{
return sr.ReadLine();
}
}
///
/// Gets the references to the tips that are currently being merged.
///
public virtual 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);
}
private string DebuggerDisplay
{
get
{
return string.Format(CultureInfo.InvariantCulture,
"{0} = \"{1}\"",
Info.IsBare ? "Gitdir" : "Workdir",
Info.IsBare ? Info.Path : Info.WorkingDirectory);
}
}
}
}