-
Notifications
You must be signed in to change notification settings - Fork 162
Expand file tree
/
Copy pathcli.rs
More file actions
3615 lines (3405 loc) · 148 KB
/
Copy pathcli.rs
File metadata and controls
3615 lines (3405 loc) · 148 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! CLI entry for Libra.
//!
//! Defines the clap subcommand grammar, performs cross-cutting preflight (locating the
//! repository database and pinning the global hash algorithm to whatever is recorded
//! in `core.objectformat`), and dispatches every parsed command to its `command::*`
//! handler.
//!
//! Because every subcommand reads or writes objects whose hash kind must match the
//! repository's recorded `core.objectformat`, this module is the single point where
//! that global is configured before any handler runs.
use std::{
env,
io::Write,
path::Path,
time::{Duration, Instant},
};
use clap::{
CommandFactory, Parser, Subcommand,
error::{ContextKind, ContextValue, ErrorKind},
};
use git_internal::hash::{HashKind, set_hash_kind};
use sea_orm::{ConnectionTrait, Statement};
use crate::{
command,
command::code::ControlMode,
internal::{config::ConfigKv, db},
utils,
utils::{
error::{CliError, CliResult},
output::OutputConfig,
},
};
// The `media` command (lore.md §6) is a cfg-gated `Commands` variant, so its
// Command-Groups entry must appear ONLY under `--features fastcdc` — otherwise
// the default-features compat matrix (which cross-checks every listed command
// against the real CLI surface) would see a command that does not exist. A
// cfg-selected macro fragment splices it into the Working Tree row.
#[cfg(feature = "fastcdc")]
macro_rules! media_group_entry {
() => {
", media"
};
}
#[cfg(not(feature = "fastcdc"))]
macro_rules! media_group_entry {
() => {
""
};
}
const ROOT_AFTER_HELP: &str = concat!(
"\
Command Groups:
Repository Setup init, clone, config, completions
Working Tree status, add, rm, mv, restore, clean, stash, dirty, layer, sparse-view, hydrate",
media_group_entry!(),
", lfs, ls-files, check-ignore, check-attr, check-mailmap, worktree
History Inspection log, shortlog, show, show-ref, format-patch, ls-remote, ls-tree, diff, grep, blame, describe, notes, archive, revision
Commit And Branching commit, branch, switch, checkout, tag, merge, rebase, reset, cherry-pick, revert, am, rerere, metadata
Remote And Cloud remote, fetch, pull, push, open, cloud, cache, publish, credential, bundle, auth, login, logout, whoami
AI And Automation code, code-control, automation, usage, graph, sandbox, agent, review, investigate, service
Maintenance And Plumbing fsck, maintenance, repack, logfile, cat-file, hash-object, write-tree, read-tree, update-index, update-ref, merge-file, merge-base, apply, mailinfo, diff-tree, diff-index, diff-files, fast-export, fast-import, replace, verify-pack, rev-parse, rev-list, symbolic-ref, reflog, bisect, for-each-ref, commit-tree, file, alternates, deps
Help Topics:
error-codes Print the stable CLI error code table (`libra help error-codes`)
Output Examples:
libra --json status Pretty JSON envelope on stdout
libra --json=ndjson log One-line-per-event newline-delimited JSON
libra --machine status Compact JSON; suppresses progress/decoration
libra --quiet --exit-code-on-warning Silent run; non-zero exit (9) if warnings occurred
libra --color=never log Force-disable colors (also via NO_COLOR=1)
For per-command flags, see `libra <cmd> --help`.
"
);
const ERROR_CODES_HELP: &str = include_str!("../docs/error-codes.md");
const CLOUD_GLOBAL_CONFIG_KEYS: &[&str] = &[
"LIBRA_D1_ACCOUNT_ID",
"LIBRA_D1_API_TOKEN",
"LIBRA_D1_DATABASE_ID",
];
// Libra's CLI dispatcher mutates process-global CWD/hash/output state and the
// object-index queue is process-global as well. Serializing public `exec_async`
// invocations keeps each command's drain/warning accounting isolated instead of
// attributing a sibling invocation's terminal index failure to the wrong caller.
static CLI_INVOCATION_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
/// Read the repository's `core.objectformat` and pin the global hash algorithm.
///
/// Functional scope:
/// - Opens the SQLite database at `<storage>/<DATABASE>` and reads
/// `core.objectformat`, defaulting to `"sha1"` when the row is absent.
/// - Calls `git_internal::hash::set_hash_kind` so every object hashed by the rest of
/// the process matches the repository's storage format.
///
/// Boundary conditions:
/// - Returns a fatal error when the database file is missing — every non-`init`,
/// non-`clone` command requires a repository, and silently continuing would hash
/// objects with the wrong algorithm.
/// - Returns a fatal error when the database cannot be opened (permissions, disk
/// corruption) so the user sees the underlying message instead of a downstream
/// panic.
/// - Currently accepts only `"sha1"` and `"sha256"`; anything else is rejected with a
/// fatal error.
async fn set_local_hash_kind_for_storage(storage: &Path) -> CliResult<()> {
let db_path = storage.join(utils::util::DATABASE);
if !db_path.exists() {
return Err(CliError::fatal(format!(
"repository database not found at '{}'",
db_path.display()
)));
}
let db_conn = db::get_db_conn_instance_for_path(&db_path)
.await
.map_err(|e| {
CliError::fatal(format!(
"failed to open repository database '{}': {}",
db_path.display(),
e
))
})?;
let object_format = ConfigKv::get_with_conn(&db_conn, "core.objectformat")
.await
.map_err(|e| {
CliError::fatal(format!(
"failed to read core.objectformat from repository database '{}': {}",
db_path.display(),
e
))
})?
.map(|e| e.value)
.unwrap_or_else(|| "sha1".to_string());
set_hash_kind_from_object_format(object_format)
}
async fn set_local_hash_kind_for_storage_without_schema_guard(storage: &Path) -> CliResult<()> {
let db_path = storage.join(utils::util::DATABASE);
if !db_path.exists() {
return Err(CliError::fatal(format!(
"repository database not found at '{}'",
db_path.display()
)));
}
let db_conn = db::open_database_without_migrations(&db_path)
.await
.map_err(|e| {
CliError::fatal(format!(
"failed to open repository database '{}': {}",
db_path.display(),
e
))
})?;
let object_format = read_schema_free_object_format(&db_conn, &db_path).await?;
set_hash_kind_from_object_format(object_format)
}
async fn read_schema_free_object_format(
db_conn: &sea_orm::DatabaseConnection,
db_path: &Path,
) -> CliResult<String> {
let has_config_kv = db_conn
.query_one_raw(Statement::from_sql_and_values(
db_conn.get_database_backend(),
"SELECT 1 FROM sqlite_master WHERE type = ? AND name = ? LIMIT 1",
["table".into(), "config_kv".into()],
))
.await
.map_err(|e| {
CliError::fatal(format!(
"failed to inspect repository database '{}': {}",
db_path.display(),
e
))
})?
.is_some();
if !has_config_kv {
return Ok("sha1".to_string());
}
let row = db_conn
.query_one_raw(Statement::from_sql_and_values(
db_conn.get_database_backend(),
"SELECT value FROM config_kv WHERE key = ? ORDER BY id DESC LIMIT 1",
["core.objectformat".into()],
))
.await
.map_err(|e| {
CliError::fatal(format!(
"failed to read core.objectformat from repository database '{}': {}",
db_path.display(),
e
))
})?;
match row {
Some(row) => row.try_get_by_index(0).map_err(|e| {
CliError::fatal(format!(
"failed to decode core.objectformat from repository database '{}': {}",
db_path.display(),
e
))
}),
None => Ok("sha1".to_string()),
}
}
fn set_hash_kind_from_object_format(object_format: String) -> CliResult<()> {
let hash_kind = match object_format.as_str() {
"sha1" => HashKind::Sha1,
"sha256" => HashKind::Sha256,
_ => {
return Err(CliError::fatal(format!(
"unsupported object format: '{object_format}'"
)));
}
};
set_hash_kind(hash_kind);
Ok(())
}
// The Cli struct represents the root of the command line interface.
#[derive(Parser, Debug)]
#[command(
about = "Libra: An AI native version control system for monorepo and trunk-based development.",
version = env!("CARGO_PKG_VERSION"),
after_help = ROOT_AFTER_HELP,
arg_required_else_help = true,
)]
pub(crate) struct Cli {
/// Emit machine-readable JSON to stdout.
/// Use `--json` alone for pretty output, or `--json=compact` / `--json=ndjson`
/// to select an alternative layout. The `=` is required when specifying a format
/// so that the subcommand name is not consumed as the value.
#[arg(
long,
short = 'J',
global = true,
value_name = "FORMAT",
num_args = 0..=1,
require_equals = true,
default_missing_value = "pretty",
value_parser = ["pretty", "compact", "ndjson"],
)]
json: Option<String>,
/// Strict machine mode.
/// Implies --json=ndjson --no-pager --color=never --quiet.
/// Disables all prompts and decorative text.
#[arg(long, global = true)]
machine: bool,
/// Disable automatic pager (less) for long output.
#[arg(long, global = true)]
no_pager: bool,
/// When to use terminal colors.
/// Also respects the NO_COLOR environment variable (see <https://no-color.org>).
#[arg(
long,
global = true,
value_name = "WHEN",
default_value = "auto",
value_parser = ["auto", "never", "always"],
)]
color: String,
/// Disable terminal colors.
/// Equivalent to --color=never and takes precedence over --color.
#[arg(long, global = true)]
no_color: bool,
/// Suppress standard stdout output; keep warnings/errors on stderr.
/// This includes primary command results, unlike some Git per-command
/// `--quiet` flags that only suppress informational chatter.
#[arg(long, short = 'q', global = true)]
quiet: bool,
/// Return non-zero exit code (exit 9) when a warning is emitted.
#[arg(long, global = true)]
exit_code_on_warning: bool,
/// Control progress output for long-running operations.
/// `json` emits NDJSON progress events; `text` shows a human-friendly bar;
/// `none` suppresses progress entirely.
#[arg(
long,
global = true,
value_name = "MODE",
default_value = "auto",
value_parser = ["json", "text", "none", "auto"],
)]
progress: String,
/// fsync object writes (and their parent directories) for power-loss
/// durability, at the cost of write throughput. Recovery-critical sequencer
/// state is always fsynced regardless of this flag. Also settable via
/// `LIBRA_SYNC_DATA=1`.
#[arg(long, global = true)]
sync_data: bool,
/// Read objects from the local store only; never fetch from the configured
/// durable tier (a needed remote object becomes a clear error). This is
/// Libra's spelling of Lore's `--offline`/`--local` read policy as a single
/// collision-free global flag (a global `--local`/`--remote` would clash with
/// `config`/`clone`/`agent` options). For the `remote`-refresh policy use
/// `LIBRA_READ_POLICY=remote`. No-op for local-only repositories.
#[arg(long, global = true)]
offline: bool,
/// Maximum number of concurrent remote connections/requests (bounds fan-out
/// on large repos / CI so connections are not exhausted). A positive integer;
/// `0` is treated as `1`. Also settable via `LIBRA_MAX_CONNECTIONS`
/// (flag wins). Default 16. No-op for purely local operations.
#[arg(long, global = true, value_name = "N")]
max_connections: Option<usize>,
#[command(subcommand)]
command: Commands,
}
/// The Commands enum represents the subcommands that can be used with the CLI.
/// subcommand's execute and args are defined in `command` module
#[derive(Subcommand, Debug)]
enum Commands {
// Each variant of the enum represents a subcommand.
// The about attribute provides a brief description of the subcommand.
// The arguments of the subcommand are defined in the command module.
#[command(about = "Initialize a new repository")]
Init(command::init::InitArgs),
#[command(about = "Clone a repository into a new directory")]
Clone(command::clone::CloneArgs),
#[command(about = "Manage repository configurations", alias = "cfg")]
Config(command::config::ConfigArgs),
#[command(about = "Show the working tree status", alias = "st")]
Status(command::status::StatusArgs),
#[command(about = "Add file contents to the index")]
Add(command::add::AddArgs),
#[command(
about = "Remove files from the working tree and from the index",
alias = "remove",
alias = "delete"
)]
Rm(command::remove::RemoveArgs),
#[command(about = "Move or rename a file, a directory, or a symlink")]
Mv(command::mv::MvArgs),
#[command(about = "Restore working tree files", alias = "unstage")]
Restore(command::restore::RestoreArgs),
#[command(about = "Remove untracked files from the working tree")]
Clean(command::clean::CleanArgs),
#[command(
subcommand,
about = "Stash the changes in a dirty working directory away",
after_help = command::stash::STASH_EXAMPLES
)]
Stash(Stash),
#[command(
subcommand,
about = "Large File Storage",
after_help = command::lfs::LFS_EXAMPLES
)]
Lfs(command::lfs::LfsCmds),
#[command(
about = "Show information about tracked and untracked files",
after_help = command::ls_files::LS_FILES_EXAMPLES
)]
LsFiles(command::ls_files::LsFilesArgs),
#[command(
about = "Manage multiple working trees attached to this repository",
alias = "wt",
after_help = command::worktree::WORKTREE_EXAMPLES
)]
Worktree(command::worktree::WorktreeArgs),
#[command(about = "Show commit logs", alias = "hist", alias = "history")]
Log(command::log::LogArgs),
#[command(
about = "Inspect the tracing log-file configuration",
after_help = command::logfile::LOGFILE_EXAMPLES
)]
Logfile(command::logfile::LogfileArgs),
#[command(
about = "Inspect the tiered-storage / LRU cache configuration",
after_help = command::cache::CACHE_EXAMPLES
)]
Cache(command::cache::CacheArgs),
#[command(
about = "Manage local, never-committed working-tree overlays (Libra extension)",
after_help = command::layer::LAYER_EXAMPLES
)]
Layer(command::layer::LayerArgs),
#[command(
about = "Object-level operations incl. payload obliteration (Libra extension)",
after_help = command::file::FILE_EXAMPLES
)]
File(command::file::FileArgs),
#[command(
about = "Manage object alternates — borrow objects from a shared store (Libra extension)",
after_help = command::alternates::ALTERNATES_EXAMPLES
)]
Alternates(command::alternates::AlternatesArgs),
#[command(
about = "Manage the file dependency graph (Libra extension)",
after_help = command::deps::DEPS_EXAMPLES
)]
Deps(command::deps::DepsArgs),
#[command(
about = "Hydrate working-tree content on demand (Libra extension)",
after_help = command::hydrate::HYDRATE_EXAMPLES
)]
Hydrate(command::hydrate::HydrateArgs),
#[cfg(feature = "fastcdc")]
#[command(
about = "FastCDC LFS media chunking client (Libra extension, lore.md §6)",
after_help = command::media::MEDIA_EXAMPLES
)]
Media(command::media::MediaArgs),
#[command(
name = "sparse-view",
about = "Manage the read-only sparse view filter over ls-files/diff (Libra extension)",
after_help = command::sparse_view::SPARSE_VIEW_EXAMPLES
)]
SparseView(command::sparse_view::SparseViewArgs),
#[command(
about = "Branch/repo metadata key-value store (Libra extension)",
after_help = command::metadata::METADATA_EXAMPLES
)]
Metadata(command::metadata::MetadataArgs),
#[command(
about = "Mark paths dirty in the dirty-set cache, or list it (Libra extension)",
after_help = command::dirty::DIRTY_EXAMPLES
)]
Dirty(command::dirty::DirtyArgs),
#[command(
about = "Manage host-scoped HTTP tokens: login, status, logout (Libra extension)",
after_help = command::auth::AUTH_EXAMPLES
)]
Auth(command::auth::AuthArgs),
#[command(about = "Log in to Libra website account via browser")]
Login(command::account::LoginArgs),
#[command(about = "Show the current Libra website account session")]
Whoami(command::account::WhoamiArgs),
#[command(about = "Log out of the Libra website account session")]
Logout(command::account::LogoutArgs),
#[command(
about = "Look up revisions by ordinal on a branch's first-parent chain (Libra extension)",
after_help = command::revision::REVISION_EXAMPLES
)]
Revision(command::revision::RevisionArgs),
#[command(
about = "Run a headless local service: notification bus + dirty-mark ingestion (Libra extension)",
after_help = command::service::SERVICE_EXAMPLES
)]
Service(command::service::ServiceArgs),
#[command(about = "Summarize commit history by author", alias = "slog")]
Shortlog(command::shortlog::ShortlogArgs),
#[command(about = "Show various types of objects")]
Show(command::show::ShowArgs),
#[command(about = "List references in a local repository")]
ShowRef(command::show_ref::ShowRefArgs),
#[command(
about = "Generate mbox-formatted patch files from commits",
after_help = command::format_patch::FORMAT_PATCH_EXAMPLES
)]
FormatPatch(command::format_patch::FormatPatchArgs),
#[command(
about = "Apply plain-text format-patch mail messages",
after_help = command::am::AM_EXAMPLES
)]
Am(command::am::AmArgs),
#[command(
about = "Extract metadata, message, and patch from one mail",
after_help = command::mailinfo::MAILINFO_EXAMPLES
)]
Mailinfo(command::mailinfo::MailinfoArgs),
#[command(
about = "Iterate over refs in a local repository with formatting and filtering",
after_help = command::for_each_ref::FOR_EACH_REF_EXAMPLES
)]
ForEachRef(command::for_each_ref::ForEachRefArgs),
#[command(about = "List references in a remote repository")]
LsRemote(command::ls_remote::LsRemoteArgs),
#[command(
about = "List the contents of a tree object",
after_help = command::ls_tree::LS_TREE_EXAMPLES
)]
LsTree(command::ls_tree::LsTreeArgs),
#[command(about = "Read or update the symbolic HEAD ref")]
SymbolicRef(command::symbolic_ref::SymbolicRefArgs),
#[command(about = "Parse and normalize revision names and repository paths")]
RevParse(command::rev_parse::RevParseArgs),
#[command(about = "List commit objects reachable from a revision")]
RevList(command::rev_list::RevListArgs),
#[command(about = "Show changes between commits, commit and working tree, etc")]
Diff(command::diff::DiffArgs),
#[command(about = "Search for patterns in tracked files")]
Grep(command::grep::GrepArgs),
#[command(about = "Show author and history of each line of a file")]
Blame(command::blame::BlameArgs),
#[command(
about = "Give an object a human readable name based on an available ref",
alias = "desc"
)]
Describe(command::describe::DescribeArgs),
#[command(
about = "Add, show, list, or remove notes attached to commits",
after_help = command::notes::NOTES_EXAMPLES
)]
Notes(command::notes::NotesArgs),
#[command(about = "Provide content, type or size info for repository objects")]
CatFile(command::cat_file::CatFileArgs),
#[command(
about = "Report pathnames excluded by Git/Libra ignore rules",
after_help = command::check_ignore::CHECK_IGNORE_EXAMPLES
)]
CheckIgnore(command::check_ignore::CheckIgnoreArgs),
#[command(
about = "Report Git/Libra attributes for pathnames",
after_help = command::check_attr::CHECK_ATTR_EXAMPLES
)]
CheckAttr(command::check_attr::CheckAttrArgs),
#[command(
about = "Resolve Name <email> contacts through .mailmap",
after_help = command::check_mailmap::CHECK_MAILMAP_EXAMPLES
)]
CheckMailmap(command::check_mailmap::CheckMailmapArgs),
#[command(
about = "Emit history as a fast-import stream (git fast-export)",
after_help = command::fast_export::FAST_EXPORT_EXAMPLES
)]
FastExport(command::fast_export::FastExportArgs),
#[command(
about = "Create and inspect Git v2 bundle files",
after_help = command::bundle::BUNDLE_EXAMPLES
)]
Bundle(command::bundle::BundleArgs),
#[command(
about = "Import a git fast-import stream",
after_help = command::fast_import::FAST_IMPORT_EXAMPLES
)]
FastImport(command::fast_import::FastImportArgs),
#[command(
about = "Generate a shell completion script",
after_help = command::completions::COMPLETIONS_EXAMPLES
)]
Completions(command::completions::CompletionsArgs),
#[command(
about = "Create an archive of files from a named tree",
after_help = command::archive::ARCHIVE_EXAMPLES
)]
Archive(command::archive::ArchiveArgs),
#[command(about = "Compute Git-compatible object IDs")]
HashObject(command::hash_object::HashObjectArgs),
#[command(
about = "Write the current index out as a tree object",
after_help = command::write_tree::WRITE_TREE_EXAMPLES
)]
WriteTree(command::write_tree::WriteTreeArgs),
#[command(
about = "Create a commit object from an existing tree (plumbing; no ref updates)",
after_help = command::commit_tree::COMMIT_TREE_EXAMPLES,
name = "commit-tree"
)]
CommitTree(command::commit_tree::CommitTreeArgs),
#[command(
about = "Read a tree object into the index",
after_help = command::read_tree::READ_TREE_EXAMPLES
)]
ReadTree(command::read_tree::ReadTreeArgs),
#[command(
about = "Modify the index directly (add/remove/cacheinfo)",
after_help = command::update_index::UPDATE_INDEX_EXAMPLES
)]
UpdateIndex(command::update_index::UpdateIndexArgs),
#[command(
about = "Safely update, create, or delete a refs/heads/<branch> ref",
after_help = command::update_ref::UPDATE_REF_EXAMPLES
)]
UpdateRef(command::update_ref::UpdateRefArgs),
#[command(about = "Validate pack index files against pack archives")]
VerifyPack(command::verify_pack::VerifyPackArgs),
#[command(about = "Record changes to the repository", alias = "ci")]
Commit(command::commit::CommitArgs),
#[command(about = "List, create, or delete branches", alias = "br")]
Branch(command::branch::BranchArgs),
#[command(about = "Switch branches", alias = "sw")]
Switch(command::switch::SwitchArgs),
#[command(
about = "Branch compatibility surface; prefer 'switch' for branches and 'restore' for files"
)]
Checkout(command::checkout::CheckoutArgs),
#[command(about = "Create a new tag")]
Tag(command::tag::TagArgs),
#[command(about = "Merge changes")]
Merge(command::merge::MergeArgs),
#[command(
about = "Three-way merge files (git merge-file)",
after_help = command::merge_file::MERGE_FILE_EXAMPLES
)]
MergeFile(command::merge_file::MergeFileArgs),
#[command(
about = "Find the best common ancestor(s) of two commits",
after_help = command::merge_base::MERGE_BASE_EXAMPLES
)]
MergeBase(command::merge_base::MergeBaseArgs),
#[command(
about = "Check whether a patch applies (git apply --check)",
after_help = command::apply::APPLY_EXAMPLES
)]
Apply(command::apply::ApplyArgs),
#[command(
about = "Diff between two trees (git diff-tree)",
after_help = command::diff_plumbing::DIFF_TREE_EXAMPLES
)]
DiffTree(command::diff_plumbing::DiffTreeArgs),
#[command(
about = "Diff a tree against the working tree (git diff-index)",
after_help = command::diff_plumbing::DIFF_INDEX_EXAMPLES
)]
DiffIndex(command::diff_plumbing::DiffIndexArgs),
#[command(
about = "Diff the index against the working tree (git diff-files)",
after_help = command::diff_plumbing::DIFF_FILES_EXAMPLES
)]
DiffFiles(command::diff_plumbing::DiffFilesArgs),
#[command(
about = "Vault-backed Git credential helper (fill/store/erase)",
after_help = command::credential::CREDENTIAL_EXAMPLES
)]
Credential(command::credential::CredentialArgs),
#[command(
about = "Reuse recorded conflict resolutions (git rerere)",
after_help = command::rerere::RERERE_EXAMPLES
)]
Rerere(command::rerere::RerereArgs),
#[command(about = "Reapply commits on top of another base tip", alias = "rb")]
Rebase(command::rebase::RebaseArgs),
#[command(about = "Reset current HEAD to specified state")]
Reset(command::reset::ResetArgs),
#[command(
about = "Apply the changes introduced by some existing commits",
alias = "cp"
)]
CherryPick(command::cherry_pick::CherryPickArgs),
#[command(about = "Update remote refs along with associated objects")]
Push(command::push::PushArgs),
#[command(about = "Download objects and refs from another repository")]
Fetch(command::fetch::FetchArgs),
#[command(about = "Fetch from and integrate with another repository or a local branch")]
Pull(command::pull::PullArgs),
#[command(about = "Verify the integrity of objects, refs, and index")]
Fsck(command::fsck::FsckArgs),
#[command(
about = "Run tasks to optimize Git repository data",
after_help = command::maintenance::MAINTENANCE_EXAMPLES
)]
Maintenance(command::maintenance::MaintenanceArgs),
#[command(
about = "Combine repository objects into a single pack",
after_help = command::repack::REPACK_EXAMPLES
)]
Repack(command::repack::RepackArgs),
#[command(about = "Revert some existing commits")]
Revert(command::revert::RevertArgs),
#[command(
about = "Create, list, or delete object replacements (refs/replace)",
after_help = command::replace::REPLACE_EXAMPLES
)]
Replace(command::replace::ReplaceArgs),
#[command(about = "Manage the log of reference changes (e.g., HEAD, branches)")]
Reflog(command::reflog::ReflogArgs),
#[command(about = "View and restore command-level operation history")]
Op(command::op::OpArgs),
#[command(
subcommand,
about = "Use binary search to find the commit that introduced a bug",
after_help = command::bisect::BISECT_EXAMPLES
)]
Bisect(Bisect),
#[command(
subcommand,
about = "Manage set of tracked repositories",
after_help = command::remote::REMOTE_EXAMPLES
)]
Remote(command::remote::RemoteCmds),
#[command(about = "Open the repository in the browser")]
Open(command::open::OpenArgs),
#[command(about = "Cloud backup and restore operations (D1/R2)")]
Cloud(command::cloud::CloudArgs),
#[command(about = "Manage read-only Cloudflare Worker publishing")]
Publish(command::publish::PublishArgs),
#[command(about = "Start Libra Code interactive TUI (with background web server)")]
Code(command::code::CodeArgs),
#[command(
about = "Deprecated forwarding shim for Code automation control (use `libra code --control stdio`; deleted in W5-01)"
)]
CodeControl(command::code_control::CodeControlArgs),
#[command(about = "Manage AI automation rules and history")]
Automation(command::automation::AutomationArgs),
#[command(about = "Report AI provider/model usage")]
Usage(command::usage::UsageArgs),
#[command(
about = "Inspect an AI thread version graph (Web Code UI; interactive TUI deprecated)"
)]
Graph(command::graph::GraphArgs),
#[command(about = "Inspect AI sandbox diagnostics")]
Sandbox(command::sandbox::SandboxArgs),
#[command(about = "Manage external-agent capture (Claude Code, Gemini, …)")]
Agent(command::agent::AgentArgs),
#[command(about = "Run read-only external-agent code reviews (AG-22)")]
Review(command::agent::review::ReviewArgs),
#[command(about = "Run read-only round-robin agent investigations (AG-23)")]
Investigate(command::agent::investigate::InvestigateArgs),
#[command(
about = "Build pack index file for an existing packed archive",
hide = true
)]
IndexPack(command::index_pack::IndexPackArgs),
#[command(
about = "Create a pack from object ids read on stdin (internal plumbing)",
after_help = command::pack_objects::PACK_OBJECTS_EXAMPLES,
hide = true
)]
PackObjects(command::pack_objects::PackObjectsArgs),
#[command(
about = "Compatibility entry for hook configurations installed by `libra agent enable`",
hide = true
)]
Hooks(command::hooks::HooksArgs),
}
#[derive(Subcommand, Debug)]
pub enum Stash {
#[command(about = "Save your local modifications to a new stash")]
Push {
#[arg(short, long, help = "The message to display for the stash")]
message: Option<String>,
#[arg(
short = 'u',
long = "include-untracked",
help = "Include untracked files in the stash",
overrides_with = "no_include_untracked"
)]
include_untracked: bool,
#[arg(
long = "no-include-untracked",
help = "Do not include untracked files (the default); countermands an earlier -u/--include-untracked (last one wins)",
overrides_with = "include_untracked"
)]
no_include_untracked: bool,
#[arg(
short = 'a',
long = "all",
help = "Include untracked and ignored files in the stash"
)]
all: bool,
#[arg(
short = 'k',
long = "keep-index",
help = "Keep staged changes in the index and working tree"
)]
keep_index: bool,
#[arg(
value_name = "pathspec",
help = "Stash only the changes to the given paths, leaving the rest of the working tree intact"
)]
pathspec: Vec<String>,
},
#[command(about = "Remove a single stashed state from the stash list")]
Pop {
#[arg(help = "The stash to pop")]
stash: Option<String>,
},
#[command(about = "List the stashes that you currently have")]
List,
#[command(about = "Like pop, but do not remove the state from the stash list")]
Apply {
#[arg(help = "The stash to apply")]
stash: Option<String>,
},
#[command(about = "Remove a single stashed state from the stash list")]
Drop {
#[arg(help = "The stash to drop")]
stash: Option<String>,
},
#[command(
about = "Show the changes recorded in the stash as a file-level summary or a unified diff (-p)"
)]
Show {
#[arg(help = "Stash reference (default: stash@{0})")]
stash: Option<String>,
#[arg(long, help = "Show only the file names that changed")]
name_only: bool,
#[arg(long, help = "Show only file names with their status code")]
name_status: bool,
#[arg(
short = 'p',
long = "patch",
help = "Show the stashed changes as a unified diff (patch)"
)]
patch: bool,
},
#[command(about = "Create and check out a new branch from the stash, then drop it")]
Branch {
#[arg(help = "Name of the new branch to create")]
branch: String,
#[arg(help = "Stash reference (default: stash@{0})")]
stash: Option<String>,
},
#[command(about = "Remove all stashed entries")]
Clear {
#[arg(
long,
help = "Skip confirmation; required outside JSON / machine modes"
)]
force: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum Bisect {
#[command(about = "Start a new bisect session")]
Start {
#[arg(help = "Bad commit to start from")]
bad: Option<String>,
#[arg(long, short, help = "Good commit to mark")]
good: Option<String>,
#[arg(
long = "first-parent",
help = "Follow only the first parent of merge commits while bisecting"
)]
first_parent: bool,
},
#[command(about = "Mark the current or given commit as bad")]
Bad {
#[arg(help = "Commit to mark as bad")]
rev: Option<String>,
},
#[command(about = "Mark the current or given commit as good")]
Good {
#[arg(help = "Commit to mark as good")]
rev: Option<String>,
},
#[command(about = "End bisect session and restore original HEAD")]
Reset {
#[arg(help = "Commit to reset to (optional)")]
rev: Option<String>,
},
#[command(about = "Skip current commit and move to next")]
Skip {
#[arg(help = "Commit to skip")]
rev: Option<String>,
},
#[command(about = "Show bisect log")]
Log,
#[command(about = "Run a script for each commit until convergence")]
Run {
#[arg(
help = "Command to run for each commit; first arg is the executable",
required = true,
trailing_var_arg = true,
allow_hyphen_values = true
)]
cmd: Vec<String>,
},
#[command(
about = "Show the current bisect state and remaining candidates",
visible_alias = "visualize"
)]
View,
}
/// Synchronous CLI entry — used by both the `libra` binary and embedders that cannot
/// (or do not wish to) own their own Tokio runtime.
///
/// Functional scope:
/// - Builds a multi-thread Tokio runtime, then drives [`parse_async`] to completion.
/// - When `args` is `None`, the underlying parser falls back to `std::env::args`.
///
/// Boundary conditions:
/// - Calling this from inside an existing Tokio runtime panics; embedders that are
/// already async must call [`parse_async`] directly. See the embedding contract in
/// [`crate::exec`].
/// - Returns `CliError::fatal` if the runtime itself cannot be constructed (extremely
/// unlikely outside of OOM scenarios).
pub fn parse(args: Option<&[&str]>) -> CliResult<()> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|e| CliError::fatal(format!("failed to create tokio runtime: {e}")))?;
// The one vetted telemetry span (lore.md 1.7): canonical subcommand name,
// duration, and on failure the stable LBR-* code — NOTHING else. Plain
// `tracing`, so it is a no-op without a matching layer; the OTLP layer is
// feature+endpoint gated, and the fmt layer excludes this target so
// LIBRA_LOG output is byte-unchanged. Library embedders calling
// parse_async/exec_async directly bypass it (documented).
let command_name = canonical_command_name(args);
let span = tracing::info_span!(
target: "libra::telemetry",
"libra.command",
libra.command = command_name.as_deref().unwrap_or("<none>"),
otel.status_code = tracing::field::Empty,
libra.error_code = tracing::field::Empty,
);
let result = span.in_scope(|| runtime.block_on(Box::pin(parse_async(args))));
if let Err(error) = &result {
// tracing-opentelemetry maps `otel.status_code` to the OTel status.
span.record("otel.status_code", "ERROR");
span.record("libra.error_code", error.stable_code().as_str());
}
result
}
/// The CANONICAL subcommand name for telemetry: the raw argv token resolved
/// through clap's own metadata (aliases like `br` canonicalize to `branch`).
/// Never derived from user argv content beyond the subcommand token itself.
fn canonical_command_name(args: Option<&[&str]>) -> Option<String> {
let argv: Vec<std::ffi::OsString> = match args {
Some(args) => args.iter().map(std::ffi::OsString::from).collect(),
None => env::args_os().collect(),
};
let (index, _) = find_subcommand_index(&argv)?;
let token = argv.get(index)?.to_str()?;
let cli = <Cli as clap::CommandFactory>::command();
cli.get_subcommands()
.find(|candidate| {
candidate.get_name() == token || candidate.get_all_aliases().any(|alias| alias == token)
})
.map(|candidate| candidate.get_name().to_string())
}
/// Rewrite Git-style `-<n>` shortcuts into the long-form `-n <n>` flag, but only when
/// the active subcommand is `log`.
///
/// Git accepts `git log -3` as shorthand for `git log -n 3`, but clap cannot express a
/// purely numeric flag without conflicting with positional revisions. This helper
/// patches argv before clap sees it so users keep the familiar shortcut.
///
/// Boundary conditions:
/// - The rewrite only fires for arguments before any `--` separator inside the `log`
/// subcommand, so paths or revisions that happen to look like `-3` are preserved
/// verbatim once the user explicitly closes the option list.
/// - When `log` is not the active subcommand the original argv is returned unchanged,
/// leaving every other command's `-<n>` semantics untouched.
///
/// See: [`tests::clap_alias_br_resolves_to_branch`] and friends for related parser
/// behaviour. The exact rewrite is exercised end-to-end by the integration tests in
/// `tests/command/log_test.rs`.
fn rewrite_log_short_number_args(args: Vec<std::ffi::OsString>) -> Vec<std::ffi::OsString> {
// Detect the real subcommand position to avoid rewriting positional args for other commands.
let subcommand = find_subcommand_index(&args);
let Some((log_index, from_double_dash)) = subcommand else {
return args;
};
if !matches!(args.get(log_index), Some(name) if name == "log") {
return args;
}
let mut out: Vec<std::ffi::OsString> = Vec::with_capacity(args.len() + 2);
if from_double_dash {
// Drop the `--` that was used to separate global args from the subcommand.
for (idx, arg) in args.iter().enumerate().take(log_index + 1) {
if idx + 1 == log_index && arg == "--" {
continue;
}
out.push(arg.clone());
}
} else {
out.extend(args.iter().take(log_index + 1).cloned());
}
// Respect `--` inside the log subcommand: stop rewriting after it.
let mut after_double_dash = false;
for arg in args.into_iter().skip(log_index + 1) {
if after_double_dash {
out.push(arg);
continue;
}
if arg == "--" {
after_double_dash = true;
out.push(arg);
continue;
}