268

Is my best be going to be a shell script which replaces symlinks with copies, or is there another way of telling Git to follow symlinks?

PS: I know it's not very secure, but I only want to do it in a few specific cases.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Matt
  • 5,522
  • 5
  • 29
  • 24
  • 7
    is there a disadvantage to using hard links for something like this? – Ehtesh Choudhury Jun 01 '11 at 05:19
  • 14
    With Windows 7, "mklink /d" (directory symbolic link) doesn't work with git, but "mklink /j" (juction) works fine. – yoyo Jan 16 '12 at 21:32
  • 3
    If the file is autogenerated by an application which regenerates it in such a way that it deletes the file and creates a new one, then yes, this is a problem hardlinks to files won't solve. – Martin Pecka Jul 18 '19 at 18:16
  • 7
    @EhteshChoudhury you can't make hard links for directories – Gaurav Kansal Apr 19 '20 at 04:39
  • 2
    @EhteshChoudhury hard links require the link and the destination to be on the same disk – Duncan MacIntyre May 27 '21 at 19:22
  • @EhteshChoudhury I tried using hard links and ended up having unexpected errors with old files being committed by accident instead of the updated ones. My solution was to write a script to convert the soft links to hard, commit and then revert all the links back to soft again. – SurpriseDog Aug 12 '21 at 17:06
  • You also can't make hard links when the files are in separate filesystems. – strupo Sep 13 '22 at 16:26

14 Answers14

177

What I did to add to get the files within a symlink into Git (I didn't use a symlink but):

sudo mount --bind SOURCEDIRECTORY TARGETDIRECTORY

Do this command in the Git-managed directory. TARGETDIRECTORY has to be created before the SOURCEDIRECTORY is mounted into it.

It works fine on Linux, but not on OS X! That trick helped me with Subversion too. I use it to include files from an Dropbox account, where a webdesigner does his/her stuff.

If you want to make this bind permanent add the following line to /etc/fstab:

/sourcedir /targetdir none bind
João Pimentel Ferreira
  • 14,289
  • 10
  • 80
  • 109
user252400
  • 1,945
  • 2
  • 12
  • 3
  • 13
    Would be a very nice approach if sudo wasnt required. – MestreLion Aug 23 '11 at 03:11
  • 15
    To undo this binding, use `umount [mydir]`. (+1 for your great tip, @user252400) – JellicleCat Jul 01 '12 at 03:40
  • 11
    This only works during the session. What is the best way to make it "eternal"? – Adobe Sep 02 '12 at 07:04
  • 19
    @Adobe: put it in /etc/fstab, like so: /sourcedir /targetdir none bind – Alexander Garden Sep 07 '12 at 15:37
  • 1
    @AlexanderGarden: it works: `/home/boris/pst/wordy/edu/HotKeyPoetry /home/boris/.bkubuntu/bks/computer/HotKeyPoetry none bind`. Thank You. – Adobe Sep 08 '12 at 16:21
  • 10
    sshfs can achieve that kind of trick without requiring the sudo, here. – PypeBros Sep 10 '12 at 19:27
  • 3
    @sylvainulg: sweet trick! you still need sudo but only to install sshfs [apt-get install sshfs]. Then a simple [sshfs username@localhost:/etc/folder ./target_folder] does the job for git. Brillant ;). Best solution here for me! – bksunday Sep 13 '12 at 01:00
  • 2
    What is/are the difference(s) between this and a hard link? – WiseOldDuck Feb 13 '14 at 18:32
  • 5
    For OSX, use http://bindfs.org/ (you can get it with `brew install bindfs`, be sure to follow the instructions displayed on screen to properly setup osxfuse). – Norswap May 15 '14 at 14:02
  • 1
    This plays nice with etckeeper. Maintain all you system wide configuration in /etc! –  Feb 14 '15 at 01:54
  • 1
    Really nice trick. @WiseOldDuck one possible difference it's really sucks makes `hardlink` for directories. With this we can use hardlinks for files and `mount --bind` for directories. – Manoel Vilela Apr 29 '16 at 08:55
  • Can you run git commands from inside the mount point? For me it thinks I'm outside of the repo. – SO_fix_the_vote_sorting_bug Jul 23 '17 at 04:03
  • 1
    If you put it in /etc/fstab with `bind,user` instead of just `bind`, it'll be set up so you can mount and unmount without sudo as needed. That's potentially useful to have that if you're going to be making changes which require the bind mount to be temporarily disconnected. Stick a `,noauto` on there to not have it bound at boot, too. – dannysauer Sep 24 '18 at 14:49
  • You can use `bwrap` to bind mount directories without `sudo`, It's available on all major distributions and likely comes preinstalled on many. See my [answer](https://stackoverflow.com/a/63645471/10126273) bellow for more details. – Tenders McChiken Aug 29 '20 at 09:35
