Skip to content

Commit 98ec2cc

Browse files
michael-borckclaude
andcommitted
Implement optional Git integration detection and basic version control (task 7.6)
- Add comprehensive Git service with repository detection and initialization - Implement Git operations: add, commit, status, history, and diff viewing - Create GitIntegration component with 3-tab interface (overview, history, settings) - Add Git hook with auto-commit functionality for session management - Support repository initialization with .gitignore and initial commit - Include commit history browsing with pagination and file change tracking - Add configurable auto-commit hooks for session saves and content generation - Implement Git installation detection and user configuration management - Add Git button to main interface with optional enablement (disabled by default) - Integrate with status feedback system for user notifications 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 40a8b92 commit 98ec2cc

File tree

8 files changed

+2316
-2
lines changed

8 files changed

+2316
-2
lines changed

src-tauri/src/git/commands.rs

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
use super::*;
2+
use crate::git::service::GitService;
3+
use tauri::State;
4+
use std::sync::Arc;
5+
use tokio::sync::Mutex;
6+
use std::path::PathBuf;
7+
use anyhow::Result;
8+
9+
#[tauri::command]
10+
pub async fn get_git_config(
11+
git_service: State<'_, Arc<Mutex<GitService>>>,
12+
) -> Result<GitConfig, String> {
13+
let service = git_service.lock().await;
14+
Ok(service.get_config().clone())
15+
}
16+
17+
#[tauri::command]
18+
pub async fn update_git_config(
19+
git_service: State<'_, Arc<Mutex<GitService>>>,
20+
config: GitConfig,
21+
) -> Result<(), String> {
22+
let mut service = git_service.lock().await;
23+
service.update_config(config)
24+
.map_err(|e| e.to_string())
25+
}
26+
27+
#[tauri::command]
28+
pub async fn detect_git_repository(
29+
git_service: State<'_, Arc<Mutex<GitService>>>,
30+
) -> Result<GitStatus, String> {
31+
let service = git_service.lock().await;
32+
service.detect_repository().await
33+
.map_err(|e| e.to_string())
34+
}
35+
36+
#[tauri::command]
37+
pub async fn initialize_git_repository(
38+
git_service: State<'_, Arc<Mutex<GitService>>>,
39+
options: GitInitOptions,
40+
) -> Result<(), String> {
41+
let service = git_service.lock().await;
42+
service.initialize_repository(options).await
43+
.map_err(|e| e.to_string())
44+
}
45+
46+
#[tauri::command]
47+
pub async fn get_git_status(
48+
git_service: State<'_, Arc<Mutex<GitService>>>,
49+
) -> Result<GitStatus, String> {
50+
let service = git_service.lock().await;
51+
service.get_status().await
52+
.map_err(|e| e.to_string())
53+
}
54+
55+
#[tauri::command]
56+
pub async fn commit_git_changes(
57+
git_service: State<'_, Arc<Mutex<GitService>>>,
58+
options: CommitOptions,
59+
) -> Result<String, String> {
60+
let service = git_service.lock().await;
61+
service.commit_changes(options).await
62+
.map_err(|e| e.to_string())
63+
}
64+
65+
#[tauri::command]
66+
pub async fn get_git_history(
67+
git_service: State<'_, Arc<Mutex<GitService>>>,
68+
page: u32,
69+
per_page: u32,
70+
) -> Result<GitHistory, String> {
71+
let service = git_service.lock().await;
72+
service.get_history(page, per_page).await
73+
.map_err(|e| e.to_string())
74+
}
75+
76+
#[tauri::command]
77+
pub async fn get_git_diff(
78+
git_service: State<'_, Arc<Mutex<GitService>>>,
79+
commit_hash: Option<String>,
80+
file_path: Option<String>,
81+
) -> Result<Vec<GitDiff>, String> {
82+
let service = git_service.lock().await;
83+
service.get_diff(
84+
commit_hash.as_deref(),
85+
file_path.as_deref()
86+
).await
87+
.map_err(|e| e.to_string())
88+
}
89+
90+
#[tauri::command]
91+
pub async fn auto_commit_session(
92+
git_service: State<'_, Arc<Mutex<GitService>>>,
93+
session_name: String,
94+
) -> Result<Option<String>, String> {
95+
let service = git_service.lock().await;
96+
service.auto_commit_session(&session_name).await
97+
.map_err(|e| e.to_string())
98+
}
99+
100+
#[tauri::command]
101+
pub async fn auto_commit_content_generation(
102+
git_service: State<'_, Arc<Mutex<GitService>>>,
103+
content_types: Vec<String>,
104+
) -> Result<Option<String>, String> {
105+
let service = git_service.lock().await;
106+
service.auto_commit_content_generation(&content_types).await
107+
.map_err(|e| e.to_string())
108+
}
109+
110+
#[tauri::command]
111+
pub async fn check_git_installation() -> Result<GitInstallationInfo, String> {
112+
// Check if git is installed and get version
113+
let output = std::process::Command::new("git")
114+
.args(&["--version"])
115+
.output();
116+
117+
match output {
118+
Ok(output) if output.status.success() => {
119+
let version_str = String::from_utf8_lossy(&output.stdout);
120+
let version = version_str.trim().strip_prefix("git version ")
121+
.unwrap_or("unknown")
122+
.to_string();
123+
124+
Ok(GitInstallationInfo {
125+
is_installed: true,
126+
version: Some(version),
127+
path: which_git().await.ok(),
128+
error: None,
129+
})
130+
}
131+
Ok(output) => {
132+
let error = String::from_utf8_lossy(&output.stderr);
133+
Ok(GitInstallationInfo {
134+
is_installed: false,
135+
version: None,
136+
path: None,
137+
error: Some(error.to_string()),
138+
})
139+
}
140+
Err(e) => {
141+
Ok(GitInstallationInfo {
142+
is_installed: false,
143+
version: None,
144+
path: None,
145+
error: Some(e.to_string()),
146+
})
147+
}
148+
}
149+
}
150+
151+
#[tauri::command]
152+
pub async fn get_git_user_config() -> Result<GitUserConfig, String> {
153+
let name_result = std::process::Command::new("git")
154+
.args(&["config", "--global", "user.name"])
155+
.output();
156+
157+
let email_result = std::process::Command::new("git")
158+
.args(&["config", "--global", "user.email"])
159+
.output();
160+
161+
let name = if let Ok(output) = name_result {
162+
if output.status.success() {
163+
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
164+
} else {
165+
None
166+
}
167+
} else {
168+
None
169+
};
170+
171+
let email = if let Ok(output) = email_result {
172+
if output.status.success() {
173+
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
174+
} else {
175+
None
176+
}
177+
} else {
178+
None
179+
};
180+
181+
Ok(GitUserConfig { name, email })
182+
}
183+
184+
#[tauri::command]
185+
pub async fn set_git_user_config(
186+
name: Option<String>,
187+
email: Option<String>,
188+
) -> Result<(), String> {
189+
if let Some(name) = name {
190+
let output = std::process::Command::new("git")
191+
.args(&["config", "--global", "user.name", &name])
192+
.output()
193+
.map_err(|e| format!("Failed to set git user name: {}", e))?;
194+
195+
if !output.status.success() {
196+
return Err(format!("Failed to set git user name: {}",
197+
String::from_utf8_lossy(&output.stderr)));
198+
}
199+
}
200+
201+
if let Some(email) = email {
202+
let output = std::process::Command::new("git")
203+
.args(&["config", "--global", "user.email", &email])
204+
.output()
205+
.map_err(|e| format!("Failed to set git user email: {}", e))?;
206+
207+
if !output.status.success() {
208+
return Err(format!("Failed to set git user email: {}",
209+
String::from_utf8_lossy(&output.stderr)));
210+
}
211+
}
212+
213+
Ok(())
214+
}
215+
216+
#[tauri::command]
217+
pub async fn validate_repository_path(
218+
path: String,
219+
) -> Result<RepositoryValidation, String> {
220+
let path_buf = PathBuf::from(&path);
221+
222+
if !path_buf.exists() {
223+
return Ok(RepositoryValidation {
224+
is_valid: false,
225+
is_git_repository: false,
226+
can_initialize: path_buf.parent().map(|p| p.exists()).unwrap_or(false),
227+
error_message: Some("Path does not exist".to_string()),
228+
});
229+
}
230+
231+
if !path_buf.is_dir() {
232+
return Ok(RepositoryValidation {
233+
is_valid: false,
234+
is_git_repository: false,
235+
can_initialize: false,
236+
error_message: Some("Path is not a directory".to_string()),
237+
});
238+
}
239+
240+
// Check if it's already a git repository
241+
let git_dir = path_buf.join(".git");
242+
let is_git_repo = git_dir.exists();
243+
244+
Ok(RepositoryValidation {
245+
is_valid: true,
246+
is_git_repository: is_git_repo,
247+
can_initialize: !is_git_repo,
248+
error_message: None,
249+
})
250+
}
251+
252+
// Helper function to find git executable
253+
async fn which_git() -> Result<PathBuf> {
254+
let output = std::process::Command::new("which")
255+
.args(&["git"])
256+
.output()?;
257+
258+
if output.status.success() {
259+
let path_str = String::from_utf8_lossy(&output.stdout);
260+
Ok(PathBuf::from(path_str.trim()))
261+
} else {
262+
Err(anyhow::anyhow!("Git executable not found in PATH"))
263+
}
264+
}
265+
266+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
267+
pub struct GitInstallationInfo {
268+
pub is_installed: bool,
269+
pub version: Option<String>,
270+
pub path: Option<PathBuf>,
271+
pub error: Option<String>,
272+
}
273+
274+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
275+
pub struct GitUserConfig {
276+
pub name: Option<String>,
277+
pub email: Option<String>,
278+
}
279+
280+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
281+
pub struct RepositoryValidation {
282+
pub is_valid: bool,
283+
pub is_git_repository: bool,
284+
pub can_initialize: bool,
285+
pub error_message: Option<String>,
286+
}

0 commit comments

Comments
 (0)