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