88

Why not create symlinks the other way around? Meaning instead of linking from the Git repository to the application directory, just link the other way around.

For example, let’s say I am setting up an application installed in ~/application that needs a configuration file config.conf:

  • I add config.conf to my Git repository, for example, at ~/repos/application/config.conf.
  • Then I create a symlink from ~/application by running ln -s ~/repos/application/config.conf.

This approach might not always work, but it worked well for me so far.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
spier
  • 2,642
  • 1
  • 19
  • 16
  • 5
    Seems to be the only way, and its not that bad... i think yours is quite an elegant approach. git tracks content, not files. So keeping all content together and symlinking from there to other places makes sense – MestreLion Aug 23 '11 at 03:11
  • 17
    In my case I wanted a link from one git repo to another, so I can edit files in either location and commit back to their respective remotes. On Windows 7, a junction ("mklink /j") did the trick. – yoyo Jan 16 '12 at 21:33
  • 3
    of course. sometimes the answer is just that simple. – BBW Before Windows May 20 '15 at 15:30
  • 1
    what if you want to git both the source AND the destination? (because both belong to different codes you want to have in different repositories) – DrGC Feb 25 '16 at 19:01
  • 1
    other way arouind there is an nginx that is refusing serve files from symlink – andrej May 18 '17 at 08:26
  • Because if you want to share a folder between two git repositories then you can't do it 'the other way around'. I honestly think git needs to incorporate a --follow-symlinks option. – Dirk R Jan 10 '18 at 07:39
  • I was looking to backup my profiles from various applications and I feel like that's the best approach, you could also add to your repository a script that creates these links back (in my case it's what I needed). – cglacet Dec 30 '19 at 11:24
  • 2
    Doesn't answer the question :( I wanted part of my repository being synced into my iCloud. Unfortunatelly, iCloud doesn't follow symlinks, so I thought I may make git follow symlinks, and store original files in iCloud. Turns out nobody follows symlinks :\ – Jerry Green Mar 15 '20 at 00:46
63

Use hard links instead. This differs from a soft (symbolic) link. All programs, including git will treat the file as a regular file. Note that the contents can be modified by changing either the source or the destination.

On macOS (before 10.13 High Sierra)

If you already have git and Xcode installed, install hardlink. It's a microscopic tool to create hard links.

To create the hard link, simply:

hln source destination

macOS High Sierra update

Does Apple File System support directory hard links?

Directory hard links are not supported by Apple File System. All directory hard links are converted to symbolic links or aliases when you convert from HFS+ to APFS volume formats on macOS.

From APFS FAQ on developer.apple.com

Follow https://github.com/selkhateeb/hardlink/issues/31 for future alternatives.

On Linux and other Unix flavors

The ln command can make hard links:

ln source destination

On Windows (Vista, 7, 8, …)

Use mklink to create a junction on Windows:

