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:

  • Write a post-checkout script that will recursively look for "symlink" text files.
  • Replace them with windows symlink (using mklink) with same name and extension as dummy "symlink"
  • Ignore these windows symlink by adding entry into .git/info/exclude
  • I have not implemented this, but I believe this is a solid approach to this problem.

    Questions:

  • What, if any, downsides do you see to this approach?
  • Is this post-checkout script even implementable? ie can I recursively find out the dummy "symlink" files git creates?
  • Has anybody already worked on such script?

  • 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.

  • There are indeed a few downsides to the proposed solution, mainly regarding an increased potential for repository pollution, or accidentally adding duplicate files while they're in their "Windows symlink" states. (More on this under "limitations" below.)
  • Yes, a post-checkout script is implementable! Maybe not as a literal post- git checkout step, but the solution below has met my needs well enough that a literal post-checkout script wasn't necessary.
  • Yes!
  • 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:

  • Can only be run from the root of the repo, else, weirdness happens...
  • Tab-based auto-completion is broken when typing out one of these aliases
  • 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.

    在这里输入图像描述

    链接地址: http://www.djcxy.com/p/78104.html

    上一篇: 如何查找目录树中的所有符号链接?

    下一篇: Windows中的Git符号链接