@hasna/terminal 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.js +404 -0
- package/dist/Browse.js +79 -0
- package/dist/FuzzyPicker.js +47 -0
- package/dist/Onboarding.js +51 -0
- package/dist/Spinner.js +12 -0
- package/dist/StatusBar.js +49 -0
- package/dist/ai.js +322 -0
- package/dist/cache.js +41 -0
- package/dist/cli.js +64 -16
- package/dist/command-rewriter.js +64 -0
- package/dist/command-validator.js +86 -0
- package/dist/compression.js +107 -0
- package/dist/context-hints.js +275 -0
- package/dist/diff-cache.js +107 -0
- package/dist/discover.js +212 -0
- package/dist/economy.js +123 -0
- package/dist/expand-store.js +38 -0
- package/dist/file-cache.js +72 -0
- package/dist/file-index.js +62 -0
- package/dist/history.js +62 -0
- package/dist/lazy-executor.js +54 -0
- package/dist/line-dedup.js +59 -0
- package/dist/loop-detector.js +75 -0
- package/dist/mcp/install.js +98 -0
- package/dist/mcp/server.js +569 -0
- package/dist/noise-filter.js +86 -0
- package/dist/output-processor.js +129 -0
- package/dist/output-router.js +41 -0
- package/dist/output-store.js +111 -0
- package/dist/parsers/base.js +2 -0
- package/dist/parsers/build.js +64 -0
- package/dist/parsers/errors.js +101 -0
- package/dist/parsers/files.js +78 -0
- package/dist/parsers/git.js +99 -0
- package/dist/parsers/index.js +48 -0
- package/dist/parsers/tests.js +89 -0
- package/dist/providers/anthropic.js +39 -0
- package/dist/providers/base.js +4 -0
- package/dist/providers/cerebras.js +95 -0
- package/dist/providers/groq.js +95 -0
- package/dist/providers/index.js +73 -0
- package/dist/providers/xai.js +95 -0
- package/dist/recipes/model.js +20 -0
- package/dist/recipes/storage.js +136 -0
- package/dist/search/content-search.js +68 -0
- package/dist/search/file-search.js +61 -0
- package/dist/search/filters.js +34 -0
- package/dist/search/index.js +5 -0
- package/dist/search/semantic.js +320 -0
- package/dist/session-boot.js +59 -0
- package/dist/session-context.js +55 -0
- package/dist/sessions-db.js +173 -0
- package/dist/smart-display.js +286 -0
- package/dist/snapshots.js +51 -0
- package/dist/supervisor.js +112 -0
- package/dist/test-watchlist.js +131 -0
- package/dist/tool-profiles.js +122 -0
- package/dist/tree.js +94 -0
- package/dist/usage-cache.js +65 -0
- package/package.json +8 -1
- package/src/ai.ts +8 -0
- package/src/cli.tsx +57 -18
- package/src/output-processor.ts +6 -1
- package/src/output-store.ts +58 -12
- package/src/tool-profiles.ts +139 -0
- package/.claude/scheduled_tasks.lock +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- package/CONTRIBUTING.md +0 -80
- package/benchmarks/benchmark.mjs +0 -115
- package/imported_modules.txt +0 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
- package/temp/rtk/.claude/agents/debugger.md +0 -519
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
- package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
- package/temp/rtk/.claude/agents/technical-writer.md +0 -355
- package/temp/rtk/.claude/commands/diagnose.md +0 -352
- package/temp/rtk/.claude/commands/test-routing.md +0 -362
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
- package/temp/rtk/.claude/rules/cli-testing.md +0 -526
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
- package/temp/rtk/.claude/skills/performance.md +0 -435
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
- package/temp/rtk/.claude/skills/repo-recap.md +0 -206
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
- package/temp/rtk/.claude/skills/security-guardian.md +0 -503
- package/temp/rtk/.claude/skills/ship.md +0 -404
- package/temp/rtk/.github/workflows/benchmark.yml +0 -34
- package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
- package/temp/rtk/.github/workflows/release-please.yml +0 -51
- package/temp/rtk/.github/workflows/release.yml +0 -343
- package/temp/rtk/.github/workflows/security-check.yml +0 -135
- package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
- package/temp/rtk/.release-please-manifest.json +0 -3
- package/temp/rtk/ARCHITECTURE.md +0 -1491
- package/temp/rtk/CHANGELOG.md +0 -640
- package/temp/rtk/CLAUDE.md +0 -605
- package/temp/rtk/CONTRIBUTING.md +0 -199
- package/temp/rtk/Cargo.lock +0 -1668
- package/temp/rtk/Cargo.toml +0 -64
- package/temp/rtk/Formula/rtk.rb +0 -43
- package/temp/rtk/INSTALL.md +0 -390
- package/temp/rtk/LICENSE +0 -21
- package/temp/rtk/README.md +0 -386
- package/temp/rtk/README_es.md +0 -159
- package/temp/rtk/README_fr.md +0 -197
- package/temp/rtk/README_ja.md +0 -159
- package/temp/rtk/README_ko.md +0 -159
- package/temp/rtk/README_zh.md +0 -167
- package/temp/rtk/ROADMAP.md +0 -15
- package/temp/rtk/SECURITY.md +0 -217
- package/temp/rtk/TEST_EXEC_TIME.md +0 -102
- package/temp/rtk/build.rs +0 -57
- package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
- package/temp/rtk/docs/FEATURES.md +0 -1410
- package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
- package/temp/rtk/docs/filter-workflow.md +0 -102
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +0 -583
- package/temp/rtk/hooks/opencode-rtk.ts +0 -39
- package/temp/rtk/hooks/rtk-awareness.md +0 -29
- package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
- package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
- package/temp/rtk/install.sh +0 -124
- package/temp/rtk/release-please-config.json +0 -10
- package/temp/rtk/scripts/benchmark.sh +0 -592
- package/temp/rtk/scripts/check-installation.sh +0 -162
- package/temp/rtk/scripts/install-local.sh +0 -37
- package/temp/rtk/scripts/rtk-economics.sh +0 -137
- package/temp/rtk/scripts/test-all.sh +0 -561
- package/temp/rtk/scripts/test-aristote.sh +0 -227
- package/temp/rtk/scripts/test-tracking.sh +0 -79
- package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
- package/temp/rtk/scripts/validate-docs.sh +0 -73
- package/temp/rtk/src/aws_cmd.rs +0 -880
- package/temp/rtk/src/binlog.rs +0 -1645
- package/temp/rtk/src/cargo_cmd.rs +0 -1727
- package/temp/rtk/src/cc_economics.rs +0 -1157
- package/temp/rtk/src/ccusage.rs +0 -340
- package/temp/rtk/src/config.rs +0 -187
- package/temp/rtk/src/container.rs +0 -855
- package/temp/rtk/src/curl_cmd.rs +0 -134
- package/temp/rtk/src/deps.rs +0 -268
- package/temp/rtk/src/diff_cmd.rs +0 -367
- package/temp/rtk/src/discover/mod.rs +0 -274
- package/temp/rtk/src/discover/provider.rs +0 -388
- package/temp/rtk/src/discover/registry.rs +0 -2022
- package/temp/rtk/src/discover/report.rs +0 -202
- package/temp/rtk/src/discover/rules.rs +0 -667
- package/temp/rtk/src/display_helpers.rs +0 -402
- package/temp/rtk/src/dotnet_cmd.rs +0 -1771
- package/temp/rtk/src/dotnet_format_report.rs +0 -133
- package/temp/rtk/src/dotnet_trx.rs +0 -593
- package/temp/rtk/src/env_cmd.rs +0 -204
- package/temp/rtk/src/filter.rs +0 -462
- package/temp/rtk/src/filters/README.md +0 -52
- package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
- package/temp/rtk/src/filters/basedpyright.toml +0 -47
- package/temp/rtk/src/filters/biome.toml +0 -45
- package/temp/rtk/src/filters/brew-install.toml +0 -37
- package/temp/rtk/src/filters/composer-install.toml +0 -40
- package/temp/rtk/src/filters/df.toml +0 -16
- package/temp/rtk/src/filters/dotnet-build.toml +0 -64
- package/temp/rtk/src/filters/du.toml +0 -16
- package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
- package/temp/rtk/src/filters/gcc.toml +0 -49
- package/temp/rtk/src/filters/gcloud.toml +0 -22
- package/temp/rtk/src/filters/hadolint.toml +0 -24
- package/temp/rtk/src/filters/helm.toml +0 -29
- package/temp/rtk/src/filters/iptables.toml +0 -27
- package/temp/rtk/src/filters/jj.toml +0 -28
- package/temp/rtk/src/filters/jq.toml +0 -24
- package/temp/rtk/src/filters/make.toml +0 -41
- package/temp/rtk/src/filters/markdownlint.toml +0 -24
- package/temp/rtk/src/filters/mix-compile.toml +0 -27
- package/temp/rtk/src/filters/mix-format.toml +0 -15
- package/temp/rtk/src/filters/mvn-build.toml +0 -44
- package/temp/rtk/src/filters/oxlint.toml +0 -43
- package/temp/rtk/src/filters/ping.toml +0 -63
- package/temp/rtk/src/filters/pio-run.toml +0 -40
- package/temp/rtk/src/filters/poetry-install.toml +0 -50
- package/temp/rtk/src/filters/pre-commit.toml +0 -35
- package/temp/rtk/src/filters/ps.toml +0 -16
- package/temp/rtk/src/filters/quarto-render.toml +0 -41
- package/temp/rtk/src/filters/rsync.toml +0 -48
- package/temp/rtk/src/filters/shellcheck.toml +0 -27
- package/temp/rtk/src/filters/shopify-theme.toml +0 -29
- package/temp/rtk/src/filters/skopeo.toml +0 -45
- package/temp/rtk/src/filters/sops.toml +0 -16
- package/temp/rtk/src/filters/ssh.toml +0 -44
- package/temp/rtk/src/filters/stat.toml +0 -34
- package/temp/rtk/src/filters/swift-build.toml +0 -41
- package/temp/rtk/src/filters/systemctl-status.toml +0 -33
- package/temp/rtk/src/filters/terraform-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
- package/temp/rtk/src/filters/tofu-init.toml +0 -38
- package/temp/rtk/src/filters/tofu-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-validate.toml +0 -17
- package/temp/rtk/src/filters/trunk-build.toml +0 -39
- package/temp/rtk/src/filters/ty.toml +0 -50
- package/temp/rtk/src/filters/uv-sync.toml +0 -37
- package/temp/rtk/src/filters/xcodebuild.toml +0 -99
- package/temp/rtk/src/filters/yamllint.toml +0 -25
- package/temp/rtk/src/find_cmd.rs +0 -598
- package/temp/rtk/src/format_cmd.rs +0 -386
- package/temp/rtk/src/gain.rs +0 -723
- package/temp/rtk/src/gh_cmd.rs +0 -1651
- package/temp/rtk/src/git.rs +0 -2012
- package/temp/rtk/src/go_cmd.rs +0 -592
- package/temp/rtk/src/golangci_cmd.rs +0 -254
- package/temp/rtk/src/grep_cmd.rs +0 -288
- package/temp/rtk/src/gt_cmd.rs +0 -810
- package/temp/rtk/src/hook_audit_cmd.rs +0 -283
- package/temp/rtk/src/hook_check.rs +0 -171
- package/temp/rtk/src/init.rs +0 -1859
- package/temp/rtk/src/integrity.rs +0 -537
- package/temp/rtk/src/json_cmd.rs +0 -231
- package/temp/rtk/src/learn/detector.rs +0 -628
- package/temp/rtk/src/learn/mod.rs +0 -119
- package/temp/rtk/src/learn/report.rs +0 -184
- package/temp/rtk/src/lint_cmd.rs +0 -694
- package/temp/rtk/src/local_llm.rs +0 -316
- package/temp/rtk/src/log_cmd.rs +0 -248
- package/temp/rtk/src/ls.rs +0 -324
- package/temp/rtk/src/main.rs +0 -2482
- package/temp/rtk/src/mypy_cmd.rs +0 -389
- package/temp/rtk/src/next_cmd.rs +0 -241
- package/temp/rtk/src/npm_cmd.rs +0 -236
- package/temp/rtk/src/parser/README.md +0 -267
- package/temp/rtk/src/parser/error.rs +0 -46
- package/temp/rtk/src/parser/formatter.rs +0 -336
- package/temp/rtk/src/parser/mod.rs +0 -311
- package/temp/rtk/src/parser/types.rs +0 -119
- package/temp/rtk/src/pip_cmd.rs +0 -302
- package/temp/rtk/src/playwright_cmd.rs +0 -479
- package/temp/rtk/src/pnpm_cmd.rs +0 -573
- package/temp/rtk/src/prettier_cmd.rs +0 -221
- package/temp/rtk/src/prisma_cmd.rs +0 -482
- package/temp/rtk/src/psql_cmd.rs +0 -382
- package/temp/rtk/src/pytest_cmd.rs +0 -384
- package/temp/rtk/src/read.rs +0 -217
- package/temp/rtk/src/rewrite_cmd.rs +0 -50
- package/temp/rtk/src/ruff_cmd.rs +0 -402
- package/temp/rtk/src/runner.rs +0 -271
- package/temp/rtk/src/summary.rs +0 -297
- package/temp/rtk/src/tee.rs +0 -405
- package/temp/rtk/src/telemetry.rs +0 -248
- package/temp/rtk/src/toml_filter.rs +0 -1655
- package/temp/rtk/src/tracking.rs +0 -1416
- package/temp/rtk/src/tree.rs +0 -209
- package/temp/rtk/src/tsc_cmd.rs +0 -259
- package/temp/rtk/src/utils.rs +0 -432
- package/temp/rtk/src/verify_cmd.rs +0 -47
- package/temp/rtk/src/vitest_cmd.rs +0 -385
- package/temp/rtk/src/wc_cmd.rs +0 -401
- package/temp/rtk/src/wget_cmd.rs +0 -260
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
- package/tsconfig.json +0 -15
package/temp/rtk/src/git.rs
DELETED
|
@@ -1,2012 +0,0 @@
|
|
|
1
|
-
use crate::tracking;
|
|
2
|
-
use anyhow::{Context, Result};
|
|
3
|
-
use std::ffi::OsString;
|
|
4
|
-
use std::process::Command;
|
|
5
|
-
|
|
6
|
-
#[derive(Debug, Clone)]
|
|
7
|
-
pub enum GitCommand {
|
|
8
|
-
Diff,
|
|
9
|
-
Log,
|
|
10
|
-
Status,
|
|
11
|
-
Show,
|
|
12
|
-
Add,
|
|
13
|
-
Commit,
|
|
14
|
-
Push,
|
|
15
|
-
Pull,
|
|
16
|
-
Branch,
|
|
17
|
-
Fetch,
|
|
18
|
-
Stash { subcommand: Option<String> },
|
|
19
|
-
Worktree,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/// Create a git Command with global options (e.g. -C, -c, --git-dir, --work-tree)
|
|
23
|
-
/// prepended before any subcommand arguments.
|
|
24
|
-
fn git_cmd(global_args: &[String]) -> Command {
|
|
25
|
-
let mut cmd = Command::new("git");
|
|
26
|
-
for arg in global_args {
|
|
27
|
-
cmd.arg(arg);
|
|
28
|
-
}
|
|
29
|
-
cmd
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
pub fn run(
|
|
33
|
-
cmd: GitCommand,
|
|
34
|
-
args: &[String],
|
|
35
|
-
max_lines: Option<usize>,
|
|
36
|
-
verbose: u8,
|
|
37
|
-
global_args: &[String],
|
|
38
|
-
) -> Result<()> {
|
|
39
|
-
match cmd {
|
|
40
|
-
GitCommand::Diff => run_diff(args, max_lines, verbose, global_args),
|
|
41
|
-
GitCommand::Log => run_log(args, max_lines, verbose, global_args),
|
|
42
|
-
GitCommand::Status => run_status(args, verbose, global_args),
|
|
43
|
-
GitCommand::Show => run_show(args, max_lines, verbose, global_args),
|
|
44
|
-
GitCommand::Add => run_add(args, verbose, global_args),
|
|
45
|
-
GitCommand::Commit => run_commit(args, verbose, global_args),
|
|
46
|
-
GitCommand::Push => run_push(args, verbose, global_args),
|
|
47
|
-
GitCommand::Pull => run_pull(args, verbose, global_args),
|
|
48
|
-
GitCommand::Branch => run_branch(args, verbose, global_args),
|
|
49
|
-
GitCommand::Fetch => run_fetch(args, verbose, global_args),
|
|
50
|
-
GitCommand::Stash { subcommand } => {
|
|
51
|
-
run_stash(subcommand.as_deref(), args, verbose, global_args)
|
|
52
|
-
}
|
|
53
|
-
GitCommand::Worktree => run_worktree(args, verbose, global_args),
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
fn run_diff(
|
|
58
|
-
args: &[String],
|
|
59
|
-
max_lines: Option<usize>,
|
|
60
|
-
verbose: u8,
|
|
61
|
-
global_args: &[String],
|
|
62
|
-
) -> Result<()> {
|
|
63
|
-
let timer = tracking::TimedExecution::start();
|
|
64
|
-
|
|
65
|
-
// Check if user wants stat output
|
|
66
|
-
let wants_stat = args
|
|
67
|
-
.iter()
|
|
68
|
-
.any(|arg| arg == "--stat" || arg == "--numstat" || arg == "--shortstat");
|
|
69
|
-
|
|
70
|
-
// Check if user wants compact diff (default RTK behavior)
|
|
71
|
-
let wants_compact = !args.iter().any(|arg| arg == "--no-compact");
|
|
72
|
-
|
|
73
|
-
if wants_stat || !wants_compact {
|
|
74
|
-
// User wants stat or explicitly no compacting - pass through directly
|
|
75
|
-
let mut cmd = git_cmd(global_args);
|
|
76
|
-
cmd.arg("diff");
|
|
77
|
-
for arg in args {
|
|
78
|
-
cmd.arg(arg);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let output = cmd.output().context("Failed to run git diff")?;
|
|
82
|
-
|
|
83
|
-
if !output.status.success() {
|
|
84
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
85
|
-
eprintln!("{}", stderr);
|
|
86
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
90
|
-
println!("{}", stdout.trim());
|
|
91
|
-
|
|
92
|
-
timer.track(
|
|
93
|
-
&format!("git diff {}", args.join(" ")),
|
|
94
|
-
&format!("rtk git diff {} (passthrough)", args.join(" ")),
|
|
95
|
-
&stdout,
|
|
96
|
-
&stdout,
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
return Ok(());
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Default RTK behavior: stat first, then compacted diff
|
|
103
|
-
let mut cmd = git_cmd(global_args);
|
|
104
|
-
cmd.arg("diff").arg("--stat");
|
|
105
|
-
|
|
106
|
-
for arg in args {
|
|
107
|
-
cmd.arg(arg);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let output = cmd.output().context("Failed to run git diff")?;
|
|
111
|
-
let stat_stdout = String::from_utf8_lossy(&output.stdout);
|
|
112
|
-
|
|
113
|
-
if verbose > 0 {
|
|
114
|
-
eprintln!("Git diff summary:");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Print stat summary first
|
|
118
|
-
println!("{}", stat_stdout.trim());
|
|
119
|
-
|
|
120
|
-
// Now get actual diff but compact it
|
|
121
|
-
let mut diff_cmd = git_cmd(global_args);
|
|
122
|
-
diff_cmd.arg("diff");
|
|
123
|
-
for arg in args {
|
|
124
|
-
diff_cmd.arg(arg);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let diff_output = diff_cmd.output().context("Failed to run git diff")?;
|
|
128
|
-
let diff_stdout = String::from_utf8_lossy(&diff_output.stdout);
|
|
129
|
-
|
|
130
|
-
let mut final_output = stat_stdout.to_string();
|
|
131
|
-
if !diff_stdout.is_empty() {
|
|
132
|
-
println!("\n--- Changes ---");
|
|
133
|
-
let compacted = compact_diff(&diff_stdout, max_lines.unwrap_or(500));
|
|
134
|
-
println!("{}", compacted);
|
|
135
|
-
final_output.push_str("\n--- Changes ---\n");
|
|
136
|
-
final_output.push_str(&compacted);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
timer.track(
|
|
140
|
-
&format!("git diff {}", args.join(" ")),
|
|
141
|
-
&format!("rtk git diff {}", args.join(" ")),
|
|
142
|
-
&format!("{}\n{}", stat_stdout, diff_stdout),
|
|
143
|
-
&final_output,
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
Ok(())
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
fn run_show(
|
|
150
|
-
args: &[String],
|
|
151
|
-
max_lines: Option<usize>,
|
|
152
|
-
verbose: u8,
|
|
153
|
-
global_args: &[String],
|
|
154
|
-
) -> Result<()> {
|
|
155
|
-
let timer = tracking::TimedExecution::start();
|
|
156
|
-
|
|
157
|
-
// If user wants --stat or --format only, pass through
|
|
158
|
-
let wants_stat_only = args
|
|
159
|
-
.iter()
|
|
160
|
-
.any(|arg| arg == "--stat" || arg == "--numstat" || arg == "--shortstat");
|
|
161
|
-
|
|
162
|
-
let wants_format = args
|
|
163
|
-
.iter()
|
|
164
|
-
.any(|arg| arg.starts_with("--pretty") || arg.starts_with("--format"));
|
|
165
|
-
|
|
166
|
-
// `git show rev:path` prints a blob, not a commit diff. In this mode we should
|
|
167
|
-
// pass through directly to avoid duplicated output from compact-show steps.
|
|
168
|
-
let wants_blob_show = args.iter().any(|arg| is_blob_show_arg(arg));
|
|
169
|
-
|
|
170
|
-
if wants_stat_only || wants_format || wants_blob_show {
|
|
171
|
-
let mut cmd = git_cmd(global_args);
|
|
172
|
-
cmd.arg("show");
|
|
173
|
-
for arg in args {
|
|
174
|
-
cmd.arg(arg);
|
|
175
|
-
}
|
|
176
|
-
let output = cmd.output().context("Failed to run git show")?;
|
|
177
|
-
if !output.status.success() {
|
|
178
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
179
|
-
eprintln!("{}", stderr);
|
|
180
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
181
|
-
}
|
|
182
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
183
|
-
if wants_blob_show {
|
|
184
|
-
print!("{}", stdout);
|
|
185
|
-
} else {
|
|
186
|
-
println!("{}", stdout.trim());
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
timer.track(
|
|
190
|
-
&format!("git show {}", args.join(" ")),
|
|
191
|
-
&format!("rtk git show {} (passthrough)", args.join(" ")),
|
|
192
|
-
&stdout,
|
|
193
|
-
&stdout,
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
return Ok(());
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Get raw output for tracking
|
|
200
|
-
let mut raw_cmd = git_cmd(global_args);
|
|
201
|
-
raw_cmd.arg("show");
|
|
202
|
-
for arg in args {
|
|
203
|
-
raw_cmd.arg(arg);
|
|
204
|
-
}
|
|
205
|
-
let raw_output = raw_cmd
|
|
206
|
-
.output()
|
|
207
|
-
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
|
208
|
-
.unwrap_or_default();
|
|
209
|
-
|
|
210
|
-
// Step 1: one-line commit summary
|
|
211
|
-
let mut summary_cmd = git_cmd(global_args);
|
|
212
|
-
summary_cmd.args(["show", "--no-patch", "--pretty=format:%h %s (%ar) <%an>"]);
|
|
213
|
-
for arg in args {
|
|
214
|
-
summary_cmd.arg(arg);
|
|
215
|
-
}
|
|
216
|
-
let summary_output = summary_cmd.output().context("Failed to run git show")?;
|
|
217
|
-
if !summary_output.status.success() {
|
|
218
|
-
let stderr = String::from_utf8_lossy(&summary_output.stderr);
|
|
219
|
-
eprintln!("{}", stderr);
|
|
220
|
-
std::process::exit(summary_output.status.code().unwrap_or(1));
|
|
221
|
-
}
|
|
222
|
-
let summary = String::from_utf8_lossy(&summary_output.stdout);
|
|
223
|
-
println!("{}", summary.trim());
|
|
224
|
-
|
|
225
|
-
// Step 2: --stat summary
|
|
226
|
-
let mut stat_cmd = git_cmd(global_args);
|
|
227
|
-
stat_cmd.args(["show", "--stat", "--pretty=format:"]);
|
|
228
|
-
for arg in args {
|
|
229
|
-
stat_cmd.arg(arg);
|
|
230
|
-
}
|
|
231
|
-
let stat_output = stat_cmd.output().context("Failed to run git show --stat")?;
|
|
232
|
-
let stat_stdout = String::from_utf8_lossy(&stat_output.stdout);
|
|
233
|
-
let stat_text = stat_stdout.trim();
|
|
234
|
-
if !stat_text.is_empty() {
|
|
235
|
-
println!("{}", stat_text);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Step 3: compacted diff
|
|
239
|
-
let mut diff_cmd = git_cmd(global_args);
|
|
240
|
-
diff_cmd.args(["show", "--pretty=format:"]);
|
|
241
|
-
for arg in args {
|
|
242
|
-
diff_cmd.arg(arg);
|
|
243
|
-
}
|
|
244
|
-
let diff_output = diff_cmd.output().context("Failed to run git show (diff)")?;
|
|
245
|
-
let diff_stdout = String::from_utf8_lossy(&diff_output.stdout);
|
|
246
|
-
let diff_text = diff_stdout.trim();
|
|
247
|
-
|
|
248
|
-
let mut final_output = summary.to_string();
|
|
249
|
-
if !diff_text.is_empty() {
|
|
250
|
-
if verbose > 0 {
|
|
251
|
-
println!("\n--- Changes ---");
|
|
252
|
-
}
|
|
253
|
-
let compacted = compact_diff(diff_text, max_lines.unwrap_or(500));
|
|
254
|
-
println!("{}", compacted);
|
|
255
|
-
final_output.push_str(&format!("\n{}", compacted));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
timer.track(
|
|
259
|
-
&format!("git show {}", args.join(" ")),
|
|
260
|
-
&format!("rtk git show {}", args.join(" ")),
|
|
261
|
-
&raw_output,
|
|
262
|
-
&final_output,
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
Ok(())
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
fn is_blob_show_arg(arg: &str) -> bool {
|
|
269
|
-
// Detect `rev:path` style arguments while ignoring flags like `--pretty=format:...`.
|
|
270
|
-
!arg.starts_with('-') && arg.contains(':')
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
pub(crate) fn compact_diff(diff: &str, max_lines: usize) -> String {
|
|
274
|
-
let mut result = Vec::new();
|
|
275
|
-
let mut current_file = String::new();
|
|
276
|
-
let mut added = 0;
|
|
277
|
-
let mut removed = 0;
|
|
278
|
-
let mut in_hunk = false;
|
|
279
|
-
let mut hunk_lines = 0;
|
|
280
|
-
let max_hunk_lines = 30;
|
|
281
|
-
|
|
282
|
-
for line in diff.lines() {
|
|
283
|
-
if line.starts_with("diff --git") {
|
|
284
|
-
// New file
|
|
285
|
-
if !current_file.is_empty() && (added > 0 || removed > 0) {
|
|
286
|
-
result.push(format!(" +{} -{}", added, removed));
|
|
287
|
-
}
|
|
288
|
-
current_file = line.split(" b/").nth(1).unwrap_or("unknown").to_string();
|
|
289
|
-
result.push(format!("\n📄 {}", current_file));
|
|
290
|
-
added = 0;
|
|
291
|
-
removed = 0;
|
|
292
|
-
in_hunk = false;
|
|
293
|
-
} else if line.starts_with("@@") {
|
|
294
|
-
// New hunk
|
|
295
|
-
in_hunk = true;
|
|
296
|
-
hunk_lines = 0;
|
|
297
|
-
let hunk_info = line.split("@@").nth(1).unwrap_or("").trim();
|
|
298
|
-
result.push(format!(" @@ {} @@", hunk_info));
|
|
299
|
-
} else if in_hunk {
|
|
300
|
-
if line.starts_with('+') && !line.starts_with("+++") {
|
|
301
|
-
added += 1;
|
|
302
|
-
if hunk_lines < max_hunk_lines {
|
|
303
|
-
result.push(format!(" {}", line));
|
|
304
|
-
hunk_lines += 1;
|
|
305
|
-
}
|
|
306
|
-
} else if line.starts_with('-') && !line.starts_with("---") {
|
|
307
|
-
removed += 1;
|
|
308
|
-
if hunk_lines < max_hunk_lines {
|
|
309
|
-
result.push(format!(" {}", line));
|
|
310
|
-
hunk_lines += 1;
|
|
311
|
-
}
|
|
312
|
-
} else if hunk_lines < max_hunk_lines && !line.starts_with("\\") {
|
|
313
|
-
// Context line
|
|
314
|
-
if hunk_lines > 0 {
|
|
315
|
-
result.push(format!(" {}", line));
|
|
316
|
-
hunk_lines += 1;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if hunk_lines == max_hunk_lines {
|
|
321
|
-
result.push(" ... (truncated)".to_string());
|
|
322
|
-
hunk_lines += 1;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if result.len() >= max_lines {
|
|
327
|
-
result.push("\n... (more changes truncated)".to_string());
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if !current_file.is_empty() && (added > 0 || removed > 0) {
|
|
333
|
-
result.push(format!(" +{} -{}", added, removed));
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
result.join("\n")
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
fn run_log(
|
|
340
|
-
args: &[String],
|
|
341
|
-
_max_lines: Option<usize>,
|
|
342
|
-
verbose: u8,
|
|
343
|
-
global_args: &[String],
|
|
344
|
-
) -> Result<()> {
|
|
345
|
-
let timer = tracking::TimedExecution::start();
|
|
346
|
-
|
|
347
|
-
let mut cmd = git_cmd(global_args);
|
|
348
|
-
cmd.arg("log");
|
|
349
|
-
|
|
350
|
-
// Check if user provided format flags
|
|
351
|
-
let has_format_flag = args.iter().any(|arg| {
|
|
352
|
-
arg.starts_with("--oneline") || arg.starts_with("--pretty") || arg.starts_with("--format")
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// Check if user provided limit flag (-N, -n N, --max-count=N, --max-count N)
|
|
356
|
-
let has_limit_flag = args.iter().any(|arg| {
|
|
357
|
-
(arg.starts_with('-') && arg.chars().nth(1).map_or(false, |c| c.is_ascii_digit()))
|
|
358
|
-
|| arg == "-n"
|
|
359
|
-
|| arg.starts_with("--max-count")
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// Apply RTK defaults only if user didn't specify them
|
|
363
|
-
if !has_format_flag {
|
|
364
|
-
cmd.args(["--pretty=format:%h %s (%ar) <%an>"]);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Determine limit: respect user's explicit -N flag, use sensible defaults otherwise
|
|
368
|
-
let (limit, user_set_limit) = if has_limit_flag {
|
|
369
|
-
// User explicitly passed -N / -n N / --max-count=N → respect their choice
|
|
370
|
-
let n = parse_user_limit(args).unwrap_or(10);
|
|
371
|
-
(n, true)
|
|
372
|
-
} else if has_format_flag {
|
|
373
|
-
// --oneline / --pretty without -N: user wants compact output, allow more
|
|
374
|
-
cmd.arg("-50");
|
|
375
|
-
(50, false)
|
|
376
|
-
} else {
|
|
377
|
-
// No flags at all: default to 10
|
|
378
|
-
cmd.arg("-10");
|
|
379
|
-
(10, false)
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
// Only add --no-merges if user didn't explicitly request merge commits
|
|
383
|
-
let wants_merges = args
|
|
384
|
-
.iter()
|
|
385
|
-
.any(|arg| arg == "--merges" || arg == "--min-parents=2");
|
|
386
|
-
if !wants_merges {
|
|
387
|
-
cmd.arg("--no-merges");
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Pass all user arguments
|
|
391
|
-
for arg in args {
|
|
392
|
-
cmd.arg(arg);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
let output = cmd.output().context("Failed to run git log")?;
|
|
396
|
-
|
|
397
|
-
if !output.status.success() {
|
|
398
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
399
|
-
eprintln!("{}", stderr);
|
|
400
|
-
// Propagate git's exit code
|
|
401
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
405
|
-
|
|
406
|
-
if verbose > 0 {
|
|
407
|
-
eprintln!("Git log output:");
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Post-process: truncate long messages, cap lines only if RTK set the default
|
|
411
|
-
let filtered = filter_log_output(&stdout, limit, user_set_limit);
|
|
412
|
-
println!("{}", filtered);
|
|
413
|
-
|
|
414
|
-
timer.track(
|
|
415
|
-
&format!("git log {}", args.join(" ")),
|
|
416
|
-
&format!("rtk git log {}", args.join(" ")),
|
|
417
|
-
&stdout,
|
|
418
|
-
&filtered,
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
Ok(())
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/// Filter git log output: truncate long messages, cap lines
|
|
425
|
-
/// Parse the user-specified limit from git log args.
|
|
426
|
-
/// Handles: -20, -n 20, --max-count=20, --max-count 20
|
|
427
|
-
fn parse_user_limit(args: &[String]) -> Option<usize> {
|
|
428
|
-
let mut iter = args.iter();
|
|
429
|
-
while let Some(arg) = iter.next() {
|
|
430
|
-
// -20 (combined digit form)
|
|
431
|
-
if arg.starts_with('-')
|
|
432
|
-
&& arg.len() > 1
|
|
433
|
-
&& arg.chars().nth(1).map_or(false, |c| c.is_ascii_digit())
|
|
434
|
-
{
|
|
435
|
-
if let Ok(n) = arg[1..].parse::<usize>() {
|
|
436
|
-
return Some(n);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
// -n 20 (two-token form)
|
|
440
|
-
if arg == "-n" {
|
|
441
|
-
if let Some(next) = iter.next() {
|
|
442
|
-
if let Ok(n) = next.parse::<usize>() {
|
|
443
|
-
return Some(n);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
// --max-count=20
|
|
448
|
-
if let Some(rest) = arg.strip_prefix("--max-count=") {
|
|
449
|
-
if let Ok(n) = rest.parse::<usize>() {
|
|
450
|
-
return Some(n);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
// --max-count 20 (two-token form)
|
|
454
|
-
if arg == "--max-count" {
|
|
455
|
-
if let Some(next) = iter.next() {
|
|
456
|
-
if let Ok(n) = next.parse::<usize>() {
|
|
457
|
-
return Some(n);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
None
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/// When `user_set_limit` is true, the user explicitly passed `-N` to git log,
|
|
466
|
-
/// so we skip line capping (git already returns exactly N commits) and use a
|
|
467
|
-
/// wider truncation threshold (120 chars) to preserve commit context that LLMs
|
|
468
|
-
/// need for rebase/squash operations.
|
|
469
|
-
fn filter_log_output(output: &str, limit: usize, user_set_limit: bool) -> String {
|
|
470
|
-
let lines: Vec<&str> = output.lines().collect();
|
|
471
|
-
|
|
472
|
-
let truncate_width = if user_set_limit { 120 } else { 80 };
|
|
473
|
-
|
|
474
|
-
let iter = lines.iter();
|
|
475
|
-
let capped: Vec<String> = if user_set_limit {
|
|
476
|
-
// User chose the limit → git already returned the right number of commits
|
|
477
|
-
iter.map(|line| truncate_line(line, truncate_width))
|
|
478
|
-
.collect()
|
|
479
|
-
} else {
|
|
480
|
-
// RTK default → cap output lines
|
|
481
|
-
iter.take(limit)
|
|
482
|
-
.map(|line| truncate_line(line, truncate_width))
|
|
483
|
-
.collect()
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
capped.join("\n").trim().to_string()
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/// Truncate a single line to `width` characters, appending "..." if needed
|
|
490
|
-
fn truncate_line(line: &str, width: usize) -> String {
|
|
491
|
-
if line.chars().count() > width {
|
|
492
|
-
let truncated: String = line.chars().take(width - 3).collect();
|
|
493
|
-
format!("{}...", truncated)
|
|
494
|
-
} else {
|
|
495
|
-
line.to_string()
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/// Format porcelain output into compact RTK status display
|
|
500
|
-
fn format_status_output(porcelain: &str) -> String {
|
|
501
|
-
let lines: Vec<&str> = porcelain.lines().collect();
|
|
502
|
-
|
|
503
|
-
if lines.is_empty() {
|
|
504
|
-
return "Clean working tree".to_string();
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
let mut output = String::new();
|
|
508
|
-
|
|
509
|
-
// Parse branch info
|
|
510
|
-
if let Some(branch_line) = lines.first() {
|
|
511
|
-
if branch_line.starts_with("##") {
|
|
512
|
-
let branch = branch_line.trim_start_matches("## ");
|
|
513
|
-
output.push_str(&format!("📌 {}\n", branch));
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Count changes by type
|
|
518
|
-
let mut staged = 0;
|
|
519
|
-
let mut modified = 0;
|
|
520
|
-
let mut untracked = 0;
|
|
521
|
-
let mut conflicts = 0;
|
|
522
|
-
|
|
523
|
-
let mut staged_files = Vec::new();
|
|
524
|
-
let mut modified_files = Vec::new();
|
|
525
|
-
let mut untracked_files = Vec::new();
|
|
526
|
-
|
|
527
|
-
for line in lines.iter().skip(1) {
|
|
528
|
-
if line.len() < 3 {
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
let status = line.get(0..2).unwrap_or(" ");
|
|
532
|
-
let file = line.get(3..).unwrap_or("");
|
|
533
|
-
|
|
534
|
-
match status.chars().next().unwrap_or(' ') {
|
|
535
|
-
'M' | 'A' | 'D' | 'R' | 'C' => {
|
|
536
|
-
staged += 1;
|
|
537
|
-
staged_files.push(file);
|
|
538
|
-
}
|
|
539
|
-
'U' => conflicts += 1,
|
|
540
|
-
_ => {}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
match status.chars().nth(1).unwrap_or(' ') {
|
|
544
|
-
'M' | 'D' => {
|
|
545
|
-
modified += 1;
|
|
546
|
-
modified_files.push(file);
|
|
547
|
-
}
|
|
548
|
-
_ => {}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if status == "??" {
|
|
552
|
-
untracked += 1;
|
|
553
|
-
untracked_files.push(file);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Build summary
|
|
558
|
-
if staged > 0 {
|
|
559
|
-
output.push_str(&format!("✅ Staged: {} files\n", staged));
|
|
560
|
-
for f in staged_files.iter().take(5) {
|
|
561
|
-
output.push_str(&format!(" {}\n", f));
|
|
562
|
-
}
|
|
563
|
-
if staged_files.len() > 5 {
|
|
564
|
-
output.push_str(&format!(" ... +{} more\n", staged_files.len() - 5));
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if modified > 0 {
|
|
569
|
-
output.push_str(&format!("📝 Modified: {} files\n", modified));
|
|
570
|
-
for f in modified_files.iter().take(5) {
|
|
571
|
-
output.push_str(&format!(" {}\n", f));
|
|
572
|
-
}
|
|
573
|
-
if modified_files.len() > 5 {
|
|
574
|
-
output.push_str(&format!(" ... +{} more\n", modified_files.len() - 5));
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
if untracked > 0 {
|
|
579
|
-
output.push_str(&format!("❓ Untracked: {} files\n", untracked));
|
|
580
|
-
for f in untracked_files.iter().take(3) {
|
|
581
|
-
output.push_str(&format!(" {}\n", f));
|
|
582
|
-
}
|
|
583
|
-
if untracked_files.len() > 3 {
|
|
584
|
-
output.push_str(&format!(" ... +{} more\n", untracked_files.len() - 3));
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if conflicts > 0 {
|
|
589
|
-
output.push_str(&format!("⚠️ Conflicts: {} files\n", conflicts));
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
output.trim_end().to_string()
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/// Minimal filtering for git status with user-provided args
|
|
596
|
-
fn filter_status_with_args(output: &str) -> String {
|
|
597
|
-
let mut result = Vec::new();
|
|
598
|
-
|
|
599
|
-
for line in output.lines() {
|
|
600
|
-
let trimmed = line.trim();
|
|
601
|
-
|
|
602
|
-
// Skip empty lines
|
|
603
|
-
if trimmed.is_empty() {
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Skip git hints - can appear at start or within line
|
|
608
|
-
if trimmed.starts_with("(use \"git")
|
|
609
|
-
|| trimmed.starts_with("(create/copy files")
|
|
610
|
-
|| trimmed.contains("(use \"git add")
|
|
611
|
-
|| trimmed.contains("(use \"git restore")
|
|
612
|
-
{
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Special case: clean working tree
|
|
617
|
-
if trimmed.contains("nothing to commit") && trimmed.contains("working tree clean") {
|
|
618
|
-
result.push(trimmed.to_string());
|
|
619
|
-
break;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
result.push(line.to_string());
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if result.is_empty() {
|
|
626
|
-
"ok ✓".to_string()
|
|
627
|
-
} else {
|
|
628
|
-
result.join("\n")
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
fn run_status(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
|
|
633
|
-
let timer = tracking::TimedExecution::start();
|
|
634
|
-
|
|
635
|
-
// If user provided flags, apply minimal filtering
|
|
636
|
-
if !args.is_empty() {
|
|
637
|
-
let output = git_cmd(global_args)
|
|
638
|
-
.arg("status")
|
|
639
|
-
.args(args)
|
|
640
|
-
.output()
|
|
641
|
-
.context("Failed to run git status")?;
|
|
642
|
-
|
|
643
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
644
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
645
|
-
|
|
646
|
-
if verbose > 0 || !stderr.is_empty() {
|
|
647
|
-
eprint!("{}", stderr);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Apply minimal filtering: strip ANSI, remove hints, empty lines
|
|
651
|
-
let filtered = filter_status_with_args(&stdout);
|
|
652
|
-
print!("{}", filtered);
|
|
653
|
-
|
|
654
|
-
timer.track(
|
|
655
|
-
&format!("git status {}", args.join(" ")),
|
|
656
|
-
&format!("rtk git status {}", args.join(" ")),
|
|
657
|
-
&stdout,
|
|
658
|
-
&filtered,
|
|
659
|
-
);
|
|
660
|
-
|
|
661
|
-
return Ok(());
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Default RTK compact mode (no args provided)
|
|
665
|
-
// Get raw git status for tracking
|
|
666
|
-
let raw_output = git_cmd(global_args)
|
|
667
|
-
.args(["status"])
|
|
668
|
-
.output()
|
|
669
|
-
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
|
670
|
-
.unwrap_or_default();
|
|
671
|
-
|
|
672
|
-
let output = git_cmd(global_args)
|
|
673
|
-
.args(["status", "--porcelain", "-b"])
|
|
674
|
-
.output()
|
|
675
|
-
.context("Failed to run git status")?;
|
|
676
|
-
|
|
677
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
678
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
679
|
-
|
|
680
|
-
if !stderr.is_empty() && stderr.contains("not a git repository") {
|
|
681
|
-
let message = "Not a git repository".to_string();
|
|
682
|
-
eprintln!("{}", message);
|
|
683
|
-
timer.track("git status", "rtk git status", &raw_output, &message);
|
|
684
|
-
std::process::exit(output.status.code().unwrap_or(128));
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
let formatted = format_status_output(&stdout);
|
|
688
|
-
|
|
689
|
-
println!("{}", formatted);
|
|
690
|
-
|
|
691
|
-
// Track for statistics
|
|
692
|
-
timer.track("git status", "rtk git status", &raw_output, &formatted);
|
|
693
|
-
|
|
694
|
-
Ok(())
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
fn run_add(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
|
|
698
|
-
let timer = tracking::TimedExecution::start();
|
|
699
|
-
|
|
700
|
-
let mut cmd = git_cmd(global_args);
|
|
701
|
-
cmd.arg("add");
|
|
702
|
-
|
|
703
|
-
// Pass all arguments directly to git (flags like -A, -p, --all, etc.)
|
|
704
|
-
if args.is_empty() {
|
|
705
|
-
cmd.arg(".");
|
|
706
|
-
} else {
|
|
707
|
-
for arg in args {
|
|
708
|
-
cmd.arg(arg);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
let output = cmd.output().context("Failed to run git add")?;
|
|
713
|
-
|
|
714
|
-
if verbose > 0 {
|
|
715
|
-
eprintln!("git add executed");
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
let raw_output = format!(
|
|
719
|
-
"{}\n{}",
|
|
720
|
-
String::from_utf8_lossy(&output.stdout),
|
|
721
|
-
String::from_utf8_lossy(&output.stderr)
|
|
722
|
-
);
|
|
723
|
-
|
|
724
|
-
if output.status.success() {
|
|
725
|
-
// Count what was added
|
|
726
|
-
let status_output = git_cmd(global_args)
|
|
727
|
-
.args(["diff", "--cached", "--stat", "--shortstat"])
|
|
728
|
-
.output()
|
|
729
|
-
.context("Failed to check staged files")?;
|
|
730
|
-
|
|
731
|
-
let stat = String::from_utf8_lossy(&status_output.stdout);
|
|
732
|
-
let compact = if stat.trim().is_empty() {
|
|
733
|
-
"ok (nothing to add)".to_string()
|
|
734
|
-
} else {
|
|
735
|
-
// Parse "1 file changed, 5 insertions(+)" format
|
|
736
|
-
let short = stat.lines().last().unwrap_or("").trim();
|
|
737
|
-
if short.is_empty() {
|
|
738
|
-
"ok ✓".to_string()
|
|
739
|
-
} else {
|
|
740
|
-
format!("ok ✓ {}", short)
|
|
741
|
-
}
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
println!("{}", compact);
|
|
745
|
-
|
|
746
|
-
timer.track(
|
|
747
|
-
&format!("git add {}", args.join(" ")),
|
|
748
|
-
&format!("rtk git add {}", args.join(" ")),
|
|
749
|
-
&raw_output,
|
|
750
|
-
&compact,
|
|
751
|
-
);
|
|
752
|
-
} else {
|
|
753
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
754
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
755
|
-
eprintln!("FAILED: git add");
|
|
756
|
-
if !stderr.trim().is_empty() {
|
|
757
|
-
eprintln!("{}", stderr);
|
|
758
|
-
}
|
|
759
|
-
if !stdout.trim().is_empty() {
|
|
760
|
-
eprintln!("{}", stdout);
|
|
761
|
-
}
|
|
762
|
-
// Propagate git's exit code
|
|
763
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
Ok(())
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
fn build_commit_command(args: &[String], global_args: &[String]) -> Command {
|
|
770
|
-
let mut cmd = git_cmd(global_args);
|
|
771
|
-
cmd.arg("commit");
|
|
772
|
-
for arg in args {
|
|
773
|
-
cmd.arg(arg);
|
|
774
|
-
}
|
|
775
|
-
cmd
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
fn run_commit(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
|
|
779
|
-
let timer = tracking::TimedExecution::start();
|
|
780
|
-
|
|
781
|
-
let original_cmd = format!("git commit {}", args.join(" "));
|
|
782
|
-
|
|
783
|
-
if verbose > 0 {
|
|
784
|
-
eprintln!("{}", original_cmd);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
let output = build_commit_command(args, global_args)
|
|
788
|
-
.output()
|
|
789
|
-
.context("Failed to run git commit")?;
|
|
790
|
-
|
|
791
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
792
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
793
|
-
let raw_output = format!("{}\n{}", stdout, stderr);
|
|
794
|
-
|
|
795
|
-
if output.status.success() {
|
|
796
|
-
// Extract commit hash from output like "[main abc1234] message"
|
|
797
|
-
let compact = if let Some(line) = stdout.lines().next() {
|
|
798
|
-
if let Some(hash_start) = line.find(' ') {
|
|
799
|
-
let hash = line[1..hash_start].split(' ').last().unwrap_or("");
|
|
800
|
-
if !hash.is_empty() && hash.len() >= 7 {
|
|
801
|
-
format!("ok ✓ {}", &hash[..7.min(hash.len())])
|
|
802
|
-
} else {
|
|
803
|
-
"ok ✓".to_string()
|
|
804
|
-
}
|
|
805
|
-
} else {
|
|
806
|
-
"ok ✓".to_string()
|
|
807
|
-
}
|
|
808
|
-
} else {
|
|
809
|
-
"ok ✓".to_string()
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
println!("{}", compact);
|
|
813
|
-
|
|
814
|
-
timer.track(&original_cmd, "rtk git commit", &raw_output, &compact);
|
|
815
|
-
} else {
|
|
816
|
-
if stderr.contains("nothing to commit") || stdout.contains("nothing to commit") {
|
|
817
|
-
println!("ok (nothing to commit)");
|
|
818
|
-
timer.track(
|
|
819
|
-
&original_cmd,
|
|
820
|
-
"rtk git commit",
|
|
821
|
-
&raw_output,
|
|
822
|
-
"ok (nothing to commit)",
|
|
823
|
-
);
|
|
824
|
-
} else {
|
|
825
|
-
eprintln!("FAILED: git commit");
|
|
826
|
-
if !stderr.trim().is_empty() {
|
|
827
|
-
eprintln!("{}", stderr);
|
|
828
|
-
}
|
|
829
|
-
if !stdout.trim().is_empty() {
|
|
830
|
-
eprintln!("{}", stdout);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
Ok(())
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
fn run_push(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
|
|
839
|
-
let timer = tracking::TimedExecution::start();
|
|
840
|
-
|
|
841
|
-
if verbose > 0 {
|
|
842
|
-
eprintln!("git push");
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
let mut cmd = git_cmd(global_args);
|
|
846
|
-
cmd.arg("push");
|
|
847
|
-
for arg in args {
|
|
848
|
-
cmd.arg(arg);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
let output = cmd.output().context("Failed to run git push")?;
|
|
852
|
-
|
|
853
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
854
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
855
|
-
let raw = format!("{}{}", stdout, stderr);
|
|
856
|
-
|
|
857
|
-
if output.status.success() {
|
|
858
|
-
let compact = if stderr.contains("Everything up-to-date") {
|
|
859
|
-
"ok (up-to-date)".to_string()
|
|
860
|
-
} else {
|
|
861
|
-
let mut result = String::new();
|
|
862
|
-
for line in stderr.lines() {
|
|
863
|
-
if line.contains("->") {
|
|
864
|
-
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
865
|
-
if parts.len() >= 3 {
|
|
866
|
-
result = format!("ok ✓ {}", parts[parts.len() - 1]);
|
|
867
|
-
break;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
if !result.is_empty() {
|
|
872
|
-
result
|
|
873
|
-
} else {
|
|
874
|
-
"ok ✓".to_string()
|
|
875
|
-
}
|
|
876
|
-
};
|
|
877
|
-
|
|
878
|
-
println!("{}", compact);
|
|
879
|
-
|
|
880
|
-
timer.track(
|
|
881
|
-
&format!("git push {}", args.join(" ")),
|
|
882
|
-
&format!("rtk git push {}", args.join(" ")),
|
|
883
|
-
&raw,
|
|
884
|
-
&compact,
|
|
885
|
-
);
|
|
886
|
-
} else {
|
|
887
|
-
eprintln!("FAILED: git push");
|
|
888
|
-
if !stderr.trim().is_empty() {
|
|
889
|
-
eprintln!("{}", stderr);
|
|
890
|
-
}
|
|
891
|
-
if !stdout.trim().is_empty() {
|
|
892
|
-
eprintln!("{}", stdout);
|
|
893
|
-
}
|
|
894
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
Ok(())
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
fn run_pull(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
|
|
901
|
-
let timer = tracking::TimedExecution::start();
|
|
902
|
-
|
|
903
|
-
if verbose > 0 {
|
|
904
|
-
eprintln!("git pull");
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
let mut cmd = git_cmd(global_args);
|
|
908
|
-
cmd.arg("pull");
|
|
909
|
-
for arg in args {
|
|
910
|
-
cmd.arg(arg);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
let output = cmd.output().context("Failed to run git pull")?;
|
|
914
|
-
|
|
915
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
916
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
917
|
-
let raw_output = format!("{}\n{}", stdout, stderr);
|
|
918
|
-
|
|
919
|
-
if output.status.success() {
|
|
920
|
-
let compact =
|
|
921
|
-
if stdout.contains("Already up to date") || stdout.contains("Already up-to-date") {
|
|
922
|
-
"ok (up-to-date)".to_string()
|
|
923
|
-
} else {
|
|
924
|
-
// Count files changed
|
|
925
|
-
let mut files = 0;
|
|
926
|
-
let mut insertions = 0;
|
|
927
|
-
let mut deletions = 0;
|
|
928
|
-
|
|
929
|
-
for line in stdout.lines() {
|
|
930
|
-
if line.contains("file") && line.contains("changed") {
|
|
931
|
-
// Parse "3 files changed, 10 insertions(+), 2 deletions(-)"
|
|
932
|
-
for part in line.split(',') {
|
|
933
|
-
let part = part.trim();
|
|
934
|
-
if part.contains("file") {
|
|
935
|
-
files = part
|
|
936
|
-
.split_whitespace()
|
|
937
|
-
.next()
|
|
938
|
-
.and_then(|n| n.parse().ok())
|
|
939
|
-
.unwrap_or(0);
|
|
940
|
-
} else if part.contains("insertion") {
|
|
941
|
-
insertions = part
|
|
942
|
-
.split_whitespace()
|
|
943
|
-
.next()
|
|
944
|
-
.and_then(|n| n.parse().ok())
|
|
945
|
-
.unwrap_or(0);
|
|
946
|
-
} else if part.contains("deletion") {
|
|
947
|
-
deletions = part
|
|
948
|
-
.split_whitespace()
|
|
949
|
-
.next()
|
|
950
|
-
.and_then(|n| n.parse().ok())
|
|
951
|
-
.unwrap_or(0);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
if files > 0 {
|
|
958
|
-
format!("ok ✓ {} files +{} -{}", files, insertions, deletions)
|
|
959
|
-
} else {
|
|
960
|
-
"ok ✓".to_string()
|
|
961
|
-
}
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
println!("{}", compact);
|
|
965
|
-
|
|
966
|
-
timer.track(
|
|
967
|
-
&format!("git pull {}", args.join(" ")),
|
|
968
|
-
&format!("rtk git pull {}", args.join(" ")),
|
|
969
|
-
&raw_output,
|
|
970
|
-
&compact,
|
|
971
|
-
);
|
|
972
|
-
} else {
|
|
973
|
-
eprintln!("FAILED: git pull");
|
|
974
|
-
if !stderr.trim().is_empty() {
|
|
975
|
-
eprintln!("{}", stderr);
|
|
976
|
-
}
|
|
977
|
-
if !stdout.trim().is_empty() {
|
|
978
|
-
eprintln!("{}", stdout);
|
|
979
|
-
}
|
|
980
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
Ok(())
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
fn run_branch(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
|
|
987
|
-
let timer = tracking::TimedExecution::start();
|
|
988
|
-
|
|
989
|
-
if verbose > 0 {
|
|
990
|
-
eprintln!("git branch");
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Detect write operations: delete, rename, copy
|
|
994
|
-
let has_action_flag = args
|
|
995
|
-
.iter()
|
|
996
|
-
.any(|a| a == "-d" || a == "-D" || a == "-m" || a == "-M" || a == "-c" || a == "-C");
|
|
997
|
-
|
|
998
|
-
// Detect list-mode flags
|
|
999
|
-
let has_list_flag = args.iter().any(|a| {
|
|
1000
|
-
a == "-a"
|
|
1001
|
-
|| a == "--all"
|
|
1002
|
-
|| a == "-r"
|
|
1003
|
-
|| a == "--remotes"
|
|
1004
|
-
|| a == "--list"
|
|
1005
|
-
|| a == "--merged"
|
|
1006
|
-
|| a == "--no-merged"
|
|
1007
|
-
|| a == "--contains"
|
|
1008
|
-
|| a == "--no-contains"
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
// Detect positional arguments (not flags) — indicates branch creation
|
|
1012
|
-
let has_positional_arg = args.iter().any(|a| !a.starts_with('-'));
|
|
1013
|
-
|
|
1014
|
-
// Write operation: action flags, or positional args without list flags (= branch creation)
|
|
1015
|
-
if has_action_flag || (has_positional_arg && !has_list_flag) {
|
|
1016
|
-
let mut cmd = git_cmd(global_args);
|
|
1017
|
-
cmd.arg("branch");
|
|
1018
|
-
for arg in args {
|
|
1019
|
-
cmd.arg(arg);
|
|
1020
|
-
}
|
|
1021
|
-
let output = cmd.output().context("Failed to run git branch")?;
|
|
1022
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1023
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
1024
|
-
let combined = format!("{}{}", stdout, stderr);
|
|
1025
|
-
|
|
1026
|
-
let msg = if output.status.success() {
|
|
1027
|
-
"ok ✓"
|
|
1028
|
-
} else {
|
|
1029
|
-
&combined
|
|
1030
|
-
};
|
|
1031
|
-
|
|
1032
|
-
timer.track(
|
|
1033
|
-
&format!("git branch {}", args.join(" ")),
|
|
1034
|
-
&format!("rtk git branch {}", args.join(" ")),
|
|
1035
|
-
&combined,
|
|
1036
|
-
msg,
|
|
1037
|
-
);
|
|
1038
|
-
|
|
1039
|
-
if output.status.success() {
|
|
1040
|
-
println!("ok ✓");
|
|
1041
|
-
} else {
|
|
1042
|
-
eprintln!("FAILED: git branch {}", args.join(" "));
|
|
1043
|
-
if !stderr.trim().is_empty() {
|
|
1044
|
-
eprintln!("{}", stderr);
|
|
1045
|
-
}
|
|
1046
|
-
if !stdout.trim().is_empty() {
|
|
1047
|
-
eprintln!("{}", stdout);
|
|
1048
|
-
}
|
|
1049
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1050
|
-
}
|
|
1051
|
-
return Ok(());
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// List mode: show compact branch list
|
|
1055
|
-
let mut cmd = git_cmd(global_args);
|
|
1056
|
-
cmd.arg("branch");
|
|
1057
|
-
if !has_list_flag {
|
|
1058
|
-
cmd.arg("-a");
|
|
1059
|
-
}
|
|
1060
|
-
cmd.arg("--no-color");
|
|
1061
|
-
for arg in args {
|
|
1062
|
-
cmd.arg(arg);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
let output = cmd.output().context("Failed to run git branch")?;
|
|
1066
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1067
|
-
let raw = stdout.to_string();
|
|
1068
|
-
|
|
1069
|
-
let filtered = filter_branch_output(&stdout);
|
|
1070
|
-
println!("{}", filtered);
|
|
1071
|
-
|
|
1072
|
-
timer.track(
|
|
1073
|
-
&format!("git branch {}", args.join(" ")),
|
|
1074
|
-
&format!("rtk git branch {}", args.join(" ")),
|
|
1075
|
-
&raw,
|
|
1076
|
-
&filtered,
|
|
1077
|
-
);
|
|
1078
|
-
|
|
1079
|
-
Ok(())
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
fn filter_branch_output(output: &str) -> String {
|
|
1083
|
-
let mut current = String::new();
|
|
1084
|
-
let mut local: Vec<String> = Vec::new();
|
|
1085
|
-
let mut remote: Vec<String> = Vec::new();
|
|
1086
|
-
|
|
1087
|
-
for line in output.lines() {
|
|
1088
|
-
let line = line.trim();
|
|
1089
|
-
if line.is_empty() {
|
|
1090
|
-
continue;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
if let Some(branch) = line.strip_prefix("* ") {
|
|
1094
|
-
current = branch.to_string();
|
|
1095
|
-
} else if line.starts_with("remotes/origin/") {
|
|
1096
|
-
let branch = line.strip_prefix("remotes/origin/").unwrap_or(line);
|
|
1097
|
-
// Skip HEAD pointer
|
|
1098
|
-
if branch.starts_with("HEAD ") {
|
|
1099
|
-
continue;
|
|
1100
|
-
}
|
|
1101
|
-
remote.push(branch.to_string());
|
|
1102
|
-
} else {
|
|
1103
|
-
local.push(line.to_string());
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
let mut result = Vec::new();
|
|
1108
|
-
result.push(format!("* {}", current));
|
|
1109
|
-
|
|
1110
|
-
if !local.is_empty() {
|
|
1111
|
-
for b in &local {
|
|
1112
|
-
result.push(format!(" {}", b));
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
if !remote.is_empty() {
|
|
1117
|
-
// Filter out remotes that already exist locally
|
|
1118
|
-
let remote_only: Vec<&String> = remote
|
|
1119
|
-
.iter()
|
|
1120
|
-
.filter(|r| *r != ¤t && !local.contains(r))
|
|
1121
|
-
.collect();
|
|
1122
|
-
if !remote_only.is_empty() {
|
|
1123
|
-
result.push(format!(" remote-only ({}):", remote_only.len()));
|
|
1124
|
-
for b in remote_only.iter().take(10) {
|
|
1125
|
-
result.push(format!(" {}", b));
|
|
1126
|
-
}
|
|
1127
|
-
if remote_only.len() > 10 {
|
|
1128
|
-
result.push(format!(" ... +{} more", remote_only.len() - 10));
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
result.join("\n")
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
fn run_fetch(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
|
|
1137
|
-
let timer = tracking::TimedExecution::start();
|
|
1138
|
-
|
|
1139
|
-
if verbose > 0 {
|
|
1140
|
-
eprintln!("git fetch");
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
let mut cmd = git_cmd(global_args);
|
|
1144
|
-
cmd.arg("fetch");
|
|
1145
|
-
for arg in args {
|
|
1146
|
-
cmd.arg(arg);
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
let output = cmd.output().context("Failed to run git fetch")?;
|
|
1150
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1151
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
1152
|
-
let raw = format!("{}{}", stdout, stderr);
|
|
1153
|
-
|
|
1154
|
-
if !output.status.success() {
|
|
1155
|
-
eprintln!("FAILED: git fetch");
|
|
1156
|
-
if !stderr.trim().is_empty() {
|
|
1157
|
-
eprintln!("{}", stderr);
|
|
1158
|
-
}
|
|
1159
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Count new refs from stderr (git fetch outputs to stderr)
|
|
1163
|
-
let new_refs: usize = stderr
|
|
1164
|
-
.lines()
|
|
1165
|
-
.filter(|l| l.contains("->") || l.contains("[new"))
|
|
1166
|
-
.count();
|
|
1167
|
-
|
|
1168
|
-
let msg = if new_refs > 0 {
|
|
1169
|
-
format!("ok fetched ({} new refs)", new_refs)
|
|
1170
|
-
} else {
|
|
1171
|
-
"ok fetched".to_string()
|
|
1172
|
-
};
|
|
1173
|
-
|
|
1174
|
-
println!("{}", msg);
|
|
1175
|
-
timer.track("git fetch", "rtk git fetch", &raw, &msg);
|
|
1176
|
-
|
|
1177
|
-
Ok(())
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
fn run_stash(
|
|
1181
|
-
subcommand: Option<&str>,
|
|
1182
|
-
args: &[String],
|
|
1183
|
-
verbose: u8,
|
|
1184
|
-
global_args: &[String],
|
|
1185
|
-
) -> Result<()> {
|
|
1186
|
-
let timer = tracking::TimedExecution::start();
|
|
1187
|
-
|
|
1188
|
-
if verbose > 0 {
|
|
1189
|
-
eprintln!("git stash {:?}", subcommand);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
match subcommand {
|
|
1193
|
-
Some("list") => {
|
|
1194
|
-
let output = git_cmd(global_args)
|
|
1195
|
-
.args(["stash", "list"])
|
|
1196
|
-
.output()
|
|
1197
|
-
.context("Failed to run git stash list")?;
|
|
1198
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1199
|
-
let raw = stdout.to_string();
|
|
1200
|
-
|
|
1201
|
-
if stdout.trim().is_empty() {
|
|
1202
|
-
let msg = "No stashes";
|
|
1203
|
-
println!("{}", msg);
|
|
1204
|
-
timer.track("git stash list", "rtk git stash list", &raw, msg);
|
|
1205
|
-
return Ok(());
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
let filtered = filter_stash_list(&stdout);
|
|
1209
|
-
println!("{}", filtered);
|
|
1210
|
-
timer.track("git stash list", "rtk git stash list", &raw, &filtered);
|
|
1211
|
-
}
|
|
1212
|
-
Some("show") => {
|
|
1213
|
-
let mut cmd = git_cmd(global_args);
|
|
1214
|
-
cmd.args(["stash", "show", "-p"]);
|
|
1215
|
-
for arg in args {
|
|
1216
|
-
cmd.arg(arg);
|
|
1217
|
-
}
|
|
1218
|
-
let output = cmd.output().context("Failed to run git stash show")?;
|
|
1219
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1220
|
-
let raw = stdout.to_string();
|
|
1221
|
-
|
|
1222
|
-
let filtered = if stdout.trim().is_empty() {
|
|
1223
|
-
let msg = "Empty stash";
|
|
1224
|
-
println!("{}", msg);
|
|
1225
|
-
msg.to_string()
|
|
1226
|
-
} else {
|
|
1227
|
-
let compacted = compact_diff(&stdout, 100);
|
|
1228
|
-
println!("{}", compacted);
|
|
1229
|
-
compacted
|
|
1230
|
-
};
|
|
1231
|
-
|
|
1232
|
-
timer.track("git stash show", "rtk git stash show", &raw, &filtered);
|
|
1233
|
-
}
|
|
1234
|
-
Some("pop") | Some("apply") | Some("drop") | Some("push") => {
|
|
1235
|
-
let sub = subcommand.unwrap();
|
|
1236
|
-
let mut cmd = git_cmd(global_args);
|
|
1237
|
-
cmd.args(["stash", sub]);
|
|
1238
|
-
for arg in args {
|
|
1239
|
-
cmd.arg(arg);
|
|
1240
|
-
}
|
|
1241
|
-
let output = cmd.output().context("Failed to run git stash")?;
|
|
1242
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1243
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
1244
|
-
let combined = format!("{}{}", stdout, stderr);
|
|
1245
|
-
|
|
1246
|
-
let msg = if output.status.success() {
|
|
1247
|
-
let msg = format!("ok stash {}", sub);
|
|
1248
|
-
println!("{}", msg);
|
|
1249
|
-
msg
|
|
1250
|
-
} else {
|
|
1251
|
-
eprintln!("FAILED: git stash {}", sub);
|
|
1252
|
-
if !stderr.trim().is_empty() {
|
|
1253
|
-
eprintln!("{}", stderr);
|
|
1254
|
-
}
|
|
1255
|
-
combined.clone()
|
|
1256
|
-
};
|
|
1257
|
-
|
|
1258
|
-
timer.track(
|
|
1259
|
-
&format!("git stash {}", sub),
|
|
1260
|
-
&format!("rtk git stash {}", sub),
|
|
1261
|
-
&combined,
|
|
1262
|
-
&msg,
|
|
1263
|
-
);
|
|
1264
|
-
|
|
1265
|
-
if !output.status.success() {
|
|
1266
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
_ => {
|
|
1270
|
-
// Default: git stash (push)
|
|
1271
|
-
let mut cmd = git_cmd(global_args);
|
|
1272
|
-
cmd.arg("stash");
|
|
1273
|
-
for arg in args {
|
|
1274
|
-
cmd.arg(arg);
|
|
1275
|
-
}
|
|
1276
|
-
let output = cmd.output().context("Failed to run git stash")?;
|
|
1277
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1278
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
1279
|
-
let combined = format!("{}{}", stdout, stderr);
|
|
1280
|
-
|
|
1281
|
-
let msg = if output.status.success() {
|
|
1282
|
-
if stdout.contains("No local changes") {
|
|
1283
|
-
let msg = "ok (nothing to stash)";
|
|
1284
|
-
println!("{}", msg);
|
|
1285
|
-
msg.to_string()
|
|
1286
|
-
} else {
|
|
1287
|
-
let msg = "ok stashed";
|
|
1288
|
-
println!("{}", msg);
|
|
1289
|
-
msg.to_string()
|
|
1290
|
-
}
|
|
1291
|
-
} else {
|
|
1292
|
-
eprintln!("FAILED: git stash");
|
|
1293
|
-
if !stderr.trim().is_empty() {
|
|
1294
|
-
eprintln!("{}", stderr);
|
|
1295
|
-
}
|
|
1296
|
-
combined.clone()
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
timer.track("git stash", "rtk git stash", &combined, &msg);
|
|
1300
|
-
|
|
1301
|
-
if !output.status.success() {
|
|
1302
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
Ok(())
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
fn filter_stash_list(output: &str) -> String {
|
|
1311
|
-
// Format: "stash@{0}: WIP on main: abc1234 commit message"
|
|
1312
|
-
let mut result = Vec::new();
|
|
1313
|
-
for line in output.lines() {
|
|
1314
|
-
if let Some(colon_pos) = line.find(": ") {
|
|
1315
|
-
let index = &line[..colon_pos];
|
|
1316
|
-
let rest = &line[colon_pos + 2..];
|
|
1317
|
-
// Compact: strip "WIP on branch:" prefix if present
|
|
1318
|
-
let message = if let Some(second_colon) = rest.find(": ") {
|
|
1319
|
-
rest[second_colon + 2..].trim()
|
|
1320
|
-
} else {
|
|
1321
|
-
rest.trim()
|
|
1322
|
-
};
|
|
1323
|
-
result.push(format!("{}: {}", index, message));
|
|
1324
|
-
} else {
|
|
1325
|
-
result.push(line.to_string());
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
result.join("\n")
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
fn run_worktree(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
|
|
1332
|
-
let timer = tracking::TimedExecution::start();
|
|
1333
|
-
|
|
1334
|
-
if verbose > 0 {
|
|
1335
|
-
eprintln!("git worktree list");
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// If args contain "add", "remove", "prune" etc., pass through
|
|
1339
|
-
let has_action = args.iter().any(|a| {
|
|
1340
|
-
a == "add" || a == "remove" || a == "prune" || a == "lock" || a == "unlock" || a == "move"
|
|
1341
|
-
});
|
|
1342
|
-
|
|
1343
|
-
if has_action {
|
|
1344
|
-
let mut cmd = git_cmd(global_args);
|
|
1345
|
-
cmd.arg("worktree");
|
|
1346
|
-
for arg in args {
|
|
1347
|
-
cmd.arg(arg);
|
|
1348
|
-
}
|
|
1349
|
-
let output = cmd.output().context("Failed to run git worktree")?;
|
|
1350
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1351
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
1352
|
-
let combined = format!("{}{}", stdout, stderr);
|
|
1353
|
-
|
|
1354
|
-
let msg = if output.status.success() {
|
|
1355
|
-
"ok ✓"
|
|
1356
|
-
} else {
|
|
1357
|
-
&combined
|
|
1358
|
-
};
|
|
1359
|
-
|
|
1360
|
-
timer.track(
|
|
1361
|
-
&format!("git worktree {}", args.join(" ")),
|
|
1362
|
-
&format!("rtk git worktree {}", args.join(" ")),
|
|
1363
|
-
&combined,
|
|
1364
|
-
msg,
|
|
1365
|
-
);
|
|
1366
|
-
|
|
1367
|
-
if output.status.success() {
|
|
1368
|
-
println!("ok ✓");
|
|
1369
|
-
} else {
|
|
1370
|
-
eprintln!("FAILED: git worktree {}", args.join(" "));
|
|
1371
|
-
if !stderr.trim().is_empty() {
|
|
1372
|
-
eprintln!("{}", stderr);
|
|
1373
|
-
}
|
|
1374
|
-
std::process::exit(output.status.code().unwrap_or(1));
|
|
1375
|
-
}
|
|
1376
|
-
return Ok(());
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// Default: list mode
|
|
1380
|
-
let output = git_cmd(global_args)
|
|
1381
|
-
.args(["worktree", "list"])
|
|
1382
|
-
.output()
|
|
1383
|
-
.context("Failed to run git worktree list")?;
|
|
1384
|
-
|
|
1385
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1386
|
-
let raw = stdout.to_string();
|
|
1387
|
-
|
|
1388
|
-
let filtered = filter_worktree_list(&stdout);
|
|
1389
|
-
println!("{}", filtered);
|
|
1390
|
-
timer.track("git worktree list", "rtk git worktree", &raw, &filtered);
|
|
1391
|
-
|
|
1392
|
-
Ok(())
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
fn filter_worktree_list(output: &str) -> String {
|
|
1396
|
-
let home = dirs::home_dir()
|
|
1397
|
-
.map(|h| h.to_string_lossy().to_string())
|
|
1398
|
-
.unwrap_or_default();
|
|
1399
|
-
|
|
1400
|
-
let mut result = Vec::new();
|
|
1401
|
-
for line in output.lines() {
|
|
1402
|
-
if line.trim().is_empty() {
|
|
1403
|
-
continue;
|
|
1404
|
-
}
|
|
1405
|
-
// Format: "/path/to/worktree abc1234 [branch]"
|
|
1406
|
-
let parts: Vec<&str> = line.split_whitespace().collect();
|
|
1407
|
-
if parts.len() >= 3 {
|
|
1408
|
-
let mut path = parts[0].to_string();
|
|
1409
|
-
if !home.is_empty() && path.starts_with(&home) {
|
|
1410
|
-
path = format!("~{}", &path[home.len()..]);
|
|
1411
|
-
}
|
|
1412
|
-
let hash = parts[1];
|
|
1413
|
-
let branch = parts[2..].join(" ");
|
|
1414
|
-
result.push(format!("{} {} {}", path, hash, branch));
|
|
1415
|
-
} else {
|
|
1416
|
-
result.push(line.to_string());
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
result.join("\n")
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
/// Runs an unsupported git subcommand by passing it through directly
|
|
1423
|
-
pub fn run_passthrough(args: &[OsString], global_args: &[String], verbose: u8) -> Result<()> {
|
|
1424
|
-
let timer = tracking::TimedExecution::start();
|
|
1425
|
-
|
|
1426
|
-
if verbose > 0 {
|
|
1427
|
-
eprintln!("git passthrough: {:?}", args);
|
|
1428
|
-
}
|
|
1429
|
-
let status = git_cmd(global_args)
|
|
1430
|
-
.args(args)
|
|
1431
|
-
.status()
|
|
1432
|
-
.context("Failed to run git")?;
|
|
1433
|
-
|
|
1434
|
-
let args_str = tracking::args_display(args);
|
|
1435
|
-
timer.track_passthrough(
|
|
1436
|
-
&format!("git {}", args_str),
|
|
1437
|
-
&format!("rtk git {} (passthrough)", args_str),
|
|
1438
|
-
);
|
|
1439
|
-
|
|
1440
|
-
if !status.success() {
|
|
1441
|
-
std::process::exit(status.code().unwrap_or(1));
|
|
1442
|
-
}
|
|
1443
|
-
Ok(())
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
#[cfg(test)]
|
|
1447
|
-
mod tests {
|
|
1448
|
-
use super::*;
|
|
1449
|
-
|
|
1450
|
-
#[test]
|
|
1451
|
-
fn test_git_cmd_no_global_args() {
|
|
1452
|
-
let cmd = git_cmd(&[]);
|
|
1453
|
-
let program = cmd.get_program();
|
|
1454
|
-
assert_eq!(program, "git");
|
|
1455
|
-
let args: Vec<_> = cmd.get_args().collect();
|
|
1456
|
-
assert!(args.is_empty());
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
#[test]
|
|
1460
|
-
fn test_git_cmd_with_directory() {
|
|
1461
|
-
let global_args = vec!["-C".to_string(), "/tmp".to_string()];
|
|
1462
|
-
let cmd = git_cmd(&global_args);
|
|
1463
|
-
let args: Vec<_> = cmd.get_args().collect();
|
|
1464
|
-
assert_eq!(args, vec!["-C", "/tmp"]);
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
#[test]
|
|
1468
|
-
fn test_git_cmd_with_multiple_global_args() {
|
|
1469
|
-
let global_args = vec![
|
|
1470
|
-
"-C".to_string(),
|
|
1471
|
-
"/tmp".to_string(),
|
|
1472
|
-
"-c".to_string(),
|
|
1473
|
-
"user.name=test".to_string(),
|
|
1474
|
-
"--git-dir".to_string(),
|
|
1475
|
-
"/foo/.git".to_string(),
|
|
1476
|
-
];
|
|
1477
|
-
let cmd = git_cmd(&global_args);
|
|
1478
|
-
let args: Vec<_> = cmd.get_args().collect();
|
|
1479
|
-
assert_eq!(
|
|
1480
|
-
args,
|
|
1481
|
-
vec![
|
|
1482
|
-
"-C",
|
|
1483
|
-
"/tmp",
|
|
1484
|
-
"-c",
|
|
1485
|
-
"user.name=test",
|
|
1486
|
-
"--git-dir",
|
|
1487
|
-
"/foo/.git"
|
|
1488
|
-
]
|
|
1489
|
-
);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
#[test]
|
|
1493
|
-
fn test_git_cmd_with_boolean_flags() {
|
|
1494
|
-
let global_args = vec!["--no-pager".to_string(), "--bare".to_string()];
|
|
1495
|
-
let cmd = git_cmd(&global_args);
|
|
1496
|
-
let args: Vec<_> = cmd.get_args().collect();
|
|
1497
|
-
assert_eq!(args, vec!["--no-pager", "--bare"]);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
#[test]
|
|
1501
|
-
fn test_compact_diff() {
|
|
1502
|
-
let diff = r#"diff --git a/foo.rs b/foo.rs
|
|
1503
|
-
--- a/foo.rs
|
|
1504
|
-
+++ b/foo.rs
|
|
1505
|
-
@@ -1,3 +1,4 @@
|
|
1506
|
-
fn main() {
|
|
1507
|
-
+ println!("hello");
|
|
1508
|
-
}
|
|
1509
|
-
"#;
|
|
1510
|
-
let result = compact_diff(diff, 100);
|
|
1511
|
-
assert!(result.contains("foo.rs"));
|
|
1512
|
-
assert!(result.contains("+"));
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
#[test]
|
|
1516
|
-
fn test_compact_diff_increased_hunk_limit() {
|
|
1517
|
-
// Build a hunk with 25 changed lines — should NOT be truncated with limit 30
|
|
1518
|
-
let mut diff =
|
|
1519
|
-
"diff --git a/big.rs b/big.rs\n--- a/big.rs\n+++ b/big.rs\n@@ -1,25 +1,25 @@\n"
|
|
1520
|
-
.to_string();
|
|
1521
|
-
for i in 1..=25 {
|
|
1522
|
-
diff.push_str(&format!("+line{}\n", i));
|
|
1523
|
-
}
|
|
1524
|
-
let result = compact_diff(&diff, 500);
|
|
1525
|
-
assert!(
|
|
1526
|
-
!result.contains("... (truncated)"),
|
|
1527
|
-
"25 lines should not be truncated with max_hunk_lines=30"
|
|
1528
|
-
);
|
|
1529
|
-
assert!(result.contains("+line25"));
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
#[test]
|
|
1533
|
-
fn test_compact_diff_increased_total_limit() {
|
|
1534
|
-
// Build a diff with 150 output result lines across multiple files — should NOT be cut at 100
|
|
1535
|
-
let mut diff = String::new();
|
|
1536
|
-
for f in 1..=5 {
|
|
1537
|
-
diff.push_str(&format!("diff --git a/file{f}.rs b/file{f}.rs\n--- a/file{f}.rs\n+++ b/file{f}.rs\n@@ -1,20 +1,20 @@\n"));
|
|
1538
|
-
for i in 1..=20 {
|
|
1539
|
-
diff.push_str(&format!("+line{f}_{i}\n"));
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
let result = compact_diff(&diff, 500);
|
|
1543
|
-
assert!(
|
|
1544
|
-
!result.contains("more changes truncated"),
|
|
1545
|
-
"5 files × 20 lines should not exceed max_lines=500"
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
#[test]
|
|
1550
|
-
fn test_is_blob_show_arg() {
|
|
1551
|
-
assert!(is_blob_show_arg("develop:modules/pairs_backtest.py"));
|
|
1552
|
-
assert!(is_blob_show_arg("HEAD:src/main.rs"));
|
|
1553
|
-
assert!(!is_blob_show_arg("--pretty=format:%h"));
|
|
1554
|
-
assert!(!is_blob_show_arg("--format=short"));
|
|
1555
|
-
assert!(!is_blob_show_arg("HEAD"));
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
#[test]
|
|
1559
|
-
fn test_filter_branch_output() {
|
|
1560
|
-
let output = "* main\n feature/auth\n fix/bug-123\n remotes/origin/HEAD -> origin/main\n remotes/origin/main\n remotes/origin/feature/auth\n remotes/origin/release/v2\n";
|
|
1561
|
-
let result = filter_branch_output(output);
|
|
1562
|
-
assert!(result.contains("* main"));
|
|
1563
|
-
assert!(result.contains("feature/auth"));
|
|
1564
|
-
assert!(result.contains("fix/bug-123"));
|
|
1565
|
-
// remote-only should show release/v2 but not main or feature/auth (already local)
|
|
1566
|
-
assert!(result.contains("remote-only"));
|
|
1567
|
-
assert!(result.contains("release/v2"));
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
#[test]
|
|
1571
|
-
fn test_filter_branch_no_remotes() {
|
|
1572
|
-
let output = "* main\n develop\n";
|
|
1573
|
-
let result = filter_branch_output(output);
|
|
1574
|
-
assert!(result.contains("* main"));
|
|
1575
|
-
assert!(result.contains("develop"));
|
|
1576
|
-
assert!(!result.contains("remote-only"));
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
#[test]
|
|
1580
|
-
fn test_filter_stash_list() {
|
|
1581
|
-
let output =
|
|
1582
|
-
"stash@{0}: WIP on main: abc1234 fix login\nstash@{1}: On feature: def5678 wip\n";
|
|
1583
|
-
let result = filter_stash_list(output);
|
|
1584
|
-
assert!(result.contains("stash@{0}: abc1234 fix login"));
|
|
1585
|
-
assert!(result.contains("stash@{1}: def5678 wip"));
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
#[test]
|
|
1589
|
-
fn test_filter_worktree_list() {
|
|
1590
|
-
let output =
|
|
1591
|
-
"/home/user/project abc1234 [main]\n/home/user/worktrees/feat def5678 [feature]\n";
|
|
1592
|
-
let result = filter_worktree_list(output);
|
|
1593
|
-
assert!(result.contains("abc1234"));
|
|
1594
|
-
assert!(result.contains("[main]"));
|
|
1595
|
-
assert!(result.contains("[feature]"));
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
#[test]
|
|
1599
|
-
fn test_format_status_output_clean() {
|
|
1600
|
-
let porcelain = "";
|
|
1601
|
-
let result = format_status_output(porcelain);
|
|
1602
|
-
assert_eq!(result, "Clean working tree");
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
#[test]
|
|
1606
|
-
fn test_format_status_output_modified_files() {
|
|
1607
|
-
let porcelain = "## main...origin/main\n M src/main.rs\n M src/lib.rs\n";
|
|
1608
|
-
let result = format_status_output(porcelain);
|
|
1609
|
-
assert!(result.contains("📌 main...origin/main"));
|
|
1610
|
-
assert!(result.contains("📝 Modified: 2 files"));
|
|
1611
|
-
assert!(result.contains("src/main.rs"));
|
|
1612
|
-
assert!(result.contains("src/lib.rs"));
|
|
1613
|
-
assert!(!result.contains("Staged"));
|
|
1614
|
-
assert!(!result.contains("Untracked"));
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
#[test]
|
|
1618
|
-
fn test_format_status_output_untracked_files() {
|
|
1619
|
-
let porcelain = "## feature/new\n?? temp.txt\n?? debug.log\n?? test.sh\n";
|
|
1620
|
-
let result = format_status_output(porcelain);
|
|
1621
|
-
assert!(result.contains("📌 feature/new"));
|
|
1622
|
-
assert!(result.contains("❓ Untracked: 3 files"));
|
|
1623
|
-
assert!(result.contains("temp.txt"));
|
|
1624
|
-
assert!(result.contains("debug.log"));
|
|
1625
|
-
assert!(result.contains("test.sh"));
|
|
1626
|
-
assert!(!result.contains("Modified"));
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
#[test]
|
|
1630
|
-
fn test_format_status_output_mixed_changes() {
|
|
1631
|
-
let porcelain = r#"## main
|
|
1632
|
-
M staged.rs
|
|
1633
|
-
M modified.rs
|
|
1634
|
-
A added.rs
|
|
1635
|
-
?? untracked.txt
|
|
1636
|
-
"#;
|
|
1637
|
-
let result = format_status_output(porcelain);
|
|
1638
|
-
assert!(result.contains("📌 main"));
|
|
1639
|
-
assert!(result.contains("✅ Staged: 2 files"));
|
|
1640
|
-
assert!(result.contains("staged.rs"));
|
|
1641
|
-
assert!(result.contains("added.rs"));
|
|
1642
|
-
assert!(result.contains("📝 Modified: 1 files"));
|
|
1643
|
-
assert!(result.contains("modified.rs"));
|
|
1644
|
-
assert!(result.contains("❓ Untracked: 1 files"));
|
|
1645
|
-
assert!(result.contains("untracked.txt"));
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
#[test]
|
|
1649
|
-
fn test_format_status_output_truncation() {
|
|
1650
|
-
// Test that >5 staged files show "... +N more"
|
|
1651
|
-
let porcelain = r#"## main
|
|
1652
|
-
M file1.rs
|
|
1653
|
-
M file2.rs
|
|
1654
|
-
M file3.rs
|
|
1655
|
-
M file4.rs
|
|
1656
|
-
M file5.rs
|
|
1657
|
-
M file6.rs
|
|
1658
|
-
M file7.rs
|
|
1659
|
-
"#;
|
|
1660
|
-
let result = format_status_output(porcelain);
|
|
1661
|
-
assert!(result.contains("✅ Staged: 7 files"));
|
|
1662
|
-
assert!(result.contains("file1.rs"));
|
|
1663
|
-
assert!(result.contains("file5.rs"));
|
|
1664
|
-
assert!(result.contains("... +2 more"));
|
|
1665
|
-
assert!(!result.contains("file6.rs"));
|
|
1666
|
-
assert!(!result.contains("file7.rs"));
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
#[test]
|
|
1670
|
-
fn test_run_passthrough_accepts_args() {
|
|
1671
|
-
// Test that run_passthrough compiles and has correct signature
|
|
1672
|
-
let _args: Vec<OsString> = vec![OsString::from("tag"), OsString::from("--list")];
|
|
1673
|
-
// Compile-time verification that the function exists with correct signature
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
#[test]
|
|
1677
|
-
fn test_filter_log_output() {
|
|
1678
|
-
let output = "abc1234 This is a commit message (2 days ago) <author>\ndef5678 Another commit (1 week ago) <other>\n";
|
|
1679
|
-
let result = filter_log_output(output, 10, false);
|
|
1680
|
-
assert!(result.contains("abc1234"));
|
|
1681
|
-
assert!(result.contains("def5678"));
|
|
1682
|
-
assert_eq!(result.lines().count(), 2);
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
#[test]
|
|
1686
|
-
fn test_filter_log_output_truncate_long() {
|
|
1687
|
-
let long_line = "abc1234 ".to_string() + &"x".repeat(100) + " (2 days ago) <author>";
|
|
1688
|
-
let result = filter_log_output(&long_line, 10, false);
|
|
1689
|
-
assert!(result.chars().count() < long_line.chars().count());
|
|
1690
|
-
assert!(result.contains("..."));
|
|
1691
|
-
assert!(result.chars().count() <= 80);
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
#[test]
|
|
1695
|
-
fn test_filter_log_output_cap_lines() {
|
|
1696
|
-
let output = (0..20)
|
|
1697
|
-
.map(|i| format!("hash{} message {} (1 day ago) <author>", i, i))
|
|
1698
|
-
.collect::<Vec<_>>()
|
|
1699
|
-
.join("\n");
|
|
1700
|
-
let result = filter_log_output(&output, 5, false);
|
|
1701
|
-
assert_eq!(result.lines().count(), 5);
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
#[test]
|
|
1705
|
-
fn test_filter_log_output_user_limit_no_cap() {
|
|
1706
|
-
// When user explicitly passes -N, all N lines should be returned (no re-truncation)
|
|
1707
|
-
let output = (0..20)
|
|
1708
|
-
.map(|i| format!("hash{} message {} (1 day ago) <author>", i, i))
|
|
1709
|
-
.collect::<Vec<_>>()
|
|
1710
|
-
.join("\n");
|
|
1711
|
-
let result = filter_log_output(&output, 20, true);
|
|
1712
|
-
assert_eq!(
|
|
1713
|
-
result.lines().count(),
|
|
1714
|
-
20,
|
|
1715
|
-
"User's -20 should return all 20 lines"
|
|
1716
|
-
);
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
#[test]
|
|
1720
|
-
fn test_filter_log_output_user_limit_wider_truncation() {
|
|
1721
|
-
// When user explicitly passes -N, lines up to 120 chars should NOT be truncated
|
|
1722
|
-
let line_90_chars = format!("abc1234 {} (2 days ago) <author>", "x".repeat(60));
|
|
1723
|
-
assert!(line_90_chars.chars().count() > 80);
|
|
1724
|
-
assert!(line_90_chars.chars().count() < 120);
|
|
1725
|
-
|
|
1726
|
-
let result_default = filter_log_output(&line_90_chars, 10, false);
|
|
1727
|
-
let result_user = filter_log_output(&line_90_chars, 10, true);
|
|
1728
|
-
|
|
1729
|
-
// Default truncates at 80 chars
|
|
1730
|
-
assert!(
|
|
1731
|
-
result_default.contains("..."),
|
|
1732
|
-
"Default should truncate at 80 chars"
|
|
1733
|
-
);
|
|
1734
|
-
// User-set limit uses wider threshold (120 chars)
|
|
1735
|
-
assert!(
|
|
1736
|
-
!result_user.contains("..."),
|
|
1737
|
-
"User limit should not truncate 90-char line"
|
|
1738
|
-
);
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
#[test]
|
|
1742
|
-
fn test_parse_user_limit_combined() {
|
|
1743
|
-
let args: Vec<String> = vec!["-20".into()];
|
|
1744
|
-
assert_eq!(parse_user_limit(&args), Some(20));
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
#[test]
|
|
1748
|
-
fn test_parse_user_limit_n_space() {
|
|
1749
|
-
let args: Vec<String> = vec!["-n".into(), "15".into()];
|
|
1750
|
-
assert_eq!(parse_user_limit(&args), Some(15));
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
#[test]
|
|
1754
|
-
fn test_parse_user_limit_max_count_eq() {
|
|
1755
|
-
let args: Vec<String> = vec!["--max-count=30".into()];
|
|
1756
|
-
assert_eq!(parse_user_limit(&args), Some(30));
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
#[test]
|
|
1760
|
-
fn test_parse_user_limit_max_count_space() {
|
|
1761
|
-
let args: Vec<String> = vec!["--max-count".into(), "25".into()];
|
|
1762
|
-
assert_eq!(parse_user_limit(&args), Some(25));
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
#[test]
|
|
1766
|
-
fn test_parse_user_limit_none() {
|
|
1767
|
-
let args: Vec<String> = vec!["--oneline".into()];
|
|
1768
|
-
assert_eq!(parse_user_limit(&args), None);
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
#[test]
|
|
1772
|
-
fn test_filter_log_output_token_savings() {
|
|
1773
|
-
fn count_tokens(text: &str) -> usize {
|
|
1774
|
-
text.split_whitespace().count()
|
|
1775
|
-
}
|
|
1776
|
-
// Simulate verbose git log output (default format with full metadata)
|
|
1777
|
-
let input = (0..20)
|
|
1778
|
-
.map(|i| {
|
|
1779
|
-
format!(
|
|
1780
|
-
"commit abc123{:02x}\nAuthor: User Name <user@example.com>\nDate: Mon Mar 10 10:00:00 2026 +0000\n\n fix: commit message number {}\n\n Extended body with details about the change.\n",
|
|
1781
|
-
i, i
|
|
1782
|
-
)
|
|
1783
|
-
})
|
|
1784
|
-
.collect::<Vec<_>>()
|
|
1785
|
-
.join("\n");
|
|
1786
|
-
let output = filter_log_output(&input, 10, false);
|
|
1787
|
-
let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(&input) as f64 * 100.0);
|
|
1788
|
-
assert!(
|
|
1789
|
-
savings >= 60.0,
|
|
1790
|
-
"Expected ≥60% token savings, got {:.1}%",
|
|
1791
|
-
savings
|
|
1792
|
-
);
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
#[test]
|
|
1796
|
-
fn test_filter_status_with_args() {
|
|
1797
|
-
let output = r#"On branch main
|
|
1798
|
-
Your branch is up to date with 'origin/main'.
|
|
1799
|
-
|
|
1800
|
-
Changes not staged for commit:
|
|
1801
|
-
(use "git add <file>..." to update what will be committed)
|
|
1802
|
-
(use "git restore <file>..." to discard changes in working directory)
|
|
1803
|
-
modified: src/main.rs
|
|
1804
|
-
|
|
1805
|
-
no changes added to commit (use "git add" and/or "git commit -a")
|
|
1806
|
-
"#;
|
|
1807
|
-
let result = filter_status_with_args(output);
|
|
1808
|
-
eprintln!("Result:\n{}", result);
|
|
1809
|
-
assert!(result.contains("On branch main"));
|
|
1810
|
-
assert!(result.contains("modified: src/main.rs"));
|
|
1811
|
-
assert!(
|
|
1812
|
-
!result.contains("(use \"git"),
|
|
1813
|
-
"Result should not contain git hints"
|
|
1814
|
-
);
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
#[test]
|
|
1818
|
-
fn test_filter_status_with_args_clean() {
|
|
1819
|
-
let output = "nothing to commit, working tree clean\n";
|
|
1820
|
-
let result = filter_status_with_args(output);
|
|
1821
|
-
assert!(result.contains("nothing to commit"));
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
#[test]
|
|
1825
|
-
fn test_filter_log_output_multibyte() {
|
|
1826
|
-
// Thai characters: each is 3 bytes. A line with >80 bytes but few chars
|
|
1827
|
-
let thai_msg = format!("abc1234 {} (2 days ago) <author>", "ก".repeat(30));
|
|
1828
|
-
let result = filter_log_output(&thai_msg, 10, false);
|
|
1829
|
-
// Should not panic
|
|
1830
|
-
assert!(result.contains("abc1234"));
|
|
1831
|
-
// The line has 30 Thai chars + other text, so > 80 chars total
|
|
1832
|
-
// truncate_line now counts chars, not bytes
|
|
1833
|
-
// 30 Thai + ~33 other = 63 chars < 80 threshold, so no truncation
|
|
1834
|
-
assert!(result.contains("abc1234"));
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
#[test]
|
|
1838
|
-
fn test_filter_log_output_emoji() {
|
|
1839
|
-
let emoji_msg = "abc1234 🎉🎊🎈🎁🎂🎄🎃🎆🎇✨🎉🎊🎈🎁🎂🎄🎃🎆🎇✨ (1 day ago) <user>";
|
|
1840
|
-
let result = filter_log_output(emoji_msg, 10, false);
|
|
1841
|
-
// Should not panic
|
|
1842
|
-
// 20 emoji + ~30 other chars = ~50 chars < 80, no truncation needed
|
|
1843
|
-
assert!(result.contains("abc1234"));
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
#[test]
|
|
1847
|
-
fn test_format_status_output_thai_filename() {
|
|
1848
|
-
let porcelain = "## main\n M สวัสดี.txt\n?? ทดสอบ.rs\n";
|
|
1849
|
-
let result = format_status_output(porcelain);
|
|
1850
|
-
// Should not panic
|
|
1851
|
-
assert!(result.contains("📌 main"));
|
|
1852
|
-
assert!(result.contains("สวัสดี.txt"));
|
|
1853
|
-
assert!(result.contains("ทดสอบ.rs"));
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
#[test]
|
|
1857
|
-
fn test_format_status_output_emoji_filename() {
|
|
1858
|
-
let porcelain = "## main\nA 🎉-party.txt\n M 日本語ファイル.rs\n";
|
|
1859
|
-
let result = format_status_output(porcelain);
|
|
1860
|
-
assert!(result.contains("📌 main"));
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
/// Regression test: `git branch <name>` must create, not list.
|
|
1864
|
-
/// Before fix, positional args fell into list mode which added `-a`,
|
|
1865
|
-
/// turning creation into a pattern-filtered listing (silent no-op).
|
|
1866
|
-
#[test]
|
|
1867
|
-
#[ignore] // Integration test: requires git repo
|
|
1868
|
-
fn test_branch_creation_not_swallowed() {
|
|
1869
|
-
let branch = "test-rtk-create-branch-regression";
|
|
1870
|
-
// Create branch via run_branch
|
|
1871
|
-
run_branch(&[branch.to_string()], 0, &[]).expect("run_branch should succeed");
|
|
1872
|
-
// Verify it exists
|
|
1873
|
-
let output = Command::new("git")
|
|
1874
|
-
.args(["branch", "--list", branch])
|
|
1875
|
-
.output()
|
|
1876
|
-
.expect("git branch --list should work");
|
|
1877
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1878
|
-
assert!(
|
|
1879
|
-
stdout.contains(branch),
|
|
1880
|
-
"Branch '{}' was not created. run_branch silently swallowed the creation.",
|
|
1881
|
-
branch
|
|
1882
|
-
);
|
|
1883
|
-
// Cleanup
|
|
1884
|
-
let _ = Command::new("git").args(["branch", "-d", branch]).output();
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
/// Regression test: `git branch <name> <commit>` must create from commit.
|
|
1888
|
-
#[test]
|
|
1889
|
-
#[ignore] // Integration test: requires git repo
|
|
1890
|
-
fn test_branch_creation_from_commit() {
|
|
1891
|
-
let branch = "test-rtk-create-from-commit";
|
|
1892
|
-
run_branch(&[branch.to_string(), "HEAD".to_string()], 0, &[])
|
|
1893
|
-
.expect("run_branch with start-point should succeed");
|
|
1894
|
-
let output = Command::new("git")
|
|
1895
|
-
.args(["branch", "--list", branch])
|
|
1896
|
-
.output()
|
|
1897
|
-
.expect("git branch --list should work");
|
|
1898
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
1899
|
-
assert!(
|
|
1900
|
-
stdout.contains(branch),
|
|
1901
|
-
"Branch '{}' was not created from commit.",
|
|
1902
|
-
branch
|
|
1903
|
-
);
|
|
1904
|
-
let _ = Command::new("git").args(["branch", "-d", branch]).output();
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
#[test]
|
|
1908
|
-
fn test_commit_single_message() {
|
|
1909
|
-
let args = vec!["-m".to_string(), "fix: typo".to_string()];
|
|
1910
|
-
let cmd = build_commit_command(&args, &[]);
|
|
1911
|
-
let cmd_args: Vec<_> = cmd
|
|
1912
|
-
.get_args()
|
|
1913
|
-
.map(|a| a.to_string_lossy().to_string())
|
|
1914
|
-
.collect();
|
|
1915
|
-
assert_eq!(cmd_args, vec!["commit", "-m", "fix: typo"]);
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
#[test]
|
|
1919
|
-
fn test_commit_multiple_messages() {
|
|
1920
|
-
let args = vec![
|
|
1921
|
-
"-m".to_string(),
|
|
1922
|
-
"feat: add multi-paragraph support".to_string(),
|
|
1923
|
-
"-m".to_string(),
|
|
1924
|
-
"This allows git commit -m \"title\" -m \"body\".".to_string(),
|
|
1925
|
-
];
|
|
1926
|
-
let cmd = build_commit_command(&args, &[]);
|
|
1927
|
-
let cmd_args: Vec<_> = cmd
|
|
1928
|
-
.get_args()
|
|
1929
|
-
.map(|a| a.to_string_lossy().to_string())
|
|
1930
|
-
.collect();
|
|
1931
|
-
assert_eq!(
|
|
1932
|
-
cmd_args,
|
|
1933
|
-
vec![
|
|
1934
|
-
"commit",
|
|
1935
|
-
"-m",
|
|
1936
|
-
"feat: add multi-paragraph support",
|
|
1937
|
-
"-m",
|
|
1938
|
-
"This allows git commit -m \"title\" -m \"body\"."
|
|
1939
|
-
]
|
|
1940
|
-
);
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
// #327: git commit -am "msg" must pass -am through to git
|
|
1944
|
-
#[test]
|
|
1945
|
-
fn test_commit_am_flag() {
|
|
1946
|
-
let args = vec!["-am".to_string(), "quick fix".to_string()];
|
|
1947
|
-
let cmd = build_commit_command(&args, &[]);
|
|
1948
|
-
let cmd_args: Vec<_> = cmd
|
|
1949
|
-
.get_args()
|
|
1950
|
-
.map(|a| a.to_string_lossy().to_string())
|
|
1951
|
-
.collect();
|
|
1952
|
-
assert_eq!(cmd_args, vec!["commit", "-am", "quick fix"]);
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
#[test]
|
|
1956
|
-
fn test_commit_amend() {
|
|
1957
|
-
let args = vec![
|
|
1958
|
-
"--amend".to_string(),
|
|
1959
|
-
"-m".to_string(),
|
|
1960
|
-
"new msg".to_string(),
|
|
1961
|
-
];
|
|
1962
|
-
let cmd = build_commit_command(&args, &[]);
|
|
1963
|
-
let cmd_args: Vec<_> = cmd
|
|
1964
|
-
.get_args()
|
|
1965
|
-
.map(|a| a.to_string_lossy().to_string())
|
|
1966
|
-
.collect();
|
|
1967
|
-
assert_eq!(cmd_args, vec!["commit", "--amend", "-m", "new msg"]);
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
#[test]
|
|
1971
|
-
#[ignore] // Requires `cargo build` first — run with `cargo test --ignored`
|
|
1972
|
-
fn test_git_status_not_a_repo_exits_nonzero() {
|
|
1973
|
-
// Run rtk git status in a directory that is not a git repo
|
|
1974
|
-
let tmp = std::env::temp_dir().join("rtk_test_not_a_repo");
|
|
1975
|
-
let _ = std::fs::create_dir_all(&tmp);
|
|
1976
|
-
|
|
1977
|
-
// Build the path to the test binary
|
|
1978
|
-
let bin_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
1979
|
-
.join("target")
|
|
1980
|
-
.join("debug")
|
|
1981
|
-
.join("rtk");
|
|
1982
|
-
assert!(
|
|
1983
|
-
bin_path.exists(),
|
|
1984
|
-
"Debug binary not found at {:?} — run `cargo build` first",
|
|
1985
|
-
bin_path
|
|
1986
|
-
);
|
|
1987
|
-
let output = std::process::Command::new(&bin_path)
|
|
1988
|
-
.args(["git", "status"])
|
|
1989
|
-
.current_dir(&tmp)
|
|
1990
|
-
.output()
|
|
1991
|
-
.expect("Failed to run rtk");
|
|
1992
|
-
|
|
1993
|
-
// Should exit with non-zero (128 from git)
|
|
1994
|
-
assert!(
|
|
1995
|
-
!output.status.success(),
|
|
1996
|
-
"Expected non-zero exit code for git status outside a repo, got {:?}",
|
|
1997
|
-
output.status.code()
|
|
1998
|
-
);
|
|
1999
|
-
|
|
2000
|
-
// Message should be on stderr, not stdout
|
|
2001
|
-
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
2002
|
-
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
2003
|
-
assert!(
|
|
2004
|
-
stderr.to_lowercase().contains("not a git repository"),
|
|
2005
|
-
"Expected 'not a git repository' on stderr, got stderr={:?}, stdout={:?}",
|
|
2006
|
-
stderr,
|
|
2007
|
-
stdout
|
|
2008
|
-
);
|
|
2009
|
-
|
|
2010
|
-
let _ = std::fs::remove_dir_all(&tmp);
|
|
2011
|
-
}
|
|
2012
|
-
}
|