Skip to content
rafiPublic

About

Fast CLI Git manager for multiple repositories grouped by projects, with GitHub/GitLab/Bitbucket support.

Topics

Resources

Stars

52 stars

Watchers

3 watching

Forks

Repository files navigation

gits

Fast CLI Git manager for multiple repositories grouped by projects, with GitHub/GitLab/Bitbucket/Gitea/Forgejo/Gerrit support.

tests Go Reference License: MIT

gits animated overview

What is gits?

gits runs one command across many git repositories at once. Point it at any directory and it works immediately, no config needed:

$ gits status ~/code
    Repo  Branch  Status  Δ±  Upstream⇅  Commit    Age  Message
  + api   main     !  –   ≠1  ↑3         c1abd2ef  now  Add feature
    web   main        –                  4f2ba910  2d   Fix layout
  / tools             –                                 not cloned

○ Showing 3 repos, 1 with changes, 1 error

Group repositories into projects in a config file and address them by name:

gits status acme     # one-line status for every repository in 'acme'
gits pull acme       # pull them all
gits clone acme      # clone the ones that aren't there yet

A project is just a label for a group of repositories. It either lists them by hand or discovers them from GitHub, GitLab, Bitbucket, Gitea, Forgejo, Gerrit or a local directory - cached, so day-to-day commands never wait on the network.

Only requirement is git.

Why gits?

  • One command, many repositories. Clone, fetch, pull, push, check status, or run an arbitrary command across a whole project, in parallel.
  • A config file you never have to write. gits discover ~/code turns the clones you already have into projects, and gits add records one at a time. Both create the file and the project for you.
  • Your repositories, discovered for you. Point a project at a GitHub user, a GitLab group, a Bitbucket workspace, a Gitea or Forgejo owner, a Gerrit prefix, or a directory, and gits finds the repositories. Self-hosted forges work too. Results are cached with a configurable TTL.
  • A status view that fits on a screen. One line per repository, with branch, staged/unstaged/untracked counts, ahead/behind, version and last commit.
  • Scriptable. -o json emits one well-defined document from list, status, and every bulk command, so you can pipe it into jq. Results go to stdout and progress goes to stderr, so piping is always safe.
  • Interactive when you want it. Fuzzy-pick projects, repositories, branches and tags; cdgit jumps your shell straight into a repository.
  • Safe by default. gits push refuses to reach --force or --mirror, and skips branches it should not re-create. gits doctor tells you what is wrong with your setup before it bites.

What makes it different

Most repo wranglers pick a lane: bulk cloner, read-only dashboard, or pull request cannon. gits covers the first two in one binary.

  • Nothing to set up. No manifest, no init, no registering repos one by one. Point it at a directory and go. Bring a config later, if ever.
  • Pipe-friendly by design. -o json on every command that lists or reports, clean stdout, progress on stderr. Your scripts, your CI, and your agents all get a straight answer.
  • Projects that nest. GitHub, GitLab, Bitbucket, and more, grouped into sub-projects and filtered down. Flat tag lists give up long before you do.

Install

Homebrew (macOS and Linux):

brew install rafi/tap/gits

Go (1.27 or newer):

go install github.com/rafi/gits/cmd/gits@latest

Binaries for Linux and macOS (amd64/arm64) are attached to each release.

Requirements

Dependency Needed for
git everything; gits drives your own git binary
fzf interactive selection only (browse, checkout, cd, and prompts when you omit a project)

Run gits doctor at any time to see which of these were found and where.

Quick start

1. Let gits write the config for you. You already have clones on disk, so there is nothing to type up by hand: point discover at them and it writes a project per directory, in one command.

$ gits discover ~/code
acme ~/code/acme 6 repositories
oss ~/code/oss 3 repositories

Added 2 projects to ~/.gits.yaml.

Adding a repository one at a time is gits add, from inside the clone:

cd ~/code/acme/api && gits add acme   # the current repository joins 'acme'
gits add acme ~/work/tools            # or name directories, globs, clone URLs

Both create the config file and the project when they do not exist yet, so there is no init step and no manifest to maintain. Run gits discover ~/code again after cloning more, and only the new arrivals are added.

Either way you end up with ~/.gits.yaml (or $XDG_CONFIG_HOME/gits/config.yaml), which you are free to edit, group and point at a code forge:

---
# ~/.gits.yaml

# Discovered from GitHub.
acme:
  path: ~/code/github
  source:
    type: github
    search: rafi          # user or organization

