Skip to content

Checkout update #292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 254 additions & 19 deletions LibGit2Sharp.Tests/CheckoutFixture.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using LibGit2Sharp.Tests.TestHelpers;
using Xunit;
using Xunit.Extensions;
Expand All @@ -10,6 +11,7 @@ public class CheckoutFixture : BaseFixture
{
private static readonly string originalFilePath = "a.txt";
private static readonly string originalFileContent = "Hello";
private static readonly string alternateFileContent = "There again";
private static readonly string otherBranchName = "other";

[Theory]
Expand Down Expand Up @@ -190,8 +192,16 @@ public void CheckoutUpdatesModifiedFilesInWorkingDirectory()
}

[Fact]
public void CanForcefullyCheckoutWithStagedChanges()
public void CanForcefullyCheckoutWithConflictingStagedChanges()
{
// This test will check that we can checkout a branch that results
// in a conflict. Here is the high level steps of the test:
// 1) Create branch otherBranch from current commit in master,
// 2) Commit change to master
// 3) Switch to otherBranch
// 4) Create conflicting change
// 5) Forcefully checkout master

TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath);

using (var repo = new Repository(path.RepositoryPath))
Expand All @@ -200,31 +210,39 @@ public void CanForcefullyCheckoutWithStagedChanges()
Branch master = repo.Branches["master"];
Assert.True(master.IsCurrentRepositoryHead);

// Set the working directory to the current head
// Set the working directory to the current head.
ResetAndCleanWorkingDirectory(repo);

Assert.False(repo.Index.RetrieveStatus().IsDirty);

// Add local change
// Create otherBranch from current Head.
repo.Branches.Add(otherBranchName, master.Tip);

// Add change to master.
string fullPath = Path.Combine(repo.Info.WorkingDirectory, fileFullPath);
File.WriteAllText(fullPath, originalFileContent);
repo.Index.Stage(fullPath);
repo.Commit("change in master", Constants.Signature, Constants.Signature);

// Verify working directory is now dirty
Assert.True(repo.Index.RetrieveStatus().IsDirty);
// Checkout otherBranch.
repo.Checkout(otherBranchName);

// Add change to otherBranch.
File.WriteAllText(fullPath, alternateFileContent);
repo.Index.Stage(fullPath);

// And that the new file exists
Assert.True(File.Exists(fileFullPath));
// Assert that normal checkout throws exception
// for the conflict.
Assert.Throws<LibGit2SharpException>(() => repo.Checkout(master.CanonicalName));

// Checkout with the force option
Branch targetBranch = repo.Branches["i-do-numbers"];
targetBranch.Checkout(CheckoutOptions.Force, null);
// Checkout with force option should succeed.
repo.Checkout(master.CanonicalName, CheckoutOptions.Force, null);

// Assert that target branch is checked out
Assert.True(targetBranch.IsCurrentRepositoryHead);
// Assert that master branch is checked out.
Assert.True(repo.Branches["master"].IsCurrentRepositoryHead);

// And that staged change (add) is no longer preset
Assert.False(File.Exists(fileFullPath));
// And that the current index is not dirty.
Assert.False(repo.Index.RetrieveStatus().IsDirty);
}
}

Expand Down Expand Up @@ -254,11 +272,11 @@ public void CheckingOutWithMergeConflictsThrows()

// Assert that checking out master throws
// when there are unstaged commits
Assert.Throws<MergeConflictException>(() => repo.Checkout("master"));
Assert.Throws<LibGit2SharpException>(() => repo.Checkout("master"));

// And when there are staged commits
repo.Index.Stage(fullPath);
Assert.Throws<MergeConflictException>(() => repo.Checkout("master"));
Assert.Throws<LibGit2SharpException>(() => repo.Checkout("master"));
}
}

Expand All @@ -267,8 +285,8 @@ public void CheckingOutInABareRepoThrows()
{
using (var repo = new Repository(BareTestRepoPath))
{
Assert.Throws<InvalidOperationException>(() => repo.Checkout(repo.Branches["refs/heads/test"]));
Assert.Throws<InvalidOperationException>(() => repo.Checkout("refs/heads/test"));
Assert.Throws<BareRepositoryException>(() => repo.Checkout(repo.Branches["refs/heads/test"]));
Assert.Throws<BareRepositoryException>(() => repo.Checkout("refs/heads/test"));
}
}

Expand Down Expand Up @@ -325,16 +343,233 @@ public void CheckingOutThroughRepositoryCallsCheckoutProgress()
}
}

[Fact]
public void CheckoutRetainsUntrackedChanges()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();

