git-third-party
Install Reference GitHub

git-third-party

Vendor third-party Git content into your repository as ordinary files. No submodules, no subtree, no out-of-tree state.

Declare what you want in third-party.toml and run git third-party update. Upstream code lands at the paths you choose as plain files โ€” visible to CI, grep, and your IDE. Pin to a tag, track a branch, filter to a subdirectory: one config file and one command handle the rest.

Add an entry
# vendor zlib at master, recursively
$ git third-party add third_party/zlib https://github.com/madler/zlib --follow master
$ git commit -m "vendor zlib"
Refresh on schedule
# pull every entry, stage the diff
$ git third-party update
# CI gate: fail if anything is stale
$ git third-party update --check

Why not submodules or subtree?

Submodules attach a foreign pointer to your repo and demand a second clone. Git subtree collapses upstream history into yours and gives you no clean way to refresh. This extension keeps a small declarative state and treats vendored content as your own files โ€” because once it lands in your tree, it belongs there.

Concern Submodules git subtree git-third-party
Single clone, no extra indirection โœ— โœ“ โœ“
Visible to grep, IDE, build out of the box โœ— โœ“ โœ“
Lockfile pinning resolved upstream commits โœ— history-only โœ“
Pull only a sub-path or filtered file set โœ— โœ— โœ“
Recurse into upstream submodules manual โœ— โœ“
Apply local patches across updates โœ— manual merge โœ“
Single-command refresh of all entries โœ— โœ— โœ“
CI gate that fails on stale vendoring โœ— โœ— โœ“

Compared to git-vendor

brettlangdon/git-vendor is the closest existing tool: a Bash wrapper around git subtree that stores vendoring metadata in squash-merge commit messages and follows a Go-style vendor/<host>/<owner>/<repo> layout. If git subtree already meets your needs and you want a thinner UX on top, it works well.

Where git-third-party differs:

  • Declarative config plus lockfile. State lives in third-party.toml and third-party.lock, not in commit messages. update --check becomes a one-line CI gate.
  • Path filtering. --subdir, --include, --exclude let you vendor only the files you actually use.
  • Submodule inlining. Upstream submodules get recursively materialized; git subtree leaves them as broken pointers.
  • Local patches. Saved tree-patches reapply via 3-way merge on every update.
  • Bindings. Python and Node bindings share the same Go core. git-vendor is shell-only.

Install

Three packages, one binary core. The wheel and the npm package each ship the platform-specific binary; building from source needs Go 1.21 or newer.

CLI

Pick whichever package manager you already have. Each distribution ships the same precompiled binary.

PyPI
# uv tool is the recommended path:
$ uv tool install git-third-party

# pip works too:
$ pip install --user git-third-party
npm
$ npm install -g git-third-party
$ pnpm add -g git-third-party
From source
$ go build -o ~/.local/bin/git-third-party .

Python bindings

The Python wheel bundles a c-shared library, so install needs no Go compiler. Mutating calls accept dry_run= and commit_msg=; every call accepts repo_path=, defaulting to the current directory.

from git_third_party import โ€ฆ
from git_third_party import init, add, list_, version

print(version())
init()
add(dir="vendor/foo", url="https://github.com/x/y", follow="main")
for e in list_():
    print(e.dir, e.commit)

Errors map to GitThirdPartyError and its subclasses: ConfigError, NetworkError, ConflictError, CheckDirtyError.

Node bindings

ECMAScript Modules (ESM) only, ships TypeScript types, requires Node 18 or newer. Same options shape as the Python API; same error hierarchy. The bridge serializes calls process-wide โ€” for parallel work, use worker_threads.

import { โ€ฆ } from 'git-third-party'
import { init, add, list, version } from "git-third-party";

console.log(version());
init();
add({ dir: "vendor/foo", url: "https://github.com/x/y", follow: "main" });
for (const e of list()) {
  console.log(e.dir, e.commit);
}

The npm package distributes platform binaries through optionalDependencies, the same pattern esbuild and swc use. Installs grab a prebuilt binary; no compiler needed.

Quick start

Vendor a directory that tracks an upstream branch, then pull updates and stage the diff. Three commands cover the whole loop.

Vendor a directory

Shell
$ git third-party add third_party/zlib https://github.com/madler/zlib --follow master
$ git commit -m "Vendor zlib"