# Discovered from a local directory, searched recursively.
vim:
  path: ~/code/vim
  desc: Vim plugins

# Listed by hand, with absolute directories.
dotfiles:
  desc: My dotfiles
  repos:
    - dir: ~/.config
      src: git@github.com:rafi/.config.git
    - dir: ~/.config/nvim
      src: git@github.com:rafi/vim-config.git

That file is examples/simple.yaml. For every setting with its default value, see examples/config.yaml.

2. Check that it is sound:

gits doctor

3. Use it:

gits list              # every project
gits list acme         # every repository in project 'acme'
gits clone acme        # clone the ones not cloned yet
gits status acme       # where does everything stand?
gits pull acme         # bring them all up to date

gits list

gits clone

gits pull

4. Or point it at a directory, with no config at all:

gits status ~/code     # every repository under a path
gits status .          # every repository under the current directory

gits status on a directory

Commands

Usage: gits [command] [project] [repo] [flags]

Command What it does
add Add cloned repositories to a project's list
browse Browse branches and tags
cd Get repository path (see cdgit)
checkout Traverse repositories and optionally checkout branch
clone Clone all repositories
completion Generate a shell completion script
discover Find groups of repositories and add them as projects
doctor Report configuration and environment problems
exec Run a command in every repository
fetch Fetch and prune from all remotes
list List project repositories
orphan Find repositories on disk that no project declares
pull Pull repositories
push Push current branch to its upstream
status Show Git repositories short status
sync Synchronize project caches
version Show version

Global flags:

Flag Meaning
-c, --config <path> Use a specific config file
-C, --color <auto|always|never> Control colored output
-v, --verbose Trace what gits did: provider pages, cache hits, git's stderr

Commands that produce a report also take -o/--output. list offers table, wide, tree, name and json; status, doctor and the bulk commands offer table and json.

Every command that addresses repositories also takes -l/--label, which picks them out by label instead of by project. See Labels.

gits list -o tree

gits status -o json piped to jq

Usage

Addressing projects and repositories

Every command takes a project, and optionally one repository inside it:

gits status acme      # the whole project
gits status acme api  # one repository
gits status acme api/ # the sub-project 'api' - note the trailing slash
gits status ~/code    # a path instead of a project name
gits list acme infra  # several projects at once

Omit the project and gits asks you to pick one interactively.

Labels

A project groups repositories by where they live. A label groups them by whatever you say: the three services behind one demo, everything you rebuild after a dependency bump, the repos you actually touch this week. Label them in the config:

acme:
  path: ~/code/acme
  labels: [work]        # every repository under the project carries it
  repos:
    - dir: api
      labels: [demo, backend]
    - dir: web
      labels: [demo]
    - dir: docs

Then address them with -l, on any command that takes a project:

gits status acme -l demo          # only the repositories labeled 'demo'
gits pull acme -l demo,backend    # either label - several labels are a union
gits pull acme -l demo -l backend # the same thing, spelled out
gits exec acme -l demo -- git gc  # one command across the labeled ones
gits list -l demo                 # across every project, no project named

A repository carries its own labels plus its project's, sub-projects included, so web above is reachable as both demo and work, and one label on a project addresses everything in it - including repositories discovered from a forge, which have no config entry of their own to label.

Matching is exact and case-insensitive, like include/exclude: no globs. A label nothing carries is reported rather than silently doing nothing, so a misspelling is visible:

$ gits pull acme -l dmeo
no repository in project "acme" carries label dmeo

gits add --label is the one place a label is written rather than selected by. It labels what it records, and labels a repository the project already lists, which is how an existing one gets labeled:

gits add acme ./api --label demo,backend  # record it, labeled
gits add acme ./docs --label demo         # label one already in the list

Labeling a whole project is a labels: line on the project, since gits add always records repositories. Labels travel in -o json too, already resolved, so jq can group by them without re-reading your config.

Status

gits status prints one line per repository, with branch, change counts, ahead/behind, version and last commit. Narrow it down with:

gits status acme --dirty     # only repositories with uncommitted changes
gits status acme --unsynced  # only repositories ahead of or behind upstream
gits status acme --stat      # add a column of uncommitted line counts

gits status

gits status --dirty

Adding repositories to a project

gits add writes already-cloned repositories into a project's repos: list, creating the project if needed. Each argument is a directory, a glob, or a clone URL:

