Link files to Git repositories.
gog copies a file into a Git repository and replaces the original with a
symbolic link to it, so that dotfiles in ${HOME} or elsewhere can be
committed, pushed, and applied on another machine. It supports multiple
repositories, which can separate personal and work files.
Archives are published on the
releases page: one per tagged
version, plus a dev release rebuilt on every push to main.
| Platform | Asset |
|---|---|
| Linux x86_64 | gog_linux_x86_64.tar.gz |
| Linux arm64 | gog_linux_arm64.tar.gz |
| macOS Apple Silicon | gog_darwin_arm64.tar.gz |
The archive also carries LICENSE and README.md at the top level, so name
the binary rather than extracting everything into the current directory:
tar -xzf gog_linux_x86_64.tar.gz gog
sudo install -m 755 gog /usr/local/bin/gogRequires Go and
Make. make install copies the binary to
/usr/local/bin with sudo; PREFIX chooses another destination.
git clone https://github.com/andornaut/gog.git
cd gog
make install# Clone a repository and add a file to it
gog repository add dotfiles https://example.com/user/dotfiles.git
gog add ~/.config/foorc
# Linked: /home/example/.config/foorc -> /home/example/.local/share/gog/dotfiles/root/\$HOME/.config/foorc
# Publish it
gog git commit -am 'Add foo config'
gog git push
# On another machine
gog repository add dotfiles https://example.com/user/dotfiles.git
gog apply| Command | Description |
|---|---|
gog add <paths>... |
Copy paths into the repository, link them back, and stage them |
gog apply |
Link the repository's contents to the filesystem |
gog ls |
Print the paths the repository holds |
gog rm <paths>... |
Restore paths as ordinary files and untrack them |
gog git ... |
Run git in the repository's directory |
gog repository add <name> [url] |
Create a repository, by clone if a URL is given |
gog repository default |
Print the default repository |
gog repository ls |
Print every repository |
gog repository rm <name> |
Restore what the repository holds, then delete it |
| Flag | Commands | Description |
|---|---|---|
-r, --repository NAME |
add, apply, ls, rm, git |
Repository to use. A unique prefix of the name is accepted |
-s, --status |
ls |
Print what applying would do to each path |
--path |
repository default, repository ls |
Print paths instead of names |
--force |
add |
Take a path over from the repository that manages it |
--force |
repository rm |
Delete even if the repository holds work that no remote has |
--version |
gog |
Print the version |
A short flag means the same thing in every tool that has it, so --path,
--force and --version are spelled out: -p, -f and -v each mean
something else in mrs.
Run gog help <command> for full usage.
A repository keeps what it links under root/, whose tree mirrors the
filesystem, and everything beside it belongs to the repository itself:
dotfiles/
root/
$HOME/.bashrc -> ~/.bashrc
etc/hosts -> /etc/hosts
.github/ never linked, as is anything else at this level
.gitignore
LICENSE
README.md
A repository with no root/ has nothing to link, and gog apply and gog ls
say so rather than exiting silently.
- A path under the home directory is stored with a literal
$HOMEcomponent:~/.bashrcis stored asroot/$HOME/.bashrc. gog applyexpands it to the home directory of whoever runs it, so one repository serves several users and machines.- A path outside the home directory is stored by its absolute name.
- The repository directory is named
$HOMEliterally. gog prints it escaped as\$HOME, so that the path can be pasted into a shell.
| Kind | Given to gog add |
Found inside an added directory |
|---|---|---|
| File, directory | Added | Added |
| Symbolic link | Refused, names its target instead | Skipped, with a warning |
| Path another repository manages | Refused unless --force |
Skipped, with a warning |
| Named pipe, socket, device node | Refused | Skipped, with a warning |
-
A symbolic link that gog created is followed, so a path the repository already holds can be added again.
-
A path that another repository manages is refused, because taking it over leaves that repository holding a copy nothing points at. Inside an added directory it is skipped instead, so that one managed file does not fail the whole directory.
--forceacts on the path you name, not on what a directory holds.Error: "/home/example/.bashrc" is managed by repository work (remove it from there first, or pass --force to take it over) Warning: skipping /home/example/.config/foorc (repository work already manages it; remove it from there first) -
A path inside a repository is refused, naming the path it is linked from, which is the one
gog addandgog rmmean. -
Skipping the irregular entries lets a directory such as
~/.gnupgbe added while the agent sockets in it are left alone. -
A file with more than one name is copied once per name. Git records contents per path, so a hard link is not preserved.
-
Every path is checked before any is copied, so one unusable argument fails the command rather than leaving the repository half modified.
Git records only the executable bit. A file added as 0600 is recreated as
0644 by the clone, and a directory added as 0700 as 0755. gog add
warns:
Warning: /home/example/.netrc has mode 0600, which git does not record; it will be applied as 0644 on another machine
Track ~/.ssh or ~/.netrc only if you accept that they will be
world-readable wherever the repository is applied.
gog apply never deletes a file it did not put there. It reports the path,
leaves it alone, carries on with the rest of the repository, and fails:
Error: "/home/example/.bashrc" already exists (move or remove it, then run the command again)
Error: some paths could not be linked
A path is replaced without asking only when nothing of yours is lost:
- a broken symbolic link
- a link into gog's data directory, left by an earlier run or by another repository that tracks the same path
- a file whose contents the repository already holds, which is what
gog addleaves behind after copying it in
$ gog ls --status
linked /home/example/.bashrc
missing /home/example/.vimrc
replace /home/example/.inputrc
conflict /home/example/.gitconfig
| State | What gog apply would do |
|---|---|
linked |
Nothing. The link is already there |
missing |
Link it. Nothing is at that path |
replace |
Discard what is there, then link it |
conflict |
Report it and leave it alone |
A repository's own .git, .gitignore, LICENSE and README.md are never
linked. gog ls leaves them out.
Runs git in the repository's directory and exits with git's own status.
- A path argument is rewritten to the file inside the repository that its link
points at, but only where git is certain to read an argument as a path: after
a
--separator, and for the operands ofadd,check-ignore,clean,rmandstagewhen one of those is the first argument. Everywhere else it is passed through, sogog git commit -m .bashrcrecords the message.bashrcandgog git branch wipcreates a branch. A global flag before the subcommand leaves it unidentified, so the path ingog git -C . add ~/.bashrcis passed through, and only a--separator still converts one. -r NAMEhas to be the first argument. Anywhere else it belongs to git, sogog git branch -randgog git ls-tree -r HEADkeep their meaning.--helpreaches git. Rungog help gitfor gog's own.- The
GIT_*variables that bind git to a repository, an index, or a configuration source are removed from its environment, so an enclosing git invocation such as a hook cannot redirect it.
gog git add ~/.bashrc # resolves to the repository's copy
gog git log -- ~/.bashrc # the same, after the separator- Restores each path as an ordinary file, then drops it from the repository and the index.
- Behaves the same whether the link is still there, was replaced with a file of your own, or was deleted.
- Reports a path the repository never held (
Skipped: ...) rather than failing. - Validates the whole batch before restoring anything.
- Not
gog git rm, which deletes the repository's copy and stages the deletion, leaving the link outside it pointing at nothing.gog rmis the one that hands the file back.
Deleting a repository cannot be undone by cloning again, so it is refused while the repository holds work that exists nowhere else:
$ gog repository rm dotfiles
Error: refusing to remove dotfiles: it holds 1 commit that no remote has and 2 uncommitted changes (pass --force to delete it anyway)
- A repository with no remote at all reports its whole history. Push it, or
pass
--force. - Nothing is restored or deleted until this check passes.
- Once it does, every file the repository had linked is restored as an ordinary file, leaving alone any path whose link belongs to another repository.
- The name is given in full: unlike
-r, a prefix is not accepted.
| Stream | Carries |
|---|---|
| stdout | What a caller consumes: everything git, ls, repository ls and repository default print |
| stderr | What gog did and what went wrong: the Repository:, Linked:, Restored:, Skipped:, Added repository: and Removed repository: lines, and Note:, Warning: and Error: |
So gog ls | xargs and gog apply > log each carry one kind of thing.
| Code | Meaning |
|---|---|
| 0 | It worked |
| 1 | It failed |
| 2 | It was typed wrong: no command, an unknown command or flag, or a missing or extra operand |
A wrong invocation prints the usage that would have been right; a command that
ran and failed does not. gog --help writes help to stdout and reports success.
gog git exits with git's status instead.
gog apply operates on one repository at a time. Repositories may hold
overlapping paths, and the one applied last owns the link.
for repoName in $(gog repository ls | sort -r); do
gog apply --repository ${repoName}
done$HOME must name a directory that exists.
| Variable | Description |
|---|---|
GOG_DEFAULT_REPOSITORY_NAME |
Repository to use when -r is not given. Default: the first repository, which gog repository default prints |
GOG_HOME |
Where gog stores repositories. Default: ${XDG_DATA_HOME}/gog if set, otherwise ${HOME}/.local/share/gog |
Every path a repository holds under root/ is linked. To keep a file out of the
linked tree, keep it out of the repository, or store it beside root/ where
nothing is linked from.
git tag -a v0.1.0 -m 'Release v0.1.0'
git push origin v0.1.0The release workflow builds every platform in
the table above, and publishes nothing until the tests pass.
GoReleaser builds and publishes a tag's archives.
Every push to main republishes the dev release from the archives the workflow
built itself. Both carry a checksums.txt.
| Command | Description |
|---|---|
make build |
Build ./gog |
make test |
Run the tests with the race detector and coverage |
make coverage |
Report coverage |
make lint |
Run golangci-lint |
make fmt |
Rewrite the source with golangci-lint's formatter |
make clean |
Run go clean, and remove dist/ and coverage.txt |
make install, make uninstall |
Copy to /usr/local/bin and remove it. Both use sudo |