修复本地新创建的分支无法推送到远程
This commit is contained in:
@@ -276,6 +276,12 @@ pub struct GitLogFileChange {
|
||||
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 {
|
||||
content
|
||||
.lines()
|
||||
@@ -3327,7 +3333,7 @@ pub async fn git_log(
|
||||
path: String,
|
||||
limit: Option<u32>,
|
||||
branch: Option<String>,
|
||||
) -> Result<Vec<GitLogEntry>, AppCommandError> {
|
||||
) -> Result<GitLogResult, AppCommandError> {
|
||||
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::<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")
|
||||
.args([
|
||||
"rev-parse",
|
||||
@@ -3579,27 +3600,65 @@ async fn get_unpushed_hashes(path: &str) -> Result<Option<HashSet<String>>, 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<Option<HashSet<String>>, AppC
|
||||
.map(|line| line.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
Ok(Some(hashes))
|
||||
Ok((Some(hashes), has_upstream))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -279,6 +279,7 @@ export function PushWorkspace({
|
||||
const { withCredentialRetry } = useGitCredential()
|
||||
|
||||
const [commits, setCommits] = useState<GitLogEntry[]>([])
|
||||
const [hasUpstream, setHasUpstream] = useState(true)
|
||||
const [listLoading, setListLoading] = useState(false)
|
||||
const [openByCommit, setOpenByCommit] = useState<Record<string, boolean>>({})
|
||||
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({
|
||||
</div>
|
||||
) : unpushedCommits.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
{t("noUnpushedCommits")}
|
||||
{!hasUpstream
|
||||
? t("newBranchNoPushedCommits")
|
||||
: t("noUnpushedCommits")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
@@ -433,7 +437,9 @@ export function PushWorkspace({
|
||||
<div className="border-t p-2">
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={pushing || unpushedCommits.length === 0}
|
||||
disabled={
|
||||
pushing || (hasUpstream && unpushedCommits.length === 0)
|
||||
}
|
||||
onClick={handlePush}
|
||||
>
|
||||
{pushing ? (
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "دفع الكود",
|
||||
"noUnpushedCommits": "لا توجد التزامات غير مدفوعة",
|
||||
"newBranchNoPushedCommits": "فرع جديد — ادفع لإنشاء فرع تتبع عن بُعد",
|
||||
"unpushed": "غير مدفوع",
|
||||
"selectFileToViewDiff": "اختر ملفًا لعرض الفرق",
|
||||
"before": "قبل",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "Code pushen",
|
||||
"noUnpushedCommits": "Keine ungepushten Commits",
|
||||
"newBranchNoPushedCommits": "Neuer Branch — pushen, um Remote-Tracking-Branch zu erstellen",
|
||||
"unpushed": "Nicht gepusht",
|
||||
"selectFileToViewDiff": "Datei auswählen, um Unterschiede anzuzeigen",
|
||||
"before": "Vorher",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "Push Code",
|
||||
"noUnpushedCommits": "No unpushed commits",
|
||||
"newBranchNoPushedCommits": "New branch — push to create remote tracking branch",
|
||||
"unpushed": "Unpushed",
|
||||
"selectFileToViewDiff": "Select a file to view diff",
|
||||
"before": "Before",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "Enviar código",
|
||||
"noUnpushedCommits": "No hay commits sin enviar",
|
||||
"newBranchNoPushedCommits": "Nueva rama — enviar para crear rama de seguimiento remota",
|
||||
"unpushed": "Sin enviar",
|
||||
"selectFileToViewDiff": "Selecciona un archivo para ver las diferencias",
|
||||
"before": "Antes",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "Pousser le code",
|
||||
"noUnpushedCommits": "Aucun commit non poussé",
|
||||
"newBranchNoPushedCommits": "Nouvelle branche — pousser pour créer la branche de suivi distante",
|
||||
"unpushed": "Non poussé",
|
||||
"selectFileToViewDiff": "Sélectionnez un fichier pour voir les différences",
|
||||
"before": "Avant",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "コードをプッシュ",
|
||||
"noUnpushedCommits": "未プッシュのコミットはありません",
|
||||
"newBranchNoPushedCommits": "新しいブランチ — プッシュしてリモート追跡ブランチを作成",
|
||||
"unpushed": "未プッシュ",
|
||||
"selectFileToViewDiff": "ファイルを選択して差分を表示",
|
||||
"before": "変更前",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "코드 푸시",
|
||||
"noUnpushedCommits": "푸시되지 않은 커밋이 없습니다",
|
||||
"newBranchNoPushedCommits": "새 브랜치 — 푸시하여 원격 추적 브랜치 생성",
|
||||
"unpushed": "미푸시",
|
||||
"selectFileToViewDiff": "파일을 선택하여 차이 보기",
|
||||
"before": "변경 전",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "Enviar código",
|
||||
"noUnpushedCommits": "Nenhum commit não enviado",
|
||||
"newBranchNoPushedCommits": "Nova branch — enviar para criar branch de rastreamento remota",
|
||||
"unpushed": "Não enviado",
|
||||
"selectFileToViewDiff": "Selecione um arquivo para ver as diferenças",
|
||||
"before": "Antes",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "推送代码",
|
||||
"noUnpushedCommits": "没有未推送的提交",
|
||||
"newBranchNoPushedCommits": "新分支 — 推送以创建远程跟踪分支",
|
||||
"unpushed": "未推送",
|
||||
"selectFileToViewDiff": "选择文件查看差异",
|
||||
"before": "修改前",
|
||||
|
||||
@@ -1039,6 +1039,7 @@
|
||||
"pushWindow": {
|
||||
"title": "推送程式碼",
|
||||
"noUnpushedCommits": "沒有未推送的提交",
|
||||
"newBranchNoPushedCommits": "新分支 — 推送以建立遠端追蹤分支",
|
||||
"unpushed": "未推送",
|
||||
"selectFileToViewDiff": "選擇檔案查看差異",
|
||||
"before": "修改前",
|
||||
|
||||
@@ -38,7 +38,7 @@ import type {
|
||||
FilePreviewContent,
|
||||
FileEditContent,
|
||||
FileSaveResult,
|
||||
GitLogEntry,
|
||||
GitLogResult,
|
||||
SystemLanguageSettings,
|
||||
SystemProxySettings,
|
||||
GitCredentials,
|
||||
@@ -1048,7 +1048,7 @@ export async function gitLog(
|
||||
path: string,
|
||||
limit?: number,
|
||||
branch?: string
|
||||
): Promise<GitLogEntry[]> {
|
||||
): Promise<GitLogResult> {
|
||||
return invoke("git_log", {
|
||||
path,
|
||||
limit: limit ?? null,
|
||||
|
||||
@@ -743,6 +743,11 @@ export interface FileTreeChangedEvent {
|
||||
refresh_git_status: boolean
|
||||
}
|
||||
|
||||
export interface GitLogResult {
|
||||
entries: GitLogEntry[]
|
||||
has_upstream: boolean
|
||||
}
|
||||
|
||||
export interface GitLogEntry {
|
||||
hash: string
|
||||
full_hash: string
|
||||
|
||||
Reference in New Issue
Block a user