gits add acme                              # the current directory
gits add acme ./api ../shared/tools        # directories
gits add acme 'backend*'                   # every repository matching a glob
gits add acme git@github.com:acme/api.git  # cloned into the current directory first

This is for hand-listed projects. For one backed by a source, use gits orphan to find repositories on disk that no project declares.

Discovering projects you already have

Already have clones scattered on disk? gits discover writes the config for you. Point it at a path, and every directory holding repositories becomes a project listing exactly those repositories:

$ gits discover ~/code
code ~/code 2 repositories
project1 ~/code/project1 2 repositories
project2 ~/code/project2 2 repositories

Added 3 projects to ~/.gits.yaml.

For a ~/code like this one, with a couple of clones loose between the project directories:

code
├── project1/{repo1,repo2}
├── repo3
├── project2/{repo4,repo5}
└── repo6

it writes:

code:
  path: ~/code
  repos:
    - dir: repo3
    - dir: repo6
project1:
  path: ~/code/project1
  repos:
    - dir: repo1
    - dir: repo2
project2:
  path: ~/code/project2
  repos:
    - dir: repo4
    - dir: repo5

Every repository lands in exactly one project, named after the directory it sits in. Note that code claims its own two clones and none of the other four: projects name their repositories instead of searching their path, so nothing gets counted twice.

gits discover ~/code -n       # report what it would add, write nothing
gits discover .              # the current directory
gits discover ~/code --min 2  # skip directories holding a single repository

Clone more, run it again: only the new arrivals are added, and a name already taken is reported rather than quietly renamed. Repositories nested inside another one are left alone, so a vendored clone never becomes a project of its own. Use gits orphan to hunt those down.

Pushing

gits push pushes each repository's current branch to its upstream:

gits push acme          # push every repository in project 'acme'
gits push acme -n       # git's own --dry-run, per repository
gits push acme --tags   # push tags instead of the current branch

A branch with no upstream, or one whose upstream is gone, is skipped rather than failed. Also supported: --all, --branches, --follow-tags, --atomic and --prune.

--force, --force-with-lease, -u/--set-upstream and --mirror are deliberately unreachable: one mistyped flag across forty remotes is not recoverable the way it is against one.

gits push

Running a command everywhere

gits exec runs one command in every repository. Everything after -- is the command:

gits exec acme -- git gc --quiet     # in every repository of 'acme'
gits exec acme api -- git log -n 1   # in one repository
gits exec acme -- sh -c 'git log -1 --format=%s | tr a-z A-Z'   # pipes need a shell

Commands run directly rather than through a shell, each in its repository, with GITS_PROJECT, GITS_REPO and GITS_REPO_PATH exported.

gits exec

Jumping to a repository

gits cd prints a repository's path; a child process cannot change your shell's directory. Source the provided function from your ~/.bashrc or ~/.zshrc (fish users want contrib/cdgit.fish):

source /path/to/gits/contrib/cdgit.sh

Then:

cdgit acme api   # cd straight there
cdgit            # pick a project and repository interactively

Synchronizing caches

gits sync drops each project's cached repository list and refetches it from its code forge:

$ gits sync
[1/2] acme [github:acme] flushed · ~/code/acme · 42 repositories · 2 new
  + acme/widgets
  + acme/docs
[2/2] infra [gitlab:mygroup] flushed · ~/work/infra · 17 repositories
Synchronized 2 projects, 59 repositories (2 new).

Repositories the refetch found that the dropped cache didn't have are listed under their project, ready for gits clone. The first sync of a project has nothing to compare with, so it lists none.

Name projects (gits sync acme infra) to sync only those. Only forge-backed projects have a cache to refresh.

Checking your configuration

gits doctor reports what is wrong with your configuration and environment, in one place:

$ gits doctor
info    config using ~/.gits.yaml
error   config unknown config key "acme.pth" in ~/.gits.yaml, ignored
error   acme.one no `path:` on the project and no `dir:` on the repository, so there is nowhere for it to live
warning vim `path: ~/code/vim` does not exist yet; `gits clone` will create it
info    git git version 2.55.0 (/opt/homebrew/bin/git)
info    finder 0.74.3 (Homebrew) (/opt/homebrew/bin/fzf)
info    cache directory ~/.cache/gits
warning cache.github-acme written by gits v0.10, will be refetched (cached 9d ago)
info    cache.github-rafi cached 1d ago, valid for 168h0m0s

gits doctor

Shell completion

gits completion <bash|zsh|fish|powershell> prints a completion script; the sub-command's help says where your shell expects it.

