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 = vcs_cli::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 | /// Start on a new change on this release line, not main. |
| 36 | #[arg(long)] |
| 37 | line: Option<String>, |
| 38 | /// Fetch each file the first time it's opened, not all at once (Linux). |
| 39 | #[arg(long)] |
| 40 | lazy: bool, |
| 41 | }, |
| 42 | /// Start a lazy workspace's mount again (after a restart, say), or --stop it. |
| 43 | Mount { |
| 44 | folder: Option<PathBuf>, |
| 45 | #[arg(long)] |
| 46 | stop: bool, |
| 47 | #[arg(long, hide = true)] |
| 48 | serve: bool, |
| 49 | }, |
| 50 | /// Start a new project that lives only on this machine. |
| 51 | Init { folder: Option<PathBuf> }, |
| 52 | /// Make a project from a git repository, with its history. |
| 53 | Import { |
| 54 | /// A git repository: a folder, a URL, or upstream.codes/owner/name. |
| 55 | url: String, |
| 56 | folder: Option<PathBuf>, |
| 57 | /// Then put it on a host as host/owner/name. |
| 58 | #[arg(long)] |
| 59 | to: Option<String>, |
| 60 | /// Bring this git branch (or any commit) into this workspace's project as a change instead. |
| 61 | #[arg(long, value_name = "REF", conflicts_with_all = ["to", "folder"])] |
| 62 | change: Option<String>, |
| 63 | }, |
| 64 | /// Put a local project on a host. |
| 65 | Publish { project: String }, |
| 66 | /// The project's git view: where git clones it and whether it's current. |
| 67 | Git { |
| 68 | /// `resume` starts a paused git view again. |
| 69 | #[arg(value_parser = ["resume"])] |
| 70 | action: Option<String>, |
| 71 | }, |
| 72 | /// Sign in to a host. |
| 73 | Login { |
| 74 | host: Option<String>, |
| 75 | /// Use this token instead of signing in through the browser. |
| 76 | #[arg(long)] |
| 77 | token: Option<String>, |
| 78 | }, |
| 79 | /// Where you are and what to do next. |
| 80 | Status { change: Option<String> }, |
| 81 | /// What a change changes. |
| 82 | Diff { |
| 83 | paths: Vec<String>, |
| 84 | #[arg(long)] |
| 85 | change: Option<String>, |
| 86 | #[arg(long)] |
| 87 | from: Option<String>, |
| 88 | #[arg(long)] |
| 89 | to: Option<String>, |
| 90 | #[arg(long)] |
| 91 | stat: bool, |
| 92 | }, |
| 93 | /// History of main, open changes, saves or operations. |
| 94 | Log { |
| 95 | /// A version number, a change, or a path (the versions that changed it). |
| 96 | what: Option<String>, |
| 97 | #[arg(long)] |
| 98 | changes: bool, |
| 99 | #[arg(long)] |
| 100 | saves: bool, |
| 101 | #[arg(long)] |
| 102 | ops: bool, |
| 103 | #[arg(short = 'n', default_value_t = 20)] |
| 104 | limit: i64, |
| 105 | /// A release line's versions. |
| 106 | #[arg(long)] |
| 107 | line: Option<String>, |
| 108 | }, |
| 109 | /// Start another change. The current one is kept. |
| 110 | New { |
| 111 | title: Option<String>, |
| 112 | /// Build on a change that hasn't landed yet. |
| 113 | #[arg(long)] |
| 114 | on: Option<String>, |
| 115 | /// Start on a release line, not main: vcs land lands there. |
| 116 | #[arg(long)] |
| 117 | line: Option<String>, |
| 118 | }, |
| 119 | /// The project's release lines, or a new one from a version of main. |
| 120 | Line { |
| 121 | name: Option<String>, |
| 122 | /// The version of main (or tag) it starts from; main's latest by default. |
| 123 | #[arg(long)] |
| 124 | from: Option<String>, |
| 125 | }, |
| 126 | /// Set the current change's description. |
| 127 | Describe { |
| 128 | #[arg(required = true)] |
| 129 | parts: Vec<String>, |
| 130 | }, |
| 131 | /// Put this workspace on another change. |
| 132 | Switch { change: String }, |
| 133 | /// Abandon a change. |
| 134 | Drop { change: Option<String> }, |
| 135 | /// Bring the change up to date with main. |
| 136 | Sync, |
| 137 | /// Put a change on main. |
| 138 | Land { |
| 139 | change: Option<String>, |
| 140 | /// Description: the first is the title, the rest the body. |
| 141 | #[arg(short = 'm')] |
| 142 | message: Vec<String>, |
| 143 | /// Land even though the secret check flagged something (recorded). |
| 144 | #[arg(long)] |
| 145 | allow_secrets: bool, |
| 146 | /// Skip vcs.toml's [land] run commands this time. |
| 147 | #[arg(long)] |
| 148 | no_run: bool, |
| 149 | /// Land at a set time ("fri 9:00", "in 2h", "2026-11-02 09:00"); the host keeps it until then. |
| 150 | #[arg(long, value_name = "WHEN")] |
| 151 | at: Option<String>, |
| 152 | /// Other workspaces whose changes land with this one: all of them, or none. |
| 153 | #[arg(long = "with", value_name = "WORKSPACE")] |
| 154 | with: Vec<PathBuf>, |
| 155 | /// Land on this release line instead (a change that landed on main is brought over). |
| 156 | #[arg(long, value_name = "LINE")] |
| 157 | to: Option<String>, |
| 158 | }, |
| 159 | /// Undo the last vcs command here, or take a landed version back out. |
| 160 | Undo { |
| 161 | /// A version of main (48) or of a release line (1.x/3) to take back out. |
| 162 | version: Option<String>, |
| 163 | }, |
| 164 | /// Bring files back from an earlier point. |
| 165 | Restore { |
| 166 | #[arg(required = true)] |
| 167 | paths: Vec<String>, |
| 168 | /// From your own earlier save: "10 min ago", "2h ago", 15:30. |
| 169 | #[arg(long)] |
| 170 | at: Option<String>, |
| 171 | /// From a version of main, a change, or change:save. |
| 172 | #[arg(long)] |
| 173 | from: Option<String>, |
| 174 | }, |
| 175 | /// See or set who can see a change or a path. |
| 176 | Access { |
| 177 | target: String, |
| 178 | who: Vec<String>, |
| 179 | /// Also let them work on the change. |
| 180 | #[arg(long)] |
| 181 | edit: bool, |
| 182 | }, |
| 183 | /// Approve someone else's change (the last approval needed lands it). |
| 184 | Approve { change: String }, |
| 185 | /// The project's host settings, like `approvals=1`. |
| 186 | Settings { pairs: Vec<String> }, |
| 187 | /// Each line of a file in main, with the version that last changed it. |
| 188 | Blame { path: String }, |
| 189 | /// Print a file as it is in main, or in --from (a version, a change or change:save). |
| 190 | Show { |
| 191 | path: String, |
| 192 | #[arg(long)] |
| 193 | from: Option<String>, |
| 194 | }, |
| 195 | /// Print shell completions: bash, zsh, fish, powershell or elvish. |
| 196 | Completions { shell: clap_complete::Shell }, |
| 197 | /// Check that nothing is damaged: this machine's objects, and the host's copy. |
| 198 | Check, |
| 199 | /// Make this machine's signing key and add it to your account on the host: your lands are signed from then on. |
| 200 | Key, |
| 201 | /// Check who signed the land that made a version of main (the latest by default). |
| 202 | Verify { version: Option<String> }, |
| 203 | /// Find the version of main that broke something, by trying versions between a good one and a bad one. |
| 204 | Bisect { |
| 205 | /// A version (or tag) where it works. |
| 206 | good: String, |
| 207 | /// A version (or tag, or main) where it's broken. |
| 208 | bad: String, |
| 209 | /// The command that tells: exit 0 means good, 125 can't tell, anything else bad. |
| 210 | #[arg(long)] |
| 211 | run: String, |
| 212 | /// Write the versions it tries to this folder and keep it (a scratch folder otherwise). |
| 213 | #[arg(long = "in", value_name = "FOLDER")] |
| 214 | folder: Option<PathBuf>, |
| 215 | }, |
| 216 | /// Clean up what this machine keeps: saves of changes that landed or were dropped, and what nothing needs. |
| 217 | Gc { |
| 218 | /// How old a landed or dropped change's saves must be to go ("30d", "12h", "2w"). |
| 219 | #[arg(long, value_name = "AGE")] |
| 220 | older: Option<String>, |
| 221 | /// Say what would go, without removing anything. |
| 222 | #[arg(long)] |
| 223 | dry_run: bool, |
| 224 | }, |
| 225 | /// Agent sessions kept with the changes they land (only you see them): |
| 226 | /// list them, show one, turn it off or on, delete them. |
| 227 | Sessions { |
| 228 | #[command(subcommand)] |
| 229 | action: Option<vcs_cli::sessions::Action>, |
| 230 | }, |
| 231 | /// Take files or folders out of the project's whole history (a leaked |
| 232 | /// secret, a big file), deleting their content. Says what it would do |
| 233 | /// unless --yes; it can't be undone. |
| 234 | Purge { |
| 235 | #[arg(required = true)] |
| 236 | paths: Vec<String>, |
| 237 | /// Do it. |
| 238 | #[arg(long)] |
| 239 | yes: bool, |
| 240 | }, |
| 241 | /// This project's workspaces on this machine (one per agent): list, add, clean up. |
| 242 | Workspaces { |
| 243 | #[command(subcommand)] |
| 244 | action: Option<WorkspacesAction>, |
| 245 | }, |
| 246 | /// Put the whole hosted project back as it was at a time (owners; the last 30 days). |
| 247 | Rollback { |
| 248 | /// When: "2h ago", "yesterday", 2026-10-27 14:30. |
| 249 | #[arg(long, conflicts_with = "undo")] |
| 250 | to: Option<String>, |
| 251 | /// Reverse a rollback, with the bookmark it printed. |
| 252 | #[arg(long)] |
| 253 | undo: Option<String>, |
| 254 | }, |
| 255 | /// Name a version of main (main's latest when left out), list the names, or --remove one. |
| 256 | Tag { |
| 257 | name: Option<String>, |
| 258 | version: Option<String>, |
| 259 | #[arg(long)] |
| 260 | remove: bool, |
| 261 | }, |
| 262 | /// The project's groups, for `vcs access <path> @group`. |
| 263 | Group { |
| 264 | name: Option<String>, |
| 265 | /// Its people (this replaces them). |
| 266 | members: Vec<String>, |
| 267 | /// Remove the group. |
| 268 | #[arg(long)] |
| 269 | remove: bool, |
| 270 | }, |
| 271 | /// Print the guide for coding agents. |
| 272 | Guide, |
| 273 | #[command(hide = true)] |
| 274 | Saver, |
| 275 | } |
| 276 |
| 277 | #[derive(Subcommand)] |
| 278 | enum WorkspacesAction { |
| 279 | /// Another workspace (for another agent): a new change on main, or on --on <change> (`.` is this one's). |
| 280 | Add { |
| 281 | folder: PathBuf, |
| 282 | #[arg(long)] |
| 283 | on: Option<String>, |
| 284 | }, |
| 285 | /// Remove workspaces whose work is done: saved, empty and unused for --idle. |
| 286 | Clean { |
| 287 | #[arg(long, default_value = "2h")] |
| 288 | idle: String, |
| 289 | }, |
| 290 | } |
| 291 |
| 292 | fn run(app: &App, command: Command) -> Out { |
| 293 | match command { |
| 294 | Command::Get { project, folder, change, only, line, lazy } => cmd::get(app, &project, folder, change, only, None, line, lazy), |
| 295 | Command::Mount { folder, stop, serve } => vcs_cli::lazy::run(app, folder, stop, serve), |
| 296 | Command::Init { folder } => cmd::init(app, folder), |
| 297 | Command::Import { url, to, folder, change } => vcs_cli::remote::import(app, &url, to, folder, change), |
| 298 | Command::Publish { project } => vcs_cli::remote::publish(app, &project), |
| 299 | Command::Git { action } => vcs_cli::remote::git_view(app, action.is_some()), |
| 300 | Command::Login { host, token } => vcs_cli::remote::login(app, host, token), |
| 301 | Command::Status { change } => cmd::status(app, change), |
| 302 | Command::Diff { paths, change, from, to, stat } => cmd::diff_cmd(app, paths, change, from, to, stat), |
| 303 | Command::Log { what, changes, saves, ops, limit, line } => match line { |
| 304 | Some(l) => app.session().and_then(|s| vcs_cli::lines::log(app, &s.project, &l, limit.max(1) as usize)), |
| 305 | None => cmd::log(app, what, changes, saves, ops, limit), |
| 306 | }, |
| 307 | Command::New { title, on, line } => cmd::new(app, title, on, line), |
| 308 | Command::Line { name, from } => vcs_cli::lines::run(app, name, from), |
| 309 | Command::Describe { parts } => cmd::describe(app, parts), |
| 310 | Command::Switch { change } => cmd::switch(app, &change), |
| 311 | Command::Drop { change } => cmd::drop(app, change), |
| 312 | Command::Sync => cmd::sync(app), |
| 313 | Command::Land { change, message, allow_secrets, no_run, at, with, to } => cmd::land(app, change, message, allow_secrets, !no_run, at, with, to), |
| 314 | Command::Undo { version } => cmd::undo(app, version), |
| 315 | Command::Restore { paths, at, from } => cmd::restore(app, paths, at, from), |
| 316 | Command::Access { target, who, edit } => access::run(app, &target, who, edit), |
| 317 | Command::Approve { change } => vcs_cli::remote::approve(app, &change), |
| 318 | Command::Settings { pairs } => vcs_cli::remote::settings(app, pairs), |
| 319 | Command::Blame { path } => vcs_cli::history::blame_cmd(app, path), |
| 320 | Command::Show { path, from } => cmd::show(app, path, from), |
| 321 | Command::Completions { shell } => { |
| 322 | clap_complete::generate(shell, &mut <Cli as clap::CommandFactory>::command(), "vcs", &mut std::io::stdout()); |
| 323 | Ok(vcs_cli::out::Report::new()) |
| 324 | } |
| 325 | Command::Check => vcs_cli::remote::check(app), |
| 326 | Command::Gc { older, dry_run } => vcs_cli::gc::run(app, older, dry_run), |
| 327 | Command::Purge { paths, yes } => vcs_cli::purge::run(app, paths, yes), |
| 328 | Command::Sessions { action } => vcs_cli::sessions::run(app, action), |
| 329 | Command::Key => vcs_cli::keys::run(app), |
| 330 | Command::Verify { version } => vcs_cli::keys::verify(app, version), |
| 331 | Command::Bisect { good, bad, run, folder } => cmd::bisect(app, good, bad, run, folder), |
| 332 | Command::Workspaces { action: None } => vcs_cli::workspaces::list(app), |
| 333 | Command::Workspaces { action: Some(WorkspacesAction::Add { folder, on }) } => vcs_cli::workspaces::add(app, folder, on), |
| 334 | Command::Workspaces { action: Some(WorkspacesAction::Clean { idle }) } => vcs_cli::workspaces::clean(app, &idle), |
| 335 | Command::Rollback { to, undo } => vcs_cli::remote::rollback(app, to, undo), |
| 336 | Command::Tag { name, version, remove } => vcs_cli::tags::run(app, name, version, remove), |
| 337 | Command::Group { name, members, remove } => vcs_cli::remote::group(app, name, members, remove), |
| 338 | Command::Guide => cmd::guide(), |
| 339 | Command::Saver => vcs_cli::remote::saver(app), |
| 340 | } |
| 341 | } |
| 342 |
| 343 | fn main() { |
| 344 | let cli = match Cli::try_parse() { |
| 345 | Ok(cli) => cli, |
| 346 | Err(e) => { |
| 347 | let code = if e.use_stderr() { 2 } else { 0 }; |
| 348 | let _ = e.print(); |
| 349 | std::process::exit(code); |
| 350 | } |
| 351 | }; |
| 352 | let json = cli.json || std::env::var("VCS_JSON").is_ok_and(|v| v == "1"); |
| 353 | let cwd = match cli.dir { |
| 354 | Some(d) => std::env::current_dir().map(|c| c.join(d)).unwrap_or_default(), |
| 355 | None => std::env::current_dir().unwrap_or_default(), |
| 356 | }; |
| 357 | let app = match App::open(json, cwd) { |
| 358 | Ok(app) => app, |
| 359 | Err(e) => { |
| 360 | Fail::from(e).print(json); |
| 361 | std::process::exit(1); |
| 362 | } |
| 363 | }; |
| 364 | let starts_saver = !matches!(cli.command, Command::Saver | Command::Guide | Command::Login { .. } | Command::Completions { .. }); |
| 365 | let result = run(&app, cli.command); |
| 366 | if starts_saver { |
| 367 | vcs_cli::saver::ensure_running(); |
| 368 | } |
| 369 | let notes = app.notes.borrow().clone(); |
| 370 | match result { |
| 371 | Ok(mut report) => { |
| 372 | if !notes.is_empty() { |
| 373 | report.set("notes", notes.clone()); |
| 374 | } |
| 375 | report.print(json); |
| 376 | if !json { |
| 377 | for n in ¬es { |
| 378 | eprintln!("note: {n}"); |
| 379 | } |
| 380 | } |
| 381 | std::process::exit(report.exit); |
| 382 | } |
| 383 | Err(mut fail) => { |
| 384 | if !notes.is_empty() { |
| 385 | fail = fail.set("notes", notes.clone()); |
| 386 | } |
| 387 | fail.print(json); |
| 388 | if !json { |
| 389 | for n in ¬es { |
| 390 | eprintln!("note: {n}"); |
| 391 | } |
| 392 | } |
| 393 | std::process::exit(fail.exit); |
| 394 | } |
| 395 | } |
| 396 | } |
| 397 |