using System.IO; using System.Linq; using LibGit2Sharp.Tests.TestHelpers; using Xunit; using Xunit.Extensions; using System; namespace LibGit2Sharp.Tests { public class RevertFixture : BaseFixture { [Fact] public void CanRevert() { // The branch name to perform the revert on, // and the file whose contents we expect to be reverted. const string revertBranchName = "refs/heads/revert"; const string revertedFile = "a.txt"; string path = CloneRevertTestRepo(); using (var repo = new Repository(path)) { // Checkout the revert branch. Branch branch = repo.Checkout(revertBranchName); Assert.NotNull(branch); // Revert tip commit. RevertResult result = repo.Revert(repo.Head.Tip, Constants.Signature); Assert.NotNull(result); Assert.Equal(RevertStatus.Reverted, result.Status); // Verify commit was made. Assert.NotNull(result.Commit); // Verify the expected commit ID. Assert.Equal("04746060fa753c9970d88a0b59151d7b212ac903", result.Commit.Id.Sha); // Verify workspace is clean. Assert.True(repo.Index.IsFullyMerged); Assert.False(repo.Index.RetrieveStatus().IsDirty); // Lookup the blob containing the expected reverted content of a.txt. Blob expectedBlob = repo.Lookup("bc90ea420cf6c5ae3db7dcdffa0d79df567f219b"); Assert.NotNull(expectedBlob); // Verify contents of Index. IndexEntry revertedIndexEntry = repo.Index[revertedFile]; Assert.NotNull(revertedIndexEntry); // Verify the contents of the index. Assert.Equal(expectedBlob.Id, revertedIndexEntry.Id); // Verify contents of workspace. string fullPath = Path.Combine(repo.Info.WorkingDirectory, revertedFile); Assert.Equal(expectedBlob.GetContentText(new FilteringOptions(revertedFile)), File.ReadAllText(fullPath)); } } [Fact] public void CanRevertAndNotCommit() { // The branch name to perform the revert on, // and the file whose contents we expect to be reverted. const string revertBranchName = "refs/heads/revert"; const string revertedFile = "a.txt"; string path = CloneRevertTestRepo(); using (var repo = new Repository(path)) { string modifiedFileFullPath = Path.Combine(repo.Info.WorkingDirectory, revertedFile); // Checkout the revert branch. Branch branch = repo.Checkout(revertBranchName); Assert.NotNull(branch); // Revert tip commit. RevertResult result = repo.Revert(repo.Head.Tip, Constants.Signature, new RevertOptions() { CommitOnSuccess = false }); Assert.NotNull(result); Assert.Equal(RevertStatus.Reverted, result.Status); // Verify the commit was made. Assert.Null(result.Commit); // Verify workspace is dirty. FileStatus fileStatus = repo.Index.RetrieveStatus(revertedFile); Assert.Equal(FileStatus.Staged, fileStatus); // This is the ID of the blob containing the expected content. Blob expectedBlob = repo.Lookup("bc90ea420cf6c5ae3db7dcdffa0d79df567f219b"); Assert.NotNull(expectedBlob); // Verify contents of Index. IndexEntry revertedIndexEntry = repo.Index[revertedFile]; Assert.NotNull(revertedIndexEntry); Assert.Equal(expectedBlob.Id, revertedIndexEntry.Id); // Verify contents of workspace. string fullPath = Path.Combine(repo.Info.WorkingDirectory, revertedFile); Assert.Equal(expectedBlob.GetContentText(new FilteringOptions(revertedFile)), File.ReadAllText(fullPath)); } } [Fact] public void RevertWithConflictDoesNotCommit() { // The branch name to perform the revert on, // and the file whose contents we expect to be reverted. const string revertBranchName = "refs/heads/revert"; string path = CloneRevertTestRepo(); using (var repo = new Repository(path)) { // Checkout the revert branch. Branch branch = repo.Checkout(revertBranchName); Assert.NotNull(branch); // The commit to revert - we know that reverting this // specific commit will generate conflicts. Commit commitToRevert = repo.Lookup("cb4f7f0eca7a0114cdafd8537332aa17de36a4e9"); Assert.NotNull(commitToRevert); // Perform the revert and verify there were conflicts. RevertResult result = repo.Revert(commitToRevert, Constants.Signature); Assert.NotNull(result); Assert.Equal(RevertStatus.Conflicts, result.Status); Assert.Null(result.Commit); // Verify there is a conflict on the expected path. Assert.False(repo.Index.IsFullyMerged); Assert.NotNull(repo.Index.Conflicts["a.txt"]); // Verify the non-conflicting paths are staged. Assert.Equal(FileStatus.Staged, repo.Index.RetrieveStatus("b.txt")); Assert.Equal(FileStatus.Staged, repo.Index.RetrieveStatus("c.txt")); } } [Theory] [InlineData(CheckoutFileConflictStrategy.Ours)] [InlineData(CheckoutFileConflictStrategy.Theirs)] public void RevertWithFileConflictStrategyOption(CheckoutFileConflictStrategy conflictStrategy) { // The branch name to perform the revert on, // and the file which we expect conflicts as result of the revert. const string revertBranchName = "refs/heads/revert"; const string conflictedFilePath = "a.txt"; string path = CloneRevertTestRepo(); using (var repo = new Repository(path)) { // Checkout the revert branch. Branch branch = repo.Checkout(revertBranchName); Assert.NotNull(branch); // Specify FileConflictStrategy. RevertOptions options = new RevertOptions() { FileConflictStrategy = conflictStrategy, }; RevertResult result = repo.Revert(repo.Head.Tip.Parents.First(), Constants.Signature, options); // Verify there is a conflict. Assert.False(repo.Index.IsFullyMerged); Conflict conflict = repo.Index.Conflicts[conflictedFilePath]; Assert.NotNull(conflict); Assert.NotNull(conflict); Assert.NotNull(conflict.Theirs); Assert.NotNull(conflict.Ours); // Get the blob containing the expected content. Blob expectedBlob = null; switch (conflictStrategy) { case CheckoutFileConflictStrategy.Theirs: expectedBlob = repo.Lookup(conflict.Theirs.Id); break; case CheckoutFileConflictStrategy.Ours: expectedBlob = repo.Lookup(conflict.Ours.Id); break; default: throw new Exception("Unexpected FileConflictStrategy"); } Assert.NotNull(expectedBlob); // Check the content of the file on disk matches what is expected. string expectedContent = expectedBlob.GetContentText(new FilteringOptions(conflictedFilePath)); Assert.Equal(expectedContent, File.ReadAllText(Path.Combine(repo.Info.WorkingDirectory, conflictedFilePath))); } } [Fact] public void RevertReportsCheckoutProgress() { const string revertBranchName = "refs/heads/revert"; string repoPath = CloneRevertTestRepo(); using (var repo = new Repository(repoPath)) { // Checkout the revert branch. Branch branch = repo.Checkout(revertBranchName); Assert.NotNull(branch); bool wasCalled = false; RevertOptions options = new RevertOptions() { OnCheckoutProgress = (path, completed, total) => wasCalled = true }; repo.Revert(repo.Head.Tip, Constants.Signature, options); Assert.True(wasCalled); } } [Fact] public void RevertReportsCheckoutNotification() { const string revertBranchName = "refs/heads/revert"; string repoPath = CloneRevertTestRepo(); using (var repo = new Repository(repoPath)) { // Checkout the revert branch. Branch branch = repo.Checkout(revertBranchName); Assert.NotNull(branch); bool wasCalled = false; CheckoutNotifyFlags actualNotifyFlags = CheckoutNotifyFlags.None; RevertOptions options = new RevertOptions() { OnCheckoutNotify = (path, notificationType) => { wasCalled = true; actualNotifyFlags = notificationType; return true; }, CheckoutNotifyFlags = CheckoutNotifyFlags.Updated, }; repo.Revert(repo.Head.Tip, Constants.Signature, options); Assert.True(wasCalled); } } [Theory] [InlineData(null)] [InlineData(true)] [InlineData(false)] public void RevertFindsRenames(bool? findRenames) { // The environment is set up such that: // - file d.txt is edited in the commit that is to be reverted (commit A) // - file d.txt is renamed to d_renamed.txt // - commit A is reverted. // If rename detection is enabled, then the revert is applied // to d_renamed.txt. If rename detection is not enabled, // then the revert results in a conflict. const string revertBranchName = "refs/heads/revert_rename"; const string commitIdToRevert = "ca3e813"; const string expectedBlobId = "0ff3bbb9c8bba2291654cd64067fa417ff54c508"; const string modifiedFilePath = "d_renamed.txt"; string repoPath = CloneRevertTestRepo(); using (var repo = new Repository(repoPath)) { Branch currentBranch = repo.Checkout(revertBranchName); Assert.NotNull(currentBranch); Commit commitToRevert = repo.Lookup(commitIdToRevert); Assert.NotNull(currentBranch); RevertOptions options; if (findRenames.HasValue) { options = new RevertOptions() { FindRenames = findRenames.Value, }; } else { options = new RevertOptions(); } RevertResult result = repo.Revert(commitToRevert, Constants.Signature, options); Assert.NotNull(result); if(!findRenames.HasValue || findRenames.Value == true) { Assert.Equal(RevertStatus.Reverted, result.Status); Assert.NotNull(result.Commit); Blob expectedBlob = repo.Lookup(expectedBlobId); Assert.NotNull(expectedBlob); GitObject blob = result.Commit.Tree[modifiedFilePath].Target as Blob; Assert.NotNull(blob); Assert.Equal(blob.Id, expectedBlob.Id); // Verify contents of workspace string fullPath = Path.Combine(repo.Info.WorkingDirectory, modifiedFilePath); Assert.Equal(expectedBlob.GetContentText(new FilteringOptions(modifiedFilePath)), File.ReadAllText(fullPath)); } else { Assert.Equal(RevertStatus.Conflicts, result.Status); Assert.Null(result.Commit); } } } [Theory] [InlineData(1, "a04ef5f22c2413a9743046436c0e5354ed903f78")] [InlineData(2, "1ae0cd88802bb4f4e6413ba63e41376d235b6fd0")] public void CanRevertMergeCommit(int mainline, string expectedId) { const string revertBranchName = "refs/heads/revert_merge"; const string commitIdToRevert = "2747045"; string repoPath = CloneRevertTestRepo(); using (var repo = new Repository(repoPath)) { Branch branch = repo.Checkout(revertBranchName); Assert.NotNull(branch); Commit commitToRevert = repo.Lookup(commitIdToRevert); Assert.NotNull(commitToRevert); RevertOptions options = new RevertOptions() { Mainline = mainline, }; RevertResult result = repo.Revert(commitToRevert, Constants.Signature, options); Assert.NotNull(result); Assert.Equal(RevertStatus.Reverted, result.Status); Assert.Equal(result.Commit.Sha, expectedId); if(mainline == 1) { // In this case, we expect "d_renamed.txt" to be reverted (deleted), // and a.txt to match the tip of the "revert" branch. Assert.Equal(FileStatus.Nonexistent, repo.Index.RetrieveStatus("d_renamed.txt")); // This is the commit containing the expected contents of a.txt. Commit commit = repo.Lookup("b6fbb29b625aabe0fb5736da6fd61d4147e4405e"); Assert.NotNull(commit); Assert.Equal(commit["a.txt"].Target.Id, repo.Index["a.txt"].Id); } else if(mainline == 2) { // In this case, we expect "d_renamed.txt" to be preset, // and a.txt to match the tip of the master branch. // In this case, we expect "d_renamed.txt" to be reverted (deleted), // and a.txt to match the tip of the "revert" branch. Assert.Equal(FileStatus.Unaltered, repo.Index.RetrieveStatus("d_renamed.txt")); // This is the commit containing the expected contents of "d_renamed.txt". Commit commit = repo.Lookup("c4b5cea70e4cd5b633ed0f10ae0ed5384e8190d8"); Assert.NotNull(commit); Assert.Equal(commit["d_renamed.txt"].Target.Id, repo.Index["d_renamed.txt"].Id); // This is the commit containing the expected contents of a.txt. commit = repo.Lookup("cb4f7f0eca7a0114cdafd8537332aa17de36a4e9"); Assert.NotNull(commit); Assert.Equal(commit["a.txt"].Target.Id, repo.Index["a.txt"].Id); } } } [Fact] public void CanNotRevertAMergeCommitWithoutSpecifyingTheMainlineBranch() { const string revertBranchName = "refs/heads/revert_merge"; const string commitIdToRevert = "2747045"; string repoPath = CloneRevertTestRepo(); using (var repo = new Repository(repoPath)) { Branch branch = repo.Checkout(revertBranchName); Assert.NotNull(branch); var commitToRevert = repo.Lookup(commitIdToRevert); Assert.NotNull(commitToRevert); Assert.Throws(() => repo.Revert(commitToRevert, Constants.Signature)); } } } }