我们的开发人员混合使用Windows和基于unix的操作系统。因此,在Unix机器上创建的符号链接成为Windows开发人员的一个问题。在Windows (MSysGit)中,符号链接被转换为一个文本文件,并带有它所指向的文件的路径。相反,我想将符号链接转换为实际的Windows符号链接。

我对此的(更新的)解决方案是:

编写一个检出后脚本,递归地查找“符号链接”文本文件。 将它们替换为Windows符号链接(使用mklink),具有与虚拟“symbolic link”相同的名称和扩展名 通过在文件.git/info/exclude中添加一个条目来忽略这些Windows符号链接

我还没有实现这个方法,但我相信这是解决这个问题的可靠方法。

如果有的话,你认为这种方法有什么缺点? 这个后签出脚本是可实现的吗?也就是说,我能递归地找到Git创建的虚拟“符号链接”文件吗?


当前回答

请注意更新

对于大多数受困于符号链接和git以及与*nix系统共享repo的Windows开发人员来说,这个问题已经解决了——只要您更新一下对mklink的Windows理解并打开开发者模式。

在深入讨论下面的git技巧之前,先看看这个更现代的答案。

老系统:

我曾经问过这个完全相同的问题(不是在这里,只是在一般情况下),最后得出了一个与OP的命题非常相似的解决方案。 我将发布我最终使用的解决方案。

但首先我将直接回答OP的3个问题:

问:“如果有的话,你认为这种方法有什么缺点?” 答:提议的解决方案确实有一些缺点,主要是增加了存储库污染的可能性,或者在它们处于“Windows符号链接”状态时意外添加重复文件。(关于这一点,请参阅下面的“限制”。)

问:“这个后检出脚本是可行的吗?例如,我可以递归找到虚拟的“符号链接”文件git创建? 答:是的,后检出脚本是可实现的!也许不能作为字面上的后git签出步骤,但是下面的解决方案已经很好地满足了我的需求,不需要字面上的后签出脚本。

问:“有人写过这样的剧本吗?” 答:是的!

解决方案:

我们的开发人员和OP的情况差不多:混合了Windows和类unix的主机、带有许多git符号链接的存储库和子模块,并且在MsysGit的发布版本中(目前)还没有本地支持在Windows主机上智能处理这些符号链接。

感谢Josh Lee指出git使用特殊文件模式120000提交符号链接。有了这些信息,就可以添加一些git别名,以便在Windows主机上创建和操作git符号链接。

