| 135212de Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
use super::*; |
| 11 |
use std::time::UNIX_EPOCH; |
| 12 |
|
| 13 |
#[derive(Debug, Clone, Default, PartialEq, Eq)] |
| 14 |
pub struct PromptInfo { |
| 15 |
pub bookmark: String, |
| 16 |
|
| 17 |
pub change_id: String, |
| 18 |
pub heads: usize, |
| 19 |
pub dirty: bool, |
| 20 |
pub conflicted: bool, |
| 21 |
|
| 22 |
pub ahead: Option<usize>, |
| 23 |
pub behind: Option<usize>, |
| 24 |
} |
| 25 |
|
| 26 |
impl PromptInfo { |
| 694805cc Andy McCall ↶ |
27 |
|
| 28 |
|
| 29 |
pub fn state_glyph(&self) -> &'static str { |
| 30 |
if self.conflicted { |
| 31 |
"‼" |
| 32 |
} else if self.dirty { |
| 33 |
"✎" |
| 34 |
} else { |
| 35 |
"✔" |
| 36 |
} |
| 37 |
} |
| 38 |
|
| 39 |
|
| 40 |
pub fn state_color(&self) -> u8 { |
| 41 |
if self.conflicted { |
| 42 |
31 |
| 43 |
} else if self.dirty { |
| 44 |
33 |
| 45 |
} else { |
| 46 |
36 |
| 47 |
} |
| 48 |
} |
| 49 |
|
| 50 |
|
| 51 |
|
| 52 |
pub fn format_colored(&self, fmt: &str, guard: (&str, &str)) -> String { |
| 75167672 Andy McCall |
53 |
format!( |
| 54 |
"{}\x1b[{}m{}{}{}\x1b[0m{}", |
| 55 |
guard.0, |
| 56 |
self.state_color(), |
| 57 |
guard.1, |
| 58 |
self.format(fmt), |
| 59 |
guard.0, |
| 60 |
guard.1 |
| 61 |
) |
| 694805cc Andy McCall ↶ |
62 |
} |
| 63 |
|
| 135212de Andy McCall ↶ |
64 |
|
| 65 |
|
| 694805cc Andy McCall ↶ |
66 |
|
| 67 |
|
| 68 |
|
| 135212de Andy McCall ↶ |
69 |
pub fn format(&self, fmt: &str) -> String { |
| 70 |
let mut out = String::new(); |
| 71 |
let mut chars = fmt.chars(); |
| 72 |
while let Some(c) = chars.next() { |
| 73 |
if c != '%' { |
| 74 |
out.push(c); |
| 75 |
continue; |
| 76 |
} |
| 77 |
match chars.next() { |
| 78 |
Some('b') => out.push_str(&self.bookmark), |
| 79 |
Some('c') => out.push_str(&self.change_id), |
| 80 |
Some('h') => out.push_str(&self.heads.to_string()), |
| 81 |
Some('d') => { |
| 82 |
if self.dirty { |
| 83 |
out.push('*'); |
| 84 |
} |
| 85 |
} |
| 86 |
Some('x') => { |
| 87 |
if self.conflicted { |
| 88 |
out.push('!'); |
| 89 |
} |
| 90 |
} |
| 91 |
Some('a') => out.push_str(&self.ahead.map(|n| n.to_string()).unwrap_or_default()), |
| 92 |
Some('B') => out.push_str(&self.behind.map(|n| n.to_string()).unwrap_or_default()), |
| 93 |
Some('A') => { |
| 94 |
if let Some(n) = self.ahead.filter(|n| *n > 0) { |
| 95 |
out.push_str(&format!("+{n}")); |
| 96 |
} |
| 97 |
} |
| 98 |
Some('D') => { |
| 99 |
if let Some(n) = self.behind.filter(|n| *n > 0) { |
| 100 |
out.push_str(&format!("-{n}")); |
| 101 |
} |
| 102 |
} |
| 694805cc Andy McCall ↶ |
103 |
Some('s') => out.push_str(self.state_glyph()), |
| 104 |
Some('U') => { |
| 105 |
if let Some(n) = self.ahead.filter(|n| *n > 0) { |
| 106 |
out.push_str(&format!("↑{n}")); |
| 107 |
} |
| 108 |
} |
| 109 |
Some('O') => { |
| 110 |
if let Some(n) = self.behind.filter(|n| *n > 0) { |
| 111 |
out.push_str(&format!("↓{n}")); |
| 112 |
} |
| 113 |
} |
| 114 |
Some('i') => out.push_str(ICON), |
| 135212de Andy McCall ↶ |
115 |
Some('%') => out.push('%'), |
| 116 |
Some(other) => { |
| 117 |
out.push('%'); |
| 118 |
out.push(other); |
| 119 |
} |
| 120 |
None => out.push('%'), |
| 121 |
} |
| 122 |
} |
| 123 |
out |
| 124 |
} |
| 125 |
} |
| 126 |
|
| 127 |
|
| b7f102fc Andy McCall ↶ |
128 |
|
| 129 |
|
| 130 |
#[derive(Clone, Debug)] |
| 131 |
pub(crate) struct ManifestEntry { |
| 132 |
pub size: u64, |
| 133 |
pub mtime_ns: u128, |
| 1966f5db Andy McCall ↶ |
134 |
pub hash: [u8; 32], |
| b7f102fc Andy McCall ↶ |
135 |
pub blob: Option<ObjectRef>, |
| cc7a0056 Andy McCall ↶ |
136 |
|
| 137 |
|
| 1966f5db Andy McCall ↶ |
138 |
pub graph: Option<[u8; 32]>, |
| da868d5d Andy McCall ↶ |
139 |
|
| 140 |
|
| 141 |
pub exec: bool, |
| b7f102fc Andy McCall ↶ |
142 |
} |
| 143 |
|
| 144 |
#[derive(Clone, Debug, Default)] |
| 145 |
pub(crate) struct Manifest { |
| 146 |
|
| 147 |
pub written_ns: u128, |
| 148 |
pub entries: BTreeMap<String, ManifestEntry>, |
| 1ec3f008 Andy McCall ↶ |
149 |
|
| 150 |
|
| 151 |
|
| 152 |
pub dirs: Vec<(String, u128)>, |
| 153 |
|
| 154 |
pub nested: Vec<String>, |
| b7f102fc Andy McCall ↶ |
155 |
} |
| 135212de Andy McCall ↶ |
156 |
|
| d0990c2b Andy McCall ↶ |
157 |
|
| 158 |
|
| 159 |
pub(crate) type CachedManifest = ((u64, u128, u64), (bool, bool, std::sync::Arc<Manifest>)); |
| 160 |
|
| 0389c3e8 Andy McCall ↶ |
161 |
|
| 236f35c0 Andy McCall ↶ |
162 |
pub(crate) fn now_ns() -> u128 { |
| 0389c3e8 Andy McCall ↶ |
163 |
std::time::SystemTime::now() |
| 164 |
.duration_since(UNIX_EPOCH) |
| 165 |
.map(|d| d.as_nanos()) |
| 166 |
.unwrap_or(0) |
| 167 |
} |
| 168 |
|
| b7f102fc Andy McCall ↶ |
169 |
pub(crate) fn mtime_ns(m: &fs::Metadata) -> u128 { |
| 75167672 Andy McCall |
170 |
m.modified() |
| 171 |
.ok() |
| 172 |
.and_then(|t| t.duration_since(UNIX_EPOCH).ok()) |
| 173 |
.map(|d| d.as_nanos()) |
| 174 |
.unwrap_or(0) |
| b7f102fc Andy McCall ↶ |
175 |
} |
| 176 |
|
| 135212de Andy McCall ↶ |
177 |
pub const DEFAULT_PROMPT_FORMAT: &str = "%b %c%d%x%A%D"; |
| 694805cc Andy McCall ↶ |
178 |
|
| 179 |
pub const ICON: &str = "⚛"; |
| 180 |
|
| 181 |
pub const GLYPH_PROMPT_FORMAT: &str = "%i %b %c %s%U%O"; |
| 135212de Andy McCall ↶ |
182 |
|
| 1966f5db Andy McCall ↶ |
183 |
|
| 184 |
|
| 185 |
|
| f9466021 Andy McCall ↶ |
186 |
|
| 187 |
|
| 188 |
|
| 1ec3f008 Andy McCall ↶ |
189 |
|
| 190 |
|
| f9466021 Andy McCall ↶ |
191 |
|
| 192 |
|
| 1ec3f008 Andy McCall ↶ |
193 |
const MANIFEST_MAGIC: &[u8; 8] = b"oxyman3\0"; |
| 1966f5db Andy McCall ↶ |
194 |
|
| 195 |
const MANIFEST_WRITTEN: std::ops::Range<usize> = 10..26; |
| f9466021 Andy McCall ↶ |
196 |
|
| 197 |
const MANIFEST_SUM: std::ops::Range<usize> = 26..58; |
| 1966f5db Andy McCall ↶ |
198 |
|
| f9466021 Andy McCall ↶ |
199 |
fn manifest_sum(buf: &[u8]) -> [u8; 32] { |
| 200 |
let mut h = blake3::Hasher::new(); |
| 201 |
h.update(&buf[..MANIFEST_SUM.start]); |
| 202 |
h.update_rayon(&buf[MANIFEST_SUM.end..]); |
| 203 |
*h.finalize().as_bytes() |
| 204 |
} |
| 205 |
|
| 1966f5db Andy McCall ↶ |
206 |
fn encode_manifest( |
| 207 |
conflicted: bool, |
| 208 |
uncommitted: bool, |
| 209 |
written_ns: u128, |
| 1ec3f008 Andy McCall ↶ |
210 |
dirs: &[(String, u128)], |
| 211 |
nested: &[String], |
| 1966f5db Andy McCall ↶ |
212 |
entries: &mut dyn Iterator<Item = (&str, ManifestEntry)>, |
| 213 |
) -> Vec<u8> { |
| 214 |
let mut w = crate::flat::Writer::default(); |
| 215 |
w.fixed(MANIFEST_MAGIC); |
| 216 |
w.u8(conflicted as u8); |
| 217 |
w.u8(uncommitted as u8); |
| 218 |
w.fixed(&written_ns.to_le_bytes()); |
| f9466021 Andy McCall ↶ |
219 |
w.fixed(&[0; 32]); |
| 1ec3f008 Andy McCall ↶ |
220 |
w.count(dirs.len()); |
| 221 |
for (d, t) in dirs { |
| 222 |
w.bytes(d.as_bytes()); |
| 223 |
w.fixed(&t.to_le_bytes()); |
| 224 |
} |
| 225 |
w.count(nested.len()); |
| 226 |
for n in nested { |
| 227 |
w.bytes(n.as_bytes()); |
| 228 |
} |
| 1966f5db Andy McCall ↶ |
229 |
for (path, e) in entries { |
| 230 |
w.bytes(path.as_bytes()); |
| 231 |
w.u64(e.size); |
| 232 |
w.fixed(&e.mtime_ns.to_le_bytes()); |
| 233 |
w.fixed(&e.hash); |
| 234 |
w.opt_object(&e.blob); |
| 235 |
match e.graph { |
| 236 |
None => w.u8(0), |
| 237 |
Some(k) => { |
| 238 |
w.u8(1); |
| 239 |
w.fixed(&k); |
| 240 |
} |
| 241 |
} |
| 242 |
w.u8(e.exec as u8); |
| 243 |
} |
| f9466021 Andy McCall ↶ |
244 |
let sum = manifest_sum(&w.0); |
| 245 |
w.0[MANIFEST_SUM].copy_from_slice(&sum); |
| 1966f5db Andy McCall ↶ |
246 |
w.0 |
| 135212de Andy McCall ↶ |
247 |
} |
| 248 |
|
| 1966f5db Andy McCall ↶ |
249 |
fn decode_manifest(buf: &[u8]) -> Option<(bool, bool, Manifest)> { |
| 250 |
let mut r = crate::flat::Reader::new(buf); |
| 251 |
if &r.fixed::<8>().ok()? != MANIFEST_MAGIC { |
| 252 |
return None; |
| 253 |
} |
| 254 |
let conflicted = r.u8().ok()? == 1; |
| 255 |
let uncommitted = r.u8().ok()? == 1; |
| 256 |
let written_ns = u128::from_le_bytes(r.fixed().ok()?); |
| f9466021 Andy McCall ↶ |
257 |
if r.fixed::<32>().ok()? != manifest_sum(buf) { |
| 258 |
return None; |
| 1ec3f008 Andy McCall ↶ |
259 |
} |
| 260 |
let mut dirs = Vec::new(); |
| 261 |
for _ in 0..r.count().ok()? { |
| 262 |
let d = std::str::from_utf8(r.bytes().ok()?).ok()?.to_string(); |
| 263 |
dirs.push((d, u128::from_le_bytes(r.fixed().ok()?))); |
| 264 |
} |
| 265 |
let mut nested = Vec::new(); |
| 266 |
for _ in 0..r.count().ok()? { |
| 267 |
nested.push(std::str::from_utf8(r.bytes().ok()?).ok()?.to_string()); |
| f9466021 Andy McCall ↶ |
268 |
} |
| 1966f5db Andy McCall ↶ |
269 |
let mut entries = Vec::new(); |
| 270 |
while !r.at_end() { |
| 271 |
let path = std::str::from_utf8(r.bytes().ok()?).ok()?.to_string(); |
| 272 |
let e = ManifestEntry { |
| 273 |
size: r.u64().ok()?, |
| 274 |
mtime_ns: u128::from_le_bytes(r.fixed().ok()?), |
| 275 |
hash: r.fixed().ok()?, |
| 276 |
blob: r.opt_object().ok()?, |
| 277 |
graph: match r.u8().ok()? { |
| 278 |
0 => None, |
| 279 |
_ => Some(r.fixed().ok()?), |
| 280 |
}, |
| 281 |
exec: r.u8().ok()? == 1, |
| 282 |
}; |
| 283 |
entries.push((path, e)); |
| 284 |
} |
| 285 |
|
| 286 |
let entries = entries.into_iter().collect(); |
| 287 |
Some(( |
| 288 |
conflicted, |
| 289 |
uncommitted, |
| 290 |
Manifest { |
| 291 |
written_ns, |
| 292 |
entries, |
| 1ec3f008 Andy McCall ↶ |
293 |
dirs, |
| 294 |
nested, |
| 1966f5db Andy McCall ↶ |
295 |
}, |
| 296 |
)) |
| 297 |
} |
| 298 |
|
| e96a430c Andy McCall ↶ |
299 |
|
| 300 |
enum Tracked { |
| 301 |
File(super::walk::Stat), |
| 302 |
Link(Vec<u8>), |
| 303 |
} |
| 304 |
|
| 305 |
|
| 306 |
|
| 307 |
|
| 308 |
|
| 309 |
|
| 310 |
|
| 311 |
fn stat_tracked(root: &Path, rels: &[&String]) -> Vec<Option<Tracked>> { |
| 312 |
use rayon::prelude::*; |
| 313 |
let mut by_dir: HashMap<&str, Vec<(usize, &str)>> = HashMap::new(); |
| 314 |
for (i, rel) in rels.iter().enumerate() { |
| 315 |
let (dir, name) = rel.rsplit_once('/').unwrap_or(("", rel.as_str())); |
| 316 |
by_dir.entry(dir).or_default().push((i, name)); |
| 317 |
} |
| 318 |
let groups: Vec<(&str, Vec<(usize, &str)>)> = by_dir.into_iter().collect(); |
| 319 |
let found: Vec<(usize, Option<Tracked>)> = groups |
| 320 |
.par_iter() |
| 321 |
.flat_map_iter(|(dir, names)| { |
| 322 |
let handle = fs::File::open(root.join(dir)).ok(); |
| 323 |
names |
| 324 |
.iter() |
| 325 |
.map(|(i, name)| { |
| 326 |
let rel = rels[*i]; |
| 327 |
let now = match &handle { |
| 328 |
Some(d) => stat_in(d, name, || root.join(rel)), |
| 329 |
None => None, |
| 330 |
}; |
| 331 |
(*i, now) |
| 332 |
}) |
| 333 |
.collect::<Vec<_>>() |
| 334 |
}) |
| 335 |
.collect(); |
| 336 |
let mut out: Vec<Option<Tracked>> = (0..rels.len()).map(|_| None).collect(); |
| 337 |
for (i, now) in found { |
| 338 |
out[i] = now; |
| 339 |
} |
| 340 |
out |
| 341 |
} |
| 342 |
|
| 343 |
|
| 344 |
|
| 345 |
#[cfg(unix)] |
| 346 |
fn stat_in(dir: &fs::File, name: &str, path: impl Fn() -> PathBuf) -> Option<Tracked> { |
| 347 |
use std::os::fd::AsRawFd; |
| 348 |
let c = std::ffi::CString::new(name).ok()?; |
| 349 |
let mut st: libc::stat = unsafe { std::mem::zeroed() }; |
| 350 |
|
| 351 |
|
| 352 |
let r = unsafe { |
| 353 |
libc::fstatat( |
| 354 |
dir.as_raw_fd(), |
| 355 |
c.as_ptr(), |
| 356 |
&mut st, |
| 357 |
libc::AT_SYMLINK_NOFOLLOW, |
| 358 |
) |
| 359 |
}; |
| 360 |
if r != 0 { |
| 361 |
return None; |
| 362 |
} |
| 363 |
match st.st_mode & libc::S_IFMT { |
| 364 |
libc::S_IFREG => Some(Tracked::File(super::walk::Stat { |
| 365 |
size: st.st_size as u64, |
| 366 |
|
| 367 |
mtime_ns: if st.st_mtime < 0 { |
| 368 |
0 |
| 369 |
} else { |
| 370 |
st.st_mtime as u128 * 1_000_000_000 + st.st_mtime_nsec as u128 |
| 371 |
}, |
| 372 |
exec: st.st_mode & 0o111 != 0, |
| 373 |
})), |
| 374 |
libc::S_IFLNK => { |
| 375 |
let t = fs::read_link(path()).ok()?; |
| 376 |
Some(Tracked::Link(t.to_string_lossy().as_bytes().to_vec())) |
| 377 |
} |
| 378 |
_ => None, |
| 379 |
} |
| 380 |
} |
| 381 |
|
| 382 |
#[cfg(not(unix))] |
| 383 |
fn stat_in(_dir: &fs::File, _name: &str, path: impl Fn() -> PathBuf) -> Option<Tracked> { |
| 384 |
let p = path(); |
| 385 |
let m = fs::symlink_metadata(&p).ok()?; |
| 386 |
if m.file_type().is_symlink() { |
| 387 |
let t = fs::read_link(&p).ok()?; |
| 388 |
Some(Tracked::Link(t.to_string_lossy().as_bytes().to_vec())) |
| 389 |
} else if m.is_file() { |
| 390 |
Some(Tracked::File(super::walk::Stat::of(&m))) |
| 391 |
} else { |
| 392 |
None |
| 393 |
} |
| 394 |
} |
| 395 |
|
| 36041796 Andy McCall ↶ |
396 |
|
| 397 |
|
| 398 |
const STATUS_MAGIC: &[u8; 8] = b"oxystat1"; |
| 399 |
|
| 400 |
fn status_byte(s: ChangeStatus) -> u8 { |
| 401 |
match s { |
| 402 |
ChangeStatus::Added => 0, |
| 403 |
ChangeStatus::Modified => 1, |
| 404 |
ChangeStatus::Removed => 2, |
| 405 |
ChangeStatus::Renamed => 3, |
| 406 |
ChangeStatus::ModeChanged => 4, |
| 407 |
ChangeStatus::ConflictEdited => 5, |
| 408 |
ChangeStatus::Resolved => 6, |
| 409 |
} |
| 410 |
} |
| 411 |
|
| 412 |
fn encode_status_cache(wc: &ChangeHash, stamp: &[u8; 32], statuses: &[PathStatus]) -> Vec<u8> { |
| 413 |
let mut out = Vec::with_capacity(80 + statuses.len() * 40); |
| 414 |
out.extend_from_slice(STATUS_MAGIC); |
| 415 |
out.extend_from_slice(&wc.0); |
| 416 |
out.extend_from_slice(stamp); |
| 417 |
out.extend_from_slice(&(statuses.len() as u32).to_le_bytes()); |
| 418 |
for s in statuses { |
| 419 |
out.push(status_byte(s.status)); |
| 420 |
out.extend_from_slice(&(s.path.len() as u32).to_le_bytes()); |
| 421 |
out.extend_from_slice(s.path.as_bytes()); |
| 422 |
} |
| 423 |
out |
| 424 |
} |
| 425 |
|
| 426 |
fn decode_status_cache(b: &[u8]) -> Option<(ChangeHash, [u8; 32], Vec<PathStatus>)> { |
| 427 |
let rest = b.strip_prefix(STATUS_MAGIC)?; |
| 428 |
let wc = ObjectRef(rest.get(..32)?.try_into().ok()?); |
| 429 |
let stamp: [u8; 32] = rest.get(32..64)?.try_into().ok()?; |
| 430 |
let n = u32::from_le_bytes(rest.get(64..68)?.try_into().ok()?) as usize; |
| 431 |
let mut at = 68; |
| 432 |
let mut out = Vec::with_capacity(n.min(1 << 16)); |
| 433 |
for _ in 0..n { |
| 434 |
let status = match *rest.get(at)? { |
| 435 |
0 => ChangeStatus::Added, |
| 436 |
1 => ChangeStatus::Modified, |
| 437 |
2 => ChangeStatus::Removed, |
| 438 |
3 => ChangeStatus::Renamed, |
| 439 |
4 => ChangeStatus::ModeChanged, |
| 440 |
5 => ChangeStatus::ConflictEdited, |
| 441 |
6 => ChangeStatus::Resolved, |
| 442 |
_ => return None, |
| 443 |
}; |
| 444 |
let len = u32::from_le_bytes(rest.get(at + 1..at + 5)?.try_into().ok()?) as usize; |
| 445 |
let path = std::str::from_utf8(rest.get(at + 5..at + 5 + len)?) |
| 446 |
.ok()? |
| 447 |
.to_string(); |
| 448 |
out.push(PathStatus { path, status }); |
| 449 |
at += 5 + len; |
| 450 |
} |
| 451 |
(at == rest.len()).then_some((wc, stamp, out)) |
| 452 |
} |
| 453 |
|
| 135212de Andy McCall ↶ |
454 |
impl Repo { |
| dfb23a3a Andy McCall ↶ |
455 |
pub(crate) fn manifest_path(&self) -> PathBuf { |
| 75167672 Andy McCall |
456 |
self.oxy |
| 457 |
.join("workspaces") |
| 458 |
.join(self.workspace.to_hex()) |
| 459 |
.join("manifest") |
| 135212de Andy McCall ↶ |
460 |
} |
| 461 |
|
| 462 |
|
| 463 |
|
| 464 |
|
| 465 |
pub(crate) fn write_manifest(&self, conflicted: bool, uncommitted: bool) -> Result<()> { |
| b7f102fc Andy McCall ↶ |
466 |
let entries = self.walk_working_copy()?; |
| 467 |
self.write_manifest_entries(conflicted, uncommitted, &entries, &HashMap::new()) |
| 468 |
} |
| 469 |
|
| 470 |
|
| 471 |
|
| 75167672 Andy McCall |
472 |
pub(crate) fn write_manifest_entries( |
| 473 |
&self, |
| 474 |
conflicted: bool, |
| 475 |
uncommitted: bool, |
| 476 |
entries: &BTreeMap<String, FsEntry>, |
| 477 |
blobs: &HashMap<String, ObjectRef>, |
| 478 |
) -> Result<()> { |
| 236f35c0 Andy McCall ↶ |
479 |
let dirs = self.walked_dirs.take().unwrap_or_default(); |
| 480 |
self.write_manifest_entries_as_of(conflicted, uncommitted, entries, blobs, now_ns(), dirs) |
| 481 |
} |
| 482 |
|
| 483 |
|
| 484 |
|
| 485 |
pub(crate) fn write_manifest_entries_as_of( |
| 486 |
&self, |
| 487 |
conflicted: bool, |
| 488 |
uncommitted: bool, |
| 489 |
entries: &BTreeMap<String, FsEntry>, |
| 490 |
blobs: &HashMap<String, ObjectRef>, |
| 491 |
written: u128, |
| 492 |
(dirs, nested): WalkedDirs, |
| 493 |
) -> Result<()> { |
| d0990c2b Andy McCall ↶ |
494 |
self.manifest_cache.take(); |
| 76740263 Andy McCall ↶ |
495 |
self.listing_cache.take(); |
| dfb23a3a Andy McCall ↶ |
496 |
let mut newest_ns: u128 = 0; |
| 1966f5db Andy McCall ↶ |
497 |
let mut rows = entries.iter().map(|(path, e)| { |
| 0554aaa7 Andy McCall ↶ |
498 |
let (size, mtime_ns) = match e.stat { |
| 499 |
Some(known) => known, |
| 1966f5db Andy McCall ↶ |
500 |
None => match fs::symlink_metadata(self.root.join(path)) { |
| 0554aaa7 Andy McCall ↶ |
501 |
Ok(m) => (m.len(), mtime_ns(&m)), |
| 502 |
Err(_) => (e.content.len() as u64, 0), |
| 503 |
}, |
| 135212de Andy McCall ↶ |
504 |
}; |
| dfb23a3a Andy McCall ↶ |
505 |
newest_ns = newest_ns.max(mtime_ns); |
| 1966f5db Andy McCall ↶ |
506 |
let entry = ManifestEntry { |
| cc7a0056 Andy McCall ↶ |
507 |
size, |
| 508 |
mtime_ns, |
| 1966f5db Andy McCall ↶ |
509 |
hash: e |
| 510 |
.content_hash |
| 511 |
.unwrap_or_else(|| *blake3::hash(&e.content).as_bytes()), |
| 512 |
blob: blobs.get(path).copied().or(e.blob), |
| 513 |
graph: e.graph_key, |
| 514 |
exec: e.exec, |
| 515 |
}; |
| 516 |
(path.as_str(), entry) |
| 517 |
}); |
| 1ec3f008 Andy McCall ↶ |
518 |
let new = encode_manifest(conflicted, uncommitted, written, &dirs, &nested, &mut rows); |
| 1966f5db Andy McCall ↶ |
519 |
drop(rows); |
| 135212de Andy McCall ↶ |
520 |
let p = self.manifest_path(); |
| dfb23a3a Andy McCall ↶ |
521 |
|
| 522 |
|
| 523 |
|
| 524 |
|
| 1966f5db Andy McCall ↶ |
525 |
if let Ok(old) = fs::read(&p) { |
| f9466021 Andy McCall ↶ |
526 |
let (w, sum) = (MANIFEST_WRITTEN, MANIFEST_SUM); |
| 1966f5db Andy McCall ↶ |
527 |
if old.len() == new.len() |
| 528 |
&& old[..w.start] == new[..w.start] |
| f9466021 Andy McCall ↶ |
529 |
&& old[sum.end..] == new[sum.end..] |
| 1966f5db Andy McCall ↶ |
530 |
{ |
| 531 |
let old_written = u128::from_le_bytes(old[w.clone()].try_into().unwrap()); |
| 532 |
if newest_ns + 2_000_000_000 < old_written { |
| dfb23a3a Andy McCall ↶ |
533 |
return Ok(()); |
| 534 |
} |
| 535 |
} |
| 536 |
} |
| 135212de Andy McCall ↶ |
537 |
fs::create_dir_all(p.parent().unwrap())?; |
| f9466021 Andy McCall ↶ |
538 |
write_cache(&p, &new) |
| 135212de Andy McCall ↶ |
539 |
} |
| 540 |
|
| 86e1824f Andy McCall ↶ |
541 |
|
| 542 |
|
| 543 |
|
| 544 |
pub(crate) fn rewrite_manifest( |
| 545 |
&self, |
| 546 |
conflicted: bool, |
| 547 |
uncommitted: bool, |
| 1966f5db Andy McCall ↶ |
548 |
keys: &HashMap<String, [u8; 32]>, |
| 86e1824f Andy McCall ↶ |
549 |
) -> Result<()> { |
| d0990c2b Andy McCall ↶ |
550 |
self.manifest_cache.take(); |
| 76740263 Andy McCall ↶ |
551 |
self.listing_cache.take(); |
| b7f102fc Andy McCall ↶ |
552 |
let p = self.manifest_path(); |
| 1966f5db Andy McCall ↶ |
553 |
let Some((_, _, m)) = fs::read(&p).ok().and_then(|b| decode_manifest(&b)) else { |
| 554 |
return self.write_manifest(conflicted, uncommitted); |
| b7f102fc Andy McCall ↶ |
555 |
}; |
| 1966f5db Andy McCall ↶ |
556 |
let mut rows = m.entries.iter().map(|(path, e)| { |
| 557 |
let mut e = e.clone(); |
| 558 |
if let Some(k) = keys.get(path) { |
| 559 |
e.graph = Some(*k); |
| b7f102fc Andy McCall ↶ |
560 |
} |
| 1966f5db Andy McCall ↶ |
561 |
(path.as_str(), e) |
| 562 |
}); |
| f9466021 Andy McCall ↶ |
563 |
write_cache( |
| 1966f5db Andy McCall ↶ |
564 |
&p, |
| 1ec3f008 Andy McCall ↶ |
565 |
&encode_manifest( |
| 566 |
conflicted, |
| 567 |
uncommitted, |
| 568 |
m.written_ns, |
| 569 |
&m.dirs, |
| 570 |
&m.nested, |
| 571 |
&mut rows, |
| 572 |
), |
| 1966f5db Andy McCall ↶ |
573 |
) |
| b7f102fc Andy McCall ↶ |
574 |
} |
| 575 |
|
| 135212de Andy McCall ↶ |
576 |
|
| 577 |
|
| 578 |
|
| 579 |
pub(crate) fn drop_manifest(&self) { |
| d0990c2b Andy McCall ↶ |
580 |
self.manifest_cache.take(); |
| 76740263 Andy McCall ↶ |
581 |
self.listing_cache.take(); |
| 135212de Andy McCall ↶ |
582 |
let _ = fs::remove_file(self.manifest_path()); |
| d0990c2b Andy McCall ↶ |
583 |
} |
| 584 |
|
| 585 |
|
| 586 |
|
| 33472ccc Andy McCall ↶ |
587 |
|
| 588 |
|
| 589 |
pub fn recorded_conflicted(&self) -> Result<bool> { |
| 590 |
Ok(self.read_manifest()?.is_some_and(|(c, _, _)| c)) |
| 591 |
} |
| 592 |
|
| d0990c2b Andy McCall ↶ |
593 |
pub(crate) fn read_manifest(&self) -> Result<Option<(bool, bool, std::sync::Arc<Manifest>)>> { |
| 594 |
let path = self.manifest_path(); |
| 595 |
let id = match fs::metadata(&path) { |
| 596 |
Ok(m) => { |
| 597 |
#[cfg(unix)] |
| 598 |
let ino = std::os::unix::fs::MetadataExt::ino(&m); |
| 599 |
#[cfg(not(unix))] |
| 600 |
let ino = 0; |
| 601 |
(m.len(), mtime_ns(&m), ino) |
| 602 |
} |
| 603 |
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), |
| 604 |
Err(e) => return Err(e.into()), |
| 605 |
}; |
| 606 |
if let Some((cached_id, m)) = &*self.manifest_cache.borrow() { |
| 607 |
if *cached_id == id { |
| 608 |
return Ok(Some(m.clone())); |
| 609 |
} |
| 610 |
} |
| 611 |
let Some((c, u, m)) = self.parse_manifest(&path)? else { |
| 612 |
return Ok(None); |
| 613 |
}; |
| 614 |
let m = (c, u, std::sync::Arc::new(m)); |
| 615 |
*self.manifest_cache.borrow_mut() = Some((id, m.clone())); |
| 616 |
Ok(Some(m)) |
| 135212de Andy McCall ↶ |
617 |
} |
| 618 |
|
| d0990c2b Andy McCall ↶ |
619 |
fn parse_manifest(&self, path: &std::path::Path) -> Result<Option<(bool, bool, Manifest)>> { |
| 1966f5db Andy McCall ↶ |
620 |
let buf = match fs::read(path) { |
| 621 |
Ok(b) => b, |
| 135212de Andy McCall ↶ |
622 |
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), |
| 623 |
Err(e) => return Err(e.into()), |
| 624 |
}; |
| 1966f5db Andy McCall ↶ |
625 |
Ok(decode_manifest(&buf)) |
| 135212de Andy McCall ↶ |
626 |
} |
| 627 |
|
| da868d5d Andy McCall ↶ |
628 |
|
| 629 |
|
| d3cd7ef9 Andy McCall ↶ |
630 |
|
| 631 |
|
| 632 |
|
| 633 |
|
| 634 |
|
| da868d5d Andy McCall ↶ |
635 |
pub fn clean_by_manifest(&self) -> Result<bool> { |
| 636 |
let _t = crate::timing::phase("status: check the manifest"); |
| f9466021 Andy McCall ↶ |
637 |
|
| 638 |
let not = |why: &str| { |
| 639 |
if crate::timing::enabled() { |
| 640 |
eprintln!("timing status: not clean by the manifest: {why}"); |
| 641 |
} |
| 642 |
Ok(false) |
| 643 |
}; |
| da868d5d Andy McCall ↶ |
644 |
let Some((conflicted, uncommitted, m)) = self.read_manifest()? else { |
| f9466021 Andy McCall ↶ |
645 |
return not("no manifest"); |
| da868d5d Andy McCall ↶ |
646 |
}; |
| 647 |
if conflicted || uncommitted || self.root.join(".oxymodules").exists() { |
| f9466021 Andy McCall ↶ |
648 |
return not("conflicted, uncommitted or subrepos"); |
| da868d5d Andy McCall ↶ |
649 |
} |
| 0389c3e8 Andy McCall ↶ |
650 |
let started = now_ns(); |
| 36041796 Andy McCall ↶ |
651 |
if self.disk_differs(&m, true)? { |
| 652 |
return not("a file differs"); |
| 0389c3e8 Andy McCall ↶ |
653 |
} |
| 654 |
|
| 655 |
|
| 656 |
|
| 657 |
|
| 658 |
|
| 659 |
|
| 660 |
|
| 661 |
let window = 2_000_000_000; |
| 662 |
let listed = self.listed_dirs.take(); |
| 663 |
let racy = m.dirs.iter().any(|(_, t)| t + window >= m.written_ns) |
| 664 |
|| m.entries |
| 665 |
.values() |
| 666 |
.any(|e| e.mtime_ns + window >= m.written_ns); |
| 667 |
if (racy || (m.dirs.is_empty() && listed.is_some())) && started > m.written_ns { |
| 668 |
let (dirs, nested) = listed.unwrap_or_else(|| (m.dirs.clone(), m.nested.clone())); |
| 669 |
self.restamp_manifest(&m, conflicted, uncommitted, started, &dirs, &nested); |
| 36041796 Andy McCall ↶ |
670 |
} |
| 671 |
Ok(true) |
| 0389c3e8 Andy McCall ↶ |
672 |
} |
| 673 |
|
| 674 |
|
| 675 |
|
| 676 |
fn restamp_manifest( |
| 677 |
&self, |
| 678 |
m: &Manifest, |
| 679 |
conflicted: bool, |
| 680 |
uncommitted: bool, |
| 681 |
written_ns: u128, |
| 682 |
dirs: &[(String, u128)], |
| 683 |
nested: &[String], |
| 684 |
) { |
| 685 |
let _t = crate::timing::phase("status: restamp the manifest"); |
| 686 |
let mut rows = m.entries.iter().map(|(p, e)| (p.as_str(), e.clone())); |
| 687 |
let bytes = encode_manifest(conflicted, uncommitted, written_ns, dirs, nested, &mut rows); |
| 688 |
self.manifest_cache.take(); |
| 689 |
let _ = write_cache(&self.manifest_path(), &bytes); |
| 36041796 Andy McCall ↶ |
690 |
} |
| 691 |
|
| 692 |
|
| 693 |
|
| 694 |
|
| 695 |
fn racy_change(&self, m: &Manifest) -> Option<String> { |
| 638fb214 Andy McCall ↶ |
696 |
use rayon::prelude::*; |
| 697 |
let racy: Vec<(&String, &ManifestEntry)> = m |
| 698 |
.entries |
| 699 |
.iter() |
| 700 |
.filter(|(_, e)| e.mtime_ns + 2_000_000_000 >= m.written_ns) |
| 701 |
.collect(); |
| 702 |
let root = &self.root; |
| 36041796 Andy McCall ↶ |
703 |
racy.par_iter() |
| 704 |
.find_any(|(rel, e)| { |
| 705 |
let path = root.join(rel.as_str()); |
| 706 |
let same = match fs::symlink_metadata(&path) { |
| 707 |
Ok(md) if md.file_type().is_symlink() => fs::read_link(&path).is_ok_and(|t| { |
| 708 |
*blake3::hash(t.to_string_lossy().as_bytes()).as_bytes() == e.hash |
| 709 |
}), |
| 710 |
Ok(md) if md.is_file() => { |
| 711 |
md.len() == e.size |
| 712 |
&& fs::read(&path).is_ok_and(|b| *blake3::hash(&b).as_bytes() == e.hash) |
| 713 |
} |
| 80677b7a Andy McCall ↶ |
714 |
|
| 715 |
|
| 716 |
Ok(md) if md.is_dir() => { |
| 717 |
path.join(OXY_DIR).exists() || path.join(".git").exists() |
| 718 |
} |
| 36041796 Andy McCall ↶ |
719 |
_ => false, |
| 720 |
}; |
| 721 |
!same |
| 722 |
}) |
| 723 |
.map(|(rel, _)| rel.to_string()) |
| 724 |
} |
| 725 |
|
| 726 |
|
| 727 |
|
| 728 |
|
| 729 |
|
| 730 |
|
| 731 |
|
| 732 |
pub fn status_by_manifest(&self) -> Result<Option<Vec<PathStatus>>> { |
| 733 |
|
| 734 |
|
| 735 |
self.listing_cache.take(); |
| 736 |
let Some((conflicted, uncommitted, m)) = self.read_manifest()? else { |
| 737 |
return Ok(None); |
| 738 |
}; |
| 739 |
if !uncommitted { |
| 740 |
return Ok(self.clean_by_manifest()?.then(Vec::new)); |
| f9466021 Andy McCall ↶ |
741 |
} |
| 36041796 Andy McCall ↶ |
742 |
let _t = crate::timing::phase("status: by the status cache"); |
| 743 |
if conflicted || self.root.join(".oxymodules").exists() { |
| 744 |
return Ok(None); |
| 745 |
} |
| 746 |
let Some((wc, stamp, statuses)) = fs::read(self.status_cache_path()) |
| 747 |
.ok() |
| 748 |
.and_then(|b| decode_status_cache(&b)) |
| 749 |
else { |
| 750 |
return Ok(None); |
| 751 |
}; |
| 752 |
let now = self.view()?.workspaces.get(&self.workspace).copied(); |
| 753 |
if now != Some(wc) || self.manifest_stamp() != Some(stamp) { |
| 754 |
return Ok(None); |
| 755 |
} |
| 1ec3f008 Andy McCall ↶ |
756 |
if self.disk_differs(&m, true)? { |
| 36041796 Andy McCall ↶ |
757 |
return Ok(None); |
| 758 |
} |
| 759 |
|
| 760 |
#[cfg(test)] |
| 761 |
{ |
| 762 |
let snap = self.snapshot_status("status cache check")?; |
| 763 |
let key = |v: &[PathStatus]| { |
| 764 |
let mut k: Vec<(String, u8)> = v |
| 765 |
.iter() |
| 766 |
.map(|p| (p.path.clone(), status_byte(p.status))) |
| 767 |
.collect(); |
| 768 |
k.sort(); |
| 769 |
k |
| 770 |
}; |
| 771 |
assert_eq!( |
| 772 |
key(&statuses), |
| 773 |
key(&snap.statuses), |
| 774 |
"status cache disagrees with a snapshot" |
| 775 |
); |
| da868d5d Andy McCall ↶ |
776 |
} |
| 36041796 Andy McCall ↶ |
777 |
Ok(Some(statuses)) |
| 778 |
} |
| 779 |
|
| 780 |
fn status_cache_path(&self) -> PathBuf { |
| 781 |
self.manifest_path().with_file_name("status") |
| 782 |
} |
| 783 |
|
| 784 |
|
| 785 |
fn manifest_stamp(&self) -> Option<[u8; 32]> { |
| 786 |
use std::io::Read; |
| 787 |
let mut head = [0u8; MANIFEST_SUM.end]; |
| 788 |
fs::File::open(self.manifest_path()) |
| 789 |
.and_then(|mut f| f.read_exact(&mut head)) |
| 790 |
.ok()?; |
| 791 |
head[MANIFEST_SUM].try_into().ok() |
| da868d5d Andy McCall ↶ |
792 |
} |
| 793 |
|
| 36041796 Andy McCall ↶ |
794 |
|
| 795 |
|
| 796 |
|
| 797 |
pub(crate) fn write_status_cache(&self, wc: Option<&ChangeHash>, statuses: &[PathStatus]) { |
| 798 |
let path = self.status_cache_path(); |
| 799 |
let (Some(wc), Some(stamp)) = (wc, self.manifest_stamp()) else { |
| 800 |
let _ = fs::remove_file(&path); |
| 801 |
return; |
| 802 |
}; |
| 803 |
let _ = write_cache(&path, &encode_status_cache(wc, &stamp, statuses)); |
| 804 |
} |
| 805 |
|
| 135212de Andy McCall ↶ |
806 |
|
| 807 |
|
| 808 |
pub fn quick_dirty(&self) -> Result<Option<bool>> { |
| 76740263 Andy McCall ↶ |
809 |
self.quick_dirty_keeping(false) |
| 810 |
} |
| 811 |
|
| 812 |
|
| 813 |
|
| 814 |
fn quick_dirty_keeping(&self, keep: bool) -> Result<Option<bool>> { |
| 75167672 Andy McCall |
815 |
let Some((_, uncommitted, manifest)) = self.read_manifest()? else { |
| 816 |
return Ok(None); |
| 817 |
}; |
| 135212de Andy McCall ↶ |
818 |
if uncommitted { |
| 819 |
return Ok(Some(true)); |
| 820 |
} |
| 36041796 Andy McCall ↶ |
821 |
Ok(Some(self.disk_differs(&manifest, keep)?)) |
| 822 |
} |
| 823 |
|
| 824 |
|
| 825 |
|
| 826 |
|
| 827 |
fn disk_differs(&self, manifest: &Manifest, keep: bool) -> Result<bool> { |
| 1ec3f008 Andy McCall ↶ |
828 |
|
| e226a6cc Andy McCall ↶ |
829 |
|
| 830 |
|
| 831 |
|
| 832 |
let racy = self.racy_change(manifest).is_some(); |
| 833 |
if let Some(differs) = self.disk_differs_by_dirs_keeping(manifest, keep, racy) { |
| 834 |
return Ok(differs || racy); |
| 835 |
} |
| 836 |
if racy { |
| 1ec3f008 Andy McCall ↶ |
837 |
return Ok(true); |
| 838 |
} |
| 839 |
self.disk_differs_listing(manifest, keep) |
| 840 |
} |
| 841 |
|
| 80677b7a Andy McCall ↶ |
842 |
|
| 843 |
|
| 844 |
|
| 845 |
|
| 846 |
fn subrepo_entries<'m>(&self, manifest: &'m Manifest) -> Vec<(&'m String, &'m ManifestEntry)> { |
| 847 |
let modules = self.modules_on_disk(); |
| 848 |
let mut out: Vec<(&'m String, &'m ManifestEntry)> = manifest |
| 849 |
.nested |
| 850 |
.iter() |
| 851 |
.map(String::as_str) |
| 852 |
.chain(modules.entries.keys().map(String::as_str)) |
| 853 |
.filter_map(|n| manifest.entries.get_key_value(n)) |
| 854 |
.collect(); |
| 855 |
out.sort_by(|a, b| a.0.cmp(b.0)); |
| 856 |
out.dedup_by(|a, b| a.0 == b.0); |
| 857 |
out |
| 858 |
} |
| 859 |
|
| 860 |
|
| 861 |
|
| 862 |
|
| 863 |
|
| 864 |
fn subrepo_moved(&self, rel: &str, me: &ManifestEntry) -> bool { |
| 865 |
let dir = self.root.join(rel); |
| 866 |
if !(dir.join(OXY_DIR).exists() || dir.join(".git").exists()) { |
| 867 |
return false; |
| 868 |
} |
| 869 |
let modules = self.modules_on_disk(); |
| 870 |
let hint = modules.entries.get(rel).and_then(|m| m.bookmark.clone()); |
| 871 |
match super::subrepos::read_pin(&dir, hint.as_deref()) { |
| 872 |
|
| 873 |
|
| 874 |
Ok(pin) => match me.blob { |
| 875 |
Some(b) => Repo::blob_ref(&pin.encode()) != b, |
| 876 |
None => *blake3::hash(&pin.encode()).as_bytes() != me.hash, |
| 877 |
}, |
| 878 |
Err(_) => true, |
| 879 |
} |
| 880 |
} |
| 881 |
|
| 1ec3f008 Andy McCall ↶ |
882 |
|
| 883 |
|
| 884 |
|
| 885 |
|
| 886 |
|
| 887 |
|
| e226a6cc Andy McCall ↶ |
888 |
#[cfg(test)] |
| 1ec3f008 Andy McCall ↶ |
889 |
pub(crate) fn disk_differs_by_dirs(&self, manifest: &Manifest) -> Option<bool> { |
| e226a6cc Andy McCall ↶ |
890 |
self.disk_differs_by_dirs_keeping(manifest, false, false) |
| 891 |
} |
| 892 |
|
| 893 |
|
| 894 |
|
| 895 |
|
| 896 |
|
| 897 |
fn disk_differs_by_dirs_keeping( |
| 898 |
&self, |
| 899 |
manifest: &Manifest, |
| 900 |
keep: bool, |
| 901 |
racy: bool, |
| 902 |
) -> Option<bool> { |
| 1ec3f008 Andy McCall ↶ |
903 |
use rayon::prelude::*; |
| 904 |
if manifest.dirs.is_empty() || !self.config.sparse_paths.is_empty() { |
| 905 |
return None; |
| 906 |
} |
| 907 |
let _t = crate::timing::phase("status: directories and files by stat"); |
| 908 |
let root = &self.root; |
| 909 |
let dirs_same = manifest.dirs.par_iter().all(|(rel, t)| { |
| 910 |
*t + 2_000_000_000 < manifest.written_ns |
| 911 |
&& fs::metadata(root.join(rel)).is_ok_and(|m| m.is_dir() && mtime_ns(&m) == *t) |
| 912 |
}); |
| 913 |
if !dirs_same { |
| 914 |
return None; |
| 915 |
} |
| 80677b7a Andy McCall ↶ |
916 |
|
| 917 |
let subrepos = self.subrepo_entries(manifest); |
| 918 |
let is_subrepo = |rel: &str| subrepos.iter().any(|(r, _)| r.as_str() == rel); |
| 919 |
let tracked: Vec<(&String, &ManifestEntry)> = manifest |
| 920 |
.entries |
| 921 |
.iter() |
| 922 |
.filter(|(rel, _)| !is_subrepo(rel)) |
| 923 |
.collect(); |
| 924 |
let rels: Vec<&String> = tracked.iter().map(|(r, _)| *r).collect(); |
| e96a430c Andy McCall ↶ |
925 |
let now = stat_tracked(root, &rels); |
| 926 |
let differs_from = |me: &ManifestEntry, rel: &str, now: &Tracked| match now { |
| 927 |
Tracked::Link(t) => *blake3::hash(t).as_bytes() != me.hash, |
| 928 |
Tracked::File(m) => { |
| 929 |
m.size != me.size |
| 930 |
|| m.exec != me.exec |
| 931 |
|| (m.mtime_ns != me.mtime_ns |
| 932 |
&& !fs::read(root.join(rel)) |
| 933 |
.is_ok_and(|b| *blake3::hash(&b).as_bytes() == me.hash)) |
| e226a6cc Andy McCall ↶ |
934 |
} |
| 935 |
}; |
| e96a430c Andy McCall ↶ |
936 |
|
| 937 |
|
| 938 |
if now.iter().any(Option::is_none) { |
| 939 |
return None; |
| 940 |
} |
| 941 |
|
| 80677b7a Andy McCall ↶ |
942 |
let entries: Vec<&ManifestEntry> = tracked.iter().map(|(_, me)| *me).collect(); |
| e96a430c Andy McCall ↶ |
943 |
let differs = rels |
| 944 |
.par_iter() |
| 945 |
.zip(entries.par_iter()) |
| 946 |
.zip(now.par_iter()) |
| 80677b7a Andy McCall ↶ |
947 |
.any(|((rel, me), n)| differs_from(me, rel, n.as_ref().unwrap())) |
| 948 |
|| subrepos.iter().any(|(rel, me)| self.subrepo_moved(rel, me)); |
| e96a430c Andy McCall ↶ |
949 |
if keep && (differs || racy) { |
| 950 |
let mut listing = super::walk::Listing { |
| 951 |
dirs: manifest.dirs.clone(), |
| 952 |
nested: manifest.nested.iter().map(|n| root.join(n)).collect(), |
| 953 |
..Default::default() |
| 954 |
}; |
| 955 |
for (rel, n) in rels.into_iter().zip(now) { |
| 956 |
match n.unwrap() { |
| 957 |
Tracked::File(m) => listing.files.push((rel.clone(), root.join(rel), m)), |
| 958 |
Tracked::Link(t) => listing.symlinks.push((rel.clone(), t)), |
| e226a6cc Andy McCall ↶ |
959 |
} |
| 960 |
} |
| e96a430c Andy McCall ↶ |
961 |
*self.listing_cache.borrow_mut() = Some(listing); |
| 962 |
} |
| 1ec3f008 Andy McCall ↶ |
963 |
if !differs { |
| 964 |
|
| 965 |
|
| 966 |
*self.untracked_nested.borrow_mut() = manifest |
| 967 |
.nested |
| 968 |
.iter() |
| 969 |
.filter(|rel| !manifest.entries.contains_key(*rel)) |
| 970 |
.cloned() |
| 971 |
.collect(); |
| 972 |
} |
| 973 |
Some(differs) |
| 974 |
} |
| 975 |
|
| 976 |
|
| 977 |
fn disk_differs_listing(&self, manifest: &Manifest, keep: bool) -> Result<bool> { |
| 76740263 Andy McCall ↶ |
978 |
use rayon::prelude::*; |
| 979 |
let sparse = &self.config.sparse_paths; |
| eaf3e906 Andy McCall ↶ |
980 |
let in_scope = |p: &str| { |
| 981 |
sparse.is_empty() |
| 982 |
|| sparse |
| 983 |
.iter() |
| 984 |
.any(|s| p == s || p.starts_with(&format!("{s}/"))) |
| 76740263 Andy McCall ↶ |
985 |
}; |
| 986 |
|
| d4dbab97 Andy McCall ↶ |
987 |
|
| 988 |
|
| 989 |
|
| 990 |
let known = |p: &str| manifest.entries.contains_key(p); |
| 991 |
let Ok(listing) = super::walk::list_knowing(&self.root, &in_scope, &known) else { |
| 36041796 Andy McCall ↶ |
992 |
return Ok(true); |
| eaf3e906 Andy McCall ↶ |
993 |
}; |
| 76740263 Andy McCall ↶ |
994 |
let differs = || -> Result<bool> { |
| 995 |
let links_differ = listing.symlinks.iter().any(|(rel, target)| { |
| 996 |
manifest |
| 997 |
.entries |
| 998 |
.get(rel) |
| 1966f5db Andy McCall ↶ |
999 |
.is_none_or(|me| *blake3::hash(target).as_bytes() != me.hash) |
| 76740263 Andy McCall ↶ |
1000 |
}); |
| 1001 |
if links_differ { |
| 1002 |
return Ok(true); |
| 1003 |
} |
| 1004 |
|
| 1005 |
|
| 1006 |
let files_differ = listing.files.par_iter().any(|(rel, path, m)| { |
| 1007 |
manifest.entries.get(rel).is_none_or(|me| { |
| e96a430c Andy McCall ↶ |
1008 |
m.size != me.size |
| 1009 |
|| m.exec != me.exec |
| 1010 |
|| (m.mtime_ns != me.mtime_ns |
| 76740263 Andy McCall ↶ |
1011 |
&& !fs::read(path) |
| 1966f5db Andy McCall ↶ |
1012 |
.is_ok_and(|b| *blake3::hash(&b).as_bytes() == me.hash)) |
| 76740263 Andy McCall ↶ |
1013 |
}) |
| 1014 |
}); |
| 1015 |
if files_differ { |
| 1016 |
return Ok(true); |
| 1017 |
} |
| 1018 |
let seen = listing.files.len() + listing.symlinks.len(); |
| 1019 |
if seen == manifest.entries.len() { |
| 1020 |
return Ok(false); |
| 1021 |
} |
| d4dbab97 Andy McCall ↶ |
1022 |
|
| 1023 |
|
| 1024 |
|
| 1025 |
|
| 1026 |
let mut listed: Vec<&str> = listing |
| 1027 |
.files |
| 1028 |
.iter() |
| 1029 |
.map(|(r, _, _)| r.as_str()) |
| 1030 |
.chain(listing.symlinks.iter().map(|(r, _)| r.as_str())) |
| 1031 |
.collect(); |
| 1032 |
listed.sort_unstable(); |
| 1033 |
let mut at = listed.iter().peekable(); |
| 1034 |
let skipped: Vec<(&String, &ManifestEntry)> = manifest |
| 1035 |
.entries |
| 1036 |
.iter() |
| 1037 |
.filter(|(rel, _)| { |
| 1038 |
while at.next_if(|l| **l < rel.as_str()).is_some() {} |
| 1039 |
at.peek().is_none_or(|l| **l != rel.as_str()) |
| 1040 |
}) |
| 1041 |
.collect(); |
| 1042 |
let root = &self.root; |
| 80677b7a Andy McCall ↶ |
1043 |
|
| 1044 |
let subrepos = self.subrepo_entries(manifest); |
| 1045 |
if subrepos.iter().any(|(rel, me)| self.subrepo_moved(rel, me)) { |
| 1046 |
return Ok(true); |
| 1047 |
} |
| 1048 |
let skipped: Vec<_> = skipped |
| 1049 |
.into_iter() |
| 1050 |
.filter(|(rel, _)| !subrepos.iter().any(|(r, _)| r == rel)) |
| 1051 |
.collect(); |
| d4dbab97 Andy McCall ↶ |
1052 |
Ok(skipped.par_iter().any(|(rel, me)| { |
| 1053 |
let path = root.join(rel.as_str()); |
| 76740263 Andy McCall ↶ |
1054 |
let Ok(m) = fs::symlink_metadata(&path) else { |
| d4dbab97 Andy McCall ↶ |
1055 |
return true; |
| eaf3e906 Andy McCall ↶ |
1056 |
}; |
| d4dbab97 Andy McCall ↶ |
1057 |
!m.file_type().is_symlink() |
| 1058 |
&& (m.len() != me.size |
| 1059 |
|| super::is_executable(&m) != me.exec |
| 1060 |
|| (mtime_ns(&m) != me.mtime_ns |
| 1061 |
&& !fs::read(&path) |
| 1062 |
.is_ok_and(|b| *blake3::hash(&b).as_bytes() == me.hash))) |
| 1063 |
})) |
| 76740263 Andy McCall ↶ |
1064 |
}; |
| 1065 |
let dirty = differs()?; |
| d4dbab97 Andy McCall ↶ |
1066 |
if !dirty { |
| 1067 |
|
| 1068 |
|
| 69692f14 Andy McCall ↶ |
1069 |
let mut nested: Vec<String> = listing |
| d4dbab97 Andy McCall ↶ |
1070 |
.nested |
| 1071 |
.iter() |
| 1072 |
.map(|d| self.rel_path(d)) |
| 1073 |
.filter(|rel| !manifest.entries.contains_key(rel)) |
| 1074 |
.collect(); |
| 69692f14 Andy McCall ↶ |
1075 |
nested.sort(); |
| 1076 |
*self.untracked_nested.borrow_mut() = nested; |
| 0389c3e8 Andy McCall ↶ |
1077 |
|
| 1078 |
|
| 1079 |
*self.listed_dirs.borrow_mut() = Some(( |
| 1080 |
listing.dirs.clone(), |
| 1081 |
listing.nested.iter().map(|d| self.rel_path(d)).collect(), |
| 1082 |
)); |
| d4dbab97 Andy McCall ↶ |
1083 |
} |
| 76740263 Andy McCall ↶ |
1084 |
if dirty && keep { |
| 1085 |
*self.listing_cache.borrow_mut() = Some(listing); |
| 9ac1fbe2 Andy McCall ↶ |
1086 |
} |
| 36041796 Andy McCall ↶ |
1087 |
Ok(dirty) |
| 135212de Andy McCall ↶ |
1088 |
} |
| 1089 |
|
| 1090 |
|
| 1091 |
pub fn quick_status(&self) -> Result<PromptInfo> { |
| 1092 |
let view = self.view()?; |
| 1093 |
let bookmark = self.config.bookmark.clone(); |
| 1094 |
let heads = self.heads_of(&view, &bookmark)?; |
| 1095 |
let wc_hash = view.workspaces.get(&self.workspace).copied(); |
| 1096 |
let change_id = match wc_hash { |
| 69816d68 Andy McCall ↶ |
1097 |
Some(h) => self.store.get_change_meta(&h)?.change_id.short(8), |
| 135212de Andy McCall ↶ |
1098 |
None => String::new(), |
| 1099 |
}; |
| 1100 |
|
| 1101 |
|
| 1102 |
let manifest = self.read_manifest()?; |
| 1103 |
let conflicted = manifest.as_ref().map(|(c, _, _)| *c).unwrap_or(false); |
| 1104 |
let dirty = self.quick_dirty()?.unwrap_or(false); |
| b35df36b Andy McCall ↶ |
1105 |
let (ahead, behind) = match view |
| 1106 |
.remotes |
| 1107 |
.get(&self.default_remote()) |
| 1108 |
.and_then(|b| b.get(&bookmark)) |
| 1109 |
{ |
| 135212de Andy McCall ↶ |
1110 |
Some(st) => { |
| 1111 |
let remote_heads = self.store.get_state(st)?.heads; |
| 1112 |
let ahead = self.negotiate(&remote_heads, &heads).map(|v| v.len()).ok(); |
| 1113 |
let behind = self.negotiate(&heads, &remote_heads).map(|v| v.len()).ok(); |
| 1114 |
(ahead, behind) |
| 1115 |
} |
| 1116 |
None => (None, None), |
| 1117 |
}; |
| 75167672 Andy McCall |
1118 |
Ok(PromptInfo { |
| 1119 |
bookmark, |
| 1120 |
change_id, |
| 1121 |
heads: heads.len(), |
| 1122 |
dirty, |
| 1123 |
conflicted, |
| 1124 |
ahead, |
| 1125 |
behind, |
| 1126 |
}) |
| 135212de Andy McCall ↶ |
1127 |
} |
| 1128 |
} |
| 694805cc Andy McCall ↶ |
1129 |
|
| 1130 |
#[cfg(test)] |
| 1131 |
mod tests { |
| 1132 |
use super::*; |
| 1133 |
|
| 1134 |
#[test] |
| 1135 |
fn placeholders_and_glyphs() { |
| 75167672 Andy McCall |
1136 |
let clean = PromptInfo { |
| 1137 |
bookmark: "main".into(), |
| 1138 |
change_id: "658990aa".into(), |
| 1139 |
heads: 1, |
| 1140 |
dirty: false, |
| 1141 |
conflicted: false, |
| 1142 |
ahead: Some(0), |
| 1143 |
behind: None, |
| 1144 |
}; |
| 694805cc Andy McCall ↶ |
1145 |
assert_eq!(clean.format(DEFAULT_PROMPT_FORMAT), "main 658990aa"); |
| 1146 |
assert_eq!(clean.format(GLYPH_PROMPT_FORMAT), "⚛ main 658990aa ✔"); |
| 75167672 Andy McCall |
1147 |
let busy = PromptInfo { |
| 1148 |
dirty: true, |
| 1149 |
ahead: Some(2), |
| 1150 |
behind: Some(3), |
| 1151 |
..clean.clone() |
| 1152 |
}; |
| 694805cc Andy McCall ↶ |
1153 |
assert_eq!(busy.format(DEFAULT_PROMPT_FORMAT), "main 658990aa*+2-3"); |
| 1154 |
assert_eq!(busy.format(GLYPH_PROMPT_FORMAT), "⚛ main 658990aa ✎↑2↓3"); |
| 75167672 Andy McCall |
1155 |
let bad = PromptInfo { |
| 1156 |
conflicted: true, |
| 1157 |
..busy.clone() |
| 1158 |
}; |
| 694805cc Andy McCall ↶ |
1159 |
assert_eq!(bad.format("%s%x%d %h %a/%B %%"), "‼!* 1 2/3 %"); |
| 1160 |
assert_eq!(bad.state_color(), 31); |
| 75167672 Andy McCall |
1161 |
assert_eq!( |
| 1162 |
busy.format_colored("%b", ("\\[", "\\]")), |
| 1163 |
"\\[\x1b[33m\\]main\\[\x1b[0m\\]" |
| 1164 |
); |
| 694805cc Andy McCall ↶ |
1165 |
assert_eq!(clean.format_colored("%b", ("", "")), "\x1b[36mmain\x1b[0m"); |
| 1166 |
assert_eq!(clean.format("%q"), "%q"); |
| 1167 |
} |
| 1168 |
} |