1use std::fs;
12use std::io;
13use std::path::{Path, PathBuf};
14
15use crate::error::{Error, Result};
16use crate::merge_base;
17use crate::objects::ObjectId;
18use crate::refs;
19use crate::repo::Repository;
20
21#[derive(Debug, Clone)]
23pub struct ReflogEntry {
24 pub old_oid: ObjectId,
26 pub new_oid: ObjectId,
28 pub identity: String,
30 pub message: String,
32}
33
34pub fn reflog_path(git_dir: &Path, refname: &str) -> PathBuf {
36 git_dir.join("logs").join(refname)
37}
38
39pub fn reflog_exists(git_dir: &Path, refname: &str) -> bool {
41 if crate::reftable::is_reftable_repo(git_dir) {
42 return crate::reftable::reftable_reflog_exists(git_dir, refname);
43 }
44 let path = reflog_path(git_dir, refname);
45 path.is_file()
46}
47
48pub fn read_reflog(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
52 if crate::reftable::is_reftable_repo(git_dir) {
53 return crate::reftable::reftable_read_reflog(git_dir, refname);
54 }
55 let path = reflog_path(git_dir, refname);
56 let content = match fs::read_to_string(&path) {
57 Ok(c) => c,
58 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
59 Err(e) => return Err(Error::Io(e)),
60 };
61
62 let mut entries = Vec::new();
63 for line in content.lines() {
64 if line.is_empty() {
65 continue;
66 }
67 if let Some(entry) = parse_reflog_line(line) {
68 entries.push(entry);
69 }
70 }
71 Ok(entries)
72}
73
74fn parse_reflog_line(line: &str) -> Option<ReflogEntry> {
78 let (before_tab, message) = if let Some(pos) = line.find('\t') {
80 (&line[..pos], line[pos + 1..].to_string())
81 } else {
82 (line, String::new())
83 };
84
85 if before_tab.len() < 83 {
87 return None;
89 }
90
91 let old_hex = &before_tab[..40];
92 let new_hex = &before_tab[41..81];
93 let identity = before_tab[82..].to_string();
94
95 let old_oid = old_hex.parse::<ObjectId>().ok()?;
96 let new_oid = new_hex.parse::<ObjectId>().ok()?;
97
98 Some(ReflogEntry {
99 old_oid,
100 new_oid,
101 identity,
102 message,
103 })
104}
105
106pub fn delete_reflog_entries(git_dir: &Path, refname: &str, indices: &[usize]) -> Result<()> {
110 let mut entries = read_reflog(git_dir, refname)?;
111 if entries.is_empty() {
112 return Ok(());
113 }
114
115 entries.reverse();
118
119 let indices_set: std::collections::HashSet<usize> = indices.iter().copied().collect();
120
121 let path = reflog_path(git_dir, refname);
122 let remaining: Vec<&ReflogEntry> = entries
123 .iter()
124 .enumerate()
125 .filter(|(i, _)| !indices_set.contains(i))
126 .map(|(_, e)| e)
127 .collect();
128
129 let mut lines = Vec::new();
131 for entry in remaining.iter().rev() {
132 lines.push(format_reflog_entry(entry));
133 }
134
135 fs::write(&path, lines.join(""))?;
136 Ok(())
137}
138
139pub fn expire_reflog(git_dir: &Path, refname: &str, expire_time: Option<i64>) -> 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
170pub fn expire_reflog_unreachable(
179 repo: &Repository,
180 git_dir: &Path,
181 refname: &str,
182 cutoff: Option<i64>,
183) -> Result<usize> {
184 let Some(cutoff) = cutoff else {
185 return Ok(0);
186 };
187 if crate::reftable::is_reftable_repo(git_dir) {
188 return Ok(0);
189 }
190 let tip = match refs::resolve_ref(git_dir, refname) {
191 Ok(o) => o,
192 Err(_) => return Ok(0),
193 };
194 let ancestors = match merge_base::ancestor_closure(repo, tip) {
195 Ok(a) => a,
196 Err(_) => return Ok(0),
197 };
198
199 let entries = read_reflog(git_dir, refname)?;
200 if entries.is_empty() {
201 return Ok(0);
202 }
203
204 let path = reflog_path(git_dir, refname);
205 let mut kept = Vec::new();
206 let mut pruned = 0usize;
207
208 for entry in &entries {
209 let ts = parse_timestamp_from_identity(&entry.identity);
210 let unreachable = !entry.new_oid.is_zero() && !ancestors.contains(&entry.new_oid);
211 let should_prune = unreachable && matches!(ts, Some(t) if t < cutoff);
212 if should_prune {
213 pruned += 1;
214 } else {
215 kept.push(format_reflog_entry(entry));
216 }
217 }
218
219 fs::write(&path, kept.join(""))?;
220 Ok(pruned)
221}
222
223fn format_reflog_entry(entry: &ReflogEntry) -> String {
225 format!(
226 "{} {} {}\t{}\n",
227 entry.old_oid, entry.new_oid, entry.identity, entry.message
228 )
229}
230
231fn parse_timestamp_from_identity(identity: &str) -> Option<i64> {
235 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
237 if parts.len() >= 2 {
238 parts[1].parse::<i64>().ok()
239 } else {
240 None
241 }
242}
243
244pub fn list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
246 let logs_dir = git_dir.join("logs");
247 let mut refs = Vec::new();
248
249 if logs_dir.join("HEAD").is_file() {
251 refs.push("HEAD".to_string());
252 }
253
254 let refs_logs = logs_dir.join("refs");
256 if refs_logs.is_dir() {
257 collect_reflog_refs(&refs_logs, "refs", &mut refs)?;
258 }
259
260 Ok(refs)
261}
262
263fn collect_reflog_refs(dir: &Path, prefix: &str, out: &mut Vec<String>) -> Result<()> {
264 let read_dir = match fs::read_dir(dir) {
265 Ok(rd) => rd,
266 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
267 Err(e) => return Err(Error::Io(e)),
268 };
269
270 for entry in read_dir {
271 let entry = entry.map_err(Error::Io)?;
272 let name = entry.file_name().to_string_lossy().to_string();
273 let full_name = format!("{prefix}/{name}");
274 let ft = entry.file_type().map_err(Error::Io)?;
275 if ft.is_dir() {
276 collect_reflog_refs(&entry.path(), &full_name, out)?;
277 } else if ft.is_file() {
278 out.push(full_name);
279 }
280 }
281 Ok(())
282}