Tab completes project names, repositories, branches, and flag values. Names come from the cache, so completion never waits on the network.

Configuration

The configuration file lives at ~/.gits.yaml or $XDG_CONFIG_HOME/gits/config.yaml. JSON and TOML are supported too, so .json, .yml and .toml extensions work at either location. Use gits -c <path> to point at any other file.

See examples/config.yaml for a fully documented config file listing every setting with its default value, or examples/simple.yaml for a minimal one.

Warning

Each project in the config file can have either a source or a repos key, not both.

The structure of the config file is as follows:

---
# ~/.gits.yaml

# Project definition
projectname:          # Project name
  desc: My projects   # Optional
  path: ~/code/github # Optional if 'repos' are specified and have absolute paths.
  source:             # Required if no 'repos' defined, default: filesystem
    type: github      # Required: github|gitlab|bitbucket|gitea|forgejo|gerrit|filesystem
    search: rafi      # Required search query (owner, group id, name prefix)
    url: https://...  # Self-hosted forge web address, see 'Self-hosted forges'
  repos:              # Required if no 'source' defined
    - dir: foo        # Optional, default: repository name
      src: git@...    # Optional, default: repository remote URL
      labels: [...]   # Optional, see 'Labels'
    - ...
  labels: [...]       # Optional labels every repository under it carries
  include: [...]      # Optional allowlist of repositories
  exclude: [...]      # Optional denylist of repositories
  clone: true         # Optional, set false to skip during `gits clone`
  subprojects: [...]  # Optional nested projects

anotherproject:
  ...

Note

When dir is omitted, the local directory is derived from the last path segment of src, with only a trailing .git suffix stripped. Earlier versions stripped everything after the last dot, so a dotted repository name without a .git suffix (e.g. rafi.github.io) previously mapped to rafi.github and now maps to rafi.github.io. If you cloned such a repo with an older version, rename the directory (or set dir: explicitly) to match.

Filtering repositories

include and exclude trim what a project contributes, which is handy when the source isn't yours to curate. Both match a repository by name, namespace, or namespace/name - exact strings, no globs:

work:
  path: ~/code/work
  source:
    type: gitlab
    search: "12345678"
  exclude:
    - acme/legacy-api   # namespace/name
    - deprecated-tool   # name
  # include:            # when non-empty, only what it names is kept
  #   - acme/api
  #   - acme/web

exclude always wins. A non-empty include is a guest list: anything unnamed stays home. Sub-projects get filtered too, each by its own lists.

Sub-projects

Real orgs don't keep their repositories in one tidy pile. GitLab groups nest as deep as you like, and sub-projects are how gits follows them down. Point a project at a GitLab group and the whole tree comes back with its shape intact - no flattening, no listing each subgroup by hand. Gerrit project names nest the same way along their / segments, below the search prefix: searching openstack/ puts openstack/nova at the top as nova, and openstack/infra/zuul under the sub-project infra.

You can also build that tree yourself. Nest projects under subprojects, as a list, so each carries its own name:

org:
  path: ~/code/org
  repos:
    - dir: platform
  subprojects:
    - name: frontend        # path becomes ~/code/org/frontend
      desc: Web clients
      repos:
        - dir: web
          src: git@github.com:myorg/web.git
    - name: backend
      path: ~/code/services # an explicit path wins over the default
      source:               # discovered on its own, like a top-level project
        type: github
        search: myorg-backend

A sub-project inherits what it doesn't declare: a path of <parent path>/<name>, and its parent's source. No parent path means nothing to inherit, so use absolute dir entries or give it a path. A project with only subprojects is fine - it's a folder, not a freeloader.

An inherited source only says where repositories come from, so a provider-backed one you haven't cloned reads as remote-only instead of an error. It won't go discover anything twice. Want a sub-project fetched on its own? Give it its own source, or just list repos.

Note

A project with a path: and no repos: is searched recursively, and that search descends into its sub-projects' directories too. If a sub-project also lists those repositories, they appear twice. Give such a parent an explicit repos: list, or point the sub-projects at directories outside the parent's path.

A trailing slash is how you say "sub-project, not repository":

gits status org frontend/   # the sub-project 'frontend'
gits status org web         # the repository 'web'

Settings

Built-in behavior is configured under the reserved settings: key:

