Git-Annex from scratch#

I found I missed an under-the-hood explanation of what Git-Annex added on top of an ordinary Git repository.

I’m partly starting from Joey Hess’ talk.

Here is a set of fundamental objects for Git-Annex, from that talk:

Git-Annex objects (GAOs)#

GAOs are objects in the same sense that Git stores objects in its object store.

For Git (not Git-Annex), when we do git add myfile, Git takes the contents of myfile (well, in fact, a compressed version of myfile) and copies it into the .git/objects directory, with a filename generated from the SHA1 hash of the object.

However, Git-Annex is designed to work with very large files that should not be compressed or copied. So when you create a Git-Annex object (with git annex add mybigfile):

  • The file is moved, not copied.

  • Within the working tree, Git-Annex leaves behind a pointer (usually a symbolic link) to the now-moved file. See below.

  • The moved file is not compressed.

  • The contents go into .git/annex/objects instead of .git/objects.

  • Git-Annex removes write permission on the moved file, to prevent you overwriting the file accidentally (see lock/unlock).

  • There are different rules for making the hash filename that we don’t need to worry about for now.

Git-Annex pointers#

Above, we said that when we make a file into a Git-Annex object, we leave a pointer to the now-moved file, in the working tree. The pointer is (on a Unix system) a symlink to the now-moved file in the .git/annex directory.

(Some systems, such as Windows by default, can’t use symbolic links; in this case Git-Annex can use pointer files).

It is these pointers (usually symbolic links) that we check into the Git repository.

Git-Annex remotes#

Git-Annex has remotes. Remotes are stores for GAOs.

This can be confusing, because:

  • Git-Annex can re-use ordinary Git repositories for this purpose. In this case the remote in the Git-Annex sense can also be a remote in the Git sense.

  • But Git-Annex also has the concept of a special remote, that is not a Git repository, and therefore cannot be a remote in the Git sense. Special remotes have various types of storage backing and interfaces, such as rclone pointing to Google Drive and so on.

I’ll call both Git re-used repositories, and special remotes — Git-Annex Remotes (GARs) — because they have the specific purpose of storing the GAOs. GARs are pointers to particular data stores that can store GAOs.

Each GAR has a Universally Unique Identifier (UUID), so that Git-Annex can record which of its remotes has each of the potential GAOs.

As this implies, any one individual GAR need not store all possible GAOs - it might have a subset. Git-Annex has to keep track of this information, so it can git annex get any files that the user asks for, that they do not already have in their .git/annex GAO object store.

Here is the confusion — your local Git repository also functions as a GAR — and has its own UUID. That is because, on your machine, after you git annex add (or, as you see later git annex get) a large file, your own local .git directory will have a copy of the GAO — in .git/annex/objects. So Git-Annex has to know that you also have a copy of the file, in your .git/annex/objects directory, so you or your collaborators can ask for a copy of the file from you, if they need to (using git annex get etc).

To say it again - a GAR is a different thing from a standard Git remote. It is specifically a place that Git-Annex can store or retrieve GAOs. Your local .git repository store is one such place, so it also functions as a GAR, from which you, or other people connected to your network, can get GAOs.

Git-Annex metadata#

Git-Annex makes and uses its own branch named git-annex to store metadata about the location of GAOs within the known remotes. We’ll come across that branch in the walkthrough below.

Walkthrough#

Make a new directory for the Git repository, that will soon also be Git-Annex-enhanced.

mkdir my-repo
cd my-repo

When we do git init we have the usual .git subdirectories — this is, so far, a normal Git repository.

git init
ls .git
Initialized empty Git repository in /tmp/tmpzhf2kc9x/annex-repos/my-repo/.git/
HEAD  config  description  hooks  info	objects  refs
tree .git/objects
.git/objects
├── info
└── pack
3 directories, 0 files
echo "Some text" > my_small_file
# Makes my_small_file into a Git object, stored in .git/objects
git add my_small_file
tree .git/objects
.git/objects
├── 01
│   └── b51e6ac5e39d1eb2fec3143437c6f117a58f03
├── info
└── pack
4 directories, 1 file

Make an ordinary Git commit. This generates a new directory-listing object, and a new commit object — the usual Git behavior.[1]

git commit -m "Added first file"
tree .git/objects
[main (root-commit) d28516c] Added first file
 1 file changed, 1 insertion(+)
 create mode 100644 my_small_file
.git/objects
├── 01
│   └── b51e6ac5e39d1eb2fec3143437c6f117a58f03
├── 7b
│   └── 527389ed9d0c64db733e80fadb582e211b5b7c
├── d2
│   └── 8516ce6e1514bed089804b10be3817e2f92076
├── info
└── pack
6 directories, 3 files

