Compare commits

...

2 Commits

Author SHA1 Message Date
Gregory Schier
f27d500a2c Merge branch 'main' into fix/git-pull-and-commit-improvements 2026-02-12 14:50:17 -08:00
Gregory Schier
d84fec8c7c Fix git pull conflicts with pull.ff=only and improve commit UX
- Replace git pull with fetch + merge to avoid conflicts with global
  git config (e.g. pull.ff=only) and background fetch --all
- Disable commit/commit+push buttons when message is empty
- Always show Push/Pull menu items even without remotes configured
- Default remote name to 'origin' when adding a new remote
2026-02-12 14:49:30 -08:00
5 changed files with 51 additions and 27 deletions

View File

@@ -44,43 +44,65 @@ pub async fn git_pull(dir: &Path) -> Result<PullResult> {
(branch_name, remote_name, remote_url)
};
let out = new_binary_command(dir)
// Step 1: fetch the specific branch
// NOTE: We use fetch + merge instead of `git pull` to avoid conflicts with
// global git config (e.g. pull.ff=only) and the background fetch --all.
let fetch_out = new_binary_command(dir)
.await?
.args(["pull", &remote_name, &branch_name])
.args(["fetch", &remote_name, &branch_name])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.await
.map_err(|e| GenericError(format!("failed to run git pull: {e}")))?;
.map_err(|e| GenericError(format!("failed to run git fetch: {e}")))?;
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = stdout + stderr;
let fetch_stdout = String::from_utf8_lossy(&fetch_out.stdout);
let fetch_stderr = String::from_utf8_lossy(&fetch_out.stderr);
let fetch_combined = format!("{fetch_stdout}{fetch_stderr}");
info!("Pulled status={} {combined}", out.status);
info!("Fetched status={} {fetch_combined}", fetch_out.status);
if combined.to_lowercase().contains("could not read") {
if fetch_combined.to_lowercase().contains("could not read") {
return Ok(PullResult::NeedsCredentials { url: remote_url.to_string(), error: None });
}
if combined.to_lowercase().contains("unable to access") {
if fetch_combined.to_lowercase().contains("unable to access") {
return Ok(PullResult::NeedsCredentials {
url: remote_url.to_string(),
error: Some(combined.to_string()),
error: Some(fetch_combined.to_string()),
});
}
if !out.status.success() {
let combined_lower = combined.to_lowercase();
if combined_lower.contains("cannot fast-forward")
|| combined_lower.contains("not possible to fast-forward")
|| combined_lower.contains("diverged")
if !fetch_out.status.success() {
return Err(GenericError(format!("Failed to fetch: {fetch_combined}")));
}
// Step 2: merge the fetched branch
let ref_name = format!("{}/{}", remote_name, branch_name);
let merge_out = new_binary_command(dir)
.await?
.args(["merge", "--ff-only", &ref_name])
.output()
.await
.map_err(|e| GenericError(format!("failed to run git merge: {e}")))?;
let merge_stdout = String::from_utf8_lossy(&merge_out.stdout);
let merge_stderr = String::from_utf8_lossy(&merge_out.stderr);
let merge_combined = format!("{merge_stdout}{merge_stderr}");
info!("Merged status={} {merge_combined}", merge_out.status);
if !merge_out.status.success() {
let merge_lower = merge_combined.to_lowercase();
if merge_lower.contains("cannot fast-forward")
|| merge_lower.contains("not possible to fast-forward")
|| merge_lower.contains("diverged")
{
return Ok(PullResult::Diverged { remote: remote_name, branch: branch_name });
}
return Err(GenericError(format!("Failed to pull {combined}")));
return Err(GenericError(format!("Failed to merge: {merge_combined}")));
}
if combined.to_lowercase().contains("up to date") {
if merge_combined.to_lowercase().contains("up to date") {
return Ok(PullResult::UpToDate);
}

View File

@@ -176,7 +176,11 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
}
if (!hasAnythingToAdd) {
return <EmptyStateText>No changes since last commit</EmptyStateText>;
return (
<div className="h-full px-6 pb-4">
<EmptyStateText>No changes since last commit</EmptyStateText>
</div>
);
}
return (
@@ -230,14 +234,14 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
hideLabel
/>
{commitError && <Banner color="danger">{commitError}</Banner>}
<HStack alignItems="center">
<HStack alignItems="center" space={2}>
<InlineCode>{status.data?.headRefShorthand}</InlineCode>
<HStack space={2} className="ml-auto">
<Button
color="secondary"
size="sm"
onClick={handleCreateCommit}
disabled={!hasAddedAnything}
disabled={!hasAddedAnything || message.trim().length === 0}
isLoading={isPushing}
>
Commit
@@ -245,7 +249,7 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
<Button
color="primary"
size="sm"
disabled={!hasAddedAnything}
disabled={!hasAddedAnything || message.trim().length === 0}
onClick={handleCreateCommitAndPush}
isLoading={isPushing}
>

View File

@@ -173,7 +173,6 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
{ type: 'separator' },
{
label: 'Push',
hidden: !hasRemotes,
leftSlot: <Icon icon="arrow_up_from_line" />,
waitForOnSelect: true,
async onSelect() {
@@ -192,7 +191,6 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
},
{
label: 'Pull',
hidden: !hasRemotes,
leftSlot: <Icon icon="arrow_down_to_line" />,
waitForOnSelect: true,
async onSelect() {

View File

@@ -2,13 +2,13 @@ import type { GitCallbacks } from '@yaakapp-internal/git';
import { sync } from '../../init/sync';
import { promptCredentials } from './credentials';
import { promptDivergedStrategy } from './diverged';
import { promptUncommittedChangesStrategy } from './uncommitted';
import { addGitRemote } from './showAddRemoteDialog';
import { promptUncommittedChangesStrategy } from './uncommitted';
export function gitCallbacks(dir: string): GitCallbacks {
return {
addRemote: async () => {
return addGitRemote(dir);
return addGitRemote(dir, 'origin');
},
promptCredentials: async ({ url, error }) => {
const creds = await promptCredentials({ url, error });

View File

@@ -3,12 +3,12 @@ import { gitMutations } from '@yaakapp-internal/git';
import { showPromptForm } from '../../lib/prompt-form';
import { gitCallbacks } from './callbacks';
export async function addGitRemote(dir: string): Promise<GitRemote> {
export async function addGitRemote(dir: string, defaultName?: string): Promise<GitRemote> {
const r = await showPromptForm({
id: 'add-remote',
title: 'Add Remote',
inputs: [
{ type: 'text', label: 'Name', name: 'name' },
{ type: 'text', label: 'Name', name: 'name', defaultValue: defaultName },
{ type: 'text', label: 'URL', name: 'url' },
],
});