settings:
  cacheTTL: 168h   # How long provider caches stay valid. Go duration
                   # syntax (e.g. "24h", "30m"). Default: 168h (7 days).
                   # Invalid or empty values fall back to the default.
  workerCount: 8   # Concurrent git workers. Default: max(NumCPU, 2).
  verbose: false   # Trace what gits did on the way to an answer - provider
                   # pages, cache hits, git's stderr - to stderr. Same as -v.
  includeArchived: false  # Include archived repositories when listing
                          # from providers. Gerrit's read-only projects
                          # count as archived. Default: false. Bitbucket
                          # Cloud has no archived flag, so the setting does
                          # not apply there.
  providerTimeout: 5m     # HTTP timeout for provider API calls. Go
                          # duration syntax. Default: 5m.
  gitTimeout: 5m          # Timeout for network git operations
                          # (clone/fetch/pull). Go duration syntax.
                          # Default: 5m.
  finder:                 # Interactive selector overrides. gits shells out
                          # to fzf; override to use a drop-in replacement or
                          # tune the invocation.
    binary: fzf           #   Executable to run. Must be on PATH.
    # args: [...]         #   Replaces the built-in default options wholesale.
    # extra: [...]        #   Appended after args, adding options.

Provider caches are written to $XDG_CACHE_HOME/gits (~/.cache/gits by default); gits doctor prints the directory in use.

Provider tokens

GitHub, GitLab and Bitbucket need an API token. Gitea, Forgejo and Gerrit discover anonymously without one, which lists public repositories; give them a token to see private ones too. Configure it per provider under settings:, either verbatim or as a command that prints it:

settings:
  github:
    tokenCommand: pass tokens/github   # `token-cmd` is also accepted
  gitlab:
    tokenCommand: op read op://private/gitlab/token
  bitbucket:
    tokenCommand: pass tokens/bitbucket
  forgejo:
    tokenCommand: pass tokens/codeberg

For each provider the first of these wins:

  1. token - used as-is. Keep in mind your config file is plain text.
  2. tokenCommand (alias token-cmd) - run through the shell, so pipes and quoting work; the first non-empty output line is the token. It runs at most once per command per gits invocation, so a passphrase prompt appears once even with several projects on the same provider. A failing command is an error - there is no silent fallback.
  3. Environment: GITHUB_TOKEN (or HOMEBREW_GITHUB_API_TOKEN), GITLAB_TOKEN, BITBUCKET_TOKEN. Only for github.com, gitlab.com and Bitbucket Cloud: a source with a url: never reads the environment, so an ambient GITHUB_TOKEN is never sent to another host. Gitea, Forgejo and Gerrit have no environment variables.

A single project can authenticate as somebody else by putting the same two keys on its source:, which is how you keep a work account and a personal one in one config file:

personal:
  path: ~/code/me
  source:
    type: github
    search: rafi
    tokenCommand: pass tokens/github-personal

work:
  path: ~/code/work
  source:
    type: github
    search: acme
    tokenCommand: pass tokens/github-work   # `token:` and `token-cmd:` too

Bitbucket takes an Atlassian API token in either of two forms:

settings:
  bitbucket:
    token: me@example.com:my-api-token   # Atlassian account email and token
    # token: my-api-token                # the token alone

Give it the read:repository:bitbucket and read:workspace:bitbucket scopes, which is all discovery reads. A legacy username:app-password still works wherever Bitbucket still honors it, but Atlassian has deprecated app passwords in favor of API tokens, so prefer a token for anything new.

Gerrit's token is an HTTP password (an auth token on Gerrit 3.13 and later), and it goes with a username. The first of these wins: username: on the source, the user in its url: (https://rafi@review.example.com), then settings.gerrit.username:

settings:
  gerrit:
    username: rafi
    tokenCommand: pass tokens/opendev

The username also goes into SSH clone URLs, ssh://rafi@host:29418/name, at the host and port the server advertises. Gerrit only reveals its SSH address to a signed-in caller, so without a token repositories clone over HTTPS instead.

Cached projects don't need a token until the cache expires or gits sync refreshes it.

Self-hosted forges

A url: on the source points it at your own host. Use the address you would open in a browser - gits adds the API path itself:

work:
  path: ~/code/work
  source:
    type: github            # GitHub Enterprise Server
    url: https://github.corp.example
    search: platform
    tokenCommand: pass tokens/github-corp

mine:
  path: ~/code/codeberg
  source:
    type: forgejo           # or gitea
    url: https://codeberg.org
    search: rafi            # a user or an organization

