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_to_string(&head_path) {
153 Ok(c) => c,
154 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(HeadState::Invalid),
155 Err(e) => return Err(Error::Io(e)),
156 };
157
158 let trimmed = content.trim();
159
160 if let Some(refname) = trimmed.strip_prefix("ref: ") {
161 let refname = refname.to_owned();
162 let short_name = refname
163 .strip_prefix("refs/heads/")
164 .unwrap_or(&refname)
165 .to_owned();
166
167 let oid = resolve_ref(git_dir, &refname)?;
169
170 Ok(HeadState::Branch {
171 refname,
172 short_name,
173 oid,
174 })
175 } else {
176 match ObjectId::from_hex(trimmed) {
178 Ok(oid) => Ok(HeadState::Detached { oid }),
179 Err(_) => Ok(HeadState::Invalid),
180 }
181 }
182}
183
184fn resolve_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
198 let ref_path = git_dir.join(refname);
199
200 match fs::read_to_string(&ref_path) {
202 Ok(content) => {
203 let trimmed = content.trim();
204 if let Some(target) = trimmed.strip_prefix("ref: ") {
206 return resolve_ref(git_dir, target);
207 }
208 match ObjectId::from_hex(trimmed) {
209 Ok(oid) => Ok(Some(oid)),
210 Err(_) => Ok(None),
211 }
212 }
213 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
214 resolve_packed_ref(git_dir, refname)
216 }
217 Err(e) => Err(Error::Io(e)),
218 }
219}
220
221fn resolve_packed_ref(git_dir: &Path, refname: &str) -> Result<Option<ObjectId>> {
223 let packed_path = git_dir.join("packed-refs");
224 let content = match fs::read_to_string(&packed_path) {
225 Ok(c) => c,
226 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
227 Err(e) => return Err(Error::Io(e)),
228 };
229
230 for line in content.lines() {
231 let line = line.trim();
232 if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
233 continue;
234 }
235 if let Some((hex, name)) = line.split_once(' ') {
237 if name == refname {
238 if let Ok(oid) = ObjectId::from_hex(hex) {
239 return Ok(Some(oid));
240 }
241 }
242 }
243 }
244
245 Ok(None)
246}
247
248pub fn detect_in_progress(git_dir: &Path) -> Vec<InProgressOperation> {
258 let mut ops = Vec::new();
259
260 if git_dir.join("MERGE_HEAD").exists() {
261 ops.push(InProgressOperation::Merge);
262 }
263
264 let rebase_merge = git_dir.join("rebase-merge");
266 if rebase_merge.is_dir() {
267 if rebase_merge.join("interactive").exists() {
268 ops.push(InProgressOperation::RebaseInteractive);
269 } else {
270 ops.push(InProgressOperation::Rebase);
271 }
272 }
273
274 let rebase_apply = git_dir.join("rebase-apply");
276 if rebase_apply.is_dir() {
277 if rebase_apply.join("applying").exists() {
278 ops.push(InProgressOperation::Am);
279 } else {
280 ops.push(InProgressOperation::Rebase);
281 }
282 }
283
284 if git_dir.join("CHERRY_PICK_HEAD").exists() {
285 ops.push(InProgressOperation::CherryPick);
286 }
287
288 if git_dir.join("REVERT_HEAD").exists() {
289 ops.push(InProgressOperation::Revert);
290 }
291
292 if git_dir.join("BISECT_LOG").exists() {
293 ops.push(InProgressOperation::Bisect);
294 }
295
296 ops
297}
298
299pub fn repo_state(git_dir: &Path, is_bare: bool) -> Result<RepoState> {
310 let head = resolve_head(git_dir)?;
311 let in_progress = detect_in_progress(git_dir);
312
313 Ok(RepoState {
314 head,
315 in_progress,
316 is_bare,
317 })
318}
319
320pub fn read_merge_heads(git_dir: &Path) -> Result<Vec<ObjectId>> {
330 let path = git_dir.join("MERGE_HEAD");
331 let content = match fs::read_to_string(&path) {
332 Ok(c) => c,
333 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
334 Err(e) => return Err(Error::Io(e)),
335 };
336
337 let mut oids = Vec::new();
338 for line in content.lines() {
339 let trimmed = line.trim();
340 if !trimmed.is_empty() {
341 oids.push(ObjectId::from_hex(trimmed)?);
342 }
343 }
344 Ok(oids)
345}
346
347pub fn read_merge_msg(git_dir: &Path) -> Result<Option<String>> {
357 let path = git_dir.join("MERGE_MSG");
358 match fs::read_to_string(&path) {
359 Ok(c) => Ok(Some(c)),
360 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
361 Err(e) => Err(Error::Io(e)),
362 }
363}
364
365pub fn read_cherry_pick_head(git_dir: &Path) -> Result<Option<ObjectId>> {
367 read_single_oid_file(&git_dir.join("CHERRY_PICK_HEAD"))
368}
369
370pub fn read_revert_head(git_dir: &Path) -> Result<Option<ObjectId>> {
372 read_single_oid_file(&git_dir.join("REVERT_HEAD"))
373}
374
375pub fn read_orig_head(git_dir: &Path) -> Result<Option<ObjectId>> {
377 read_single_oid_file(&git_dir.join("ORIG_HEAD"))
378}
379
380fn read_single_oid_file(path: &Path) -> Result<Option<ObjectId>> {
382 match fs::read_to_string(path) {
383 Ok(content) => {
384 let trimmed = content.trim();
385 if trimmed.is_empty() {
386 Ok(None)
387 } else {
388 Ok(Some(ObjectId::from_hex(trimmed)?))
389 }
390 }
391 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
392 Err(e) => Err(Error::Io(e)),
393 }
394}
395
396pub fn upstream_tracking(_git_dir: &Path, _branch: &str) -> Result<Option<(usize, usize)>> {
410 Ok(None)
412}