1use std::fs;
16use std::path::{Path, PathBuf};
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 = resolve_ref(git_dir, &refname)?;
181
182 Ok(HeadState::Branch {
183 refname,
184 short_name,
185 oid,
186 })
187 } else {
188 match ObjectId::from_hex(trimmed) {
190 Ok(oid) => Ok(HeadState::Detached { oid }),
191 Err(_) => Ok(HeadState::Invalid),
192 }
193 }
194}
195
196fn resolve_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
210 if crate::reftable::is_reftable_repo(git_dir) {
212 match crate::reftable::reftable_resolve_ref(git_dir, refname) {
213 Ok(oid) => return Ok(Some(oid)),
214 Err(_) => return Ok(None),
215 }
216 }
217
218 let ref_path = git_dir.join(refname);
219
220 match fs::read_to_string(&ref_path) {
222 Ok(content) => {
223 let trimmed = content.trim();
224 if let Some(target) = trimmed.strip_prefix("ref: ") {
226 return resolve_ref(git_dir, target);
227 }
228 match ObjectId::from_hex(trimmed) {
229 Ok(oid) => Ok(Some(oid)),
230 Err(_) => Ok(None),
231 }
232 }
233 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
234 if let Some(oid) = resolve_packed_ref(git_dir, refname)? {
236 return Ok(Some(oid));
237 }
238
239 if let Some(common) = common_dir_for(git_dir) {
242 if common != git_dir {
243 let common_ref = common.join(refname);
245 match fs::read_to_string(&common_ref) {
246 Ok(content) => {
247 let trimmed = content.trim();
248 if let Some(target) = trimmed.strip_prefix("ref: ") {
249 return resolve_ref(git_dir, target);
250 }
251 if let Ok(oid) = ObjectId::from_hex(trimmed) {
252 return Ok(Some(oid));
253 }
254 }
255 Err(e2) if e2.kind() == std::io::ErrorKind::NotFound => {}
256 Err(e2)
257 if e2.kind() == std::io::ErrorKind::IsADirectory
258 || e2.kind() == std::io::ErrorKind::NotADirectory
259 || e2.raw_os_error() == Some(21)
260 || e2.raw_os_error() == Some(20) => {}
261 Err(e2) => return Err(Error::Io(e2)),
262 }
263 return resolve_packed_ref(&common, refname);
265 }
266 }
267 Ok(None)
268 }
269 Err(e)
270 if e.kind() == std::io::ErrorKind::IsADirectory
271 || e.kind() == std::io::ErrorKind::NotADirectory
272 || e.raw_os_error() == Some(21)
273 || e.raw_os_error() == Some(20) =>
274 {
275 Ok(None)
279 }
280 Err(e) => Err(Error::Io(e)),
281 }
282}
283
284fn common_dir_for(git_dir: &Path) -> Option<PathBuf> {
286 let raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
287 let rel = raw.trim();
288 let path = if Path::new(rel).is_absolute() {
289 PathBuf::from(rel)
290 } else {
291 git_dir.join(rel)
292 };
293 path.canonicalize().ok()
294}
295
296fn resolve_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
298 let packed_path = git_dir.join("packed-refs");
299 let content = match fs::read_to_string(&packed_path) {
300 Ok(c) => c,
301 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
302 Err(e) => return Err(Error::Io(e)),
303 };
304
305 for line in content.lines() {
306 let line = line.trim();
307 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
308 continue;
309 }
310 if let Some((hex, name)) = line.split_once(' ') {
312 if name == refname {
313 if let Ok(oid) = ObjectId::from_hex(hex) {
314 return Ok(Some(oid));
315 }
316 }
317 }
318 }
319
320 Ok(None)
321}
322
323pub fn detect_in_progress(git_dir: &Path) -> Vec<InProgressOperation> {
333 let mut ops = Vec::new();
334
335 if git_dir.join("MERGE_HEAD").exists() {
336 ops.push(InProgressOperation::Merge);
337 }
338
339 let rebase_merge = git_dir.join("rebase-merge");
341 if rebase_merge.is_dir() {
342 if rebase_merge.join("interactive").exists() {
343 ops.push(InProgressOperation::RebaseInteractive);
344 } else {
345 ops.push(InProgressOperation::Rebase);
346 }
347 }
348
349 let rebase_apply = git_dir.join("rebase-apply");
351 if rebase_apply.is_dir() {
352 if rebase_apply.join("applying").exists() {
353 ops.push(InProgressOperation::Am);
354 } else {
355 ops.push(InProgressOperation::Rebase);
356 }
357 }
358
359 if git_dir.join("CHERRY_PICK_HEAD").exists() {
360 ops.push(InProgressOperation::CherryPick);
361 }
362
363 if git_dir.join("REVERT_HEAD").exists() {
364 ops.push(InProgressOperation::Revert);
365 }
366
367 if git_dir.join("BISECT_LOG").exists() {
368 ops.push(InProgressOperation::Bisect);
369 }
370
371 ops
372}
373
374pub fn repo_state(git_dir: &Path, is_bare: bool) -> Result<RepoState> {
385 let head = resolve_head(git_dir)?;
386 let in_progress = detect_in_progress(git_dir);
387
388 Ok(RepoState {
389 head,
390 in_progress,
391 is_bare,
392 })
393}
394
395pub fn read_merge_heads(git_dir: &Path) -> Result<Vec<ObjectId>> {
405 let path = git_dir.join("MERGE_HEAD");
406 let content = match fs::read_to_string(&path) {
407 Ok(c) => c,
408 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
409 Err(e) => return Err(Error::Io(e)),
410 };
411
412 let mut oids = Vec::new();
413 for line in content.lines() {
414 let trimmed = line.trim();
415 if !trimmed.is_empty() {
416 oids.push(ObjectId::from_hex(trimmed)?);
417 }
418 }
419 Ok(oids)
420}
421
422pub fn read_merge_msg(git_dir: &Path) -> Result<Option<String>> {
432 let path = git_dir.join("MERGE_MSG");
433 match fs::read_to_string(&path) {
434 Ok(c) => Ok(Some(c)),
435 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
436 Err(e) => Err(Error::Io(e)),
437 }
438}
439
440pub fn read_cherry_pick_head(git_dir: &Path) -> Result<Option<ObjectId>> {
442 read_single_oid_file(&git_dir.join("CHERRY_PICK_HEAD"))
443}
444
445pub fn read_revert_head(git_dir: &Path) -> Result<Option<ObjectId>> {
447 read_single_oid_file(&git_dir.join("REVERT_HEAD"))
448}
449
450pub fn read_orig_head(git_dir: &Path) -> Result<Option<ObjectId>> {
452 read_single_oid_file(&git_dir.join("ORIG_HEAD"))
453}
454
455fn read_single_oid_file(path: &Path) -> Result<Option<ObjectId>> {
457 match fs::read_to_string(path) {
458 Ok(content) => {
459 let trimmed = content.trim();
460 if trimmed.is_empty() {
461 Ok(None)
462 } else {
463 Ok(Some(ObjectId::from_hex(trimmed)?))
464 }
465 }
466 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
467 Err(e) => Err(Error::Io(e)),
468 }
469}
470
471pub fn upstream_tracking(_git_dir: &Path, _branch: &str) -> Result<Option<(usize, usize)>> {
485 Ok(None)
487}