Skip to main content

wt/
lib.rs

1//! `wt` — a Git worktree and GitHub PR manager (library crate).
2//!
3//! All real logic lives here so it is unit-testable and counted by coverage;
4//! `src/main.rs` is a thin entry point. See `spec.md` for the full behavior
5//! specification.
6//!
7//! The single entry point is [`run`], which takes the command-line arguments
8//! and a [`Cx`] (injected I/O, environment, and working directory) and returns
9//! the process exit code. Keeping the side-effecting handles in `Cx` makes the
10//! whole dispatch path testable without touching the real terminal.
11
12pub mod agent;
13pub(crate) mod cli;
14pub(crate) mod commands;
15pub mod config;
16pub mod copy;
17pub mod cx;
18pub mod error;
19pub mod gh;
20pub mod git;
21pub mod hooks;
22pub mod keys;
23pub mod model;
24pub mod output;
25pub mod query;
26pub mod slug;
27pub mod template;
28pub mod time;
29pub mod tui;
30pub mod util;
31pub mod version;
32pub(crate) mod worktree_service;
33
34#[cfg(test)]
35mod testutil;
36
37pub use cx::{Cx, Env, Stream};
38pub use error::{Error, Result};
39
40/// Runs `wt` with the given command-line arguments (excluding `argv[0]`),
41/// writing through the provided [`Cx`], and returns the process exit code.
42pub fn run(args: Vec<String>, cx: &mut Cx) -> u8 {
43    let result = cli::dispatch(args, cx);
44    finish(result, &mut cx.err)
45}
46
47/// Maps a command result to an exit code, reporting any error to `err`.
48fn finish(result: Result<u8>, err: &mut Stream) -> u8 {
49    match result {
50        Ok(code) => code,
51        Err(e) => {
52            let _ = err.line(&format!("error: {e}"));
53            e.exit_code()
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::testutil::test_cx;
62
63    #[test]
64    fn finish_passes_through_success_code() {
65        let mut t = test_cx(&[], "/tmp");
66        assert_eq!(finish(Ok(0), &mut t.cx.err), 0);
67        assert_eq!(finish(Ok(1), &mut t.cx.err), 1);
68        assert!(t.err.contents().is_empty());
69    }
70
71    #[test]
72    fn finish_reports_error_to_stderr_and_maps_code() {
73        let mut t = test_cx(&[], "/tmp");
74        let code = finish(Err(Error::usage("bad flag")), &mut t.cx.err);
75        assert_eq!(code, 2);
76        assert_eq!(t.err.contents(), "error: bad flag\n");
77        assert!(t.out.contents().is_empty());
78    }
79
80    #[test]
81    fn run_help_exits_zero_via_clap() {
82        let mut t = test_cx(&[], "/tmp");
83        assert_eq!(run(vec!["--help".to_string()], &mut t.cx), 0);
84        assert!(t.out.contents().contains("Usage"));
85    }
86
87    #[test]
88    fn run_maps_command_error_to_exit_code() {
89        // No subcommand launches the TUI, which fails at discovery from a
90        // non-repo dir: exit 1 with the NotInRepo message.
91        let mut t = test_cx(&[], "/tmp");
92        assert_eq!(run(vec![], &mut t.cx), 1);
93        assert!(t.err.contents().contains("not in a git repository"));
94    }
95}