Features/ai commits #8

Merged
Christoph merged 4 commits from features/ai_commits into master 2026-07-02 20:15:00 +00:00
3 changed files with 168 additions and 44 deletions
Showing only changes of commit d415cbd3a1 - Show all commits
+3 -1
View File
@@ -110,7 +110,9 @@ impl CommitAiEngine {
};
if diff.trim().is_empty() {
return Err("Keine gestagten Änderungen für eine Commit-Message vorhanden.".to_string());
return Err(
"Keine gestagten Änderungen für eine Commit-Message vorhanden.".to_string(),
);
}
let (system, user) = build_messages(diff, notes);
+156 -34
View File
@@ -5,8 +5,8 @@ use std::{
path::{Path, PathBuf},
process::{Command, Output, Stdio},
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex,
atomic::{AtomicU64, Ordering},
},
thread,
time::Duration,
@@ -606,12 +606,13 @@ pub fn push(
password: Option<String>,
) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?;
let push_args = push_args_for_repo(&repo)?;
match (username.as_deref(), password.as_deref()) {
(Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => {
run_git_authenticated(&repo, ["push"], u, p)?;
run_git_authenticated(&repo, push_args, u, p)?;
}
_ => {
run_git(&repo, ["push"])?;
run_git(&repo, push_args)?;
}
}
status_for_repo(&repo)
@@ -670,19 +671,11 @@ fn remote_url_for(repo: &Path, remote: &str) -> Option<String> {
return None;
}
let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
if url.is_empty() {
None
} else {
Some(url)
}
if url.is_empty() { None } else { Some(url) }
}
fn upstream_remote_name(repo: &Path) -> Option<String> {
let branch = run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"]).ok()?;
let branch = String::from_utf8_lossy(&branch).trim().to_string();
if branch.is_empty() || branch == "HEAD" {
return None;
}
let branch = current_branch_name(repo).ok()?;
let out = git_command()
.arg("-C")
.arg(repo)
@@ -693,11 +686,7 @@ fn upstream_remote_name(repo: &Path) -> Option<String> {
return None;
}
let name = String::from_utf8_lossy(&out.stdout).trim().to_string();
if name.is_empty() {
None
} else {
Some(name)
}
if name.is_empty() { None } else { Some(name) }
}
fn first_remote_name(repo: &Path) -> Option<String> {
@@ -709,6 +698,50 @@ fn first_remote_name(repo: &Path) -> Option<String> {
.map(str::to_string)
}
fn current_branch_name(repo: &Path) -> Result<String, String> {
let branch = run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"])?;
let branch = String::from_utf8_lossy(&branch).trim().to_string();
if branch.is_empty() || branch == "HEAD" {
return Err("Aktueller Branch konnte nicht ermittelt werden.".to_string());
}
Ok(branch)
}
fn branch_has_upstream(repo: &Path) -> bool {
git_command()
.arg("-C")
.arg(repo)
.args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn initial_push_remote_name(repo: &Path) -> Result<String, String> {
if remote_url_for(repo, "origin").is_some() {
return Ok("origin".to_string());
}
first_remote_name(repo).ok_or_else(|| {
"Dieser Branch hat keinen Upstream und es ist kein Remote konfiguriert.".to_string()
})
}
fn push_args_for_repo(repo: &Path) -> Result<Vec<OsString>, String> {
if branch_has_upstream(repo) {
return Ok(vec![OsString::from("push")]);
}
let branch = current_branch_name(repo)?;
let remote = initial_push_remote_name(repo)?;
Ok(vec![
OsString::from("push"),
OsString::from("--set-upstream"),
OsString::from(remote),
OsString::from(branch),
])
}
#[tauri::command]
pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
let entry = cred_entry(&key)?;
@@ -1604,7 +1637,6 @@ fn status_for_repo(repo: &Path) -> Result<GitStatus, String> {
let (branch, mut files) = parse_status_output(&output)?;
detect_worktree_renames(repo, &mut files);
Ok(GitStatus {
repo_path: repo.to_string_lossy().to_string(),
current_branch: branch.current_branch,
@@ -3170,6 +3202,22 @@ mod tests {
}
fn init_temp_repo(name: &str) -> TempRepo {
let repo = temp_dir(name);
run_git_test(&repo.path, ["init", "-q"]);
run_git_test(&repo.path, ["config", "user.email", "test@example.com"]);
run_git_test(&repo.path, ["config", "user.name", "Tester"]);
repo
}
fn init_bare_temp_repo(name: &str) -> TempRepo {
let repo = temp_dir(name);
run_git_test(&repo.path, ["init", "--bare", "-q"]);
repo
}
fn temp_dir(name: &str) -> TempRepo {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after Unix epoch")
@@ -3181,9 +3229,6 @@ mod tests {
));
fs::create_dir_all(&path).expect("temp repo directory should be created");
run_git_test(&path, ["init", "-q"]);
run_git_test(&path, ["config", "user.email", "test@example.com"]);
run_git_test(&path, ["config", "user.name", "Tester"]);
TempRepo { path }
}
@@ -3504,14 +3549,18 @@ mod tests {
)
.unwrap();
assert!(comparison
.files
.iter()
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified));
assert!(comparison
.files
.iter()
.any(|file| file.path == "added.txt" && file.status == FileStatusKind::Added));
assert!(
comparison
.files
.iter()
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified)
);
assert!(
comparison
.files
.iter()
.any(|file| file.path == "added.txt" && file.status == FileStatusKind::Added)
);
assert!(comparison.patch.contains("second line"));
}
@@ -3568,10 +3617,12 @@ mod tests {
assert_eq!(comparison.to_short, "working tree");
assert!(comparison.to_hash.is_empty());
assert!(comparison
.files
.iter()
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified));
assert!(
comparison
.files
.iter()
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified)
);
assert!(comparison.patch.contains("working tree change"));
}
@@ -3705,6 +3756,77 @@ mod tests {
assert_eq!(parent_count, 2);
}
#[test]
fn push_args_use_set_upstream_for_branch_without_tracking_remote() {
let repo = init_temp_repo("push_sets_upstream");
let remote = init_bare_temp_repo("push_sets_upstream_remote");
commit_initial_file(&repo.path);
run_git_test(
&repo.path,
["checkout", "-q", "-b", "features/ai_commits_local"],
);
fs::write(repo.path.join("feature.txt"), "feature\n")
.expect("feature file should be written");
run_git_test(&repo.path, ["add", "feature.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "feature work"]);
run_git_test(
&repo.path,
["remote", "add", "origin", remote.path.to_str().unwrap()],
);
let args = push_args_for_repo(&repo.path).unwrap();
let args = args
.iter()
.map(|arg| arg.to_string_lossy().to_string())
.collect::<Vec<_>>();
assert_eq!(
args,
vec![
"push",
"--set-upstream",
"origin",
"features/ai_commits_local"
]
);
}
#[test]
#[cfg_attr(
windows,
ignore = "Git for Windows can fail local push tests with a sh signal pipe error"
)]
fn push_sets_upstream_for_branch_without_tracking_remote() {
let repo = init_temp_repo("push_sets_upstream_integration");
let remote = init_bare_temp_repo("push_sets_upstream_integration_remote");
commit_initial_file(&repo.path);
run_git_test(
&repo.path,
["checkout", "-q", "-b", "features/ai_commits_local"],
);
fs::write(repo.path.join("feature.txt"), "feature\n")
.expect("feature file should be written");
run_git_test(&repo.path, ["add", "feature.txt"]);
run_git_test(&repo.path, ["commit", "-q", "-m", "feature work"]);
run_git_test(
&repo.path,
["remote", "add", "origin", remote.path.to_str().unwrap()],
);
let status = push(repo.path.to_string_lossy().to_string(), None, None).unwrap();
assert_eq!(
status.upstream.as_deref(),
Some("origin/features/ai_commits_local")
);
let local_head = git_output_test(&repo.path, ["rev-parse", "HEAD"]);
let remote_head = git_output_test(
&remote.path,
["rev-parse", "refs/heads/features/ai_commits_local"],
);
assert_eq!(remote_head, local_head);
}
#[test]
fn read_and_resolve_conflict_round_trip() {
let repo = init_temp_repo("resolve_conflict");
+9 -9
View File
@@ -3,15 +3,15 @@
mod git;
use git::{
apply_file_patch, cancel_code_search, cancel_file_history, checkout_branch, commit,
commit_ai_generate, commit_ai_status, compare_commits, compare_file_to_head,
compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save, delete_branch,
diff_file_against_working_tree, get_file_patch, get_remote_url, get_status, list_branches,
list_commits, list_file_history, list_repository_files, merge_branch, open_repo_in_explorer,
open_repository, open_repository_bundle, open_repository_file, pull, push, read_conflict,
rename_branch, resolve_conflict, resolve_conflict_side, restore_file_from_commit,
restore_files, restore_to_commit, search_code_introductions, stage_files, unstage_files,
SearchCancellationState,
SearchCancellationState, apply_file_patch, cancel_code_search, cancel_file_history,
checkout_branch, commit, commit_ai_generate, commit_ai_status, compare_commits,
compare_file_to_head, compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save,
delete_branch, diff_file_against_working_tree, get_file_patch, get_remote_url, get_status,
list_branches, list_commits, list_file_history, list_repository_files, merge_branch,
open_repo_in_explorer, open_repository, open_repository_bundle, open_repository_file, pull,
push, read_conflict, rename_branch, resolve_conflict, resolve_conflict_side,
restore_file_from_commit, restore_files, restore_to_commit, search_code_introductions,
stage_files, unstage_files,
};
fn main() {