1#![forbid(unsafe_code)]
23
24use git2::Branch;
25use git2::ReferenceType;
26use git2::Repository;
27use git2::{ErrorClass, ErrorCode};
28use git2::{Status, StatusOptions, StatusShow};
29use std::fmt;
30use std::io;
31use std::path::Path;
32use std::time::Instant;
33
34mod shell_writer;
36pub use shell_writer::*;
37
38#[derive(Debug, Default)]
40pub struct Reference {
41 pub name: String,
43
44 pub kind: String,
46
47 pub error: String,
49}
50
51impl Reference {
52 #[must_use]
54 pub fn new<N: fmt::Display, K: fmt::Display>(name: N, kind: K) -> Self {
55 Self {
56 name: name.to_string(),
57 kind: kind.to_string(),
58 error: "".to_owned(),
59 }
60 }
61
62 #[must_use]
64 pub fn new_with_error<N, K, E>(name: N, kind: K, error: E) -> Self
65 where
66 N: fmt::Display,
67 K: fmt::Display,
68 E: fmt::Debug,
69 {
70 Self {
71 name: name.to_string(),
72 kind: kind.to_string(),
73 error: format!("{error:?}"),
74 }
75 }
76
77 #[must_use]
79 pub fn symbolic<N: fmt::Display>(name: N) -> Self {
80 Self::new(name, "symbolic")
81 }
82
83 #[must_use]
85 pub fn direct<N: fmt::Display>(name: N) -> Self {
86 Self::new(name, "direct")
87 }
88
89 #[must_use]
92 pub fn short(&self) -> &str {
93 self.name
94 .strip_prefix("refs/heads/")
95 .or_else(|| self.name.strip_prefix("refs/tags/"))
96 .unwrap_or(&self.name)
97 }
98}
99
100impl ShellVars for Reference {
101 fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
103 out.write_var("name", &self.name);
104 out.write_var("short", self.short());
105 out.write_var("kind", &self.kind);
106 out.write_var("error", &self.error);
107 }
108}
109
110#[derive(Debug, Default)]
112pub struct Head {
113 pub trail: Vec<Reference>,
115
116 pub hash: String,
118
119 pub ahead_of_upstream: Option<usize>,
124
125 pub behind_upstream: Option<usize>,
130
131 pub upstream_error: String,
133}
134
135impl ShellVars for Head {
136 fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
137 let trail = self.trail.get(1..).unwrap_or(&[]);
138 out.write_var("ref_length", trail.len());
139 for (i, reference) in trail.iter().enumerate() {
140 #[allow(clippy::arithmetic_side_effects)]
142 out.group_n("ref", i + 1).write_vars(reference);
143 }
144 out.write_var("hash", &self.hash);
145 out.write_var("ahead", display_option(self.ahead_of_upstream));
146 out.write_var("behind", display_option(self.behind_upstream));
147 out.write_var("upstream_error", &self.upstream_error);
148 }
149}
150
151pub fn summarize_repository<W: std::io::Write>(
169 out: &ShellWriter<W>,
170 opened: Result<Repository, git2::Error>,
171) {
172 let result = match opened {
173 Ok(mut repository) => summarize_opened_repository(out, &mut repository),
174 Err(error)
175 if error.code() == ErrorCode::NotFound
176 && error.class() == ErrorClass::Repository =>
177 {
178 out.write_var("repo_state", "NotFound");
179 Ok(())
180 }
181 Err(error) => Err(error),
182 };
183
184 if let Err(error) = result {
185 out.write_var("repo_state", "Error");
186 out.write_var_debug("repo_error", error);
187 }
188}
189
190pub fn summarize_opened_repository<W: std::io::Write>(
216 out: &ShellWriter<W>,
217 repository: &mut Repository,
218) -> Result<(), git2::Error> {
219 out.write_var(
220 "repo_workdir",
221 display_option(
222 time("repository.workdir()", || repository.workdir())
223 .map(Path::display),
224 ),
225 );
226
227 out.write_var(
228 "repo_empty",
229 time("repository.is_empty()", || repository.is_empty())?,
230 );
231
232 out.write_var(
233 "repo_bare",
234 time("repository.is_bare()", || repository.is_bare()),
235 );
236
237 out.group("head")
238 .write_vars(&time("head_info(repository)", || head_info(repository)));
239
240 out.write_vars(&time("count_changes(repository)", || {
241 count_changes(repository)
242 })?);
243
244 out.write_var(
245 "stash_count",
246 time("count_stash(repository)", || count_stash(repository))?,
247 );
248
249 out.write_var_debug(
252 "repo_state",
253 time("repository.state()", || repository.state()),
254 );
255
256 Ok(())
257}
258
259#[allow(clippy::similar_names)]
266#[must_use]
267pub fn head_info(repository: &Repository) -> Head {
268 let mut current = "HEAD".to_owned();
269 let mut head = Head::default();
270 loop {
271 match repository.find_reference(¤t) {
272 Ok(reference) => match reference.kind() {
273 Some(ReferenceType::Direct) => {
274 head.trail.push(Reference::direct(display_option(
275 reference.name(),
276 )));
277 head.hash = display_option(reference.target());
278 break;
279 }
280 Some(ReferenceType::Symbolic) => {
281 head.trail.push(Reference::symbolic(display_option(
282 reference.name(),
283 )));
284 let target = reference
285 .symbolic_target()
286 .expect("Symbolic ref should have symbolic target");
287 target.clone_into(&mut current);
288 }
289 None => {
290 head.trail.push(Reference::new(
291 display_option(reference.name()),
292 "unknown",
293 ));
294 break;
295 }
296 },
297 Err(error) => {
298 head.trail
299 .push(Reference::new_with_error(current, "", error));
300 break;
301 }
302 };
303 }
304
305 match get_upstream_difference(repository) {
306 Ok(Some((ahead, behind))) => {
307 head.ahead_of_upstream = Some(ahead);
308 head.behind_upstream = Some(behind);
309 }
310 Ok(None) => {}
311 Err(error) => {
312 head.upstream_error = format!("{error:?}");
313 }
314 }
315
316 head
317}
318
319pub fn get_upstream_difference(
328 repository: &Repository,
329) -> Result<Option<(usize, usize)>, git2::Error> {
330 let local_ref = repository.head()?.resolve()?;
331 if let Some(local_oid) = local_ref.target() {
332 Branch::wrap(local_ref)
333 .upstream()?
334 .get()
335 .target()
336 .map(|upstream_oid| {
337 repository.graph_ahead_behind(local_oid, upstream_oid)
338 })
339 .transpose()
340 } else {
341 Ok(None)
342 }
343}
344
345fn display_option<V: fmt::Display>(s: Option<V>) -> String {
347 s.map(|s| s.to_string()).unwrap_or_else(|| "".to_owned())
348}
349
350#[derive(Debug, Default)]
352pub struct ChangeCounters {
353 pub untracked: usize,
355
356 pub unstaged: usize,
358
359 pub staged: usize,
361
362 pub conflicted: usize,
364}
365
366impl From<[usize; 4]> for ChangeCounters {
367 fn from(array: [usize; 4]) -> Self {
368 Self {
369 untracked: array[0],
370 unstaged: array[1],
371 staged: array[2],
372 conflicted: array[3],
373 }
374 }
375}
376
377impl ShellVars for ChangeCounters {
378 fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
380 out.write_var("untracked_count", self.untracked);
381 out.write_var("unstaged_count", self.unstaged);
382 out.write_var("staged_count", self.staged);
383 out.write_var("conflicted_count", self.conflicted);
384 }
385}
386
387pub fn count_changes(
394 repository: &Repository,
395) -> Result<ChangeCounters, git2::Error> {
396 if repository.is_bare() {
397 return Ok(ChangeCounters::default());
399 }
400
401 let mut options = StatusOptions::new();
402 options
404 .show(StatusShow::IndexAndWorkdir)
405 .include_untracked(true)
406 .exclude_submodules(true)
407 .no_refresh(true)
408 .update_index(false);
409 let statuses = repository.statuses(Some(&mut options))?;
410
411 let mut counters: [usize; 4] = [0; 4];
412 let buckets = [
413 Status::WT_NEW,
415 Status::WT_MODIFIED
417 | Status::WT_DELETED
418 | Status::WT_TYPECHANGE
419 | Status::WT_RENAMED,
420 Status::INDEX_NEW
422 | Status::INDEX_MODIFIED
423 | Status::INDEX_DELETED
424 | Status::INDEX_RENAMED
425 | Status::INDEX_TYPECHANGE,
426 Status::CONFLICTED,
428 ];
429
430 for status in statuses.iter() {
431 for (i, bits) in buckets.iter().enumerate() {
432 if status.status().intersects(*bits) {
433 counters[i] = counters[i].saturating_add(1);
434 }
435 }
436 }
437
438 Ok(ChangeCounters::from(counters))
439}
440
441pub fn count_stash(repository: &mut Repository) -> Result<usize, git2::Error> {
453 let mut count: usize = 0;
454 repository.stash_foreach(|_, _, _| {
455 count = count.saturating_add(1);
456 true
457 })?;
458
459 Ok(count)
460}
461
462fn time<D, F, R>(name: D, func: F) -> R
464where
465 D: fmt::Display,
466 F: FnOnce() -> R,
467{
468 let start = Instant::now();
469 let result = func();
470 tracing::debug!("{name}: {:?}", start.elapsed());
471 result
472}