Skip to main content

opencodecommit/
lib.rs

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/// Crate-level error type.
17#[derive(Debug)]
18pub enum Error {
19    /// Git command failed or repo not found.
20    Git(String),
21    /// No changes found to generate a message from.
22    NoChanges,
23    /// AI backend not found or not executable.
24    BackendNotFound(String),
25    /// AI backend execution failed.
26    BackendExecution(String),
27    /// Backend timed out.
28    BackendTimeout(u64),
29    /// Configuration error.
30    Config(String),
31    /// IO error.
32    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
66// --- High-level public API ---
67
68/// Generate a commit message from the current git repo state.
69///
70/// This is the main entry point for library users. It gathers context from git,
71/// builds a prompt, executes the AI backend, and returns the formatted message.
72pub 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
102/// Refine an existing commit message based on user feedback.
103pub 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
128/// Generate a branch name from the current repo state.
129pub 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
158/// Generate a commit message and execute git commit.
159pub 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
166/// Generate a branch name and create+checkout the branch.
167pub 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}