Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .github/workflows/auto-format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ jobs:
BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
for repo in meta_plugin_protocol meta_git_lib meta_cli meta_core loop_lib; do
if git clone --depth 1 -b "$BRANCH" "https://github.com/harmony-labs/${repo}.git" 2>/dev/null; then
if git clone --depth 1 -b "$BRANCH" "https://github.com/gitkb/${repo}.git" 2>/dev/null; then
echo "Cloned ${repo}@${BRANCH}"
elif git clone --depth 1 "https://github.com/harmony-labs/${repo}.git"; then
elif git clone --depth 1 "https://github.com/gitkb/${repo}.git"; then
echo "Branch ${BRANCH} not found for ${repo}; falling back to default branch"
else
echo "::error::Failed to clone ${repo}"
Expand All @@ -48,7 +48,7 @@ jobs:

- name: Create workspace root
run: |
printf '[workspace]\nmembers = ["meta_git_cli"]\nresolver = "2"\n\n[workspace.package]\nversion = "0.0.0"\nedition = "2021"\nlicense = "MIT"\nrepository = "https://github.com/harmony-labs/meta"\n' > Cargo.toml
printf '[workspace]\nmembers = ["meta_git_cli"]\nresolver = "2"\n\n[workspace.package]\nversion = "0.0.0"\nedition = "2021"\nlicense = "MIT"\nrepository = "https://github.com/gitkb/meta"\n' > Cargo.toml

- uses: dtolnay/rust-toolchain@stable
with:
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ jobs:
BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
for repo in meta_plugin_protocol meta_git_lib meta_cli meta_core loop_lib; do
if git clone --depth 1 -b "$BRANCH" "https://github.com/harmony-labs/${repo}.git" 2>/dev/null; then
if git clone --depth 1 -b "$BRANCH" "https://github.com/gitkb/${repo}.git" 2>/dev/null; then
echo "Cloned ${repo}@${BRANCH}"
elif git clone --depth 1 "https://github.com/harmony-labs/${repo}.git"; then
elif git clone --depth 1 "https://github.com/gitkb/${repo}.git"; then
echo "Branch ${BRANCH} not found for ${repo}; falling back to default branch"
else
echo "::error::Failed to clone ${repo}"
Expand All @@ -44,7 +44,7 @@ jobs:
- name: Create workspace root
shell: bash
run: |
printf '[workspace]\nmembers = ["meta_git_cli"]\nresolver = "2"\n\n[workspace.package]\nversion = "0.0.0"\nedition = "2021"\nlicense = "MIT"\nrepository = "https://github.com/harmony-labs/meta"\n' > Cargo.toml
printf '[workspace]\nmembers = ["meta_git_cli"]\nresolver = "2"\n\n[workspace.package]\nversion = "0.0.0"\nedition = "2021"\nlicense = "MIT"\nrepository = "https://github.com/gitkb/meta"\n' > Cargo.toml

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand Down Expand Up @@ -72,9 +72,9 @@ jobs:
BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
for repo in meta_plugin_protocol meta_git_lib meta_cli meta_core loop_lib; do
if git clone --depth 1 -b "$BRANCH" "https://github.com/harmony-labs/${repo}.git" 2>/dev/null; then
if git clone --depth 1 -b "$BRANCH" "https://github.com/gitkb/${repo}.git" 2>/dev/null; then
echo "Cloned ${repo}@${BRANCH}"
elif git clone --depth 1 "https://github.com/harmony-labs/${repo}.git"; then
elif git clone --depth 1 "https://github.com/gitkb/${repo}.git"; then
echo "Branch ${BRANCH} not found for ${repo}; falling back to default branch"
else
echo "::error::Failed to clone ${repo}"
Expand All @@ -84,7 +84,7 @@ jobs:

