1use crate::git::url::parse_url;
2use eyre::{eyre, Context, ContextCompat, Result};
3use std::{
4 env,
5 path::{Path, PathBuf},
6 process::{Command, Stdio},
7};
8use tracing::{debug, info, instrument};
9
10pub struct LocalRepository {
11 path: String,
12}
13
14impl LocalRepository {
15 #[instrument]
16 pub fn init(path: Option<String>) -> Result<LocalRepository> {
17 let path = if let Some(path) = path {
18 PathBuf::from(path)
19 } else {
20 env::current_dir()?
21 };
22 info!("Repository directory is {}.", path.to_string_lossy());
23
24 let path = path.into_os_string().into_string().unwrap();
25 Ok(LocalRepository { path })
26 }
27
28 #[instrument(skip_all)]
29 fn run(&self, args: Vec<&str>, inherit: bool) -> Result<Vec<String>> {
30 let command = Command::new("git")
31 .current_dir(&self.path)
32 .args(args)
33 .stdout(if inherit {
34 Stdio::inherit()
35 } else {
36 Stdio::piped()
37 })
38 .stderr(if inherit {
39 Stdio::inherit()
40 } else {
41 Stdio::piped()
42 })
43 .output()?;
44
45 if command.status.success() {
46 let output = String::from_utf8_lossy(&command.stdout).to_string();
47 Ok(output
48 .lines()
49 .map(|s| s.to_string())
50 .collect::<Vec<String>>())
51 } else {
52 Err(eyre!(String::from_utf8_lossy(&command.stderr).to_string()))
53 }
54 }
55
56 #[instrument(skip_all)]
57 pub fn has_git(self: &LocalRepository) -> bool {
58 self.run(vec!["rev-parse"], false).is_ok()
59 }
60
61 #[instrument(skip_all)]
62 pub fn has_modifications(self: &LocalRepository) -> Result<bool> {
63 self.run(vec!["status", "--porcelain"], false)
64 .map(|rows| {
65 debug!("There are local changes ({}).", rows.join(", "));
66 !rows.is_empty()
67 })
68 .or(Err(eyre!("Unable to get modifications from repository.")))
69 }
70
71 #[instrument(skip(self))]
72 pub fn get_remotes(self: &LocalRepository) -> Result<Vec<String>> {
73 self.run(vec!["remote"], false)
74 }
75
76 #[instrument(skip(self))]
77 pub fn set_remote(self: &LocalRepository, name: String, url: String) -> Result<()> {
78 let existing_remotes = self.get_remotes()?;
79 if existing_remotes.iter().any(|s| s == &name) {
80 self.run(vec!["remote", "set-url", &name, &url], false)?;
81 } else {
82 self.run(vec!["remote", "add", &name, &url], false)?;
83 }
84
85 Ok(())
86 }
87
88 #[instrument(skip_all)]
89 pub fn get_branch(self: &LocalRepository) -> Result<String> {
90 let head = self
91 .run(vec!["rev-parse", "--abbrev-ref", "HEAD"], false)
92 .wrap_err("Cannot get current branch.")?
93 .into_iter()
94 .next()
95 .filter(|h| h != "HEAD")
96 .wrap_err(eyre!("We are not on a branch currently."))?;
97
98 info!("Current branch is {head}.");
99 Ok(head)
100 }
101
102 #[instrument(skip(self))]
103 pub fn get_branch_upstream(
104 self: &LocalRepository,
105 branch_name: Option<String>,
106 ) -> Result<(String, Option<String>)> {
107 let branch_name = if let Some(branch_name) = branch_name {
108 branch_name
109 } else {
110 self.get_branch()?
111 };
112
113 self.run(
115 vec!["config", &format!("branch.{branch_name}.remote")],
116 false,
117 )
118 .and_then(|remotes| {
120 remotes
121 .into_iter()
122 .next()
123 .map(|remote| (remote, Some(branch_name.clone())))
124 .wrap_err(eyre!(
125 "Branch {branch_name} doesn't have an upstream branch.",
126 ))
127 })
128 .or_else(|_| {
130 debug!("There is no remote for {branch_name}, falling back to first remote.");
131 self.get_remotes().and_then(|remotes| {
132 remotes
134 .into_iter()
135 .next()
136 .map(|remote| (remote, None))
137 .wrap_err(eyre!("Repository doesn't have any origin branches."))
138 })
139 })
140 }
141
142 #[instrument(skip(self))]
143 pub fn get_branch_sha(self: &LocalRepository, branch_name: Option<String>) -> Result<String> {
144 let branch_name = if let Some(branch_name) = branch_name {
145 branch_name
146 } else {
147 self.get_branch()?
148 };
149 self.run(vec!["rev-parse", &branch_name], false)
150 .map(|s| s.join(""))
151 .wrap_err("Cannot get commit SHA for the branch {branch}.")
152 }
153
154 #[instrument(skip(self))]
155 pub fn get_parsed_remote(
156 self: &LocalRepository,
157 branch_name: Option<String>,
158 ) -> Result<(String, String, Option<String>)> {
159 let (remote_name, branch_name) = self.get_branch_upstream(branch_name)?;
160
161 let remote_url = self
163 .run(vec!["remote", "get-url", &remote_name], false)
164 .wrap_err(eyre!("Cannot get remote for {remote_name}."))?
165 .into_iter()
166 .next()
167 .wrap_err(eyre!("Cannot get URL for {remote_name}."))?;
168
169 info!("Using remote {remote_name} with url {remote_url}.");
170
171 parse_url(&remote_url).map(|(host, repo)| (host, repo, branch_name))
172 }
173
174 #[instrument(skip(self))]
175 pub fn delete_branch(self: &LocalRepository, branch_name: String) -> Result<()> {
176 self.run(vec!["branch", "-d", &branch_name], false)?;
177
178 Ok(())
179 }
180
181 #[instrument(skip(self))]
182 pub fn get_branch_commits_from_target(
183 self: &LocalRepository,
184 branch_name: Option<String>,
185 target_name: String,
186 ) -> Result<Vec<String>> {
187 let branch_name = if let Some(branch_name) = branch_name {
188 branch_name
189 } else {
190 self.get_branch()?
191 };
192
193 let messages = self
195 .run(
196 vec![
197 "log",
198 &format!("{target_name}..{branch_name}"),
199 "--pretty=%s",
200 ],
201 false,
202 )
203 .wrap_err(eyre!("Branch {} not found.", &target_name))?;
204
205 Ok(messages)
206 }
207
208 #[instrument(skip(self))]
209 pub fn checkout_remote_branch(
210 self: &LocalRepository,
211 target_branch: String,
212 output: bool,
213 ) -> Result<()> {
214 self.run(vec!["checkout", &target_branch], false)?;
215
216 debug!("Git pulling in {}.", &self.path);
217 self.run(vec!["pull"], output)?;
218
219 Ok(())
220 }
221
222 #[instrument(skip(self))]
223 pub fn push(self: &LocalRepository, remote: &str, branch: &str) -> Result<()> {
224 self.run(vec!["push", "-u", remote, branch], true)
225 .wrap_err(eyre!("Could not push {branch} to {remote}"))?;
226
227 Ok(())
228 }
229
230 #[instrument(skip(self))]
231 pub fn clone(self: &LocalRepository, url: String, dir: Option<String>) -> Result<()> {
232 if let Some(dir) = dir {
233 if Path::new(&dir).exists() {
234 self.run(vec!["clone", &url], true)
236 } else {
237 LocalRepository::init(None)?.run(vec!["clone", &url, &dir], true)
239 }
240 } else {
241 self.run(vec!["clone", &url], true)
242 }
243 .wrap_err(eyre!("Could not clone {url}."))?;
244
245 Ok(())
246 }
247}