using (var repo = Repository.Init(scd.DirectoryPath))
{
PopulateBasicRepository(repo);

// Generate an unstaged change.
string fullPathFileB = Path.Combine(repo.Info.WorkingDirectory, "b.txt");
File.WriteAllText(fullPathFileB, alternateFileContent);

// Verify that there is an untracked entry.
Assert.Equal(1, repo.Index.RetrieveStatus().Untracked.Count());
Assert.Equal(FileStatus.Untracked, repo.Index.RetrieveStatus(fullPathFileB));

repo.Checkout(otherBranchName);

// Verify untracked entry still exists.
Assert.Equal(1, repo.Index.RetrieveStatus().Untracked.Count());
Assert.Equal(FileStatus.Untracked, repo.Index.RetrieveStatus(fullPathFileB));
}
}

[Fact]
public void ForceCheckoutRetainsUntrackedChanges()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();

using (var repo = Repository.Init(scd.DirectoryPath))
{
PopulateBasicRepository(repo);

// Generate an unstaged change.
string fullPathFileB = Path.Combine(repo.Info.WorkingDirectory, "b.txt");
File.WriteAllText(fullPathFileB, alternateFileContent);

// Verify that there is an untracked entry.
Assert.Equal(1, repo.Index.RetrieveStatus().Untracked.Count());
Assert.Equal(FileStatus.Untracked, repo.Index.RetrieveStatus(fullPathFileB));

repo.Checkout(otherBranchName, CheckoutOptions.Force, null);

// Verify untracked entry still exists.
Assert.Equal(1, repo.Index.RetrieveStatus().Untracked.Count());
Assert.Equal(FileStatus.Untracked, repo.Index.RetrieveStatus(fullPathFileB));
}
}

[Fact]
public void CheckoutRetainsUnstagedChanges()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();

using (var repo = Repository.Init(scd.DirectoryPath))
{
PopulateBasicRepository(repo);

// Generate an unstaged change.
string fullPathFileA = Path.Combine(repo.Info.WorkingDirectory, originalFilePath);
File.WriteAllText(fullPathFileA, alternateFileContent);

// Verify that there is a modified entry.
Assert.Equal(1, repo.Index.RetrieveStatus().Modified.Count());
Assert.Equal(FileStatus.Modified, repo.Index.RetrieveStatus(fullPathFileA));

repo.Checkout(otherBranchName);

// Verify modified entry still exists.
Assert.Equal(1, repo.Index.RetrieveStatus().Modified.Count());
Assert.Equal(FileStatus.Modified, repo.Index.RetrieveStatus(fullPathFileA));
}
}

[Fact]
public void CheckoutRetainsStagedChanges()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();

using (var repo = Repository.Init(scd.DirectoryPath))
{
PopulateBasicRepository(repo);

// Generate a staged change.
string fullPathFileA = Path.Combine(repo.Info.WorkingDirectory, originalFilePath);
File.WriteAllText(fullPathFileA, alternateFileContent);
repo.Index.Stage(fullPathFileA);

// Verify that there is a staged entry.
Assert.Equal(1, repo.Index.RetrieveStatus().Staged.Count());
Assert.Equal(FileStatus.Staged, repo.Index.RetrieveStatus(fullPathFileA));

repo.Checkout(otherBranchName);

// Verify staged entry still exists.
Assert.Equal(1, repo.Index.RetrieveStatus().Staged.Count());
Assert.Equal(FileStatus.Staged, repo.Index.RetrieveStatus(fullPathFileA));
}
}

[Fact]
public void CheckoutRetainsIgnoredChanges()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();

using (var repo = Repository.Init(scd.DirectoryPath))
{
PopulateBasicRepository(repo);

// Create a bin directory.
string ignoredDirectoryPath = Path.Combine(repo.Info.WorkingDirectory, "bin");
Directory.CreateDirectory(ignoredDirectoryPath);

// Create file in ignored bin directory.
string ignoredFilePath = Path.Combine(repo.Info.WorkingDirectory, Path.Combine("bin", "some_ignored_file.txt"));
File.WriteAllText(ignoredFilePath, "hello from this ignored file.");

// The following check does not report ignored entries...
// Assert.Equal(1, repo.Index.RetrieveStatus().Ignored.Count());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that Index.RetrieveStatus() does not return Ignored entries? Is this expected? If so, what does the Ignored property represent?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StatusFixture has passing tests around ignored files, so it would surprise me that this assert fails but the following one passes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm...

I've got the following test unexpectedly failing 😢. On it.

[Fact]
public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectivesThroughoutDirectories()
{
    TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath);
    using (var repo = new Repository(path.RepositoryPath))
    {
        string ignoredDirectoryPath = Path.Combine(repo.Info.WorkingDirectory, "bin");
        Directory.CreateDirectory(ignoredDirectoryPath); 

        string relativePath = Path.Combine("bin", "look-ma.txt");
        string fullFilePath = Path.Combine(repo.Info.WorkingDirectory, relativePath);
        File.WriteAllText(fullFilePath, "I'm going to be ignored!");

        string gitignorePath = Path.Combine(repo.Info.WorkingDirectory, ".gitignore");
        File.WriteAllText(gitignorePath, "bin");

        RepositoryStatus newStatus = repo.Index.RetrieveStatus();

        Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(relativePath));
        Assert.Equal(new[] { relativePath }, newStatus.Ignored);
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nulltoken Yes, I think that is the same (unexpected) behavior I noticed.


Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(ignoredFilePath));

