//! `vcs access`: who can see a change or a path. //! //! `me` on a path makes it one of your own files (like .env): saved and //! carried to your other workspaces, never landed. Paths for groups and //! sharing changes are rules the host keeps. use serde_json::json; use crate::app::{App, Session}; use crate::ids; use crate::out::{Fail, Out, Report}; pub fn run(app: &App, target: &str, who: Vec, edit: bool) -> Out { let s = app.session()?; let change = app.save(&s)?; let _ = change; let is_change = ids::looks_like_change(target) && !app.db.changes_by_prefix(&s.project.id, target)?.is_empty(); if is_change { return crate::remote::access_change(app, &s, target, who, edit); } let path = normalize(app, &s, target); let mut patterns = personal(app, &s.project.id)?; if who.is_empty() { let mut r = Report::new(); let mine = patterns.iter().any(|p| p == &path) || crate::ws::Mine::new(&crate::ws::config(&s.root), &patterns).matches(&path); if mine { r.line("access", format!("{path}: only you (one of your own files)")); r.set("who", vec!["me"]); } else { let rules = crate::remote::path_rules(app, &s.project)?; match rules.iter().find(|(p, _)| crate::path_matches(&path, p)) { Some((p, who)) => { r.line("access", format!("{path}: {} (rule on {p})", who.join(", "))); r.set("who", who.clone()); } None => { r.line("access", format!("{path}: everyone with access to the project")); r.set("who", vec!["team"]); } } } r.set("path", path); return Ok(r); } if who.len() == 1 && who[0] == "me" { if patterns.contains(&path) { let mut r = Report::new(); r.line("access", format!("{path} is already only yours")); return Ok(r); } let before = patterns.clone(); patterns.push(path.clone()); set_personal(app, &s.project.id, &patterns)?; app.db.add_op(&s.ws.id, "access", &json!({ "target": path, "who": "me", "personal_before": before }))?; let mut r = Report::new(); r.line("access", format!("{path} is now only yours: saved and carried to your other workspaces, never landed")); r.set("path", path).set("who", vec!["me"]); if crate::app::index_files(&crate::ws::Index::load(&s.root)).contains_key(&r.json["path"].as_str().unwrap_or("").to_string()) { r.next("main still has it; land a change that removes it from main if others shouldn't see it"); } return Ok(r); } if !s.project.hosted() { return Err(Fail::new("bad_usage", "a project on this machine only has you; rules for other people need a hosted project") .state("nothing changed") .next("vcs publish to put it on a host")); } crate::remote::set_path_rule(app, &s, &path, who) } fn normalize(app: &App, s: &Session, target: &str) -> String { let abs = app.cwd.join(target); let rel = abs.strip_prefix(&s.root).map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|_| target.to_string()); rel.trim_matches('/').to_string() } pub fn personal(app: &App, project: &str) -> anyhow::Result> { let text: Option = app .db .conn .query_row("SELECT patterns FROM personal WHERE project = ?1", [project], |r| r.get(0)) .ok(); Ok(text.and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default()) } fn set_personal(app: &App, project: &str, patterns: &[String]) -> anyhow::Result<()> { app.db.conn.execute( "INSERT OR REPLACE INTO personal (project, patterns) VALUES (?1, ?2)", rusqlite::params![project, serde_json::to_string(patterns)?], )?; Ok(()) } pub fn undo(app: &App, s: &Session, data: &serde_json::Value) -> Result<(), Fail> { if let Some(before) = data.get("personal_before") { let patterns: Vec = serde_json::from_value(before.clone()).unwrap_or_default(); set_personal(app, &s.project.id, &patterns)?; return Ok(()); } if let Some(before) = data.get("rule_before") { let path = data.get("target").and_then(|v| v.as_str()).unwrap_or(""); let who: Vec = serde_json::from_value(before.clone()).unwrap_or_default(); crate::remote::set_path_rule(app, s, path, if who.is_empty() { vec!["team".into()] } else { who })?; return Ok(()); } Err(Fail::new("failed", "that access change can't be undone here")) }