From 017b5986491591a4835efc06377d35b3f5ea2593 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Mon, 23 Mar 2026 16:39:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=9C=A8git=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E6=97=B6=E9=80=89=E6=8B=A9=E8=BF=9C=E7=A8=8B=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/folders.rs | 109 ++++++++++++++++---- src-tauri/src/lib.rs | 1 + src/components/layout/push-workspace.tsx | 123 +++++++++++++++++++---- src/lib/tauri.ts | 16 ++- src/lib/types.ts | 6 ++ 5 files changed, 217 insertions(+), 38 deletions(-) diff --git a/src-tauri/src/commands/folders.rs b/src-tauri/src/commands/folders.rs index 42e9de1..3a25490 100644 --- a/src-tauri/src/commands/folders.rs +++ b/src-tauri/src/commands/folders.rs @@ -148,6 +148,13 @@ pub struct GitPushResult { pub upstream_set: bool, } +#[derive(Debug, Serialize)] +pub struct GitPushInfo { + pub branch: String, + pub remotes: Vec, + pub tracking_remote: Option, +} + #[derive(Debug, Serialize)] pub struct GitMergeResult { pub merged_commits: usize, @@ -852,17 +859,68 @@ pub async fn git_fetch( Ok(String::from_utf8_lossy(&output.stderr).trim().to_string()) } +#[tauri::command] +pub async fn git_push_info(path: String) -> Result { + // Get current branch name + let branch_output = crate::process::tokio_command("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .current_dir(&path) + .output() + .await + .map_err(AppCommandError::io)?; + let branch = String::from_utf8_lossy(&branch_output.stdout) + .trim() + .to_string(); + + // Get tracking remote for current branch + let remote_key = format!("branch.{}.remote", branch); + let remote_output = crate::process::tokio_command("git") + .args(["config", "--get", &remote_key]) + .current_dir(&path) + .output() + .await; + let tracking_remote = remote_output + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .filter(|v| !v.is_empty()); + + // Get all remotes + let remotes = git_list_remotes(path).await?; + + Ok(GitPushInfo { + branch, + remotes, + tracking_remote, + }) +} + #[tauri::command] pub async fn git_push( app: tauri::AppHandle, window: tauri::WebviewWindow, path: String, + remote: Option, credentials: Option, db: tauri::State<'_, AppDatabase>, ) -> Result { let pushed_commits = estimate_push_commit_count(&path).await; - // Check if the current branch has an upstream configured + // Determine the target remote (use provided or fall back to tracking remote) + let target_remote = remote.unwrap_or_else(|| "origin".to_string()); + + // Check if the current branch has an upstream configured for this remote + let branch_output = crate::process::tokio_command("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .current_dir(&path) + .output() + .await + .map_err(AppCommandError::io)?; + let branch = String::from_utf8_lossy(&branch_output.stdout) + .trim() + .to_string(); + + // Check if upstream is set and points to the target remote let upstream_check = crate::process::tokio_command("git") .args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) .current_dir(&path) @@ -870,28 +928,32 @@ pub async fn git_push( .await .map_err(AppCommandError::io)?; - let has_upstream = upstream_check.status.success(); + let current_upstream = if upstream_check.status.success() { + Some( + String::from_utf8_lossy(&upstream_check.stdout) + .trim() + .to_string(), + ) + } else { + None + }; - let output = if !has_upstream { - // No upstream: get current branch name and push with --set-upstream - let branch_output = crate::process::tokio_command("git") - .args(["rev-parse", "--abbrev-ref", "HEAD"]) - .current_dir(&path) - .output() - .await - .map_err(AppCommandError::io)?; - let branch = String::from_utf8_lossy(&branch_output.stdout) - .trim() - .to_string(); + // Need to set upstream if: no upstream at all, or upstream points to a different remote + let needs_set_upstream = match ¤t_upstream { + None => true, + Some(upstream) => !upstream.starts_with(&format!("{}/", target_remote)), + }; + let output = if needs_set_upstream { let mut cmd = crate::process::tokio_command("git"); - cmd.args(["push", "--set-upstream", "origin", &branch]) + cmd.args(["push", "--set-upstream", &target_remote, &branch]) .current_dir(&path); prepare_remote_git_cmd(&mut cmd, &path, credentials.as_ref(), &db, &app).await; cmd.output().await.map_err(AppCommandError::io)? } else { let mut cmd = crate::process::tokio_command("git"); - cmd.args(["push"]).current_dir(&path); + cmd.args(["push", &target_remote, &branch]) + .current_dir(&path); prepare_remote_git_cmd(&mut cmd, &path, credentials.as_ref(), &db, &app).await; cmd.output().await.map_err(AppCommandError::io)? }; @@ -900,7 +962,7 @@ pub async fn git_push( return Err(classify_remote_git_error("push", &output.stderr)); } - let upstream_set = !has_upstream; + let upstream_set = needs_set_upstream; if let Some(folder_id) = window .label() @@ -3333,6 +3395,7 @@ pub async fn git_log( path: String, limit: Option, branch: Option, + remote: Option, ) -> Result { const COMMIT_META_PREFIX: &str = "__COMMIT__\0"; const MESSAGE_END_MARKER: &str = "__COMMIT_MESSAGE_END__"; @@ -3431,7 +3494,7 @@ pub async fn git_log( } let log_limit = limit.unwrap_or(100); - let (unpushed_hashes, has_upstream) = get_unpushed_hashes(&path, log_limit) + let (unpushed_hashes, has_upstream) = get_unpushed_hashes(&path, log_limit, remote.as_deref()) .await .unwrap_or((None, false)); for entry in entries.iter_mut() { @@ -3585,6 +3648,7 @@ fn parse_numstat_count(value: &str) -> u32 { async fn get_unpushed_hashes( path: &str, limit: u32, + remote_override: Option<&str>, ) -> Result<(Option>, bool), AppCommandError> { let limit_arg = format!("-{}", limit); @@ -3605,7 +3669,16 @@ async fn get_unpushed_hashes( .trim() .is_empty(); - let rev_list_output = if has_upstream { + // When remote_override is specified, always compare against that remote + let rev_list_output = if let Some(target_remote) = remote_override { + let remote_arg = format!("--remotes={}", target_remote); + crate::process::tokio_command("git") + .args(["rev-list", &limit_arg, "HEAD", "--not", &remote_arg]) + .current_dir(path) + .output() + .await + .map_err(AppCommandError::io)? + } else if has_upstream { let upstream = String::from_utf8_lossy(&upstream_output.stdout) .trim() .to_string(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ede0554..06f538b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -207,6 +207,7 @@ pub fn run() { folders::git_start_pull_merge, folders::git_has_merge_head, folders::git_fetch, + folders::git_push_info, folders::git_push, folders::git_new_branch, folders::git_worktree_add, diff --git a/src/components/layout/push-workspace.tsx b/src/components/layout/push-workspace.tsx index e806df6..4d3b986 100644 --- a/src/components/layout/push-workspace.tsx +++ b/src/components/layout/push-workspace.tsx @@ -3,9 +3,11 @@ import type { ReactElement } from "react" import { useCallback, useEffect, useMemo, useState } from "react" import { + ArrowRight, ChevronsDownUp, ChevronsUpDown, CloudOff, + GitBranch, Loader2, Upload, } from "lucide-react" @@ -42,10 +44,17 @@ import { } from "@/components/ai-elements/commit" import { DiffViewer } from "@/components/diff/diff-viewer" import { Button } from "@/components/ui/button" -import { gitLog, gitPush, gitShowFile } from "@/lib/tauri" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { gitLog, gitPush, gitPushInfo, gitShowFile } from "@/lib/tauri" import { toErrorMessage } from "@/lib/app-error" import { languageFromPath } from "@/lib/language-detect" -import type { GitLogEntry, GitLogFileChange } from "@/lib/types" +import type { GitLogEntry, GitLogFileChange, GitPushInfo } from "@/lib/types" import { useGitCredential } from "@/contexts/git-credential-context" // --- File tree types & builder (same as aux-panel-git-log-tab) --- @@ -278,6 +287,8 @@ export function PushWorkspace({ const tLog = useTranslations("Folder.gitLogTab") const { withCredentialRetry } = useGitCredential() + const [pushInfoData, setPushInfoData] = useState(null) + const [selectedRemote, setSelectedRemote] = useState(null) const [commits, setCommits] = useState([]) const [hasUpstream, setHasUpstream] = useState(true) const [listLoading, setListLoading] = useState(false) @@ -295,22 +306,60 @@ export function PushWorkspace({ [commits] ) - const loadCommits = useCallback(async () => { - setListLoading(true) - try { - const result = await gitLog(folderPath, 100) - setCommits(result.entries) - setHasUpstream(result.has_upstream) - } catch (err) { - toast.error(toErrorMessage(err)) - } finally { - setListLoading(false) - } + // Load push info (branch, remotes, tracking remote) + useEffect(() => { + gitPushInfo(folderPath) + .then((info) => { + setPushInfoData(info) + // Default to tracking remote or first remote + const defaultRemote = + info.tracking_remote ?? + (info.remotes.length > 0 ? info.remotes[0].name : null) + setSelectedRemote(defaultRemote) + }) + .catch((err) => { + toast.error(toErrorMessage(err)) + }) }, [folderPath]) + // Deduplicate remotes (git remote -v returns fetch + push entries) + const uniqueRemotes = useMemo(() => { + if (!pushInfoData) return [] + const seen = new Set() + return pushInfoData.remotes.filter((r) => { + if (seen.has(r.name)) return false + seen.add(r.name) + return true + }) + }, [pushInfoData]) + + const loadCommits = useCallback( + async (remote?: string) => { + setListLoading(true) + try { + const result = await gitLog( + folderPath, + 100, + undefined, + remote ?? undefined + ) + setCommits(result.entries) + setHasUpstream(result.has_upstream) + } catch (err) { + toast.error(toErrorMessage(err)) + } finally { + setListLoading(false) + } + }, + [folderPath] + ) + + // Reload commits when selected remote changes useEffect(() => { - loadCommits() - }, [loadCommits]) + if (selectedRemote !== null) { + loadCommits(selectedRemote) + } + }, [selectedRemote, loadCommits]) async function handleSelectFile(commitHash: string, file: string) { setSelectedFile(file) @@ -334,9 +383,10 @@ export function PushWorkspace({ async function handlePush() { setPushing(true) try { - await withCredentialRetry((creds) => gitPush(folderPath, creds), { - folderPath, - }) + await withCredentialRetry( + (creds) => gitPush(folderPath, selectedRemote, creds), + { folderPath } + ) onPushed?.() } catch (err) { toast.error(t("toasts.pushFailed"), { @@ -349,6 +399,43 @@ export function PushWorkspace({ return (
+ {/* Push target header: branch → remote/branch */} + {pushInfoData && ( +
+ + + {pushInfoData.branch} + + + {uniqueRemotes.length <= 1 ? ( + + {selectedRemote ?? "origin"}/{pushInfoData.branch} + + ) : ( +
+ + + /{pushInfoData.branch} + +
+ )} +
+ )} + {/* Left panel: commit list */} diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index 7951c30..6648475 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -24,6 +24,7 @@ import type { GitBranchList, GitPullResult, GitPushResult, + GitPushInfo, GitMergeResult, GitRebaseResult, GitConflictFileVersions, @@ -548,11 +549,20 @@ export async function gitFetch( return invoke("git_fetch", { path, credentials: credentials ?? null }) } +export async function gitPushInfo(path: string): Promise { + return invoke("git_push_info", { path }) +} + export async function gitPush( path: string, + remote?: string | null, credentials?: GitCredentials | null ): Promise { - return invoke("git_push", { path, credentials: credentials ?? null }) + return invoke("git_push", { + path, + remote: remote ?? null, + credentials: credentials ?? null, + }) } export async function gitNewBranch( @@ -1047,12 +1057,14 @@ export async function createFileTreeEntry( export async function gitLog( path: string, limit?: number, - branch?: string + branch?: string, + remote?: string ): Promise { return invoke("git_log", { path, limit: limit ?? null, branch: branch ?? null, + remote: remote ?? null, }) } diff --git a/src/lib/types.ts b/src/lib/types.ts index ac62c3c..3dc8603 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -675,6 +675,12 @@ export interface GitPushResult { upstream_set: boolean } +export interface GitPushInfo { + branch: string + remotes: GitRemote[] + tracking_remote: string | null +} + export interface GitMergeResult { merged_commits: number conflict?: GitConflictInfo | null