I have seen numerous people, including myself use incorrect gitignore[1] patterns when referencing symbolic files and directories. So in this article I investigate how gitignore’s pattern matching is affected by symbolic links.

TLDR

The quick synopsis for files is that the gitignore’s pattern matching is not affected by symbolic links. The summary for directories is that symbolic directories match like files, because a symbolic link is a ‘special kind of file that points to another file’[2]. Therefore a pattern ending with a ‘/’ would match a real directory but not a symbolic directory with the same name.

Real File

filefilesuffixprefixfile
./file
/fileIgnored.
fileIgnored.
./file/
/file/
file/

Real File Test Script

Symbolic File

filefilesuffixprefixfile
./file
/fileIgnored.
fileIgnored.
./file/
/file/
file/

Symbolic File Test Script

Real Directory

directory/filedirectorysuffix/fileprefixdirectory/file
./directory
/directoryIgnored.
directoryIgnored.
./directory/
/directory/Ignored.
directory/Ignored.

Real Directory Test Script

Symbolic Directory

directorydirectorysuffixprefixdirectory
./directory
/directoryIgnored.
directoryIgnored.
./directory/
/directory/
directory/

Symbolic Directory Test Script

Appendix

Real File Test Script

#!/usr/bin/env sh

set -o errexit

cwd=$(pwd)

# Variables.
file="file"
suffix_file="${file}suffix"
prefix_file="prefix${file}"

print_file_row() {
	echo "|\`${gitignore_content}\`|$(git ls-files . --exclude-standard --others | grep -q "^${file}$" && echo "" || echo "Ignored.")|$(git ls-files . --exclude-standard --others | grep -q "^${suffix_file}$" && echo "" || echo "Ignored.")|$(git ls-files . --exclude-standard --others | grep -q "^${prefix_file}$" && echo "" || echo "Ignored.")|"
}

# Setup.
test_directory=$(mktemp -d)
cd "${test_directory}"
git init >/dev/null 2>&1

touch "${file}"
touch "${suffix_file}"
touch "${prefix_file}"

# Testing.
echo "||\`${file}\`|\`${suffix_file}\`|\`${prefix_file}\`|"
echo "|---|---|---|---|"
gitignore_content="./${file}" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="/${file}" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="${file}" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="./${file}/" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="/${file}/" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="${file}/" && echo "${gitignore_content}" >.gitignore && print_file_row
echo ""

# Cleaning up.
cd "${cwd}"
rm -rf "${test_directory}"

Symbolic File Test Script

#!/usr/bin/env sh

set -o errexit

cwd=$(pwd)

# Variables.
file="file"
suffix_file="${file}suffix"
prefix_file="prefix${file}"

print_file_row() {
	echo "|\`${gitignore_content}\`|$(git ls-files . --exclude-standard --others | grep -q "^${file}$" && echo "" || echo "Ignored.")|$(git ls-files . --exclude-standard --others | grep -q "^${suffix_file}$" && echo "" || echo "Ignored.")|$(git ls-files . --exclude-standard --others | grep -q "^${prefix_file}$" && echo "" || echo "Ignored.")|"
}

# Setup.
test_directory=$(mktemp -d)
cd "${test_directory}"
git init >/dev/null 2>&1

real_file=$(mktemp)
touch "${real_file}"

ln -s "${real_file}" "${file}"
ln -s "${real_file}" "${suffix_file}"
ln -s "${real_file}" "${prefix_file}"

# Testing.
echo "||\`${file}\`|\`${suffix_file}\`|\`${prefix_file}\`|"
echo "|---|---|---|---|"
gitignore_content="./${file}" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="/${file}" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="${file}" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="./${file}/" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="/${file}/" && echo "${gitignore_content}" >.gitignore && print_file_row
gitignore_content="${file}/" && echo "${gitignore_content}" >.gitignore && print_file_row
echo ""

# Cleaning up.
cd "${cwd}"
rm -rf "${test_directory}"

Real Directory Test Script

#!/usr/bin/env sh

set -o errexit

cwd=$(pwd)

# Variables.
file="file"

directory="directory"
suffix_directory="${directory}suffix"
prefix_directory="prefix${directory}"

directory_file="${directory}/${file}"
suffix_directory_file="${suffix_directory}/${file}"
prefix_directory_file="${prefix_directory}/${file}"

is_ignored() {
	is_ignored_result="$(git ls-files . --exclude-standard --others | grep -q -E "$1" && echo "" || echo "Ignored.")"
}

print_directory_row() {
	# ($|/) as directories are ignored and symlinks are files.
	directory_result=$(is_ignored "^${directory}($|/)" && echo "${is_ignored_result}")
	suffix_directory_result=$(is_ignored "^${suffix_directory}($|/)" && echo "${is_ignored_result}")
	prefix_directory_result=$(is_ignored "^${prefix_directory}($|/)" && echo "${is_ignored_result}")

	echo "|\`${gitignore_content}\`|${directory_result}|${suffix_directory_result}|${prefix_directory_result}|"
}

# Setup.
test_directory=$(mktemp -d)
cd "${test_directory}"
git init >/dev/null 2>&1

mkdir "${directory}"
touch "${directory_file}"
mkdir "${suffix_directory}"
touch "${suffix_directory_file}"
mkdir "${prefix_directory}"
touch "${prefix_directory_file}"

# Testing.
echo "||\`${directory}\`|\`${suffix_directory}\`|\`${prefix_directory}\`|"
echo "|---|---|---|---|"
gitignore_content="./${directory}" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="/${directory}" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="${directory}" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="./${directory}/" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="/${directory}/" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="${directory}/" && echo "${gitignore_content}" >.gitignore && print_directory_row
echo ""

# Cleaning up.
cd "${cwd}"
rm -rf "${test_directory}"

Symbolic Directory Test Script

#!/usr/bin/env sh

set -o errexit

cwd=$(pwd)

# Variables.
file="file"

directory="directory"
suffix_directory="${directory}suffix"
prefix_directory="prefix${directory}"

is_ignored() {
	is_ignored_result="$(git ls-files . --exclude-standard --others | grep -q -E "$1" && echo "" || echo "Ignored.")"
}

print_directory_row() {
	# ($|/) as directories are ignored and symlinks are files.
	directory_result=$(is_ignored "^${directory}($|/)" && echo "${is_ignored_result}")
	suffix_directory_result=$(is_ignored "^${suffix_directory}($|/)" && echo "${is_ignored_result}")
	prefix_directory_result=$(is_ignored "^${prefix_directory}($|/)" && echo "${is_ignored_result}")

	echo "|\`${gitignore_content}\`|${directory_result}|${suffix_directory_result}|${prefix_directory_result}|"
}

# Setup.
test_directory=$(mktemp -d)
cd "${test_directory}"
git init >/dev/null 2>&1

real_directory=$(mktemp -d)
real_directory_file="${real_directory}/${file}"
touch "${real_directory_file}"

ln -s "${real_directory}" "${directory}"
ln -s "${real_directory}" "${suffix_directory}"
ln -s "${real_directory}" "${prefix_directory}"

# Testing.
echo "||\`${directory}\`|\`${suffix_directory}\`|\`${prefix_directory}\`|"
echo "|---|---|---|---|"
gitignore_content="./${directory}" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="/${directory}" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="${directory}" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="./${directory}/" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="/${directory}/" && echo "${gitignore_content}" >.gitignore && print_directory_row
gitignore_content="${directory}/" && echo "${gitignore_content}" >.gitignore && print_directory_row
echo ""

# Cleaning up.
cd "${cwd}"
rm -rf "${test_directory}"

References