Skip to content
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

Windows: Symlinks are not treated as a directory in gitignore #6250

Open
ehuss opened this issue Mar 22, 2022 · 4 comments
Open

Windows: Symlinks are not treated as a directory in gitignore #6250

ehuss opened this issue Mar 22, 2022 · 4 comments

Comments

@ehuss
Copy link
Contributor

ehuss commented Mar 22, 2022

When there is a gitignore entry with a trailing slash, git_index_add_all will add symlinks that match the gitignore patterns on Windows but not other platforms.

Reproduction steps

The following Rust program demonstrates the problem. Sorry for only including a Rust reproduction, hopefully it is easy to see the equivalent C api calls (it is mostly 1:1). I couldn't figure out how to build a project with Visual Studio linking to libgit2 (if there are docs somewhere on how to do that, I'd be happy to make a C example).

use git2::*;
use std::fs;

fn main() {
    // Create a repo with a gitignore file and a symlink.
    fs::create_dir_all("repo/src").unwrap();
    fs::write("repo/src/samplefile", "test").unwrap();
    #[cfg(windows)]
    {
        std::os::windows::fs::symlink_dir("src", "repo/src2").unwrap();
    }
    #[cfg(unix)]
    {
        std::os::unix::fs::symlink("src", "repo/src2").unwrap();
    }
    fs::write("repo/.gitignore", "/src2/").unwrap();
    let repo = Repository::init("repo").unwrap();
    // Add all and commit.
    let mut index = repo.index().unwrap();
    index
        .add_all(["*"], git2::IndexAddOption::DEFAULT, None)
        .unwrap();
    index.write().unwrap();
    let tree_id = index.write_tree().unwrap();
    let tree_oid = repo.find_tree(tree_id).unwrap();
    let sig = repo.signature().unwrap();
    let oid = repo
        .commit(Some("HEAD"), &sig, &sig, "initial commit", &tree_oid, &[])
        .unwrap();
    // Check the contents of the commit.
    let commit = repo.find_commit(oid).unwrap();
    let mut names = Vec::new();
    commit
        .tree()
        .unwrap()
        .walk(TreeWalkMode::PreOrder, |_name, entry| {
            names.push(entry.name().unwrap().to_string());
            TreeWalkResult::Ok
        })
        .unwrap();
    names.sort();
    assert_eq!(names, [".gitignore", "samplefile", "src"]);
}

In this example, there is a file src/samplefile and a symlink src2 -> src. The .gitignore has a pattern /src2/ intending to prevent src2 from being added.

Expected behavior

git_index_add_all will only add .gitignore and src/samplefile.

Actual behavior

On Windows, it also adds the src2 symlink, causing the final assert to fail.

This seems to only happen with patterns with a trailing slash. A summary of the the gitignore pattern behavior:

  • /src2/ and src2/ fail on Windows.
  • /src2 and src2 work as expected on all platforms.

I've also tested with core.symlinks=true or false, it doesn't seem to affect it.

Version of libgit2 (release number or SHA1)

2a0d0bd

Operating system(s) tested

Windows, macOS, Linux

@ethomson
Copy link
Member

Thanks @ehuss - you need not apologize for including rust repro steps.

Windows junction points / symlinks are ... complex, so apologies for the question. When you say that it's a symlink - how was this created? mklink /D ...?

@ehuss
Copy link
Contributor Author

ehuss commented Mar 24, 2022

Yea, CreateSymbolicLinkW with SYMBOLIC_LINK_FLAG_DIRECTORY, which I'm pretty certain is equivalent to mklink /d.

@Byron
Copy link

Byron commented Mar 17, 2024

I am investigating the issue for gitoxide and wanted to share some findings that might be relevant for libgit2 as well.

When reproducing the issue on MacOS with this script…

git init excluded-symlinks-to-dir
(cd excluded-symlinks-to-dir
  cat <<EOF >.gitignore
src1
src2/
EOF
  git add .gitignore && git commit -m "init"

  mkdir src
  >src/file

  ln -s src src1
  ln -s src src2
)

…and running git status in `excluded-symlinks-to-dir…

❯ git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/
        src2

nothing added to commit but untracked files present (use "git add" to track)

…I would have expected that git (git version 2.39.3 (Apple Git-146)) will only show src as untracked as it will resolve src2 as directory which matches src2/ in the .gitignore file.

Thus it seems that Git also doesn't handle this case. Arguments can probably be made for and against this behaviour, such that…

  • …symlinks to directories are always considered files OR
  • …symlinks to directories are considered directories

…by the exclude/gitignore machinery of the directory walk.

It seems that libgit2 changes the behaviour that I can reproduce with Git.

@ethomson
Copy link
Member

I would expect us to match git's behavior on POSIXy systems. But I think that windows is just different. There's several different types of things that are "links", all (IIRC) implemented as "reparse points", and potentially all treated a little bit differently. We should probably match git for windows behavior here, but I don't think that there's been some well-designed, thoughtful plan around this, I think it's just "whatever mingw does". So it will most likely be a disappointing change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants