//! Release lines: named lines of versions next to main (a maintained 1.x //! while main goes on). A line starts at a version of main and has its own //! numbered versions, `1.x/1`, `1.x/2`... A change lands on a line when it //! was started there (`vcs new --line`, `vcs get --line`) or with //! `vcs land --to `, which also backports a change that //! landed on main. The git view has each line as a branch of that name. use serde::Deserialize; use serde_json::json; use vcs_core::Id; use crate::app::App; use crate::db::{Line, LineVersion, Project}; use crate::ids; use crate::out::{Fail, Out, Report}; use crate::remote::Remote; #[derive(Deserialize)] struct HostLine { name: String, from: u64, head: u64, head_tree: Id, } #[derive(Deserialize)] struct HostLines { lines: Vec, } fn host_lines(project: &Project) -> Result, Fail> { Ok(Remote::for_project(project).get_json::("/lines")?.lines) } pub fn list(app: &App, project: &Project) -> Result, Fail> { if project.hosted() { return Ok(host_lines(project)?.into_iter().map(|l| Line { name: l.name, from: l.from, head: l.head }).collect()); } Ok(app.db.lines(&project.id)?) } /// A line and its latest version's tree (here, ready to read). pub fn head(app: &App, project: &Project, name: &str) -> Result<(Line, Id), Fail> { let missing = || Fail::new("not_found", format!("there's no line {name}")).state("nothing changed").next("vcs line to list them"); if project.hosted() { let l = host_lines(project)?.into_iter().find(|l| l.name == name).ok_or_else(missing)?; app.host(project).fetch_tree(&app.store, project, &l.head_tree, None)?; return Ok((Line { name: l.name, from: l.from, head: l.head }, l.head_tree)); } let line = app.db.line(&project.id, name)?.ok_or_else(missing)?; 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")))?; Ok((line, tree)) } /// A line's versions, newest first. pub fn versions(app: &App, project: &Project, name: &str, limit: usize) -> Result, Fail> { if project.hosted() { #[derive(Deserialize)] struct List { versions: Vec, } return Ok(Remote::for_project(project).get_json::(&format!("/lines/versions?name={}&limit={limit}", crate::history::encode(name)))?.versions); } Ok(app.db.line_versions(&project.id, name, limit as i64)?) } /// "1.x" (its latest version) or "1.x/3": the tree there, and how to say it. pub fn tree_at(app: &App, project: &Project, text: &str) -> Result, Fail> { let (name, n) = match text.rsplit_once('/') { Some((name, n)) if n.parse::().is_ok() => (name, n.parse::().ok()), _ => (text, None), }; let Ok((line, head)) = head(app, project, name) else { return Ok(None) }; let Some(n) = n.filter(|n| *n != line.head) else { return Ok(Some((head, format!("{name}/{}", line.head)))) }; if n == 0 { let v = app.version(project, line.from)?.ok_or_else(|| Fail::new("not_found", format!("main has no version {}", line.from)))?; app.host(project).fetch_tree(&app.store, project, &v.tree, None)?; return Ok(Some((v.tree, format!("main {}", line.from)))); } let missing = || Fail::new("not_found", format!("line {name} has no version {n}")).state("nothing changed"); if n > line.head { return Err(missing()); } let v = versions(app, project, name, (line.head - n + 1) as usize)? .into_iter() .find(|v| v.n == n) .ok_or_else(missing)?; app.host(project).fetch_tree(&app.store, project, &v.tree, None)?; Ok(Some((v.tree, format!("{name}/{n}")))) } /// Why a name can't be a line's: a tag's rules (it's a git branch too), and /// no "/", which names a line's version ("1.x/3"). pub fn bad_name(name: &str) -> Option { if name.contains('/') { return Some("a line's name can't have / in it".into()); } crate::tags::bad_name(name).map(|why| why.replace("a tag", "a name").replace("tags", "names")) } /// `vcs line [name [--from ]]`: the project's lines, or a new one /// from a version of main (the latest by default). pub fn run(app: &App, name: Option, from: Option) -> Out { let s = app.session()?; let Some(name) = name else { let lines = list(app, &s.project)?; let mut r = Report::new(); for l in &lines { r.line(&l.name, format!("from main {}, {} {}", l.from, l.head, if l.head == 1 { "version" } else { "versions" })); } if lines.is_empty() { r.line("lines", "none yet; vcs line 1.x --from starts one"); } r.set("lines", serde_json::to_value(&lines).unwrap_or_default()); return Ok(r); }; if let Some(why) = bad_name(&name) { return Err(Fail::new("bad_usage", format!("{name:?} can't be a line: {why}")).state("nothing changed")); } let main = app.refresh(&s.project)?.map(|v| v.n).unwrap_or(0); let from_n = match from.as_deref() { None | Some("main") => main, Some(text) => text.parse::().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"))?, }; if from_n == 0 || from_n > main { return Err(Fail::new("bad_usage", format!("main has no version {from_n}")).state("nothing changed")); } if s.project.hosted() { let _: serde_json::Value = Remote::for_project(&s.project).send_json("POST", "/lines", &json!({ "name": name, "from": from_n }))?; } else { if app.db.line(&s.project.id, &name)?.is_some() { return Err(Fail::new("exists", format!("there's a line {name} already")).state("nothing changed")); } app.db.put_line(&s.project.id, &name, from_n)?; } let mut r = Report::new(); r.line("line", format!("{name} from main {from_n}")); r.set("line", name.clone()).set("from", from_n); r.next(format!("vcs new \"Title\" --line {name} to work on it, or vcs land --to {name} to bring a change over")); Ok(r) } /// `vcs log --line `: a line's versions. pub fn log(app: &App, project: &Project, name: &str, limit: usize) -> Out { let (line, _) = head(app, project, name)?; let list = versions(app, project, name, limit)?; let mut r = Report::new(); for v in &list { r.raw(format!("{:>8} {} {} {}\n", format!("{name}/{}", v.n), v.title, v.author, ids::ago(v.time))); } r.raw(format!("{:>8} (from main {})\n", format!("{name}/0"), line.from)); r.set("line", name).set("from", line.from).set("versions", serde_json::to_value(&list).unwrap_or_default()); Ok(r) }