Skip to main content

git-shed

CI

A repository-aware link manager for local files kept outside Git.

git-shed prepares a storage directory ("shed") for each Git remote you work with, and links it into the repository as .shed/<name>.

Notes, investigation SQL, debug scripts, local-only configuration, sample API responses, scratch code, work notes for an AI, test data — things you want next to a repository but not inside it.

~/.git-shed/
├── config.toml
└── sheds/
    ├── company/
    └── backend/

~/src/foo/
└── .shed/
    ├── company -> ~/.git-shed/sheds/company
    └── backend -> ~/.git-shed/sheds/backend

The real data lives outside the clone, so rm -rf on the clone keeps it, every clone of the same remote sees the same shed, and several repositories can share one shed. git-shed never looks inside a shed.

Install

uv tool install git-shed

This puts git-shed on PATH, which is all Git needs to dispatch git shed.

Tutorial

Clone a repository and ask for a shed. Nothing matches yet, so sync offers to create one; the defaults come from the remote:

$ git clone git@github.com:acme/api.git ~/src/api
$ cd ~/src/api
$ git shed sync

No shed matches:
  github.com/acme/api

Create a new shed? [Y/n] y

Shed name [api]:
Match pattern [github.com/acme/api]:

Create shed:
  name:  api
  match:
    - github.com/acme/api
  path:  /home/user/.git-shed/sheds/api

Create this shed? (y/n) y
Created shed:
  api

Linked:
  .shed/api -> /home/user/.git-shed/sheds/api

Write whatever you want under the link. It lives outside the clone, and Git does not see it:

echo '# debugging notes' > .shed/api/notes.md
git status --short          # nothing, .shed/ is in .git/info/exclude

Check out a second working tree and run sync there too. It matches the same remote, so it gets the same shed — the notes are already there:

$ git worktree add ../api-hotfix hotfix
$ cd ../api-hotfix
$ git shed sync

Linked:
  .shed/api -> /home/user/.git-shed/sheds/api

$ cat .shed/api/notes.md
# debugging notes

Delete either working tree and ~/.git-shed/sheds/api stays where it is.

Configuration

~/.git-shed/config.toml (or $GIT_SHED_ROOT/config.toml):

[[shed]]
name = "company"
match = ["github.com/acme/*"]

[[shed]]
name = "backend"
match = [
  "github.com/acme/api",
  "github.com/acme/worker",
]

[[shed]]
name = "personal"
match = ["github.com/me/*"]

Sheds are not exclusive: github.com/acme/api matches both company and backend, and gets both links.

A shed name starts with an ASCII letter or digit, continues with letters, digits, ., _ and -, and is at most 64 characters — the safe shape for a directory name on every platform. Windows device names (CON, NUL, ...) and a trailing . are rejected.

The data of a shed lives at ~/.git-shed/sheds/<name>, by convention rather than by configuration. GIT_SHED_ROOT moves the whole root — the configuration file and the shed data — elsewhere, for the whole tool:

export GIT_SHED_ROOT=/data/git-shed     # -> /data/git-shed/config.toml
                                        #    /data/git-shed/sheds/company

A leading ~ is expanded, since the variable is often set where a shell does not expand it (.env files, systemd units, container environments). The value has to denote an absolute path; a relative one is an error rather than something resolved against the current directory.

Usage

git shed sync              # create and drop links for the current repository
git shed sync --no-interactive
git shed add company 'github.com/acme/*' 'github.com/other/*'
git shed add company 'github.com/more/*'   # adds to the shed that exists
git shed add               # asks for whatever is missing
git shed link scratch      # link a shed here even though it does not match
git shed unlink scratch    # drop that link again
git shed remove company    # drops the definition, keeps the data
git shed remove company 'github.com/other/*'   # drops only these patterns
git shed status
git shed list              # matching sheds and their patterns
git shed list --all        # every defined shed and its patterns
git shed path company      # /home/user/.git-shed/sheds/company
git shed open company      # open it in the file manager

git shed list shows each shed with every pattern it is defined with, not only the one that matched:

