git_bot_feedback/client/
local.rs1use super::RestApiClient;
2use crate::{OutputVariable, RestClientError as ClientError, ReviewOptions, ThreadCommentOptions};
3
4#[cfg(feature = "file-changes")]
5use crate::{FileDiffLines, FileFilter, LinesChangedOnly, parse_diff};
6#[cfg(feature = "file-changes")]
7use std::{collections::HashMap, process::Command};
8
9#[derive(Debug, Clone, PartialEq, Eq, Default)]
29pub struct LocalClient;
30
31#[cfg(feature = "file-changes")]
33fn git_rev_parse(base: &str) -> Result<String, ClientError> {
34 match Command::new("git").args(["rev-parse", base]).output() {
35 Err(e) => Err(ClientError::Io {
36 task: format!("invoke `git rev-parse {base}` to validate reference"),
37 source: e,
38 }),
39 Ok(output) => {
40 if output.status.success() {
41 Ok(String::from_utf8_lossy(output.stdout.trim_ascii()).to_string())
42 } else if base.chars().all(|c| c.is_ascii_digit()) {
43 git_rev_parse(format!("HEAD~{base}").as_str())
48 } else {
49 let err_msg = String::from_utf8_lossy(&output.stderr).to_string();
50 Err(ClientError::GitCommand(err_msg))
52 }
53 }
54 }
55}
56
57#[async_trait::async_trait]
58impl RestApiClient for LocalClient {
59 fn client_kind(&self) -> String {
60 "local".to_string()
61 }
62
63 #[cfg(feature = "file-changes")]
64 async fn get_list_of_changed_files(
65 &self,
66 file_filter: &FileFilter,
67 lines_changed_only: &LinesChangedOnly,
68 base_diff: Option<String>,
69 ignore_index: bool,
70 ) -> Result<HashMap<String, FileDiffLines>, ClientError> {
71 let git_status = if ignore_index {
72 0
73 } else {
74 match Command::new("git").args(["status", "--short"]).output() {
75 Err(e) => {
76 return Err(ClientError::io("invoke `git status`", e));
77 }
78 Ok(output) => {
79 if output.status.success() {
80 String::from_utf8_lossy(&output.stdout)
81 .to_string()
82 .trim_end_matches('\n')
84 .lines()
85 .filter(|l| !l.starts_with(' '))
87 .count()
88 } else {
89 let err_msg = String::from_utf8_lossy(&output.stderr).to_string();
90 return Err(ClientError::GitCommand(err_msg));
91 }
92 }
93 }
94 };
95 let mut diff_args = vec![];
96 let mut git_sub_cmd = vec!["--no-pager"];
97 if git_status != 0 {
98 diff_args.push("--staged".to_string());
101 }
102 if let Some(base) = base_diff {
103 let resolved_base = git_rev_parse(&base)?;
104 diff_args.push(resolved_base);
105 } else if git_status == 0 {
106 let resolved_head = git_rev_parse("HEAD~1")?;
109 diff_args.push(resolved_head);
110 }
111 if ignore_index {
112 diff_args.push("--format=%b".to_string());
115 git_sub_cmd.push("show");
116 } else {
117 git_sub_cmd.push("diff");
118 };
119 log::debug!(
120 "Getting diff with `git {} {}`",
121 git_sub_cmd.join(" "),
122 diff_args.join(" ")
123 );
124 match Command::new("git")
125 .args(&git_sub_cmd)
126 .args(&diff_args)
127 .output()
128 {
129 Err(e) => Err(ClientError::Io {
130 task: format!(
131 "invoke `git {} {}`",
132 git_sub_cmd.join(" "),
133 diff_args.join(" ")
134 ),
135 source: e,
136 }),
137 Ok(output) => {
138 if output.status.success() {
139 let diff_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
140 let files = parse_diff(&diff_str, file_filter, lines_changed_only)?;
141 Ok(files)
142 } else {
143 let err_msg = String::from_utf8_lossy(&output.stderr).to_string();
144 Err(ClientError::GitCommand(err_msg))
145 }
146 }
147 }
148 }
149
150 fn is_pr_event(&self) -> bool {
151 false
152 }
153
154 fn set_user_agent(&mut self, _user_agent: &str) -> Result<(), ClientError> {
155 Ok(())
156 }
157
158 async fn post_thread_comment(&self, _options: ThreadCommentOptions) -> Result<(), ClientError> {
159 Ok(())
160 }
161
162 async fn cull_pr_reviews(&mut self, _options: &mut ReviewOptions) -> Result<(), ClientError> {
163 Ok(())
164 }
165
166 async fn post_pr_review(&mut self, _options: &ReviewOptions) -> Result<(), ClientError> {
167 Ok(())
168 }
169
170 fn write_output_variables(&self, vars: &[OutputVariable]) -> Result<(), ClientError> {
171 for var in vars {
172 log::info!("{}: {}", var.name, var.value);
173 }
174 Ok(())
175 }
176}