repo.Checkout(otherBranchName);

// Verify that the ignored file still exists.
Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(ignoredFilePath));
Assert.True(File.Exists(ignoredFilePath));
}
}

[Fact]
public void ForceCheckoutRetainsIgnoredChanges()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();

using (var repo = Repository.Init(scd.DirectoryPath))
{
PopulateBasicRepository(repo);

// Create a bin directory.
string ignoredDirectoryPath = Path.Combine(repo.Info.WorkingDirectory, "bin");
Directory.CreateDirectory(ignoredDirectoryPath);

// Create file in ignored bin directory.
string ignoredFilePath = Path.Combine(repo.Info.WorkingDirectory, Path.Combine("bin", "some_ignored_file.txt"));
File.WriteAllText(ignoredFilePath, "hello from this ignored file.");

// The following check does not report ignored entries...
// Assert.Equal(1, repo.Index.RetrieveStatus().Ignored.Count());

Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(ignoredFilePath));

repo.Checkout(otherBranchName, CheckoutOptions.Force, null);

// Verify that the ignored file still exists.
Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(ignoredFilePath));
Assert.True(File.Exists(ignoredFilePath));
}
}

[Fact]
public void CheckoutBranchSnapshot()
{
SelfCleaningDirectory scd = BuildSelfCleaningDirectory();

using (var repo = Repository.Init(scd.DirectoryPath))
{
PopulateBasicRepository(repo);

// Get the current status of master
// and the current tip.
Branch initial = repo.Branches["master"];
Commit initialCommit = initial.Tip;

// Add commit to master
string fullPath = Path.Combine(repo.Info.WorkingDirectory, originalFilePath);
File.WriteAllText(fullPath, "Update : hello from master branch!\n");
repo.Index.Stage(fullPath);
repo.Commit("2nd commit", Constants.Signature, Constants.Signature);

initial.Checkout();

// Head should point at initial commit.
Assert.Equal(repo.Head.Tip, initialCommit);
Assert.False(repo.Index.RetrieveStatus().IsDirty);
// Verify that HEAD is detached.
Assert.Equal(repo.Refs["HEAD"].TargetIdentifier, initial.Tip.Sha);
}
}

[Fact]
public void CheckingOutRemoteBranchResultsInDetachedHead()
{
TemporaryCloneOfTestRepo path = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath);
using (var repo = new Repository(path.RepositoryPath))
{
Branch master = repo.Branches["master"];
Assert.True(master.IsCurrentRepositoryHead);

// Set the working directory to the current head
ResetAndCleanWorkingDirectory(repo);

repo.Checkout("refs/remotes/origin/master");

// Verify that HEAD is detached.
Assert.Equal(repo.Refs["HEAD"].TargetIdentifier, repo.Branches["origin/master"].Tip.Sha);

}
}

/// <summary>
/// Helper method to populate a simple repository with
/// a single file and two branches.
/// </summary>
/// <param name="repo">Repository to populate</param>
private void PopulateBasicRepository(Repository repo)
{
string fullPathFileA = Path.Combine(repo.Info.WorkingDirectory, "a.txt");
// Generate a .gitignore file.
string gitIgnoreFilePath = Path.Combine(repo.Info.WorkingDirectory, ".gitignore");
File.WriteAllText(gitIgnoreFilePath, "bin");
repo.Index.Stage(gitIgnoreFilePath);

string fullPathFileA = Path.Combine(repo.Info.WorkingDirectory, originalFilePath);
File.WriteAllText(fullPathFileA, originalFileContent);
repo.Index.Stage(fullPathFileA);

repo.Commit("Initial commit", Constants.Signature, Constants.Signature);

repo.CreateBranch(otherBranchName);
Expand Down
2 changes: 1 addition & 1 deletion LibGit2Sharp.Tests/ResetHeadFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public void HardResetInABareRepositoryThrows()
}
}

[Fact(Skip = "Not working against current libgit2 version")]
[Fact]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

public void HardResetUpdatesTheContentOfTheWorkingDirectory()
{
var clone = BuildTemporaryCloneOfTestRepo(StandardTestRepoWorkingDirPath);
Expand Down
10 changes: 8 additions & 2 deletions LibGit2Sharp/Branch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,21 @@ public virtual Remote Remote
}

/// <summary>
/// Checkout this branch.
/// Checkout the tip commit of this <see cref = "Branch" /> 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.
/// </summary>
public virtual void Checkout()
{
repo.Checkout(this);
}

/// <summary>
/// Checkout this branch with a callback for progress reporting.
/// Checkout the tip commit of this <see cref = "Branch" /> object
/// with a callback for progress reporting. 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.
/// </summary>
/// <param name="checkoutOptions">Options controlling checkout behavior.</param>
/// <param name="onCheckoutProgress">Callback method to report checkout progress updates through.</param>
Expand Down
Loading