We only have one branch - the default main branch:

git branch -a
* main

Now we overlay the Git-Annex stuff on the normal Git repository:

git annex init
init  ok
(recording state in git...)

Notice that we now have a new annex directory in the .git directory.

ls .git
COMMIT_EDITMSG	annex	description  index  logs     refs
HEAD		config	hooks	     info   objects

.git/annex doesn’t have any objects yet, just some housekeeping files.

tree .git/annex
.git/annex
├── fsck
│   ├── fsck.lck
│   └── fsckdb
│       └── db
├── index
├── index.lck
├── journal
├── journal.lck
├── mergedrefs
├── othertmp
├── othertmp.lck
├── sentinal
└── sentinal.cache
5 directories, 9 files

We also have a new git-annex branch:

git branch -a
  git-annex
* main

Next we look at git annex info to show the Git-Annex-Remotes:

git annex info
trusted repositories: 0
semitrusted repositories: 3
	00000000-0000-0000-0000-000000000001 -- web
 	00000000-0000-0000-0000-000000000002 -- bittorrent
 	ae3098b4-633a-4130-bf1c-bbe3ffa9dd80 -- runner@runnervmgx7h7:/tmp/tmpzhf2kc9x/annex-repos/my-repo [here]
untrusted repositories: 0
transfers in progress: none
available local disk space: 90.81 gigabytes (+100 megabytes reserved)
local annex keys: 0
local annex size: 0 bytes
annexed files in working tree: 0
size of annexed files in working tree: 0 bytes
combined annex size of all repositories: 0 bytes
annex sizes of repositories:
backend usage:
bloom filter size: 32 mebibytes (0% full)

For the moment, notice the “semitrusted repositories”. These are GARs - places that Git-Annex knows can be sources or destinations for the Git-Annex-Object files. The first two (web and bittorrent) are generic sources corresponding to URLs, and bittorrent files, but we’ll ignore these for now. The third points to the local .git directory. This is a “remote” in a rather confusing sense - that is - it’s a place that can serve as a source for the files that Git-Annex will store (the GAOs). It’s a remote in the sense that other Git-Annex overlaid repositories may be able to use to get those files. You can think of it as a remote to other repositories, and a local store to this one.

Now we’ll add some potentially large file, using git annex add. That is going to make a Git-Annex Object (GAO), and leave a pointer (symlink) behind in the working directory.

First we make a file that we will pretend is large. We’ll also calculate its SHA1 sum - you’ll see why later.

echo "Something really large" > my_large_file
shasum my_large_file
eee0481dabe361a026281b3260a37732fa79881d  my_large_file

Now we make that large file into a Git-Annex Object with git annex add:

git annex add my_large_file
add my_large_file
100%  23 B             77 KiB/s 0s
ok
(recording state in git...)

Notice now that:

  • The file has moved to .git/annex/objects.

  • The moved file is now read-only (to prevent you accidentally overwriting it — see lock/unlock).

  • There’s a symlink to that file in the working directory.

  • Git-Annex added the symlink (and not the file itself) to the ordinary Git staging area.

In order:

