Skip to content

Cloning a TFS project whose first changeset is a rename fails when --gitignore option is used  #1409

@fineol

Description

@fineol

When running git-tfs v0.32.45 to clone a TFS project whose first TFS changeset is a rename (don't ask me how that happened, because it happened before my time), if you supply the --gitignore option, then git-tfs stops after fetching the first changeset. Git-tfs does not display any error messages, but the repository is devoid of user files and none of the TFS changesets (including the first) are committed.

I encountered this issue in a production TFS system, but for reproducibility I created the following two unit tests:

public void CloneWithFirstTFSChangesetIsRename()
{
    h.SetupFake(r =>
    {
        r.Changeset(1, "First TFS changeset: rename the top-level folder", DateTime.Parse("2012-01-01 12:12:12 -05:00"))
            .Change(TfsChangeType.Rename, TfsItemType.Folder, "$/MyProject");
        r.Changeset(2, "Second TFS changeset: one folder and two files added", DateTime.Parse("2012-01-02 12:12:12 -05:00"))
            .Change(TfsChangeType.Add, TfsItemType.Folder, "$/MyProject/Folder")
            .Change(TfsChangeType.Add, TfsItemType.File, "$/MyProject/Folder/File.txt", "File contents")
            .Change(TfsChangeType.Add, TfsItemType.File, "$/MyProject/README", "tldr");
    });
    h.Run("clone", h.TfsUrl, "$/MyProject", "MyProject");
    h.AssertCommitMessage("MyProject", "HEAD", "Second TFS changeset: one folder and two files added", "", "git-tfs-id: [" + h.TfsUrl + "]$/MyProject;C2");
    h.AssertFileInWorkspace("MyProject", "Folder/File.txt", "File contents");
    h.AssertFileInWorkspace("MyProject", "README", "tldr");
    AssertNewClone("MyProject", new[] { "HEAD", "refs/heads/master", "refs/remotes/tfs/default" },
        commit: "726e937beab54f17fae545744497d68aa7c36507",
        tree: "41ab05d8f2a0f7f7f3a39c623e94fee68f64797e");
}
public void CloneWithFirstTFSChangesetIsRenameAndGitignoreGiven()
{
    string gitignoreFile = Path.Combine(h.Workdir, "gitignore");
    string gitignoreContent = "*.exe\r\n*.com\r\n";
    File.WriteAllText(gitignoreFile, gitignoreContent);

    h.SetupFake(r =>
    {
        r.Changeset(1, "First TFS changeset: rename the top-level folder", DateTime.Parse("2012-01-01 12:12:12 -05:00"))
            .Change(TfsChangeType.Rename, TfsItemType.Folder, "$/MyProject");
        r.Changeset(2, "Second TFS changeset: one folder and two files added", DateTime.Parse("2012-01-02 12:12:12 -05:00"))
            .Change(TfsChangeType.Add, TfsItemType.Folder, "$/MyProject/Folder")
            .Change(TfsChangeType.Add, TfsItemType.File, "$/MyProject/Folder/File.txt", "File contents")
            .Change(TfsChangeType.Add, TfsItemType.File, "$/MyProject/README", "tldr");
    });
    h.Run("clone", h.TfsUrl, "$/MyProject", "MyProject", $"--gitignore={gitignoreFile}");
    h.AssertCommitMessage("MyProject", "HEAD", "Second TFS changeset: one folder and two files added", "", "git-tfs-id: [" + h.TfsUrl + "]$/MyProject;C2");
    h.AssertFileInWorkspace("MyProject", "Folder/File.txt", "File contents");
    h.AssertFileInWorkspace("MyProject", "README", "tldr");
    AssertNewClone("MyProject", new[] { "HEAD", "refs/heads/master", "refs/remotes/tfs/default" },
        commit: "d1802bd0cee53f20ed69f182d1835e93697762a1",
        tree: "2ef92a065910b3cc3a1379e41a034e90f2e610ec");
}

Here is what the repository created via the CloneWithFirstTFSChangesetIsRename unit test looks like. Everything is as expected:

$ ls -a
./  ../  .git/  Folder/  README

$ git ls-files
Folder/File.txt
README

 $ git log --oneline
726e937 (HEAD -> master, tfs/default) Second TFS changeset: one folder and two files added
7c4693b First TFS changeset: rename the top-level folder

Here is what the repository created via the CloneWithFirstTFSChangesetIsRenameAndGitignoreGiven unit test looks like. Notice that the commits and files corresponding to the two TFS changesets are missing. Only the .gitignore file and its commit are present:

$ ls -a
./  ../  .git/  .gitignore

$ git ls-files
.gitignore

$ git log --oneline
077fd68 (HEAD -> master, tfs/default) .gitignore

Here is what I expect the repository created via the CloneWithFirstTFSChangesetIsRenameAndGitignoreGiven unit test to look like:

$ ls -a
./  ../  .git/  .gitignore  Folder/  README

$ git ls-files
.gitignore
Folder/File.txt
README

$ git log --oneline
d1802bd (HEAD -> master, tfs/default) Second TFS changeset: one folder and two files added
4f417c9 First TFS changeset: rename the top-level folder
077fd68 .gitignore

Lines 388-401 of the FetchWithMerge method in GitTfsRemote.cs contain special case logic for handling the case when the first TFS changeset is a rename:

var parentSha = (renameResult != null && renameResult.IsProcessingRenameChangeset) ? renameResult.LastParentCommitBeforeRename
var isFirstCommitInRepository = (parentSha == null);
var log = Apply(parentSha, changeset, objects);
if (changeset.IsRenameChangeset && !isFirstCommitInRepository)
{
    if (renameResult == null || !renameResult.IsProcessingRenameChangeset)
    {
        fetchResult.IsProcessingRenameChangeset = true;
        fetchResult.LastParentCommitBeforeRename = MaxCommitHash;
        return fetchResult;
    }
    renameResult.IsProcessingRenameChangeset = false;
    renameResult.LastParentCommitBeforeRename = null;
}

The problem is with the isFirstCommitInRepository variable. When a .gitignore file is supplied, git-tfs commits it before the cloning begins, making isFirstCommitInRepository false the first time FetchWithMerge runs when processing the actual TFS changesets and incorrectly bypassing the special-case logic. The isFirstCommitInRepository variable should be set based on whether a commit is the first TFS changeset being committed, not whether it is the first commit at all in the cloned repository.

I plan to submit a pull request with a proposed solution.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions