A compact CLI tool for creating full backups of Git repositories, including the .git directory and all ignored/untracked files.
DevBack creates structured repository snapshots with automatic rotation, flexible naming styles, and legacy directory migration. Designed for sysadmins and developers who need reliable Git repository backups.
- Full backup: Includes
.gitdirectory and all ignored/untracked files - Structured snapshots: Automatic organization by date and time
- Incremental snapshots: Unchanged files are hardlinked to the previous snapshot instead of copied
- Automatic rotation: Manage backup size and count
- Flexible naming: Multiple directory naming styles
- Auto-migration: Automatic migration from legacy directory structures
- Parallel copy: Efficient handling of large repositories
- Global init and hook installation:
devback initanddevback setupcommands - Status and diagnostics:
devback statuscommand - Git worktree support: Correct handling of shared hooks
- TOML configuration: Single config file for all commands
- Standardized exit codes: For automation and monitoring
<backup_dir>/<repo_key>/<YYYY-MM-DD>/<HHMMSS-NNNNNNNNN>/
├── .partial (created on start)
├── .done (created after successful completion)
└── .git/ (full copy of the Git repository)
└── ... (all ignored/untracked files)
The time directory uses HHMMSS-NNNNNNNNN format, where the suffix is nanoseconds to guarantee uniqueness across repeated runs within the same second.
Snapshot directories are reserved atomically via exclusive directory creation and a .reserve marker file. This prevents collisions during parallel runs. The marker is removed after a successful backup.
By default (link_dedup = true), each file is compared against the file at
the same path in the previous completed snapshot. When the file is unchanged
(same size, mtime at second precision, and permission bits), the new snapshot
gets a hardlink to the previous snapshot's file instead of a fresh copy. Heavy
immutable files (.git/objects packs, build artifacts, models) occupy disk
space once, no matter how many snapshots contain them.
Properties:
- links are created only between snapshots — a snapshot never links to files of the source repository;
- any link failure (different filesystem, no hardlink support) silently falls back to a regular copy; deduplication can never break a backup;
- snapshots remain plain browsable directories; restore is a regular
cp -a(copying dereferences hardlinks); - rotation works as before: deleting a snapshot drops its links, and disk space is freed when the last link disappears;
- size-based rotation and
devback status --scan-backupsreport real disk usage (each inode is counted once); - the backup log reports
Linked N file(s) from previous snapshot.
The first backup after enabling the feature is always full (older snapshots carry copy-time mtimes); deduplication kicks in from the second cycle.
Important: never edit files inside backups in place — a file shared by several snapshots changes in all of them at once. Backups are meant to be read-only and restored by copying.
brew tap arumata/tap
brew install --cask devbackgo install github.com/arumata/devback/cmd/app@latestRequires Go 1.24+. The binary will be installed to $GOPATH/bin (or $HOME/go/bin by default).
Pre-built binaries for Linux and macOS (amd64/arm64) are available on the Releases page.
Requirements: Go 1.24+, Git.
git clone https://github.com/arumata/devback.git
cd devback
# Build
VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo dev)
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
CGO_ENABLED=0 go build \
-trimpath \
-ldflags "-s -w -X main.version=$VERSION -X main.commit=$COMMIT -X main.date=$DATE" \
-o devback ./cmd/app
# Install
sudo install devback /usr/local/bin/
# or
install devback ~/.local/bin/devback init
cd ~/projects/my-app
devback setup --slug "company/my-app"
devback statusAfter setup, hooks automatically trigger a backup after git commit, git merge,
and git rebase/git commit --amend (via post-rewrite).
To run hook commands manually:
devback hook post-commit
devback hook post-merge
devback hook post-rewrite rebaseWhen a hook decides not to back up an enrolled repository, the reason is
written to the file log (~/.local/state/devback/logs/devback-YYYY-MM-DD.log)
as a skip hook record with hook, repo, and reason attributes (info
level, independent of logging.level); nothing is printed to the terminal
during normal git operations. Reason codes:
| Reason | Meaning |
|---|---|
SKIP_REBASE_IN_PROGRESS |
A rebase is running (.git/rebase-merge/rebase-apply); post-rewrite backs up when it finishes |
SKIP_REBASE_REFLOG |
post-commit fired inside a rebase (GIT_REFLOG_ACTION) |
SKIP_REBASE_STATE_UNREADABLE |
The rebase state in .git could not be read |
SKIP_DEBOUNCE |
A backup of this same HEAD was already taken within the last 60 seconds (e.g. by post-commit during git commit --amend); a new HEAD within the window still gets backed up |
SKIP_LOCK_BUSY |
A backup of this repository is already running |
SKIP_INTERRUPTED |
The backup was interrupted by a signal |
Configuration problems in an enrolled repository (SKIP_CONFIG_ERROR,
SKIP_NO_CONFIG, SKIP_NO_BASEDIR, SKIP_NO_HOMEDIR) are printed as
warnings to stderr on every commit — the file log is not available at that
point. In repositories without backup.enabled, hooks stay silent.
To run a backup manually:
devbackGlobal initialization: creates ~/.config/devback/config.toml, installs hook templates
to ~/.local/share/devback/templates/hooks/, and (by default) sets git init.templateDir
to ~/.local/share/devback/templates/. If init.templateDir is already set to a different
path, the command will fail and suggest using --force (overwrite) or --no-gitconfig (skip).
Flags:
--backup-dir PATH- base backup directory (required when creating config or with--force; suggested:~/.local/share/devback/backups)--force- overwrite existing config (with aconfig.toml.bak.<timestamp>backup) and foreigninit.templateDirvalue--no-gitconfig- don't modify~/.gitconfig--templates-only- only update hook templates (skipconfig.tomlandgitconfig)--dry-run- show planned changes without writing to disk
Repository setup: copies hooks from global templates and sets
git config backup.enabled=true (unless --no-hooks is used). Optionally sets backup.slug. Requires a prior
devback init (hook templates must exist).
Flags:
--slug NAME- setbackup.slug(for worktree, writes toconfig.worktree)--force- overwrite existing hooks (main repository only)--no-hooks- skip hook installation and hook-related git config changes--dry-run- show planned changes without writing to disk
Notes:
--forceis not available for worktrees; hooks are installed only from the main repository- if hooks are not installed in the main repository,
devback setupin a worktree will fail (or only warn with--no-hooks) --no-hooksdoes not enablebackup.enabledand does not modify existing hooks- if hook files already exist and
--forceis not used, DevBack merges them by creating a backup likepost-commit.devback.orig(or with numeric suffix) and installing a wrapper that runs the original hook first anddevback hook <name>second. The original hook exit code takes priority.
Shows global configuration and current repository status. Outside a repository, only the global section is displayed.
Flags:
--no-repo- show only global configuration--scan-backups- scan backups to count snapshots/size (may be slow)--dry-run- accepted for CLI consistency, does not change behavior
Manual backup using backup.base_dir from config.toml.
Positional arguments are not supported.
Flags:
-v,--verbose- verbose output--dry-run- full simulation without filesystem changes--print-repo-key- print the repository key and exit--test-locks- test the locking mechanism and exit (does not requirebackup.base_dir)
Location: ~/.config/devback/config.toml. Created by devback init
and used by init, setup, status commands (paths support ~ and $HOME).
If the file is missing, defaults are used and status will show (not found) for the config.
Configuration example:
[backup]
base_dir = "~/.local/share/devback/backups"
keep_count = 30
keep_days = 90
max_total_gb = 10
size_margin_mb = 0
no_size = true
link_dedup = true
[notifications]
enabled = true
sound = "default"
[logging]
dir = "~/.local/state/devback/logs"
level = "info"
[repo_key]
style = "auto"
auto_remote_merge = false
remote_hash_len = 8| Field | Type | Default | Description |
|---|---|---|---|
base_dir |
string | "" (empty) |
Base directory for snapshots. Required. Set via devback init --backup-dir. Supports path expansion. Suggested: ~/.local/share/devback/backups |
keep_count |
int | 30 |
Maximum number of snapshots to keep per repository. Oldest snapshots are removed first. |
keep_days |
int | 90 |
Maximum snapshot age in days. Snapshots older than this are removed during rotation. |
max_total_gb |
int | 10 |
Maximum total size (GB) of all snapshots per repository. Ignored when no_size = true. |
size_margin_mb |
int | 0 |
Margin in MB added to max_total_gb before triggering size-based rotation. |
no_size |
bool | true |
Disable size-based rotation. When true, max_total_gb and size_margin_mb are ignored. |
link_dedup |
bool | true |
Hardlink unchanged files to the previous snapshot instead of copying. See Incremental Snapshots. |
Rotation settings from the [backup] section can be overridden per repository
via git config. Git config variable names cannot contain underscores, so the
keys use camelCase:
| git config | config.toml |
|---|---|
backup.keepCount |
keep_count |
backup.keepDays |
keep_days |
backup.maxTotalGb |
max_total_gb |
backup.sizeMarginMb |
size_margin_mb |
backup.noSize |
no_size |
backup.linkDedup |
link_dedup |
Example — keep only 8 snapshots for a repository with large snapshots:
git config --local backup.keepCount 8Resolution order matches backup.enabled: worktree config → local → global
git config, applied on top of the global config.toml. Check the effective
values with devback status (each value is marked repo override or
global) or devback -v --dry-run. An invalid value (not an integer, a
negative number, or a non-boolean for backup.noSize) fails the backup with
an explicit error instead of silently falling back to the global value. The
same applies to backup.linkDedup.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable desktop notifications after backup completion. |
sound |
string | "default" |
Notification sound name. Use "default" for the system default sound. Platform-dependent. |
| Field | Type | Default | Description |
|---|---|---|---|
dir |
string | "~/.local/state/devback/logs" |
Directory for log files. Supports path expansion. Created automatically if it does not exist. |
level |
string | "info" |
Minimum log level. One of: debug, info, warn, error. |
| Field | Type | Default | Description |
|---|---|---|---|
style |
string | "auto" |
Repository key naming style. See Naming Styles for details. |
auto_remote_merge |
bool | false |
Merge snapshots from clones with the same remote.origin.url into a single directory. |
remote_hash_len |
int | 8 |
Hash suffix length appended to the directory name in remote-hierarchy style. |
Automatic style selection in priority order:
backup.slug+ repository basename- an existing completed
name+hashsnapshot chain (ifrepo_key.auto_remote_merge=false) remote-hierarchy+ hash (ifrepo_key.auto_remote_merge=false)name+hash(fallback)
backup.slug can be set via devback setup --slug.
If origin is added after snapshots have been created, the existing name+hash
chain keeps priority and the backup path remains unchanged. Setting
auto_remote_merge=true explicitly allows switching to the shared remote key.
Format: basename--8char_hash
Example: app--4f7a2d9c
Format: host/owner/repo
Example: github.com/acme/app
Uses git config backup.slug + repository basename
Example: work/acme/prod/app
backup.slug can be set via devback setup --slug.
DevBack stores per-repository settings in git config. These are managed by devback setup and read by hooks at runtime.
| Setting | Scope | Set by | Description |
|---|---|---|---|
backup.enabled |
local / worktree | devback setup |
Enable or disable backup for this repository. |
backup.slug |
local / worktree | devback setup --slug |
Custom prefix for the repository key (e.g., company/team). |
Lookup priority for backup.enabled: worktree config → local config → global config.
For worktrees, backup.slug is written to per-worktree config (extensions.worktreeConfig is enabled automatically).
Accepted boolean values: 1, true, yes, on (case-insensitive) are treated as true; everything else is treated as false.
| Variable | Description |
|---|---|
LINK_DEDUP |
0/1/true/false/... — overrides link_dedup from config.toml for a single run. Invalid values are ignored with a warning. |
NO_COLOR |
When set (any value), disables colored terminal output. Follows the no-color convention. |
TERM=dumb |
Disables colored terminal output. |
GIT_REFLOG_ACTION |
Used internally by hooks. When it contains rebase, the post-commit hook is skipped to avoid duplicate backups (the post-rewrite hook handles rebase instead). |
All path values in config.toml (base_dir, dir) support the following expansions:
| Pattern | Expansion |
|---|---|
~ or ~/path |
User's home directory |
$HOME or $HOME/path |
User's home directory |
${HOME} or ${HOME}/path |
User's home directory |
Example: base_dir = "~/backups" expands to /home/user/backups.
DevBack installs only three hooks: post-commit, post-merge, post-rewrite.
Templates are located in ~/.local/share/devback/templates/hooks/ and copied to .git/hooks/
by devback setup. The wrappers are minimal: they use the path captured during devback init
with an automatic fallback to PATH lookup:
#!/bin/sh
DEVBACK="__DEVBACK_BIN__"
[ -x "$DEVBACK" ] || DEVBACK="$(command -v devback 2>/dev/null)"
[ -x "$DEVBACK" ] || { echo "SKIP: devback not found" >&2; exit 0; }
exec "$DEVBACK" hook post-commit "$@"All hook logic is implemented in Go for cross-platform compatibility.
- Hooks always exit with code 0 and never block git operations
- If
backup.enabled=falsein git config, the backup is skipped - If
config.tomlis missing orbackup.base_diris empty, the backup is skipped - If
git ls-filesfails, the backup ends with a critical error - If the configured DevBack binary path is not executable, the hook script logs a skip message to
stderrand exits with code 0
For manual testing, use devback hook <name>.
Create a .devbackignore file in the repository root to exclude files from backup:
# Exclude temporary files
*.tmp
*.temp
# Exclude logs
*.log
# Exclude directories
node_modules/
dist/
build/
# Exclude specific files
.env.local
config.local.json
By default, devback init installs a .devbackignore template at
~/.local/share/devback/repo-templates/devbackignore. devback setup creates
.devbackignore in the repository root only if the file is missing and the template exists.
The tool automatically manages snapshot size and count:
- By age: Removes snapshots older than
backup.keep_daysdays - By count: Keeps no more than
backup.keep_countsnapshots - By size: Removes old snapshots when
backup.max_total_gbis exceeded
Dry-run is available via --dry-run and simulates the entire process including rotation.
- File locking to prevent conflicts
.gitdirectory existence verification- Proper copy error handling
- Full operation logging
- Parallel file copying (worker count = CPU * 2)
- Hardlink deduplication: unchanged files are linked, not copied
- Efficient handling of large repositories
- Minimal memory usage
- Optimized rotation algorithms
stderrfor logs (info/warn/error)stdoutfor useful output only (e.g.,devback statusor--print-repo-key)- Verbose mode with detailed process information
DevBack uses standardized exit codes for integration with automated systems (for all CLI commands):
| Exit Code | Name | Description |
|---|---|---|
| 0 | ExitSuccess |
Successful completion |
| 1 | ExitCriticalError |
Any critical error (e.g., .git directory not found) |
| 2 | ExitUsageError |
Command-line argument error |
| 76 | ExitLockBusy |
Could not acquire lock (another process is running) |
| 130 | ExitInterrupted |
Process interrupted by signal |
- Any critical error terminates the process with exit code 1
- Recommended for mission-critical repositories and automation
- Operating systems: Linux, macOS
- Git versions: Compatible with Git 1.8+
- File systems: Supports all major file systems
# Daily backup at 02:00
0 2 * * * /usr/local/bin/devback >> /var/log/backup.log 2>&1[Unit]
Description=Git Repository Backup Service
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/devback -v
User=backup
Group=backup
[Install]
WantedBy=multi-user.target-
"not a git repository" error
- Make sure you are in the root of a Git repository
- Check that the
.gitdirectory exists
-
Permission issues
- Check write permissions on the backup directory
- Ensure read access to the repository
-
Insufficient disk space
- Decrease
backup.max_total_gb - Decrease
backup.keep_daysfor more frequent rotation
- Decrease
# Verbose output for diagnostics
devback -v
# Print repository key
devback --print-repo-key
# Dry-run backup
devback --dry-run -v
# Check exit code for automation
devback; echo "Exit code: $?"# Monitoring script example
#!/bin/bash
devback
case $? in
0) echo "SUCCESS: Backup completed" ;;
76) echo "INFO: Another backup running, skipped" ;;
1) echo "ERROR: Critical backup failure" ; exit 1 ;;
*) echo "UNKNOWN: Unexpected exit code $?" ;;
esacDevBack is built on clean architecture principles with clear layer separation:
- cmd/app: Entry point and initialization
- internal/usecase: Application business logic
- internal/adapters: External dependency adapters
All operations support context, structured logging, and standardized exit codes.
git clone https://github.com/arumata/devback.git
cd devback
# Tests
go test ./... -count=1
# Linter (install: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest)
golangci-lint run
# Formatting (install: go install mvdan.cc/gofumpt@latest)
gofumpt -w .This project is distributed under the license specified in the LICENSE file.
For support or to report issues, please create an issue in the project repository.