Creating git symlinks on Windows git config --global alias.add-symlink '!'"$(cat <<'ETX' __git_add_symlink() { if [ $# -ne 2 ] || [ "$1" = "-h" ]; then printf '%b\n' \ 'usage: git add-symlink <source_file_or_dir> <target_symlink>\n' \ 'Create a symlink in a git repository on a Windows host.\n' \ 'Note: source MUST be a path relative to the location of target' [ "$1" = "-h" ] && return 0 || return 2 fi source_file_or_dir=${1#./} source_file_or_dir=${source_file_or_dir%/} target_symlink=${2#./} target_symlink=${target_symlink%/} target_symlink="${GIT_PREFIX}${target_symlink}" target_symlink=${target_symlink%/.} : "${target_symlink:=.}" if [ -d "$target_symlink" ]; then target_symlink="${target_symlink%/}/${source_file_or_dir##*/}" fi case "$target_symlink" in (*/*) target_dir=${target_symlink%/*} ;; (*) target_dir=$GIT_PREFIX ;; esac target_dir=$(cd "$target_dir" && pwd) if [ ! -e "${target_dir}/${source_file_or_dir}" ]; then printf 'error: git-add-symlink: %s: No such file or directory\n' \ "${target_dir}/${source_file_or_dir}" >&2 printf '(Source MUST be a path relative to the location of target!)\n' >&2 return 2 fi git update-index --add --cacheinfo 120000 \ "$(printf '%s' "$source_file_or_dir" | git hash-object -w --stdin)" \ "${target_symlink}" \ && git checkout -- "$target_symlink" \ && printf '%s -> %s\n' "${target_symlink#$GIT_PREFIX}" "$source_file_or_dir" \ || return $? } __git_add_symlink ETX )" Usage: git add-symlink <source_file_or_dir> <target_symlink>, where the argument corresponding to the source file or directory must take the form of a path relative to the target symlink. You can use this alias the same way you would normally use ln. E.g., 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-symlinks '!'"$(cat <<'ETX' __git_rm_symlinks() { case "$1" in (-h) printf 'usage: git rm-symlinks [symlink] [symlink] [...]\n' return 0 esac ppid=$$ case $# in (0) git ls-files -s | grep -E '^120000' | cut -f2 ;; (*) printf '%s\n' "$@" ;; esac | while IFS= read -r symlink; do case "$symlink" in (*/*) symdir=${symlink%/*} ;; (*) symdir=. ;; esac git checkout -- "$symlink" src="${symdir}/$(cat "$symlink")" posix_to_dos_sed='s_^/\([A-Za-z]\)_\1:_;s_/_\\\\_g' doslnk=$(printf '%s\n' "$symlink" | sed "$posix_to_dos_sed") dossrc=$(printf '%s\n' "$src" | sed "$posix_to_dos_sed") if [ -f "$src" ]; then rm -f "$symlink" cmd //C mklink //H "$doslnk" "$dossrc" elif [ -d "$src" ]; then rm -f "$symlink" cmd //C mklink //J "$doslnk" "$dossrc" else printf 'error: git-rm-symlink: Not a valid source\n' >&2 printf '%s =/=> %s (%s =/=> %s)...\n' \ "$symlink" "$src" "$doslnk" "$dossrc" >&2 false fi || printf 'ESC[%d]: %d\n' "$ppid" "$?" git update-index --assume-unchanged "$symlink" done | awk ' BEGIN { status_code = 0 } /^ESC\['"$ppid"'\]: / { status_code = $2 ; next } { print } END { exit status_code } ' } __git_rm_symlinks ETX )" git config --global alias.rm-symlink '!git rm-symlinks' # for back-compat. Usage: git rm-symlinks [symlink] [symlink] [...] This alias can remove git symlinks one-by-one or all-at-once in one fell swoop. Symlinks will be replaced 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. 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 '!'"$(cat <<'ETX' __git_checkout_symlinks() { case "$1" in (-h) printf 'usage: git checkout-symlinks [symlink] [symlink] [...]\n' return 0 esac case $# in (0) git ls-files -s | grep -E '^120000' | cut -f2 ;; (*) printf '%s\n' "$@" ;; esac | while IFS= read -r symlink; do git update-index --no-assume-unchanged "$symlink" rmdir "$symlink" >/dev/null 2>&1 git checkout -- "$symlink" printf 'Restored git symlink: %s -> %s\n' "$symlink" "$(cat "$symlink")" done } __git_checkout_symlinks ETX )" git config --global alias.co-symlinks '!git checkout-symlinks' Usage: git checkout-symlinks [symlink] [symlink] [...], 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: Directories/files/symlinks with spaces in their paths should work. But tabs or newlines? YMMV… (By this I mean: don’t do that, because it will not work.) If yourself or others forget to git checkout-symlinks before doing something with potentially wide-sweeping consequences like git add -A, the local repository could end up in a polluted state. 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.

引用:

http://git-scm.com/book/en/Git-Internals-Git-Objects

http://technet.microsoft.com/en-us/library/cc753194

最后更新:2019-03-13

POSIX compliance (well, except for those mklink calls, of course) — no more Bashisms! Directories and files with spaces in them are supported. Zero and non-zero exit status codes (for communicating success/failure of the requested command, respectively) are now properly preserved/returned. The add-symlink alias now works more like ln(1) and can be used from any directory in the repository, not just the repository’s root directory. The rm-symlink alias (singular) has been superseded by the rm-symlinks alias (plural), which now accepts multiple arguments (or no arguments at all, which finds all of the symlinks throughout the repository, as before) for selectively transforming git symlinks into NTFS hardlinks+junctions. The checkout-symlinks alias has also been updated to accept multiple arguments (or none at all, == everything) for selective reversal of the aforementioned transformations.

Final Note: While I did test loading and running these aliases using Bash 3.2 (and even 3.1) for those who may still be stuck on such ancient versions for any number of reasons, be aware that versions as old as these are notorious for their parser bugs. If you experience issues while trying to install any of these aliases, the first thing you should look into is upgrading your shell (for Bash, check the version with CTRL+X, CTRL+V). Alternatively, if you’re trying to install them by pasting them into your terminal emulator, you may have more luck pasting them into a file and sourcing it instead, e.g. as

. ./git-win-symlinks.sh

其他回答

它应该在MSysGit中实现,但是有两个缺点:

符号链接只在Windows Vista及以后版本中可用(在2011年应该不是问题,但它确实是…),因为旧版本只支持目录连接。 微软认为符号链接存在安全风险,因此默认情况下只有管理员可以创建符号链接。您需要提升Git进程的特权,或者使用fstool在您工作的每台机器上更改此行为。

我做了一个快速搜索,这方面的工作正在积极进行;见第224期。

下面是一个批处理脚本,用于转换存储库中的符号链接,仅用于文件,基于Josh Lee的回答。https://gist.github.com/Quazistax/8daf09080bf54b4c7641上有一个带有管理员权限检查功能的脚本。

@echo off
pushd "%~dp0"
setlocal EnableDelayedExpansion

for /f "tokens=3,*" %%e in ('git ls-files -s ^| findstr /R /C:"^120000"') do (
     call :processFirstLine %%f
)
REM pause
goto :eof

:processFirstLine
@echo.
@echo FILE:    %1

dir "%~f1" | find "<SYMLINK>" >NUL && (
  @echo FILE already is a symlink
  goto :eof
)

for /f "usebackq tokens=*" %%l in ("%~f1") do (
  @echo LINK TO: %%l

  del "%~f1"
  if not !ERRORLEVEL! == 0 (
    @echo FAILED: del
    goto :eof
  )

  setlocal
  call :expandRelative linkto "%1" "%%l"
  mklink "%~f1" "!linkto!"
  endlocal
  if not !ERRORLEVEL! == 0 (
    @echo FAILED: mklink
    @echo reverting deletion...
    git checkout -- "%~f1"
    goto :eof
  )

  git update-index --assume-unchanged "%1"
  if not !ERRORLEVEL! == 0 (
    @echo FAILED: git update-index --assume-unchanged
    goto :eof
  )
  @echo SUCCESS
  goto :eof
)
goto :eof

:: param1 = result variable
:: param2 = reference path from which relative will be resolved
:: param3 = relative path
:expandRelative
  pushd .
  cd "%~dp2"
  set %1=%~f3
  popd
goto :eof

我一直在寻找一种简单的解决方案来处理Windows上的Unix符号链接。非常感谢您在之前的回答中提供Git别名。

可以对rm-符号链接进行一个小小的优化,以便在别名意外第二次运行时它不会删除目标文件夹中的文件。请观察循环中的新if条件,以确保在逻辑运行之前文件不是到目录的链接。

git config --global alias.rm-symlinks '!__git_rm_symlinks(){
for symlink in $(git ls-files -s | egrep "^120000" | cut -f2); do
    *if [ -d "$symlink" ]; then
      continue
    fi*
    git rm-symlink "$symlink"
    git update-index --assume-unchanged "$symlink"
done
}; __git_rm_symlinksenter

