1pub mod auth;
4mod manage;
5mod read;
6mod write;
7
8use std::path::{Path, PathBuf};
9
10use rskit_errors::AppResult;
11use rskit_process::{ProcessConfig, ProcessResult, ProcessSpec};
12
13use crate::core::Executor;
14use crate::error::GitError;
15use crate::types::Oid;
16
17pub struct GitCli {
19 root: PathBuf,
20}
21
22impl GitCli {
23 pub fn new(root: PathBuf) -> Self {
25 Self { root }
26 }
27
28 pub fn root(&self) -> &Path {
30 &self.root
31 }
32
33 pub(crate) fn run(&self, args: &[&str]) -> AppResult<Vec<u8>> {
34 let output = self.run_result(args)?;
35 if output.success() && !output.stdout_truncated && !output.stderr_truncated {
36 Ok(output.stdout_bytes)
37 } else {
38 Err(Self::command_failed(args, output))
39 }
40 }
41
42 pub(crate) fn run_result(&self, args: &[&str]) -> AppResult<ProcessResult> {
43 let command = ProcessSpec::new("git")
44 .args(args.iter().copied())
45 .dir(&self.root)
46 .env("GIT_TERMINAL_PROMPT", "0");
47 let config = ProcessConfig::default().with_timeout(None);
48 rskit_process::run(&command, &config)
49 }
50
51 #[allow(dead_code)]
52 pub(crate) fn not_implemented<T>(&self, operation: &'static str) -> AppResult<T> {
53 Err(GitError::NotImplemented { operation }.into())
54 }
55
56 pub(crate) fn command_failed(args: &[&str], output: ProcessResult) -> rskit_errors::AppError {
57 GitError::CommandFailed {
58 args: args.iter().map(|arg| (*arg).to_string()).collect(),
59 exit_code: output.exit_code,
60 stdout: output.stdout.trim().to_string(),
61 stderr: output.stderr.trim().to_string(),
62 stdout_truncated: output.stdout_truncated,
63 stderr_truncated: output.stderr_truncated,
64 }
65 .into()
66 }
67}
68
69pub(crate) fn parse_oid(hex: &str) -> AppResult<Oid> {
70 let hex = hex.trim();
71 if hex.len() != 40 {
72 return Err(GitError::InvalidOid {
73 value: hex.to_string(),
74 }
75 .into());
76 }
77
78 let mut bytes = [0u8; 20];
79 for i in 0..20 {
80 bytes[i] =
81 u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| GitError::InvalidOid {
82 value: hex.to_string(),
83 })?;
84 }
85
86 Ok(Oid::from_bytes(bytes))
87}
88
89impl Executor for GitCli {
90 fn exec(&self, args: &[&str]) -> AppResult<Vec<u8>> {
91 self.run(args)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use std::time::Duration;
98
99 use rskit_process::ProcessResult;
100
101 use super::*;
102
103 #[test]
104 fn new_exposes_root_and_executes_git_commands() {
105 let root = rskit_testutil::test_workspace!("git-cli-root");
106 let cli = GitCli::new(root.path().to_path_buf());
107
108 assert_eq!(cli.root(), root.path());
109 let version = cli.exec(&["--version"]).expect("git version runs");
110 assert!(String::from_utf8_lossy(&version).contains("git version"));
111 }
112
113 #[test]
114 fn command_failed_preserves_diagnostics_and_truncation_flags() {
115 let output = ProcessResult::completed(
116 Some(129),
117 b"usage\n".to_vec(),
118 b"bad args\n".to_vec(),
119 false,
120 false,
121 Duration::ZERO,
122 true,
123 false,
124 );
125
126 let err = GitCli::command_failed(&["bad"], output);
127
128 assert_eq!(err.code(), rskit_errors::ErrorCode::ExternalService);
129 assert!(err.message().contains("external service error"));
130 }
131
132 #[test]
133 fn parse_oid_rejects_wrong_length_and_invalid_hex() {
134 assert!(parse_oid("abc").is_err());
135 assert!(parse_oid("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz").is_err());
136 assert_eq!(
137 parse_oid("0000000000000000000000000000000000000001")
138 .unwrap()
139 .to_string(),
140 "0000000000000000000000000000000000000001"
141 );
142 }
143
144 #[test]
145 fn not_implemented_maps_to_internal_error() {
146 let root = rskit_testutil::test_workspace!("git-cli-not-implemented");
147 let cli = GitCli::new(root.path().to_path_buf());
148
149 let err = cli
150 .not_implemented::<()>("future operation")
151 .expect_err("operation is not implemented");
152
153 assert_eq!(err.code(), rskit_errors::ErrorCode::InvalidInput);
154 }
155}