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