您可以通过查找模式为120000的文件来查找符号链接,可能使用以下命令:

git ls-files -s | awk '/120000/{print $4}'

一旦你替换了链接,我建议使用git update-index——assume-unchanged将它们标记为不变,而不是将它们列在.git/info/exclude中。

请注意更新

对于大多数受困于符号链接和git以及与*nix系统共享repo的Windows开发人员来说,这个问题已经解决了——只要您更新一下对mklink的Windows理解并打开开发者模式。

在深入讨论下面的git技巧之前,先看看这个更现代的答案。

老系统:

我曾经问过这个完全相同的问题(不是在这里,只是在一般情况下),最后得出了一个与OP的命题非常相似的解决方案。 我将发布我最终使用的解决方案。

但首先我将直接回答OP的3个问题:

问:“如果有的话,你认为这种方法有什么缺点?” 答:提议的解决方案确实有一些缺点,主要是增加了存储库污染的可能性,或者在它们处于“Windows符号链接”状态时意外添加重复文件。(关于这一点,请参阅下面的“限制”。)

问:“这个后检出脚本是可行的吗?例如,我可以递归找到虚拟的“符号链接”文件git创建? 答:是的,后检出脚本是可实现的!也许不能作为字面上的后git签出步骤,但是下面的解决方案已经很好地满足了我的需求,不需要字面上的后签出脚本。

