up_rs/tasks/git/
status.rs1use crate::tasks::git::branch::get_branch_name;
3use crate::tasks::git::branch::get_push_branch;
4use crate::tasks::git::cherry::unmerged_commits;
5use crate::tasks::git::errors::GitError as E;
6use crate::utils::files::to_utf8_path;
7use color_eyre::eyre::ensure;
8use color_eyre::eyre::eyre;
9use color_eyre::eyre::Result;
10use git2::BranchType;
11use git2::Config;
12use git2::ErrorCode;
13use git2::Repository;
14use git2::StatusOptions;
15use git2::Statuses;
16use git2::SubmoduleIgnore;
17use std::fmt::Write as _; use tracing::trace;
19use tracing::warn;
20
21pub(super) fn ensure_repo_clean(repo: &Repository) -> Result<()> {
24 let statuses = repo_statuses(repo)?;
25 trace!("Repo statuses: '{}'", status_short(repo, &statuses)?);
26 ensure!(
27 statuses.is_empty(),
28 E::UncommittedChanges {
29 status: status_short(repo, &statuses)?
30 }
31 );
32 Ok(())
33}
34
35pub(super) fn warn_for_unpushed_changes(
50 repo: &mut Repository,
51 user_git_config: &Config,
52) -> Result<()> {
53 {
55 let statuses = repo_statuses(repo)?;
56 if !statuses.is_empty() {
57 warn!("Uncommitted changes:\n{}", status_short(repo, &statuses)?);
58 }
59 }
60
61 {
63 let mut stash_messages = Vec::new();
64 repo.stash_foreach(|_index, message, _stash_id| {
65 stash_messages.push(message.to_owned());
66 true
67 })?;
68 if !stash_messages.is_empty() {
69 warn!("Stashed changes:\n{:#?}", stash_messages);
70 }
71 }
72
73 for branch in repo.branches(Some(BranchType::Local))? {
74 let branch = branch?.0;
75 let branch_name = get_branch_name(&branch)?;
76 if let Some(push_branch) = get_push_branch(repo, &branch_name, user_git_config)? {
77 if unmerged_commits(repo, &push_branch, &branch)? {
79 warn!("Branch '{branch_name}' has changes that aren't in @{{push}}.",);
80 }
81 } else {
82 match branch.upstream() {
83 Ok(upstream_branch) => {
84 if unmerged_commits(repo, &upstream_branch, &branch)? {
86 warn!("Branch '{branch_name}' has changes that aren't in @{{upstream}}.",);
87 }
88 }
89 Err(e) if e.code() == ErrorCode::NotFound => {
90 warn!("Branch '{branch_name}' has no @{{upstream}} or @{{push}} branch.",);
92 }
93 Err(e) => {
94 return Err(e.into());
96 }
97 }
98 }
99 }
100
101 let mut unmerged_branches = Vec::new();
104 for branch in repo.branches(Some(BranchType::Remote))? {
105 let branch = branch?.0;
106 let branch_name = get_branch_name(&branch)?;
107 if branch_name.contains("fork")
111 && !branch_name.contains("HEAD")
113 && !branch_name.contains("forkmain")
115 {
116 unmerged_branches.push(
117 branch_name,
119 );
120 }
121 }
122 if !unmerged_branches.is_empty() {
123 warn!("Unmerged fork branches: {} .", unmerged_branches.join(" "),);
124 }
125
126 Ok(())
127}
128
129fn repo_statuses(repo: &Repository) -> Result<Statuses> {
135 let mut status_options = StatusOptions::new();
136 status_options
138 .include_ignored(false)
139 .include_untracked(true);
140 Ok(repo.statuses(Some(&mut status_options))?)
141}
142
143#[allow(clippy::too_many_lines, clippy::useless_let_if_seq)]
147fn status_short(repo: &Repository, statuses: &git2::Statuses) -> Result<String> {
148 let mut output = String::new();
149 for entry in statuses
150 .iter()
151 .filter(|e| e.status() != git2::Status::CURRENT)
152 {
153 let mut index_status = match entry.status() {
154 s if s.contains(git2::Status::INDEX_NEW) => 'A',
155 s if s.contains(git2::Status::INDEX_MODIFIED) => 'M',
156 s if s.contains(git2::Status::INDEX_DELETED) => 'D',
157 s if s.contains(git2::Status::INDEX_RENAMED) => 'R',
158 s if s.contains(git2::Status::INDEX_TYPECHANGE) => 'T',
159 _ => ' ',
160 };
161 let mut worktree_status = match entry.status() {
162 s if s.contains(git2::Status::WT_NEW) => {
163 if index_status == ' ' {
164 index_status = '?';
165 }
166 '?'
167 }
168 s if s.contains(git2::Status::WT_MODIFIED) => 'M',
169 s if s.contains(git2::Status::WT_DELETED) => 'D',
170 s if s.contains(git2::Status::WT_RENAMED) => 'R',
171 s if s.contains(git2::Status::WT_TYPECHANGE) => 'T',
172 _ => ' ',
173 };
174
175 if entry.status().contains(git2::Status::IGNORED) {
176 index_status = '!';
177 worktree_status = '!';
178 }
179 if index_status == '?' && worktree_status == '?' {
180 continue;
181 }
182 let mut extra = "";
183
184 let status = entry.index_to_workdir().and_then(|diff| {
189 let ignore = SubmoduleIgnore::Unspecified;
190 diff.new_file()
191 .path_bytes()
192 .and_then(|s| std::str::from_utf8(s).ok())
193 .and_then(|name| repo.submodule_status(name, ignore).ok())
194 });
195 if let Some(status) = status {
196 if status.contains(git2::SubmoduleStatus::WD_MODIFIED) {
197 extra = " (new commits)";
198 } else if status.contains(git2::SubmoduleStatus::WD_INDEX_MODIFIED)
199 || status.contains(git2::SubmoduleStatus::WD_WD_MODIFIED)
200 {
201 extra = " (modified content)";
202 } else if status.contains(git2::SubmoduleStatus::WD_UNTRACKED) {
203 extra = " (untracked content)";
204 }
205 }
206
207 let (mut a, mut b, mut c) = (None, None, None);
208 if let Some(diff) = entry.head_to_index() {
209 a = diff.old_file().path();
210 b = diff.new_file().path();
211 }
212 if let Some(diff) = entry.index_to_workdir() {
213 a = a.or_else(|| diff.old_file().path());
214 b = b.or_else(|| diff.old_file().path());
215 c = diff.new_file().path();
216 }
217 let a = to_utf8_path(a.ok_or_else(|| eyre!("Couldn't work out diff status a"))?)?;
218 let b = to_utf8_path(b.ok_or_else(|| eyre!("Couldn't work out diff status b"))?)?;
219 let c = to_utf8_path(c.ok_or_else(|| eyre!("Couldn't work out diff status c"))?)?;
220
221 output += &match (index_status, worktree_status) {
222 ('R', 'R') => format!("RR {a} {b} {c}{extra}\n"),
223 ('R', worktree_status) => format!("R{worktree_status} {a} {b}{extra}\n"),
224 (index_status, 'R') => {
225 format!("{index_status}R {a} {c}{extra}\n")
226 }
227 (index_status, worktree_status) => {
228 format!("{index_status}{worktree_status} {a}{extra}\n")
229 }
230 }
231 }
232
233 for entry in statuses
234 .iter()
235 .filter(|e| e.status() == git2::Status::WT_NEW)
236 {
237 _ = writeln!(
238 output,
239 "?? {}",
240 to_utf8_path(
241 entry
242 .index_to_workdir()
243 .ok_or_else(|| eyre!("Couldn't find the workdir for current status entry."))?
244 .old_file()
245 .path()
246 .ok_or_else(|| eyre!("Couldn't work out path to old file."))?
247 )?
248 );
249 }
250 Ok(output)
251}