Git Symlinks in Windows
Our developers use a mix of Windows and Unix based OS's. Therefore, symlinks created on Unix machines become a problem for Windows developers. In windows (msysgit), the symlink is converted to a text file with a path to the file it points to. Instead, I'd like to convert the symlink into an actual Windows symlink.
The ( updated ) solution I have to this is:
I have not implemented this, but I believe this is a solid approach to this problem.
Questions:
You can find the symlinks by looking for files that have a mode of 120000
, possibly with this command:
git ls-files -s | awk '/120000/{print $4}'
Once you replace the links, I would recommend marking them as unchanged with git update-index --assume-unchanged
, rather than listing them in .git/info/exclude
.
I was asking this exact same question a while back (not here, just in general) and ended up coming up with a very similar solution to OP's proposition. First I'll provide direct answers to questions 1 2 & 3, and then I'll post the solution I ended up using.
git checkout
step, but the solution below has met my needs well enough that a literal post-checkout script wasn't necessary. The Solution:
Our developers are in much the same situation as OP's: a mixture of Windows and Unix-based hosts, repositories and submodules with many git symlinks, and no native support (yet) in the release version of MsysGit for intelligently handling these symlinks on Windows hosts.
Thanks to Josh Lee for pointing out the fact that git commits symlinks with special filemode 120000
. With this information it's possible to add a few git aliases that allow for the creation and manipulation of git symlinks on Windows hosts.
Creating git symlinks on Windows
UPDATED 2014-11-12 (see below)
git config --global alias.add-symlink '!__git_add_symlink(){
argv=($@)
argc=${#argv[@]}
# Look for options
options=(" -h")
o_help="false"
case "${argv[@]}" in *" -h"*) o_help="true" ;; esac
if [ "$o_help" == "true" -o "$argc" -lt "2" ]; then
echo "
Usage: git add-symlink <target> <link>
* <target> is a RELATIVE PATH, respective to <link>.
* <link> is a RELATIVE PATH, respective to the repository'''s root dir.
* Command must be run from the repository'''s root dir."
return 0
fi
target_arg=${argv[0]}
link_arg=${argv[1]}
if [ ! -e "$target_arg" ]; then
echo "ERROR: Target $target_arg does not exist; not creating invalid symlink."
return 1
fi
hash=$(echo -n "$target_arg" | git hash-object -w --stdin)
git update-index --add --cacheinfo 120000 "$hash" "$link_arg"
git checkout -- "$link_arg"
}; __git_add_symlink "$@"'
Usage: git add-symlink <src> <dst>
, where <src>
is a relative reference (with respect to <dst>
) to the current location of the file or directory to link to, and <dst>
is a relative reference (with respect to the repository's root) to the link's desired destination.
Eg, the repository tree:
dir/
dir/foo/
dir/foo/bar/
dir/foo/bar/baz (file containing "I am baz")
dir/foo/bar/lnk_file (symlink to ../../../file)
file (file containing "I am file")
lnk_bar (symlink to dir/foo/bar/)
Can be created on Windows as follows:
git init
mkdir -p dir/foo/bar/
echo "I am baz" > dir/foo/bar/baz
echo "I am file" > file
git add -A
git commit -m "Add files"
git add-symlink ../../../file dir/foo/bar/lnk_file
git add-symlink dir/foo/bar/ lnk_bar
git commit -m "Add symlinks"
Replacing git symlinks with NTFS hardlinks+junctions
git config --global alias.rm-symlink '!__git_rm_symlink(){
git checkout -- "$1"
link=$(echo "$1")
POS=$'''/'''
DOS=$'''\'''
doslink=${link//$POS/$DOS}
dest=$(dirname "$link")/$(cat "$link")
dosdest=${dest//$POS/$DOS}
if [ -f "$dest" ]; then
rm -f "$link"
cmd //C mklink //H "$doslink" "$dosdest"
elif [ -d "$dest" ]; then
rm -f "$link"
cmd //C mklink //J "$doslink" "$dosdest"
else
echo "ERROR: Something went wrong when processing $1 . . ."
echo " $dest may not actually exist as a valid target."
fi
}; __git_rm_symlink "$1"'
git config --global alias.rm-symlinks '!__git_rm_symlinks(){
for symlink in $(git ls-files -s | egrep "^120000" | cut -f2); do
git rm-symlink "$symlink"
git update-index --assume-unchanged "$symlink"
done
}; __git_rm_symlinks'
Usage:
git rm-symlink dir/foo/bar/lnk_file
git rm-symlink lnk_bar
git update-index --assume-unchanged dir/foo/bar/lnk_file
git update-index --assume-unchanged lnk_bar
This removes git symlinks one-by-one, replacing them with NTFS hardlinks (in the case of files) or NTFS junctions (in the case of directories). The benefit of using hardlinks+junctions over "true" NTFS symlinks is that elevated UAC permissions are not required in order for them to be created. Finally, at your own leisure, you may choose to unflag-as-modified (or not) the "removed" symlinks with git update-index
.
For convenience's sake, you can also just run:
git rm-symlinks
This removes ALL git symlinks in the current repository, replacing them with hardlinks+junctions as necessary, and automatically flagging the changes to be ignored by git status
.
To remove symlinks from submodules, just use git's built-in support for iterating over them:
git submodule foreach --recursive git rm-symlinks
But, for every drastic action like this, a reversal is nice to have...
Restoring git symlinks on Windows
git config --global alias.checkout-symlinks '!__git_checkout_symlinks(){
POS=$'''/'''
DOS=$'''\'''
for symlink in $(git ls-files -s | egrep "^120000" | cut -f2); do
git update-index --no-assume-unchanged "$symlink"
dossymlink=${symlink//$POS/$DOS}
cmd //C rmdir //Q "$dossymlink" 2>/dev/null
git checkout -- "$symlink"
echo "Restored git symlink $symlink <<===>> $(cat $symlink)"
done
}; __git_checkout_symlinks'
Usage: git checkout-symlinks
, which undoes git rm-symlinks
, effectively restoring the repository to its natural state (except for your changes, which should stay intact).
And for submodules:
git submodule foreach --recursive git checkout-symlinks
Limitations:
If people forget to git checkout-symlinks
before doing something like git add -A
, they could pollute the repo!
Using our "example repo" from before:
echo "I am nuthafile" > dir/foo/bar/nuthafile
echo "Updating file" >> file
git add -A
git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: dir/foo/bar/nuthafile
# modified: file
# deleted: lnk_bar # POLLUTION
# new file: lnk_bar/baz # POLLUTION
# new file: lnk_bar/lnk_file # POLLUTION
# new file: lnk_bar/nuthafile # POLLUTION
#
Whoops...
For this reason, it's nice to include these aliases as steps to perform for Windows users before-and-after building a project, rather than after checkout or before pushing. But each situation is different. These aliases have been useful enough for me that a true post-checkout solution hasn't been necessary.
Hope that helps!
References:
http://git-scm.com/book/en/Git-Internals-Git-Objects
http://technet.microsoft.com/en-us/library/cc753194
UPDATE 2014-11-12: Because I personally only ever made heavy use of the rm-symlinks
and checkout-symlinks
aliases above, I managed to overlook a fairly nasty bug in the add-symlink
alias. Previously, -n
was not getting passed to the echo
statement responsible for creating the git symlink file that would later be added to the staging area as a part of add-symlink
's operation. This means that a trailing newline ( 0x0D 0x0A
on Windows hosts) was getting added to all git symlinks created with add-symlink
. While these git symlinks would still be "removable" on Windows hosts with rm-symlinks
just fine, if they were ever committed to a public repo and later cloned on a genuine posix-based system, these links would always come out broken on the other side. This issue has been fixed, and add-symlink
should now work as expected.
The most recent version of git scm (testet 2.11.1) allows to enable symbolic links. But you have to clone the repository with the symlinks again git clone -c core.symlinks=true <URL>
. You need to run this command with administrator rights. It is also possible to create symlinks on Windows with mklink. Check out the wiki.
上一篇: 如何查找目录树中的所有符号链接?
下一篇: Windows中的Git符号链接