问:“有人写过这样的剧本吗?” 答:是的!

解决方案:

我们的开发人员和OP的情况差不多:混合了Windows和类unix的主机、带有许多git符号链接的存储库和子模块,并且在MsysGit的发布版本中(目前)还没有本地支持在Windows主机上智能处理这些符号链接。

感谢Josh Lee指出git使用特殊文件模式120000提交符号链接。有了这些信息,就可以添加一些git别名,以便在Windows主机上创建和操作git符号链接。

Creating git symlinks on Windows git config --global alias.add-symlink '!'"$(cat <<'ETX' __git_add_symlink() { if [ $# -ne 2 ] || [ "$1" = "-h" ]; then printf '%b\n' \ 'usage: git add-symlink <source_file_or_dir> <target_symlink>\n' \ 'Create a symlink in a git repository on a Windows host.\n' \ 'Note: source MUST be a path relative to the location of target' [ "$1" = "-h" ] && return 0 || return 2 fi source_file_or_dir=${1#./} source_file_or_dir=${source_file_or_dir%/} target_symlink=${2#./} target_symlink=${target_symlink%/} target_symlink="${GIT_PREFIX}${target_symlink}" target_symlink=${target_symlink%/.} : "${target_symlink:=.}" if [ -d "$target_symlink" ]; then target_symlink="${target_symlink%/}/${source_file_or_dir##*/}" fi case "$target_symlink" in (*/*) target_dir=${target_symlink%/*} ;; (*) target_dir=$GIT_PREFIX ;; esac target_dir=$(cd "$target_dir" && pwd) if [ ! -e "${target_dir}/${source_file_or_dir}" ]; then printf 'error: git-add-symlink: %s: No such file or directory\n' \ "${target_dir}/${source_file_or_dir}" >&2 printf '(Source MUST be a path relative to the location of target!)\n' >&2 return 2 fi git update-index --add --cacheinfo 120000 \ "$(printf '%s' "$source_file_or_dir" | git hash-object -w --stdin)" \ "${target_symlink}" \ && git checkout -- "$target_symlink" \ && printf '%s -> %s\n' "${target_symlink#$GIT_PREFIX}" "$source_file_or_dir" \ || return $? } __git_add_symlink ETX )" Usage: git add-symlink <source_file_or_dir> <target_symlink>, where the argument corresponding to the source file or directory must take the form of a path relative to the target symlink. You can use this alias the same way you would normally use ln. E.g., 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-symlinks '!'"$(cat <<'ETX' __git_rm_symlinks() { case "$1" in (-h) printf 'usage: git rm-symlinks [symlink] [symlink] [...]\n' return 0 esac ppid=$$ case $# in (0) git ls-files -s | grep -E '^120000' | cut -f2 ;; (*) printf '%s\n' "$@" ;; esac | while IFS= read -r symlink; do case "$symlink" in (*/*) symdir=${symlink%/*} ;; (*) symdir=. ;; esac git checkout -- "$symlink" src="${symdir}/$(cat "$symlink")" posix_to_dos_sed='s_^/\([A-Za-z]\)_\1:_;s_/_\\\\_g' doslnk=$(printf '%s\n' "$symlink" | sed "$posix_to_dos_sed") dossrc=$(printf '%s\n' "$src" | sed "$posix_to_dos_sed") if [ -f "$src" ]; then rm -f "$symlink" cmd //C mklink //H "$doslnk" "$dossrc" elif [ -d "$src" ]; then rm -f "$symlink" cmd //C mklink //J "$doslnk" "$dossrc" else printf 'error: git-rm-symlink: Not a valid source\n' >&2 printf '%s =/=> %s (%s =/=> %s)...\n' \ "$symlink" "$src" "$doslnk" "$dossrc" >&2 false fi || printf 'ESC[%d]: %d\n' "$ppid" "$?" git update-index --assume-unchanged "$symlink" done | awk ' BEGIN { status_code = 0 } /^ESC\['"$ppid"'\]: / { status_code = $2 ; next } { print } END { exit status_code } ' } __git_rm_symlinks ETX )" git config --global alias.rm-symlink '!git rm-symlinks' # for back-compat. Usage: git rm-symlinks [symlink] [symlink] [...] This alias can remove git symlinks one-by-one or all-at-once in one fell swoop. Symlinks will be replaced 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. 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 '!'"$(cat <<'ETX' __git_checkout_symlinks() { case "$1" in (-h) printf 'usage: git checkout-symlinks [symlink] [symlink] [...]\n' return 0 esac case $# in (0) git ls-files -s | grep -E '^120000' | cut -f2 ;; (*) printf '%s\n' "$@" ;; esac | while IFS= read -r symlink; do git update-index --no-assume-unchanged "$symlink" rmdir "$symlink" >/dev/null 2>&1 git checkout -- "$symlink" printf 'Restored git symlink: %s -> %s\n' "$symlink" "$(cat "$symlink")" done } __git_checkout_symlinks ETX )" git config --global alias.co-symlinks '!git checkout-symlinks' Usage: git checkout-symlinks [symlink] [symlink] [...], 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: Directories/files/symlinks with spaces in their paths should work. But tabs or newlines? YMMV… (By this I mean: don’t do that, because it will not work.) If yourself or others forget to git checkout-symlinks before doing something with potentially wide-sweeping consequences like git add -A, the local repository could end up in a polluted state. 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.

引用:

http://git-scm.com/book/en/Git-Internals-Git-Objects

http://technet.microsoft.com/en-us/library/cc753194

最后更新:2019-03-13

POSIX compliance (well, except for those mklink calls, of course) — no more Bashisms! Directories and files with spaces in them are supported. Zero and non-zero exit status codes (for communicating success/failure of the requested command, respectively) are now properly preserved/returned. The add-symlink alias now works more like ln(1) and can be used from any directory in the repository, not just the repository’s root directory. The rm-symlink alias (singular) has been superseded by the rm-symlinks alias (plural), which now accepts multiple arguments (or no arguments at all, which finds all of the symlinks throughout the repository, as before) for selectively transforming git symlinks into NTFS hardlinks+junctions. The checkout-symlinks alias has also been updated to accept multiple arguments (or none at all, == everything) for selective reversal of the aforementioned transformations.

Final Note: While I did test loading and running these aliases using Bash 3.2 (and even 3.1) for those who may still be stuck on such ancient versions for any number of reasons, be aware that versions as old as these are notorious for their parser bugs. If you experience issues while trying to install any of these aliases, the first thing you should look into is upgrading your shell (for Bash, check the version with CTRL+X, CTRL+V). Alternatively, if you’re trying to install them by pasting them into your terminal emulator, you may have more luck pasting them into a file and sourcing it instead, e.g. as

. ./git-win-symlinks.sh