# It's the same file exactly as the one previously in the working directory.
shasum .git/annex/objects/*/*/*/*
eee0481dabe361a026281b3260a37732fa79881d  .git/annex/objects/4k/p7/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720
# The moved file is read-only to prevent accidental overwrites.
ls -al .git/annex/objects/*/*/*/*
-r--r--r-- 1 runner runner 23 Aug 30 09:36 .git/annex/objects/4k/p7/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720
# The file in the working tree has become a symlink.
ls -al my_large_file
lrwxrwxrwx 1 runner runner 180 Aug 30 09:36 my_large_file -> .git/annex/objects/4k/p7/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720
# The symlink has been added to the Git staging area.
git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   my_large_file

git annex list tells us which GAR has each GAO. here is the name for the local GAR, in .git/annex:

git annex list
here
|web
||bittorrent
|||
X__ my_large_file

We can now do a commit - but notice - the thing that gets added to the standard Git history, and stays in .git/objects, is nothing but the symlink:

git commit -m "Add link to my_large_file"
[main d302fb2] Add link to my_large_file
 1 file changed, 1 insertion(+)
 create mode 120000 my_large_file

git show below identifies the symlink with the file mode 120000:

# Show contents of last commit.
git show main
commit d302fb2a1aa1285acc4707b523370cd00d890fac (HEAD -> main)
Author: ci <ci@example.com>
Date:   Sun Aug 30 09:36:48 2026 +0000
    Add link to my_large_file
diff --git a/my_large_file b/my_large_file
new file mode 120000
index 0000000..aa09c8c
--- /dev/null
+++ b/my_large_file
@@ -0,0 +1 @@
+.git/annex/objects/4k/p7/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720
\ No newline at end of file

To recap - the Git .git/objects directory never got a copy of my_large_file. The content of my_large_file belongs to Git-Annex, and is stored in the .git/annex/objects directory, that does not get transferred when you git push, or you git clone the repository. Git itself only has the symbolic link to the file. But as we’ll see soon, it does know where copies of my_large_file live, through the information in the git-annex branch.

To illustrate, if we do a typical clone of this my-repo repository, we do not have the contents of my_large_file; we only have the symlink:

cd ..
# Simulate pushing to some upstream service such as Github.
git clone --bare my-repo my-repo-upstream.git
# Simulate cloning from there.
git clone my-repo-upstream.git my-repo-clone
cd my-repo-clone
Cloning into bare repository 'my-repo-upstream.git'...
done.
Cloning into 'my-repo-clone'...
done.

We have put ourselves into the usual situation, where we’ve done a git push to a remote service, and then done a git clone from the remote service, to another computer. After doing that:

# No .git/annex directory in the clone.
ls .git
HEAD  config  description  hooks  index  info  logs  objects  packed-refs  refs
# But we do have access to the remote git-annex branch, which maps
# repositories to GAOs.
git branch -a
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/git-annex
  remotes/origin/main
# my_large_file is a broken symlink.
file my_large_file
my_large_file: broken symbolic link to .git/annex/objects/4k/p7/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720

If we try and fetch the file to which the symlink points, with git annex get, Git-Annex can’t do it, because it has no record (in the git-annex remote branch) that the origin (upstream) repository has that file in its .git/annex filesystem.

git annex list
here
|origin
||web
|||bittorrent
||||
____ my_large_file

However, the git-annex remote branch has told it where we might find the file (see Git-Annex internals). Notice the suggestion generated during the failure:

git annex get my_large_file
get my_large_file (not available)
  Maybe add some of these git remotes (git remote add ...):
  	ae3098b4-633a-4130-bf1c-bbe3ffa9dd80 -- runner@runnervmgx7h7:/tmp/tmpzhf2kc9x/annex-repos/my-repo
failed
get: 1 failed

This message is telling us that we need to point directly to the original repository as a source for the GAO files (in its .git/annex folder).

# Add the original repository as an ordinary Git remote.
git remote add original-repo ../my-repo --fetch
Updating original-repo
From ../my-repo
 * [new branch]      git-annex  -> original-repo/git-annex
 * [new branch]      main       -> original-repo/main
# Git-Annex now knows it can get the file.
git annex list
here
|origin
||original-repo
|||web
||||bittorrent
|||||
__X__ my_large_file

We ask Git-Annex to fetch the file.

git annex get my_large_file
get my_large_file (from original-repo...)
100%  23 B             10 KiB/s 0s
ok
(recording state in git...)
# There are two repositories that Git-Annex knows have
# the file (in `.git/annex`) - this repo, and the original.
git annex list
here
|origin
||original-repo
|||web
||||bittorrent
|||||
X_X__ my_large_file
# After git annex get, the symlink is fixed, because the file exists.
file my_large_file
my_large_file: symbolic link to .git/annex/objects/4k/p7/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720/SHA256E-s23--5533fe49b4ab945dc8c4b9d878a8c6e74232c8e00fe9a2b2e5f8d88a70406720

The special remotes#

As you have seen, Git-Annex can use Git repositories on filesystems as storage for GAOs — in the repository .git/annex directory.

But Git-Annex can also have special remotes, that have no necessary relationship to Git repositories. For example, they can work with directories on file-sharing systems such as Dropbox and Google Drive.

You create special remotes with the git annex initremote command. I don’t cover those here, but see the Git-Annex page on special remotes, and the doc page on special remotes.

Which files go where#

Consider setting annex.largefiles entries with git annex config, or in your .gitattributes file, to tell Git-Annex which files it should handle (as GAOs) and which Git should handle. See the annex.largefiles page for details. Note that annex.largefiles just identifies files that Git-Annex should handle. You can use values for annex.largefiles to make Git-Annex always operate on files larger than a particular size, but you can also use that setting to configure Git / Git-Annex to select files by path name.

Consider using git annex wanted commands to tell Git-Annex which remotes should house which files. These rules get stored in the git-annex branch. See the Git-Annex wanted page for more.

Whither Git-Annex#

That was a tour of the basics. You might now want to have a look at the primary Git-Annex pages.