1#![forbid(unsafe_code)]
21
22use git2::Branch;
23use git2::ReferenceType;
24use git2::Repository;
25use git2::{ErrorClass, ErrorCode};
26use git2::{Status, StatusOptions, StatusShow};
27use std::fmt;
28use std::io;
29use std::path::Path;
30
31mod shell_writer;
33pub use shell_writer::*;
34
35#[derive(Debug, Default)]
37pub struct Reference {
38 pub name: String,
40
41 pub kind: String,
43
44 pub error: String,
46}
47
48impl Reference {
49 #[must_use]
51 pub fn new<N: fmt::Display, K: fmt::Display>(name: N, kind: K) -> Self {
52 Self {
53 name: name.to_string(),
54 kind: kind.to_string(),
55 error: "".to_owned(),
56 }
57 }
58
59 #[must_use]
61 pub fn new_with_error<N, K, E>(name: N, kind: K, error: E) -> Self
62 where
63 N: fmt::Display,
64 K: fmt::Display,
65 E: fmt::Debug,
66 {
67 Self {
68 name: name.to_string(),
69 kind: kind.to_string(),
70 error: format!("{error:?}"),
71 }
72 }
73
74 #[must_use]
76 pub fn symbolic<N: fmt::Display>(name: N) -> Self {
77 Self::new(name, "symbolic")
78 }
79
80 #[must_use]
82 pub fn direct<N: fmt::Display>(name: N) -> Self {
83 Self::new(name, "direct")
84 }
85
86 #[must_use]
89 pub fn short(&self) -> &str {
90 self.name
91 .strip_prefix("refs/heads/")
92 .or_else(|| self.name.strip_prefix("refs/tags/"))
93 .unwrap_or(&self.name)
94 }
95}
96
97impl ShellVars for Reference {
98 fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
100 out.write_var("name", &self.name);
101 out.write_var("short", self.short());
102 out.write_var("kind", &self.kind);
103 out.write_var("error", &self.error);
104 }
105}
106
107#[derive(Debug, Default)]
109pub struct Head {
110 pub trail: Vec<Reference>,
112
113 pub hash: String,
115
116 pub ahead_of_upstream: Option<usize>,
121
122 pub behind_upstream: Option<usize>,
127
128 pub upstream_error: String,
130}
131
132impl ShellVars for Head {
133 fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
134 let trail = self.trail.get(1..).unwrap_or(&[]);
135 out.write_var("ref_length", trail.len());
136 for (i, reference) in trail.iter().enumerate() {
137 #[allow(clippy::arithmetic_side_effects)]
139 out.group_n("ref", i + 1).write_vars(reference);
140 }
141 out.write_var("hash", &self.hash);
142 out.write_var("ahead", display_option(self.ahead_of_upstream));
143 out.write_var("behind", display_option(self.behind_upstream));
144 out.write_var("upstream_error", &self.upstream_error);
145 }
146}
147
148pub fn summarize_repository<W: std::io::Write>(
166 out: &ShellWriter<W>,
167 opened: Result<Repository, git2::Error>,
168) {
169 let result = match opened {
170 Ok(mut repository) => summarize_opened_repository(out, &mut repository),
171 Err(error)
172 if error.code() == ErrorCode::NotFound
173 && error.class() == ErrorClass::Repository =>
174 {
175 out.write_var("repo_state", "NotFound");
176 Ok(())
177 }
178 Err(error) => Err(error),
179 };
180
181 if let Err(error) = result {
182 out.write_var("repo_state", "Error");
183 out.write_var_debug("repo_error", error);
184 }
185}
186
187pub fn summarize_opened_repository<W: std::io::Write>(
212 out: &ShellWriter<W>,
213 repository: &mut Repository,
214) -> Result<(), git2::Error> {
215 let state = repository.state();
216 let workdir = display_option(repository.workdir().map(Path::display));
217 let empty = repository.is_empty()?;
218 let bare = repository.is_bare();
219 let head = &head_info(repository);
220 let changes = &count_changes(repository)?;
221 let stash = count_stash(repository)?;
222
223 out.write_var_debug("repo_state", state);
224 out.write_var("repo_workdir", workdir);
225 out.write_var("repo_empty", empty);
226 out.write_var("repo_bare", bare);
227 out.group("head").write_vars(head);
228 out.write_vars(changes);
229 out.write_var("stash_count", stash);
230
231 Ok(())
232}
233
234#[allow(clippy::similar_names)]
241#[must_use]
242pub fn head_info(repository: &Repository) -> Head {
243 let mut current = "HEAD".to_owned();
244 let mut head = Head::default();
245 loop {
246 match repository.find_reference(¤t) {
247 Ok(reference) => match reference.kind() {
248 Some(ReferenceType::Direct) => {
249 head.trail.push(Reference::direct(display_option(
250 reference.name(),
251 )));
252 head.hash = display_option(reference.target());
253 break;
254 }
255 Some(ReferenceType::Symbolic) => {
256 head.trail.push(Reference::symbolic(display_option(
257 reference.name(),
258 )));
259 let target = reference
260 .symbolic_target()
261 .expect("Symbolic ref should have symbolic target");
262 target.clone_into(&mut current);
263 }
264 None => {
265 head.trail.push(Reference::new(
266 display_option(reference.name()),
267 "unknown",
268 ));
269 break;
270 }
271 },
272 Err(error) => {
273 head.trail
274 .push(Reference::new_with_error(current, "", error));
275 break;
276 }
277 };
278 }
279
280 match get_upstream_difference(repository) {
281 Ok(Some((ahead, behind))) => {
282 head.ahead_of_upstream = Some(ahead);
283 head.behind_upstream = Some(behind);
284 }
285 Ok(None) => {}
286 Err(error) => {
287 head.upstream_error = format!("{error:?}");
288 }
289 }
290
291 head
292}
293
294pub fn get_upstream_difference(
303 repository: &Repository,
304) -> Result<Option<(usize, usize)>, git2::Error> {
305 let local_ref = repository.head()?.resolve()?;
306 if let Some(local_oid) = local_ref.target() {
307 Branch::wrap(local_ref)
308 .upstream()?
309 .get()
310 .target()
311 .map(|upstream_oid| {
312 repository.graph_ahead_behind(local_oid, upstream_oid)
313 })
314 .transpose()
315 } else {
316 Ok(None)
317 }
318}
319
320fn display_option<V: fmt::Display>(s: Option<V>) -> String {
322 s.map(|s| s.to_string()).unwrap_or_else(|| "".to_owned())
323}
324
325#[derive(Debug, Default)]
327pub struct ChangeCounters {
328 pub untracked: usize,
330
331 pub unstaged: usize,
333
334 pub staged: usize,
336
337 pub conflicted: usize,
339}
340
341impl From<[usize; 4]> for ChangeCounters {
342 fn from(array: [usize; 4]) -> Self {
343 Self {
344 untracked: array[0],
345 unstaged: array[1],
346 staged: array[2],
347 conflicted: array[3],
348 }
349 }
350}
351
352impl ShellVars for ChangeCounters {
353 fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
355 out.write_var("untracked_count", self.untracked);
356 out.write_var("unstaged_count", self.unstaged);
357 out.write_var("staged_count", self.staged);
358 out.write_var("conflicted_count", self.conflicted);
359 }
360}
361
362pub fn count_changes(
369 repository: &Repository,
370) -> Result<ChangeCounters, git2::Error> {
371 if repository.is_bare() {
372 return Ok(ChangeCounters::default());
374 }
375
376 let mut options = StatusOptions::new();
377 options
379 .show(StatusShow::IndexAndWorkdir)
380 .include_untracked(true)
381 .exclude_submodules(true);
382 let statuses = repository.statuses(Some(&mut options))?;
383
384 let mut counters: [usize; 4] = [0; 4];
385 let buckets = [
386 Status::WT_NEW,
388 Status::WT_MODIFIED
390 | Status::WT_DELETED
391 | Status::WT_TYPECHANGE
392 | Status::WT_RENAMED,
393 Status::INDEX_NEW
395 | Status::INDEX_MODIFIED
396 | Status::INDEX_DELETED
397 | Status::INDEX_RENAMED
398 | Status::INDEX_TYPECHANGE,
399 Status::CONFLICTED,
401 ];
402
403 for status in statuses.iter() {
404 for (i, bits) in buckets.iter().enumerate() {
405 if status.status().intersects(*bits) {
406 counters[i] = counters[i].saturating_add(1);
407 }
408 }
409 }
410
411 Ok(ChangeCounters::from(counters))
412}
413
414pub fn count_stash(repository: &mut Repository) -> Result<usize, git2::Error> {
426 let mut count: usize = 0;
427 repository.stash_foreach(|_, _, _| {
428 count = count.saturating_add(1);
429 true
430 })?;
431
432 Ok(count)
433}