1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
use std::path::{Path, PathBuf};
use git2::Repository;
use crate::gitinfo::{self, status::Status};
/// Holds information about a Git repository for status display.
#[expect(
clippy::struct_excessive_bools,
reason = "This structure holds repository state flags that are naturally represented as booleans"
)]
#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct RepoInfo {
/// The directory name of the repository.
pub name: String,
/// The current branch name.
pub branch: String,
/// Number of commits ahead of upstream.
pub ahead: usize,
/// Number of commits behind upstream.
pub behind: usize,
/// Total number of commits in the current branch.
pub commits: usize,
/// Status of the repository.
pub status: Status,
/// True if there are unpushed commits.
pub has_unpushed: bool,
/// Remote URL (if available).
pub remote_url: Option<String>,
/// Path to the repository directory.
pub path: PathBuf,
/// Number of stashes in the repository.
pub stash_count: usize,
/// True if the current branch has no upstream (local-only).
pub is_local_only: bool,
/// True if the repository was fast-forwarded
pub fast_forwarded: bool,
/// relative path from the starting directory
pub repo_path: String,
/// True if this is a Git worktree
pub is_worktree: bool,
}
impl RepoInfo {
/// Creates a new `RepoInfo` instance.
/// # Arguments
/// * `repo` - The Git repository to gather information from.
/// * `show_remote` - Whether to include the remote URL in the info.
/// * `fetch` - Whether to run a fetch operation before gathering info.
/// * `path` - The path to the repository directory.
///
/// # Returns
/// A `RepoInfo` instance containing the repository's status information.
///
/// # Errors
/// Returns an error if the repository cannot be opened, or if fetching fails.
/// If `fetch` is true, it will attempt to fetch from the "origin"
/// remote to update upstream information.
/// If fetching fails, it will use that error to return an error.
pub fn new(
repo: &mut Repository,
name: &str,
show_remote: bool,
fetch: bool,
merge: bool,
dir: &Path,
) -> anyhow::Result<Self> {
if fetch || merge {
// Attempt to fetch from origin, ignoring errors
gitinfo::fetch_origin(repo)?;
}
let name = gitinfo::get_repo_name(repo).unwrap_or_else(|| name.to_owned());
let branch = gitinfo::get_branch_name(repo);
let (ahead, behind, is_local_only) = gitinfo::get_ahead_behind_and_local_status(repo);
let commits = gitinfo::get_total_commits(repo)?;
let status = Status::new(repo);
let has_unpushed = ahead > 0;
let remote_url = if show_remote {
gitinfo::get_remote_url(repo)
} else {
None
};
let path = gitinfo::get_repo_path(repo);
let stash_count = gitinfo::get_stash_count(repo);
let repo_path = path.canonicalize().unwrap_or_else(|_| path.clone());
let root_path = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
let repo_path_relative = repo_path.strip_prefix(&root_path).unwrap_or(&repo_path);
// if relative path is empty, use repo_path
let repo_path_relative = if repo_path_relative.as_os_str().is_empty() {
&repo_path
} else {
repo_path_relative
};
let fast_forwarded = if merge {
// Fast-forward merge
gitinfo::merge_ff(repo)?
} else {
false
};
let is_worktree = repo.is_worktree();
Ok(Self {
name,
branch,
ahead,
behind,
commits,
status,
has_unpushed,
remote_url,
path,
stash_count,
is_local_only,
fast_forwarded,
repo_path: repo_path_relative.display().to_string(),
is_worktree,
})
}
/// Formats the local status showing ahead/behind counts or local-only indication.
/// # Returns
/// A formatted string showing ahead/behind counts or local-only indication.
pub fn format_local_status(&self) -> String {
if self.is_local_only {
"local-only".to_owned()
} else {
format!("↑{} ↓{}", self.ahead, self.behind)
}
}
/// Formats the status with stash information if stashes are present.
/// # Returns
/// A formatted string showing status and stash count if present.
pub fn format_status_with_stash_and_ff(&self) -> String {
let mut status_str = self.status.to_string();
if self.stash_count > 0 {
status_str = format!("{status_str} ({}*)", self.stash_count);
}
if self.fast_forwarded {
status_str = format!("{status_str} ↑↑");
}
status_str
}
}