mklink /j "source" "destination"
fregante
  • 29,050
  • 14
  • 119
  • 159
  • This is only for HFS/HFS+ on osX! – user2284570 Oct 13 '14 at 02:44
  • 8
    Just a note: this is basically what I was looking for, but then I learned that on Linux, a hardlink unfortunately cannot cross filesystem boundaries (which is my use case). – sdaau Jan 17 '15 at 18:31
  • This approach won't work if your target has some symlink in its path. – rindeal Aug 10 '15 at 08:02
  • 43
    You can't hardlink to directories, can you? – Nanne Nov 09 '15 at 13:19
  • 1
    `ln source destination` works in OS X, too. Tested on El Capitan. – Mahdi Dibaiee Dec 04 '15 at 09:35
  • 9
    @Nanne no, but you can do: `cp -al source destination`. `-l' means hard link files instead of copying. – Paolo Mar 01 '16 at 20:42
  • 11
    Unfortunately you cannot hard-link directories, or across file system boundaries. This makes this solution twofold unworkable for me. – Konrad Rudolph Jan 23 '17 at 12:05
  • Even @Paolo solution is KO across file systems: cp: cannot create hard link ‘localDirpath1/file’ to ‘/dirpath2/file’: Invalid cross-device link – user1767316 Jul 21 '17 at 14:09
  • Directory hardlinks are not working on APFS with OSX High Sierra. – jmcouillard Nov 11 '17 at 17:38
  • 1
    Works beautifully on windows – Jack Oct 04 '18 at 21:35
  • 1
    Does this not break as soon as any operation unlinks either instance of the file (as in one has both copies of the file but the hardlink is broken)? Reverting patches, etc. use unlink. – Walf Jul 14 '21 at 03:08
  • @Walf , yes good editors and git itself usually don't simply overwrite _into_ files with `st_nlink > 1`, and at least ask for overwrite vs creating a new file - preferring the new file creation. So using hard links here is like painting on water ... – kxr Sep 06 '22 at 10:41
  • I find the given `ln` notation confusing (and the wrong way around?). From the man page: `ln TARGET LINK_NAME` – Stefan Apr 12 '23 at 13:24
46

NOTE: This advice is now out-dated as per comment since Git 1.6.1. Git used to behave this way, and no longer does.


Git by default attempts to store symlinks instead of following them (for compactness, and it's generally what people want).

However, I accidentally managed to get it to add files beyond the symlink when the symlink is a directory.

I.e.:

  /foo/
  /foo/baz
  /bar/foo --> /foo
  /bar/foo/baz

by doing

 git add /bar/foo/baz

it appeared to work when I tried it. That behavior was however unwanted by me at the time, so I can't give you information beyond that.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kent Fredric
  • 56,416
  • 14
  • 107
  • 150
  • 77
    The commits 725b06050a083474e240a2436121e0a80bb9f175 and 806d13b1ccdbdde4bbdfb96902791c4b7ed125f6 introduced changes that stopped you adding files beyond symlinked directories, so this won't work in versions of git since 1.6.1 – Mark Longair Aug 09 '10 at 10:15
  • 1
    $ git add src/main/path/ConvertSymlinkToDir fatal: 'src/main/path/ConvertSymlinkToDir' is beyond a symbolic link – user1767316 Jul 21 '17 at 14:04
  • 2
    @user1767316 read the whole thing, and the comments. It used to work, it no longer does. Software changes, but stack-overflow accepted answers don't. I've clarified this doesn't work already. Look at another answer. – Kent Fredric Jul 22 '17 at 23:21
  • yes @KentFrederic but wiriting the exact error message returned help stack user's search for the solution to its problem.tried to cancel downvote but locked sorry. On the one hand your answer is right given the warning, on the other hand one should give priority to answer working now rather than in the past – user1767316 Jul 26 '17 at 16:29
  • 1
    Commit [e0d201b](https://github.com/git/git/commit/e0d201b61601e17e24ed00cc3d16e8e25ca68596) makes it seem like the git maintainers really don't want symbolic links to be resolved. I don't understand why, but I suppose I'll resort to using a bind mount. – Paul Jul 24 '20 at 19:38
30

This is a pre-commit hook which replaces the symlink blobs in the index, with the content of those symlinks.

Put this in .git/hooks/pre-commit, and make it executable:

#!/bin/sh
# (replace "find ." with "find ./<path>" below, to work with only specific paths)

# (these lines are really all one line, on multiple lines for clarity)
# ...find symlinks which do not dereference to directories...
find . -type l -exec test '!' -d {} ';' -print -exec sh -c \
# ...remove the symlink blob, and add the content diff, to the index/cache
    'git rm --cached "$1"; diff -au /dev/null "$1" | git apply --cached -p1 -' \
# ...and call out to "sh".
    "process_links_to_nondir" {} ';'

# the end

Notes

We use POSIX compliant functionality as much as possible; however, diff -a is not POSIX compliant, possibly among other things.

There may be some mistakes/errors in this code, even though it was tested somewhat.

Abbafei
  • 3,088
  • 3
  • 27
  • 24
  • 4
    It's great to see an attempt to actually answer the question for files and not directories. However, note that the above will still show ``typechange`` in ``git status`` for the files which are actually symlinks though git now things they are not. – David Fraser Mar 11 '14 at 19:55
  • 1
    Thanks for this; just wanted to know, what is `process_links_to_nondir`? – sdaau Jan 17 '15 at 20:00
  • 1
    @sdaau It is the name/`argv[0]` which is used as the command-name for the `sh` process. (Took me a bit to figure that out, since I didn't remember what it was either ☺ ) – Abbafei Jan 19 '15 at 03:52
  • 3
    @Abbafei Can you modify the script to make it work on Ubuntu (14.04)? It is showing `find: missing argument to -exec'`. May be a step by step command execution is needed instead of piping & combining everything into single line. – Khurshid Alam Feb 13 '15 at 17:10
  • 1
    @KhurshidAlam For me it worked to remove the commented lines in between the command lines. However, the hook does not work as expected (I get the `typechange` like @DavidFraser, but the linked file seems not to be staged anymore) – Scz Mar 15 '17 at 08:53
