Skip to main content

worktree_io/
multi_workspace.rs

1//! Multi-repo workspace creation: one unified folder across several repos.
2use anyhow::{Context, Result};
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7
8use crate::{
9    git::{
10        bare_clone, branch_exists_remote, create_worktree, detect_default_branch, git_fetch,
11        git_worktree_prune,
12    },
13    issue::IssueRef,
14    name_gen,
15    ttl::WorkspaceRegistry,
16};
17
18/// A spec for one repo in a multi-workspace.
19pub enum MultiSpec {
20    /// Repo paired with an issue — creates a branch, folder is `<repo>-<id>`.
21    WithIssue(IssueRef),
22    /// Bare repo slug — checked out on its default branch, folder is `<repo>`.
23    BareRepo {
24        /// GitHub owner (org or user).
25        owner: String,
26        /// Repository name.
27        repo: String,
28    },
29}
30
31/// Create a unified workspace under `workspaces_root`, one sub-dir per spec.
32/// Registers the root with `WorkspaceRegistry` for TTL-based pruning.
33/// # Errors
34/// Returns an error if any directory, clone, fetch, or worktree step fails.
35// LLVM_COV_EXCL_START
36pub fn create_multi_workspace(specs: &[MultiSpec], workspaces_root: &Path) -> Result<PathBuf> {
37    let root = workspaces_root.join(name_gen::generate_name());
38    fs::create_dir_all(&root)
39        .with_context(|| format!("failed to create workspace root {}", root.display()))?;
40    for spec in specs {
41        open_one(spec, &root)?;
42    }
43    if let Ok(mut registry) = WorkspaceRegistry::load() {
44        registry.register(root.clone());
45        let _ = registry.save();
46    }
47    Ok(root)
48}
49
50fn github_bare_path(owner: &str, repo: &str) -> PathBuf {
51    dirs::home_dir()
52        .expect("could not determine home directory")
53        .join("worktrees")
54        .join("github")
55        .join(owner)
56        .join(repo)
57}
58
59fn open_one(spec: &MultiSpec, root: &Path) -> Result<()> {
60    match spec {
61        MultiSpec::WithIssue(issue) => open_one_issue(issue, root),
62        MultiSpec::BareRepo { owner, repo } => open_one_bare(owner, repo, root),
63    }
64}
65
66fn open_one_issue(issue: &IssueRef, root: &Path) -> Result<()> {
67    let bare_path = issue.bare_clone_path();
68    if bare_path.exists() {
69        eprintln!("Fetching origin for {}…", bare_path.display());
70        git_fetch(&bare_path)?;
71    } else {
72        eprintln!("Cloning {} (bare)…", issue.clone_url());
73        bare_clone(&issue.clone_url(), &bare_path)?;
74    }
75    let base_branch = detect_default_branch(&bare_path)?;
76    let branch = issue.branch_name();
77    let branch_exists = branch_exists_remote(&bare_path, &branch);
78    let _ = git_worktree_prune(&bare_path);
79    let dest = root.join(issue.multi_dir_name());
80    create_worktree(&bare_path, &dest, &branch, &base_branch, branch_exists)
81        .with_context(|| format!("failed to create worktree at {}", dest.display()))
82}
83
84fn open_one_bare(owner: &str, repo: &str, root: &Path) -> Result<()> {
85    let bare_path = github_bare_path(owner, repo);
86    let url = format!("https://github.com/{owner}/{repo}.git");
87    if bare_path.exists() {
88        eprintln!("Fetching origin for {}…", bare_path.display());
89        git_fetch(&bare_path)?;
90    } else {
91        eprintln!("Cloning {url} (bare)…");
92        bare_clone(&url, &bare_path)?;
93    }
94    let branch = detect_default_branch(&bare_path)?;
95    let _ = git_worktree_prune(&bare_path);
96    let dest = root.join(repo);
97    create_worktree(&bare_path, &dest, &branch, &branch, true)
98        .with_context(|| format!("failed to create worktree at {}", dest.display()))
99}
100// LLVM_COV_EXCL_STOP