Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f2b6e46
feat: implement CLI, extend VendorWorktree, and refactor gitattribute…
jdc-pub Jun 9, 2026
133aa23
test: add integration tests for prepare_merge and track_vendor
jdc-pub Jun 9, 2026
4e50374
fix: correct several CLI correctness bugs found in adversarial review
jdc-pub Jun 12, 2026
5136a64
feat: make name optional in add, defaulting to URL leaf
jdc-pub Jun 12, 2026
03d8deb
feat: friendlier CLI UX
jdc-pub Jun 12, 2026
d4e7285
feat: add apply subcommand
jdc-pub Jun 13, 2026
9e1afb5
fix: read upstream OID from refmap to avoid HEAD-symref misresolution
jdc-pub Jun 13, 2026
4d0b229
feat: normalize --prefix and stage-only add
jdc-pub Jun 13, 2026
43ff0d6
fix: add missing TLS and `reqwest` features
jdc-pub Jun 13, 2026
5d8ab0b
fix: stage rewritten `.gitvendors` on `remove`
jdc-pub Jun 13, 2026
32a39d8
fix: track vendor paths when `add`/`update` conflicts
jdc-pub Jun 13, 2026
7a6f180
refactor: clarify that `staged_attrs_blob` only reads the index
jdc-pub Jun 13, 2026
245777b
refactor: simplify `check_attr_pattern` special-character test
jdc-pub Jun 13, 2026
6215e57
refactor: move command logic to binary executor with injectable IO
jdc-pub Jun 13, 2026
b4fb40d
refactor: return blob OID from `track_vendor` and `untrack_vendor`
jdc-pub Jun 13, 2026
329f141
fix: force `refs/vendor/<name>` to a direct ref after fetch
jdc-pub Jul 3, 2026
f895e2b
fix: peel fetched OID to a commit unconditionally
jdc-pub Jul 3, 2026
9ce6bb3
fix: propagate `vendor_paths` errors in `update`/`apply`
jdc-pub Jul 3, 2026
a75768a
fix: make advance_head a compare-and-swap on HEAD
jdc-pub Jul 3, 2026
1784c17
fix: untrack .gitattributes on `remove --keep-files`
jdc-pub Jul 3, 2026
a796f3c
fix: make `remove` work on an unborn HEAD
jdc-pub Jul 3, 2026
cdb481c
fix: validate trackable paths before checkout mutates state
jdc-pub Jul 3, 2026
1b4bd84
fix: escape glob metacharacters when writing .gitattributes patterns
jdc-pub Jul 3, 2026
a8d4d84
fix: stop checkout_vendor from clobbering staged index entries
jdc-pub Jul 3, 2026
867038d
fix: guard against silently resolving a .gitattributes merge conflict
jdc-pub Jul 3, 2026
8d9e072
refactor: move exe.rs unit tests into exe_tests.rs
jdc-pub Jul 3, 2026
1b5fd0c
refactor: fold `apply` into `update --no-fetch`, stop auto-committing
jdc-pub Jul 3, 2026
75f086c
refactor: extract shared merge/entry-resolution helpers in exe.rs
jdc-pub Jul 3, 2026
aa96571
fix: prune empty ancestor directories after remove
jdc-pub Jul 3, 2026
216c2af
test: record proptest regression seed for vendor_tip
jdc-pub Jul 3, 2026
bc20425
test: drop spurious proptest regression seed
jdc-pub Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,269 changes: 1,255 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ missing_docs = "warn"
[workspace.dependencies]
clap = { version = "4.5.60", features = ["derive"] }
clap_mangen = "0.2.31"
gix = { version = "0.83", features = ["merge", "tree-editor", "blocking-network-client", "worktree-mutation"] }
gix = { version = "0.83", features = ["attributes", "merge", "tree-editor", "blocking-network-client", "worktree-mutation", "blocking-http-transport-reqwest-rust-tls"] }
gix-glob = "0.26.0"
gix-quote = "0.7"
tempfile = "3"
thiserror = "2"

Expand Down
1 change: 1 addition & 0 deletions crates/git-vendor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ git-set-attr = { version = "0.1.2", path = "../git-set-attr" }
tempfile.workspace = true
thiserror.workspace = true
gix-glob.workspace = true
gix-quote.workspace = true

[dev-dependencies]
proptest = "1.5.0"
Expand Down
44 changes: 44 additions & 0 deletions crates/git-vendor/src/attr_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use rstest::rstest;

use super::{check_attr_pattern, split_attr_line};

#[rstest]
#[case(b"vendor/a.txt")]
#[case(b"src/lib.rs")]
#[case(b"deep/nested/path.txt")]
fn check_plain_path_ok(#[case] path: &[u8]) {
assert!(check_attr_pattern(path).is_ok());
}

#[rstest]
#[case(b"vendor/a b.txt")]
#[case(b"#readme")]
#[case(b"say \"hi\"")]
#[case(b"a\tb")]
#[case(b"a\x01b")]
#[case(b"a\x7fb")]
fn check_path_with_special_chars_errors(#[case] path: &[u8]) {
assert!(check_attr_pattern(path).is_err());
}

