| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | | use crate::cbor::{self, Value}; |
| 9 | | use crate::hash::ChangeHash; |
| 10 | | use crate::objects::{ |
| 11 | | array, bytes, check_version, field, oref, text, uint, ObjectError, FORMAT_VERSION, |
| 12 | | }; |
| 13 | | use std::collections::BTreeMap; |
| 14 | |
|
| 15 | | #[derive(Clone, Debug, PartialEq, Eq)] |
| 16 | | pub enum Pin { |
| 17 | | Oxy { |
| 18 | | repo: [u8; 16], |
| 19 | | heads: Vec<ChangeHash>, |
| 20 | | bookmark: String, |
| 21 | | }, |
| 22 | | Git { |
| 23 | | sha: String, |
| 24 | | branch: String, |
| 25 | | }, |
| 26 | | } |
| 27 | |
|
| 28 | | impl Pin { |
| 29 | | |
| 30 | | |
| 31 | | |
| 32 | | |
| 33 | 64× | pub fn same_state(&self, other: &Pin) -> bool { |
| 34 | 64× | match (self, other) { |
| 35 | | ( |
| 36 | | Pin::Oxy { |
| 37 | 62× | repo: r1, |
| 38 | 62× | heads: h1, |
| 39 | | .. |
| 40 | | }, |
| 41 | | Pin::Oxy { |
| 42 | 62× | repo: r2, |
| 43 | 62× | heads: h2, |
| 44 | | .. |
| 45 | | }, |
| 46 | | ) => { |
| 47 | 62× | let (mut a, mut b) = (h1.clone(), h2.clone()); |
| 48 | 62× | a.sort(); |
| 49 | 62× | a.dedup(); |
| 50 | 62× | b.sort(); |
| 51 | 62× | b.dedup(); |
| 52 | 62× | r1 == r2 && a == b |
| 53 | | } |
| 54 | 2× | (Pin::Git { sha: a, .. }, Pin::Git { sha: b, .. }) => a == b, |
| 55 | 0× | _ => false, |
| 56 | | } |
| 57 | 64× | } |
| 58 | |
|
| 59 | 42× | pub fn encode(&self) -> Vec<u8> { |
| 60 | 42× | let v = match self { |
| 61 | | Pin::Oxy { |
| 62 | 40× | repo, |
| 63 | 40× | heads, |
| 64 | 40× | bookmark, |
| 65 | | } => { |
| 66 | 40× | let mut heads = heads.clone(); |
| 67 | 40× | heads.sort(); |
| 68 | 40× | heads.dedup(); |
| 69 | 40× | Value::map([ |
| 70 | 40× | (0, Value::Uint(FORMAT_VERSION)), |
| 71 | 40× | (1, Value::Uint(0)), |
| 72 | 40× | (2, Value::Bytes(repo.to_vec())), |
| 73 | | ( |
| 74 | | 3, |
| 75 | 46× | Value::Array(heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect()), |
| 76 | | ), |
| 77 | 40× | (4, Value::Text(bookmark.clone())), |
| 78 | | ]) |
| 79 | | } |
| 80 | 2× | Pin::Git { sha, branch } => Value::map([ |
| 81 | 2× | (0, Value::Uint(FORMAT_VERSION)), |
| 82 | 2× | (1, Value::Uint(1)), |
| 83 | 2× | (2, Value::Null), |
| 84 | 2× | (3, Value::Text(sha.clone())), |
| 85 | 2× | (4, Value::Text(branch.clone())), |
| 86 | 2× | ]), |
| 87 | | }; |
| 88 | 42× | cbor::encode(&v) |
| 89 | 42× | } |
| 90 | |
|
| 91 | 76× | pub fn decode(b: &[u8]) -> Result<Pin, ObjectError> { |
| 92 | 76× | let v = cbor::decode(b)?; |
| 93 | 76× | let m = v |
| 94 | 76× | .as_map() |
| 95 | 76× | .ok_or_else(|| ObjectError::Schema("pin: expected map".into()))?; |
| 96 | 76× | check_version(m)?; |
| 97 | 76× | let bookmark = text(field(m, 4, "bookmark")?, "bookmark")?.to_owned(); |
| 98 | 76× | match uint(field(m, 1, "kind")?, "kind")? { |
| 99 | | 0 => { |
| 100 | 70× | let repo: [u8; 16] = bytes(field(m, 2, "repo")?, "repo")? |
| 101 | 70× | .try_into() |
| 102 | 70× | .map_err(|_| ObjectError::Schema("pin: repo id must be 16 bytes".into()))?; |
| 103 | 70× | let heads = array(field(m, 3, "heads")?, "heads")? |
| 104 | 70× | .iter() |
| 105 | 71× | .map(|h| oref(h, "head")) |
| 106 | 70× | .collect::<Result<Vec<_>, _>>()?; |
| 107 | 70× | Ok(Pin::Oxy { |
| 108 | 70× | repo, |
| 109 | 70× | heads, |
| 110 | 70× | bookmark, |
| 111 | 70× | }) |
| 112 | | } |
| 113 | | 1 => Ok(Pin::Git { |
| 114 | 6× | sha: text(field(m, 3, "sha")?, "sha")?.to_owned(), |
| 115 | 6× | branch: bookmark, |
| 116 | | }), |
| 117 | 0× | k => Err(ObjectError::Schema(format!("pin: unknown kind {k}"))), |
| 118 | | } |
| 119 | 76× | } |
| 120 | |
|
| 121 | | |
| 122 | 6× | pub fn summary(&self) -> String { |
| 123 | 6× | match self { |
| 124 | | Pin::Oxy { |
| 125 | 6× | heads, bookmark, .. |
| 126 | 6× | } => format!( |
| 127 | | "oxy {bookmark} {}", |
| 128 | 6× | heads |
| 129 | 6× | .iter() |
| 130 | 6× | .map(|h| h.short(12)) |
| 131 | 6× | .collect::<Vec<_>>() |
| 132 | 6× | .join(" ") |
| 133 | | ), |
| 134 | 0× | Pin::Git { sha, branch } => format!("git {branch} {}", &sha[..sha.len().min(12)]), |
| 135 | | } |
| 136 | 6× | } |
| 137 | |
|
| 138 | 2× | pub fn is_git(&self) -> bool { |
| 139 | 2× | matches!(self, Pin::Git { .. }) |
| 140 | 2× | } |
| 141 | | } |
| 142 | |
|
| 143 | |
|
| 144 | | #[derive(Clone, Debug, Default, PartialEq, Eq)] |
| 145 | | pub struct Module { |
| 146 | | pub url: String, |
| 147 | | |
| 148 | | pub bookmark: Option<String>, |
| 149 | | pub git: bool, |
| 150 | | } |
| 151 | |
|
| 152 | |
|
| 153 | |
|
| 154 | | #[derive(Clone, Debug, Default, PartialEq, Eq)] |
| 155 | | pub struct Modules { |
| 156 | | pub entries: BTreeMap<String, Module>, |
| 157 | | } |
| 158 | |
|
| 159 | | impl Modules { |
| 160 | 113× | pub fn parse(text: &str) -> Modules { |
| 161 | 113× | let mut out = Modules::default(); |
| 162 | 113× | let mut current: Option<String> = None; |
| 163 | 347× | for line in text.lines() { |
| 164 | 347× | let line = line.trim(); |
| 165 | 347× | if line.is_empty() || line.starts_with('#') { |
| 166 | 106× | continue; |
| 167 | 241× | } |
| 168 | 241× | if let Some(inner) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { |
| 169 | 115× | let path = inner |
| 170 | 115× | .trim() |
| 171 | 115× | .trim_matches('"') |
| 172 | 115× | .trim_end_matches('/') |
| 173 | 115× | .to_string(); |
| 174 | 115× | out.entries.entry(path.clone()).or_default(); |
| 175 | 115× | current = Some(path); |
| 176 | 115× | continue; |
| 177 | 126× | } |
| 178 | 126× | let (Some(path), Some((k, v))) = (¤t, line.split_once('=')) else { |
| 179 | 0× | continue; |
| 180 | | }; |
| 181 | 126× | let v = v.trim().trim_matches('"').to_string(); |
| 182 | 126× | let m = out.entries.get_mut(path).unwrap(); |
| 183 | 126× | match k.trim() { |
| 184 | 126× | "url" => m.url = v, |
| 185 | 11× | "bookmark" => { |
| 186 | 2× | m.bookmark = Some(v); |
| 187 | 2× | m.git = false; |
| 188 | 2× | } |
| 189 | 9× | "branch" => { |
| 190 | 2× | m.bookmark = Some(v); |
| 191 | 2× | m.git = true; |
| 192 | 2× | } |
| 193 | 7× | "kind" => m.git = v == "git", |
| 194 | 0× | _ => {} |
| 195 | | } |
| 196 | | } |
| 197 | 113× | out |
| 198 | 113× | } |
| 199 | |
|
| 200 | 14× | pub fn to_text(&self) -> String { |
| 201 | 14× | let mut s = String::new(); |
| 202 | 15× | for (path, m) in &self.entries { |
| 203 | 15× | s.push_str(&format!("[{path}]\nurl = {}\n", m.url)); |
| 204 | 15× | if m.git { |
| 205 | 2× | s.push_str("kind = git\n"); |
| 206 | 13× | } |
| 207 | 15× | if let Some(b) = &m.bookmark { |
| 208 | 2× | s.push_str(&format!( |
| 209 | | "{} = {b}\n", |
| 210 | 2× | if m.git { "branch" } else { "bookmark" } |
| 211 | | )); |
| 212 | 13× | } |
| 213 | 15× | s.push('\n'); |
| 214 | | } |
| 215 | 14× | s |
| 216 | 14× | } |
| 217 | |
|
| 218 | | |
| 219 | 2× | pub fn from_gitmodules(text: &str) -> Modules { |
| 220 | 2× | let mut out = Modules::default(); |
| 221 | 2× | let mut path: Option<String> = None; |
| 222 | 2× | let mut url = String::new(); |
| 223 | 2× | let mut branch: Option<String> = None; |
| 224 | 2× | let flush = |
| 225 | 5× | |out: &mut Modules, path: &Option<String>, url: &str, branch: &Option<String>| { |
| 226 | 5× | if let Some(p) = path { |
| 227 | 3× | out.entries.insert( |
| 228 | 3× | p.clone(), |
| 229 | 3× | Module { |
| 230 | 3× | url: url.to_string(), |
| 231 | 3× | bookmark: branch.clone(), |
| 232 | 3× | git: true, |
| 233 | 3× | }, |
| 234 | 3× | ); |
| 235 | 3× | } |
| 236 | 5× | }; |
| 237 | 10× | for line in text.lines() { |
| 238 | 10× | let line = line.trim(); |
| 239 | 10× | if line.starts_with("[submodule") { |
| 240 | 3× | flush(&mut out, &path, &url, &branch); |
| 241 | 3× | path = None; |
| 242 | 3× | url.clear(); |
| 243 | 3× | branch = None; |
| 244 | 3× | continue; |
| 245 | 7× | } |
| 246 | 7× | if let Some((k, v)) = line.split_once('=') { |
| 247 | 7× | let v = v.trim().to_string(); |
| 248 | 7× | match k.trim() { |
| 249 | 7× | "path" => path = Some(v.trim_end_matches('/').to_string()), |
| 250 | 4× | "url" => url = v, |
| 251 | 1× | "branch" => branch = Some(v), |
| 252 | 0× | _ => {} |
| 253 | | } |
| 254 | 0× | } |
| 255 | | } |
| 256 | 2× | flush(&mut out, &path, &url, &branch); |
| 257 | 2× | out |
| 258 | 2× | } |
| 259 | |
|
| 260 | | |
| 261 | 2× | pub fn to_gitmodules(&self) -> String { |
| 262 | 2× | let mut s = String::new(); |
| 263 | 4× | for (path, m) in &self.entries { |
| 264 | 4× | if !m.git { |
| 265 | 0× | continue; |
| 266 | 4× | } |
| 267 | 4× | s.push_str(&format!( |
| 268 | 4× | "[submodule \"{path}\"]\n\tpath = {path}\n\turl = {}\n", |
| 269 | 4× | m.url |
| 270 | 4× | )); |
| 271 | 4× | if let Some(b) = &m.bookmark { |
| 272 | 2× | s.push_str(&format!("\tbranch = {b}\n")); |
| 273 | 2× | } |
| 274 | | } |
| 275 | 2× | s |
| 276 | 2× | } |
| 277 | | } |
| 278 | |
|
| 279 | | #[cfg(test)] |
| 280 | | mod tests { |
| 281 | | use super::*; |
| 282 | | use crate::hash::ObjectRef; |
| 283 | |
|
| 284 | | #[test] |
| 285 | 1× | fn pins_round_trip() { |
| 286 | 1× | let o = Pin::Oxy { |
| 287 | 1× | repo: [7; 16], |
| 288 | 1× | heads: vec![ObjectRef([2; 32]), ObjectRef([1; 32])], |
| 289 | 1× | bookmark: "main".into(), |
| 290 | 1× | }; |
| 291 | 1× | let back = Pin::decode(&o.encode()).unwrap(); |
| 292 | 1× | assert_eq!( |
| 293 | | back, |
| 294 | 1× | Pin::Oxy { |
| 295 | 1× | repo: [7; 16], |
| 296 | 1× | heads: vec![ObjectRef([1; 32]), ObjectRef([2; 32])], |
| 297 | 1× | bookmark: "main".into() |
| 298 | 1× | } |
| 299 | | ); |
| 300 | 1× | let g = Pin::Git { |
| 301 | 1× | sha: "0cb4742f64cf204e5fafd0963749d84afc561892".into(), |
| 302 | 1× | branch: "main".into(), |
| 303 | 1× | }; |
| 304 | 1× | assert_eq!(Pin::decode(&g.encode()).unwrap(), g); |
| 305 | 1× | assert!(g.is_git() && !o.is_git()); |
| 306 | 1× | } |
| 307 | |
|
| 308 | | #[test] |
| 309 | 1× | fn modules_files() { |
| 310 | 1× | let text = "[libs/sub]\nurl = ../sub\nbookmark = main\n\n[vendor/x]\nurl = https://example.org/x.git\nkind = git\nbranch = trunk\n"; |
| 311 | 1× | let m = Modules::parse(text); |
| 312 | 1× | assert_eq!( |
| 313 | 1× | m.entries["libs/sub"], |
| 314 | 1× | Module { |
| 315 | 1× | url: "../sub".into(), |
| 316 | 1× | bookmark: Some("main".into()), |
| 317 | 1× | git: false |
| 318 | 1× | } |
| 319 | | ); |
| 320 | 1× | assert_eq!( |
| 321 | 1× | m.entries["vendor/x"], |
| 322 | 1× | Module { |
| 323 | 1× | url: "https://example.org/x.git".into(), |
| 324 | 1× | bookmark: Some("trunk".into()), |
| 325 | 1× | git: true |
| 326 | 1× | } |
| 327 | | ); |
| 328 | 1× | assert_eq!(Modules::parse(&m.to_text()), m); |
| 329 | 1× | let gm = "[submodule \"rstar-api\"]\n\tpath = rstar-api\n\turl = https://github.com/x/rstar-api.git\n[submodule \"c\"]\n\tpath = cstar-api\n\turl = https://github.com/x/cstar-api.git\n\tbranch = dev\n"; |
| 330 | 1× | let g = Modules::from_gitmodules(gm); |
| 331 | 1× | assert_eq!(g.entries.len(), 2); |
| 332 | 1× | assert!(g.entries["rstar-api"].git && g.entries["rstar-api"].bookmark.is_none()); |
| 333 | 1× | assert_eq!(g.entries["cstar-api"].bookmark.as_deref(), Some("dev")); |
| 334 | 1× | assert!( |
| 335 | 1× | g.to_gitmodules().contains("[submodule \"cstar-api\"]") |
| 336 | 1× | && g.to_gitmodules().contains("branch = dev") |
| 337 | | ); |
| 338 | 1× | } |
| 339 | | } |