Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 6 | //! landed on main. The git view has each line as a branch of that name. |
| 7 |
| 8 | use serde::Deserialize; |
| 9 | use serde_json::json; |
| 10 | use vcs_core::Id; |
| 11 |
| 12 | use crate::app::App; |
| 13 | use crate::db::{Line, LineVersion, Project}; |
| 14 | use crate::ids; |
| 15 | use crate::out::{Fail, Out, Report}; |
| 16 | use crate::remote::Remote; |
| 17 |
| 18 | #[derive(Deserialize)] |
| 19 | struct HostLine { |
| 20 | name: String, |
| 21 | from: u64, |
| 22 | head: u64, |
| 23 | head_tree: Id, |
| 24 | } |
| 25 |
| 26 | #[derive(Deserialize)] |
| 27 | struct HostLines { |
| 28 | lines: Vec<HostLine>, |
| 29 | } |
| 30 |
| 31 | fn host_lines(project: &Project) -> Result<Vec<HostLine>, Fail> { |
| 32 | Ok(Remote::for_project(project).get_json::<HostLines>("/lines")?.lines) |
| 33 | } |
| 34 |
| 35 | pub fn list(app: &App, project: &Project) -> Result<Vec<Line>, Fail> { |
| 36 | if project.hosted() { |
| 37 | return Ok(host_lines(project)?.into_iter().map(|l| Line { name: l.name, from: l.from, head: l.head }).collect()); |
| 38 | } |
| 39 | Ok(app.db.lines(&project.id)?) |
| 40 | } |
| 41 |
| 42 | /// A line and its latest version's tree (here, ready to read). |
| 43 | pub fn head(app: &App, project: &Project, name: &str) -> Result<(Line, Id), Fail> { |
| 44 | let missing = || Fail::new("not_found", format!("there's no line {name}")).state("nothing changed").next("vcs line to list them"); |
| 45 | if project.hosted() { |
| 46 | let l = host_lines(project)?.into_iter().find(|l| l.name == name).ok_or_else(missing)?; |
| 47 | app.host(project).fetch_tree(&app.store, project, &l.head_tree, None)?; |
| 48 | return Ok((Line { name: l.name, from: l.from, head: l.head }, l.head_tree)); |
| 49 | } |
| 50 | let line = app.db.line(&project.id, name)?.ok_or_else(missing)?; |
| 51 | let tree = app.db.line_tree(&project.id, &line, line.head)?.ok_or_else(|| Fail::new("not_found", format!("line {name}'s start isn't here")))?; |
| 52 | Ok((line, tree)) |
| 53 | } |
| 54 |
| 55 | /// A line's versions, newest first. |
| 56 | pub fn versions(app: &App, project: &Project, name: &str, limit: usize) -> Result<Vec<LineVersion>, Fail> { |
| 57 | if project.hosted() { |
| 58 | #[derive(Deserialize)] |
| 59 | struct List { |
| 60 | versions: Vec<LineVersion>, |
| 61 | } |
| 62 | return Ok(Remote::for_project(project).get_json::<List>(&format!("/lines/versions?name={}&limit={limit}", crate::history::encode(name)))?.versions); |
| 63 | } |
| 64 | Ok(app.db.line_versions(&project.id, name, limit as i64)?) |
| 65 | } |
| 66 |
| 67 | /// "1.x" (its latest version) or "1.x/3": the tree there, and how to say it. |
| 68 | pub fn tree_at(app: &App, project: &Project, text: &str) -> Result<Option<(Id, String)>, Fail> { |
| 69 | let (name, n) = match text.rsplit_once('/') { |
| 70 | Some((name, n)) if n.parse::<u64>().is_ok() => (name, n.parse::<u64>().ok()), |
| 71 | _ => (text, None), |
| 72 | }; |
| 73 | let Ok((line, head)) = head(app, project, name) else { return Ok(None) }; |
| 74 | let Some(n) = n.filter(|n| *n != line.head) else { return Ok(Some((head, format!("{name}/{}", line.head)))) }; |
| 75 | if n == 0 { |
| 76 | let v = app.version(project, line.from)?.ok_or_else(|| Fail::new("not_found", format!("main has no version {}", line.from)))?; |
| 77 | app.host(project).fetch_tree(&app.store, project, &v.tree, None)?; |
| 78 | return Ok(Some((v.tree, format!("main {}", line.from)))); |
| 79 | } |
| 80 | let missing = || Fail::new("not_found", format!("line {name} has no version {n}")).state("nothing changed"); |
| 81 | if n > line.head { |
| 82 | return Err(missing()); |
| 83 | } |
| 84 | let v = versions(app, project, name, (line.head - n + 1) as usize)? |
| 85 | .into_iter() |
| 86 | .find(|v| v.n == n) |
| 87 | .ok_or_else(missing)?; |
| 88 | app.host(project).fetch_tree(&app.store, project, &v.tree, None)?; |
| 89 | Ok(Some((v.tree, format!("{name}/{n}")))) |
| 90 | } |
| 91 |
| 92 | /// Why a name can't be a line's: a tag's rules (it's a git branch too), and |
| 93 | /// no "/", which names a line's version ("1.x/3"). |
| 94 | pub fn bad_name(name: &str) -> Option<String> { |
| 95 | if name.contains('/') { |
| 96 | return Some("a line's name can't have / in it".into()); |
| 97 | } |
| 98 | crate::tags::bad_name(name).map(|why| why.replace("a tag", "a name").replace("tags", "names")) |
| 99 | } |
| 100 |
| 101 | /// `vcs line [name [--from <version>]]`: the project's lines, or a new one |
| 102 | /// from a version of main (the latest by default). |
| 103 | pub fn run(app: &App, name: Option<String>, from: Option<String>) -> Out { |
| 104 | let s = app.session()?; |
| 105 | let Some(name) = name else { |
| 106 | let lines = list(app, &s.project)?; |
| 107 | let mut r = Report::new(); |
| 108 | for l in &lines { |
| 109 | r.line(&l.name, format!("from main {}, {} {}", l.from, l.head, if l.head == 1 { "version" } else { "versions" })); |
| 110 | } |
| 111 | if lines.is_empty() { |
| 112 | r.line("lines", "none yet; vcs line 1.x --from <version> starts one"); |
| 113 | } |
| 114 | r.set("lines", serde_json::to_value(&lines).unwrap_or_default()); |
| 115 | return Ok(r); |
| 116 | }; |
| 117 | if let Some(why) = bad_name(&name) { |
| 118 | return Err(Fail::new("bad_usage", format!("{name:?} can't be a line: {why}")).state("nothing changed")); |
| 119 | } |
| 120 | let main = app.refresh(&s.project)?.map(|v| v.n).unwrap_or(0); |
| 121 | let from_n = match from.as_deref() { |
| 122 | None | Some("main") => main, |
| 123 | Some(text) => text.parse::<u64>().ok().or(crate::tags::find(app, &s.project, text)?).ok_or_else(|| Fail::new("bad_usage", format!("{text} isn't a version of main or a tag")).state("nothing changed"))?, |
| 124 | }; |
| 125 | if from_n == 0 || from_n > main { |
| 126 | return Err(Fail::new("bad_usage", format!("main has no version {from_n}")).state("nothing changed")); |
| 127 | } |
| 128 | if s.project.hosted() { |
| 129 | let _: serde_json::Value = Remote::for_project(&s.project).send_json("POST", "/lines", &json!({ "name": name, "from": from_n }))?; |
| 130 | } else { |
| 131 | if app.db.line(&s.project.id, &name)?.is_some() { |
| 132 | return Err(Fail::new("exists", format!("there's a line {name} already")).state("nothing changed")); |
| 133 | } |
| 134 | app.db.put_line(&s.project.id, &name, from_n)?; |
| 135 | } |
| 136 | let mut r = Report::new(); |
| 137 | r.line("line", format!("{name} from main {from_n}")); |
| 138 | r.set("line", name.clone()).set("from", from_n); |
| 139 | r.next(format!("vcs new \"Title\" --line {name} to work on it, or vcs land <change> --to {name} to bring a change over")); |
| 140 | Ok(r) |
| 141 | } |
| 142 |
| 143 | /// `vcs log --line <name>`: a line's versions. |
| 144 | pub fn log(app: &App, project: &Project, name: &str, limit: usize) -> Out { |
| 145 | let (line, _) = head(app, project, name)?; |
| 146 | let list = versions(app, project, name, limit)?; |
| 147 | let mut r = Report::new(); |
| 148 | for v in &list { |
| 149 | r.raw(format!("{:>8} {} {} {}\n", format!("{name}/{}", v.n), v.title, v.author, ids::ago(v.time))); |
| 150 | } |
| 151 | r.raw(format!("{:>8} (from main {})\n", format!("{name}/0"), line.from)); |
| 152 | r.set("line", name).set("from", line.from).set("versions", serde_json::to_value(&list).unwrap_or_default()); |
| 153 | Ok(r) |
| 154 | } |
| 155 |