#[rstest]
#[case(b"vendor/a.txt vendor=mylib", b"vendor/a.txt", b"vendor=mylib")]
#[case(b"vendor/a.txt\tvendor=mylib", b"vendor/a.txt", b"vendor=mylib")]
#[case(b"\"vendor/a b.txt\" vendor=mylib", b"vendor/a b.txt", b"vendor=mylib")]
#[case(b"\"say \\\"hi\\\"\" vendor=mylib", b"say \"hi\"", b"vendor=mylib")]
fn split_line(#[case] line: &[u8], #[case] pattern: &[u8], #[case] attr: &[u8]) {
let (p, a) = split_attr_line(line).unwrap();
assert_eq!(p.as_ref(), pattern);
assert_eq!(a, attr);
}

#[rstest]
#[case(b"vendor/a.txt")]
#[case(b"")]
#[case(b"# comment line")]
#[case(b"# vendor=mylib")]
#[case(b" ")]
#[case(b" vendor/a.txt attr")]
fn split_no_attr_returns_none(#[case] line: &[u8]) {
assert!(split_attr_line(line).is_none());
}
114 changes: 114 additions & 0 deletions crates/git-vendor/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Command-line interface shape for `git-vendor`.

use clap::{Parser, Subcommand};

/// Manage vendored external repository content.
#[derive(Parser)]
#[command(name = "git-vendor", bin_name = "git vendor")]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}

#[derive(Subcommand)]
pub enum Command {
/// Add a new vendor dependency and integrate it into the current branch.
///
/// Fetches the upstream ref and three-way merges it into the working
/// tree, staging the result for review. Run `git commit` to complete,
/// mirroring `git submodule add` (which also never commits on your
/// behalf).
Add {
/// Remote URL of the upstream repository.
url: String,

/// Unique name for this vendor (used in `.gitvendors` and `.gitattributes`).
/// Defaults to the last path component of the URL, stripped of `.git`.
name: Option<String>,

/// Branch, tag, or SHA to track on the upstream (defaults to `HEAD`).
#[arg(long = "ref", value_name = "REF")]
ref_name: Option<String>,

/// Destination directory for vendored files (defaults to `vendor/<name>/`).
/// Ignored when `--pattern` is also given.
#[arg(long, value_name = "DIR")]
prefix: Option<String>,

/// File pattern used to filter the upstream vendor content with optional
/// remapping into the working copy, e.g. `src/**:vendor/lib/`. May be
/// repeated to select multiple paths.
#[arg(long = "pattern", short = 'p', value_name = "GLOB[:DEST]")]
patterns: Vec<String>,

/// Record upstream history as a parentless squash commit instead of a
/// full merge.
#[arg(long)]
squash: bool,

/// Show what would be fetched and merged without making any changes.
#[arg(long)]
dry_run: bool,

/// Commit message (defaults to `vendor: add <name>`).
#[arg(long, short = 'm', value_name = "MSG")]
message: Option<String>,
},

/// Fetch and integrate upstream updates for one or all vendors.
///
/// Equivalent to `git subtree pull` or `git submodule update --remote`.
/// Stages the result for review; run `git commit` to complete, mirroring
/// `git submodule` (which never commits on your behalf).
Update {
/// Vendor name to update; updates all configured vendors if omitted.
name: Option<String>,

/// Commit message (defaults to `vendor: update <name>`).
#[arg(long, short = 'm', value_name = "MSG")]
message: Option<String>,

/// Allow integrating an upstream that was force-pushed (rewound
/// history). Without this flag, a force-push is reported as an error.
#[arg(long)]
force: bool,

/// Rebuild from the recorded upstream base instead of fetching. Use
/// after editing a vendor's `pattern` entries to move or refilter its
/// files. Local modifications to vendored files would be discarded,
/// so this refuses to proceed on a modified vendor unless `--force`
/// is also given.
#[arg(long)]
no_fetch: bool,

/// Show what would be fetched and merged without making any changes.
#[arg(long)]
dry_run: bool,
},

/// Show synchronization status for one or all vendors.
Status {
/// Vendor name to check; checks all configured vendors if omitted.
name: Option<String>,

/// Fetch from upstream before reporting status.
#[arg(long, short = 'f')]
fetch: bool,
},

/// Remove a vendor dependency and its files from the working tree.
#[command(visible_alias = "rm")]
Remove {
/// Vendor name to remove.
name: String,

/// Remove the config entry and `.gitattributes` tracking but leave the
/// vendored files in the working tree.
#[arg(long)]
keep_files: bool,
},

/// List all configured vendor dependencies.
#[command(visible_alias = "ls")]
List,
}
2 changes: 2 additions & 0 deletions crates/git-vendor/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub enum Error {
Fetch(String),
#[error("merge conflict: {0}")]
Conflict(String),
#[error("path requires quoting in .gitattributes: {0}")]
InvalidPath(String),
#[error(transparent)]
Gix(Box<dyn std::error::Error + Send + Sync + 'static>),
}
Expand Down
Loading
Loading