Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 4 |
| use vcs_cli::app::App; |
| 5 | use vcs_cli::out::{Fail, Out}; |
| 6 | use vcs_cli::{access, cmd}; |
| 7 |
| 8 | /// Version control where nobody waits and nothing gets lost. |
| 9 | #[derive(Parser)] |
| 10 | #[command(name = "vcs", version, about, disable_help_subcommand = true)] |
| 11 | struct Cli { |
| 12 | /// Print one JSON object instead of text. |
| 13 | #[arg(long, global = true)] |
| 14 | json: bool, |
| 15 | /// Run as if started in this folder. |
| 16 | #[arg(short = 'C', global = true, value_name = "FOLDER")] |
| 17 | dir: Option<PathBuf>, |
| 18 | #[command(subcommand)] |
| 19 | command: Command, |
| 20 | } |
| 21 |
| 22 | #[derive(Subcommand)] |
| 23 | enum Command { |
| 24 | /// Make a workspace for a project. |
| 25 | Get { |
| 26 | /// host/owner/name, or the folder of a local project. |
| 27 | project: String, |
| 28 | folder: Option<PathBuf>, |
| 29 | /// Start on an existing change instead of a new one. |
| 30 | #[arg(long)] |
| 31 | change: Option<String>, |
| 32 | /// Get only these folders (repeatable). |
| 33 | #[arg(long)] |
| 34 | only: Vec<String>, |
| 35 | }, |
| 36 | /// Start a new project that lives only on this machine. |
| 37 | Init { folder: Option<PathBuf> }, |
| 38 | /// Make a project from a git repository, with its history. |
| 39 | Import { |
| 40 | /// A git repository: a folder, a URL, or upstream.codes/owner/name. |
| 41 | url: String, |
| 42 | folder: Option<PathBuf>, |
| 43 | /// Then put it on a host as host/owner/name. |
| 44 | #[arg(long)] |
| 45 | to: Option<String>, |
| 46 | /// Bring this git branch (or any commit) into this workspace's project as a change instead. |
| 47 | #[arg(long, value_name = "REF", conflicts_with_all = ["to", "folder"])] |
| 48 | change: Option<String>, |
| 49 | }, |
| 50 | /// Put a local project on a host. |
| 51 | Publish { project: String }, |
| 52 | /// The project's git view: where git clones it and whether it's current. |
| 53 | Git { |
| 54 | /// `resume` starts a paused git view again. |
| 55 | #[arg(value_parser = ["resume"])] |
| 56 | action: Option<String>, |
| 57 | }, |
| 58 | /// Sign in to a host. |
| 59 | Login { |
| 60 | host: Option<String>, |
| 61 | /// Use this token instead of signing in through the browser. |
| 62 | #[arg(long)] |
| 63 | token: Option<String>, |
| 64 | }, |
| 65 | /// Where you are and what to do next. |
| 66 | Status { change: Option<String> }, |
| 67 | /// What a change changes. |
| 68 | Diff { |
| 69 | paths: Vec<String>, |
| 70 | #[arg(long)] |
| 71 | change: Option<String>, |
| 72 | #[arg(long)] |
| 73 | from: Option<String>, |
| 74 | #[arg(long)] |
| 75 | to: Option<String>, |
| 76 | #[arg(long)] |
| 77 | stat: bool, |
| 78 | }, |
| 79 | /// History of main, open changes, saves or operations. |
| 80 | Log { |
| 81 | /// A version number or a change. |
| 82 | what: Option<String>, |
| 83 | #[arg(long)] |
| 84 | changes: bool, |
| 85 | #[arg(long)] |
| 86 | saves: bool, |
| 87 | #[arg(long)] |
| 88 | ops: bool, |
| 89 | #[arg(short = 'n', default_value_t = 20)] |
| 90 | limit: i64, |
| 91 | }, |
| 92 | /// Start another change. The current one is kept. |
| 93 | New { |
| 94 | title: Option<String>, |
| 95 | /// Build on a change that hasn't landed yet. |
| 96 | #[arg(long)] |
| 97 | on: Option<String>, |
| 98 | }, |
| 99 | /// Set the current change's description. |
| 100 | Describe { |
| 101 | #[arg(required = true)] |
| 102 | parts: Vec<String>, |
| 103 | }, |
| 104 | /// Put this workspace on another change. |
| 105 | Switch { change: String }, |
| 106 | /// Abandon a change. |
| 107 | Drop { change: Option<String> }, |
| 108 | /// Bring the change up to date with main. |
| 109 | Sync, |
| 110 | /// Put a change on main. |
| 111 | Land { |
| 112 | change: Option<String>, |
| 113 | /// Description: the first is the title, the rest the body. |
| 114 | #[arg(short = 'm')] |
| 115 | message: Vec<String>, |
| 116 | /// Land even though the secret check flagged something (recorded). |
| 117 | #[arg(long)] |
| 118 | allow_secrets: bool, |
| 119 | }, |
| 120 | /// Undo the last vcs command here, or take a landed version back out. |
| 121 | Undo { version: Option<u64> }, |
| 122 | /// Bring files back from an earlier point. |
| 123 | Restore { |
| 124 | #[arg(required = true)] |
| 125 | paths: Vec<String>, |
| 126 | /// From your own earlier save: "10 min ago", "2h ago", 15:30. |
| 127 | #[arg(long)] |
| 128 | at: Option<String>, |
| 129 | /// From a version of main, a change, or change:save. |
| 130 | #[arg(long)] |
| 131 | from: Option<String>, |
| 132 | }, |
| 133 | /// See or set who can see a change or a path. |
| 134 | Access { |
| 135 | target: String, |
| 136 | who: Vec<String>, |
| 137 | /// Also let them work on the change. |
| 138 | #[arg(long)] |
| 139 | edit: bool, |
| 140 | }, |
| 141 | /// Print the guide for coding agents. |
| 142 | Guide, |
| 143 | #[command(hide = true)] |
| 144 | Saver, |
| 145 | } |
| 146 |
| 147 | fn run(app: &App, command: Command) -> Out { |
| 148 | match command { |
| 149 | Command::Get { project, folder, change, only } => cmd::get(app, &project, folder, change, only), |
| 150 | Command::Init { folder } => cmd::init(app, folder), |
| 151 | Command::Import { url, to, folder, change } => vcs_cli::remote::import(app, &url, to, folder, change), |
| 152 | Command::Publish { project } => vcs_cli::remote::publish(app, &project), |
| 153 | Command::Git { action } => vcs_cli::remote::git_view(app, action.is_some()), |
| 154 | Command::Login { host, token } => vcs_cli::remote::login(app, host, token), |
| 155 | Command::Status { change } => cmd::status(app, change), |
| 156 | Command::Diff { paths, change, from, to, stat } => cmd::diff_cmd(app, paths, change, from, to, stat), |
| 157 | Command::Log { what, changes, saves, ops, limit } => cmd::log(app, what, changes, saves, ops, limit), |
| 158 | Command::New { title, on } => cmd::new(app, title, on), |
| 159 | Command::Describe { parts } => cmd::describe(app, parts), |
| 160 | Command::Switch { change } => cmd::switch(app, &change), |
| 161 | Command::Drop { change } => cmd::drop(app, change), |
| 162 | Command::Sync => cmd::sync(app), |
| 163 | Command::Land { change, message, allow_secrets } => cmd::land(app, change, message, allow_secrets), |
| 164 | Command::Undo { version } => cmd::undo(app, version), |
| 165 | Command::Restore { paths, at, from } => cmd::restore(app, paths, at, from), |
| 166 | Command::Access { target, who, edit } => access::run(app, &target, who, edit), |
| 167 | Command::Guide => cmd::guide(), |
| 168 | Command::Saver => vcs_cli::remote::saver(app), |
| 169 | } |
| 170 | } |
| 171 |
| 172 | fn main() { |
| 173 | let cli = match Cli::try_parse() { |
| 174 | Ok(cli) => cli, |
| 175 | Err(e) => { |
| 176 | let code = if e.use_stderr() { 2 } else { 0 }; |
| 177 | let _ = e.print(); |
| 178 | std::process::exit(code); |
| 179 | } |
| 180 | }; |
| 181 | let json = cli.json || std::env::var("VCS_JSON").is_ok_and(|v| v == "1"); |
| 182 | let cwd = match cli.dir { |
| 183 | Some(d) => std::env::current_dir().map(|c| c.join(d)).unwrap_or_default(), |
| 184 | None => std::env::current_dir().unwrap_or_default(), |
| 185 | }; |
| 186 | let app = match App::open(json, cwd) { |
| 187 | Ok(app) => app, |
| 188 | Err(e) => { |
| 189 | Fail::from(e).print(json); |
| 190 | std::process::exit(1); |
| 191 | } |
| 192 | }; |
| 193 | let starts_saver = !matches!(cli.command, Command::Saver | Command::Guide | Command::Login { .. }); |
| 194 | let result = run(&app, cli.command); |
| 195 | if starts_saver { |
| 196 | vcs_cli::saver::ensure_running(); |
| 197 | } |
| 198 | let notes = app.notes.borrow().clone(); |
| 199 | match result { |
| 200 | Ok(mut report) => { |
| 201 | if !notes.is_empty() { |
| 202 | report.set("notes", notes.clone()); |
| 203 | } |
| 204 | report.print(json); |
| 205 | if !json { |
| 206 | for n in ¬es { |
| 207 | eprintln!("note: {n}"); |
| 208 | } |
| 209 | } |
| 210 | std::process::exit(report.exit); |
| 211 | } |
| 212 | Err(mut fail) => { |
| 213 | if !notes.is_empty() { |
| 214 | fail = fail.set("notes", notes.clone()); |
| 215 | } |
| 216 | fail.print(json); |
| 217 | if !json { |
| 218 | for n in ¬es { |
| 219 | eprintln!("note: {n}"); |
| 220 | } |
| 221 | } |
| 222 | std::process::exit(fail.exit); |
| 223 | } |
| 224 | } |
| 225 | } |
| 226 |