1use std::fs;
16use std::path::Path;
17
18use crate::error::{Error, Result};
19use crate::objects::ObjectId;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum HeadState {
24 Branch {
26 refname: String,
28 short_name: String,
30 oid: Option<ObjectId>,
33 },
34 Detached {
36 oid: ObjectId,
38 },
39 Invalid,
41}
42
43impl HeadState {
44 #[must_use]
46 pub fn oid(&self) -> Option<&ObjectId> {
47 match self {
48 Self::Branch { oid, .. } => oid.as_ref(),
49 Self::Detached { oid } => Some(oid),
50 Self::Invalid => None,
51 }
52 }
53
54 #[must_use]
56 pub fn branch_name(&self) -> Option<&str> {
57 match self {
58 Self::Branch { short_name, .. } => Some(short_name),
59 _ => None,
60 }
61 }
62
63 #[must_use]
65 pub fn is_unborn(&self) -> bool {
66 matches!(self, Self::Branch { oid: None, .. })
67 }
68
69 #[must_use]
71 pub fn is_detached(&self) -> bool {
72 matches!(self, Self::Detached { .. })
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum InProgressOperation {
79 Merge,
81 RebaseInteractive,
83 Rebase,
85 CherryPick,
87 Revert,
89 Bisect,
91 Am,
93}
94
95impl InProgressOperation {
96 #[must_use]
98 pub fn description(&self) -> &'static str {
99 match self {
100 Self::Merge => "merge",
101 Self::RebaseInteractive => "interactive rebase",
102 Self::Rebase => "rebase",
103 Self::CherryPick => "cherry-pick",
104 Self::Revert => "revert",
105 Self::Bisect => "bisect",
106 Self::Am => "am",
107 }
108 }
109
110 #[must_use]
112 pub fn hint(&self) -> &'static str {
113 match self {
114 Self::Merge => "fix conflicts and run \"git commit\"\n (use \"git merge --abort\" to abort the merge)",
115 Self::RebaseInteractive => "fix conflicts and then run \"git rebase --continue\"\n (use \"git rebase --abort\" to abort the rebase)",
116 Self::Rebase => "fix conflicts and then run \"git rebase --continue\"\n (use \"git rebase --abort\" to abort the rebase)",
117 Self::CherryPick => "fix conflicts and run \"git cherry-pick --continue\"\n (use \"git cherry-pick --abort\" to abort the cherry-pick)",
118 Self::Revert => "fix conflicts and run \"git revert --continue\"\n (use \"git revert --abort\" to abort the revert)",
119 Self::Bisect => "use \"git bisect reset\" to get back to the original branch",
120 Self::Am => "fix conflicts and then run \"git am --continue\"\n (use \"git am --abort\" to abort the am)",
121 }
122 }
123}
124
125#[derive(Debug, Clone)]
130pub struct RepoState {
131 pub head: HeadState,
133 pub in_progress: Vec<InProgressOperation>,
135 pub is_bare: bool,
137}
138
139pub fn resolve_head(git_dir: &Path) -> Result<HeadState> {
151 let head_path = git_dir.join("HEAD");
152 let content = match fs::read_link(&head_path) {
153 Ok(link_target) => {
154 let rendered = link_target.to_string_lossy();
155 if link_target.is_absolute() {
156 format!("ref: {rendered}")
157 } else if rendered.starts_with("refs/") {
158 format!("ref: {rendered}")
159 } else {
160 fs::read_to_string(&head_path).map_err(Error::Io)?
161 }
162 }
163 Err(_) => match fs::read_to_string(&head_path) {
164 Ok(c) => c,
165 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(HeadState::Invalid),
166 Err(e) => return Err(Error::Io(e)),
167 },
168 };
169
170 let trimmed = content.trim();
171
172 if let Some(refname) = trimmed.strip_prefix("ref: ") {
173 let refname = refname.to_owned();
174 let short_name = refname
175 .strip_prefix("refs/heads/")
176 .unwrap_or(&refname)
177 .to_owned();
178
179 let oid = match crate::refs::resolve_ref(git_dir, &refname) {
182 Ok(oid) => Some(oid),
183 Err(Error::InvalidRef(msg)) if msg.starts_with("ref not found:") => None,
184 Err(e) => return Err(e),
185 };
186
187 Ok(HeadState::Branch {
188 refname,
189 short_name,
190 oid,
191 })
192 } else {
193 match ObjectId::from_hex(trimmed) {
195 Ok(oid) => Ok(HeadState::Detached { oid }),
196 Err(_) => Ok(HeadState::Invalid),
197 }
198 }
199}
200
201pub fn detect_in_progress(git_dir: &Path) -> Vec<InProgressOperation> {
211 let mut ops = Vec::new();
212
213 if git_dir.join("MERGE_HEAD").exists() {
214 ops.push(InProgressOperation::Merge);
215 }
216
217 let rebase_merge = git_dir.join("rebase-merge");
219 if rebase_merge.is_dir() {
220 if rebase_merge.join("interactive").exists() {
221 ops.push(InProgressOperation::RebaseInteractive);
222 } else {
223 ops.push(InProgressOperation::Rebase);
224 }
225 }
226
227 let rebase_apply = git_dir.join("rebase-apply");
229 if rebase_apply.is_dir() {
230 if rebase_apply.join("applying").exists() {
231 ops.push(InProgressOperation::Am);
232 } else {
233 ops.push(InProgressOperation::Rebase);
234 }
235 }
236
237 if git_dir.join("CHERRY_PICK_HEAD").exists() {
238 ops.push(InProgressOperation::CherryPick);
239 }
240
241 if git_dir.join("REVERT_HEAD").exists() {
242 ops.push(InProgressOperation::Revert);
243 }
244
245 let bisect_log = crate::refs::common_dir(git_dir)
246 .unwrap_or_else(|| git_dir.to_path_buf())
247 .join("BISECT_LOG");
248 if bisect_log.exists() {
249 ops.push(InProgressOperation::Bisect);
250 }
251
252 ops
253}
254
255pub fn repo_state(git_dir: &Path, is_bare: bool) -> Result<RepoState> {
266 let head = resolve_head(git_dir)?;
267 let in_progress = detect_in_progress(git_dir);
268
269 Ok(RepoState {
270 head,
271 in_progress,
272 is_bare,
273 })
274}
275
276pub fn read_merge_heads(git_dir: &Path) -> Result<Vec<ObjectId>> {
286 let path = git_dir.join("MERGE_HEAD");
287 let content = match fs::read_to_string(&path) {
288 Ok(c) => c,
289 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
290 Err(e) => return Err(Error::Io(e)),
291 };
292
293 let mut oids = Vec::new();
294 for line in content.lines() {
295 let trimmed = line.trim();
296 if !trimmed.is_empty() {
297 oids.push(ObjectId::from_hex(trimmed)?);
298 }
299 }
300 Ok(oids)
301}
302
303pub fn read_merge_msg(git_dir: &Path) -> Result<Option<String>> {
313 let path = git_dir.join("MERGE_MSG");
314 match fs::read_to_string(&path) {
315 Ok(c) => Ok(Some(c)),
316 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
317 Err(e) => Err(Error::Io(e)),
318 }
319}
320
321pub fn read_cherry_pick_head(git_dir: &Path) -> Result<Option<ObjectId>> {
323 read_single_oid_file(&git_dir.join("CHERRY_PICK_HEAD"))
324}
325
326pub fn read_revert_head(git_dir: &Path) -> Result<Option<ObjectId>> {
328 read_single_oid_file(&git_dir.join("REVERT_HEAD"))
329}
330
331pub fn read_orig_head(git_dir: &Path) -> Result<Option<ObjectId>> {
333 read_single_oid_file(&git_dir.join("ORIG_HEAD"))
334}
335
336fn read_single_oid_file(path: &Path) -> Result<Option<ObjectId>> {
338 match fs::read_to_string(path) {
339 Ok(content) => {
340 let trimmed = content.trim();
341 if trimmed.is_empty() {
342 Ok(None)
343 } else {
344 Ok(Some(ObjectId::from_hex(trimmed)?))
345 }
346 }
347 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
348 Err(e) => Err(Error::Io(e)),
349 }
350}
351
352pub fn upstream_tracking(_git_dir: &Path, _branch: &str) -> Result<Option<(usize, usize)>> {
366 Ok(None)
368}