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