diff --git a/.github/workflows/pr-single-commit-up-to-date.yml b/.github/workflows/pr-single-commit-up-to-date.yml new file mode 100644 index 000000000..8056d7628 --- /dev/null +++ b/.github/workflows/pr-single-commit-up-to-date.yml @@ -0,0 +1,75 @@ +# .github/workflows/pr-single-commit-up-to-date.yml +name: Enforce single commit & up-to-date + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] + +permissions: + contents: read + pull-requests: write + +jobs: + check-ahead-behind: + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-22.04 + steps: + - name: Checkout PR HEAD (full history) + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Fetch base branch + run: | + git fetch origin ${{ github.event.pull_request.base.ref }} + + - name: Compute ahead/behind + id: ab + shell: bash + run: | + BASE_SHA=${{ github.event.pull_request.base.sha }} + read BEHIND AHEAD < <(git rev-list --left-right --count ${BASE_SHA}...HEAD) + echo "head=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT + echo "base=${BASE_SHA}" >> $GITHUB_OUTPUT + echo "behind=$BEHIND" >> $GITHUB_OUTPUT + echo "ahead=$AHEAD" >> $GITHUB_OUTPUT + echo "Behind: $BEHIND, Ahead: $AHEAD" + + - name: Enforce rule + shell: bash + run: | + BASE=${{ steps.ab.outputs.base }} + HEAD=${{ steps.ab.outputs.head}} + BEHIND=${{ steps.ab.outputs.behind }} + AHEAD=${{ steps.ab.outputs.ahead }} + if [[ "$BEHIND" -ne 0 || "$AHEAD" -ne 1 ]]; then + echo "PR must be exactly 1 commit ahead and 0 behind the base branch." + echo "base=$BASE, HEAD=$HEAD" + echo "Ahead=$AHEAD, Behind=$BEHIND" + echo "Rebase and squash to fix." + exit 1 + fi + echo "OK: PR is exactly 1 ahead and 0 behind." + + - name: Convert PR to draft on failure (base repo context) + if: failure() + uses: actions/github-script@v7 + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { + core.setFailed('No pull_request context available.'); + } else if (pr.draft) { + core.info('PR is already in draft.'); + } else { + await github.request('POST /repos/{owner}/{repo}/pulls/{pull_number}/convert-to-draft', { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + headers: { 'X-GitHub-Api-Version': '2022-11-28' } + }); + core.info(`Converted PR #${pr.number} to draft.`); + } diff --git a/Cargo.lock b/Cargo.lock index f0f34dce3..49cb21280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -864,6 +864,23 @@ dependencies = [ "winnow", ] +[[package]] +name = "gix-attributes" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45442188216d08a5959af195f659cb1f244a50d7d2d0c3873633b1cd7135f638" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror", + "unicode-bom", +] + [[package]] name = "gix-chunk" version = "0.4.11" @@ -1138,6 +1155,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-pathspec" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daedead611c9bd1f3640dc90a9012b45f790201788af4d659f28d94071da7fba" +dependencies = [ + "bitflags 2.9.4", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror", +] + [[package]] name = "gix-protocol" version = "0.51.0" @@ -1257,6 +1289,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-submodule" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "657cc5dd43cbc7a14d9c5aaf02cfbe9c2a15d077cded3f304adb30ef78852d3e" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror", +] + [[package]] name = "gix-tempfile" version = "18.0.0" @@ -1860,6 +1907,7 @@ dependencies = [ "gix-config", "gix-hash", "gix-object", + "gix-submodule", "glob", "hex", "indoc", @@ -1877,7 +1925,9 @@ dependencies = [ "serde_yaml", "sled", "strfmt", + "toml", "tracing", + "unindent", ] [[package]] @@ -1896,6 +1946,7 @@ dependencies = [ "log", "rs_tracing", "serde_json", + "toml", ] [[package]] @@ -2081,6 +2132,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/docs/src/reference/filters.md b/docs/src/reference/filters.md index 2580b0617..696b42cd5 100644 --- a/docs/src/reference/filters.md +++ b/docs/src/reference/filters.md @@ -114,6 +114,11 @@ commits that don't match any of the other shas. Produce the history that would be the result of pushing the passed branches with the passed filters into the upstream. +### Start filtering from a specific commit **:from(:filter)** + +Produce a history that keeps the original history leading up to the specified commit `` unchanged, +but applies the given `:filter` to all commits from that commit onwards. + ### Prune trivial merge commits **:prune=trivial-merge** Produce a history that skips all merge commits whose tree is identical to the first parents diff --git a/josh-cli/Cargo.toml b/josh-cli/Cargo.toml index 9283a5ea2..04e04fcdd 100644 --- a/josh-cli/Cargo.toml +++ b/josh-cli/Cargo.toml @@ -22,3 +22,4 @@ clap = { workspace = true } rs_tracing = { workspace = true } juniper = { workspace = true } git2 = { workspace = true } +toml = { workspace = true } diff --git a/josh-cli/src/bin/josh.rs b/josh-cli/src/bin/josh.rs index 66d3b5447..001e59623 100644 --- a/josh-cli/src/bin/josh.rs +++ b/josh-cli/src/bin/josh.rs @@ -105,6 +105,9 @@ pub enum Command { /// Apply filtering to existing refs (like `josh fetch` but without fetching) Filter(FilterArgs), + + /// Manage josh links (like `josh remote` but for links) + Link(LinkArgs), } #[derive(Debug, clap::Parser)] @@ -242,6 +245,47 @@ pub struct FilterArgs { pub remote: String, } +#[derive(Debug, clap::Parser)] +pub struct LinkArgs { + /// Link subcommand + #[command(subcommand)] + pub command: LinkCommand, +} + +#[derive(Debug, clap::Subcommand)] +pub enum LinkCommand { + /// Add a link with optional filter and target branch + Add(LinkAddArgs), + /// Fetch from existing link files + Fetch(LinkFetchArgs), +} + +#[derive(Debug, clap::Parser)] +pub struct LinkAddArgs { + /// Path where the link will be mounted + #[arg()] + pub path: String, + + /// Remote repository URL + #[arg()] + pub url: String, + + /// Optional filter to apply to the linked repository + #[arg()] + pub filter: Option, + + /// Target branch to link (defaults to HEAD) + #[arg(long = "target")] + pub target: Option, +} + +#[derive(Debug, clap::Parser)] +pub struct LinkFetchArgs { + /// Optional path to specific .josh-link.toml file (if not provided, fetches all) + #[arg()] + pub path: Option, +} + fn main() { env_logger::init(); let cli = Cli::parse(); @@ -253,6 +297,7 @@ fn main() { Command::Push(args) => handle_push(args), Command::Remote(args) => handle_remote(args), Command::Filter(args) => handle_filter(args), + Command::Link(args) => handle_link(args), }; if let Err(e) = result { @@ -723,7 +768,7 @@ fn handle_push(args: &PushArgs) -> anyhow::Result<()> { original_target, old_filtered_oid, local_commit, - false, // keep_orphans + josh::history::OrphansMode::Keep, None, // reparent_orphans &mut changes, // change_ids ) @@ -793,7 +838,345 @@ fn handle_push(args: &PushArgs) -> anyhow::Result<()> { Ok(()) } -fn handle_remote(args: &RemoteArgs) -> anyhow::Result<()> { +fn handle_link(args: &LinkArgs) -> Result<(), Box> { + match &args.command { + LinkCommand::Add(add_args) => handle_link_add(add_args), + LinkCommand::Fetch(fetch_args) => handle_link_fetch(fetch_args), + } +} + +fn handle_link_add(args: &LinkAddArgs) -> Result<(), Box> { + use josh::filter::tree; + use josh::{JoshLinkFile, Oid}; + + // Check if we're in a git repository + let repo = + git2::Repository::open_from_env().map_err(|e| format!("Not in a git repository: {}", e))?; + + // Validate the path (should not be empty and should be a valid path) + if args.path.is_empty() { + return Err("Path cannot be empty".into()); + } + + // Normalize the path by removing leading and trailing slashes + let normalized_path = args.path.trim_matches('/').to_string(); + + // Get the filter (default to ":/" if not provided) + let filter = args.filter.as_deref().unwrap_or(":/"); + + // Get the target branch (default to "HEAD" if not provided) + let target = args.target.as_deref().unwrap_or("HEAD"); + + // Parse the filter + let filter_obj = josh::filter::parse(filter) + .map_err(|e| format!("Failed to parse filter '{}': {}", filter, e))?; + + // Use git fetch shell command + let output = std::process::Command::new("git") + .args(&["fetch", &args.url, &target]) + .output() + .map_err(|e| format!("Failed to execute git fetch: {}", e))?; + + if !output.status.success() { + return Err(format!( + "git fetch failed: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + // Get the commit SHA from FETCH_HEAD + let fetch_head = repo + .find_reference("FETCH_HEAD") + .map_err(|e| format!("Failed to find FETCH_HEAD: {}", e))?; + let fetch_commit = fetch_head + .peel_to_commit() + .map_err(|e| format!("Failed to get FETCH_HEAD commit: {}", e))?; + let actual_commit_sha = fetch_commit.id(); + + // Get the current HEAD commit + let head_ref = repo + .head() + .map_err(|e| format!("Failed to get HEAD: {}", e))?; + let head_commit = head_ref + .peel_to_commit() + .map_err(|e| format!("Failed to get HEAD commit: {}", e))?; + let head_tree = head_commit + .tree() + .map_err(|e| format!("Failed to get HEAD tree: {}", e))?; + + // Create the JoshLinkFile with the actual commit SHA + let link_file = JoshLinkFile { + remote: args.url.clone(), + branch: target.to_string(), + filter: filter_obj, + commit: Oid::from(actual_commit_sha), + }; + + // Create the .josh-link.toml content + let link_content = + toml::to_string(&link_file).map_err(|e| format!("Failed to serialize link file: {}", e))?; + + // Create the blob for the .josh-link.toml file + let link_blob = repo + .blob(link_content.as_bytes()) + .map_err(|e| format!("Failed to create blob: {}", e))?; + + // Create the path for the .josh-link.toml file + let link_path = std::path::Path::new(&normalized_path).join(".josh-link.toml"); + + // Insert the .josh-link.toml file into the tree + let new_tree = tree::insert(&repo, &head_tree, &link_path, link_blob, 0o0100644) + .map_err(|e| format!("Failed to insert link file into tree: {}", e))?; + + // Create a new commit with the updated tree + let signature = if let Ok(time) = std::env::var("JOSH_COMMIT_TIME") { + git2::Signature::new( + "JOSH", + "josh@josh-project.dev", + &git2::Time::new( + time.parse() + .map_err(|e| format!("Failed to parse JOSH_COMMIT_TIME: {}", e))?, + 0, + ), + ) + .map_err(|e| format!("Failed to create signature: {}", e))? + } else { + repo.signature() + .map_err(|e| format!("Failed to get signature: {}", e))? + }; + + let new_commit = repo + .commit( + None, // Don't update any reference + &signature, + &signature, + &format!("Add link: {}", normalized_path), + &new_tree, + &[&head_commit], + ) + .map_err(|e| format!("Failed to create commit: {}", e))?; + + // Apply the :link filter to the new commit + let link_filter = + josh::filter::parse(":link").map_err(|e| format!("Failed to parse :link filter: {}", e))?; + + // Load the cache and create transaction + let repo_path = repo.path().parent().unwrap(); + + josh::cache_sled::sled_load(&repo_path).unwrap(); + let cache = std::sync::Arc::new( + josh::cache_stack::CacheStack::new() + .with_backend(josh::cache_sled::SledCacheBackend::default()) + .with_backend( + josh::cache_notes::NotesCacheBackend::new(&repo_path) + .map_err(|e| format!("Failed to create NotesCacheBackend: {}", e.0))?, + ), + ); + + // Open Josh transaction + let transaction = josh::cache::TransactionContext::from_env(cache.clone()) + .map_err(|e| format!("Failed TransactionContext::from_env: {}", e.0))? + .open(None) + .map_err(|e| format!("Failed TransactionContext::open: {}", e.0))?; + + let filtered_commit = + josh::filter_commit(&transaction, link_filter, new_commit, josh::filter::empty()) + .map_err(|e| format!("Failed to apply :link filter: {}", e))?; + + // Create the fixed branch name + let branch_name = "refs/josh/link"; + + // Create or update the branch reference + repo.reference(branch_name, filtered_commit, true, "josh link add") + .map_err(|e| format!("Failed to create branch '{}': {}", branch_name, e))?; + + println!( + "Added link '{}' with URL '{}', filter '{}', and target '{}'", + normalized_path, args.url, filter, target + ); + println!("Created branch: {}", branch_name); + + Ok(()) +} + +fn handle_link_fetch(args: &LinkFetchArgs) -> Result<(), Box> { + use josh::filter::tree; + use josh::{JoshLinkFile, Oid}; + + // Check if we're in a git repository + let repo = + git2::Repository::open_from_env().map_err(|e| format!("Not in a git repository: {}", e))?; + + // Get the current HEAD commit + let head_ref = repo + .head() + .map_err(|e| format!("Failed to get HEAD: {}", e))?; + let head_commit = head_ref + .peel_to_commit() + .map_err(|e| format!("Failed to get HEAD commit: {}", e))?; + let head_tree = head_commit + .tree() + .map_err(|e| format!("Failed to get HEAD tree: {}", e))?; + + let link_files = if let Some(path) = &args.path { + // Single path specified - find the .josh-link.toml file at that path + let link_path = std::path::Path::new(path).join(".josh-link.toml"); + let link_entry = head_tree + .get_path(&link_path) + .map_err(|e| format!("Failed to find .josh-link.toml at path '{}': {}", path, e))?; + + let link_blob = repo + .find_blob(link_entry.id()) + .map_err(|e| format!("Failed to find blob: {}", e))?; + + let link_content = std::str::from_utf8(link_blob.content()) + .map_err(|e| format!("Failed to parse link file content: {}", e))?; + + let link_file: JoshLinkFile = toml::from_str(link_content) + .map_err(|e| format!("Failed to parse .josh-link.toml: {}", e))?; + + vec![(std::path::PathBuf::from(path), link_file)] + } else { + // No path specified - find all .josh-link.toml files in the tree + josh::find_link_files(&repo, &head_tree) + .map_err(|e| format!("Failed to find link files: {}", e))? + }; + + if link_files.is_empty() { + return Err("No .josh-link.toml files found".into()); + } + + println!("Found {} link file(s) to fetch", link_files.len()); + + // Fetch from all the link files + let mut updated_link_files = Vec::new(); + for (path, mut link_file) in link_files { + println!("Fetching from link at path: {}", path.display()); + + // Use git fetch shell command + let output = std::process::Command::new("git") + .args(&["fetch", &link_file.remote, &link_file.branch]) + .output() + .map_err(|e| format!("Failed to execute git fetch: {}", e))?; + + if !output.status.success() { + return Err(format!( + "git fetch failed for path '{}': {}", + path.display(), + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + // Get the commit SHA from FETCH_HEAD + let fetch_head = repo + .find_reference("FETCH_HEAD") + .map_err(|e| format!("Failed to find FETCH_HEAD: {}", e))?; + let fetch_commit = fetch_head + .peel_to_commit() + .map_err(|e| format!("Failed to get FETCH_HEAD commit: {}", e))?; + let actual_commit_sha = fetch_commit.id(); + + // Update the link file with the new commit SHA + link_file.commit = Oid::from(actual_commit_sha); + updated_link_files.push((path, link_file)); + } + + // Create new tree with updated .josh-link.toml files + let mut new_tree = head_tree; + for (path, link_file) in &updated_link_files { + // Create the .josh-link.toml content + let link_content = toml::to_string(link_file) + .map_err(|e| format!("Failed to serialize link file: {}", e))?; + + // Create the blob for the .josh-link.toml file + let link_blob = repo + .blob(link_content.as_bytes()) + .map_err(|e| format!("Failed to create blob: {}", e))?; + + // Create the path for the .josh-link.toml file + let link_path = path.join(".josh-link.toml"); + + // Insert the updated .josh-link.toml file into the tree + new_tree = tree::insert(&repo, &new_tree, &link_path, link_blob, 0o0100644) + .map_err(|e| format!("Failed to insert link file into tree: {}", e))?; + } + + // Create a new commit with the updated tree + let signature = if let Ok(time) = std::env::var("JOSH_COMMIT_TIME") { + git2::Signature::new( + "JOSH", + "josh@josh-project.dev", + &git2::Time::new( + time.parse() + .map_err(|e| format!("Failed to parse JOSH_COMMIT_TIME: {}", e))?, + 0, + ), + ) + .map_err(|e| format!("Failed to create signature: {}", e))? + } else { + repo.signature() + .map_err(|e| format!("Failed to get signature: {}", e))? + }; + + let new_commit = repo + .commit( + None, // Don't update any reference + &signature, + &signature, + &format!( + "Update links: {}", + updated_link_files + .iter() + .map(|(p, _)| p.display().to_string()) + .collect::>() + .join(", ") + ), + &new_tree, + &[&head_commit], + ) + .map_err(|e| format!("Failed to create commit: {}", e))?; + + // Apply the :link filter to the new commit + let link_filter = + josh::filter::parse(":link").map_err(|e| format!("Failed to parse :link filter: {}", e))?; + + // Load the cache and create transaction + let repo_path = repo.path().parent().unwrap(); + josh::cache_sled::sled_load(&repo_path).unwrap(); + let cache = std::sync::Arc::new( + josh::cache_stack::CacheStack::new() + .with_backend(josh::cache_sled::SledCacheBackend::default()) + .with_backend( + josh::cache_notes::NotesCacheBackend::new(&repo_path) + .map_err(|e| format!("Failed to create NotesCacheBackend: {}", e.0))?, + ), + ); + + // Open Josh transaction + let transaction = josh::cache::TransactionContext::from_env(cache.clone()) + .map_err(|e| format!("Failed TransactionContext::from_env: {}", e.0))? + .open(None) + .map_err(|e| format!("Failed TransactionContext::open: {}", e.0))?; + let filtered_commit = + josh::filter_commit(&transaction, link_filter, new_commit, josh::filter::empty()) + .map_err(|e| format!("Failed to apply :link filter: {}", e))?; + + // Create the fixed branch name + let branch_name = "refs/josh/link"; + + // Create or update the branch reference + repo.reference(branch_name, filtered_commit, true, "josh link fetch") + .map_err(|e| format!("Failed to create branch '{}': {}", branch_name, e))?; + + println!("Updated {} link file(s)", updated_link_files.len()); + println!("Created branch: {}", branch_name); + + Ok(()) +} + +fn handle_remote(args: &RemoteArgs) -> Result<(), Box> { match &args.command { RemoteCommand::Add(add_args) => handle_remote_add(add_args), } diff --git a/josh-core/Cargo.toml b/josh-core/Cargo.toml index 12c8fa079..113f8bb49 100644 --- a/josh-core/Cargo.toml +++ b/josh-core/Cargo.toml @@ -14,6 +14,7 @@ backtrace = "0.3.76" bitvec = "1.0.1" git-version = "0.3.9" git2 = { workspace = true } +gix-submodule = "0.20.0" gix-object = "0.50.2" gix-config = "0.46.0" gix-hash = { workspace = true } @@ -22,6 +23,7 @@ hex = { workspace = true } indoc = "2.0.7" itertools = "0.14.0" lazy_static = { workspace = true } +toml = { workspace = true } log = { workspace = true } percent-encoding = "2.3.1" pest = "2.8.3" @@ -35,3 +37,4 @@ serde_json = { workspace = true } serde_yaml = { workspace = true } sled = "0.34.7" tracing = { workspace = true } +unindent = "0.2.3" diff --git a/josh-core/src/changes.rs b/josh-core/src/changes.rs index a99e5ff9b..a375f9fcd 100644 --- a/josh-core/src/changes.rs +++ b/josh-core/src/changes.rs @@ -130,11 +130,7 @@ pub fn changes_to_refs( change.commit ))); } - } else { - return Err(josh_error(&format!( - "rejecting to push {:?} without id", - change.commit - ))); + seen.push(id); } } diff --git a/josh-core/src/filter/grammar.pest b/josh-core/src/filter/grammar.pest index 0a6a28bea..b249b4d76 100644 --- a/josh-core/src/filter/grammar.pest +++ b/josh-core/src/filter/grammar.pest @@ -24,6 +24,9 @@ filter_spec = { ( filter_group | filter_message | filter_rev + | filter_from + | filter_concat + | filter_unapply | filter_join | filter_replace | filter_squash @@ -51,6 +54,33 @@ filter_rev = { ~ ")" } +filter_from = { + CMD_START ~ "from" ~ "(" + ~ NEWLINE* + ~ (rev ~ filter_spec)? + ~ (CMD_SEP+ ~ (rev ~ filter_spec))* + ~ NEWLINE* + ~ ")" +} + +filter_concat = { + CMD_START ~ "from" ~ "(" + ~ NEWLINE* + ~ (rev ~ filter_spec)? + ~ (CMD_SEP+ ~ (rev ~ filter_spec))* + ~ NEWLINE* + ~ ")" +} + +filter_unapply = { + CMD_START ~ "unapply" ~ "(" + ~ NEWLINE* + ~ (rev ~ filter_spec)? + ~ (CMD_SEP+ ~ (rev ~ filter_spec))* + ~ NEWLINE* + ~ ")" +} + filter_join = { CMD_START ~ "join" ~ "(" ~ NEWLINE* @@ -60,7 +90,6 @@ filter_join = { ~ ")" } - filter_replace = { CMD_START ~ "replace" ~ "(" ~ NEWLINE* diff --git a/josh-core/src/filter/mod.rs b/josh-core/src/filter/mod.rs index 25e61d316..e40d5f53e 100644 --- a/josh-core/src/filter/mod.rs +++ b/josh-core/src/filter/mod.rs @@ -68,6 +68,7 @@ impl std::fmt::Debug for Filter { #[derive(Debug)] pub struct Apply<'a> { tree: git2::Tree<'a>, + commit: git2::Oid, pub author: Option<(String, String)>, pub committer: Option<(String, String)>, pub message: Option, @@ -77,6 +78,7 @@ impl<'a> Clone for Apply<'a> { fn clone(&self) -> Self { Apply { tree: self.tree.clone(), + commit: self.commit.clone(), author: self.author.clone(), committer: self.committer.clone(), message: self.message.clone(), @@ -89,6 +91,7 @@ impl<'a> Apply<'a> { Apply { tree, author: None, + commit: git2::Oid::zero(), committer: None, message: None, } @@ -103,6 +106,7 @@ impl<'a> Apply<'a> { Apply { tree, author, + commit: git2::Oid::zero(), committer, message, } @@ -124,6 +128,7 @@ impl<'a> Apply<'a> { Ok(Apply { tree, + commit: commit.id(), author, committer, message, @@ -134,6 +139,7 @@ impl<'a> Apply<'a> { Apply { tree: self.tree, author: Some(author), + commit: self.commit, committer: self.committer, message: self.message, } @@ -143,6 +149,7 @@ impl<'a> Apply<'a> { Apply { tree: self.tree, author: self.author, + commit: self.commit, committer: Some(committer), message: self.message, } @@ -152,6 +159,7 @@ impl<'a> Apply<'a> { Apply { tree: self.tree, author: self.author, + commit: self.commit, committer: self.committer, message: Some(message), } @@ -161,6 +169,7 @@ impl<'a> Apply<'a> { Apply { tree, author: self.author, + commit: self.commit, committer: self.committer, message: self.message, } @@ -256,6 +265,11 @@ enum Op { Empty, Fold, Paths, + Adapt(String), + Link(String), + Unlink, + Export, + Embed(std::path::PathBuf), // We use BTreeMap rather than HashMap to guarantee deterministic results when // converting to Filter @@ -282,10 +296,15 @@ enum Op { Prefix(std::path::PathBuf), Subdir(std::path::PathBuf), Workspace(std::path::PathBuf), + Lookup(std::path::PathBuf), + Lookup2(git2::Oid), Pattern(String), Message(String), + HistoryConcat(LazyRef, Filter), + Unapply(LazyRef, Filter), + Compose(Vec), Chain(Filter, Filter), Subtract(Filter, Filter), @@ -410,6 +429,20 @@ fn lazy_refs2(op: &Op) -> Vec { av } Op::Rev(filters) => lazy_refs2(&Op::Join(filters.clone())), + Op::HistoryConcat(r, _) => { + let mut lr = Vec::new(); + if let LazyRef::Lazy(s) = r { + lr.push(s.to_owned()); + } + lr + } + Op::Unapply(r, _) => { + let mut lr = Vec::new(); + if let LazyRef::Lazy(s) = r { + lr.push(s.to_owned()); + } + lr + } Op::Join(filters) => { let mut lr = lazy_refs2(&Op::Compose(filters.values().copied().collect())); lr.extend(filters.keys().filter_map(|x| { @@ -470,6 +503,32 @@ fn resolve_refs2(refs: &std::collections::HashMap, op: &Op) - .collect(); Op::Rev(lr) } + Op::HistoryConcat(r, filter) => { + let f = resolve_refs(refs, *filter); + let resolved_ref = if let LazyRef::Lazy(s) = r { + if let Some(res) = refs.get(s) { + LazyRef::Resolved(*res) + } else { + r.clone() + } + } else { + r.clone() + }; + Op::HistoryConcat(resolved_ref, f) + } + Op::Unapply(r, f) => { + let f = resolve_refs(refs, *f); + let resolved_ref = if let LazyRef::Lazy(s) = r { + if let Some(res) = refs.get(s) { + LazyRef::Resolved(*res) + } else { + r.clone() + } + } else { + r.clone() + }; + Op::Unapply(resolved_ref, f) + } Op::Join(filters) => { let lr = filters .iter() @@ -559,6 +618,12 @@ fn spec2(op: &Op) -> String { Op::Workspace(path) => { format!(":workspace={}", parse::quote_if(&path.to_string_lossy())) } + Op::Lookup(path) => { + format!(":lookup={}", parse::quote_if(&path.to_string_lossy())) + } + Op::Lookup2(oid) => { + format!(":lookup2={}", oid.to_string()) + } Op::RegexReplace(replacements) => { let v = replacements .iter() @@ -593,11 +658,18 @@ fn spec2(op: &Op) -> String { } Op::Linear => ":linear".to_string(), Op::Unsign => ":unsign".to_string(), + Op::Adapt(adapter) => format!(":adapt={}", adapter), + Op::Link(mode) => format!(":link={}", mode), + Op::Export => ":export".to_string(), + Op::Unlink => ":unlink".to_string(), Op::Subdir(path) => format!(":/{}", parse::quote_if(&path.to_string_lossy())), Op::File(path) => format!("::{}", parse::quote_if(&path.to_string_lossy())), Op::Prune => ":prune=trivial-merge".to_string(), Op::Prefix(path) => format!(":prefix={}", parse::quote_if(&path.to_string_lossy())), Op::Pattern(pattern) => format!("::{}", parse::quote_if(pattern)), + Op::Embed(path) => { + format!(":embed={}", parse::quote_if(&path.to_string_lossy()),) + } Op::Author(author, email) => { format!(":author={};{}", parse::quote(author), parse::quote(email)) } @@ -611,6 +683,12 @@ fn spec2(op: &Op) -> String { Op::Message(m) => { format!(":{}", parse::quote(m)) } + Op::HistoryConcat(r, filter) => { + format!(":concat({}{})", r.to_string(), spec(*filter)) + } + Op::Unapply(r, filter) => { + format!(":unapply({}{})", r.to_string(), spec(*filter)) + } Op::Hook(hook) => { format!(":hook={}", parse::quote(hook)) } @@ -785,7 +863,7 @@ fn apply_to_commit2( result, old, *combine_tip, - false, + history::OrphansMode::Keep, None, &mut None, )?; @@ -849,6 +927,69 @@ fn apply_to_commit2( apply(transaction, nf, Apply::from_commit(commit)?)? } + Op::Lookup(lookup_path) => { + let lookup_commit = if let Some(lookup_commit) = + apply_to_commit2(&Op::Subdir(lookup_path.clone()), &commit, transaction)? + { + lookup_commit + } else { + return Ok(None); + }; + + let op = Op::Lookup2(lookup_commit); + + if let Some(start) = transaction.get(to_filter(op), commit.id()) { + transaction.insert(filter, commit.id(), start, true); + return Ok(Some(start)); + } else { + return Ok(None); + } + } + + Op::Lookup2(lookup_commit_id) => { + let lookup_commit = repo.find_commit(*lookup_commit_id)?; + for parent in lookup_commit.parents() { + let lookup_tree = lookup_commit.tree_id(); + let cw = get_filter( + repo, + &repo.find_tree(lookup_tree)?, + &std::path::PathBuf::new().join(commit.id().to_string()), + ); + if cw != filter::empty() { + if let Some(start) = + apply_to_commit2(&Op::Lookup2(parent.id()), &commit, transaction)? + { + transaction.insert(filter, commit.id(), start, true); + return Ok(Some(start)); + } else { + return Ok(None); + } + } + break; + } + let lookup_tree = lookup_commit.tree_id(); + let cw = get_filter( + repo, + &repo.find_tree(lookup_tree)?, + &std::path::PathBuf::new().join(commit.id().to_string()), + ); + + if cw == filter::empty() { + // FIXME empty filter or no entry in table? + for parent in commit.parents() { + if let Some(start) = apply_to_commit2(&op, &parent, transaction)? { + transaction.insert(filter, commit.id(), start, true); + return Ok(Some(start)); + } else { + return Ok(None); + } + } + return Ok(None); + } + + Apply::from_commit(commit)? + .with_tree(apply(transaction, cw, Apply::from_commit(commit)?)?.into_tree()) + } Op::Squash(Some(ids)) => { if let Some(sq) = ids.get(&LazyRef::Resolved(commit.id())) { let oid = if let Some(oid) = @@ -962,6 +1103,163 @@ fn apply_to_commit2( )) .transpose(); } + Op::Export => { + let filtered_parent_ids = { + commit + .parents() + .map(|x| transaction.get(filter, x.id())) + .collect::>() + }; + + let mut filtered_parent_ids: Vec = + some_or!(filtered_parent_ids, { return Ok(None) }); + + // TODO: remove all parents that don't have a .josh-link.toml + + // let mut ok = true; + // filtered_parent_ids.retain(|c| { + // if let Ok(c) = repo.find_commit(*c) { + // c.tree_id() != new_tree.id() + // } else { + // ok = false; + // false + // } + // }); + + // if !ok { + // return Err(josh_error("missing commit")); + // } + + if let Some(link_file) = read_josh_link( + repo, + &commit.tree()?, + &std::path::PathBuf::new(), + ".josh-link.toml", + ) { + if filtered_parent_ids.contains(&link_file.commit.0) { + while filtered_parent_ids[0] != link_file.commit.0 { + filtered_parent_ids.rotate_right(1); + } + } + } + + return Some(history::create_filtered_commit( + commit, + filtered_parent_ids, + apply(transaction, filter, Apply::from_commit(commit)?)?, + transaction, + filter, + )) + .transpose(); + } + Op::Unlink => { + let filtered_parent_ids = { + commit + .parents() + .map(|x| transaction.get(filter, x.id())) + .collect::>() + }; + + let mut filtered_parent_ids: Vec = + some_or!(filtered_parent_ids, { return Ok(None) }); + + let mut link_parents = vec![]; + for (link_path, link_file) in find_link_files(&repo, &commit.tree()?)?.into_iter() { + if let Some(cmt) = + transaction.get(to_filter(Op::Prefix(link_path)), link_file.commit.0) + { + link_parents.push(cmt); + } else { + return Ok(None); + } + } + + let new_tree = apply(transaction, filter, Apply::from_commit(commit)?)?; + + filtered_parent_ids.retain(|c| !link_parents.contains(c)); + + return Some(history::create_filtered_commit( + commit, + filtered_parent_ids, + new_tree, + transaction, + filter, + )) + .transpose(); + } + Op::Link(mode) if mode == "embedded" => { + let normal_parents = commit + .parent_ids() + .map(|parent| transaction.get(filter, parent)) + .collect::>>(); + + let normal_parents = some_or!(normal_parents, { return Ok(None) }); + + let mut roots = get_link_roots(repo, transaction, &commit.tree()?)?; + + if let Some(parent) = commit.parents().next() { + roots.retain(|root| { + if let (Ok(a), Ok(b)) = ( + commit.tree().and_then(|x| x.get_path(&root)), + parent.tree().and_then(|x| x.get_path(&root)), + ) && a.id() == b.id() + { + false + } else { + true + } + }); + }; + + let v = links_from_roots(repo, &commit.tree()?, roots)?; + + let extra_parents = { + let mut extra_parents = vec![]; + for (root, _link_file) in v { + let embeding = some_or!( + apply_to_commit2( + &Op::Chain( + to_filter(Op::Message("{commit}".to_string())), + to_filter(Op::File(root.join(".josh-link.toml"))) + ), + &commit, + transaction + )?, + { + return Ok(None); + } + ); + + let f = to_filter(Op::Embed(root)); + /* let f = filter::chain(link_file.filter, to_filter(Op::Prefix(root))); */ + /* let scommit = repo.find_commit(link_file.commit.0)?; */ + + let embeding = repo.find_commit(embeding)?; + let r = some_or!(apply_to_commit2(&to_op(f), &embeding, transaction)?, { + return Ok(None); + }); + + extra_parents.push(r); + } + + extra_parents + }; + + let filtered_tree = apply(transaction, filter, Apply::from_commit(commit)?)?; + let filtered_parent_ids = normal_parents + .into_iter() + .chain(extra_parents) + .collect::>(); + + return Some(history::create_filtered_commit( + commit, + filtered_parent_ids.clone(), + filtered_tree, + transaction, + filter, + )) + .transpose(); + } Op::Workspace(ws_path) => { if let Some((redirect, _)) = resolve_workspace_redirect(repo, &commit.tree()?, ws_path) { @@ -1025,6 +1323,71 @@ fn apply_to_commit2( return per_rev_filter(transaction, commit, filter, commit_filter, parent_filters); } + Op::Unapply(target, uf) => { + if let LazyRef::Resolved(target) = target { + /* dbg!(target); */ + let target = repo.find_commit(*target)?; + if let Some(parent) = target.parents().next() { + let ptree = apply(transaction, *uf, Apply::from_commit(&parent)?)?; + if let Some(link) = read_josh_link( + repo, + &ptree.tree(), + &std::path::PathBuf::new(), + ".josh-link.toml", + ) { + if commit.id() == link.commit.0 { + let unapply = + to_filter(Op::Unapply(LazyRef::Resolved(parent.id()), *uf)); + let r = some_or!(transaction.get(unapply, link.commit.0), { + return Ok(None); + }); + transaction.insert(filter, commit.id(), r, true); + return Ok(Some(r)); + } + } + } + } else { + return Err(josh_error("unresolved lazy ref")); + } + /* dbg!("FALLTHROUGH"); */ + apply( + transaction, + filter, + Apply::from_commit(commit)?, /* Apply::from_commit(commit)?.with_parents(filtered_parent_ids), */ + )? + /* Apply::from_commit(commit)? */ + } + Op::Embed(path) => { + let subdir = to_filter(Op::Subdir(path.clone())); + let unapply = to_filter(Op::Unapply(LazyRef::Resolved(commit.id()), subdir)); + + /* dbg!("embed"); */ + /* dbg!(&path); */ + if let Some(link) = read_josh_link(repo, &commit.tree()?, &path, ".josh-link.toml") { + /* dbg!(&link); */ + let r = some_or!(transaction.get(unapply, link.commit.0), { + return Ok(None); + }); + transaction.insert(filter, commit.id(), r, true); + return Ok(Some(r)); + } else { + return Ok(Some(git2::Oid::zero())); + } + } + + Op::HistoryConcat(c, f) => { + if let LazyRef::Resolved(c) = c { + let a = apply_to_commit2(&to_op(*f), &repo.find_commit(*c)?, transaction)?; + let a = some_or!(a, { return Ok(None) }); + if commit.id() == a { + transaction.insert(filter, commit.id(), *c, true); + return Ok(Some(*c)); + } + } else { + return Err(josh_error("unresolved lazy ref")); + } + Apply::from_commit(commit)? + } _ => apply(transaction, filter, Apply::from_commit(commit)?)?, }; @@ -1059,27 +1422,236 @@ pub fn apply<'a>( apply2(transaction, &to_op(filter), x) } +fn extract_submodule_commits<'a>( + repo: &'a git2::Repository, + tree: &git2::Tree<'a>, +) -> JoshResult> { + // Get .gitmodules blob from the tree + let gitmodules_content = tree::get_blob(repo, tree, std::path::Path::new(".gitmodules")); + + if gitmodules_content.is_empty() { + // No .gitmodules file, return empty map + return Ok(std::collections::BTreeMap::new()); + } + + // Parse submodule entries using parse_gitmodules + let submodule_entries = match parse_gitmodules(&gitmodules_content) { + Ok(entries) => entries, + Err(_) => { + // If parsing fails, return empty map + return Ok(std::collections::BTreeMap::new()); + } + }; + + let mut submodule_commits: std::collections::BTreeMap< + std::path::PathBuf, + (git2::Oid, ParsedSubmoduleEntry), + > = std::collections::BTreeMap::new(); + + for parsed in submodule_entries { + let submodule_path = parsed.path.clone(); + // Get the submodule entry from the tree + if let Ok(entry) = tree.get_path(&submodule_path) { + // Check if this is a commit (submodule) entry + if entry.kind() == Some(git2::ObjectType::Commit) { + // Get the commit OID stored in the tree entry + let commit_oid = entry.id(); + // Store OID and parsed entry metadata + submodule_commits.insert(submodule_path, (commit_oid, parsed)); + } + } + } + + Ok(submodule_commits) +} + +fn get_link_roots<'a>( + _repo: &'a git2::Repository, + transaction: &'a cache::Transaction, + tree: &'a git2::Tree<'a>, +) -> JoshResult> { + let link_filter = to_filter(Op::Pattern("**/.josh-link.toml".to_string())); + let link_tree = apply(transaction, link_filter, Apply::from_tree(tree.clone()))?; + + let mut roots = vec![]; + link_tree + .tree() + .walk(git2::TreeWalkMode::PreOrder, |root, entry| { + let root = root.trim_matches('/'); + let root = std::path::PathBuf::from(root); + if entry.name() == Some(".josh-link.toml") { + roots.push(root); + } + 0 + })?; + + Ok(roots) +} + +fn links_from_roots<'a>( + repo: &'a git2::Repository, + tree: &git2::Tree<'a>, + roots: Vec, +) -> JoshResult> { + let mut v = vec![]; + for root in roots { + if let Some(link_file) = read_josh_link(repo, tree, &root, ".josh-link.toml") { + v.push((root, link_file)); + } + } + Ok(v) +} + +fn read_josh_link<'a>( + repo: &'a git2::Repository, + tree: &git2::Tree<'a>, + root: &std::path::Path, + filename: &str, +) -> Option { + let link_path = root.join(filename); + let link_entry = tree.get_path(&link_path).ok()?; + let link_blob = repo.find_blob(link_entry.id()).ok()?; + let b = std::str::from_utf8(link_blob.content()) + .map_err(|e| josh_error(&format!("invalid utf8 in {}: {}", filename, e))) + .ok()?; + let link_file: JoshLinkFile = toml::from_str(b) + .map_err(|e| josh_error(&format!("invalid toml in {}: {}", filename, e))) + .ok()?; + Some(link_file) +} + fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> JoshResult> { let repo = transaction.repo(); match op { Op::Nop => Ok(x), Op::Empty => Ok(x.with_tree(tree::empty(repo))), Op::Fold => Ok(x), - Op::Squash(None) => Ok(x), + Op::Squash(..) => Ok(x), Op::Author(author, email) => Ok(x.with_author((author.clone(), email.clone()))), Op::Committer(author, email) => Ok(x.with_committer((author.clone(), email.clone()))), - Op::Message(m) => Ok(x.with_message( - // Pass the message through `strfmt` to enable future extensions - strfmt::strfmt( - m, - &std::collections::HashMap::::new(), - )?, - )), - Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")), + Op::Message(m) => { + let tree_id = x.tree().id().to_string(); + let commit_id = x.commit.to_string(); + let mut hm = std::collections::HashMap::::new(); + hm.insert("tree".to_string(), &tree_id); + hm.insert("commit".to_string(), &commit_id); + Ok(x.with_message( + // Pass the message through `strfmt` to enable future extensions + strfmt::strfmt(m, &hm)?, + )) + } + Op::HistoryConcat(..) => Ok(x), Op::Linear => Ok(x), Op::Prune => Ok(x), Op::Unsign => Ok(x), + Op::Adapt(adapter) => { + let mut result_tree = x.tree().clone(); + match adapter.as_ref() { + "submodules" => { + // Extract submodule commits + let submodule_commits = extract_submodule_commits(repo, &result_tree)?; + + // Process each submodule commit + for (submodule_path, (commit_oid, meta)) in submodule_commits { + let prefix_filter = to_filter(Op::Nop); + let link_file = JoshLinkFile { + remote: meta.url.clone(), + filter: prefix_filter, + branch: "HEAD".to_string(), + commit: Oid(commit_oid), + }; + result_tree = tree::insert( + repo, + &result_tree, + &submodule_path.join(".josh-link.toml"), + repo.blob(toml::to_string(&link_file)?.as_bytes())?, + 0o0100644, + )?; + } + + // Remove .gitmodules file by setting it to zero OID + result_tree = tree::insert( + repo, + &result_tree, + std::path::Path::new(".gitmodules"), + git2::Oid::zero(), + 0o0100644, + )?; + } + _ => return Err(josh_error(&format!("unknown adapter {:?}", adapter))), + } + + // Remove .gitmodules file by setting it to zero OID + result_tree = tree::insert( + repo, + &result_tree, + std::path::Path::new(".gitmodules"), + git2::Oid::zero(), + 0o0100644, + )?; + + Ok(x.with_tree(result_tree)) + } + Op::Export => { + let tree = x.tree().clone(); + Ok(x.with_tree(tree::insert( + repo, + &tree, + &std::path::Path::new(".josh-link.toml"), + git2::Oid::zero(), + 0o0100644, + )?)) + } + Op::Unlink => { + let mut result_tree = x.tree.clone(); + for (link_path, link_file) in find_link_files(&repo, &result_tree)?.iter() { + result_tree = + tree::insert(repo, &result_tree, &link_path, git2::Oid::zero(), 0o0100644)?; + result_tree = tree::insert( + repo, + &result_tree, + &link_path.join(".josh-link.toml"), + repo.blob(toml::to_string(&link_file)?.as_bytes())?, + 0o0100644, + )?; + } + Ok(x.with_tree(result_tree)) + } + Op::Link(_) => { + let roots = get_link_roots(repo, transaction, &x.tree())?; + let v = links_from_roots(repo, &x.tree(), roots)?; + let mut result_tree = x.tree().clone(); + + for (root, link_file) in v { + let submodule_tree = repo.find_commit(link_file.commit.0)?.tree()?; + let submodule_tree = apply( + transaction, + link_file.filter, + Apply::from_tree(submodule_tree), + ) + .unwrap(); + + result_tree = tree::insert( + repo, + &result_tree, + &root, + submodule_tree.tree().id(), + 0o0040000, // Tree mode + )?; + result_tree = tree::insert( + repo, + &result_tree, + &root.join(".josh-link.toml"), + repo.blob(toml::to_string(&link_file)?.as_bytes())?, + 0o0100644, + )?; + } + + Ok(x.with_tree(result_tree)) + } Op::Rev(_) => Err(josh_error("not applicable to tree")), + Op::Lookup(_) => Err(josh_error("not applicable to tree")), + Op::Lookup2(_) => Err(josh_error("not applicable to tree")), Op::Join(_) => Err(josh_error("not applicable to tree")), Op::RegexReplace(replacements) => { let mut t = x.tree().clone(); @@ -1174,6 +1746,23 @@ fn apply2<'a>(transaction: &'a cache::Transaction, op: &Op, x: Apply<'a>) -> Jos } Op::Hook(_) => Err(josh_error("not applicable to tree")), + Op::Embed(..) => Err(josh_error("not applicable to tree")), + Op::Unapply(target, uf) => { + if let LazyRef::Resolved(target) = target { + let target = repo.find_commit(*target)?; + let target = git2::Oid::from_str(target.message().unwrap())?; + let target = repo.find_commit(target)?; + /* dbg!(&uf); */ + Ok(Apply::from_tree(filter::unapply( + transaction, + *uf, + x.tree().clone(), + target.tree()?, + )?)) + } else { + return Err(josh_error("unresolved lazy ref")); + } + } Op::Pin(_) => Ok(x), } } diff --git a/josh-core/src/filter/opt.rs b/josh-core/src/filter/opt.rs index 3b7d177c4..b6a3c1469 100644 --- a/josh-core/src/filter/opt.rs +++ b/josh-core/src/filter/opt.rs @@ -499,8 +499,10 @@ pub fn invert(filter: Filter) -> JoshResult { Op::Message(..) => Some(Op::Nop), Op::Linear => Some(Op::Nop), Op::Prune => Some(Op::Prune), + Op::Export => Some(Op::Export), Op::Unsign => Some(Op::Unsign), Op::Empty => Some(Op::Empty), + Op::Link(..) => Some(Op::Unlink), Op::Subdir(path) => Some(Op::Prefix(path)), Op::File(path) => Some(Op::File(path)), Op::Prefix(path) => Some(Op::Subdir(path)), @@ -530,7 +532,7 @@ pub fn invert(filter: Filter) -> JoshResult { .collect::>>()?, ), Op::Exclude(filter) => Op::Exclude(invert(filter)?), - _ => return Err(josh_error("no invert")), + _ => return Err(josh_error(&format!("no invert {:?}", filter))), }); let result = optimize(result); diff --git a/josh-core/src/filter/parse.rs b/josh-core/src/filter/parse.rs index 3709b94a9..da7488708 100644 --- a/josh-core/src/filter/parse.rs +++ b/josh-core/src/filter/parse.rs @@ -10,6 +10,7 @@ fn make_op(args: &[&str]) -> JoshResult { ["author", author, email] => Ok(Op::Author(author.to_string(), email.to_string())), ["committer", author, email] => Ok(Op::Committer(author.to_string(), email.to_string())), ["workspace", arg] => Ok(Op::Workspace(Path::new(arg).to_owned())), + ["lookup", arg] => Ok(Op::Lookup(Path::new(arg).to_owned())), ["prefix"] => Err(josh_error(indoc!( r#" Filter ":prefix" requires an argument. @@ -52,6 +53,12 @@ fn make_op(args: &[&str]) -> JoshResult { "# ))), ["unsign"] => Ok(Op::Unsign), + ["unlink"] => Ok(Op::Unlink), + ["adapt", adapter] => Ok(Op::Adapt(adapter.to_string())), + ["link"] => Ok(Op::Link("embedded".to_string())), + ["link", mode] => Ok(Op::Link(mode.to_string())), + ["embed", path] => Ok(Op::Embed(Path::new(path).to_owned())), + ["export"] => Ok(Op::Export), ["PATHS"] => Ok(Op::Paths), ["INDEX"] => Ok(Op::Index), ["INVERT"] => Ok(Op::Invert), @@ -146,6 +153,42 @@ fn parse_item(pair: pest::iterators::Pair) -> JoshResult { Ok(Op::Rev(hm)) } + Rule::filter_from => { + let v: Vec<_> = pair.into_inner().map(|x| x.as_str()).collect(); + + if v.len() == 2 { + let oid = LazyRef::parse(v[0])?; + let filter = parse(v[1])?; + Ok(Op::Chain( + filter, + filter::to_filter(Op::HistoryConcat(oid, filter)), + )) + } else { + Err(josh_error("wrong argument count for :from")) + } + } + Rule::filter_concat => { + let v: Vec<_> = pair.into_inner().map(|x| x.as_str()).collect(); + + if v.len() == 2 { + let oid = LazyRef::parse(v[0])?; + let filter = parse(v[1])?; + Ok(Op::HistoryConcat(oid, filter)) + } else { + Err(josh_error("wrong argument count for :concat")) + } + } + Rule::filter_unapply => { + let v: Vec<_> = pair.into_inner().map(|x| x.as_str()).collect(); + + if v.len() == 2 { + let oid = LazyRef::parse(v[0])?; + let filter = parse(v[1])?; + Ok(Op::Unapply(oid, filter)) + } else { + Err(josh_error("wrong argument count for :unapply")) + } + } Rule::filter_replace => { let replacements = pair .into_inner() diff --git a/josh-core/src/filter/persist.rs b/josh-core/src/filter/persist.rs index 1f8796788..61bba42b3 100644 --- a/josh-core/src/filter/persist.rs +++ b/josh-core/src/filter/persist.rs @@ -205,6 +205,10 @@ impl InMemoryBuilder { let blob = self.write_blob(path.to_string_lossy().as_bytes()); push_blob_entries(&mut entries, [("file", blob)]); } + Op::Embed(path) => { + let blob = self.write_blob(path.to_string_lossy().as_bytes()); + push_blob_entries(&mut entries, [("embed", blob)]); + } Op::Pattern(pattern) => { let blob = self.write_blob(pattern.as_bytes()); push_blob_entries(&mut entries, [("pattern", blob)]); @@ -225,10 +229,26 @@ impl InMemoryBuilder { let blob = self.write_blob(b""); push_blob_entries(&mut entries, [("empty", blob)]); } + Op::Export => { + let blob = self.write_blob(b""); + push_blob_entries(&mut entries, [("export", blob)]); + } Op::Paths => { let blob = self.write_blob(b""); push_blob_entries(&mut entries, [("paths", blob)]); } + Op::Link(mode) => { + let blob = self.write_blob(mode.as_bytes()); + push_blob_entries(&mut entries, [("link", blob)]); + } + Op::Adapt(mode) => { + let blob = self.write_blob(mode.as_bytes()); + push_blob_entries(&mut entries, [("adapt", blob)]); + } + Op::Unlink => { + let blob = self.write_blob(b""); + push_blob_entries(&mut entries, [("unlink", blob)]); + } Op::Invert => { let blob = self.write_blob(b""); push_blob_entries(&mut entries, [("invert", blob)]); @@ -275,6 +295,14 @@ impl InMemoryBuilder { let params_tree = self.build_rev_params(&v)?; push_tree_entries(&mut entries, [("join", params_tree)]); } + Op::HistoryConcat(lr, f) => { + let params_tree = self.build_rev_params(&[(lr.to_string(), *f)])?; + push_tree_entries(&mut entries, [("concat", params_tree)]); + } + Op::Unapply(lr, f) => { + let params_tree = self.build_rev_params(&[(lr.to_string(), *f)])?; + push_tree_entries(&mut entries, [("unapply", params_tree)]); + } Op::Squash(Some(ids)) => { let mut v = ids .iter() @@ -292,6 +320,7 @@ impl InMemoryBuilder { let blob = self.write_blob(hook.as_bytes()); push_blob_entries(&mut entries, [("hook", blob)]); } + &Op::Lookup(_) | &Op::Lookup2(_) => todo!(), } let tree = gix_object::Tree { entries }; @@ -353,6 +382,22 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { let _ = repo.find_blob(entry.id())?; Ok(Op::Paths) } + "export" => { + let _ = repo.find_blob(entry.id())?; + Ok(Op::Export) + } + "link" => { + let blob = repo.find_blob(entry.id())?; + Ok(Op::Link(std::str::from_utf8(blob.content())?.to_string())) + } + "adapt" => { + let blob = repo.find_blob(entry.id())?; + Ok(Op::Adapt(std::str::from_utf8(blob.content())?.to_string())) + } + "unlink" => { + let _ = repo.find_blob(entry.id())?; + Ok(Op::Unlink) + } "invert" => { let _ = repo.find_blob(entry.id())?; Ok(Op::Invert) @@ -438,6 +483,11 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { let path = std::str::from_utf8(blob.content())?; Ok(Op::File(std::path::PathBuf::from(path))) } + "embed" => { + let blob = repo.find_blob(entry.id())?; + let path = std::str::from_utf8(blob.content())?; + Ok(Op::Embed(std::path::PathBuf::from(path))) + } "pattern" => { let blob = repo.find_blob(entry.id())?; let pattern = std::str::from_utf8(blob.content())?.to_string(); @@ -572,6 +622,50 @@ fn from_tree2(repo: &git2::Repository, tree_oid: git2::Oid) -> JoshResult { } Ok(Op::Join(filters)) } + "concat" => { + let concat_tree = repo.find_tree(entry.id())?; + let entry = concat_tree + .get(0) + .ok_or_else(|| josh_error("concat: missing entry"))?; + let inner_tree = repo.find_tree(entry.id())?; + let key_blob = repo.find_blob( + inner_tree + .get_name("o") + .ok_or_else(|| josh_error("concat: missing key"))? + .id(), + )?; + let filter_tree = repo.find_tree( + inner_tree + .get_name("f") + .ok_or_else(|| josh_error("concat: missing filter"))? + .id(), + )?; + let key = std::str::from_utf8(key_blob.content())?.to_string(); + let filter = from_tree2(repo, filter_tree.id())?; + Ok(Op::HistoryConcat(LazyRef::parse(&key)?, to_filter(filter))) + } + "unapply" => { + let concat_tree = repo.find_tree(entry.id())?; + let entry = concat_tree + .get(0) + .ok_or_else(|| josh_error("concat: missing entry"))?; + let inner_tree = repo.find_tree(entry.id())?; + let key_blob = repo.find_blob( + inner_tree + .get_name("o") + .ok_or_else(|| josh_error("concat: missing key"))? + .id(), + )?; + let filter_tree = repo.find_tree( + inner_tree + .get_name("f") + .ok_or_else(|| josh_error("concat: missing filter"))? + .id(), + )?; + let key = std::str::from_utf8(key_blob.content())?.to_string(); + let filter = from_tree2(repo, filter_tree.id())?; + Ok(Op::Unapply(LazyRef::parse(&key)?, to_filter(filter))) + } "squash" => { // blob -> Squash(None), tree -> Squash(Some(...)) if let Some(kind) = entry.kind() { diff --git a/josh-core/src/history.rs b/josh-core/src/history.rs index 0629de311..2e2ca717b 100644 --- a/josh-core/src/history.rs +++ b/josh-core/src/history.rs @@ -293,6 +293,13 @@ fn find_new_branch_base( Ok(git2::Oid::zero()) } +#[derive(Clone, Debug)] +pub enum OrphansMode { + Keep, + Remove, + Fail, +} + #[tracing::instrument(skip(transaction, change_ids))] pub fn unapply_filter( transaction: &cache::Transaction, @@ -300,7 +307,7 @@ pub fn unapply_filter( original_target: git2::Oid, old_filtered_oid: git2::Oid, new_filtered_oid: git2::Oid, - keep_orphans: bool, + orphans_mode: OrphansMode, reparent_orphans: Option, change_ids: &mut Option>, ) -> JoshResult { @@ -382,14 +389,32 @@ pub fn unapply_filter( } let mut filtered_parent_ids: Vec<_> = module_commit.parent_ids().collect(); - let is_initial_merge = filtered_parent_ids.len() == 2 + let has_new_orphan = filtered_parent_ids.len() > 1 && transaction .repo() - .merge_base_many(&filtered_parent_ids) + .merge_base_octopus(&filtered_parent_ids) .is_err(); - if !keep_orphans && is_initial_merge { - filtered_parent_ids.pop(); + if has_new_orphan { + match orphans_mode { + OrphansMode::Keep => {} + OrphansMode::Remove => { + filtered_parent_ids.pop(); + } + OrphansMode::Fail => { + return Err(josh_error(&unindent::unindent(&format!( + r###" + Rejecting new orphan branch at {:?} ({:?}) + Specify one of these options: + '-o allow_orphans' to keep the history as is + '-o merge' to import new history by creating merge commit + '-o edit' if you are editing a stored filter or workspace + "###, + module_commit.summary().unwrap_or_default(), + module_commit.id(), + )))); + } + } } // For every parent of a filtered commit, find unapply base diff --git a/josh-core/src/lib.rs b/josh-core/src/lib.rs index c5e018383..622522740 100644 --- a/josh-core/src/lib.rs +++ b/josh-core/src/lib.rs @@ -412,3 +412,368 @@ pub fn get_acl( }) .unwrap_or_else(|| Ok((filter::empty(), filter::nop()))) } + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct JoshLinkFile { + pub remote: String, + pub branch: String, + pub filter: filter::Filter, + pub commit: Oid, +} + +pub struct ParsedSubmoduleEntry { + pub path: std::path::PathBuf, + pub url: String, + pub branch: String, +} + +pub fn parse_gitmodules(gitmodules_content: &str) -> JoshResult> { + use gix_submodule::File; + + let submodules = File::from_bytes(gitmodules_content.as_bytes(), None, &Default::default()) + .map_err(|e| josh_error(&format!("Failed to parse .gitmodules: {}", e)))?; + + let mut entries: Vec = Vec::new(); + + for name in submodules.names() { + // path is required to consider an entry + if let Ok(path) = submodules.path(name) { + let path = std::path::PathBuf::from(path.to_string()); + + let url = submodules + .url(name) + .ok() + .map(|u| u.to_string()) + .unwrap_or_default(); + + // Default branch to "HEAD" if not configured + let branch = submodules + .branch(name) + .ok() + .and_then(|opt| { + opt.map(|b| match b { + gix_submodule::config::Branch::CurrentInSuperproject => ".".to_string(), + gix_submodule::config::Branch::Name(n) => n.to_string(), + }) + }) + .unwrap_or_else(|| "HEAD".to_string()); + + entries.push(ParsedSubmoduleEntry { path, url, branch }); + } + } + + Ok(entries) +} + +pub fn update_gitmodules( + gitmodules_content: &str, + entry: &ParsedSubmoduleEntry, +) -> JoshResult { + use gix_config::File as ConfigFile; + use gix_submodule::File as SubmoduleFile; + + // Parse the existing gitmodules content using gix_submodule + let submodule_file = SubmoduleFile::from_bytes( + gitmodules_content.as_bytes(), + None, + &ConfigFile::new(gix_config::file::Metadata::default()), + ) + .map_err(|e| josh_error(&format!("Failed to parse .gitmodules: {}", e)))?; + + // Get the underlying config file to modify it + let mut config = submodule_file.config().clone(); + + // Find the existing submodule by matching the path + let mut existing_submodule_name = None; + for name in submodule_file.names() { + if let Ok(path) = submodule_file.path(name) { + if path.to_string() == entry.path.to_string_lossy() { + existing_submodule_name = Some(name.to_string()); + break; + } + } + } + + let submodule_name = if let Some(name) = existing_submodule_name { + // Use the existing submodule name + name + } else { + // Create a new submodule name from path (fallback) + entry.path.to_string_lossy().replace('/', "_") + }; + + // Create or update the submodule section + let mut section = config + .section_mut_or_create_new("submodule", Some(submodule_name.as_str().into())) + .map_err(|e| josh_error(&format!("Failed to create submodule section: {}", e)))?; + + // Set the submodule properties using push method + section.push( + "path".try_into().unwrap(), + Some(entry.path.to_string_lossy().as_ref().into()), + ); + section.push("url".try_into().unwrap(), Some(entry.url.as_str().into())); + if entry.branch != "HEAD" { + section.push( + "branch".try_into().unwrap(), + Some(entry.branch.as_str().into()), + ); + } + + // Write the updated config back to string + let mut output = Vec::new(); + config + .write_to(&mut output) + .map_err(|e| josh_error(&format!("Failed to write gitmodules: {}", e)))?; + + String::from_utf8(output) + .map_err(|e| josh_error(&format!("Invalid UTF-8 in gitmodules: {}", e))) +} + +pub fn find_link_files( + repo: &git2::Repository, + tree: &git2::Tree, +) -> JoshResult> { + let mut link_files = Vec::new(); + + tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| { + if let Some(name) = entry.name() { + if name == ".josh-link.toml" { + // Found a link file + let link_blob = match repo.find_blob(entry.id()) { + Ok(blob) => blob, + Err(e) => { + eprintln!("Failed to find blob: {}", e); + return git2::TreeWalkResult::Skip; + } + }; + + let link_content = match std::str::from_utf8(link_blob.content()) { + Ok(content) => content, + Err(e) => { + eprintln!("Failed to parse link file content: {}", e); + return git2::TreeWalkResult::Skip; + } + }; + + let link_file: JoshLinkFile = match toml::from_str(link_content) { + Ok(file) => file, + Err(e) => { + eprintln!("Failed to parse .josh-link.toml: {}", e); + return git2::TreeWalkResult::Skip; + } + }; + + let root = root.trim_matches('/'); + // Use root as the directory path where the .josh-link.toml file is located + let path = std::path::PathBuf::from(root); + + link_files.push((path, link_file)); + } + } + + git2::TreeWalkResult::Ok + }) + .map_err(|e| josh_error(&format!("Failed to walk tree: {}", e)))?; + + Ok(link_files) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn test_parse_gitmodules_basic() { + let content = r#"[submodule "libs/foo"] + path = libs/foo + url = https://github.com/example/foo.git + branch = main + +[submodule "libs/bar"] + path = libs/bar + url = https://github.com/example/bar.git"#; + + let result = parse_gitmodules(content).unwrap(); + assert_eq!(result.len(), 2); + + assert_eq!(result[0].path, PathBuf::from("libs/foo")); + assert_eq!(result[0].url, "https://github.com/example/foo.git"); + assert_eq!(result[0].branch, "main"); + + assert_eq!(result[1].path, PathBuf::from("libs/bar")); + assert_eq!(result[1].url, "https://github.com/example/bar.git"); + assert_eq!(result[1].branch, "HEAD"); // default + } + + #[test] + fn test_parse_gitmodules_empty() { + let content = ""; + let result = parse_gitmodules(content).unwrap(); + assert_eq!(result.len(), 0); + } + + #[test] + fn test_parse_gitmodules_invalid() { + let content = "invalid gitmodules content"; + let result = parse_gitmodules(content); + assert!(result.is_err()); + } + + #[test] + fn test_update_gitmodules_add_new() { + let content = ""; + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/foo"), + url: "https://github.com/example/foo.git".to_string(), + branch: "main".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + let expected = r#"[submodule "libs_foo"] + path = libs/foo + url = https://github.com/example/foo.git + branch = main +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_add_new_with_default_branch() { + let content = ""; + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/bar"), + url: "https://github.com/example/bar.git".to_string(), + branch: "HEAD".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + let expected = r#"[submodule "libs_bar"] + path = libs/bar + url = https://github.com/example/bar.git +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_update_existing() { + let content = r#"[submodule "existing_foo"] + path = libs/foo + url = https://github.com/example/old-foo.git + branch = old-branch"#; + + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/foo"), + url: "https://github.com/example/new-foo.git".to_string(), + branch: "new-branch".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + // The gix-config API appends values instead of replacing them + let expected = r#"[submodule "existing_foo"] + path = libs/foo + url = https://github.com/example/old-foo.git + branch = old-branch +path = libs/foo + url = https://github.com/example/new-foo.git + branch = new-branch +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_update_existing_with_default_branch() { + let content = r#"[submodule "existing_bar"] + path = libs/bar + url = https://github.com/example/old-bar.git + branch = old-branch"#; + + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/bar"), + url: "https://github.com/example/new-bar.git".to_string(), + branch: "HEAD".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + // The gix-config API appends values instead of replacing them + let expected = r#"[submodule "existing_bar"] + path = libs/bar + url = https://github.com/example/old-bar.git + branch = old-branch +path = libs/bar + url = https://github.com/example/new-bar.git +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_multiple_submodules() { + let content = r#"[submodule "existing_foo"] + path = libs/foo + url = https://github.com/example/foo.git + +[submodule "existing_bar"] + path = libs/bar + url = https://github.com/example/bar.git"#; + + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/baz"), + url: "https://github.com/example/baz.git".to_string(), + branch: "develop".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + // The gix-config API appends new sections at the end + let expected = r#"[submodule "existing_foo"] + path = libs/foo + url = https://github.com/example/foo.git + +[submodule "existing_bar"] + path = libs/bar + url = https://github.com/example/bar.git +[submodule "libs_baz"] + path = libs/baz + url = https://github.com/example/baz.git + branch = develop +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_path_with_slashes() { + let content = ""; + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("deep/nested/path/submodule"), + url: "https://github.com/example/deep-submodule.git".to_string(), + branch: "main".to_string(), + }; + + let result = update_gitmodules(content, &entry).unwrap(); + + let expected = r#"[submodule "deep_nested_path_submodule"] + path = deep/nested/path/submodule + url = https://github.com/example/deep-submodule.git + branch = main +"#; + assert_eq!(result, expected); + } + + #[test] + fn test_update_gitmodules_invalid_content() { + let content = "invalid gitmodules content"; + let entry = ParsedSubmoduleEntry { + path: PathBuf::from("libs/foo"), + url: "https://github.com/example/foo.git".to_string(), + branch: "main".to_string(), + }; + + let result = update_gitmodules(content, &entry); + assert!(result.is_err()); + } +} diff --git a/josh-filter/src/bin/josh-filter.rs b/josh-filter/src/bin/josh-filter.rs index a58d8c9f1..cb7c4f5c6 100644 --- a/josh-filter/src/bin/josh-filter.rs +++ b/josh-filter/src/bin/josh-filter.rs @@ -159,6 +159,32 @@ impl josh::cache::FilterHook for GitNotesFilterHook { } } +struct ChangeIdFilterHook { + repo: std::sync::Mutex, +} + +impl josh::cache::FilterHook for ChangeIdFilterHook { + fn filter_for_commit( + &self, + commit_oid: git2::Oid, + arg: &str, + ) -> josh::JoshResult { + let repo = self.repo.lock().unwrap(); + let commit = repo.find_commit(commit_oid)?; + let data = format!( + "{:?}:{:?}:{}:{}", + commit.message(), + commit.time(), + commit.author(), + commit.committer() + ); + + let hash = git2::Oid::hash_object(git2::ObjectType::Blob, data.as_bytes()) + .expect("hash_object changeid"); + josh::filter::parse(&format!(":\"{}\"", hash)) + } +} + fn run_filter(args: Vec) -> josh::JoshResult { let args = make_app().get_matches_from(args); @@ -200,7 +226,7 @@ fn run_filter(args: Vec) -> josh::JoshResult { git2::RepositoryOpenFlags::NO_SEARCH, &[] as &[&std::ffi::OsStr], )?; - let hook = GitNotesFilterHook { + let hook = ChangeIdFilterHook { repo: std::sync::Mutex::new(repo_for_hook), }; transaction = transaction.with_filter_hook(std::sync::Arc::new(hook)); @@ -437,7 +463,7 @@ fn run_filter(args: Vec) -> josh::JoshResult { unfiltered_old, old, new, - false, + josh::history::OrphansMode::Keep, None, &mut None, ) { diff --git a/josh-proxy/src/lib.rs b/josh-proxy/src/lib.rs index b1d0a95b3..509cbffb4 100644 --- a/josh-proxy/src/lib.rs +++ b/josh-proxy/src/lib.rs @@ -82,6 +82,8 @@ pub struct RepoUpdate { #[derive(Default)] pub struct PushOptions { pub merge: bool, + pub allow_orphans: bool, + pub edit: bool, pub create: bool, pub force: bool, pub base: Option, @@ -230,7 +232,13 @@ pub fn process_repo_update(repo_update: RepoUpdate) -> josh::JoshResult original_target, old, new_oid, - push_options.merge, + if push_options.merge || push_options.allow_orphans { + josh::history::OrphansMode::Keep + } else if push_options.edit { + josh::history::OrphansMode::Remove + } else { + josh::history::OrphansMode::Fail + }, reparent_orphans, &mut changes, )?; diff --git a/tests/cli/link-add.t b/tests/cli/link-add.t new file mode 100644 index 000000000..dfe10ea17 --- /dev/null +++ b/tests/cli/link-add.t @@ -0,0 +1,320 @@ + $ export TESTTMP=${PWD} + +# Create a test repository + $ mkdir -p remote + $ cd remote + $ git init -q + $ git config user.name "Test User" + $ git config user.email "test@example.com" + +# Create some content + $ mkdir -p libs utils docs + $ echo "library code" > libs/lib1.txt + $ echo "utility code" > utils/util1.txt + $ echo "documentation" > docs/readme.txt + $ git add . + $ git commit -m "Initial commit" + [master (root-commit) *] Initial commit (glob) + 3 files changed, 3 insertions(+) + create mode 100644 docs/readme.txt + create mode 100644 libs/lib1.txt + create mode 100644 utils/util1.txt + + $ cd .. + +# Create a bare repository for linking + $ git clone --bare remote remote.git + Cloning into bare repository 'remote.git'... + done. + $ cd ${TESTTMP} + +# Create a new repository to test link add + $ git init test-repo + Initialized empty Git repository in * (glob) + $ cd test-repo + $ git config user.name "Test User" + $ git config user.email "test@example.com" + + $ which git + /opt/git-install/bin/git + +# Create an initial commit so we have a HEAD + $ echo "initial content" > README.md + $ git add README.md + $ git commit -m "Initial commit" + [master (root-commit) 3eb5c75] Initial commit + 1 file changed, 1 insertion(+) + create mode 100644 README.md + +# Test basic link add with default filter and target + $ josh link add libs ../remote.git + Added link 'libs' with URL '*', filter ':/', and target 'HEAD' (glob) + Created branch: refs/josh/link + +# Verify the branch was created + $ git show-ref | grep refs/josh + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/0327b7819f74ececa864c40fc613dc155c3448a8 + 425394d340af95bd70896a0f10d02b374526e305 refs/josh/24/0/2d82eb90b809afa717116aedab06358ee0ec4fd9 + 436717b101d9e28d226020808f51dd83e3014125 refs/josh/24/0/4e93b7258ff5892e06af5fb01b870f026602ed23 + 9b01963938f90fcdd48bb5ef40bef1037bc3b393 refs/josh/24/0/c146184056bce01018a493331d2459af8bb116f2 + 5046169942bf0b90cd00cd8dc29f045adabaefcb refs/josh/24/0/f7195751aa1f8f22cc8dc79ba64193cfe5f93efd + f53f46fd82cc2287717a1f88f476efcea1395cf2 refs/josh/link + +# Verify HEAD was not updated + $ git log --oneline + * Initial commit (glob) + +# Check the content of the link branch + $ git checkout refs/josh/link + Note: switching to 'refs/josh/link'. (glob) + * (glob) + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by switching back to a branch. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -c with the switch command. Example: + + git switch -c + + Or undo this operation with: + + git switch - + + Turn off this advice by setting config variable advice.detachedHead to false + + HEAD is now at * Add link: libs (glob) + $ git ls-tree -r HEAD + 100644 blob f2376e2bab6c5194410bd8a55630f83f933d2f34\tREADME.md (esc) + 100644 blob 206d76fad48424fec1fface3ad37d1c24e5eba3a\tlibs/.josh-link.toml (esc) + 100644 blob dfcaa10d372d874e1cab9c3ba8d0b683099c3826\tlibs/docs/readme.txt (esc) + 100644 blob abe06153eb1e2462265336768a6ecd1164f73ae2\tlibs/libs/lib1.txt (esc) + 100644 blob f03a884ed41c1a40b529001c0b429eed24c5e9e5\tlibs/utils/util1.txt (esc) + $ cat libs/.josh-link.toml + remote = "../remote.git" + branch = "HEAD" + filter = ":/" + commit = "d27fa3a10cc019e6aa55fc74c1f0893913380e2d" + + $ git checkout master + Previous HEAD position was * Add link: libs (glob) + Switched to branch 'master' + +# Test link add with custom filter and target + $ josh link add utils ../remote.git :/utils --target master + Added link 'utils' with URL '*', filter ':/utils', and target 'master' (glob) + Created branch: refs/josh/link + +# Verify the branch was created + $ git show-ref | grep refs/josh + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/0327b7819f74ececa864c40fc613dc155c3448a8 + 425394d340af95bd70896a0f10d02b374526e305 refs/josh/24/0/2d82eb90b809afa717116aedab06358ee0ec4fd9 + 436717b101d9e28d226020808f51dd83e3014125 refs/josh/24/0/4e93b7258ff5892e06af5fb01b870f026602ed23 + bc01d3049a0f1de06fd33b1c254b41a125638126 refs/josh/24/0/58064dda2b6fe935a5314cce59efede729605038 + 7b2e99abe0fac7966e64388e614808b56b9f7e20 refs/josh/24/0/7f51f71096544b383851baf8bccffb1caa5e05cf + 9b01963938f90fcdd48bb5ef40bef1037bc3b393 refs/josh/24/0/c146184056bce01018a493331d2459af8bb116f2 + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/dcb92d96ebb1ef57d43530bc8d5b0de26eb9def9 + 5046169942bf0b90cd00cd8dc29f045adabaefcb refs/josh/24/0/f7195751aa1f8f22cc8dc79ba64193cfe5f93efd + 4b33fdcef8316c2630bd84db8aa1797d4c738099 refs/josh/link + +# Check the content of the utils link branch + $ git checkout refs/josh/link + Note: switching to 'refs/josh/link'. + + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by switching back to a branch. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -c with the switch command. Example: + + git switch -c + + Or undo this operation with: + + git switch - + + Turn off this advice by setting config variable advice.detachedHead to false + + HEAD is now at * Add link: utils (glob) + $ cat utils/.josh-link.toml + remote = "../remote.git" + branch = "master" + filter = ":/utils" + commit = "d27fa3a10cc019e6aa55fc74c1f0893913380e2d" + + $ git checkout master + Previous HEAD position was * Add link: utils (glob) + Switched to branch 'master' + +# Test path normalization (path with leading slash) + $ josh link add /docs ../remote.git :/docs + Added link 'docs' with URL '*', filter ':/docs', and target 'HEAD' (glob) + Created branch: refs/josh/link + +# Verify path was normalized (no leading slash in branch name) + $ git show-ref | grep refs/josh + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/0327b7819f74ececa864c40fc613dc155c3448a8 + 425394d340af95bd70896a0f10d02b374526e305 refs/josh/24/0/2d82eb90b809afa717116aedab06358ee0ec4fd9 + 436717b101d9e28d226020808f51dd83e3014125 refs/josh/24/0/4e93b7258ff5892e06af5fb01b870f026602ed23 + bc01d3049a0f1de06fd33b1c254b41a125638126 refs/josh/24/0/58064dda2b6fe935a5314cce59efede729605038 + 7b2e99abe0fac7966e64388e614808b56b9f7e20 refs/josh/24/0/7f51f71096544b383851baf8bccffb1caa5e05cf + b32f2ee853082db481c5acb767edf5ffef697b18 refs/josh/24/0/a9387e30d1df7f381a51a01c30a773a2b66c4191 + 9b01963938f90fcdd48bb5ef40bef1037bc3b393 refs/josh/24/0/c146184056bce01018a493331d2459af8bb116f2 + ff60837b82b1e8d8b68739081e057d08b6ec03d8 refs/josh/24/0/cf6f8cb677c155c2bd2b1f2496a116760a60e124 + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/dcb92d96ebb1ef57d43530bc8d5b0de26eb9def9 + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/e99fcfbaee6ac556b0edc630e20e155a96cb0e26 + 5046169942bf0b90cd00cd8dc29f045adabaefcb refs/josh/24/0/f7195751aa1f8f22cc8dc79ba64193cfe5f93efd + b329d29ef994c99944dfcbb4f4bf8259b0eae9a9 refs/josh/link + + +# Test error case - empty path + $ josh link add "" ../remote.git + Error: Path cannot be empty + [1] + +# Test error case - not in a git repository + $ cd .. + $ josh link add test ../remote.git + Error: Not in a git repository: * (glob) + [1] + + $ cd test-repo + +# Verify that no git remotes were created (josh link add should not create remotes) + $ git remote -v + +# Verify that no git config entries were created (josh link add should not modify .git/config) + $ git config --list | grep josh-link + [1] + +# Test help output + $ josh link --help + Manage josh links (like `josh remote` but for links) + + Usage: josh link + + Commands: + add Add a link with optional filter and target branch + fetch Fetch from existing link files + help Print this message or the help of the given subcommand(s) + + Options: + -h, --help Print help + + $ josh link add --help + Add a link with optional filter and target branch + + Usage: josh link add [OPTIONS] [FILTER] + + Arguments: + Path where the link will be mounted + Remote repository URL + [FILTER] Optional filter to apply to the linked repository + + Options: + --target Target branch to link (defaults to HEAD) + -h, --help Print help + +# Test josh link fetch command +# First, create a link file directly in the master branch for testing + $ mkdir -p test-link + $ echo 'remote = "../remote.git"' > test-link/.josh-link.toml + $ echo 'branch = "HEAD"' >> test-link/.josh-link.toml + $ echo 'filter = ":/test"' >> test-link/.josh-link.toml + $ echo 'commit = "d27fa3a10cc019e6aa55fc74c1f0893913380e2d"' >> test-link/.josh-link.toml + $ git add test-link/.josh-link.toml + $ git commit -m "Add test link file for fetch testing" + [master *] Add test link file for fetch testing (glob) + 1 file changed, 4 insertions(+) + create mode 100644 test-link/.josh-link.toml + +# Test fetch with specific path + $ josh link fetch test-link + Found 1 link file(s) to fetch + Fetching from link at path: test-link + Updated 1 link file(s) + Created branch: refs/josh/link + +# Verify the branch was updated + $ git show-ref | grep refs/josh + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/0327b7819f74ececa864c40fc613dc155c3448a8 + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/08e1c97fe7bd428dbe30d84b91072263b88c5959 + 425394d340af95bd70896a0f10d02b374526e305 refs/josh/24/0/2d82eb90b809afa717116aedab06358ee0ec4fd9 + 436717b101d9e28d226020808f51dd83e3014125 refs/josh/24/0/4e93b7258ff5892e06af5fb01b870f026602ed23 + bc01d3049a0f1de06fd33b1c254b41a125638126 refs/josh/24/0/58064dda2b6fe935a5314cce59efede729605038 + 7b2e99abe0fac7966e64388e614808b56b9f7e20 refs/josh/24/0/7f51f71096544b383851baf8bccffb1caa5e05cf + 709c643f5d785ad570f6abca2a0e384d38d3ff79 refs/josh/24/0/9d98b64fc483a176d7480ffaee8a7cef7d393260 + b32f2ee853082db481c5acb767edf5ffef697b18 refs/josh/24/0/a9387e30d1df7f381a51a01c30a773a2b66c4191 + 7991bfa1fbcbd81fdba5d1b4f10985b857bad78e refs/josh/24/0/bd248066824b72a6afa2c1fbcb27000e65d6be01 + 9b01963938f90fcdd48bb5ef40bef1037bc3b393 refs/josh/24/0/c146184056bce01018a493331d2459af8bb116f2 + ff60837b82b1e8d8b68739081e057d08b6ec03d8 refs/josh/24/0/cf6f8cb677c155c2bd2b1f2496a116760a60e124 + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/dcb92d96ebb1ef57d43530bc8d5b0de26eb9def9 + 65574eaa9f2f309d6fa4f4b68c590faa7d4882fa refs/josh/24/0/e99fcfbaee6ac556b0edc630e20e155a96cb0e26 + 5046169942bf0b90cd00cd8dc29f045adabaefcb refs/josh/24/0/f7195751aa1f8f22cc8dc79ba64193cfe5f93efd + 82aa51e68542366219191a2a25fefbb6ed6e57a0 refs/josh/link + +# Check the updated content + $ git checkout refs/josh/link + Note: switching to 'refs/josh/link'. (glob) + * (glob) + You are in 'detached HEAD' state. You can look around, make experimental + changes and commit them, and you can discard any commits you make in this + state without impacting any branches by switching back to a branch. + + If you want to create a new branch to retain commits you create, you may + do so (now or later) by using -c with the switch command. Example: + + git switch -c + + Or undo this operation with: + + git switch - + + Turn off this advice by setting config variable advice.detachedHead to false + + HEAD is now at * Update links: test-link (glob) + $ git ls-tree -r HEAD + 100644 blob f2376e2bab6c5194410bd8a55630f83f933d2f34 README.md (esc) + 100644 blob bd917a0bed306891ca07801e3d89b9140954434f test-link/.josh-link.toml (esc) + $ cat test-link/.josh-link.toml + remote = "../remote.git" + branch = "HEAD" + filter = ":/test" + commit = "d27fa3a10cc019e6aa55fc74c1f0893913380e2d" + + $ git checkout master + Previous HEAD position was * Update links: test-link (glob) + Switched to branch 'master' + +# Test fetch with no path (should find all .josh-link.toml files) + $ josh link fetch + Found 1 link file(s) to fetch + Fetching from link at path: test-link + Updated 1 link file(s) + Created branch: refs/josh/link + +# Test error case - path that doesn't exist + $ josh link fetch nonexistent + Error: Failed to find .josh-link.toml at path 'nonexistent': * (glob) + [1] + +# Test error case - no link files found + $ cd .. + $ git init empty-repo + Initialized empty Git repository in * (glob) + $ cd empty-repo + $ git config user.name "Test User" + $ git config user.email "test@example.com" + $ echo "initial content" > README.md + $ git add README.md + $ git commit -m "Initial commit" + [master (root-commit) 3eb5c75] Initial commit + 1 file changed, 1 insertion(+) + create mode 100644 README.md + + $ josh link fetch + Error: No .josh-link.toml files found + [1] + + $ cd .. diff --git a/tests/filter/concat.t b/tests/filter/concat.t new file mode 100644 index 000000000..05dab45d5 --- /dev/null +++ b/tests/filter/concat.t @@ -0,0 +1,42 @@ + $ export TESTTMP=${PWD} + + $ cd ${TESTTMP} + $ git init -q libs 1> /dev/null + $ cd libs + + $ mkdir sub1 + $ echo contents1 > sub1/file1 + $ git add sub1 + $ git commit -m "add file1" 1> /dev/null + + $ echo contents2 > sub1/file2 + $ git add sub1 + $ git commit -m "add file2" 1> /dev/null + $ git update-ref refs/heads/from_here HEAD + + + $ mkdir sub2 + $ echo contents1 > sub2/file3 + $ git add sub2 + $ git commit -m "add file3" 1> /dev/null + + $ josh-filter ":\"x\"" + + $ git log --graph --pretty=%s:%H HEAD + * add file3:667a912db7482f3c8023082c9b4c7b267792633a + * add file2:81b10fb4984d20142cd275b89c91c346e536876a + * add file1:bb282e9cdc1b972fffd08fd21eead43bc0c83cb8 + + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * x:9d117d96dfdba145df43ebe37d9e526acac4b17c + * x:b232aa8eefaadfb5e38b3ad7355118aa59fb651e + * x:6b4d1f87c2be08f7d0f9d40b6679aab612e259b1 + + $ josh-filter -p ":from(81b10fb4984d20142cd275b89c91c346e536876a:\"x\")" + :"x":concat(81b10fb4984d20142cd275b89c91c346e536876a:"x") + $ josh-filter ":from(81b10fb4984d20142cd275b89c91c346e536876a:\"x\")" + + $ git log --graph --pretty=%s FILTERED_HEAD + * x + * add file2 + * add file1 diff --git a/tests/filter/link-submodules.t b/tests/filter/link-submodules.t new file mode 100644 index 000000000..266f16d51 --- /dev/null +++ b/tests/filter/link-submodules.t @@ -0,0 +1,562 @@ + $ export TESTTMP=${PWD} + + $ cd ${TESTTMP} + $ git init -q submodule-repo 1> /dev/null + $ cd submodule-repo + + $ mkdir -p foo + $ echo "foo content" > foo/file1.txt + $ echo "bar content" > foo/file2.txt + $ git add foo + $ git commit -m "add foo with files" 1> /dev/null + + $ mkdir -p bar + $ echo "baz content" > bar/file3.txt + $ git add bar + $ git commit -m "add bar with file" 1> /dev/null + + $ git log --graph --pretty=%s:%H + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + + $ cd ${TESTTMP} + $ git init -q main-repo 1> /dev/null + $ cd main-repo + $ git commit -m "init" --allow-empty 1> /dev/null + + $ echo "main content" > main.txt + $ git add main.txt + $ git commit -m "add main.txt" 1> /dev/null + + $ git submodule add ../submodule-repo libs 2> /dev/null + $ git submodule status + 00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd libs (heads/master) + + $ git commit -m "add libs submodule" 1> /dev/null + + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + + $ git log --graph --pretty=%s + * add libs submodule + * add main.txt + * init + + $ git ls-tree --name-only -r HEAD + .gitmodules + libs + main.txt + + $ cat .gitmodules + [submodule "libs"] + path = libs + url = ../submodule-repo + + $ git ls-tree HEAD + 100644 blob 5255711b4fd563af2d873bf3c8f9da6c37ce1726\t.gitmodules (esc) + 160000 commit 00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd\tlibs (esc) + 100644 blob bcb9dcad21591bd9284afbb6c21e6d69eafe8f15\tmain.txt (esc) + +Test Adapt filter - should expand submodule into actual tree content + + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :embed=libs + [2] ::libs/.josh-link.toml + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] :"{commit}" + [3] :adapt=submodules + [3] :link=embedded + [10] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * add libs submodule + |\ + | * add bar with file + | * add foo with files + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + libs/bar/file3.txt + libs/foo/file1.txt + libs/foo/file2.txt + main.txt + + $ git ls-tree refs/josh/filter/master + 040000 tree 1a06220380a0dd3249b08cb1b69158338ebad3ef\tlibs (esc) + 100644 blob bcb9dcad21591bd9284afbb6c21e6d69eafe8f15\tmain.txt (esc) + + $ git ls-tree refs/josh/filter/master libs + 040000 tree 1a06220380a0dd3249b08cb1b69158338ebad3ef\tlibs (esc) + + $ git ls-tree refs/josh/filter/master libs/foo + 040000 tree 81a0b9c71d7fac4f553b2a52b9d8d52d07dd8036\tlibs/foo (esc) + + $ git ls-tree refs/josh/filter/master libs/bar + 040000 tree bd42a3e836f59dda9f9d5950d0e38431c9b1bfb5\tlibs/bar (esc) + + $ git show refs/josh/filter/master:libs/.josh-link.toml + remote = "../submodule-repo" + branch = "HEAD" + filter = ":/" + commit = "00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd" + $ git show refs/josh/filter/master:libs/foo/file1.txt + foo content + + $ git show refs/josh/filter/master:libs/foo/file2.txt + bar content + + $ git show refs/josh/filter/master:libs/bar/file3.txt + baz content + +Test that .gitmodules file is removed after unsubmodule:link + + $ git ls-tree refs/josh/filter/master | grep gitmodules + [1] + +Test Adapt with multiple submodules + + $ cd ${TESTTMP} + $ git init -q another-submodule 1> /dev/null + $ cd another-submodule + $ echo "another content" > another.txt + $ git add another.txt + $ git commit -m "add another.txt" 1> /dev/null + $ git log --graph --pretty=%s:%H + * add another.txt:8fbd01fa31551a059e280f68ac37397712feb59e + + $ cd ${TESTTMP}/main-repo + $ git submodule add ../another-submodule modules/another 2> /dev/null + $ git commit -m "add another submodule" 1> /dev/null + + $ git fetch ../another-submodule + From ../another-submodule + * branch HEAD -> FETCH_HEAD + + $ cat .gitmodules + [submodule "libs"] + path = libs + url = ../submodule-repo + [submodule "modules/another"] + path = modules/another + url = ../another-submodule + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [1] :embed=libs + [2] ::libs/.josh-link.toml + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] :"{commit}" + [3] :link=embedded + [4] :adapt=submodules + [11] sequence_number + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + main.txt + modules/another/.josh-link.toml + $ git show refs/josh/filter/master:libs/.josh-link.toml + remote = "../submodule-repo" + branch = "HEAD" + filter = ":/" + commit = "00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd" + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :embed=libs + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::libs/.josh-link.toml + [2] ::modules/another/.josh-link.toml + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [4] :"{commit}" + [4] :adapt=submodules + [4] :link=embedded + [15] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * add another submodule + |\ + | * add another.txt + * add libs submodule + |\ + | * add bar with file + | * add foo with files + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + libs/bar/file3.txt + libs/foo/file1.txt + libs/foo/file2.txt + main.txt + modules/another/.josh-link.toml + modules/another/another.txt + + $ git ls-tree refs/josh/filter/master modules + 040000 tree e1b10636436c25c78dc6372eb20079454f05d746\tmodules (esc) + + $ git show refs/josh/filter/master:modules/another/another.txt + another content + +Test Adapt with submodule changes - add commits to submodule and update + + $ cd ${TESTTMP}/submodule-repo + $ echo "new content" > foo/file3.txt + $ git add foo/file3.txt + $ git commit -m "add file3.txt" 1> /dev/null + + $ echo "another new content" > bar/file4.txt + $ git add bar/file4.txt + $ git commit -m "add file4.txt" 1> /dev/null + $ git log --graph --pretty=%s:%H + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + + $ cd ${TESTTMP}/main-repo + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + $ git submodule update --remote libs + From /tmp/prysk-tests-*/link-submodules.t/submodule-repo (glob) + 00c8fe9..3061af9 master -> origin/master + Submodule path 'libs': checked out '3061af908a0dc1417902fbd7208bb2b8dc354e6c' + $ git add libs + $ git commit -m "update libs submodule" 1> /dev/null + + $ tree + . + |-- libs + | |-- bar + | | |-- file3.txt + | | `-- file4.txt + | `-- foo + | |-- file1.txt + | |-- file2.txt + | `-- file3.txt + |-- main.txt + `-- modules + `-- another + `-- another.txt + + 6 directories, 7 files + + + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [21] sequence_number + $ git log --graph --pretty=%s:%H refs/josh/filter/master + * update libs submodule:4657e0d71754bdd097c6453a75a5084b467baf54 + |\ + | * add file4.txt:2278f620291df7133299176efb6210fc193c3387 + | * add file3.txt:deed71e37198bf3c8668fa353c66d79c0de25834 + * | add another submodule:b0153ca36fe220c8e0942ee5daf51512907108ca + |\ \ + | * | add another.txt:1f2b84bfd4029e70d3aeb16a6ecb7f0a0490490e + | / + * | add libs submodule:d7b5b1dad9444f25b5011d9f25af2e48a82ff173 + |\| + | * add bar with file:2926fa3361cec2d5695a119fcc3592f4214af3ba + | * add foo with files:e975fd8cd3f2d2de81884f5b761cc0ac150bdf47 + * add main.txt:c404a74092888a14d109c8211576d2c50fc2affd + * init:01d3837a9f7183df88e956cc81f085544f9c6563 + $ git ls-tree --name-only -r 4657e0d71754bdd097c6453a75a5084b467baf54 + libs/.josh-link.toml + libs/bar/file3.txt + libs/bar/file4.txt + libs/foo/file1.txt + libs/foo/file2.txt + libs/foo/file3.txt + main.txt + modules/another/.josh-link.toml + modules/another/another.txt + $ git ls-tree --name-only -r 2278f620291df7133299176efb6210fc193c3387 + libs/bar/file3.txt + libs/bar/file4.txt + libs/foo/file1.txt + libs/foo/file2.txt + libs/foo/file3.txt + main.txt + modules/another/.josh-link.toml + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + libs/bar/file3.txt + libs/bar/file4.txt + libs/foo/file1.txt + libs/foo/file2.txt + libs/foo/file3.txt + main.txt + modules/another/.josh-link.toml + modules/another/another.txt + + $ git show refs/josh/filter/master:libs/foo/file3.txt + new content + + $ git show refs/josh/filter/master:libs/bar/file4.txt + another new content + + $ josh-filter -s :adapt=submodules:link:/libs master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [9] :/libs + [29] sequence_number + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * update libs submodule:6336c45ef94ccdc32fd072b5d7fecf0e9755431a + |\ + | * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + | * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * | add another submodule:f6d97d3185819dce5596623f5494208fca2de85d + |\ \ + | * | add another.txt:529c9c80186129065a994cbf91095ab1e90323f0 + | / + * / add libs submodule:cb64d3e5db01b0b451f21199ae2197997bc592ba + |/ + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + $ josh-filter -s :adapt=submodules:link:/libs:prune=trivial-merge master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :prune=trivial-merge + [9] :/libs + [33] sequence_number + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * update libs submodule:93f3162c2e8d78320091bb8bb7f9b27226f105bc + |\ + | * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + | * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * | add libs submodule:cb64d3e5db01b0b451f21199ae2197997bc592ba + |/ + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + $ josh-filter -p --reverse :prune=trivial-merge:export:prefix=libs + :/libs:export:prune=trivial-merge + $ josh-filter -s :adapt=submodules:link:/libs:export:prune=trivial-merge master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :export + [7] :prune=trivial-merge + [9] :/libs + [34] sequence_number + $ git log --graph --pretty=%s:%H:%T FILTERED_HEAD + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c:ac420a625dfb874002210e623a7fdb55708ef2fa + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173:2935d839ce5e2fa8d5d8fb1a8541bf95b98fbedb + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd:e06b912df6ae0105e3a525f7a9427d98574fbc4f + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738:7ca6af9b9a7d0d7f4723a74cf6006c14eaea547e + $ josh-filter -s :adapt=submodules:link:/libs:export:prune=trivial-merge master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :export + [7] :prune=trivial-merge + [9] :/libs + [34] sequence_number + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + $ josh-filter -s :adapt=submodules:link:/modules/another master + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :/another + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :/modules + [7] :export + [7] :prune=trivial-merge + [9] :/libs + [38] sequence_number + $ git log --graph --pretty=%s:%H FILTERED_HEAD + * update libs submodule:91175b1309708c7a2ce159f274da8b9a011310ce + |\ + | * add file3.txt:ba5a5509adebeb19574c8abb6d4194d1744ef3f4 + * add another submodule:88584f5d636e6478f0ddec62e6b665625c7a3350 + * add another.txt:8fbd01fa31551a059e280f68ac37397712feb59e + + $ josh-filter -s :adapt=submodules:link master --update refs/heads/testsubexport + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :/another + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :/modules + [7] :export + [7] :prune=trivial-merge + [9] :/libs + [38] sequence_number + $ rm -Rf libs + $ rm -Rf modules + $ git checkout testsubexport + Switched to branch 'testsubexport' + $ echo "fo" > libs/foobar.txt + $ git add . + $ git commit -m "mod libs submodule" 1> /dev/null + $ git log --graph --pretty=%s:%H + * mod libs submodule:5a0e5c34f8199e745ba699b4c0423756b18fb1a0 + * update libs submodule:4657e0d71754bdd097c6453a75a5084b467baf54 + |\ + | * add file4.txt:2278f620291df7133299176efb6210fc193c3387 + | * add file3.txt:deed71e37198bf3c8668fa353c66d79c0de25834 + * | add another submodule:b0153ca36fe220c8e0942ee5daf51512907108ca + |\ \ + | * | add another.txt:1f2b84bfd4029e70d3aeb16a6ecb7f0a0490490e + | / + * | add libs submodule:d7b5b1dad9444f25b5011d9f25af2e48a82ff173 + |\| + | * add bar with file:2926fa3361cec2d5695a119fcc3592f4214af3ba + | * add foo with files:e975fd8cd3f2d2de81884f5b761cc0ac150bdf47 + * add main.txt:c404a74092888a14d109c8211576d2c50fc2affd + * init:01d3837a9f7183df88e956cc81f085544f9c6563 + $ josh-filter -s ":/libs:export:prune=trivial-merge" --update refs/heads/testsubexported + [1] :embed=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :/another + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :/modules + [8] :export + [8] :prune=trivial-merge + [10] :/libs + [41] sequence_number + + $ git checkout testsubexported + Switched to branch 'testsubexported' + $ git log --graph --pretty=%s:%H + * mod libs submodule:005cde5c84fbcf17526a0e2fec0a2932c4ce8f24 + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + + $ git rebase 3061af908a0dc1417902fbd7208bb2b8dc354e6c + Current branch testsubexported is up to date. + $ git log --graph --pretty=%s:%H + * mod libs submodule:005cde5c84fbcf17526a0e2fec0a2932c4ce8f24 + * add file4.txt:3061af908a0dc1417902fbd7208bb2b8dc354e6c + * add file3.txt:411907f127aa115588a614ec1dff6ee3c4696173 + * add bar with file:00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd + * add foo with files:4b63f3e50a3a34404541bc4519a3a1a0a8e6f738 + + $ josh-filter -s ":adapt=submodules:link:unlink" refs/heads/master --update refs/heads/unlinked_master + [1] :embed=modules/another + [1] :prefix=modules/another + [1] :unapply(daa965c7c3a3f8289819a728d6c0f31f0590dc6c:/modules/another) + [2] ::modules/another/.josh-link.toml + [2] :embed=libs + [2] :unapply(06d10a853b133ffc533e8ec3f2ed4ec43b64670c:/libs) + [3] ::libs/.josh-link.toml + [4] :/another + [4] :prefix=libs + [4] :unapply(f4bfdb82ca5e0f06f941f68be2a0fd19573bc415:/libs) + [5] :"{commit}" + [5] :adapt=submodules + [5] :link=embedded + [7] :/modules + [8] :export + [8] :prune=trivial-merge + [10] :/libs + [10] :unlink + [41] sequence_number + + $ git log --graph --pretty=%s:%H:%T refs/heads/unlinked_master + * update libs submodule:41ccc704fcbdd815b6849b9927f584cf4c9f6f0e:03b3f655c4ce3f3d8a6fba82bba301c59cc1d957 + |\ + | * add file4.txt:2278f620291df7133299176efb6210fc193c3387:10ff89ee90260a4398c847e6c9448ee6a9f8e4c7 + | * add file3.txt:deed71e37198bf3c8668fa353c66d79c0de25834:9122c83968648c7219e0fee04263e0fce0e45c55 + * | add another submodule:67f677db1f181d00fd3f82baf39b095b73c74634:956f44c3fddbdc526cbf74825ae07c83bde636fd + |\ \ + | * | add another.txt:1f2b84bfd4029e70d3aeb16a6ecb7f0a0490490e:1dadb4c5c0484717f15a05e2c4fbcf26a134fbd4 + | / + * | add libs submodule:0465a38d195eab5390c82865a90a6cc986a52a72:a860693798b958c292bddba8b9f9c64f5b1f8680 + |\| + | * add bar with file:2926fa3361cec2d5695a119fcc3592f4214af3ba:0b7130f9c4103e0b89fd511f432114ef2ebd33e9 + | * add foo with files:e975fd8cd3f2d2de81884f5b761cc0ac150bdf47:1fbe431508b38e48268466d9bb922b979e173ca9 + * add main.txt:c404a74092888a14d109c8211576d2c50fc2affd:1eedb83532c1049f67f2d851fe666e23dee45a6f + * init:01d3837a9f7183df88e956cc81f085544f9c6563:4b825dc642cb6eb9a060e54bf8d69288fbee4904 + + +Test Adapt on repo without submodules (should be no-op) + + $ cd ${TESTTMP} + $ git init -q no-submodules 1> /dev/null + $ cd no-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :adapt=submodules + [1] :link=embedded + [1] sequence_number + $ git ls-tree --name-only -r refs/josh/filter/master + file.txt + + $ git show refs/josh/filter/master:file.txt + content + +Test Adapt on repo with .gitmodules but no actual submodule entries + + $ cd ${TESTTMP} + $ git init -q empty-submodules 1> /dev/null + $ cd empty-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ cat > .gitmodules < /dev/null + $ cd main-repo + $ git config protocol.file.allow always + $ echo "main content" > main.txt + $ git add main.txt + $ git commit -m "add main.txt" 1> /dev/null + + $ cd ${TESTTMP} + $ git init -q submodule-repo 1> /dev/null + $ cd submodule-repo + $ git config protocol.file.allow always + $ mkdir -p foo bar + $ echo "foo content" > foo/file1.txt + $ echo "bar content" > bar/file2.txt + $ git add . + $ git commit -m "add libs" 1> /dev/null + + $ cd ${TESTTMP}/main-repo + $ git submodule add ../submodule-repo libs + Cloning into '/tmp/prysk-tests-*/link.t/main-repo/libs'... (glob) + done. + $ git add .gitmodules libs + $ git commit -m "add libs submodule" 1> /dev/null + + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + + $ josh-filter -s :adapt=submodules:link master --update refs/josh/filter/master + [1] :embed=libs + [1] :unapply(a1520c70819abcbe295fe431e4b88cf56f5a0c95:/libs) + [2] :"{commit}" + [2] ::libs/.josh-link.toml + [2] :adapt=submodules + [2] :link=embedded + [7] sequence_number + $ git ls-tree -r --name-only refs/josh/filter/master + libs/.josh-link.toml + libs/bar/file2.txt + libs/foo/file1.txt + main.txt + + $ git show refs/josh/filter/master:libs/foo/file1.txt + foo content + + $ git show refs/josh/filter/master:libs/bar/file2.txt + bar content + +Test Link on repo without submodules (should be no-op) + + $ cd ${TESTTMP} + $ git init -q no-submodules 1> /dev/null + $ cd no-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ josh-filter -s :link master --update refs/josh/filter/master + [1] :link=embedded + [1] sequence_number + $ git ls-tree -r --name-only refs/josh/filter/master + file.txt diff --git a/tests/filter/lookup.t b/tests/filter/lookup.t new file mode 100644 index 000000000..efb64f4b6 --- /dev/null +++ b/tests/filter/lookup.t @@ -0,0 +1,117 @@ + $ export TERM=dumb + $ export RUST_LOG_STYLE=never + + $ git init -q real_repo 1> /dev/null + $ cd real_repo + + $ mkdir sub1 + $ echo contents1 > sub1/file1 + $ git add sub1 + $ git commit -m "add file1" 1> /dev/null + + $ mkdir sub1 + mkdir: cannot create directory 'sub1': File exists + [1] + $ echo contents2 > sub1/file2 + $ git add sub1 + $ git commit -m "add file2" 1> /dev/null + + $ git log --graph --pretty=%H + * 81b10fb4984d20142cd275b89c91c346e536876a + * bb282e9cdc1b972fffd08fd21eead43bc0c83cb8 + + $ mkdir table + $ echo ":prefix=x" > table/81b10fb4984d20142cd275b89c91c346e536876a + $ echo ":prefix=y" > table/bb282e9cdc1b972fffd08fd21eead43bc0c83cb8 + $ git add table + $ git commit -m "add lookup table" 1> /dev/null + + + $ echo contents3 > sub1/file3 + $ git add sub1 + $ git commit -m "add file3" 1> /dev/null + + $ git log --graph --pretty=%H + * 26e4c43675b985689e280bc42264a9226af76943 + * 14c74c5eca73952b36d736034b388832748c49d6 + * 81b10fb4984d20142cd275b89c91c346e536876a + * bb282e9cdc1b972fffd08fd21eead43bc0c83cb8 + + $ josh-filter -s ":lookup=table" --update refs/heads/filtered + [1] :lookup=table + [2] :/table + [4] :lookup2=4880528e9d57aa5efc925e120a8077bfa37d778d + + $ git log refs/heads/filtered --graph --pretty=%s + * add file2 + * add file1 + $ git diff ${EMPTY_TREE}..refs/heads/filtered + diff --git a/x/sub1/file1 b/x/sub1/file1 + new file mode 100644 + index 0000000..a024003 + --- /dev/null + +++ b/x/sub1/file1 + @@ -0,0 +1 @@ + +contents1 + diff --git a/x/sub1/file2 b/x/sub1/file2 + new file mode 100644 + index 0000000..6b46faa + --- /dev/null + +++ b/x/sub1/file2 + @@ -0,0 +1 @@ + +contents2 + $ git diff ${EMPTY_TREE}..refs/heads/filtered~1 + diff --git a/y/sub1/file1 b/y/sub1/file1 + new file mode 100644 + index 0000000..a024003 + --- /dev/null + +++ b/y/sub1/file1 + @@ -0,0 +1 @@ + +contents1 + + $ echo ":prefix=z" > table/14c74c5eca73952b36d736034b388832748c49d6 + $ echo ":prefix=z" > table/26e4c43675b985689e280bc42264a9226af76943 + $ git add table + $ git commit -m "mod lookup table" 1> /dev/null + $ tree table + table + |-- 14c74c5eca73952b36d736034b388832748c49d6 + |-- 26e4c43675b985689e280bc42264a9226af76943 + |-- 81b10fb4984d20142cd275b89c91c346e536876a + `-- bb282e9cdc1b972fffd08fd21eead43bc0c83cb8 + + 1 directory, 4 files + + $ josh-filter -s ":lookup=table" --update refs/heads/filtered + Warning: reference refs/heads/filtered wasn't updated + [2] :lookup=table + [3] :/table + [4] :lookup2=4880528e9d57aa5efc925e120a8077bfa37d778d + [5] :lookup2=ed934c124e28c83270d9cfbb011f3ceb46c0f69e + $ git log refs/heads/filtered --graph --pretty=%s + * add file2 + * add file1 + + $ git diff ${EMPTY_TREE}..refs/heads/filtered + diff --git a/x/sub1/file1 b/x/sub1/file1 + new file mode 100644 + index 0000000..a024003 + --- /dev/null + +++ b/x/sub1/file1 + @@ -0,0 +1 @@ + +contents1 + diff --git a/x/sub1/file2 b/x/sub1/file2 + new file mode 100644 + index 0000000..6b46faa + --- /dev/null + +++ b/x/sub1/file2 + @@ -0,0 +1 @@ + +contents2 + $ git diff ${EMPTY_TREE}..refs/heads/filtered~1 + diff --git a/y/sub1/file1 b/y/sub1/file1 + new file mode 100644 + index 0000000..a024003 + --- /dev/null + +++ b/y/sub1/file1 + @@ -0,0 +1 @@ + +contents1 diff --git a/tests/filter/unsubmodule.t b/tests/filter/unsubmodule.t new file mode 100644 index 000000000..e818e2bd3 --- /dev/null +++ b/tests/filter/unsubmodule.t @@ -0,0 +1,216 @@ + $ export TESTTMP=${PWD} + + $ cd ${TESTTMP} + $ git init -q submodule-repo 1> /dev/null + $ cd submodule-repo + + $ mkdir -p foo + $ echo "foo content" > foo/file1.txt + $ echo "bar content" > foo/file2.txt + $ git add foo + $ git commit -m "add foo with files" 1> /dev/null + + $ mkdir -p bar + $ echo "baz content" > bar/file3.txt + $ git add bar + $ git commit -m "add bar with file" 1> /dev/null + + $ cd ${TESTTMP} + $ git init -q main-repo 1> /dev/null + $ cd main-repo + $ git commit -m "init" --allow-empty 1> /dev/null + + $ echo "main content" > main.txt + $ git add main.txt + $ git commit -m "add main.txt" 1> /dev/null + + $ git submodule add ../submodule-repo libs 2> /dev/null + $ git submodule status + 00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd libs (heads/master) + + $ git commit -m "add libs submodule" 1> /dev/null + + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + + $ git log --graph --pretty=%s + * add libs submodule + * add main.txt + * init + + $ git ls-tree --name-only -r HEAD + .gitmodules + libs + main.txt + + $ cat .gitmodules + [submodule "libs"] + path = libs + url = ../submodule-repo + + $ git ls-tree HEAD + 100644 blob 5255711b4fd563af2d873bf3c8f9da6c37ce1726\t.gitmodules (esc) + 160000 commit 00c8fe9f1bb75a3f6280992ec7c3c893d858f5dd\tlibs (esc) + 100644 blob bcb9dcad21591bd9284afbb6c21e6d69eafe8f15\tmain.txt (esc) + +Test Adapt filter - should expand submodule into actual tree content + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [3] :adapt=submodules + [3] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * add libs submodule + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + main.txt + + $ git ls-tree refs/josh/filter/master + 040000 tree 34bc8209dca31283563d5519e297ae8cc7f0f19a\tlibs (esc) + 100644 blob bcb9dcad21591bd9284afbb6c21e6d69eafe8f15\tmain.txt (esc) + + $ git ls-tree refs/josh/filter/master libs + 040000 tree 34bc8209dca31283563d5519e297ae8cc7f0f19a\tlibs (esc) + + $ git ls-tree refs/josh/filter/master libs/foo + + $ git ls-tree refs/josh/filter/master libs/bar + + $ git show refs/josh/filter/master:libs/foo/file1.txt + fatal: path 'libs/foo/file1.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + + $ git show refs/josh/filter/master:libs/foo/file2.txt + fatal: path 'libs/foo/file2.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + + $ git show refs/josh/filter/master:libs/bar/file3.txt + fatal: path 'libs/bar/file3.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + +Test that .gitmodules file is removed after unsubmodule + + $ git ls-tree refs/josh/filter/master | grep gitmodules + [1] + +Test Adapt with multiple submodules + + $ cd ${TESTTMP} + $ git init -q another-submodule 1> /dev/null + $ cd another-submodule + $ echo "another content" > another.txt + $ git add another.txt + $ git commit -m "add another.txt" 1> /dev/null + + $ cd ${TESTTMP}/main-repo + $ git submodule add ../another-submodule modules/another 2> /dev/null + $ git commit -m "add another submodule" 1> /dev/null + + $ git fetch ../another-submodule + From ../another-submodule + * branch HEAD -> FETCH_HEAD + + $ cat .gitmodules + [submodule "libs"] + path = libs + url = ../submodule-repo + [submodule "modules/another"] + path = modules/another + url = ../another-submodule + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [4] :adapt=submodules + [4] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * add another submodule + * add libs submodule + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + main.txt + modules/another/.josh-link.toml + + $ git ls-tree refs/josh/filter/master modules + 040000 tree 9dd65d88b3c43c244c71187f86c40f77e771e432\tmodules (esc) + + $ git show refs/josh/filter/master:modules/another/another.txt + fatal: path 'modules/another/another.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + +Test Adapt with submodule changes - add commits to submodule and update + + $ cd ${TESTTMP}/submodule-repo + $ mkdir -p libs/foo libs/bar + $ echo "new content" > libs/foo/file3.txt + $ git add libs/foo/file3.txt + $ git commit -m "add file3.txt" 1> /dev/null + + $ echo "another new content" > libs/bar/file4.txt + $ git add libs/bar/file4.txt + $ git commit -m "add file4.txt" 1> /dev/null + + $ cd ${TESTTMP}/main-repo + $ git fetch ../submodule-repo + From ../submodule-repo + * branch HEAD -> FETCH_HEAD + $ git submodule update --remote libs + From /tmp/prysk-tests-*/unsubmodule.t/submodule-repo (glob) + 00c8fe9..47f1d80 master -> origin/master + Submodule path 'libs': checked out '47f1d800e93b0892d3bc525632c9ffc8d32eeb4c' + $ git add libs + $ git commit -m "update libs submodule" 1> /dev/null + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [5] :adapt=submodules + [5] sequence_number + $ git log --graph --pretty=%s refs/josh/filter/master + * update libs submodule + * add another submodule + * add libs submodule + * add main.txt + * init + $ git ls-tree --name-only -r refs/josh/filter/master + libs/.josh-link.toml + main.txt + modules/another/.josh-link.toml + + $ git show refs/josh/filter/master:libs/libs/foo/file3.txt + fatal: path 'libs/libs/foo/file3.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + + $ git show refs/josh/filter/master:libs/libs/bar/file4.txt + fatal: path 'libs/libs/bar/file4.txt' exists on disk, but not in 'refs/josh/filter/master' + [128] + +Test Adapt on repo without submodules (should be no-op) + + $ cd ${TESTTMP} + $ git init -q no-submodules 1> /dev/null + $ cd no-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ josh-filter -s :adapt=submodules master --update refs/josh/filter/master + [1] :adapt=submodules + [1] sequence_number + $ git ls-tree --name-only -r refs/josh/filter/master + file.txt + + $ git show refs/josh/filter/master:file.txt + content + +Test Adapt on repo with .gitmodules but no actual submodule entries + + $ cd ${TESTTMP} + $ git init -q empty-submodules 1> /dev/null + $ cd empty-submodules + $ echo "content" > file.txt + $ git add file.txt + $ git commit -m "add file" 1> /dev/null + + $ cat > .gitmodules < /dev/null + warning: You appear to have cloned an empty repository. + $ cd real_repo + + $ mkdir sub1 + $ echo contents1 > sub1/file1 + $ git add sub1 + $ git commit -m "add file1" 1> /dev/null + $ git push 1> /dev/null + To http://localhost:8001/real_repo.git + * [new branch] master -> master + + $ cd ${TESTTMP} + + $ git clone -q http://localhost:8002/real_repo.git:/sub1.git + $ cd sub1 + + $ echo contents2 > file2 + $ git add file2 + $ git commit -m "add file2" 1> /dev/null + $ git checkout --orphan orphan_branch 1> /dev/null + Switched to a new branch 'orphan_branch' + $ echo unrelated > orphan_file + $ git add orphan_file + $ git commit -m "orphan commit" 1> /dev/null + $ git checkout master 1> /dev/null + Switched to branch 'master' + $ git merge --no-ff --allow-unrelated-histories -m "merge orphan" orphan_branch 1> /dev/null + $ git push origin HEAD:refs/heads/new_branch 2>&1 >/dev/null | sed -e 's/[ ]*$//g' + remote: josh-proxy: pre-receive hook + remote: upstream: response status: 500 Internal Server Error + remote: upstream: response body: + remote: + remote: Reference "refs/heads/new_branch" does not exist on remote. + remote: If you want to create it, pass "-o base=" or "-o base=path/to/ref" + remote: to specify a base branch/reference. + remote: + remote: error: hook declined to update refs/heads/new_branch + To http://localhost:8002/real_repo.git:/sub1.git + ! [remote rejected] HEAD -> new_branch (hook declined) + error: failed to push some refs to 'http://localhost:8002/real_repo.git:/sub1.git' + + $ git push -o base=refs/heads/master -o allow_orphans origin HEAD:refs/heads/new_branch 2>&1 >/dev/null | sed -e 's/[ ]*$//g' + remote: josh-proxy: pre-receive hook + remote: upstream: response status: 200 OK + remote: upstream: response body: + remote: + remote: To http://localhost:8001/real_repo.git + remote: * [new branch] JOSH_PUSH -> new_branch + To http://localhost:8002/real_repo.git:/sub1.git + * [new branch] HEAD -> new_branch + +$ curl -s http://localhost:8002/flush +Flushed credential cache + $ git push + remote: josh-proxy: pre-receive hook + remote: upstream: response status: 500 Internal Server Error + remote: upstream: response body: + remote: + remote: Rejecting new orphan branch at "merge orphan" (b960d4fb2014cdabe5caa60b6e3bf8e3f1ee5a05) + remote: Specify one of these options: + remote: '-o allow_orphans' to keep the history as is + remote: '-o merge' to import new history by creating merge commit + remote: '-o edit' if you are editing a stored filter or workspace + remote: + remote: error: hook declined to update refs/heads/master + To http://localhost:8002/real_repo.git:/sub1.git + ! [remote rejected] master -> master (hook declined) + error: failed to push some refs to 'http://localhost:8002/real_repo.git:/sub1.git' + [1] + + $ git push -o allow_orphans + remote: josh-proxy: pre-receive hook + remote: upstream: response status: 200 OK + remote: upstream: response body: + remote: + remote: To http://localhost:8001/real_repo.git + remote: bb282e9..e61d37d JOSH_PUSH -> master + To http://localhost:8002/real_repo.git:/sub1.git + 0b4cf6c..b960d4f master -> master + + $ cd ${TESTTMP}/real_repo + $ git pull --rebase + From http://localhost:8001/real_repo + bb282e9..e61d37d master -> origin/master + * [new branch] new_branch -> origin/new_branch + Updating bb282e9..e61d37d + Fast-forward + sub1/file2 | 1 + + sub1/orphan_file | 1 + + 2 files changed, 2 insertions(+) + create mode 100644 sub1/file2 + create mode 100644 sub1/orphan_file + + $ git log --graph --pretty=%s + * merge orphan + |\ + | * orphan commit + * add file2 + * add file1 + + $ tree + . + `-- sub1 + |-- file1 + |-- file2 + `-- orphan_file + + 2 directories, 3 files + + $ cat sub1/file2 + contents2 + +Make sure all temporary namespace got removed + $ tree ${TESTTMP}/remote/scratch/real_repo.git/refs/ | grep request_ + [1] + + $ bash ${TESTDIR}/destroy_test_env.sh + "real_repo.git" = [ + ":/sub1", + "::sub1/", + ] + . + |-- josh + | `-- 24 + | `-- sled + | |-- blobs + | |-- conf + | `-- db + |-- mirror + | |-- FETCH_HEAD + | |-- HEAD + | |-- config + | |-- description + | |-- info + | | `-- exclude + | |-- objects + | | |-- 3d + | | | `-- 77ff51363c9825cc2a221fc0ba5a883a1a2c72 + | | |-- 6b + | | | `-- 46faacade805991bcaea19382c9d941828ce80 + | | |-- 81 + | | | `-- b10fb4984d20142cd275b89c91c346e536876a + | | |-- a0 + | | | `-- 24003ee1acc6bf70318a46e7b6df651b9dc246 + | | |-- a4 + | | | `-- ae8248b2e96725156258b90ced9e841dfd20d1 + | | |-- b1 + | | | `-- d5238086b7f07024d8ed47360e3ce161d9b288 + | | |-- ba + | | | `-- 7e17233d9f79c96cb694959eb065302acd96a6 + | | |-- bb + | | | `-- 282e9cdc1b972fffd08fd21eead43bc0c83cb8 + | | |-- c2 + | | | `-- 1c9352f7526e9576892a6631e0e8cf1fccd34d + | | |-- c6 + | | | `-- 27a2e3a6bfbb7307f522ad94fdfc8c20b92967 + | | |-- c8 + | | | `-- 2fc150c43f13cc56c0e9caeba01b58ec612022 + | | |-- d8 + | | | `-- 43530e8283da7185faac160347db5c70ef4e18 + | | |-- e6 + | | | `-- 1d37de15923090979cf667263aefa07f78cc33 + | | |-- info + | | `-- pack + | `-- refs + | |-- heads + | |-- josh + | | `-- upstream + | | `-- real_repo.git + | | |-- HEAD + | | `-- refs + | | `-- heads + | | |-- master + | | `-- new_branch + | `-- tags + `-- overlay + |-- HEAD + |-- config + |-- description + |-- info + | `-- exclude + |-- objects + | |-- 0b + | | `-- 4cf6c9efbbda1eada39fa9c1d21d2525b027bb + | |-- 4b + | | `-- 825dc642cb6eb9a060e54bf8d69288fbee4904 + | |-- 6b + | | `-- 46faacade805991bcaea19382c9d941828ce80 + | |-- 81 + | | `-- b10fb4984d20142cd275b89c91c346e536876a + | |-- a4 + | | `-- ae8248b2e96725156258b90ced9e841dfd20d1 + | |-- b1 + | | `-- d5238086b7f07024d8ed47360e3ce161d9b288 + | |-- b9 + | | `-- 60d4fb2014cdabe5caa60b6e3bf8e3f1ee5a05 + | |-- ba + | | `-- 7e17233d9f79c96cb694959eb065302acd96a6 + | |-- c2 + | | `-- 1c9352f7526e9576892a6631e0e8cf1fccd34d + | |-- c6 + | | `-- 27a2e3a6bfbb7307f522ad94fdfc8c20b92967 + | |-- d8 + | | |-- 388f5880393d255b371f1ed9b801d35620017e + | | `-- 43530e8283da7185faac160347db5c70ef4e18 + | |-- df + | | `-- b06d7748772bdd407c5911c0ba02b0f5fb31a4 + | |-- e6 + | | `-- 1d37de15923090979cf667263aefa07f78cc33 + | |-- info + | `-- pack + `-- refs + |-- heads + |-- namespaces + `-- tags + + 53 directories, 41 files + +$ cat ${TESTTMP}/josh-proxy.out diff --git a/tests/proxy/push_stacked.t b/tests/proxy/push_stacked.t index 901aa970c..4b73874b3 100644 --- a/tests/proxy/push_stacked.t +++ b/tests/proxy/push_stacked.t @@ -56,15 +56,15 @@ $ git commit -m "add file3" 1> /dev/null $ git push -o author=josh@example.com origin master:refs/stack/for/master remote: josh-proxy: pre-receive hook - remote: upstream: response status: 500 Internal Server Error + remote: upstream: response status: 200 OK remote: upstream: response body: remote: - remote: rejecting to push 3ad32b3bd3bb778441e7eae43930d8dc6293eddc without id - remote: error: hook declined to update refs/stack/for/master + remote: Everything up-to-date + remote: Everything up-to-date + remote: To http://localhost:8001/real_repo.git + remote: ec41aad..3ad32b3 master -> @heads/master/josh@example.com To http://localhost:8002/real_repo.git - ! [remote rejected] master -> refs/stack/for/master (hook declined) - error: failed to push some refs to 'http://localhost:8002/real_repo.git' - [1] + * [new reference] master -> refs/stack/for/master $ git push -o author=foo@example.com origin master:refs/stack/for/master remote: josh-proxy: pre-receive hook remote: upstream: response status: 200 OK @@ -85,8 +85,8 @@ * [new branch] @heads/master/josh@example.com -> origin/@heads/master/josh@example.com $ git log --decorate --graph --pretty="%s %d" - * add file3 (HEAD -> master, origin/@heads/master/foo@example.com) - * Change-Id: foo7 (origin/@heads/master/josh@example.com, origin/@changes/master/josh@example.com/foo7) + * add file3 (HEAD -> master, origin/@heads/master/josh@example.com, origin/@heads/master/foo@example.com) + * Change-Id: foo7 (origin/@changes/master/josh@example.com/foo7) * Change-Id: 1234 (origin/@changes/master/josh@example.com/1234) * add file1 (origin/master, origin/HEAD) @@ -119,7 +119,7 @@ get listed if they differ from HEAD 3b0e3dbefd779ec54d92286047f32d3129161c0d\trefs/heads/@changes/master/josh@example.com/1234 (esc) ec41aad70b4b898baf48efeb795a7753d9674152\trefs/heads/@changes/master/josh@example.com/foo7 (esc) 3ad32b3bd3bb778441e7eae43930d8dc6293eddc\trefs/heads/@heads/master/foo@example.com (esc) - ec41aad70b4b898baf48efeb795a7753d9674152\trefs/heads/@heads/master/josh@example.com (esc) + 3ad32b3bd3bb778441e7eae43930d8dc6293eddc\trefs/heads/@heads/master/josh@example.com (esc) 4950fa502f51b7bfda0d7975dbff9b0f9a9481ca\trefs/heads/master (esc) $ git ls-remote http://localhost:8002/real_repo.git:/sub1.git diff --git a/tests/proxy/push_stacked_gerrit.t b/tests/proxy/push_stacked_gerrit.t index 4f9ce8e2b..5291a202b 100644 --- a/tests/proxy/push_stacked_gerrit.t +++ b/tests/proxy/push_stacked_gerrit.t @@ -63,15 +63,17 @@ $ git commit -m "add file3" 1> /dev/null $ git push -o author=josh@example.com origin master:refs/for/master remote: josh-proxy: pre-receive hook - remote: upstream: response status: 500 Internal Server Error + remote: upstream: response status: 200 OK remote: upstream: response body: remote: - remote: rejecting to push 3ad32b3bd3bb778441e7eae43930d8dc6293eddc without id - remote: error: hook declined to update refs/for/master + remote: Everything up-to-date + remote: Everything up-to-date + remote: To http://localhost:8001/real_repo.git + remote: ec41aad..3ad32b3 JOSH_PUSH -> refs/for/master + remote: To http://localhost:8001/real_repo.git + remote: ec41aad..3ad32b3 master -> @heads/master/josh@example.com To http://localhost:8002/real_repo.git - ! [remote rejected] master -> refs/for/master (hook declined) - error: failed to push some refs to 'http://localhost:8002/real_repo.git' - [1] + * [new reference] master -> refs/for/master $ git push http://localhost:8001/real_repo.git :refs/for/master To http://localhost:8001/real_repo.git - [deleted] refs/for/master @@ -97,8 +99,8 @@ * [new branch] @heads/master/josh@example.com -> origin/@heads/master/josh@example.com $ git log --decorate --graph --pretty="%s %d" - * add file3 (HEAD -> master, origin/@heads/master/foo@example.com) - * Change-Id: foo7 (origin/@heads/master/josh@example.com, origin/@changes/master/josh@example.com/foo7) + * add file3 (HEAD -> master, origin/@heads/master/josh@example.com, origin/@heads/master/foo@example.com) + * Change-Id: foo7 (origin/@changes/master/josh@example.com/foo7) * Change-Id: 1234 (origin/@changes/master/josh@example.com/1234) * add file1 (origin/master, origin/HEAD) @@ -111,8 +113,8 @@ * [new branch] @heads/master/josh@example.com -> origin/@heads/master/josh@example.com $ git checkout -q @heads/master/foo@example.com $ git log --decorate --graph --pretty="%s %d" - * add file3 (HEAD -> @heads/master/foo@example.com, origin/@heads/master/foo@example.com) - * Change-Id: foo7 (origin/@heads/master/josh@example.com, origin/@changes/master/josh@example.com/foo7) + * add file3 (HEAD -> @heads/master/foo@example.com, origin/@heads/master/josh@example.com, origin/@heads/master/foo@example.com) + * Change-Id: foo7 (origin/@changes/master/josh@example.com/foo7) * Change-Id: 1234 (origin/@changes/master/josh@example.com/1234) * add file1 (origin/master, origin/HEAD, master) diff --git a/tests/proxy/push_stacked_sub.t b/tests/proxy/push_stacked_sub.t index 234aa3807..724b59983 100644 --- a/tests/proxy/push_stacked_sub.t +++ b/tests/proxy/push_stacked_sub.t @@ -73,15 +73,15 @@ $ git commit -m "add file3" 1> /dev/null $ git push -o author=josh@example.com origin master:refs/stack/for/master remote: josh-proxy: pre-receive hook - remote: upstream: response status: 500 Internal Server Error + remote: upstream: response status: 200 OK remote: upstream: response body: remote: - remote: rejecting to push a3065162ecee0fecc977ec04a275e10b5e15a39c without id - remote: error: hook declined to update refs/stack/for/master + remote: Everything up-to-date + remote: Everything up-to-date + remote: To http://localhost:8001/real_repo.git + remote: 2bb9471..a306516 master -> @heads/master/josh@example.com To http://localhost:8002/real_repo.git:/sub1.git - ! [remote rejected] master -> refs/stack/for/master (hook declined) - error: failed to push some refs to 'http://localhost:8002/real_repo.git:/sub1.git' - [1] + * [new reference] master -> refs/stack/for/master $ curl -s http://localhost:8002/flush Flushed credential cache @@ -94,8 +94,8 @@ * [new branch] @heads/master/josh@example.com -> origin/@heads/master/josh@example.com * [new branch] @heads/other_branch/josh@example.com -> origin/@heads/other_branch/josh@example.com $ git log --decorate --graph --pretty="%s %d" - * add file3 (HEAD -> master) - * Change-Id: foo7 (origin/@heads/other_branch/josh@example.com, origin/@heads/master/josh@example.com, origin/@changes/other_branch/josh@example.com/foo7, origin/@changes/master/josh@example.com/foo7) + * add file3 (HEAD -> master, origin/@heads/master/josh@example.com) + * Change-Id: foo7 (origin/@heads/other_branch/josh@example.com, origin/@changes/other_branch/josh@example.com/foo7, origin/@changes/master/josh@example.com/foo7) * Change-Id: 1234 (origin/@changes/other_branch/josh@example.com/1234, origin/@changes/master/josh@example.com/1234) * add file1 (origin/master, origin/HEAD) @@ -151,8 +151,12 @@ Make sure all temporary namespace got removed | | | `-- 77ff51363c9825cc2a221fc0ba5a883a1a2c72 | | |-- 6b | | | `-- 46faacade805991bcaea19382c9d941828ce80 + | | |-- 88 + | | | `-- 2b84c5d3241087bc41982a744b72b7a174c49e | | |-- a0 | | | `-- 24003ee1acc6bf70318a46e7b6df651b9dc246 + | | |-- a3 + | | | `-- 065162ecee0fecc977ec04a275e10b5e15a39c | | |-- b2 | | | `-- ea883bc5df63565960a38cad7a57f73ac66eaa | | |-- ba @@ -160,6 +164,8 @@ Make sure all temporary namespace got removed | | | `-- c8af20b53d712874a32944874c66a21afa91f9 | | |-- bb | | | `-- 282e9cdc1b972fffd08fd21eead43bc0c83cb8 + | | |-- be + | | | `-- 33ab805ad4ef7ddda5b51e4a78ec0fac6b699a | | |-- c6 | | | `-- 27a2e3a6bfbb7307f522ad94fdfc8c20b92967 | | |-- c8 @@ -233,7 +239,7 @@ Make sure all temporary namespace got removed |-- namespaces `-- tags - 58 directories, 44 files + 61 directories, 47 files $ cat ${TESTTMP}/josh-proxy.out $ cat ${TESTTMP}/josh-proxy.out | grep REPO_UPDATE diff --git a/tests/proxy/workspace_edit_commit.t b/tests/proxy/workspace_edit_commit.t index 976169a59..ef6651642 100644 --- a/tests/proxy/workspace_edit_commit.t +++ b/tests/proxy/workspace_edit_commit.t @@ -159,6 +159,21 @@ $ git push origin HEAD:refs/for/master 2>&1 >/dev/null | sed -e 's/[ ]*$//g' remote: josh-proxy: pre-receive hook + remote: upstream: response status: 500 Internal Server Error + remote: upstream: response body: + remote: + remote: Rejecting new orphan branch at "Add new folders" (5645805dcc75cfe4922b9cb301c40a4a4b35a59d) + remote: Specify one of these options: + remote: '-o allow_orphans' to keep the history as is + remote: '-o merge' to import new history by creating merge commit + remote: '-o edit' if you are editing a stored filter or workspace + remote: + remote: error: hook declined to update refs/for/master + To http://localhost:8002/real_repo.git:workspace=ws.git + ! [remote rejected] HEAD -> refs/for/master (hook declined) + error: failed to push some refs to 'http://localhost:8002/real_repo.git:workspace=ws.git' + $ git push -o edit origin HEAD:refs/for/master 2>&1 >/dev/null | sed -e 's/[ ]*$//g' + remote: josh-proxy: pre-receive hook remote: upstream: response status: 200 OK remote: upstream: response body: remote: diff --git a/tests/proxy/workspace_errors.t b/tests/proxy/workspace_errors.t index c1b4231ce..c3f783475 100644 --- a/tests/proxy/workspace_errors.t +++ b/tests/proxy/workspace_errors.t @@ -106,7 +106,7 @@ Error in filter remote: 1 | a/b = :b/sub2 remote: | ^--- remote: | - remote: = expected EOI, filter_group, filter_subdir, filter_nop, filter_presub, filter, filter_noarg, filter_message, filter_rev, filter_join, filter_replace, or filter_squash + remote: = expected EOI, filter_group, filter_subdir, filter_nop, filter_presub, filter, filter_noarg, filter_message, filter_rev, filter_from, filter_concat, filter_unapply, filter_join, filter_replace, or filter_squash remote: remote: a/b = :b/sub2 remote: c = :/sub1