git_adr/
lib.rs

1//! git-adr: Architecture Decision Records management using git notes
2//!
3//! This crate provides a library and CLI tool for managing Architecture Decision Records (ADRs)
4//! stored in git notes. ADRs are stored in `refs/notes/adr` and can be synchronized across
5//! remotes like regular git content.
6//!
7//! # Features
8//!
9//! - Store ADRs in git notes (non-intrusive, no file clutter)
10//! - Multiple ADR formats (MADR, Nygard, Y-Statement, Alexandrian, etc.)
11//! - Full-text search and indexing
12//! - Binary artifact attachments
13//! - Sync with remotes
14//! - Optional AI integration for drafting and suggestions
15//! - Optional wiki sync (GitHub, GitLab)
16//! - Export to multiple formats (Markdown, JSON, HTML, DOCX)
17//!
18//! # Example
19//!
20//! ```no_run
21//! use git_adr::core::{Git, NotesManager, ConfigManager};
22//!
23//! // Initialize git executor
24//! let git = Git::new();
25//!
26//! // Load configuration
27//! let config = ConfigManager::new(git.clone()).load()?;
28//!
29//! // Create notes manager
30//! let notes = NotesManager::new(git, config);
31//!
32//! // List all ADRs
33//! let adrs = notes.list()?;
34//! for adr in adrs {
35//!     println!("{}: {}", adr.id, adr.title());
36//! }
37//! # Ok::<(), git_adr::Error>(())
38//! ```
39
40// Lints are configured in Cargo.toml [lints] section
41#![allow(clippy::struct_excessive_bools)]
42#![allow(clippy::needless_pass_by_value)]
43
44pub mod cli;
45pub mod core;
46
47#[cfg(feature = "ai")]
48pub mod ai;
49
50#[cfg(feature = "wiki")]
51pub mod wiki;
52
53#[cfg(feature = "export")]
54pub mod export;
55
56use thiserror::Error;
57
58/// Result type alias for git-adr operations.
59pub type Result<T> = std::result::Result<T, Error>;
60
61/// Errors that can occur in git-adr operations.
62#[derive(Error, Debug)]
63pub enum Error {
64    /// Git command execution failed.
65    #[error("git error: {message}")]
66    Git {
67        /// Error message.
68        message: String,
69        /// Git command that failed.
70        command: Vec<String>,
71        /// Exit code from git.
72        exit_code: i32,
73        /// Standard error output.
74        stderr: String,
75    },
76
77    /// Git executable not found.
78    #[error("git executable not found - please install git")]
79    GitNotFound,
80
81    /// Not in a git repository.
82    #[error("not a git repository{}", path.as_ref().map(|p| format!(": {p}")).unwrap_or_default())]
83    NotARepository {
84        /// Path that was checked.
85        path: Option<String>,
86    },
87
88    /// git-adr not initialized in this repository.
89    #[error("git-adr not initialized - run 'git adr init' first")]
90    NotInitialized,
91
92    /// ADR not found.
93    #[error("ADR not found: {id}")]
94    AdrNotFound {
95        /// ADR ID that was not found.
96        id: String,
97    },
98
99    /// Invalid ADR format.
100    #[error("invalid ADR format: {message}")]
101    InvalidAdr {
102        /// Error message.
103        message: String,
104    },
105
106    /// YAML parsing error.
107    #[error("YAML error: {0}")]
108    Yaml(#[from] serde_yaml::Error),
109
110    /// JSON parsing error.
111    #[error("JSON error: {0}")]
112    Json(#[from] serde_json::Error),
113
114    /// IO error.
115    #[error("IO error: {0}")]
116    Io(#[from] std::io::Error),
117
118    /// Template rendering error.
119    #[error("template error: {0}")]
120    Template(#[from] tera::Error),
121
122    /// Configuration error.
123    #[error("configuration error: {message}")]
124    Config {
125        /// Error message.
126        message: String,
127    },
128
129    /// Validation error.
130    #[error("validation error: {message}")]
131    Validation {
132        /// Error message.
133        message: String,
134    },
135
136    /// Content too large.
137    #[error("content too large: {size} bytes (max: {max} bytes)")]
138    ContentTooLarge {
139        /// Actual size.
140        size: usize,
141        /// Maximum allowed size.
142        max: usize,
143    },
144
145    /// Feature not available.
146    #[error("feature not available: {feature} - install with 'cargo install git-adr --features {feature}'")]
147    FeatureNotAvailable {
148        /// Feature name.
149        feature: String,
150    },
151
152    /// Invalid status value.
153    #[error("invalid status '{status}', valid values are: {}", valid.join(", "))]
154    InvalidStatus {
155        /// The invalid status provided.
156        status: String,
157        /// Valid status values.
158        valid: Vec<String>,
159    },
160
161    /// Parse error.
162    #[error("parse error: {message}")]
163    ParseError {
164        /// Error message.
165        message: String,
166    },
167
168    /// Template error.
169    #[error("template error: {message}")]
170    TemplateError {
171        /// Error message.
172        message: String,
173    },
174
175    /// Template not found.
176    #[error("template not found: {name}")]
177    TemplateNotFound {
178        /// Template name.
179        name: String,
180    },
181
182    /// AI not configured.
183    #[cfg(feature = "ai")]
184    #[error("AI not configured: {message}")]
185    AiNotConfigured {
186        /// Error message.
187        message: String,
188    },
189
190    /// Invalid AI provider.
191    #[cfg(feature = "ai")]
192    #[error("invalid AI provider: {provider}")]
193    InvalidProvider {
194        /// Provider name.
195        provider: String,
196    },
197
198    /// Wiki error.
199    #[cfg(feature = "wiki")]
200    #[error("wiki error: {message}")]
201    WikiError {
202        /// Error message.
203        message: String,
204    },
205
206    /// Export error.
207    #[cfg(feature = "export")]
208    #[error("export error: {message}")]
209    ExportError {
210        /// Error message.
211        message: String,
212    },
213
214    /// Invalid format.
215    #[error("invalid format: {format}")]
216    InvalidFormat {
217        /// Format name.
218        format: String,
219    },
220
221    /// IO error with context.
222    #[error("IO error: {message}")]
223    IoError {
224        /// Error message.
225        message: String,
226    },
227
228    /// Generic error with message.
229    #[error("{0}")]
230    Other(String),
231}
232
233impl Error {
234    /// Create a git error.
235    #[must_use]
236    pub fn git(
237        message: impl Into<String>,
238        command: Vec<String>,
239        exit_code: i32,
240        stderr: impl Into<String>,
241    ) -> Self {
242        Self::Git {
243            message: message.into(),
244            command,
245            exit_code,
246            stderr: stderr.into(),
247        }
248    }
249
250    /// Create an ADR not found error.
251    #[must_use]
252    pub fn adr_not_found(id: impl Into<String>) -> Self {
253        Self::AdrNotFound { id: id.into() }
254    }
255
256    /// Create an invalid ADR error.
257    #[must_use]
258    pub fn invalid_adr(message: impl Into<String>) -> Self {
259        Self::InvalidAdr {
260            message: message.into(),
261        }
262    }
263
264    /// Create a config error.
265    #[must_use]
266    pub fn config(message: impl Into<String>) -> Self {
267        Self::Config {
268            message: message.into(),
269        }
270    }
271
272    /// Create a validation error.
273    #[must_use]
274    pub fn validation(message: impl Into<String>) -> Self {
275        Self::Validation {
276            message: message.into(),
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_error_git() {
287        let err = Error::git(
288            "command failed",
289            vec!["git".to_string(), "status".to_string()],
290            1,
291            "error output",
292        );
293        assert!(matches!(err, Error::Git { .. }));
294        assert!(format!("{err}").contains("git error"));
295    }
296
297    #[test]
298    fn test_error_adr_not_found() {
299        let err = Error::adr_not_found("ADR-0001");
300        assert!(format!("{err}").contains("ADR not found"));
301    }
302
303    #[test]
304    fn test_error_invalid_adr() {
305        let err = Error::invalid_adr("invalid content");
306        assert!(format!("{err}").contains("invalid ADR format"));
307    }
308
309    #[test]
310    fn test_error_config() {
311        let err = Error::config("config error");
312        assert!(format!("{err}").contains("configuration error"));
313    }
314
315    #[test]
316    fn test_error_validation() {
317        let err = Error::validation("validation failed");
318        assert!(format!("{err}").contains("validation error"));
319    }
320
321    #[test]
322    fn test_error_not_initialized() {
323        let err = Error::NotInitialized;
324        assert!(format!("{err}").contains("not initialized"));
325    }
326
327    #[test]
328    fn test_error_git_not_found() {
329        let err = Error::GitNotFound;
330        assert!(format!("{err}").contains("git executable not found"));
331    }
332
333    #[test]
334    fn test_error_not_a_repository() {
335        let err = Error::NotARepository {
336            path: Some("/tmp/test".to_string()),
337        };
338        assert!(format!("{err}").contains("not a git repository"));
339        assert!(format!("{err}").contains("/tmp/test"));
340    }
341
342    #[test]
343    fn test_error_not_a_repository_no_path() {
344        let err = Error::NotARepository { path: None };
345        let msg = format!("{err}");
346        assert!(msg.contains("not a git repository"));
347    }
348
349    #[test]
350    fn test_error_content_too_large() {
351        let err = Error::ContentTooLarge {
352            size: 1024,
353            max: 512,
354        };
355        assert!(format!("{err}").contains("content too large"));
356    }
357
358    #[test]
359    fn test_error_feature_not_available() {
360        let err = Error::FeatureNotAvailable {
361            feature: "ai".to_string(),
362        };
363        assert!(format!("{err}").contains("feature not available"));
364    }
365
366    #[test]
367    fn test_error_invalid_status() {
368        let err = Error::InvalidStatus {
369            status: "invalid".to_string(),
370            valid: vec!["proposed".to_string(), "accepted".to_string()],
371        };
372        assert!(format!("{err}").contains("invalid status"));
373    }
374
375    #[test]
376    fn test_error_parse_error() {
377        let err = Error::ParseError {
378            message: "parse failed".to_string(),
379        };
380        assert!(format!("{err}").contains("parse error"));
381    }
382
383    #[test]
384    fn test_error_template_not_found() {
385        let err = Error::TemplateNotFound {
386            name: "custom".to_string(),
387        };
388        assert!(format!("{err}").contains("template not found"));
389    }
390
391    #[test]
392    fn test_error_other() {
393        let err = Error::Other("generic error".to_string());
394        assert!(format!("{err}").contains("generic error"));
395    }
396}