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