$ git shed list
company
  github.com/acme/*
backend
  github.com/acme/api
  github.com/acme/worker

Shed names sit at the left margin and their patterns are indented, so git shed list | grep -v "^ " still gives you plain names for a script.

git shed add takes a shed name followed by any number of patterns. Naming a shed that already exists is not an error: its patterns are extended with the ones you give, and a pattern it already has is reported instead of duplicated. With no patterns on the command line it asks for one.

git shed remove with just a name drops the whole shed definition (never the data). With patterns after the name it removes only those patterns and keeps the shed; naming a pattern the shed does not have is an error.

git shed sync resolves the identities of the repository, creates ~/.git-shed/sheds/<name> and repo/.shed/<name> for every matching shed, drops the links that no longer match, and adds /.shed/ to .git/info/exclude. .gitignore is never touched.

It reports what it did:

Linked:
  .shed/company -> /home/user/.git-shed/sheds/company

Unlinked:
  .shed/old-shed

One-off links

git shed link <shed> links a shed into the current repository even though no match rule selects it — for the occasional reference that is not worth a pattern. The name is recorded in .shed/.sheds (one per line, # comments allowed), which is what keeps sync from dropping the link again:

$ git shed link scratch

Linked:
  .shed/scratch -> /home/user/.git-shed/sheds/scratch

The shed does not have to be defined in the configuration: a name whose data directory already exists under the storage root is enough, so a directory created by hand can be linked without writing a match rule for it. A name with neither a definition nor a directory is refused, which keeps a typo from creating one.

git shed unlink scratch removes the entry and the link; the data stays. A link that comes from the configuration is not unlink's business — drop its match rule instead.

When nothing matches the repository and the terminal is interactive, sync offers to create a shed:

$ git shed sync

No shed matches:
  github.com/acme/foo

Create a new shed? [Y/n] y

Shed name [foo]: company
Match pattern [github.com/acme/foo]: github.com/acme/*

Create shed:
  name:  company
  match:
    - github.com/acme/*
  path:  /home/user/.git-shed/sheds/company

Create this shed? (y/n) y

Remote identity

Matching uses a canonical identity rather than the raw remote URL, so all of

git@github.com:acme/foo.git
https://github.com/acme/foo.git
ssh://git@github.com/acme/foo
https://github.com/acme/foo

become

github.com/acme/foo

Every remote contributes an identity, and the sheds matching any of them are linked. In a fork checkout both the fork and the upstream repository are matched without configuring anything:

origin     git@github.com:me/foo.git       -> github.com/me/foo
upstream   git@github.com:acme/foo.git     -> github.com/acme/foo

To match on specific remotes only, name them — the setting is repeatable:

git config --local shed.remote upstream
git config --local --add shed.remote origin

A shed.remote naming a remote that does not exist is an error. A remote whose URL cannot be interpreted is skipped with a warning, and the other remotes are still used:

git shed: warning: ignoring remote 'helper': cannot interpret remote URL: transport::address

A repository with no usable remote is identified by the name of its working tree directory, so ~/src/notebook gets the identity notebook and is matched by a pattern of the same name:

[[shed]]
name = "notebook"
match = ["notebook"]

Patterns

Identities are matched segment by segment:

*    one segment
**   zero or more segments

A * can also stand for part of a segment, so a naming convention is enough to group repositories:

github.com/acme/foo        exactly this repository
github.com/acme/*          every repository of acme
github.com/acme-*/api      the api repository of acme-jp, acme-us, ...
github.com/acme/repo_*     acme repositories named repo_...
*/acme/*                   acme on any host
gitlab.com/company/**      company and everything below its subgroups

Matching is case-insensitive, and several sheds may match the same repository — there is no priority or specificity rule to reason about.

Safety

This tool holds data that is not in Git, so it stays away from anything destructive.

  • sync and remove only ever delete links / junctions, never shed data.

  • A real file or directory inside .shed/ is left alone, with a warning on stderr:

    git shed: warning: .shed/notes is not a link, left untouched
    
  • A changed remote does not move or delete anything; status shows the difference.

  • Only .git/info/exclude is modified, never .gitignore.

Platform support

Symbolic links on Linux and macOS, directory junctions on Windows. .shed/ itself is always a plain directory; only its entries are links.

Copying a project directory behaves differently on the two: cp -r keeps the links, so the copy shares the same shed, while most Windows tools (Explorer, xcopy, robocopy, Copy-Item) follow a junction and duplicate what is behind it — use robocopy /XJ to leave junctions out. Running git shed sync in the copy restores the links either way.

Exit codes

0 when the command completed — a repository matching no shed is not an error. Non-zero for a broken configuration file, a repository or remote that cannot be resolved, missing input for add, or a link that cannot be created.

Not in scope

The contents of a shed (layout, name collisions, backup, sync, encryption, secrets), per-shed paths, orphan cleanup, GC, migration and shell completion. It is not a replacement for git-annex or Git LFS.

Development

The test suite runs on Python 3.11 through 3.14. That matrix lives under [tool.uv-matrix] in pyproject.toml and is driven by uv-matrix; ruff needs no matrix:

uvx uv-matrix run --max-jobs 4       # the suite on every interpreter
uvx uv-matrix list                   # what the matrix expands to
uvx uv-matrix run --filter python-version=3.13

uv run --extra dev ruff check src tests
uv run --extra dev ruff format --check src tests

CI runs exactly these commands on Ubuntu. A single interpreter without uv works too:

pip install -e ".[dev]"
python -m pytest

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

git_shed-0.2.0.tar.gz (30.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

git_shed-0.2.0-py3-none-any.whl (23.9 kB view details)

Uploaded Python 3

File details

Details for the file git_shed-0.2.0.tar.gz.

File metadata

  • Download URL: git_shed-0.2.0.tar.gz
  • Upload date:
  • Size: 30.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.2

File hashes

Hashes for git_shed-0.2.0.tar.gz
Algorithm Hash digest
SHA256 473aa834e64f711b015b23812c374835fffe033fcea407513086048139e324c5
MD5 303d27ad0219996d210580e2463f89c4
BLAKE2b-256 e54c52ec173a79596e8f996da08fe9a106a573bcb539df1dbbeb54ab7d9e5a0a

See more details on using hashes here.

File details

Details for the file git_shed-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: git_shed-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 23.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.2

File hashes

Hashes for git_shed-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1cf051ab484e6b85d2485fa7cdef5684f0e213247c0ae7e382d100b3e33589f4
MD5 4eedcc60122380f97bace5d8a1a4c088
BLAKE2b-256 6ff8f209509d786ca7be77b7267a6347914a421cc21a569e3b2b51e7f7c87cd5

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page