diff --git a/src-tauri/src/commands/folders.rs b/src-tauri/src/commands/folders.rs index 939f1c1..42e9de1 100644 --- a/src-tauri/src/commands/folders.rs +++ b/src-tauri/src/commands/folders.rs @@ -276,6 +276,12 @@ pub struct GitLogFileChange { pub deletions: u32, } +#[derive(Debug, Serialize)] +pub struct GitLogResult { + pub entries: Vec, + pub has_upstream: bool, +} + fn count_non_empty_lines(content: &str) -> usize { content .lines() @@ -3327,7 +3333,7 @@ pub async fn git_log( path: String, limit: Option, branch: Option, -) -> Result, AppCommandError> { +) -> Result { const COMMIT_META_PREFIX: &str = "__COMMIT__\0"; const MESSAGE_END_MARKER: &str = "__COMMIT_MESSAGE_END__"; @@ -3359,7 +3365,10 @@ pub async fn git_log( if stderr_str.contains("does not have any commits yet") || stderr_str.contains("unknown revision or path not in the working tree") { - return Ok(Vec::new()); + return Ok(GitLogResult { + entries: Vec::new(), + has_upstream: false, + }); } return Err(git_command_error("log", &output.stderr)); } @@ -3421,14 +3430,20 @@ pub async fn git_log( entries.push(entry.finish()); } - let unpushed_hashes = get_unpushed_hashes(&path).await.ok().flatten(); + let log_limit = limit.unwrap_or(100); + let (unpushed_hashes, has_upstream) = get_unpushed_hashes(&path, log_limit) + .await + .unwrap_or((None, false)); for entry in entries.iter_mut() { entry.pushed = unpushed_hashes .as_ref() .map(|hashes| !hashes.contains(&entry.full_hash)); } - Ok(entries) + Ok(GitLogResult { + entries, + has_upstream, + }) } #[tauri::command] @@ -3566,7 +3581,13 @@ fn parse_numstat_count(value: &str) -> u32 { value.parse::().unwrap_or(0) } -async fn get_unpushed_hashes(path: &str) -> Result>, AppCommandError> { +/// Returns (unpushed_hashes, has_upstream). +async fn get_unpushed_hashes( + path: &str, + limit: u32, +) -> Result<(Option>, bool), AppCommandError> { + let limit_arg = format!("-{}", limit); + let upstream_output = crate::process::tokio_command("git") .args([ "rev-parse", @@ -3579,27 +3600,65 @@ async fn get_unpushed_hashes(path: &str) -> Result>, AppC .await .map_err(AppCommandError::io)?; - if !upstream_output.status.success() { - return Ok(None); - } + let has_upstream = upstream_output.status.success() + && !String::from_utf8_lossy(&upstream_output.stdout) + .trim() + .is_empty(); - let upstream = String::from_utf8_lossy(&upstream_output.stdout) - .trim() - .to_string(); - if upstream.is_empty() { - return Ok(None); - } + let rev_list_output = if has_upstream { + let upstream = String::from_utf8_lossy(&upstream_output.stdout) + .trim() + .to_string(); + let range = format!("{upstream}..HEAD"); + crate::process::tokio_command("git") + .args(["rev-list", &limit_arg, &range]) + .current_dir(path) + .output() + .await + .map_err(AppCommandError::io)? + } else { + // No upstream (e.g. newly created branch): fall back to comparing + // against all remote branches to find commits not yet pushed. + let branch_output = crate::process::tokio_command("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .current_dir(path) + .output() + .await + .map_err(AppCommandError::io)?; + if !branch_output.status.success() { + return Ok((None, has_upstream)); + } + let branch = String::from_utf8_lossy(&branch_output.stdout) + .trim() + .to_string(); + if branch.is_empty() || branch == "HEAD" { + return Ok((None, has_upstream)); + } - let range = format!("{upstream}..HEAD"); - let rev_list_output = crate::process::tokio_command("git") - .args(["rev-list", &range]) - .current_dir(path) - .output() - .await - .map_err(AppCommandError::io)?; + 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 remote = remote_output + .ok() + .filter(|output| output.status.success()) + .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| "origin".to_string()); + + let remote_arg = format!("--remotes={}", remote); + crate::process::tokio_command("git") + .args(["rev-list", &limit_arg, "HEAD", "--not", &remote_arg]) + .current_dir(path) + .output() + .await + .map_err(AppCommandError::io)? + }; if !rev_list_output.status.success() { - return Ok(None); + return Ok((None, has_upstream)); } let hashes = String::from_utf8_lossy(&rev_list_output.stdout) @@ -3608,5 +3667,5 @@ async fn get_unpushed_hashes(path: &str) -> Result>, AppC .map(|line| line.to_string()) .collect::>(); - Ok(Some(hashes)) + Ok((Some(hashes), has_upstream)) } diff --git a/src/components/layout/aux-panel-git-log-tab.tsx b/src/components/layout/aux-panel-git-log-tab.tsx index dd069b8..1443c7f 100644 --- a/src/components/layout/aux-panel-git-log-tab.tsx +++ b/src/components/layout/aux-panel-git-log-tab.tsx @@ -782,10 +782,12 @@ export function GitLogTab() { } setError(null) try { - const log = await gitLog(folder.path, 100, branch ?? undefined) - setEntries(log) + const result = await gitLog(folder.path, 100, branch ?? undefined) + setEntries(result.entries) if (inline) { - const commitHashes = new Set(log.map((entry) => entry.full_hash)) + const commitHashes = new Set( + result.entries.map((entry) => entry.full_hash) + ) setOpenByCommit((prev) => filterRecordByCommitHashes(prev, commitHashes) ) diff --git a/src/components/layout/push-workspace.tsx b/src/components/layout/push-workspace.tsx index cf8f7c2..e806df6 100644 --- a/src/components/layout/push-workspace.tsx +++ b/src/components/layout/push-workspace.tsx @@ -279,6 +279,7 @@ export function PushWorkspace({ const { withCredentialRetry } = useGitCredential() const [commits, setCommits] = useState([]) + const [hasUpstream, setHasUpstream] = useState(true) const [listLoading, setListLoading] = useState(false) const [openByCommit, setOpenByCommit] = useState>({}) const [pushing, setPushing] = useState(false) @@ -297,8 +298,9 @@ export function PushWorkspace({ const loadCommits = useCallback(async () => { setListLoading(true) try { - const entries = await gitLog(folderPath, 100) - setCommits(entries) + const result = await gitLog(folderPath, 100) + setCommits(result.entries) + setHasUpstream(result.has_upstream) } catch (err) { toast.error(toErrorMessage(err)) } finally { @@ -358,7 +360,9 @@ export function PushWorkspace({ ) : unpushedCommits.length === 0 ? (
- {t("noUnpushedCommits")} + {!hasUpstream + ? t("newBranchNoPushedCommits") + : t("noUnpushedCommits")}
) : (
@@ -433,7 +437,9 @@ export function PushWorkspace({