tracevault_cli/commands/
commit_push.rs1use crate::api_client::{resolve_credentials, ApiClient};
2use crate::config::TracevaultConfig;
3use serde_json::json;
4use std::path::Path;
5use std::process::Command;
6use tracevault_core::streaming::CommitPushRequest;
7
8pub async fn run_commit_push(project_root: &Path) -> Result<(), Box<dyn std::error::Error>> {
9 let config = TracevaultConfig::load(project_root).ok_or("config not found")?;
10 let org_slug = config.org_slug.ok_or("org_slug not configured")?;
11 let repo_id = config.repo_id.ok_or("repo_id not configured")?;
12
13 let (server_url, token) = resolve_credentials(project_root);
14 let server_url = server_url.ok_or("server_url not configured")?;
15 let client = ApiClient::new(&server_url, token.as_deref());
16
17 let run_git = |args: &[&str]| -> Option<String> {
19 Command::new("git")
20 .args(args)
21 .current_dir(project_root)
22 .output()
23 .ok()
24 .filter(|o| o.status.success())
25 .and_then(|o| String::from_utf8(o.stdout).ok())
26 .map(|s| s.trim().to_string())
27 };
28
29 let commit_sha = run_git(&["rev-parse", "HEAD"]).ok_or("failed to get HEAD sha")?;
30 let branch = run_git(&["rev-parse", "--abbrev-ref", "HEAD"]);
31 let author = run_git(&["log", "-1", "--format=%ae"]).unwrap_or_default();
32
33 let diff_output = run_git(&["diff", "HEAD~1..HEAD", "--unified=3"]);
35
36 let diff_data = diff_output.map(|diff| parse_diff_to_json(&diff));
37
38 let req = CommitPushRequest {
39 commit_sha,
40 branch,
41 author,
42 diff_data,
43 committed_at: Some(chrono::Utc::now()),
44 };
45
46 match client.push_commit(&org_slug, &repo_id, &req).await {
47 Ok(resp) => {
48 println!(
49 "Commit pushed: {} ({} attributions)",
50 resp.commit_db_id, resp.attributions_count
51 );
52 }
53 Err(e) => {
54 eprintln!("Warning: commit push failed: {e}");
55 }
57 }
58
59 Ok(())
60}
61
62fn parse_diff_to_json(diff: &str) -> serde_json::Value {
63 let mut files: Vec<serde_json::Value> = Vec::new();
64 let mut current_file: Option<String> = None;
65 let mut current_hunks: Vec<serde_json::Value> = Vec::new();
66 let mut current_hunk_lines: Vec<String> = Vec::new();
67 let mut current_new_start: i64 = 0;
68 let mut current_new_count: i64 = 0;
69
70 let flush_hunk =
71 |hunks: &mut Vec<serde_json::Value>, lines: &mut Vec<String>, start: i64, count: i64| {
72 if !lines.is_empty() {
73 hunks.push(json!({
74 "new_start": start,
75 "new_count": count,
76 "added_lines": lines.clone(),
77 }));
78 lines.clear();
79 }
80 };
81
82 let flush_file = |files: &mut Vec<serde_json::Value>,
83 file: &Option<String>,
84 hunks: &mut Vec<serde_json::Value>| {
85 if let Some(path) = file {
86 if !hunks.is_empty() {
87 files.push(json!({
88 "path": path,
89 "hunks": hunks.clone(),
90 }));
91 hunks.clear();
92 }
93 }
94 };
95
96 for line in diff.lines() {
97 if let Some(path) = line.strip_prefix("+++ b/") {
98 flush_hunk(
100 &mut current_hunks,
101 &mut current_hunk_lines,
102 current_new_start,
103 current_new_count,
104 );
105 flush_file(&mut files, ¤t_file, &mut current_hunks);
106 current_file = Some(path.to_string());
107 } else if line.starts_with("@@ ") {
108 flush_hunk(
110 &mut current_hunks,
111 &mut current_hunk_lines,
112 current_new_start,
113 current_new_count,
114 );
115 if let Some(plus_part) = line.split('+').nth(1) {
117 let nums: Vec<&str> = plus_part
118 .split(' ')
119 .next()
120 .unwrap_or("")
121 .split(',')
122 .collect();
123 current_new_start = nums.first().and_then(|s| s.parse().ok()).unwrap_or(0);
124 current_new_count = nums.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
125 }
126 } else if let Some(added) = line.strip_prefix('+') {
127 current_hunk_lines.push(added.to_string());
128 }
129 }
130
131 flush_hunk(
133 &mut current_hunks,
134 &mut current_hunk_lines,
135 current_new_start,
136 current_new_count,
137 );
138 flush_file(&mut files, ¤t_file, &mut current_hunks);
139
140 json!({ "files": files })
141}