gr_bin/git/
git.rs

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        // Get remote name for branch
114        self.run(
115            vec!["config", &format!("branch.{branch_name}.remote")],
116            false,
117        )
118        // Find the first line as the remote
119        .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        // If there are no remotes (branch hasn't been pushed), fallback to the list of branches
129        .or_else(|_| {
130            debug!("There is no remote for {branch_name}, falling back to first remote.");
131            self.get_remotes().and_then(|remotes| {
132                // Fall back to first remote
133                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        // Find remote URL
162        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        // Find all commit summaries between the two branches
194        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                // If the path exists, we have to clone inside of it
235                self.run(vec!["clone", &url], true)
236            } else {
237                // Otherwise we have to reinitialize the repository to allow cloning into empty repo
238                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}