- name: Create workspace root
run: |
printf '[workspace]\nmembers = ["meta_git_cli"]\nresolver = "2"\n\n[workspace.package]\nversion = "0.0.0"\nedition = "2021"\nlicense = "MIT"\nrepository = "https://github.com/harmony-labs/meta"\n' > Cargo.toml
printf '[workspace]\nmembers = ["meta_git_cli"]\nresolver = "2"\n\n[workspace.package]\nversion = "0.0.0"\nedition = "2021"\nlicense = "MIT"\nrepository = "https://github.com/gitkb/meta"\n' > Cargo.toml

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/notify-parent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ jobs:
steps:
- name: Notify parent repo
# PARENT_REPO_PAT requires `repo` scope (or fine-grained `contents:write`)
# on harmony-labs/meta. Configure in Settings → Secrets → Actions.
# on gitkb/meta. Configure in Settings → Secrets → Actions.
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.PARENT_REPO_PAT }}
repository: harmony-labs/meta
repository: gitkb/meta
event-type: child-repo-updated
client-payload: >-
{
Expand Down
4 changes: 4 additions & 0 deletions src/commands/worktree/cli_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ pub struct CreateArgs {
#[arg(long, conflicts_with = "repos")]
pub all: bool,

/// Preview planned worktree operations without creating anything
#[arg(long)]
pub dry_run: bool,

/// Alias for positional <COMMIT-ISH> (hidden, kept for backward compatibility)
#[arg(long, value_name = "REF", hide = true, conflicts_with_all = ["commit_ish", "from_pr"])]
pub from_ref: Option<String>,
Expand Down
106 changes: 102 additions & 4 deletions src/commands/worktree/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub(crate) fn handle_create(
let branch_flag = args.branch.as_deref();
let repo_specs = &args.repos;
let use_all = args.all;
let dry_run = args.dry_run;
let ephemeral = args.ephemeral;
let ttl_seconds = args.ttl;
// Parse custom metadata, collecting any invalid entries for strict mode
Expand Down Expand Up @@ -58,9 +59,6 @@ pub(crate) fn handle_create(
anyhow::bail!("Cannot specify both a commit-ish and --from-pr");
}

// Resolve --from-pr: get PR head branch and identify matching repo
let from_pr_info = from_pr_spec.map(resolve_from_pr).transpose()?;

let no_deps = args.no_deps;
let recursive = args.recursive;

Expand Down Expand Up @@ -145,8 +143,24 @@ pub(crate) fn handle_create(
let repos_to_create =
ensure_intermediate_parents(&meta_dir, repos_to_create, name, branch_flag)?;

// Apply --from-pr: override branch for the matching repo and fetch
if dry_run {
let plan = CreateDryRunPlan {
name,
wt_dir: &wt_dir,
repos_to_create: &repos_to_create,
from_ref,
from_pr_spec,
ephemeral,
ttl_seconds,
custom_meta: &custom_meta,
};
print_create_dry_run(&plan);
return Ok(());
}

// Resolve/apply --from-pr: get PR head branch, override branch for the matching repo, and fetch
let mut repos_to_create = repos_to_create;
let from_pr_info = from_pr_spec.map(resolve_from_pr).transpose()?;
if let Some((ref pr_repo_spec, _pr_num, ref pr_branch)) = from_pr_info {
let mut matched = false;
for (alias, source, branch) in repos_to_create.iter_mut() {
Expand Down Expand Up @@ -328,6 +342,90 @@ pub(crate) fn handle_create(
Ok(())
}

struct CreateDryRunPlan<'a> {
name: &'a str,
wt_dir: &'a std::path::Path,
repos_to_create: &'a [(String, std::path::PathBuf, String)],
from_ref: Option<&'a str>,
from_pr_spec: Option<&'a str>,
ephemeral: bool,
ttl_seconds: Option<u64>,
custom_meta: &'a HashMap<String, String>,
}

fn print_create_dry_run(plan: &CreateDryRunPlan<'_>) {
println!(
"[DRY RUN] Would create worktree set '{}' at {}",
plan.name,
plan.wt_dir.display()
);
println!();

if let Some(from_ref) = plan.from_ref {
println!("Start ref: {from_ref}");
}
if let Some(from_pr_spec) = plan.from_pr_spec {
println!("PR source: would resolve {from_pr_spec} and fetch the matching head branch");
}
if plan.ephemeral {
println!("Ephemeral: true");
}
if let Some(ttl) = plan.ttl_seconds {
println!("TTL: {}", format_duration(ttl as i64));
}
if !plan.custom_meta.is_empty() {
println!(
"Metadata: {}",
plan.custom_meta
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(", ")
);
}

println!("Planned repo operations:");
if plan.repos_to_create.is_empty() {
println!(" (none)");
}
for (alias, source, branch) in plan.repos_to_create {
let dest = if alias == "." {
plan.wt_dir.to_path_buf()
} else {
plan.wt_dir.join(alias)
};
let mut cmd = format!(
"git -C {} worktree add -b {} {}",
shell_quote(&source.display().to_string()),
shell_quote(branch),
shell_quote(&dest.display().to_string())
);
if let Some(from_ref) = plan.from_ref {
cmd.push(' ');
cmd.push_str(&shell_quote(from_ref));
}
println!(" {alias}:");
println!(" source: {}", source.display());
println!(" dest: {}", dest.display());
println!(" branch: {branch}");
println!(" bash: {cmd}");
}

println!();
println!("No directories, branches, store entries, hooks, or .gitignore changes were written.");
}

fn shell_quote(value: &str) -> String {
if value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '/' | ':'))
{
value.to_string()
} else {
format!("'{}'", value.replace('\'', "'\\''"))
}
}

/// Resolve repos with automatic dependency resolution.
///
/// When --repo is specified without --no-deps:
Expand Down
1 change: 1 addition & 0 deletions src/commands/worktree/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ fn handle_ephemeral_exec(args: ExecArgs, verbose: bool, json: bool) -> Result<()
branch: args.branch,
repos: args.repos,
all: args.all,
dry_run: false,
from_ref: args.from_ref,
from_pr: args.from_pr,
ephemeral: true,
Expand Down
11 changes: 10 additions & 1 deletion src/commands/worktree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub fn execute_worktree_command(
json: bool,
strict: bool,
recursive: bool,
dry_run: bool,
) -> CommandResult {
// Extract the subcommand name from the matched command prefix (if present).
// When the plugin registers bare "worktree", the subcommand is in args[0] instead.
Expand All @@ -63,12 +64,19 @@ pub fn execute_worktree_command(
if recursive && is_create && !clap_args.iter().any(|a| a == "--recursive" || a == "-r") {
clap_args.push("--recursive".to_string());
}
if dry_run && is_create && !clap_args.iter().any(|a| a == "--dry-run") {
clap_args.push("--dry-run".to_string());
}

// No subcommand at all — show help
if clap_args.is_empty() {
print_worktree_help();
return CommandResult::Message(String::new());
}
if clap_args.len() == 1 && matches!(clap_args[0].as_str(), "--help" | "-h") {
print_worktree_help();
return CommandResult::Message(String::new());
}

match WorktreeParser::try_parse_from(&clap_args) {
Ok(parsed) => match handle_worktree_command(parsed.command, verbose, json, strict) {
Expand All @@ -82,7 +90,7 @@ pub fn execute_worktree_command(
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
// These are success cases - print and exit 0
println!("{e}");
CommandResult::ShowHelp(None)
CommandResult::Message(String::new())
}
_ => {
// Actual parse errors
Expand Down Expand Up @@ -190,6 +198,7 @@ fn write_worktree_help(w: &mut dyn std::io::Write) {
w,
" --from-pr <OWNER/REPO#N> Start from a PR's head branch"
);
let _ = writeln!(w, " --dry-run Preview planned operations");
let _ = writeln!(w, " --ephemeral Mark for automatic cleanup");
let _ = writeln!(
w,
Expand Down
95 changes: 93 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub fn execute_command(
);
debug!("[meta_git_cli] Projects from meta_cli: {projects:?}");

let help_requested = has_help_token(command, args);

// Worktree commands (meta worktree * and meta git worktree *)
// Checked before --help intercept so worktree subcommands can handle their own --help via clap
if command.starts_with("worktree") || command.starts_with("git worktree") {
Expand All @@ -52,11 +54,18 @@ pub fn execute_command(
options.json_output,
options.strict,
options.recursive,
options.dry_run,
);
}

// Intercept --help/-h before dispatching to git subcommand handlers
if args.iter().any(|a| a == "--help" || a == "-h") {
if command.starts_with("git snapshot") && help_requested {
return snapshot::execute_snapshot_command_help(command);
}

if help_requested {
if let Some(help) = build_passthrough_git_help(command, args, projects, cwd) {
return CommandResult::Message(help);
}
return CommandResult::ShowHelp(None);
}

Expand Down Expand Up @@ -90,6 +99,88 @@ fn is_remote_command(command: &str) -> bool {
remote_commands.iter().any(|rc| cmd_lower.contains(rc))
}

fn has_help_token(command: &str, args: &[String]) -> bool {
command
.split_whitespace()
.chain(args.iter().map(String::as_str))
.any(|arg| arg == "--help" || arg == "-h")
}

fn build_passthrough_git_help(
command: &str,
args: &[String],
projects: &[String],
cwd: &Path,
) -> Option<String> {
let command_words: Vec<&str> = command
.split_whitespace()
.filter(|word| *word != "--help" && *word != "-h")
.collect();

if command_words.first().copied() != Some("git") {
return None;
}

let adapted_commands = [
"clone", "status", "update", "commit", "snapshot", "worktree",
];
let git_args: Vec<String> = command_words
.iter()
.skip(1)
.map(|s| s.to_string())
.chain(
args.iter()
.filter(|arg| arg.as_str() != "--help" && arg.as_str() != "-h")
.cloned(),
)
.collect();
let subcommand = git_args.first()?;

if adapted_commands.contains(&subcommand.as_str()) {
return None;
}

let display_command = format!("git {}", git_args.join(" "));
let scope = match get_project_directories_with_fallback(projects, cwd) {
Ok(dirs) => {
if dirs.is_empty() {
"Current scope: no repos were discovered.".to_string()
} else {
format!(
"Current scope: {} repo(s): {}",
dirs.len(),
dirs.iter()
.map(|d| d.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
}
Err(_) => "Current scope: unable to resolve repos from the current directory.".to_string(),
};

Some(format!(
"meta git {subcommand} - Pass-through git command\n\n\
Usage: meta git {subcommand} [GIT_ARGS...] [META_OPTIONS]\n\n\
Runs `{display_command}` in each repo in the current meta workspace.\n\n\
{scope}\n\n\
Meta options:\n\
--dry-run Show the command that would run in each repo\n\
--recursive Include repos from nested meta workspaces\n\
--depth <N> Limit recursive traversal depth\n\
--include <REPOS> Only run in specified repos/directories\n\
--exclude <REPOS> Skip specified repos/directories\n\
--tag <TAGS> Filter by project tag(s), comma-separated\n\
--parallel Run repo commands concurrently\n\
--sequential Run repo commands one at a time\n\n\
Examples:\n\
meta git {subcommand}\n\
meta git {subcommand} --dry-run\n\
meta git {subcommand} --recursive --dry-run\n\n\
Git help: run `git {subcommand} --help` directly for upstream git options."
))
}

/// Execute a raw git command across all repos (fallback for unrecognized subcommands)
fn execute_raw_git_command(
command: &str,
Expand Down
Loading
Loading