21

On macOS (I have Mojave/ 10.14, git version 2.7.1), use bindfs.

brew install bindfs

cd /path/to/git_controlled_dir

mkdir local_copy_dir

bindfs </full/path/to/source_dir> </full/path/to/local_copy_dir>

It's been hinted by other comments, but not clearly provided in other answers. Hopefully this saves someone some time.

ijoseph
  • 6,505
  • 4
  • 26
  • 26
  • seems great for teams that all run on mac os, I think hard links outlined below should work for windows and linux. – Devin Rhode Dec 27 '19 at 05:19
  • 1
    This was helpful, and I think the best solution for a useful-but-missing capability in MacOS. Note however that I was getting `Failed to resolve`...`No such file or directory` errors *unless* I used **full path names** with the `bindfs` command. – electromaggot Mar 13 '20 at 04:15
  • Thanks @electromaggot . Added clarification that full paths necessary – ijoseph Mar 13 '20 at 21:52
  • 1
    Works great on macos Catalina! – Jerry Green Mar 15 '20 at 02:01
  • Not working on Big Sur, error: `bindfs has been disabled because it requires closed-source macFUSE!` – Prashant Shubham Dec 03 '21 at 11:21
  • 1
    @PrashantShubham , https://github.com/telepresenceio/telepresence/issues/1654#issuecomment-873538291 might help (explicitly `brew install --cask macfuse`) – ijoseph Dec 04 '21 at 19:31
  • 1
    @PrashantShubham @ijoseph once you install `macfuse`, you have to download the latest version of `bindfs` and build it. You MUST use `gmake` (the version installed with `brew`) not the Mac default `make` to get it to work. Follow the steps in the `README` to make and install. Once its installed, be sure to create the empty sub-directory first, since this is essentially a mount operation. And as noted, `bindfs` requires a full path on both parameters (and in target be sure to add the directory you created as the mount point as the last element in the path) – Aron T Dec 10 '21 at 10:40
  • @AronT Weirdly enough, I needed to do this again 4 years later, and had to find this answer to remember how. "You MUST use gmake (the version installed with brew) not the Mac default make to get it to work" was untrue for me; `/usr/bin/make` (`GNU Make 3.81` (Copyright (C) 2006)) worked fine. – ijoseph Jun 04 '23 at 03:56
  • 1
    @ijoseph that is indeed strange. Maybe there is something else going on that `gmake` stepped around, which newer version of Mac OS X deal with. Even weirder is that Apple hasn't upgraded its version of `make` nearly 2 years on! – Aron T Jun 04 '23 at 12:00
  • @AronT ah, that's explained by Apple wanting to DRM its stuff and related aversion to GPL-3: https://www.reddit.com/r/bash/comments/393oqv/why_is_the_version_of_bash_included_in_os_x_so_old/cs03n0r/ – ijoseph Jun 05 '23 at 18:11
20

I got tired of every solution in here either being outdated or requiring root, so I made an LD_PRELOAD-based solution (Linux only).

It hooks into Git's internals, overriding the 'is this a symlink?' function, allowing symlinks to be treated as their contents. By default, all links to outside the repo are inlined; see the link for details.

Alcaro
  • 1,664
  • 16
  • 21
  • Very creative solution using `LD_PRELOAD` to override library functions! – iBug May 20 '18 at 01:47
  • Yes, but it ought to be elaborated here. Can you do that? – Peter Mortensen Oct 27 '18 at 10:46
  • Not sure how much elaboration would help; unless I copypaste the entire source code, this answer will always rely on that link, where a readme can also be found. But yes, I guess I could reproduce the important parts of the readme. – Alcaro Oct 27 '18 at 14:34
  • This does not compile on OS X (Mojave 10.14.2). Get four errors complaining about 'strchrnul' (did you mean 'strchr'?) and one about '__xstat64' (did you mean '__lxstat64'?). Finally I get a "member access into incomplete type 'dirent64'" error. Happens regardless if I use "make", "make OPT=1" or "sh install.sh". – Erik Veland Jan 04 '19 at 05:37
  • @ErikVeland I tried a bit, but it seems like OSX does not support LD_PRELOAD, nor anything similar. Various docs suggest various things, but they're all several years old, and Apple loves deprecating stuff; I couldn't get any of them working. Sorry. – Alcaro Jan 04 '19 at 21:44
  • This is really a smart and Niubi solutions. I like it and it works – fishshrimp鱼虾爆栈 Jan 16 '23 at 14:56
15