opendev:
  path: ~/code/opendev
  source:
    type: gerrit
    url: https://review.opendev.org
    search: openstack/      # a project name prefix
  • gitea, forgejo and gerrit require url:. github and gitlab take it for Enterprise and self-managed hosts; https://github.com and https://gitlab.com mean the same as leaving it out. bitbucket does not take it: only Bitbucket Cloud is supported.
  • The url may name a user, https://rafi@host, but never a password - use token or tokenCommand. The user is kept out of the cache and -o json.
  • Two projects with the same search on different hosts are cached apart.
  • A custom host never gets a token from the environment, so set one on the source or under settings:.

Rate limits

gits paces its requests to each host so a big sync does not trip a forge's abuse limits, and tries a request up to three times when a host answers 429 Too Many Requests or 503 Service Unavailable, waiting as long as its Retry-After asks, a minute at most. Set the pace per provider in requests per second:

settings:
  gerrit:
    rateLimit: 5   # Default: 10. 0 turns pacing off.

Projects on one host share one pace, and two providers pointed at the same host share the slower of their two rates.

Themes

gits ships 31 color themes. The default, ansi, uses your terminal's own 16 colors. Pick another in config, per run with --theme, or with GITS_THEME:

settings:
  theme:
    name: catppuccin-mocha
    # or follow the terminal background:
    # name: { dark: catppuccin-mocha, light: catppuccin-latte }
    styles:                       # override individual roles
      statusStaged: { fg: green, bold: true }
      statusBehind: { faint: false }

gits theme list shows every theme; gits theme show <name> renders a sample. To browse them live:

gits theme list | fzf --preview 'gits -C always theme show {}'

Colors adapt to the terminal: truecolor terminals get the exact palette, 256-color and 16-color terminals get hand-picked fallbacks. Override colors are a palette name (fg muted subtle red green yellow blue magenta cyan orange accent), none, #rrggbb, or an ANSI index. See examples/config.yaml for every role.

Config examples

Each project in the following example is defined differently:

---
# ~/.gits.yaml

# Github source.
mygithub:
  source:
    type: github
    search: rafi

# GitLab source, note that `path` and `desc` are optional.
work:
  path: ~/code/work
  desc: My work GitLab projects
  source:
    type: gitlab
    search: "12345678"  # Make sure GitLab group id is quoted

# Bitbucket source.
mybitbucket:
  source:
    type: bitbucket
    search: rafi

# Forgejo source on Codeberg. Gitea works the same with `type: gitea`.
codeberg:
  source:
    type: forgejo
    url: https://codeberg.org
    search: forgejo-contrib

# Gerrit source, every project whose name starts with the prefix.
opendev:
  path: ~/code/opendev
  source:
    type: gerrit
    url: https://review.opendev.org
    search: openstack/oslo

# Filesystem source that will be searched recursively.
explore:
  desc: Exploring projects
  path: ~/code/explore
  source:
    type: filesystem

# Relative directory name and implicit remote source URL.
acme:
  path: ~/code/acme
  desc: Acme is a really cool app.
  repos:
  - dir: admin
  - dir: ant-design-pro
  - dir: api
  - dir: infra
  - dir: ios
  - dir: react-native
  - dir: web
  - dir: webapp

# Relative directory name and explicit remote source URL.
myapp:
  path: ~/code/myapp
  repos:
  - dir: api                             # Can be absolute or relative to path
    src: https://github.com/app/api.git  # Optional remote clone URL
  - dir: ios
    src: https://github.com/app/ios.git
  - dir: android
    src: https://github.com/app/android.git

# Absolute directories and explicit remote source URL. (No project path)
rafi:
  desc: My dotfiles
  repos:
  - dir: ~/.config
    src: git@github.com:rafi/.config.git
  - dir: ~/.config/nvim
    src: git@github.com:rafi/vim-config.git

Development

gits is written in Go and needs no code generation or vendored tools to build:

git clone https://github.com/rafi/gits.git && cd gits
go build ./cmd/gits
go test ./...

A justfile wraps the common tasks (just required):

just build     # build the binary into bin/release
just test      # run the test suite
just lint      # golangci-lint
just release   # cross-compile for linux/darwin, amd64/arm64

Contributions are welcome - please open an issue or pull request on GitHub.

License

MIT © 2018-2026 Rafael Bodill. See LICENSE.txt.

About

Fast CLI Git manager for multiple repositories grouped by projects, with GitHub/GitLab/Bitbucket support.

Topics

Resources

Stars

52 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages