git_workspace/
repository.rs1use anyhow::{anyhow, Context};
2use console::{strip_ansi_codes, truncate_str};
3use git2::build::CheckoutBuilder;
4use git2::{Repository as Git2Repository, StatusOptions};
5use indicatif::ProgressBar;
6use serde::{Deserialize, Serialize};
7use std::io::{BufRead, BufReader};
8use std::path::{Path, PathBuf};
9use std::process::{Command, Stdio};
10
11#[derive(Deserialize, Serialize, Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
13pub struct Repository {
14 path: String,
15 url: String,
16 pub upstream: Option<String>,
17 pub branch: Option<String>,
18}
19
20impl Repository {
21 pub fn new(
22 path: String,
23 url: String,
24 branch: Option<String>,
25 upstream: Option<String>,
26 ) -> Repository {
27 let norm_path = if cfg!(windows) {
31 path.replace('/', std::path::MAIN_SEPARATOR.to_string().as_str())
32 } else {
33 path
34 };
35
36 Repository {
37 path: norm_path,
38 url,
39 branch,
40 upstream,
41 }
42 }
43
44 pub fn set_upstream(&self, root: &Path) -> anyhow::Result<()> {
45 let upstream = match &self.upstream {
46 Some(upstream) => upstream,
47 None => return Ok(()),
48 };
49
50 let mut command = Command::new("git");
51 let child = command
52 .arg("-C")
53 .arg(root.join(self.name()))
54 .arg("remote")
55 .arg("rm")
56 .arg("upstream")
57 .stdout(Stdio::null())
58 .stderr(Stdio::null());
59
60 child.status()?;
61
62 let mut command = Command::new("git");
63 let child = command
64 .arg("-C")
65 .arg(root.join(self.name()))
66 .arg("remote")
67 .arg("add")
68 .arg("upstream")
69 .arg(upstream);
70
71 let output = child.output()?;
72 if !output.status.success() {
73 let stderr =
74 std::str::from_utf8(&output.stderr).with_context(|| "Error decoding git output")?;
75 return Err(anyhow!(
76 "Failed to set upstream on repo {}: {}",
77 root.display(),
78 stderr.trim()
79 ));
80 }
81 Ok(())
82 }
83
84 fn run_with_progress(
85 &self,
86 command: &mut Command,
87 progress_bar: &ProgressBar,
88 ) -> anyhow::Result<()> {
89 progress_bar.set_message(format!("{}: starting", self.name()));
90 let mut spawned = command
91 .stdin(Stdio::null())
92 .stdout(Stdio::piped())
93 .stderr(Stdio::piped())
94 .spawn()
95 .with_context(|| format!("Error starting command {:?}", command))?;
96
97 let mut last_line = format!("{}: running...", self.name());
98 progress_bar.set_message(last_line.clone());
99
100 if let Some(ref mut stderr) = spawned.stderr {
101 let lines = BufReader::new(stderr).split(b'\r');
102 for line in lines {
103 let output = line.unwrap();
104 if output.is_empty() {
105 continue;
106 }
107 let line = std::str::from_utf8(&output).unwrap();
108 let plain_line = strip_ansi_codes(line).replace('\n', " ");
109 let truncated_line = truncate_str(plain_line.trim(), 70, "...");
110 progress_bar.set_message(format!("{}: {}", self.name(), truncated_line));
111 last_line = plain_line;
112 }
113 }
114 let exit_code = spawned
115 .wait()
116 .context("Error waiting for process to finish")?;
117 if !exit_code.success() {
118 return Err(anyhow!(
119 "Git exited with code {}: {}",
120 exit_code.code().unwrap(),
121 last_line
122 ));
123 }
124 Ok(())
125 }
126
127 pub fn execute_cmd(
128 &self,
129 root: &Path,
130 progress_bar: &ProgressBar,
131 cmd: &str,
132 args: &[String],
133 ) -> anyhow::Result<()> {
134 let mut command = Command::new(cmd);
135 let child = command.args(args).current_dir(root.join(self.name()));
136
137 self.run_with_progress(child, progress_bar)
138 .with_context(|| format!("Error running command in repo {}", self.name()))?;
139
140 Ok(())
141 }
142
143 pub fn switch_to_primary_branch(&self, root: &Path) -> anyhow::Result<()> {
144 let branch = match &self.branch {
145 None => return Ok(()),
146 Some(b) => b,
147 };
148 let repo = Git2Repository::init(root.join(self.name()))?;
149 let status = repo.statuses(Some(&mut StatusOptions::default()))?;
150 if !status.is_empty() {
151 return Err(anyhow!(
152 "Repository is dirty, cannot switch to branch {}",
153 branch
154 ));
155 }
156 repo.set_head(&format!("refs/heads/{}", branch))
157 .with_context(|| format!("Cannot find branch {}", branch))?;
158 repo.checkout_head(Some(CheckoutBuilder::default().safe().force()))
159 .with_context(|| format!("Error checking out branch {}", branch))?;
160 Ok(())
161 }
162
163 pub fn clone(&self, root: &Path, progress_bar: &ProgressBar) -> anyhow::Result<()> {
164 let mut command = Command::new("git");
165
166 let child = command
167 .arg("clone")
168 .arg("--recurse-submodules")
169 .arg("--progress")
170 .arg(&self.url)
171 .arg(root.join(self.name()));
172
173 self.run_with_progress(child, progress_bar)
174 .with_context(|| {
175 format!("Error cloning repo into {} from {}", self.name(), &self.url)
176 })?;
177
178 Ok(())
179 }
180 pub fn name(&self) -> &String {
181 &self.path
182 }
183 pub fn get_path(&self, root: &Path) -> anyhow::Result<PathBuf> {
184 let joined = root.join(self.name());
185 joined
186 .canonicalize()
187 .with_context(|| format!("Cannot resolve {}", joined.display()))
188 }
189 pub fn exists(&self, root: &Path) -> bool {
190 match self.get_path(root) {
191 Ok(path) => {
192 let git_dir = root.join(path).join(".git");
193 git_dir.exists() && git_dir.is_dir()
194 }
195 Err(_) => false,
196 }
197 }
198}