I used to add files beyond symlinks for quite some time now. This used to work just fine, without making any special arrangements. Since I updated to Git 1.6.1, this does not work any more.

You may be able to switch to Git 1.6.0 to make this work. I hope that a future version of Git will have a flag to git-add allowing it to follow symlinks again.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Erik Schnetter
  • 196
  • 1
  • 4
7

With Git 2.3.2+ (Q1 2015), there is one other case where Git will not follow symlink anymore: see commit e0d201b by Junio C Hamano (gitster) (main Git maintainer)

apply: do not touch a file beyond a symbolic link

Because Git tracks symbolic links as symbolic links, a path that has a symbolic link in its leading part (e.g. path/to/dir/file, where path/to/dir is a symbolic link to somewhere else, be it inside or outside the working tree) can never appear in a patch that validly applies, unless the same patch first removes the symbolic link to allow a directory to be created there.

Detect and reject such a patch.

Similarly, when an input creates a symbolic link path/to/dir and then creates a file path/to/dir/file, we need to flag it as an error without actually creating path/to/dir symbolic link in the filesystem.

Instead, for any patch in the input that leaves a path (i.e. a non deletion) in the result, we check all leading paths against the resulting tree that the patch would create by inspecting all the patches in the input and then the target of patch application (either the index or the working tree).

This way, we:

  • catch a mischief or a mistake to add a symbolic link path/to/dir and a file path/to/dir/file at the same time,
  • while allowing a valid patch that removes a symbolic link path/to/dir and then adds a file path/to/dir/file.

That means, in that case, the error message won't be a generic one like "%s: patch does not apply", but a more specific one:

affected file '%s' is beyond a symbolic link
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
4

Hmmm, mount --bind doesn't seem to work on Darwin.

Does anyone have a trick that does?

[edited]

OK, I found the answer on Mac OS X is to make a hardlink. Except that that API is not exposed via ln, so you have to use your own tiny program to do this. Here is a link to that program:

Creating directory hard links in Mac OS X

Enjoy!

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
J Chris A
  • 1,014
  • 8
  • 12
  • 1
    If the destination directory of this hardlink is a subdirectory of another git repo, this would be a chaos. Doing git operations in the hardlink would apply to this other git repo. Just double check what you are doing. – yegle Nov 18 '12 at 19:32
  • 4
    It's possible to do this via https://code.google.com/p/bindfs/ which can be installed using port. – Kit Sunde Feb 12 '13 at 21:40
1

An alternative implementation of what @user252400 proposes is to use bwrap, a small setuid sandbox that can be found in all major distributions - often installed by default. bwrap allows you to bind mount directories without sudo and automatically unbinds them when git or your shell exits.

Assuming your development process isn't too crazy (see bellow), start bash in a private namespace and bind the external directory under the git directory:

bwrap --ro-bind / / \
      --bind {GIT-DIR} {GIT-DIR} \
      --bind {EXTERNAL-DIR} {MOUNTPOINT-IN-GIT-DIR} \
      --dev /dev \
      bash

Then do everything you'd do normally like git add, git commit, and so on. When you're done, just exit bash. Clean and simple.

Caveats: To prevent sandbox escapes, bwrap is not allowed to execute other setuid binaries. See man bwrap for more details.

Tenders McChiken
  • 1,216
  • 13
  • 21
  • @tenders-mcchicken Doesn't work when using git commit. `fatal: Unable to create '/home/pcuser/Documents/Desktop-Projects/localhost/.git/index.lock': Read-only file system` – Khurshid Alam Jun 30 '23 at 08:58
  • @KhurshidAlam Yep because everything but the external directory was mounted as read-only, You can either mount everything as read-write (i.e. `--bind / /`), or you can mount only the git directory as writable (which is what I did in my updated answer). Please note that I may no longer answer or update my answers on this site because of this: https://meta.stackexchange.com/questions/389922/june-2023-data-dump-is-missing – Tenders McChiken Jul 01 '23 at 13:00
1

1. You should use hard links as changes made in the hard links are staged by git.

2. The syntax for creating a hard link is ln file1 file2.

3. Here file1 is the location of the file whose hard link you want to create and file2 is the location of the hard link .

4. I hope that helps.

0

I'm using Git 1.5.4.3 and it's following the passed symlink if it has a trailing slash. E.g.

# Adds the symlink itself
$ git add symlink

# Follows symlink and adds the denoted directory's contents
$ git add symlink/
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
0

Conversion from symlinks could be useful. Link in a Git folder instead of a symlink by a script.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Yurij73
  • 5,281
  • 3
  • 15
  • 14