diff --git a/crates/moon/src/cli/deps.rs b/crates/moon/src/cli/deps.rs index 599659501..1ab1077a3 100644 --- a/crates/moon/src/cli/deps.rs +++ b/crates/moon/src/cli/deps.rs @@ -30,10 +30,16 @@ use moonutil::{ use super::UniversalFlags; use super::install_binary::{ GitRef, install_binary, install_from_git, install_from_local, is_git_url, is_local_path, - parse_package_spec, + list_installed_binaries, parse_package_spec, }; pub(crate) fn install_cli(cli: UniversalFlags, cmd: InstallSubcommand) -> anyhow::Result { + let install_dir = cmd.bin.clone().unwrap_or_else(moon_dir::bin); + + if cmd.list { + return list_installed_binaries(&install_dir, cli.quiet); + } + // If no package path and no local path, use legacy behavior if cmd.package_path.is_none() && cmd.path.is_none() { eprintln!( @@ -54,7 +60,6 @@ pub(crate) fn install_cli(cli: UniversalFlags, cmd: InstallSubcommand) -> anyhow ); } - let install_dir = cmd.bin.unwrap_or_else(moon_dir::bin); let has_git_ref = cmd.rev.is_some() || cmd.branch.is_some() || cmd.tag.is_some(); // Explicit --path takes priority diff --git a/crates/moon/src/cli/install_binary.rs b/crates/moon/src/cli/install_binary.rs index 445c80c49..4489cca54 100644 --- a/crates/moon/src/cli/install_binary.rs +++ b/crates/moon/src/cli/install_binary.rs @@ -27,10 +27,16 @@ use mooncake::registry::{OnlineRegistry, Registry}; use moonutil::{ cli::UniversalFlags, common::{FileLock, RunMode, TargetBackend}, - mooncakes::{ModuleName, RegistryConfig}, + mooncakes::{DEFAULT_VERSION, ModuleName, RegistryConfig}, }; use semver::Version; -use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs::File, + io::{BufReader, BufWriter, Write}, + path::{Path, PathBuf}, +}; use crate::{ cli::BuildFlags, @@ -57,6 +63,58 @@ enum PackageFilter { } const GIT_URL_PREFIXES: &[&str] = &["https://", "http://", "git://", "ssh://", "git@"]; +const INSTALL_RECEIPTS_FILE: &str = ".moon-install-receipts.json"; +const INSTALL_RECEIPTS_SCHEMA_VERSION: u32 = 1; + +#[derive(Debug, Clone)] +struct InstallMetadata { + module_name: ModuleName, + module_version: Version, + source_kind: InstallSourceKind, + source_ref: Option, + git_ref: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum InstallSourceKind { + Registry, + Local, + Git, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +struct InstallReceiptEntry { + /// Logical command name shown to users (no platform suffix). + binary_name: String, + /// On-disk executable filename used for overwrite/upsert identity. + /// On Windows this includes `.exe`, while `binary_name` does not. + executable_name: String, + module_name: String, + module_version: String, + package_path: String, + source_kind: InstallSourceKind, + #[serde(default, skip_serializing_if = "Option::is_none")] + source_ref: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + git_ref: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +struct InstallReceiptFile { + schema_version: u32, + #[serde(default)] + entries: Vec, +} + +impl Default for InstallReceiptFile { + fn default() -> Self { + Self { + schema_version: INSTALL_RECEIPTS_SCHEMA_VERSION, + entries: Vec::new(), + } + } +} /// Check if a string looks like a git URL. pub(super) fn is_git_url(s: &str) -> bool { @@ -121,6 +179,116 @@ pub(super) fn parse_package_spec(input: &str) -> anyhow::Result { }) } +fn install_receipts_path(install_dir: &Path) -> PathBuf { + install_dir.join(INSTALL_RECEIPTS_FILE) +} + +fn load_install_receipts(install_dir: &Path) -> anyhow::Result { + let receipts_path = install_receipts_path(install_dir); + if !receipts_path.exists() { + return Ok(InstallReceiptFile::default()); + } + + let reader = BufReader::new( + File::open(&receipts_path) + .with_context(|| format!("Failed to open `{}`", receipts_path.display()))?, + ); + let receipts: InstallReceiptFile = serde_json_lenient::from_reader(reader) + .with_context(|| format!("Failed to parse `{}`", receipts_path.display()))?; + + if receipts.schema_version != INSTALL_RECEIPTS_SCHEMA_VERSION { + bail!( + "Unsupported install receipt schema version {} in `{}`", + receipts.schema_version, + receipts_path.display() + ); + } + + Ok(receipts) +} + +fn save_install_receipts(install_dir: &Path, receipts: &InstallReceiptFile) -> anyhow::Result<()> { + std::fs::create_dir_all(install_dir).with_context(|| { + format!( + "Failed to create install directory `{}`", + install_dir.display() + ) + })?; + + let receipts_path = install_receipts_path(install_dir); + let file = File::create(&receipts_path) + .with_context(|| format!("Failed to create `{}`", receipts_path.display()))?; + let mut writer = BufWriter::new(file); + serde_json_lenient::to_writer_pretty(&mut writer, receipts) + .with_context(|| format!("Failed to write `{}`", receipts_path.display()))?; + writer.write_all(b"\n")?; + writer.flush()?; + Ok(()) +} + +fn record_install_entry(install_dir: &Path, entry: InstallReceiptEntry) -> anyhow::Result<()> { + let mut receipts = load_install_receipts(install_dir)?; + // Upsert by on-disk filename because that is what can be overwritten. + if let Some(existing) = receipts + .entries + .iter_mut() + .find(|existing| existing.executable_name == entry.executable_name) + { + *existing = entry; + } else { + receipts.entries.push(entry); + } + save_install_receipts(install_dir, &receipts) +} + +fn format_receipt_header(entry: &InstallReceiptEntry) -> String { + let base = format!("{} v{}", entry.module_name, entry.module_version); + match entry.source_kind { + InstallSourceKind::Registry => base, + InstallSourceKind::Local => match &entry.source_ref { + Some(source_ref) => format!("{base} ({source_ref})"), + None => base, + }, + InstallSourceKind::Git => match (&entry.source_ref, &entry.git_ref) { + (Some(source_ref), Some(git_ref)) => format!("{base} ({source_ref}#{git_ref})"), + (Some(source_ref), None) => format!("{base} ({source_ref})"), + (None, Some(git_ref)) => format!("{base} ({git_ref})"), + (None, None) => base, + }, + } +} + +fn format_install_receipt_list(receipts: &InstallReceiptFile) -> String { + let mut groups: BTreeMap> = BTreeMap::new(); + for entry in &receipts.entries { + groups + .entry(format_receipt_header(entry)) + .or_default() + .insert(entry.binary_name.clone()); + } + + let mut out = String::new(); + for (header, binaries) in groups { + out.push_str(&header); + out.push_str(":\n"); + for binary in binaries { + out.push_str(" "); + out.push_str(&binary); + out.push('\n'); + } + } + out +} + +pub(super) fn list_installed_binaries(install_dir: &Path, quiet: bool) -> anyhow::Result { + let receipts = load_install_receipts(install_dir)?; + let out = format_install_receipt_list(&receipts); + if !quiet && !out.is_empty() { + print!("{out}"); + } + Ok(0) +} + /// Install a binary package from the registry. pub(super) fn install_binary( cli: &UniversalFlags, @@ -190,7 +358,15 @@ pub(super) fn install_binary( PackageFilter::ByPackagePath(spec.package_path.clone().unwrap_or_default()) }; - build_and_install_packages(cli, &spec.module_name, module_dir, install_dir, filter) + let metadata = InstallMetadata { + module_name: spec.module_name.clone(), + module_version: version, + source_kind: InstallSourceKind::Registry, + source_ref: Some(registry_config.registry), + git_ref: None, + }; + + build_and_install_packages(cli, module_dir, install_dir, filter, &metadata) } /// Install from a local path. @@ -216,6 +392,10 @@ pub(super) fn install_from_local( let module = moonutil::common::read_module_desc_file_in_dir(&module_root)?; let module_name: ModuleName = module.name.parse().map_err(|e| anyhow::anyhow!("{}", e))?; + let module_version = module + .version + .clone() + .unwrap_or_else(|| DEFAULT_VERSION.clone()); let filter = if input_path == module_root { PackageFilter::Wildcard { @@ -225,10 +405,19 @@ pub(super) fn install_from_local( PackageFilter::ByPath(input_path) }; - build_and_install_packages(cli, &module_name, &module_root, install_dir, filter) + let metadata = InstallMetadata { + module_name, + module_version, + source_kind: InstallSourceKind::Local, + source_ref: Some(module_root.display().to_string()), + git_ref: None, + }; + + build_and_install_packages(cli, &module_root, install_dir, filter, &metadata) } /// Git reference type for checkout. +#[derive(Clone, Copy)] pub(super) enum GitRef<'a> { /// Checkout a specific revision (commit hash) Rev(&'a str), @@ -240,6 +429,17 @@ pub(super) enum GitRef<'a> { Default, } +impl<'a> GitRef<'a> { + fn as_receipt_ref(self) -> Option { + match self { + GitRef::Rev(rev) => Some(format!("rev:{rev}")), + GitRef::Branch(branch) => Some(format!("branch:{branch}")), + GitRef::Tag(tag) => Some(format!("tag:{tag}")), + GitRef::Default => Some("default".to_string()), + } + } +} + /// Install from a git repository. pub(super) fn install_from_git( cli: &UniversalFlags, @@ -324,6 +524,10 @@ pub(super) fn install_from_git( let module = moonutil::common::read_module_desc_file_in_dir(&module_root)?; let module_name: ModuleName = module.name.parse().map_err(|e| anyhow::anyhow!("{}", e))?; + let module_version = module + .version + .clone() + .unwrap_or_else(|| DEFAULT_VERSION.clone()); let is_module_root = target_path == module_root; let is_wildcard = package_path @@ -346,16 +550,24 @@ pub(super) fn install_from_git( ) }; - build_and_install_packages(cli, &module_name, &module_root, install_dir, filter) + let metadata = InstallMetadata { + module_name, + module_version, + source_kind: InstallSourceKind::Git, + source_ref: Some(git_url.to_string()), + git_ref: git_ref.as_receipt_ref(), + }; + + build_and_install_packages(cli, &module_root, install_dir, filter, &metadata) } /// Build matching packages and install binaries using RR build engine. fn build_and_install_packages( cli: &UniversalFlags, - module_name: &ModuleName, module_dir: &Path, install_dir: &Path, filter: PackageFilter, + metadata: &InstallMetadata, ) -> anyhow::Result { let quiet = cli.quiet; @@ -409,20 +621,23 @@ fn build_and_install_packages( } PackageFilter::Wildcard { prefix } => { if prefix.is_empty() { - bail!("No main packages found in module `{}`", module_name); + bail!( + "No main packages found in module `{}`", + metadata.module_name + ); } else { bail!( "No main packages found matching pattern `{}/{}/...`", - module_name, + metadata.module_name, prefix ); } } PackageFilter::ByPackagePath(target) => { let full_name = if target.is_empty() { - module_name.to_string() + metadata.module_name.to_string() } else { - format!("{}/{}", module_name, target) + format!("{}/{}", metadata.module_name, target) }; bail!( "Package `{}` not found or is not a main package (is-main: true required)", @@ -441,7 +656,7 @@ fn build_and_install_packages( .rsplit('/') .next() .filter(|s| !s.is_empty()) - .unwrap_or(&module_name.unqual) + .unwrap_or(&metadata.module_name.unqual) .to_string(); // Check if binary name would overwrite a reserved toolchain binary @@ -455,9 +670,9 @@ fn build_and_install_packages( } let full_pkg_name = if pkg_path.is_empty() { - module_name.to_string() + metadata.module_name.to_string() } else { - format!("{}/{}", module_name, pkg_path) + format!("{}/{}", metadata.module_name, pkg_path) }; if !quiet { @@ -512,7 +727,7 @@ fn build_and_install_packages( } else { binary_name.clone() }; - let binary_dst = install_dir.join(dst_name); + let binary_dst = install_dir.join(&dst_name); std::fs::copy(&binary_src, &binary_dst).with_context(|| { format!( @@ -530,6 +745,20 @@ fn build_and_install_packages( std::fs::set_permissions(&binary_dst, perms)?; } + record_install_entry( + install_dir, + InstallReceiptEntry { + binary_name: binary_name.clone(), + executable_name: dst_name, + module_name: metadata.module_name.to_string(), + module_version: metadata.module_version.to_string(), + package_path: pkg_path.clone(), + source_kind: metadata.source_kind.clone(), + source_ref: metadata.source_ref.clone(), + git_ref: metadata.git_ref.clone(), + }, + )?; + if !quiet { eprintln!( "{}: Installed `{}` to `{}`", @@ -661,4 +890,83 @@ mod tests { // Invalid version assert!(parse_package_spec("user/module@invalid").is_err()); } + + #[test] + fn test_record_install_entry_upserts_by_executable_name() { + let install_dir = tempfile::tempdir().unwrap(); + let install_dir = install_dir.path(); + + let old_entry = InstallReceiptEntry { + binary_name: "hello".to_string(), + executable_name: "hello".to_string(), + module_name: "author/pkg".to_string(), + module_version: "0.1.0".to_string(), + package_path: "main".to_string(), + source_kind: InstallSourceKind::Registry, + source_ref: Some("https://mooncakes.io".to_string()), + git_ref: None, + }; + record_install_entry(install_dir, old_entry).unwrap(); + + let new_entry = InstallReceiptEntry { + binary_name: "hello".to_string(), + executable_name: "hello".to_string(), + module_name: "author/pkg".to_string(), + module_version: "0.2.0".to_string(), + package_path: "main".to_string(), + source_kind: InstallSourceKind::Registry, + source_ref: Some("https://mooncakes.io".to_string()), + git_ref: None, + }; + record_install_entry(install_dir, new_entry).unwrap(); + + let receipts = load_install_receipts(install_dir).unwrap(); + assert_eq!(receipts.entries.len(), 1); + assert_eq!(receipts.entries[0].module_version, "0.2.0"); + } + + #[test] + fn test_format_install_receipt_list_groups_and_sorts() { + let receipts = InstallReceiptFile { + schema_version: INSTALL_RECEIPTS_SCHEMA_VERSION, + entries: vec![ + InstallReceiptEntry { + binary_name: "main-js".to_string(), + executable_name: "main-js".to_string(), + module_name: "zed/mod".to_string(), + module_version: "1.2.3".to_string(), + package_path: "main-js".to_string(), + source_kind: InstallSourceKind::Local, + source_ref: Some("/tmp/zed".to_string()), + git_ref: None, + }, + InstallReceiptEntry { + binary_name: "main-native".to_string(), + executable_name: "main-native".to_string(), + module_name: "zed/mod".to_string(), + module_version: "1.2.3".to_string(), + package_path: "main-native".to_string(), + source_kind: InstallSourceKind::Local, + source_ref: Some("/tmp/zed".to_string()), + git_ref: None, + }, + InstallReceiptEntry { + binary_name: "tool".to_string(), + executable_name: "tool".to_string(), + module_name: "abc/pkg".to_string(), + module_version: "0.1.0".to_string(), + package_path: "tool".to_string(), + source_kind: InstallSourceKind::Git, + source_ref: Some("https://example.com/repo.git".to_string()), + git_ref: Some("branch:main".to_string()), + }, + ], + }; + + let out = format_install_receipt_list(&receipts); + assert_eq!( + out, + "abc/pkg v0.1.0 (https://example.com/repo.git#branch:main):\n tool\nzed/mod v1.2.3 (/tmp/zed):\n main-js\n main-native\n" + ); + } } diff --git a/crates/moon/tests/test_cases/mod.rs b/crates/moon/tests/test_cases/mod.rs index 42f9662ff..e48e1bd07 100644 --- a/crates/moon/tests/test_cases/mod.rs +++ b/crates/moon/tests/test_cases/mod.rs @@ -1928,6 +1928,45 @@ fn test_moon_install_bin() { assert!(content.contains("()")); } +#[test] +fn test_moon_install_list() { + use std::ffi::OsString; + + let top_dir = TestDir::new("moon_install_bin.in"); + let author1 = top_dir.join("author1.in"); + let install_dir = top_dir.join("moon_bin"); + let install_dir_os = install_dir.into_os_string(); + + get_stdout( + &author1, + vec![ + OsString::from("install"), + OsString::from("--path"), + OsString::from("./src/main-js"), + OsString::from("--bin"), + install_dir_os.clone(), + ], + ); + + let out = get_stdout( + &author1, + vec![ + OsString::from("install"), + OsString::from("--list"), + OsString::from("--bin"), + install_dir_os, + ], + ); + + check( + out, + expect![[r#" + username/flash v0.1.0 ($ROOT): + main-js + "#]], + ); +} + #[test] #[ignore = "platform-dependent behavior"] fn test_strip_debug() { diff --git a/crates/moon/tests/test_cases/moon_commands/shell_completion_bash.stdout b/crates/moon/tests/test_cases/moon_commands/shell_completion_bash.stdout index 2e448648a..78f0da056 100644 --- a/crates/moon/tests/test_cases/moon_commands/shell_completion_bash.stdout +++ b/crates/moon/tests/test_cases/moon_commands/shell_completion_bash.stdout @@ -1931,7 +1931,7 @@ _moon() { return 0 ;; moon__install) - opts="-q -v -h --bin --path --rev --branch --tag --manifest-path --target-dir --quiet --verbose --trace --dry-run --build-graph --help [PACKAGE_PATH] [PACKAGE_PATH_IN_REPO]" + opts="-q -v -h --bin --list --path --rev --branch --tag --manifest-path --target-dir --quiet --verbose --trace --dry-run --build-graph --help [PACKAGE_PATH] [PACKAGE_PATH_IN_REPO]" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/crates/moon/tests/test_cases/moon_commands/shell_completion_elvish.stdout b/crates/moon/tests/test_cases/moon_commands/shell_completion_elvish.stdout index f437a4ff3..83fa011f9 100644 --- a/crates/moon/tests/test_cases/moon_commands/shell_completion_elvish.stdout +++ b/crates/moon/tests/test_cases/moon_commands/shell_completion_elvish.stdout @@ -466,6 +466,7 @@ set edit:completion:arg-completer[moon] = {|@words| cand --tag 'Git tag to checkout (requires git URL)' cand --manifest-path 'Path to `moon.mod.json` to use as the project manifest (does not change the working directory)' cand --target-dir 'The target directory. Defaults to `/_build`' + cand --list 'List globally installed binaries recorded by `moon install`' cand -q 'Suppress output' cand --quiet 'Suppress output' cand -v 'Increase verbosity' diff --git a/crates/moon/tests/test_cases/moon_commands/shell_completion_fish.stdout b/crates/moon/tests/test_cases/moon_commands/shell_completion_fish.stdout index f8d98e445..6ffe1d870 100644 --- a/crates/moon/tests/test_cases/moon_commands/shell_completion_fish.stdout +++ b/crates/moon/tests/test_cases/moon_commands/shell_completion_fish.stdout @@ -363,6 +363,7 @@ complete -c moon -n "__fish_moon_using_subcommand install" -l branch -d 'Git bra complete -c moon -n "__fish_moon_using_subcommand install" -l tag -d 'Git tag to checkout (requires git URL)' -r complete -c moon -n "__fish_moon_using_subcommand install" -l manifest-path -d 'Path to `moon.mod.json` to use as the project manifest (does not change the working directory)' -r -F complete -c moon -n "__fish_moon_using_subcommand install" -l target-dir -d 'The target directory. Defaults to `/_build`' -r -F +complete -c moon -n "__fish_moon_using_subcommand install" -l list -d 'List globally installed binaries recorded by `moon install`' complete -c moon -n "__fish_moon_using_subcommand install" -s q -l quiet -d 'Suppress output' complete -c moon -n "__fish_moon_using_subcommand install" -s v -l verbose -d 'Increase verbosity' complete -c moon -n "__fish_moon_using_subcommand install" -l trace -d 'Trace the execution of the program' diff --git a/crates/moon/tests/test_cases/moon_commands/shell_completion_powershell.stdout b/crates/moon/tests/test_cases/moon_commands/shell_completion_powershell.stdout index 20542429c..b420395a8 100644 --- a/crates/moon/tests/test_cases/moon_commands/shell_completion_powershell.stdout +++ b/crates/moon/tests/test_cases/moon_commands/shell_completion_powershell.stdout @@ -484,6 +484,7 @@ Register-ArgumentCompleter -Native -CommandName 'moon' -ScriptBlock { [CompletionResult]::new('--tag', 'tag', [CompletionResultType]::ParameterName, 'Git tag to checkout (requires git URL)') [CompletionResult]::new('--manifest-path', 'manifest-path', [CompletionResultType]::ParameterName, 'Path to `moon.mod.json` to use as the project manifest (does not change the working directory)') [CompletionResult]::new('--target-dir', 'target-dir', [CompletionResultType]::ParameterName, 'The target directory. Defaults to `/_build`') + [CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List globally installed binaries recorded by `moon install`') [CompletionResult]::new('-q', 'q', [CompletionResultType]::ParameterName, 'Suppress output') [CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Suppress output') [CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Increase verbosity') diff --git a/crates/moon/tests/test_cases/moon_commands/shell_completion_zsh.stdout b/crates/moon/tests/test_cases/moon_commands/shell_completion_zsh.stdout index 9af8317c1..abda692ed 100644 --- a/crates/moon/tests/test_cases/moon_commands/shell_completion_zsh.stdout +++ b/crates/moon/tests/test_cases/moon_commands/shell_completion_zsh.stdout @@ -482,6 +482,7 @@ _arguments "${_arguments_options[@]}" : / '--tag=[Git tag to checkout (requires git URL)]:TAG: ' / '--manifest-path=[Path to /`moon.mod.json/` to use as the project manifest (does not change the working directory)]:PATH:_files' / '--target-dir=[The target directory. Defaults to /`/_build/`]:TARGET_DIR:_files' / +'(--path --rev --branch --tag)--list[List globally installed binaries recorded by /`moon install/`]' / '-q[Suppress output]' / '--quiet[Suppress output]' / '-v[Increase verbosity]' / diff --git a/crates/mooncake/src/pkg/install.rs b/crates/mooncake/src/pkg/install.rs index 929d1b477..763c3f80b 100644 --- a/crates/mooncake/src/pkg/install.rs +++ b/crates/mooncake/src/pkg/install.rs @@ -54,6 +54,16 @@ pub struct InstallSubcommand { #[clap(long)] pub bin: Option, + /// List globally installed binaries recorded by `moon install` + #[clap( + long, + conflicts_with = "package_path", + conflicts_with = "package_path_in_repo", + conflicts_with = "path", + conflicts_with = "git_ref" + )] + pub list: bool, + /// Install from local path instead of registry #[clap( long, diff --git a/docs/manual-zh/src/commands.md b/docs/manual-zh/src/commands.md index 559f7c4e1..c27eb3824 100644 --- a/docs/manual-zh/src/commands.md +++ b/docs/manual-zh/src/commands.md @@ -436,6 +436,7 @@ Install a binary package globally or install project dependencies (deprecated wi ###### **Options:** * `--bin ` — Specify installation directory (default: ~/.moon/bin/) +* `--list` — List globally installed binaries recorded by `moon install` * `--path ` — Install from local path instead of registry * `--rev ` — Git revision to checkout (commit hash, requires git URL) * `--branch ` — Git branch to checkout (requires git URL) diff --git a/docs/manual/src/commands.md b/docs/manual/src/commands.md index 559f7c4e1..c27eb3824 100644 --- a/docs/manual/src/commands.md +++ b/docs/manual/src/commands.md @@ -436,6 +436,7 @@ Install a binary package globally or install project dependencies (deprecated wi ###### **Options:** * `--bin ` — Specify installation directory (default: ~/.moon/bin/) +* `--list` — List globally installed binaries recorded by `moon install` * `--path ` — Install from local path instead of registry * `--rev ` — Git revision to checkout (commit hash, requires git URL) * `--branch ` — Git branch to checkout (requires git URL)