Pull updates

Shell
# all entries:
$ git third-party update

# one entry:
$ git third-party update third_party/zlib

# dry-run โ€” would anything change?
$ git third-party status

$ git commit -m "Update vendored zlib"

List, rename, remove

Shell
$ git third-party list
$ git third-party rename third_party/zlib vendor/zlib
$ git third-party remove vendor/zlib

What update does

01
Resolve
Look up the configured ref against the upstream remote.
02
Walk
Apply --subdir, --include, --exclude; recurse into upstream submodules.
03
Re-patch
3-way merge any saved tree-patch against the new upstream tree.
04
Splice
Write the result into your working tree at the configured path.
05
Stage
git add the diff. Garbage-collect refs no longer in use.

Tracking a ref

Each entry tracks one of: a moving branch tip, a pinned tag, or a commit SHA.

--follow <branch>
Track the branch tip and re-resolve on every update. Default when neither flag is set (resolved from the remote's HEAD).
--pin <tag-or-sha>
Pin to a tag (resolved once and then cached) or to a commit SHA (used as-is). 40-hex-character strings count as SHAs.

Switch tracking mode with set:

Shell
# pin to a tag:
$ git third-party set vendor/zlib --pin v1.3.1
# back to a moving branch tip:
$ git third-party set vendor/zlib --follow master

Filtering content

Most upstream repos contain test fixtures, CI config, docs, examples, and build systems you don't need. Three flags keep only the files you use.

Shell
$ git third-party add vendor/foo https://example.com/foo.git \
    --subdir src \
    --include '*.c' --include '*.h' \
    --exclude 'tests/'
--subdir DIR
Start from a subdirectory of the upstream repo. Other paths are dropped before include/exclude run.
--include PATTERN · repeatable
Keep only matching paths. Default keeps everything.
--exclude PATTERN · repeatable
Drop matching paths. Always wins over --include.

Patterns follow .gitignore rules with documented deviations: no trailing /**, no . or .. segments. See git third-party add --help for the full specification. Upstream submodules inline recursively unless excluded.

Configuration files

Two files live at the repository root; commit both alongside the vendored content.

third-party.toml · hand-edited
Hand-editable intent. Each [[third_party]] table is one vendored directory. Each add/set/rename/remove rewrites the file, dropping any user comments.
third-party.lock · generated
Records the resolved commit and any saved tree-patch per entry. Sorted by dir for stable diffs. Do not edit by hand.

Example third-party.toml

third-party.toml
# git third-party โ€” vendored content config.

[[third_party]]
dir = "third_party/zlib"
url = "https://github.com/madler/zlib"
follow = "master"

[[third_party]]
dir = "vendor/foo"
url = "https://example.com/foo.git"
pin = "v1.3.1"
subdir = "src"
include = ["*.c", "*.h"]
exclude = ["tests/"]

Corresponding third-party.lock

third-party.lock
# git third-party lockfile โ€” generated; do not edit by hand.
version = 1

[[third_party]]
dir = "third_party/zlib"
commit = "abc1234567890abcdef1234567890abcdef12345"

[[third_party]]
dir = "vendor/foo"
commit = "def4567890abcdef1234567890abcdef12345678"

Command reference

The full CLI surface. The table lists aliases where they exist; every command is also reachable as git third-party <verb>.

Command Aliases Purpose
init โ€” Create an empty third-party.toml. Most users skip this โ€” add creates it.
add DIR URL โ€” Register a new entry and download it.
set DIR โ€ฆ edit Change URL, ref, or filters for an existing entry.
unset DIR FIELDโ€ฆ โ€” Clear subdir, include, or exclude on an entry.
update [DIR] up Re-fetch and stage updates.
status [DIR] st update --dry-run.
list [DIR] ls Show entries and their tracking mode.
info DIR show Print full details for one entry.
rename DIR NEW_DIR mv Move a vendored directory and update the config.
remove DIR rm Unregister a directory and git rm -r its content.
patch save DIR experimental โ€” Record local edits as a tree-patch.
patch diff DIR experimental โ€” Show the saved tree-patch via git diff.
completion SHELL โ€” Print a bash/zsh/fish/powershell completion script.

Common flags

-v / -vv / -q · logging
Debug, trace, or warn-only output. --log-level=trace|debug|info|warn|error sets the level explicitly.
--log-format=text|json
Switch the stderr handler to JSON for machine-readable logs.
--color=auto|always|never
Honors NO_COLOR.
--dry-run
Plan without staging.
-f / --allow-dir-exists
Let add and rename write into a non-empty target.
--profile PATH
Write a CPU profile.
--json
Emit a structured entryResult (or array) on stdout instead of human text. Schema lives in output.go.
--commit MSG
Run git commit -m MSG after the command stages changes.
--check · update / status
Exit non-zero if any entry would change. Useful in CI and pre-commit hooks.

Exit codes

Stable for scripting. Wire status and update --check into CI.

Code Meaning
0 Success.
1 Generic failure.
2 Configuration invalid (TOML parse, validation, lockfile schema mismatch).
3 Network, fetch, or ref-resolution failure.
4 Unresolvable merge conflict during update.
5 --check detected a pending change.
.github/workflows/vendor.yml
- name: Verify vendoring is up to date
  run: git third-party update --check

Settings

Settings resolve through five layers, each overriding the previous: built-in defaults, per-user git config, the repository's [settings] table, environment variables, CLI flags.

  1. Built-in defaults.
  2. Per-user: git config --global third-party.<key>.
  3. Per-repo: a [settings] table in third-party.toml.
  4. Environment variables.
  5. CLI flags.

Environment variables

GIT_THIRD_PARTY_LOG_LEVEL
Same as --log-level: trace, debug, info, warn, error.
GIT_THIRD_PARTY_LOG_FORMAT
Same as --log-format: text or json.
GIT_THIRD_PARTY_COLOR
Same as --color: auto, always, never.
GIT_THIRD_PARTY_EXPERIMENTAL
Comma-separated feature names; same as --experimental.
NO_COLOR · cross-tool
Standard convention; disables ANSI color even when --color=auto.

Shell completions

Generate completion scripts for the major shells.

Shell
# bash
$ source <(git third-party completion bash)

# zsh
$ git third-party completion zsh > "${fpath[1]}/_git third-party"

# fish
$ git third-party completion fish > ~/.config/fish/completions/git-third-party.fish

Editing vendored content

Experimental

Record local modifications as a tree-level patch; update re-applies it on every refresh.

If you need to modify vendored code โ€” a typo fix, a small portability tweak โ€” record it as a tree-level patch. update re-applies the patch via 3-way merge. Conflicts get tagged with a -conflicts suffix in the lockfile, keeping conflicts visible until resolved.

Shell
# enable the experimental feature once
$ git config --global third-party.experimental patch

# record local edits as a tree-patch
$ git third-party patch save vendor/foo

# inspect the saved patch via git diff
$ git third-party patch diff vendor/foo

If update produces conflicts, git-third-party stores the patch with a -conflicts suffix; resolve with git add followed by another patch save. Review the resulting commits carefully โ€” the feature is experimental for a reason.

FAQ

Common questions, short answers.

Yes. Vendored content lives directly in your tree โ€” no .gitmodules, no second clone, no submodule pointer to forget.

git subtree merges upstream history into yours, which makes refreshing painful and obscures provenance. git-third-party keeps a tiny declarative state โ€” intent plus lock โ€” and rewrites the tree on update. Closer to a package manager than a Git operation.

Yes. --subdir starts from a subdirectory; --include and --exclude filter individual paths.

git third-party update re-resolves the ref, fetches the new commit, applies your filters, re-applies any saved local patches, and stages the result. You commit it.

Yes. --pin v1.3.1 or --pin <40-hex-sha>. Tags resolve once and freeze; SHAs go through verbatim.

Yes. Authentication uses your existing Git configuration โ€” anything that works for git fetch works here.

git third-party update --check exits non-zero (code 5) if any entry would change. Wire it into a pre-commit hook or CI job to fail on stale vendoring.

Many monorepo automation systems use one or the other. Both bindings call into the same Go core through a cgo bridge, so behavior matches the CLI exactly.

git on PATH. The bindings also need CPython or Node 18 or newer. Every distribution ships a precompiled binary, so runtime needs no Go compiler.

github.com/khwstolle/git-third-party. Issues and pull requests welcome.