1use std::fs;
12use std::io;
13use std::path::{Path, PathBuf};
14
15use crate::error::{Error, Result};
16use crate::objects::ObjectId;
17
18#[derive(Debug, Clone)]
20pub struct ReflogEntry {
21 pub old_oid: ObjectId,
23 pub new_oid: ObjectId,
25 pub identity: String,
27 pub message: String,
29}
30
31pub fn reflog_path(git_dir: &Path, refname: &str) -> PathBuf {
33 git_dir.join("logs").join(refname)
34}
35
36pub fn reflog_exists(git_dir: &Path, refname: &str) -> bool {
38 let path = reflog_path(git_dir, refname);
39 path.is_file()
40}
41
42pub fn read_reflog(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
46 let path = reflog_path(git_dir, refname);
47 let content = match fs::read_to_string(&path) {
48 Ok(c) => c,
49 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
50 Err(e) => return Err(Error::Io(e)),
51 };
52
53 let mut entries = Vec::new();
54 for line in content.lines() {
55 if line.is_empty() {
56 continue;
57 }
58 if let Some(entry) = parse_reflog_line(line) {
59 entries.push(entry);
60 }
61 }
62 Ok(entries)
63}
64
65fn parse_reflog_line(line: &str) -> Option<ReflogEntry> {
69 let (before_tab, message) = if let Some(pos) = line.find('\t') {
71 (&line[..pos], line[pos + 1..].to_string())
72 } else {
73 (line, String::new())
74 };
75
76 if before_tab.len() < 83 {
78 return None;
80 }
81
82 let old_hex = &before_tab[..40];
83 let new_hex = &before_tab[41..81];
84 let identity = before_tab[82..].to_string();
85
86 let old_oid = old_hex.parse::<ObjectId>().ok()?;
87 let new_oid = new_hex.parse::<ObjectId>().ok()?;
88
89 Some(ReflogEntry {
90 old_oid,
91 new_oid,
92 identity,
93 message,
94 })
95}
96
97pub fn delete_reflog_entries(
101 git_dir: &Path,
102 refname: &str,
103 indices: &[usize],
104) -> Result<()> {
105 let mut entries = read_reflog(git_dir, refname)?;
106 if entries.is_empty() {
107 return Ok(());
108 }
109
110 entries.reverse();
113
114 let indices_set: std::collections::HashSet<usize> =
115 indices.iter().copied().collect();
116
117 let path = reflog_path(git_dir, refname);
118 let remaining: Vec<&ReflogEntry> = entries
119 .iter()
120 .enumerate()
121 .filter(|(i, _)| !indices_set.contains(i))
122 .map(|(_, e)| e)
123 .collect();
124
125 let mut lines = Vec::new();
127 for entry in remaining.iter().rev() {
128 lines.push(format_reflog_entry(entry));
129 }
130
131 fs::write(&path, lines.join(""))?;
132 Ok(())
133}
134
135pub fn expire_reflog(
139 git_dir: &Path,
140 refname: &str,
141 expire_time: Option<i64>,
142) -> Result<usize> {
143 let entries = read_reflog(git_dir, refname)?;
144 if entries.is_empty() {
145 return Ok(0);
146 }
147
148 let path = reflog_path(git_dir, refname);
149 let mut kept = Vec::new();
150 let mut pruned = 0usize;
151
152 for entry in &entries {
153 let ts = parse_timestamp_from_identity(&entry.identity);
154 let dominated = match (expire_time, ts) {
155 (Some(cutoff), Some(t)) => t < cutoff,
156 (None, _) => true, (Some(_), None) => false, };
159 if dominated {
160 pruned += 1;
161 } else {
162 kept.push(format_reflog_entry(entry));
163 }
164 }
165
166 fs::write(&path, kept.join(""))?;
167 Ok(pruned)
168}
169
170fn format_reflog_entry(entry: &ReflogEntry) -> String {
172 format!(
173 "{} {} {}\t{}\n",
174 entry.old_oid, entry.new_oid, entry.identity, entry.message
175 )
176}
177
178fn parse_timestamp_from_identity(identity: &str) -> Option<i64> {
182 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
184 if parts.len() >= 2 {
185 parts[1].parse::<i64>().ok()
186 } else {
187 None
188 }
189}
190
191pub fn list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
193 let logs_dir = git_dir.join("logs");
194 let mut refs = Vec::new();
195
196 if logs_dir.join("HEAD").is_file() {
198 refs.push("HEAD".to_string());
199 }
200
201 let refs_logs = logs_dir.join("refs");
203 if refs_logs.is_dir() {
204 collect_reflog_refs(&refs_logs, "refs", &mut refs)?;
205 }
206
207 Ok(refs)
208}
209
210fn collect_reflog_refs(dir: &Path, prefix: &str, out: &mut Vec<String>) -> Result<()> {
211 let read_dir = match fs::read_dir(dir) {
212 Ok(rd) => rd,
213 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
214 Err(e) => return Err(Error::Io(e)),
215 };
216
217 for entry in read_dir {
218 let entry = entry.map_err(Error::Io)?;
219 let name = entry.file_name().to_string_lossy().to_string();
220 let full_name = format!("{prefix}/{name}");
221 let ft = entry.file_type().map_err(Error::Io)?;
222 if ft.is_dir() {
223 collect_reflog_refs(&entry.path(), &full_name, out)?;
224 } else if ft.is_file() {
225 out.push(full_name);
226 }
227 }
228 Ok(())
229}