修复本地新创建的分支无法推送到远程

This commit is contained in:
xintaofei
2026-03-23 16:09:18 +08:00
parent c8af3e07ac
commit b010ef071d
15 changed files with 114 additions and 32 deletions

View File

@@ -276,6 +276,12 @@ pub struct GitLogFileChange {
pub deletions: u32, pub deletions: u32,
} }
#[derive(Debug, Serialize)]
pub struct GitLogResult {
pub entries: Vec<GitLogEntry>,
pub has_upstream: bool,
}
fn count_non_empty_lines(content: &str) -> usize { fn count_non_empty_lines(content: &str) -> usize {
content content
.lines() .lines()
@@ -3327,7 +3333,7 @@ pub async fn git_log(
path: String, path: String,
limit: Option<u32>, limit: Option<u32>,
branch: Option<String>, branch: Option<String>,
) -> Result<Vec<GitLogEntry>, AppCommandError> { ) -> Result<GitLogResult, AppCommandError> {
const COMMIT_META_PREFIX: &str = "__COMMIT__\0"; const COMMIT_META_PREFIX: &str = "__COMMIT__\0";
const MESSAGE_END_MARKER: &str = "__COMMIT_MESSAGE_END__"; 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") if stderr_str.contains("does not have any commits yet")
|| stderr_str.contains("unknown revision or path not in the working tree") || 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)); return Err(git_command_error("log", &output.stderr));
} }
@@ -3421,14 +3430,20 @@ pub async fn git_log(
entries.push(entry.finish()); 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() { for entry in entries.iter_mut() {
entry.pushed = unpushed_hashes entry.pushed = unpushed_hashes
.as_ref() .as_ref()
.map(|hashes| !hashes.contains(&entry.full_hash)); .map(|hashes| !hashes.contains(&entry.full_hash));
} }
Ok(entries) Ok(GitLogResult {
entries,
has_upstream,
})
} }
#[tauri::command] #[tauri::command]
@@ -3566,7 +3581,13 @@ fn parse_numstat_count(value: &str) -> u32 {
value.parse::<u32>().unwrap_or(0) value.parse::<u32>().unwrap_or(0)
} }
async fn get_unpushed_hashes(path: &str) -> Result<Option<HashSet<String>>, AppCommandError> { /// Returns (unpushed_hashes, has_upstream).
async fn get_unpushed_hashes(
path: &str,
limit: u32,
) -> Result<(Option<HashSet<String>>, bool), AppCommandError> {
let limit_arg = format!("-{}", limit);
let upstream_output = crate::process::tokio_command("git") let upstream_output = crate::process::tokio_command("git")
.args([ .args([
"rev-parse", "rev-parse",
@@ -3579,27 +3600,65 @@ async fn get_unpushed_hashes(path: &str) -> Result<Option<HashSet<String>>, AppC
.await .await
.map_err(AppCommandError::io)?; .map_err(AppCommandError::io)?;
if !upstream_output.status.success() { let has_upstream = upstream_output.status.success()
return Ok(None); && !String::from_utf8_lossy(&upstream_output.stdout)
} .trim()
.is_empty();
let upstream = String::from_utf8_lossy(&upstream_output.stdout) let rev_list_output = if has_upstream {
.trim() let upstream = String::from_utf8_lossy(&upstream_output.stdout)
.to_string(); .trim()
if upstream.is_empty() { .to_string();
return Ok(None); 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 remote_key = format!("branch.{}.remote", branch);
let rev_list_output = crate::process::tokio_command("git") let remote_output = crate::process::tokio_command("git")
.args(["rev-list", &range]) .args(["config", "--get", &remote_key])
.current_dir(path) .current_dir(path)
.output() .output()
.await .await;
.map_err(AppCommandError::io)?; 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() { if !rev_list_output.status.success() {
return Ok(None); return Ok((None, has_upstream));
} }
let hashes = String::from_utf8_lossy(&rev_list_output.stdout) let hashes = String::from_utf8_lossy(&rev_list_output.stdout)
@@ -3608,5 +3667,5 @@ async fn get_unpushed_hashes(path: &str) -> Result<Option<HashSet<String>>, AppC
.map(|line| line.to_string()) .map(|line| line.to_string())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
Ok(Some(hashes)) Ok((Some(hashes), has_upstream))
} }

View File

@@ -782,10 +782,12 @@ export function GitLogTab() {
} }
setError(null) setError(null)
try { try {
const log = await gitLog(folder.path, 100, branch ?? undefined) const result = await gitLog(folder.path, 100, branch ?? undefined)
setEntries(log) setEntries(result.entries)
if (inline) { 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) => setOpenByCommit((prev) =>
filterRecordByCommitHashes(prev, commitHashes) filterRecordByCommitHashes(prev, commitHashes)
) )

View File

@@ -279,6 +279,7 @@ export function PushWorkspace({
const { withCredentialRetry } = useGitCredential() const { withCredentialRetry } = useGitCredential()
const [commits, setCommits] = useState<GitLogEntry[]>([]) const [commits, setCommits] = useState<GitLogEntry[]>([])
const [hasUpstream, setHasUpstream] = useState(true)
const [listLoading, setListLoading] = useState(false) const [listLoading, setListLoading] = useState(false)
const [openByCommit, setOpenByCommit] = useState<Record<string, boolean>>({}) const [openByCommit, setOpenByCommit] = useState<Record<string, boolean>>({})
const [pushing, setPushing] = useState(false) const [pushing, setPushing] = useState(false)
@@ -297,8 +298,9 @@ export function PushWorkspace({
const loadCommits = useCallback(async () => { const loadCommits = useCallback(async () => {
setListLoading(true) setListLoading(true)
try { try {
const entries = await gitLog(folderPath, 100) const result = await gitLog(folderPath, 100)
setCommits(entries) setCommits(result.entries)
setHasUpstream(result.has_upstream)
} catch (err) { } catch (err) {
toast.error(toErrorMessage(err)) toast.error(toErrorMessage(err))
} finally { } finally {
@@ -358,7 +360,9 @@ export function PushWorkspace({
</div> </div>
) : unpushedCommits.length === 0 ? ( ) : unpushedCommits.length === 0 ? (
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground"> <div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
{t("noUnpushedCommits")} {!hasUpstream
? t("newBranchNoPushedCommits")
: t("noUnpushedCommits")}
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-2 p-2"> <div className="flex flex-col gap-2 p-2">
@@ -433,7 +437,9 @@ export function PushWorkspace({
<div className="border-t p-2"> <div className="border-t p-2">
<Button <Button
className="w-full" className="w-full"
disabled={pushing || unpushedCommits.length === 0} disabled={
pushing || (hasUpstream && unpushedCommits.length === 0)
}
onClick={handlePush} onClick={handlePush}
> >
{pushing ? ( {pushing ? (

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "دفع الكود", "title": "دفع الكود",
"noUnpushedCommits": "لا توجد التزامات غير مدفوعة", "noUnpushedCommits": "لا توجد التزامات غير مدفوعة",
"newBranchNoPushedCommits": "فرع جديد — ادفع لإنشاء فرع تتبع عن بُعد",
"unpushed": "غير مدفوع", "unpushed": "غير مدفوع",
"selectFileToViewDiff": "اختر ملفًا لعرض الفرق", "selectFileToViewDiff": "اختر ملفًا لعرض الفرق",
"before": "قبل", "before": "قبل",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "Code pushen", "title": "Code pushen",
"noUnpushedCommits": "Keine ungepushten Commits", "noUnpushedCommits": "Keine ungepushten Commits",
"newBranchNoPushedCommits": "Neuer Branch — pushen, um Remote-Tracking-Branch zu erstellen",
"unpushed": "Nicht gepusht", "unpushed": "Nicht gepusht",
"selectFileToViewDiff": "Datei auswählen, um Unterschiede anzuzeigen", "selectFileToViewDiff": "Datei auswählen, um Unterschiede anzuzeigen",
"before": "Vorher", "before": "Vorher",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "Push Code", "title": "Push Code",
"noUnpushedCommits": "No unpushed commits", "noUnpushedCommits": "No unpushed commits",
"newBranchNoPushedCommits": "New branch — push to create remote tracking branch",
"unpushed": "Unpushed", "unpushed": "Unpushed",
"selectFileToViewDiff": "Select a file to view diff", "selectFileToViewDiff": "Select a file to view diff",
"before": "Before", "before": "Before",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "Enviar código", "title": "Enviar código",
"noUnpushedCommits": "No hay commits sin enviar", "noUnpushedCommits": "No hay commits sin enviar",
"newBranchNoPushedCommits": "Nueva rama — enviar para crear rama de seguimiento remota",
"unpushed": "Sin enviar", "unpushed": "Sin enviar",
"selectFileToViewDiff": "Selecciona un archivo para ver las diferencias", "selectFileToViewDiff": "Selecciona un archivo para ver las diferencias",
"before": "Antes", "before": "Antes",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "Pousser le code", "title": "Pousser le code",
"noUnpushedCommits": "Aucun commit non poussé", "noUnpushedCommits": "Aucun commit non poussé",
"newBranchNoPushedCommits": "Nouvelle branche — pousser pour créer la branche de suivi distante",
"unpushed": "Non poussé", "unpushed": "Non poussé",
"selectFileToViewDiff": "Sélectionnez un fichier pour voir les différences", "selectFileToViewDiff": "Sélectionnez un fichier pour voir les différences",
"before": "Avant", "before": "Avant",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "コードをプッシュ", "title": "コードをプッシュ",
"noUnpushedCommits": "未プッシュのコミットはありません", "noUnpushedCommits": "未プッシュのコミットはありません",
"newBranchNoPushedCommits": "新しいブランチ — プッシュしてリモート追跡ブランチを作成",
"unpushed": "未プッシュ", "unpushed": "未プッシュ",
"selectFileToViewDiff": "ファイルを選択して差分を表示", "selectFileToViewDiff": "ファイルを選択して差分を表示",
"before": "変更前", "before": "変更前",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "코드 푸시", "title": "코드 푸시",
"noUnpushedCommits": "푸시되지 않은 커밋이 없습니다", "noUnpushedCommits": "푸시되지 않은 커밋이 없습니다",
"newBranchNoPushedCommits": "새 브랜치 — 푸시하여 원격 추적 브랜치 생성",
"unpushed": "미푸시", "unpushed": "미푸시",
"selectFileToViewDiff": "파일을 선택하여 차이 보기", "selectFileToViewDiff": "파일을 선택하여 차이 보기",
"before": "변경 전", "before": "변경 전",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "Enviar código", "title": "Enviar código",
"noUnpushedCommits": "Nenhum commit não enviado", "noUnpushedCommits": "Nenhum commit não enviado",
"newBranchNoPushedCommits": "Nova branch — enviar para criar branch de rastreamento remota",
"unpushed": "Não enviado", "unpushed": "Não enviado",
"selectFileToViewDiff": "Selecione um arquivo para ver as diferenças", "selectFileToViewDiff": "Selecione um arquivo para ver as diferenças",
"before": "Antes", "before": "Antes",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "推送代码", "title": "推送代码",
"noUnpushedCommits": "没有未推送的提交", "noUnpushedCommits": "没有未推送的提交",
"newBranchNoPushedCommits": "新分支 — 推送以创建远程跟踪分支",
"unpushed": "未推送", "unpushed": "未推送",
"selectFileToViewDiff": "选择文件查看差异", "selectFileToViewDiff": "选择文件查看差异",
"before": "修改前", "before": "修改前",

View File

@@ -1039,6 +1039,7 @@
"pushWindow": { "pushWindow": {
"title": "推送程式碼", "title": "推送程式碼",
"noUnpushedCommits": "沒有未推送的提交", "noUnpushedCommits": "沒有未推送的提交",
"newBranchNoPushedCommits": "新分支 — 推送以建立遠端追蹤分支",
"unpushed": "未推送", "unpushed": "未推送",
"selectFileToViewDiff": "選擇檔案查看差異", "selectFileToViewDiff": "選擇檔案查看差異",
"before": "修改前", "before": "修改前",

View File

@@ -38,7 +38,7 @@ import type {
FilePreviewContent, FilePreviewContent,
FileEditContent, FileEditContent,
FileSaveResult, FileSaveResult,
GitLogEntry, GitLogResult,
SystemLanguageSettings, SystemLanguageSettings,
SystemProxySettings, SystemProxySettings,
GitCredentials, GitCredentials,
@@ -1048,7 +1048,7 @@ export async function gitLog(
path: string, path: string,
limit?: number, limit?: number,
branch?: string branch?: string
): Promise<GitLogEntry[]> { ): Promise<GitLogResult> {
return invoke("git_log", { return invoke("git_log", {
path, path,
limit: limit ?? null, limit: limit ?? null,

View File

@@ -743,6 +743,11 @@ export interface FileTreeChangedEvent {
refresh_git_status: boolean refresh_git_status: boolean
} }
export interface GitLogResult {
entries: GitLogEntry[]
has_upstream: boolean
}
export interface GitLogEntry { export interface GitLogEntry {
hash: string hash: string
full_hash: string full_hash: string