Skip to main content

commit_bridge/polling/
git.rs

1//! Operations to fetch and extract git branch data from remote repositories.
2
3use crate::{domain::CommitHash, error::CommitHashError};
4use async_trait::async_trait;
5
6/// Allows running git commands.
7#[async_trait]
8pub trait GitFetcher: Send + Sync {
9    /// Returns the latest hash of a git branch.
10    async fn get_latest_hash(
11        &self,
12        repo_url: &str,
13        branch: &str,
14    ) -> Result<CommitHash, CommitHashError>;
15}
16
17/// Runs git commands.
18pub struct MainGitFetcher;
19
20#[async_trait]
21impl GitFetcher for MainGitFetcher {
22    async fn get_latest_hash(
23        &self,
24        repo_url: &str,
25        branch: &str,
26    ) -> Result<CommitHash, CommitHashError> {
27        tokio::process::Command::new("git")
28            .args(["ls-remote", "--", repo_url, branch])
29            .output()
30            .await
31            .map_err(CommitHashError::from)
32            .and_then(handle_git_output_result)
33            .and_then(|stdout| extract_hash(stdout, repo_url.to_string(), branch.to_string()))
34    }
35}
36
37/// Analyzes the `git` exit status to handle process output.
38fn handle_git_output_result(output: std::process::Output) -> Result<Vec<u8>, CommitHashError> {
39    if output.status.success() {
40        Ok(output.stdout)
41    } else {
42        let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
43        Err(CommitHashError::UnexpectedStatus(stderr))
44    }
45}
46
47/// Extracts the commit hash from a `git ls-remote` process stdout.
48fn extract_hash(
49    stdout: Vec<u8>,
50    repo_url: String,
51    branch: String,
52) -> Result<CommitHash, CommitHashError> {
53    let hash_str = String::from_utf8_lossy(&stdout)
54        .split_whitespace()
55        .next()
56        .map(|s| s.to_string())
57        .ok_or(CommitHashError::UnexpectedOutput {
58            stdout: String::from_utf8_lossy(&stdout).into_owned(),
59            repo_url,
60            branch,
61        })?;
62
63    Ok(CommitHash::new(hash_str)?)
64}
65
66#[cfg(test)]
67mod tests {
68    #![allow(
69        clippy::panic,
70        clippy::expect_used,
71        clippy::todo,
72        clippy::unimplemented,
73        clippy::indexing_slicing
74    )]
75
76    use super::*;
77
78    #[test]
79    fn test_extract_hash_success() {
80        let stdout = b"678c4343237127dbadbf1806dd98b2154ffd2ebe\trefs/heads/main\n".to_vec();
81        let repo_url = "https://github.com/Nilirad/commit-bridge".to_string();
82        let branch = "main".to_string();
83
84        let result = extract_hash(stdout, repo_url, branch);
85        assert!(result.is_ok());
86        assert_eq!(
87            result.unwrap().as_str(),
88            "678c4343237127dbadbf1806dd98b2154ffd2ebe"
89        );
90    }
91
92    #[test]
93    fn test_extract_hash_empty_output() {
94        let stdout = b"".to_vec();
95        let repo_url = "https://github.com/Nilirad/commit-bridge".to_string();
96        let branch = "main".to_string();
97
98        let result = extract_hash(stdout.clone(), repo_url.clone(), branch.clone());
99
100        let Err(CommitHashError::UnexpectedOutput {
101            stdout: err_stdout,
102            repo_url: err_repo_url,
103            branch: err_branch,
104        }) = result
105        else {
106            panic!("Expected UnexpectedOutput error");
107        };
108
109        assert_eq!(err_stdout, String::from_utf8(stdout).unwrap_or_default());
110        assert_eq!(err_repo_url, repo_url);
111        assert_eq!(err_branch, branch);
112    }
113
114    #[test]
115    fn test_extract_hash_whitespace_only() {
116        let stdout = b"   \n\t ".to_vec();
117        let repo_url = "https://github.com/Nilirad/commit-bridge".to_string();
118        let branch = "main".to_string();
119
120        let result = extract_hash(stdout.clone(), repo_url.clone(), branch.clone());
121
122        let Err(CommitHashError::UnexpectedOutput {
123            stdout: err_stdout,
124            repo_url: err_repo_url,
125            branch: err_branch,
126        }) = result
127        else {
128            panic!("Expected UnexpectedOutput error");
129        };
130
131        assert_eq!(err_stdout, String::from_utf8(stdout).unwrap_or_default());
132        assert_eq!(err_repo_url, repo_url);
133        assert_eq!(err_branch, branch);
134    }
135}