Skip to main content

git_workflow/commands/
worktree_pool.rs

1//! `gw worktree pool` commands — Per-leader worktree pool management
2//!
3//! Each leader worktree owns its own pool under `.worktrees/`.
4//! Pool state is derived from the filesystem, not from an inventory file.
5//! Marker files in the per-worktree git dir track acquisition state.
6
7use std::path::{Path, PathBuf};
8
9use crate::error::{GwError, Result};
10use crate::git;
11use crate::output;
12use crate::pool::{PoolEntry, PoolLock, PoolNextAction, PoolState, WorktreeStatus};
13
14/// Directory name under the per-worktree git dir for pool metadata
15const POOL_META_DIR: &str = "pool";
16
17/// Directory name under worktree root for pool worktrees
18const POOL_WORKTREES_DIR: &str = ".worktrees";
19
20/// Setup hook path relative to repo root
21const SETUP_HOOK: &str = ".gw/setup";
22
23/// Subdirectory for acquire markers
24const ACQUIRED_DIR: &str = "acquired";
25
26/// Canonicalize a path, stripping the `\\?\` prefix on Windows so that
27/// external tools (like git) can consume the path without issues.
28fn canonicalize_clean(path: &Path) -> PathBuf {
29    let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
30    #[cfg(target_os = "windows")]
31    {
32        let s = canonical.to_string_lossy();
33        if let Some(stripped) = s.strip_prefix(r"\\?\") {
34            return PathBuf::from(stripped);
35        }
36    }
37    canonical
38}
39
40/// Get the current worktree root (works from main repo or any worktree)
41fn leader_root() -> Result<PathBuf> {
42    let root = git::worktree_root()?;
43    Ok(canonicalize_clean(&root))
44}
45
46/// Get the leader name (the worktree directory name, e.g., "web-2").
47/// Sanitizes the name to be valid as part of a git branch name.
48fn leader_name() -> Result<String> {
49    let root = leader_root()?;
50    let raw = root
51        .file_name()
52        .and_then(|s| s.to_str())
53        .map(String::from)
54        .ok_or_else(|| GwError::Other("Could not determine leader name".to_string()))?;
55    // Strip leading dots (invalid in git branch names)
56    let sanitized = raw.trim_start_matches('.');
57    if sanitized.is_empty() {
58        return Err(GwError::Other(format!(
59            "Leader directory name is not valid for branch naming: {raw}"
60        )));
61    }
62    Ok(sanitized.to_string())
63}
64
65/// Pool entry name prefix for the current leader (e.g., "web-2-pool-")
66fn pool_prefix() -> Result<String> {
67    Ok(format!("{}-pool-", leader_name()?))
68}
69
70/// Get the main repository root (parent of .git), even from inside a worktree.
71fn main_repo_root() -> Result<PathBuf> {
72    let common = git::git_common_dir()?;
73    let common = canonicalize_clean(&common);
74    common
75        .parent()
76        .map(|p| p.to_path_buf())
77        .ok_or_else(|| GwError::Other("Could not determine main repository root".to_string()))
78}
79
80/// Resolve the pool metadata directory (per-worktree: {git_dir}/pool/)
81fn pool_dir() -> Result<PathBuf> {
82    let git_dir = git::git_dir()?;
83    let git_dir = canonicalize_clean(&git_dir);
84    Ok(git_dir.join(POOL_META_DIR))
85}
86
87/// Resolve the acquired markers directory
88fn acquired_dir() -> Result<PathBuf> {
89    Ok(pool_dir()?.join(ACQUIRED_DIR))
90}
91
92/// Resolve the worktrees directory ({leader_root}/.worktrees/)
93fn worktrees_dir() -> Result<PathBuf> {
94    let root = leader_root()?;
95    Ok(root.join(POOL_WORKTREES_DIR))
96}
97
98/// Run the setup hook if it exists
99fn run_setup_hook(repo_root: &Path, worktree_path: &str, verbose: bool) -> Result<()> {
100    let hook = repo_root.join(SETUP_HOOK);
101    if !hook.exists() {
102        return Ok(());
103    }
104
105    if verbose {
106        output::action(&format!("Running setup hook: {}", hook.display()));
107    }
108
109    let status = std::process::Command::new(&hook)
110        .arg(worktree_path)
111        .current_dir(worktree_path)
112        .status()?;
113
114    if !status.success() {
115        return Err(GwError::Other(format!(
116            "Setup hook failed with exit code: {}",
117            status.code().unwrap_or(-1)
118        )));
119    }
120    Ok(())
121}
122
123/// Run a git command inside `path`, returning trimmed stdout on success.
124fn git_capture(path: &Path, args: &[&str]) -> Option<String> {
125    std::process::Command::new("git")
126        .args(args)
127        .current_dir(path)
128        .output()
129        .ok()
130        .filter(|o| o.status.success())
131        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
132}
133
134/// Get the current branch of a worktree by running git in that directory
135fn worktree_current_branch(path: &Path) -> String {
136    git_capture(path, &["rev-parse", "--abbrev-ref", "HEAD"]).unwrap_or_else(|| "???".to_string())
137}
138
139/// Detect an in-progress git operation (rebase/merge/cherry-pick/...) in a
140/// worktree by probing the well-known state files in its git dir.
141fn in_progress_operation(path: &Path) -> Option<String> {
142    // (git-path, human label)
143    const PROBES: &[(&str, &str)] = &[
144        ("rebase-merge", "rebase"),
145        ("rebase-apply", "rebase"),
146        ("MERGE_HEAD", "merge"),
147        ("CHERRY_PICK_HEAD", "cherry-pick"),
148        ("REVERT_HEAD", "revert"),
149        ("BISECT_LOG", "bisect"),
150    ];
151    for (git_path, label) in PROBES {
152        if let Some(resolved) = git_capture(path, &["rev-parse", "--git-path", git_path]) {
153            let candidate = PathBuf::from(&resolved);
154            let full = if candidate.is_absolute() {
155                candidate
156            } else {
157                path.join(candidate)
158            };
159            if full.exists() {
160                return Some((*label).to_string());
161            }
162        }
163    }
164    None
165}
166
167/// Inspect a pool worktree and return the reasons it is NOT in a clean,
168/// returnable state. An empty vec means the worktree is clean:
169///
170/// - checked out on its pool home branch (== entry name),
171/// - no staged/unstaged/untracked changes,
172/// - no in-progress git operation (rebase/merge/cherry-pick/...).
173///
174/// Sync state is intentionally not checked: being behind `origin` is fine
175/// because `acquire` fast-forwards the worktree before handing it out.
176fn worktree_issues(entry: &PoolEntry) -> Vec<String> {
177    let path = entry.path.as_path();
178    let mut issues = Vec::new();
179
180    if !path.exists() {
181        issues.push("worktree directory is missing".to_string());
182        return issues;
183    }
184
185    let branch = worktree_current_branch(path);
186    if branch != entry.branch {
187        issues.push(format!(
188            "on branch '{}', expected pool home branch '{}'",
189            branch, entry.branch
190        ));
191    }
192
193    match git_capture(path, &["status", "--porcelain"]) {
194        Some(s) if !s.trim().is_empty() => {
195            issues.push("has uncommitted or untracked changes".to_string());
196        }
197        None => issues.push("could not read working tree status".to_string()),
198        _ => {}
199    }
200
201    if let Some(op) = in_progress_operation(path) {
202        issues.push(format!("a {op} is in progress"));
203    }
204
205    issues
206}
207
208/// Ensure `.worktrees/` is excluded via `.git/info/exclude`.
209/// This is local-only and never pollutes `.gitignore` or the working tree.
210fn ensure_excluded() -> Result<()> {
211    let common = git::git_common_dir()?;
212    let exclude_path = common.join("info").join("exclude");
213    let entry = ".worktrees/";
214
215    if exclude_path.exists() {
216        let content = std::fs::read_to_string(&exclude_path)?;
217        if content.lines().any(|line| line.trim() == entry) {
218            return Ok(());
219        }
220        let prefix = if content.ends_with('\n') { "" } else { "\n" };
221        std::fs::write(&exclude_path, format!("{content}{prefix}{entry}\n"))?;
222    } else {
223        std::fs::create_dir_all(common.join("info"))?;
224        std::fs::write(&exclude_path, format!("{entry}\n"))?;
225    }
226
227    Ok(())
228}
229
230/// Release a single pool worktree back to the pool.
231///
232/// Just removes the acquire marker. The worktree should have been
233/// cleaned up (gw cleanup) before release.
234fn release_one(entry: &PoolEntry, acquired_dir: &Path) -> Result<()> {
235    let marker = acquired_dir.join(&entry.name);
236    if marker.exists() {
237        std::fs::remove_file(&marker)?;
238    }
239    output::success(&format!("{} released", entry.name));
240    Ok(())
241}
242
243// --- Pool commands ---
244
245/// `gw worktree pool warm <n>`
246pub fn warm(count: usize, verbose: bool) -> Result<()> {
247    if !git::is_git_repo() {
248        return Err(GwError::NotAGitRepository);
249    }
250
251    let pool_dir = pool_dir()?;
252    let wt_dir = worktrees_dir()?;
253    let acquired_dir = acquired_dir()?;
254    let repo_root = main_repo_root()?;
255    let prefix = pool_prefix()?;
256
257    println!();
258    output::info(&format!(
259        "Warming worktree pool to {} available",
260        output::bold(&count.to_string())
261    ));
262
263    // Acquire lock and scan filesystem
264    let _lock = PoolLock::acquire(&pool_dir)?;
265    let mut state = PoolState::scan(&wt_dir, &acquired_dir, &prefix)?;
266
267    let available = state.count_by_status(&WorktreeStatus::Available);
268    let acquired = state.count_by_status(&WorktreeStatus::Acquired);
269    let total = state.entries.len();
270    if available >= count {
271        output::success(&format!(
272            "Pool already has {available} available ({acquired} acquired, {total} total), nothing to do"
273        ));
274        return Ok(());
275    }
276
277    let to_create = count - available;
278
279    // Ensure .worktrees/ is excluded locally (via .git/info/exclude)
280    ensure_excluded()?;
281
282    // Fetch once
283    output::info("Fetching from origin...");
284    git::fetch_prune(verbose)?;
285    output::success("Fetched");
286
287    let default_remote = git::get_default_remote_branch()?;
288
289    // Create worktrees dir
290    std::fs::create_dir_all(&wt_dir)?;
291
292    let mut created = 0;
293    for i in 0..to_create {
294        let name = state.next_name(&prefix);
295        let abs_path = canonicalize_clean(&wt_dir).join(&name);
296        let abs_path_str = abs_path.to_string_lossy().to_string();
297        // Branch name = directory name (gw convention: dir name = home branch)
298        let branch = name.clone();
299
300        output::info(&format!(
301            "[{}/{}] Creating {}...",
302            i + 1,
303            to_create,
304            output::bold(&name)
305        ));
306
307        // Create the worktree
308        if let Err(e) = git::worktree_add(&abs_path_str, &branch, &default_remote, verbose) {
309            output::warn(&format!("Failed to create {name}: {e}"));
310            continue;
311        }
312
313        // Run setup hook
314        if let Err(e) = run_setup_hook(&repo_root, &abs_path_str, verbose) {
315            output::warn(&format!(
316                "Setup hook failed for {name}: {e}. Removing worktree."
317            ));
318            let _ = git::worktree_remove(&abs_path_str, verbose);
319            let _ = git::force_delete_branch(&branch, verbose);
320            continue;
321        }
322
323        // Track in-memory for next_name() to work correctly
324        state.entries.push(PoolEntry {
325            name: name.clone(),
326            path: abs_path,
327            branch,
328            status: WorktreeStatus::Available,
329            owner: None,
330        });
331        created += 1;
332
333        output::success(&format!("[{}/{}] Created {}", i + 1, to_create, name));
334    }
335
336    // Re-scan for accurate final counts
337    let final_state = PoolState::scan(&wt_dir, &acquired_dir, &prefix)?;
338    let total = final_state.entries.len();
339    let available = final_state.count_by_status(&WorktreeStatus::Available);
340
341    println!();
342    output::success(&format!(
343        "Pool warmed: {created} created, {available} available, {total} total"
344    ));
345
346    Ok(())
347}
348
349/// `gw worktree pool acquire`
350pub fn acquire(verbose: bool) -> Result<()> {
351    if !git::is_git_repo() {
352        return Err(GwError::NotAGitRepository);
353    }
354
355    let pool_dir = pool_dir()?;
356    let wt_dir = worktrees_dir()?;
357    let acquired_dir = acquired_dir()?;
358    let prefix = pool_prefix()?;
359
360    if !wt_dir.exists() {
361        return Err(GwError::PoolNotInitialized);
362    }
363
364    let _lock = PoolLock::acquire(&pool_dir)?;
365
366    // Ensure acquired dir exists
367    std::fs::create_dir_all(&acquired_dir)?;
368
369    let state = PoolState::scan(&wt_dir, &acquired_dir, &prefix)?;
370
371    if state.entries.is_empty() {
372        return Err(GwError::PoolNotInitialized);
373    }
374
375    // Inspect each available worktree before handing one out. A dirty available
376    // worktree (e.g. left behind by an agent that crashed without a clean
377    // release) must not be loaned to the next agent — skip it with a warning.
378    // This is the CLI-side last line of defense; release is the first.
379    let available: Vec<&PoolEntry> = state
380        .entries
381        .iter()
382        .filter(|e| e.status == WorktreeStatus::Available)
383        .collect();
384    if available.is_empty() {
385        return Err(GwError::PoolExhausted);
386    }
387    let mut entry = None;
388    for candidate in &available {
389        let issues = worktree_issues(candidate);
390        if issues.is_empty() {
391            entry = Some(*candidate);
392            break;
393        }
394        // Warnings go to stderr so stdout stays "path only".
395        eprintln!(
396            "\x1b[0;33m\u{26a0}\x1b[0m Skipping unclean worktree {}: {}",
397            candidate.name,
398            issues.join("; ")
399        );
400    }
401    let entry = entry.ok_or(GwError::PoolNoCleanWorktree)?;
402
403    // Create marker file with leader name as owner
404    let owner = leader_name()?;
405    std::fs::write(acquired_dir.join(&entry.name), &owner)?;
406
407    // Sync worktree to latest (gw home equivalent)
408    let wt_path = entry.path.to_string_lossy().to_string();
409    git::git_run_in_dir(&wt_path, &["fetch", "--prune"], verbose)?;
410    let default_remote = git::get_default_remote_branch()?;
411    let default_branch = default_remote.strip_prefix("origin/").unwrap_or("main");
412    git::git_run_in_dir(&wt_path, &["pull", "origin", default_branch], verbose)?;
413
414    let path = entry.path.to_string_lossy().to_string();
415    let name = entry.name.clone();
416
417    let remaining = state.count_by_status(&WorktreeStatus::Available) - 1;
418    eprintln!(
419        "\x1b[0;32m\u{2713}\x1b[0m Acquired {} ({} remaining)",
420        name, remaining,
421    );
422
423    // Print ONLY the path to stdout for `path=$(gw worktree pool acquire)`
424    println!("{path}");
425
426    Ok(())
427}
428
429/// `gw worktree pool release [name]`
430///
431/// Removes acquire markers. No git operations — cleanup should have
432/// been run inside the worktree before releasing.
433pub fn release(name: Option<String>, _verbose: bool) -> Result<()> {
434    if !git::is_git_repo() {
435        return Err(GwError::NotAGitRepository);
436    }
437
438    let pool_dir = pool_dir()?;
439    let wt_dir = worktrees_dir()?;
440    let acquired_dir = acquired_dir()?;
441    let prefix = pool_prefix()?;
442
443    if !wt_dir.exists() {
444        return Err(GwError::PoolNotInitialized);
445    }
446
447    let _lock = PoolLock::acquire(&pool_dir)?;
448    let state = PoolState::scan(&wt_dir, &acquired_dir, &prefix)?;
449
450    if state.entries.is_empty() {
451        return Err(GwError::PoolNotInitialized);
452    }
453
454    match name {
455        Some(ref n) => {
456            let entry = state
457                .find_by_name_or_path(n)
458                .ok_or_else(|| GwError::PoolWorktreeNotFound(n.clone()))?;
459
460            if entry.status != WorktreeStatus::Acquired {
461                return Err(GwError::PoolWorktreeNotAcquired(entry.name.clone()));
462            }
463
464            // Inspect before returning to the pool: an explicitly-named dirty
465            // worktree is a hard error so the caller notices and fixes it.
466            let issues = worktree_issues(entry);
467            if !issues.is_empty() {
468                return Err(GwError::PoolWorktreeDirty {
469                    name: entry.name.clone(),
470                    reason: issues.join("; "),
471                });
472            }
473
474            release_one(entry, &acquired_dir)?;
475        }
476        None => {
477            let acquired: Vec<_> = state
478                .entries
479                .iter()
480                .filter(|e| e.status == WorktreeStatus::Acquired)
481                .collect();
482
483            if acquired.is_empty() {
484                return Err(GwError::PoolNoneAcquired);
485            }
486
487            // Release every clean worktree; keep the dirty ones acquired (so
488            // their work can be inspected) and report them at the end.
489            let mut skipped = Vec::new();
490            for entry in &acquired {
491                let issues = worktree_issues(entry);
492                if issues.is_empty() {
493                    release_one(entry, &acquired_dir)?;
494                } else {
495                    output::warn(&format!("Kept {}: {}", entry.name, issues.join("; ")));
496                    skipped.push(entry.name.clone());
497                }
498            }
499
500            if !skipped.is_empty() {
501                return Err(GwError::Other(format!(
502                    "Kept {} unclean worktree(s) acquired: {}. Run `gw cleanup` inside each \
503                     (or fix it), then `gw worktree pool release <name>`.",
504                    skipped.len(),
505                    skipped.join(", ")
506                )));
507            }
508        }
509    }
510
511    // Re-scan for final counts
512    let final_state = PoolState::scan(&wt_dir, &acquired_dir, &prefix)?;
513    let available = final_state.count_by_status(&WorktreeStatus::Available);
514    let acquired_count = final_state.count_by_status(&WorktreeStatus::Acquired);
515    let total = final_state.entries.len();
516
517    println!();
518    output::success(&format!(
519        "Pool: {} available, {} acquired, {} total",
520        available, acquired_count, total
521    ));
522
523    Ok(())
524}
525
526/// `gw worktree pool status`
527pub fn status(verbose: bool) -> Result<()> {
528    if !git::is_git_repo() {
529        return Err(GwError::NotAGitRepository);
530    }
531
532    let wt_dir = worktrees_dir()?;
533    let acquired_dir = acquired_dir()?;
534    let prefix = pool_prefix()?;
535
536    if !wt_dir.exists() {
537        return Err(GwError::PoolNotInitialized);
538    }
539
540    // Read-only — no lock needed
541    let state = PoolState::scan(&wt_dir, &acquired_dir, &prefix)?;
542
543    if state.entries.is_empty() {
544        return Err(GwError::PoolNotInitialized);
545    }
546
547    let available = state.count_by_status(&WorktreeStatus::Available);
548    let acquired = state.count_by_status(&WorktreeStatus::Acquired);
549    let total = state.entries.len();
550
551    println!();
552    output::info(&format!(
553        "Pool: {} available, {} acquired, {} total",
554        output::bold(&available.to_string()),
555        output::bold(&acquired.to_string()),
556        output::bold(&total.to_string()),
557    ));
558
559    if acquired > 0 {
560        println!();
561        let header = format!("{:<24} {}", "NAME", "BRANCH");
562        println!("{header}");
563        println!("{}", "-".repeat(48));
564
565        for entry in &state.entries {
566            if entry.status != WorktreeStatus::Acquired {
567                continue;
568            }
569            let branch = worktree_current_branch(&entry.path);
570            let branch_display = if branch == entry.name {
571                "(idle)".to_string()
572            } else {
573                branch
574            };
575            println!("{:<24} {}", entry.name, branch_display);
576            println!("    {}", entry.path.display());
577        }
578    }
579
580    if verbose {
581        // --verbose: show all entries
582        println!();
583        output::info("All entries:");
584        println!();
585        let header = format!("{:<24} {:<12} {:<24}", "NAME", "STATUS", "BRANCH");
586        println!("{header}");
587        println!("{}", "-".repeat(60));
588
589        for entry in &state.entries {
590            let branch = if entry.status == WorktreeStatus::Acquired {
591                worktree_current_branch(&entry.path)
592            } else {
593                entry.branch.clone()
594            };
595            println!("{:<24} {:<12} {}", entry.name, entry.status, branch);
596        }
597    }
598
599    // Show next action
600    let next = state.next_action();
601    println!();
602    display_pool_next_action(&next);
603
604    println!();
605    Ok(())
606}
607
608fn display_pool_next_action(action: &PoolNextAction) {
609    match action {
610        PoolNextAction::WarmPool => {
611            output::action("Next: warm the pool");
612            println!("  gw worktree pool warm <count>");
613        }
614        PoolNextAction::Ready { available } => {
615            output::action(&format!("Ready: {} worktree(s) available", available));
616            println!("  gw worktree pool acquire");
617            println!("  gw worktree pool release [name]");
618        }
619        PoolNextAction::Exhausted { acquired } => {
620            output::action(&format!(
621                "All {} worktree(s) acquired. Release or warm more.",
622                acquired
623            ));
624            println!("  gw worktree pool release [name]");
625            println!("  gw worktree pool warm <count>");
626        }
627        PoolNextAction::AllIdle { available } => {
628            output::action(&format!(
629                "All {} worktree(s) idle. Acquire or drain.",
630                available
631            ));
632            println!("  gw worktree pool acquire");
633            println!("  gw worktree pool drain");
634        }
635    }
636}
637
638/// `gw worktree pool drain [--force]`
639pub fn drain(force: bool, verbose: bool) -> Result<()> {
640    if !git::is_git_repo() {
641        return Err(GwError::NotAGitRepository);
642    }
643
644    let pool_dir = pool_dir()?;
645    let wt_dir = worktrees_dir()?;
646    let acquired_dir = acquired_dir()?;
647    let prefix = pool_prefix()?;
648    // Resolve all paths upfront — the cwd might be inside a pool worktree
649    // that we're about to delete.
650    let leader = leader_root()?;
651    let leader_str = leader.to_string_lossy().to_string();
652
653    if !wt_dir.exists() {
654        return Err(GwError::PoolNotInitialized);
655    }
656
657    println!();
658    output::info("Draining worktree pool...");
659
660    let _lock = PoolLock::acquire(&pool_dir)?;
661    let state = PoolState::scan(&wt_dir, &acquired_dir, &prefix)?;
662
663    if state.entries.is_empty() {
664        return Err(GwError::PoolNotInitialized);
665    }
666
667    // Check for acquired worktrees
668    let acquired = state.count_by_status(&WorktreeStatus::Acquired);
669    if acquired > 0 && !force {
670        return Err(GwError::PoolHasAcquiredWorktrees(acquired));
671    }
672
673    let total = state.entries.len();
674
675    for (i, entry) in state.entries.iter().enumerate() {
676        output::info(&format!(
677            "[{}/{}] Removing {}...",
678            i + 1,
679            total,
680            output::bold(&entry.name)
681        ));
682
683        let path_str = entry.path.to_string_lossy().to_string();
684
685        // Remove the worktree (run from leader root so it works even if cwd is deleted)
686        if let Err(e) = git::git_run_in_dir(
687            &leader_str,
688            &["worktree", "remove", "--force", &path_str],
689            verbose,
690        ) {
691            output::warn(&format!("Failed to remove worktree {}: {e}", entry.name));
692            let _ = std::fs::remove_dir_all(&entry.path);
693        }
694
695        // Delete the pool branch
696        if let Err(e) = git::git_run_in_dir(&leader_str, &["branch", "-D", &entry.branch], verbose)
697        {
698            output::warn(&format!("Failed to delete branch {}: {e}", entry.branch));
699        }
700
701        // Remove acquired marker if present
702        let marker = acquired_dir.join(&entry.name);
703        let _ = std::fs::remove_file(&marker);
704
705        output::success(&format!("[{}/{}] Removed {}", i + 1, total, entry.name));
706    }
707
708    // Clean up pool metadata
709    if acquired_dir.exists() {
710        let _ = std::fs::remove_dir_all(&acquired_dir);
711    }
712    let _ = std::fs::remove_file(pool_dir.join("pool.lock"));
713
714    // Prune worktree references
715    git::git_run_in_dir(&leader_str, &["worktree", "prune"], verbose)?;
716
717    // Remove empty directories
718    if wt_dir.exists() {
719        let _ = std::fs::remove_dir(&wt_dir);
720    }
721    drop(_lock);
722    let _ = std::fs::remove_dir(&pool_dir);
723
724    println!();
725    output::success(&format!("Drained {total} worktree(s) from pool"));
726
727    Ok(())
728}