add upstream

This commit is contained in:
Christoph Brandau
2026-07-02 20:24:46 +02:00
parent f2aa48d2ec
commit d415cbd3a1
3 changed files with 168 additions and 44 deletions
+3 -1
View File
@@ -110,7 +110,9 @@ impl CommitAiEngine {
}; };
if diff.trim().is_empty() { 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); let (system, user) = build_messages(diff, notes);
+156 -34
View File
@@ -5,8 +5,8 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Command, Output, Stdio}, process::{Command, Output, Stdio},
sync::{ sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex, Arc, Mutex,
atomic::{AtomicU64, Ordering},
}, },
thread, thread,
time::Duration, time::Duration,
@@ -606,12 +606,13 @@ pub fn push(
password: Option<String>, password: Option<String>,
) -> Result<GitStatus, String> { ) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?; let repo = resolve_repo(&path)?;
let push_args = push_args_for_repo(&repo)?;
match (username.as_deref(), password.as_deref()) { match (username.as_deref(), password.as_deref()) {
(Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => { (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) status_for_repo(&repo)
@@ -670,19 +671,11 @@ fn remote_url_for(repo: &Path, remote: &str) -> Option<String> {
return None; return None;
} }
let url = String::from_utf8_lossy(&out.stdout).trim().to_string(); let url = String::from_utf8_lossy(&out.stdout).trim().to_string();
if url.is_empty() { if url.is_empty() { None } else { Some(url) }
None
} else {
Some(url)
}
} }
fn upstream_remote_name(repo: &Path) -> Option<String> { fn upstream_remote_name(repo: &Path) -> Option<String> {
let branch = run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"]).ok()?; let branch = current_branch_name(repo).ok()?;
let branch = String::from_utf8_lossy(&branch).trim().to_string();
if branch.is_empty() || branch == "HEAD" {
return None;
}
let out = git_command() let out = git_command()
.arg("-C") .arg("-C")
.arg(repo) .arg(repo)
@@ -693,11 +686,7 @@ fn upstream_remote_name(repo: &Path) -> Option<String> {
return None; return None;
} }
let name = String::from_utf8_lossy(&out.stdout).trim().to_string(); let name = String::from_utf8_lossy(&out.stdout).trim().to_string();
if name.is_empty() { if name.is_empty() { None } else { Some(name) }
None
} else {
Some(name)
}
} }
fn first_remote_name(repo: &Path) -> Option<String> { fn first_remote_name(repo: &Path) -> Option<String> {
@@ -709,6 +698,50 @@ fn first_remote_name(repo: &Path) -> Option<String> {
.map(str::to_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] #[tauri::command]
pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> { pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
let entry = cred_entry(&key)?; 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)?; let (branch, mut files) = parse_status_output(&output)?;
detect_worktree_renames(repo, &mut files); detect_worktree_renames(repo, &mut files);
Ok(GitStatus { Ok(GitStatus {
repo_path: repo.to_string_lossy().to_string(), repo_path: repo.to_string_lossy().to_string(),
current_branch: branch.current_branch, current_branch: branch.current_branch,
@@ -3170,6 +3202,22 @@ mod tests {
} }
fn init_temp_repo(name: &str) -> TempRepo { 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() let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("system time should be after 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"); 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 } TempRepo { path }
} }
@@ -3504,14 +3549,18 @@ mod tests {
) )
.unwrap(); .unwrap();
assert!(comparison assert!(
.files comparison
.iter() .files
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified)); .iter()
assert!(comparison .any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified)
.files );
.iter() assert!(
.any(|file| file.path == "added.txt" && file.status == FileStatusKind::Added)); comparison
.files
.iter()
.any(|file| file.path == "added.txt" && file.status == FileStatusKind::Added)
);
assert!(comparison.patch.contains("second line")); assert!(comparison.patch.contains("second line"));
} }
@@ -3568,10 +3617,12 @@ mod tests {
assert_eq!(comparison.to_short, "working tree"); assert_eq!(comparison.to_short, "working tree");
assert!(comparison.to_hash.is_empty()); assert!(comparison.to_hash.is_empty());
assert!(comparison assert!(
.files comparison
.iter() .files
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified)); .iter()
.any(|file| file.path == "old.txt" && file.status == FileStatusKind::Modified)
);
assert!(comparison.patch.contains("working tree change")); assert!(comparison.patch.contains("working tree change"));
} }
@@ -3705,6 +3756,77 @@ mod tests {
assert_eq!(parent_count, 2); 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] #[test]
fn read_and_resolve_conflict_round_trip() { fn read_and_resolve_conflict_round_trip() {
let repo = init_temp_repo("resolve_conflict"); let repo = init_temp_repo("resolve_conflict");
+9 -9
View File
@@ -3,15 +3,15 @@
mod git; mod git;
use git::{ use git::{
apply_file_patch, cancel_code_search, cancel_file_history, checkout_branch, commit, SearchCancellationState, apply_file_patch, cancel_code_search, cancel_file_history,
commit_ai_generate, commit_ai_status, compare_commits, compare_file_to_head, checkout_branch, commit, commit_ai_generate, commit_ai_status, compare_commits,
compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save, delete_branch, compare_file_to_head, compare_file_to_parent, create_branch, cred_delete, cred_load, cred_save,
diff_file_against_working_tree, get_file_patch, get_remote_url, get_status, list_branches, delete_branch, diff_file_against_working_tree, get_file_patch, get_remote_url, get_status,
list_commits, list_file_history, list_repository_files, merge_branch, open_repo_in_explorer, list_branches, list_commits, list_file_history, list_repository_files, merge_branch,
open_repository, open_repository_bundle, open_repository_file, pull, push, read_conflict, open_repo_in_explorer, open_repository, open_repository_bundle, open_repository_file, pull,
rename_branch, resolve_conflict, resolve_conflict_side, restore_file_from_commit, push, read_conflict, rename_branch, resolve_conflict, resolve_conflict_side,
restore_files, restore_to_commit, search_code_introductions, stage_files, unstage_files, restore_file_from_commit, restore_files, restore_to_commit, search_code_introductions,
SearchCancellationState, stage_files, unstage_files,
}; };
fn main() { fn main() {