1pub mod api;
2pub mod backend;
3pub mod config;
4pub mod context;
5pub mod dispatch;
6pub mod git;
7pub mod languages;
8pub mod prompt;
9pub mod response;
10pub mod scan;
11pub mod sensitive;
12
13use std::fmt;
14use std::sync::{LazyLock, Mutex};
15
16#[derive(Debug)]
18pub enum Error {
19 Git(String),
21 NoChanges,
23 BackendNotFound(String),
25 BackendExecution(String),
27 BackendTimeout(u64),
29 Config(String),
31 Io(std::io::Error),
33}
34
35impl fmt::Display for Error {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Error::Git(msg) => write!(f, "git error: {msg}"),
39 Error::NoChanges => write!(f, "no changes found — stage some changes first"),
40 Error::BackendNotFound(backend) => {
41 write!(
42 f,
43 "{backend} CLI not found — install it or set the path in config"
44 )
45 }
46 Error::BackendExecution(msg) => write!(f, "backend error: {msg}"),
47 Error::BackendTimeout(secs) => write!(f, "backend timed out after {secs} seconds"),
48 Error::Config(msg) => write!(f, "config error: {msg}"),
49 Error::Io(err) => write!(f, "IO error: {err}"),
50 }
51 }
52}
53
54impl std::error::Error for Error {}
55
56impl From<std::io::Error> for Error {
57 fn from(err: std::io::Error) -> Self {
58 Error::Io(err)
59 }
60}
61
62pub type Result<T> = std::result::Result<T, Error>;
63
64pub static TEST_CWD_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
65
66pub fn generate_commit_message(cfg: &config::Config) -> Result<String> {
73 let repo_root = git::get_repo_root()?;
74 let mut context = context::gather_context(&repo_root, cfg)?;
75
76 if context.diff.len() > cfg.max_diff_length {
77 context.diff = format!("{}\n... (truncated)", &context.diff[..cfg.max_diff_length]);
78 }
79
80 let prompt = prompt::build_prompt(&context, cfg, Some(cfg.commit_mode));
81 let response = dispatch::dispatch(
82 cfg.backend,
83 &prompt,
84 cfg,
85 dispatch::DispatchTask::Commit,
86 cfg.commit_branch_timeout_seconds,
87 )?;
88
89 let message = match cfg.commit_mode {
90 config::CommitMode::Adaptive | config::CommitMode::AdaptiveOneliner => {
91 response::format_adaptive_message(&response)
92 }
93 config::CommitMode::Conventional | config::CommitMode::ConventionalOneliner => {
94 let parsed = response::parse_response(&response);
95 response::format_commit_message(&parsed, cfg)
96 }
97 };
98
99 Ok(message)
100}
101
102pub fn refine_commit_message(
104 current_message: &str,
105 feedback: &str,
106 cfg: &config::Config,
107) -> Result<String> {
108 let repo_root = git::get_repo_root()?;
109 let mut context = context::gather_context(&repo_root, cfg)?;
110
111 if context.diff.len() > cfg.max_diff_length {
112 context.diff = format!("{}\n... (truncated)", &context.diff[..cfg.max_diff_length]);
113 }
114
115 let prompt = prompt::build_refine_prompt(current_message, feedback, &context.diff, cfg);
116 let response = dispatch::dispatch(
117 cfg.backend,
118 &prompt,
119 cfg,
120 dispatch::DispatchTask::Refine,
121 cfg.commit_branch_timeout_seconds,
122 )?;
123
124 let parsed = response::parse_response(&response);
125 Ok(response::format_commit_message(&parsed, cfg))
126}
127
128pub fn generate_branch_name(cfg: &config::Config) -> Result<String> {
130 let repo_root = git::get_repo_root()?;
131
132 let diff = git::get_diff(cfg.diff_source, &repo_root).ok();
133
134 let existing_branches = if cfg.branch_mode == config::BranchMode::Adaptive {
135 git::get_recent_branch_names(&repo_root, 20).unwrap_or_default()
136 } else {
137 vec![]
138 };
139
140 let prompt = prompt::build_branch_prompt(
141 "",
142 diff.as_deref(),
143 cfg,
144 cfg.branch_mode,
145 &existing_branches,
146 );
147 let response = dispatch::dispatch(
148 cfg.backend,
149 &prompt,
150 cfg,
151 dispatch::DispatchTask::Branch,
152 cfg.commit_branch_timeout_seconds,
153 )?;
154
155 Ok(response::format_branch_name(&response))
156}
157
158pub fn generate_and_commit(cfg: &config::Config) -> Result<(String, String)> {
160 let message = generate_commit_message(cfg)?;
161 let repo_root = git::get_repo_root()?;
162 let git_output = git::git_commit(&repo_root, &message)?;
163 Ok((message, git_output))
164}
165
166pub fn generate_and_create_branch(cfg: &config::Config) -> Result<String> {
168 let name = generate_branch_name(cfg)?;
169 let repo_root = git::get_repo_root()?;
170 git::create_and_checkout_branch(&repo_root, &name)?;
171 Ok(name)
172}