| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
use super::cache::{Loaded, StateCache}; |
| 7 |
use super::view::{self, ChangeView, ConflictView, RefTarget}; |
| 8 |
use super::RepoError; |
| 9 |
use oxy_core::hash::{ChangeHash, KeyId, ObjectRef}; |
| 10 |
use oxy_core::objects::StateObject; |
| 11 |
use oxy_core::repo::{FileDiff, Repo}; |
| 12 |
use oxy_core::state::FileContent; |
| 13 |
use std::collections::{BTreeMap, BTreeSet, HashMap}; |
| 14 |
use std::sync::Arc; |
| 15 |
|
| 16 |
type Result<T> = std::result::Result<T, RepoError>; |
| 17 |
|
| 18 |
|
| 19 |
pub enum Source<'a> { |
| 20 |
|
| 21 |
Bookmark(Vec<ChangeHash>), |
| 22 |
|
| 23 |
Fork { |
| 24 |
fork: &'a Repo, |
| 25 |
heads: Vec<ChangeHash>, |
| 26 |
}, |
| 27 |
Changes(Vec<ChangeHash>), |
| 28 |
} |
| 29 |
|
| 30 |
|
| 31 |
pub struct Projection { |
| 32 |
pub target_heads: Vec<ChangeHash>, |
| 33 |
pub target_state: ObjectRef, |
| 34 |
pub heads: Vec<ChangeHash>, |
| 35 |
pub state: ObjectRef, |
| 36 |
|
| 37 |
pub changes: Vec<ChangeView>, |
| 38 |
pub hashes: Vec<ChangeHash>, |
| 39 |
pub conflicted: bool, |
| 40 |
pub conflicts: Vec<ConflictView>, |
| 41 |
pub files: Vec<FileDiff>, |
| 42 |
pub bases: BTreeMap<String, Vec<u8>>, |
| 43 |
pub loaded: Arc<Loaded>, |
| 44 |
} |
| 45 |
|
| 46 |
|
| 47 |
|
| 48 |
|
| 49 |
pub fn project(repo: &Repo, cache: &StateCache, target: &str, source: Source<'_>) -> Result<Projection> { |
| 50 |
let target_heads = repo.bookmarks()?.into_iter().find(|b| b.name == target).map(|b| b.heads).unwrap_or_default(); |
| 51 |
let candidate: Vec<ChangeHash> = match &source { |
| 52 |
Source::Bookmark(heads) => heads.clone(), |
| 53 |
Source::Changes(hashes) => hashes.clone(), |
| 54 |
Source::Fork { fork, heads } => { |
| 55 |
|
| 56 |
let fork_state = fork.load_state(heads)?; |
| 57 |
let missing: Vec<ChangeHash> = fork_state.changes().map(|(h, _)| *h).filter(|h| !repo.store().has(h)).collect(); |
| 58 |
if !missing.is_empty() { |
| 59 |
let bundle = fork.bundle_for(&missing)?; |
| 60 |
repo.import_bundle(&bundle, repo.current_policy()?.is_some())?; |
| 61 |
} |
| 62 |
heads.clone() |
| 63 |
} |
| 64 |
}; |
| 65 |
for h in &candidate { |
| 66 |
if !repo.store().has(h) { |
| 67 |
return Err(RepoError::Oxy(oxy_core::repo::RepoError::Msg(format!("change {} is not in this repository", h.short(12))))); |
| 68 |
} |
| 69 |
} |
| 70 |
let mut union: Vec<ChangeHash> = target_heads.clone(); |
| 71 |
union.extend(candidate.iter().copied()); |
| 72 |
let loaded = cache.load(repo, &union)?; |
| 73 |
let heads = loaded.state.heads(); |
| 74 |
let target_loaded = cache.load(repo, &target_heads)?; |
| 75 |
let labels = view::head_labels(repo)?; |
| 76 |
let head_set: BTreeSet<ChangeHash> = heads.iter().copied().collect(); |
| 77 |
let mut added: Vec<(ChangeHash, oxy_core::state::ChangeMeta)> = |
| 78 |
loaded.state.changes().filter(|(h, _)| !target_loaded.state.contains(h)).map(|(h, m)| (*h, m.clone())).collect(); |
| 79 |
|
| 80 |
added.sort_by(|a, b| a.1.hlc.cmp(&b.1.hlc).then(a.0.cmp(&b.0))); |
| 81 |
let hashes: Vec<ChangeHash> = added.iter().map(|(h, _)| *h).collect(); |
| 82 |
let changes: Vec<ChangeView> = added.iter().map(|(h, m)| view::change_view_pub(repo, h, m, head_set.contains(h), &labels)).collect(); |
| 83 |
let conflicted = loaded.snapshot.is_conflicted(); |
| 84 |
let conflicts = if conflicted { view::conflicts_in(repo, &loaded, "projected") } else { vec![] }; |
| 85 |
let (files, bases) = crate::render::diff::snapshot_diff(&target_loaded.snapshot, &loaded.snapshot); |
| 86 |
let state = StateObject::new(heads.clone()).hash(); |
| 87 |
let target_state = StateObject::new(target_heads.clone()).hash(); |
| 88 |
Ok(Projection { target_heads, target_state, heads, state, changes, hashes, conflicted, conflicts, files, bases, loaded }) |
| 89 |
} |
| 90 |
|
| 91 |
|
| 92 |
pub fn atom_at(loaded: &Loaded, path: &str, new_no: usize) -> Option<(ChangeHash, u32)> { |
| 93 |
let f = loaded.snapshot.files.get(path)?; |
| 94 |
if !matches!(f.content, FileContent::Text { .. }) { |
| 95 |
return None; |
| 96 |
} |
| 97 |
let lines = loaded.state.live_lines(&f.node); |
| 98 |
lines.get(new_no.checked_sub(1)?).map(|((h, i), _)| (*h, *i)) |
| 99 |
} |
| 100 |
|
| 101 |
|
| 102 |
|
| 103 |
pub fn atoms_of(loaded: &Loaded, path: &str) -> Option<Vec<(ChangeHash, u32)>> { |
| 104 |
let f = loaded.snapshot.files.get(path)?; |
| 105 |
if !matches!(f.content, FileContent::Text { .. }) { |
| 106 |
return None; |
| 107 |
} |
| 108 |
Some(loaded.state.live_lines(&f.node).iter().map(|((h, i), _)| (*h, *i)).collect()) |
| 109 |
} |
| 110 |
|
| 111 |
|
| 112 |
pub fn line_of(loaded: &Loaded, path: &str, atom: (ChangeHash, u32)) -> Option<usize> { |
| 113 |
let f = loaded.snapshot.files.get(path)?; |
| 114 |
let lines = loaded.state.live_lines(&f.node); |
| 115 |
lines.iter().position(|((h, i), _)| (*h, *i) == atom).map(|p| p + 1) |
| 116 |
} |
| 117 |
|
| 118 |
|
| 119 |
pub fn sealing_keys(repo: &Repo, hashes: &[ChangeHash]) -> Result<Vec<KeyId>> { |
| 120 |
if hashes.is_empty() { |
| 121 |
return Ok(vec![]); |
| 122 |
} |
| 123 |
let mut counts: HashMap<KeyId, usize> = HashMap::new(); |
| 124 |
for h in hashes { |
| 125 |
let mut keys_here: BTreeSet<KeyId> = BTreeSet::new(); |
| 126 |
for seal_ref in repo.store().seals_for(h)? { |
| 127 |
let attest = repo.store().get_attest(&seal_ref)?; |
| 128 |
let oxy_core::attest::AttestBody::Seal { hashes: covered } = &attest.body else { continue }; |
| 129 |
if !covered.contains(h) { |
| 130 |
continue; |
| 131 |
} |
| 132 |
for sig in repo.store().signatures(&seal_ref)? { |
| 133 |
if let Ok(id) = repo.store().get_identity(&sig.key) { |
| 134 |
if oxy_core::identity::verify(&id, &sig, oxy_core::identity::Domain::Attest, &seal_ref).is_ok() { |
| 135 |
keys_here.insert(sig.key); |
| 136 |
} |
| 137 |
} |
| 138 |
} |
| 139 |
} |
| 140 |
for k in keys_here { |
| 141 |
*counts.entry(k).or_default() += 1; |
| 142 |
} |
| 143 |
} |
| 144 |
Ok(counts.into_iter().filter(|(_, n)| *n == hashes.len()).map(|(k, _)| k).collect()) |
| 145 |
} |
| 146 |
|
| 147 |
|
| 148 |
|
| 149 |
pub fn owners_for(target: &Loaded, touched: &[String]) -> Vec<String> { |
| 150 |
let Some(f) = target.snapshot.files.get(".oxyowners") else { return vec![] }; |
| 151 |
let FileContent::Text { bytes, .. } = &f.content else { return vec![] }; |
| 152 |
let text = String::from_utf8_lossy(bytes); |
| 153 |
let rules: Vec<(String, Vec<String>)> = text |
| 154 |
.lines() |
| 155 |
.map(str::trim) |
| 156 |
.filter(|l| !l.is_empty() && !l.starts_with('#')) |
| 157 |
.filter_map(|l| { |
| 158 |
let mut parts = l.split_whitespace(); |
| 159 |
let pattern = parts.next()?.to_string(); |
| 160 |
let owners: Vec<String> = parts.map(|o| o.trim_start_matches('@').to_string()).collect(); |
| 161 |
Some((pattern, owners)) |
| 162 |
}) |
| 163 |
.collect(); |
| 164 |
let mut out: BTreeSet<String> = BTreeSet::new(); |
| 165 |
for path in touched { |
| 166 |
|
| 167 |
if let Some((_, owners)) = rules.iter().rev().find(|(pattern, _)| owner_pattern_matches(pattern, path)) { |
| 168 |
out.extend(owners.iter().cloned()); |
| 169 |
} |
| 170 |
} |
| 171 |
out.into_iter().collect() |
| 172 |
} |
| 173 |
|
| 174 |
fn owner_pattern_matches(pattern: &str, path: &str) -> bool { |
| 175 |
let p = pattern.trim_start_matches('/'); |
| 176 |
if p == "*" { |
| 177 |
return true; |
| 178 |
} |
| 179 |
if let Some(dir) = p.strip_suffix('/') { |
| 180 |
return path.starts_with(&format!("{dir}/")); |
| 181 |
} |
| 182 |
if let Some(ext) = p.strip_prefix("*.") { |
| 183 |
return path.ends_with(&format!(".{ext}")); |
| 184 |
} |
| 185 |
if let Some(prefix) = p.strip_suffix("/*") { |
| 186 |
return path.strip_prefix(&format!("{prefix}/")).is_some_and(|rest| !rest.contains('/')); |
| 187 |
} |
| 188 |
if let Some(prefix) = p.strip_suffix("/**") { |
| 189 |
return path.starts_with(&format!("{prefix}/")); |
| 190 |
} |
| 191 |
path == p || path.starts_with(&format!("{p}/")) |
| 192 |
} |
| 193 |
|
| 194 |
|
| 195 |
pub fn state_ref(heads: &[ChangeHash]) -> ObjectRef { |
| 196 |
StateObject::new(heads.to_vec()).hash() |
| 197 |
} |
| 198 |
|
| 199 |
|
| 200 |
pub fn target_of(label: &str, heads: &[ChangeHash]) -> RefTarget { |
| 201 |
RefTarget { label: label.into(), kind: view::RefKind::Change, heads: heads.to_vec() } |
| 202 |
} |
| 203 |
|
| 204 |
#[cfg(test)] |
| 205 |
mod tests { |
| 206 |
use super::*; |
| 207 |
|
| 208 |
#[test] |
| 209 |
fn owner_patterns() { |
| 210 |
assert!(owner_pattern_matches("*", "any/thing")); |
| 211 |
assert!(owner_pattern_matches("docs/", "docs/spec.md")); |
| 212 |
assert!(!owner_pattern_matches("docs/", "src/docs.rs")); |
| 213 |
assert!(owner_pattern_matches("*.rs", "src/main.rs")); |
| 214 |
assert!(owner_pattern_matches("src/*", "src/main.rs")); |
| 215 |
assert!(!owner_pattern_matches("src/*", "src/a/b.rs")); |
| 216 |
assert!(owner_pattern_matches("crates/**", "crates/hub/src/lib.rs")); |
| 217 |
assert!(owner_pattern_matches("Cargo.toml", "Cargo.toml")); |
| 218 |
} |
| 219 |
} |