Fix/remote base branches (#557)

* fix remote branch detection and worktree interactions

Refactor GitService to improve remote handling and branch management

fix: update branch selection logic to include all branches and improve condition checks

* Clippy, fmt

* Fix branch upstream setting in GitService to handle non-remote branches

* Remove force push from refspec in GitService to prevent non-fast-forward updates

* Add error handling for diverged branches in GitService

* Fix base-branch normalization robust for PRs

---------

Co-authored-by: Solomon <abcpro11051@disroot.org>
This commit is contained in:
Alex Netsch
2025-08-26 18:09:47 +01:00
committed by GitHub
parent 9190281cfc
commit 5d8a209785
6 changed files with 286 additions and 218 deletions

View File

@@ -691,7 +691,6 @@ impl ContainerService for LocalContainerService {
fn task_attempt_to_current_dir(&self, task_attempt: &TaskAttempt) -> PathBuf {
PathBuf::from(task_attempt.container_ref.clone().unwrap_or_default())
}
/// Create a container
async fn create(&self, task_attempt: &TaskAttempt) -> Result<ContainerRef, ContainerError> {
let task = task_attempt
@@ -712,7 +711,7 @@ impl ContainerService for LocalContainerService {
&project.git_repo_path,
&task_branch_name,
&worktree_path,
Some(&task_attempt.base_branch),
&task_attempt.base_branch,
true, // create new branch
)
.await?;
@@ -930,7 +929,7 @@ impl ContainerService for LocalContainerService {
task_attempt.id
)))?;
let is_ahead = if let Ok((ahead, _)) = self.git().get_local_branch_status(
let is_ahead = if let Ok((ahead, _)) = self.git().get_branch_status(
&project_repo_path,
&task_branch,
&task_attempt.base_branch,

View File

@@ -1,3 +1,5 @@
use std::path::PathBuf;
use axum::{
extract::{Query, State},
http::StatusCode,
@@ -27,7 +29,7 @@ use executors::{
profile::{ProfileConfigs, ProfileVariantLabel},
};
use futures_util::TryStreamExt;
use local_deployment::container;
use git2::BranchType;
use serde::{Deserialize, Serialize};
use services::services::{
container::ContainerService,
@@ -106,12 +108,11 @@ pub async fn create_task_attempt(
profile_variant_label.profile
)))
})?;
let task_attempt = TaskAttempt::create(
&deployment.db().pool,
&CreateTaskAttempt {
profile: profile.default.label.clone(),
base_branch: payload.base_branch,
base_branch: payload.base_branch.clone(),
},
payload.task_id,
)
@@ -361,24 +362,21 @@ pub async fn push_task_attempt_branch(
let github_service = GitHubService::new(&github_token)?;
github_service.check_token().await?;
let pool = &deployment.db().pool;
let task = task_attempt
.parent_task(pool)
.await?
.ok_or(ApiError::TaskAttempt(TaskAttemptError::TaskNotFound))?;
let project = Project::find_by_id(pool, task.project_id)
.await?
.ok_or(ApiError::Project(ProjectError::ProjectNotFound))?;
let branch_name = task_attempt.branch.as_ref().ok_or_else(|| {
ApiError::TaskAttempt(TaskAttemptError::ValidationError(
"No branch found for task attempt".to_string(),
))
})?;
let ws_path = PathBuf::from(
deployment
.container()
.ensure_container_exists(&task_attempt)
.await?,
);
deployment
.git()
.push_to_github(&project.git_repo_path, branch_name, &github_token)?;
.push_to_github(&ws_path, branch_name, &github_token)?;
Ok(ResponseJson(ApiResponse::success(())))
}
@@ -436,12 +434,17 @@ pub async fn create_github_pr(
"No branch found for task attempt".to_string(),
))
})?;
let workspace_path = PathBuf::from(
deployment
.container()
.ensure_container_exists(&task_attempt)
.await?,
);
// Push the branch to GitHub first
if let Err(e) =
deployment
.git()
.push_to_github(&project.git_repo_path, branch_name, &github_token)
if let Err(e) = deployment
.git()
.push_to_github(&workspace_path, branch_name, &github_token)
{
tracing::error!("Failed to push branch to GitHub: {}", e);
let gh_e = GitHubServiceError::from(e);
@@ -453,12 +456,32 @@ pub async fn create_github_pr(
)));
}
}
let norm_base_branch_name = if matches!(
deployment
.git()
.find_branch_type(&project.git_repo_path, &base_branch)?,
BranchType::Remote
) {
// Remote branches are formatted as {remote}/{branch} locally.
// For PR APIs, we must provide just the branch name.
let remote = deployment
.git()
.get_remote_name_from_branch_name(&workspace_path, &base_branch)?;
let remote_prefix = format!("{}/", remote);
base_branch
.strip_prefix(&remote_prefix)
.unwrap_or(&base_branch)
.to_string()
} else {
base_branch
};
// Create the PR using GitHub service
let pr_request = CreatePrRequest {
title: request.title.clone(),
body: request.body.clone(),
head_branch: branch_name.clone(),
base_branch: base_branch.clone(),
base_branch: norm_base_branch_name.clone(),
};
match github_service.create_pr(&repo_info, &pr_request).await {
@@ -467,7 +490,7 @@ pub async fn create_github_pr(
if let Err(e) = Merge::create_pr(
pool,
task_attempt.id,
&base_branch,
&norm_base_branch_name,
pr_info.number,
&pr_info.url,
)
@@ -599,26 +622,32 @@ pub async fn get_task_attempt_branch_status(
.ok_or(ApiError::TaskAttempt(TaskAttemptError::ValidationError(
"No branch found for task attempt".to_string(),
)))?;
let base_branch_type = deployment
.git()
.find_branch_type(&ctx.project.git_repo_path, &task_attempt.base_branch)?;
let (commits_ahead, commits_behind) = deployment.git().get_local_branch_status(
&ctx.project.git_repo_path,
&task_branch,
&task_attempt.base_branch,
)?;
let (commits_ahead, commits_behind) = if matches!(base_branch_type, BranchType::Local) {
let (a, b) = deployment.git().get_branch_status(
&ctx.project.git_repo_path,
&task_branch,
&task_attempt.base_branch,
)?;
(Some(a), Some(b))
} else {
(None, None)
};
// Fetch merges for this task attempt and add to branch status
let merges = Merge::find_by_task_attempt_id(pool, task_attempt.id).await?;
let mut branch_status = BranchStatus {
commits_ahead: Some(commits_ahead),
commits_behind: Some(commits_behind),
commits_ahead,
commits_behind,
has_uncommitted_changes,
remote_commits_ahead: None,
remote_commits_behind: None,
merges,
base_branch_name: task_attempt.base_branch.clone(),
};
// check remote status if the attempt has an open PR
if branch_status.merges.first().is_some_and(|m| {
let has_open_pr = branch_status.merges.first().is_some_and(|m| {
matches!(
m,
Merge::Pr(PrMerge {
@@ -629,14 +658,29 @@ pub async fn get_task_attempt_branch_status(
..
})
)
}) {
});
// check remote status if the attempt has an open PR or the base_branch is a remote branch
if has_open_pr || base_branch_type == BranchType::Remote {
let github_config = deployment.config().read().await.github.clone();
let token = github_config
.token()
.ok_or(ApiError::GitHubService(GitHubServiceError::TokenInvalid))?;
let (remote_commits_ahead, remote_commits_behind) = deployment
.git()
.get_remote_branch_status(&ctx.project.git_repo_path, &task_branch, token)?;
// For an attempt with a remote base branch, we compare against that
// After opening a PR, the attempt has a remote branch itself, so we use that
let remote_base_branch = if base_branch_type == BranchType::Remote && !has_open_pr {
Some(task_attempt.base_branch)
} else {
None
};
let (remote_commits_ahead, remote_commits_behind) =
deployment.git().get_remote_branch_status(
&ctx.project.git_repo_path,
&task_branch,
remote_base_branch.as_deref(),
token,
)?;
branch_status.remote_commits_ahead = Some(remote_commits_ahead);
branch_status.remote_commits_behind = Some(remote_commits_behind);
}
@@ -682,14 +726,12 @@ pub async fn rebase_task_attempt(
if let Some(new_base_branch) = &effective_base_branch {
if new_base_branch != &ctx.task_attempt.base_branch {
// for remote branches, store the local branch name in the database
let db_branch_name = if new_base_branch.starts_with("origin/") {
new_base_branch.strip_prefix("origin/").unwrap()
} else {
new_base_branch
};
TaskAttempt::update_base_branch(&deployment.db().pool, task_attempt.id, db_branch_name)
.await?;
TaskAttempt::update_base_branch(
&deployment.db().pool,
task_attempt.id,
new_base_branch,
)
.await?;
}
}

View File

@@ -2,8 +2,8 @@ use std::{collections::HashMap, path::Path};
use chrono::{DateTime, Utc};
use git2::{
BranchType, CherrypickOptions, Delta, DiffFindOptions, DiffOptions, Error as GitError,
FetchOptions, Repository, Sort, build::CheckoutBuilder,
Branch, BranchType, CherrypickOptions, Delta, DiffFindOptions, DiffOptions, Error as GitError,
FetchOptions, Reference, Remote, Repository, Sort, build::CheckoutBuilder,
};
use regex;
use serde::Serialize;
@@ -27,6 +27,8 @@ pub enum GitServiceError {
BranchNotFound(String),
#[error("Merge conflicts: {0}")]
MergeConflicts(String),
#[error("Branches diverged: {0}")]
BranchesDiverged(String),
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("{0} has uncommitted changes: {1}")]
@@ -94,6 +96,19 @@ impl GitService {
Repository::open(repo_path).map_err(GitServiceError::from)
}
pub fn default_remote_name(&self, repo: &Repository) -> String {
if let Ok(repos) = repo.remotes() {
repos
.iter()
.flatten()
.next()
.map(|r| r.to_owned())
.unwrap_or_else(|| "origin".to_string())
} else {
"origin".to_string()
}
}
/// Initialize a new git repository with a main branch and initial commit
pub fn initialize_repo_with_main_branch(
&self,
@@ -218,10 +233,8 @@ impl GitService {
base_branch,
} => {
let repo = Repository::open(worktree_path)?;
let base_ref = repo
.find_branch(base_branch, BranchType::Local)
.map_err(|_| GitServiceError::BranchNotFound(base_branch.to_string()))?;
let base_tree = base_ref.get().peel_to_commit()?.tree()?;
let base_git_branch = GitService::find_branch(&repo, base_branch)?;
let base_tree = base_git_branch.get().peel_to_commit()?.tree()?;
let mut diff_opts = DiffOptions::new();
diff_opts
@@ -251,15 +264,11 @@ impl GitService {
base_branch,
} => {
let repo = self.open_repo(repo_path)?;
let base_tree = repo
.find_branch(base_branch, BranchType::Local)
.map_err(|_| GitServiceError::BranchNotFound(base_branch.to_string()))?
let base_tree = Self::find_branch(&repo, base_branch)?
.get()
.peel_to_commit()?
.tree()?;
let branch_tree = repo
.find_branch(branch_name, BranchType::Local)
.map_err(|_| GitServiceError::BranchNotFound(branch_name.to_string()))?
let branch_tree = Self::find_branch(&repo, branch_name)?
.get()
.peel_to_commit()?
.tree()?;
@@ -527,14 +536,10 @@ impl GitService {
self.check_worktree_clean(&main_repo)?;
// Verify the task branch exists in the worktree
let task_branch = worktree_repo
.find_branch(branch_name, BranchType::Local)
.map_err(|_| GitServiceError::BranchNotFound(branch_name.to_string()))?;
let task_branch = Self::find_branch(&worktree_repo, branch_name)?;
// Get the base branch from the worktree
let base_branch = worktree_repo
.find_branch(base_branch_name, BranchType::Local)
.map_err(|_| GitServiceError::BranchNotFound(base_branch_name.to_string()))?;
let base_branch = Self::find_branch(&worktree_repo, base_branch_name)?;
// Get commits
let base_commit = base_branch.get().peel_to_commit()?;
@@ -579,54 +584,61 @@ impl GitService {
Ok(squash_commit_id.to_string())
}
fn get_branch_status_inner(
&self,
repo: &Repository,
branch_ref: &Reference,
base_branch_ref: &Reference,
) -> Result<(usize, usize), GitServiceError> {
let (a, b) = repo.graph_ahead_behind(
branch_ref.target().ok_or(GitServiceError::BranchNotFound(
"Branch not found".to_string(),
))?,
base_branch_ref
.target()
.ok_or(GitServiceError::BranchNotFound(
"Branch not found".to_string(),
))?,
)?;
Ok((a, b))
}
pub fn get_local_branch_status(
pub fn get_branch_status(
&self,
repo_path: &Path,
branch_name: &str,
base_branch_name: &str,
) -> Result<(usize, usize), GitServiceError> {
let repo = Repository::open(repo_path)?;
let branch_ref = repo
// try "refs/heads/<name>" first, then raw name
.find_reference(&format!("refs/heads/{branch_name}"))
.or_else(|_| repo.find_reference(branch_name))?;
let branch_oid = branch_ref.target().unwrap();
// Calculate ahead/behind counts using the stored base branch
let base_oid = repo
.find_branch(base_branch_name, BranchType::Local)?
.get()
.target()
.ok_or(GitServiceError::BranchNotFound(format!(
"refs/heads/{base_branch_name}"
)))?;
let (a, b) = repo.graph_ahead_behind(branch_oid, base_oid)?;
Ok((a, b))
let branch = Self::find_branch(&repo, branch_name)?;
let base_branch = Self::find_branch(&repo, base_branch_name)?;
self.get_branch_status_inner(
&repo,
&branch.into_reference(),
&base_branch.into_reference(),
)
}
pub fn get_remote_branch_status(
&self,
repo_path: &Path,
branch_name: &str,
base_branch_name: Option<&str>,
github_token: String,
) -> Result<(usize, usize), GitServiceError> {
let repo = Repository::open(repo_path)?;
let branch_ref = repo
// try "refs/heads/<name>" first, then raw name
.find_reference(&format!("refs/heads/{branch_name}"))
.or_else(|_| repo.find_reference(branch_name))?;
let branch_oid = branch_ref.target().unwrap();
// Check for unpushed commits by comparing with origin/branch_name
self.fetch_from_remote(&repo, &github_token)?;
let remote_oid = repo
.find_reference(&format!("refs/remotes/origin/{branch_name}"))?
.target()
.ok_or(GitServiceError::BranchNotFound(format!(
"origin/{branch_name}"
)))?;
let (a, b) = repo.graph_ahead_behind(branch_oid, remote_oid)?;
Ok((a, b))
let branch_ref = Self::find_branch(&repo, branch_name)?.into_reference();
// base branch is either given or upstream of branch_name
let base_branch_ref = if let Some(bn) = base_branch_name {
Self::find_branch(&repo, bn)?
} else {
repo.find_branch(branch_name, BranchType::Local)?
.upstream()?
}
.into_reference();
let remote = self.get_remote_from_branch_ref(&repo, &base_branch_ref)?;
self.fetch_from_remote(&repo, &github_token, &remote)?;
self.get_branch_status_inner(&repo, &branch_ref, &base_branch_ref)
}
pub fn is_worktree_clean(&self, worktree_path: &Path) -> Result<bool, GitServiceError> {
@@ -848,7 +860,7 @@ impl GitService {
}
// Get the target base branch reference
let base_branch_name = match new_base_branch {
let new_base_branch_name = match new_base_branch {
Some(branch) => branch.to_string(),
None => main_repo
.head()
@@ -856,76 +868,30 @@ impl GitService {
.and_then(|head| head.shorthand().map(|s| s.to_string()))
.unwrap_or_else(|| "main".to_string()),
};
let base_branch_name = base_branch_name.as_str();
// Handle remote branches by fetching them first and creating/updating local tracking branches
let local_branch_name = if base_branch_name.starts_with("origin/") {
let nbr = Self::find_branch(&main_repo, &new_base_branch_name)?.into_reference();
let new_base_commit_id = if nbr.is_remote() {
let github_token = github_token.ok_or(GitServiceError::TokenUnavailable)?;
// This is a remote branch, fetch it and create/update local tracking branch
let remote_branch_name = base_branch_name.strip_prefix("origin/").unwrap();
let remote = self.get_remote_from_branch_ref(&main_repo, &nbr)?;
// First, fetch the latest changes from remote
self.fetch_from_remote(&main_repo, &github_token)?;
self.fetch_from_remote(&main_repo, &github_token, &remote)?;
// Try to find the remote branch after fetch
let remote_branch = main_repo
.find_branch(base_branch_name, BranchType::Remote)
.map_err(|_| GitServiceError::BranchNotFound(base_branch_name.to_string()))?;
// Check if local tracking branch exists
match main_repo.find_branch(remote_branch_name, BranchType::Local) {
Ok(mut local_branch) => {
// Local tracking branch exists, update it to match remote
let remote_commit = remote_branch.get().peel_to_commit()?;
local_branch
.get_mut()
.set_target(remote_commit.id(), "Update local branch to match remote")?;
}
Err(_) => {
// Local tracking branch doesn't exist, create it
let remote_commit = remote_branch.get().peel_to_commit()?;
main_repo.branch(remote_branch_name, &remote_commit, false)?;
}
}
let remote_branch = Self::find_branch(&main_repo, &new_base_branch_name)?;
remote_branch.into_reference().peel_to_commit()?.id()
// Use the local branch name for rebase
remote_branch_name
} else {
// This is already a local branch
base_branch_name
nbr.peel_to_commit()?.id()
};
// Get the local branch for rebase
let base_branch = main_repo
.find_branch(local_branch_name, BranchType::Local)
.map_err(|_| GitServiceError::BranchNotFound(local_branch_name.to_string()))?;
let new_base_commit_id = base_branch.get().peel_to_commit()?.id();
// Remember the original task-branch commit before we touch anything
let original_head_oid = worktree_repo.head()?.peel_to_commit()?.id();
// Get the HEAD commit of the worktree (the changes to rebase)
let head = worktree_repo.head()?;
let task_branch_commit_id = head.peel_to_commit()?.id();
let task_branch_commit_id = worktree_repo.head()?.peel_to_commit()?.id();
let signature = worktree_repo.signature()?;
// Find the old base branch
let old_base_branch_ref = if old_base_branch.starts_with("origin/") {
// Remote branch - get local tracking branch name
let remote_branch_name = old_base_branch.strip_prefix("origin/").unwrap();
main_repo
.find_branch(remote_branch_name, BranchType::Local)
.map_err(|_| GitServiceError::BranchNotFound(remote_branch_name.to_string()))?
} else {
// Local branch
main_repo
.find_branch(old_base_branch, BranchType::Local)
.map_err(|_| GitServiceError::BranchNotFound(old_base_branch.to_string()))?
};
let old_base_commit_id = old_base_branch_ref.get().peel_to_commit()?.id();
let old_base_commit_id = Self::find_branch(&main_repo, old_base_branch)?
.into_reference()
.peel_to_commit()?
.id();
// Find commits unique to the task branch
let unique_commits = Self::find_unique_commits(
@@ -964,12 +930,47 @@ impl GitService {
}
// Get the final commit ID after rebase
let final_head = worktree_repo.head()?;
let final_commit = final_head.peel_to_commit()?;
let final_commit = worktree_repo.head()?.peel_to_commit()?;
Ok(final_commit.id().to_string())
}
pub fn find_branch_type(
&self,
repo_path: &Path,
branch_name: &str,
) -> Result<BranchType, GitServiceError> {
let repo = self.open_repo(repo_path)?;
// Try to find the branch as a local branch first
match repo.find_branch(branch_name, BranchType::Local) {
Ok(_) => Ok(BranchType::Local),
Err(_) => {
// If not found, try to find it as a remote branch
match repo.find_branch(branch_name, BranchType::Remote) {
Ok(_) => Ok(BranchType::Remote),
Err(_) => Err(GitServiceError::BranchNotFound(branch_name.to_string())),
}
}
}
}
pub fn find_branch<'a>(
repo: &'a Repository,
branch_name: &str,
) -> Result<git2::Branch<'a>, GitServiceError> {
// Try to find the branch as a local branch first
match repo.find_branch(branch_name, BranchType::Local) {
Ok(branch) => Ok(branch),
Err(_) => {
// If not found, try to find it as a remote branch
match repo.find_branch(branch_name, BranchType::Remote) {
Ok(branch) => Ok(branch),
Err(_) => Err(GitServiceError::BranchNotFound(branch_name.to_string())),
}
}
}
}
/// Delete a file from the repository and commit the change
pub fn delete_file_and_commit(
&self,
@@ -1039,13 +1040,14 @@ impl GitService {
repo_path: &Path,
) -> Result<GitHubRepoInfo, GitServiceError> {
let repo = self.open_repo(repo_path)?;
let remote = repo.find_remote("origin").map_err(|_| {
GitServiceError::InvalidRepository("No 'origin' remote found".to_string())
let remote_name = self.default_remote_name(&repo);
let remote = repo.find_remote(&remote_name).map_err(|_| {
GitServiceError::InvalidRepository(format!("No '{remote_name}' remote found"))
})?;
let url = remote.url().ok_or_else(|| {
GitServiceError::InvalidRepository("Remote origin has no URL".to_string())
})?;
let url = remote
.url()
.ok_or_else(|| GitServiceError::InvalidRepository("Remote has no URL".to_string()))?;
// Parse GitHub URL (supports both HTTPS and SSH formats)
let github_regex = regex::Regex::new(r"github\.com[:/]([^/]+)/(.+?)(?:\.git)?/?$")
@@ -1062,7 +1064,43 @@ impl GitService {
}
}
/// Push the branch to GitHub remote
pub fn get_remote_name_from_branch_name(
&self,
repo_path: &Path,
branch_name: &str,
) -> Result<String, GitServiceError> {
let repo = Repository::open(repo_path)?;
let branch_ref = Self::find_branch(&repo, branch_name)?.into_reference();
let default_remote = self.default_remote_name(&repo);
self.get_remote_from_branch_ref(&repo, &branch_ref)
.map(|r| r.name().unwrap_or(&default_remote).to_string())
}
fn get_remote_from_branch_ref<'a>(
&self,
repo: &'a Repository,
branch_ref: &Reference,
) -> Result<Remote<'a>, GitServiceError> {
let branch_name = branch_ref
.name()
.map(|name| name.to_string())
.ok_or_else(|| GitServiceError::InvalidRepository("Invalid branch ref".into()))?;
let remote_name_buf = repo.branch_remote_name(&branch_name)?;
let remote_name = str::from_utf8(&remote_name_buf)
.map_err(|e| {
GitServiceError::InvalidRepository(format!(
"Invalid remote name for branch {branch_name}: {e}"
))
})?
.to_string();
repo.find_remote(&remote_name).map_err(|_| {
GitServiceError::InvalidRepository(format!(
"Remote '{remote_name}' for branch '{branch_name}' not found"
))
})
}
pub fn push_to_github(
&self,
worktree_path: &Path,
@@ -1073,10 +1111,12 @@ impl GitService {
self.check_worktree_clean(&repo)?;
// Get the remote
let remote = repo.find_remote("origin")?;
let remote_url = remote.url().ok_or_else(|| {
GitServiceError::InvalidRepository("Remote origin has no URL".to_string())
})?;
let remote_name = self.default_remote_name(&repo);
let remote = repo.find_remote(&remote_name)?;
let remote_url = remote
.url()
.ok_or_else(|| GitServiceError::InvalidRepository("Remote has no URL".to_string()))?;
let https_url = self.convert_to_https_url(remote_url);
// Create a temporary remote with HTTPS URL for pushing
@@ -1108,7 +1148,19 @@ impl GitService {
let _ = repo.remote_delete(temp_remote_name);
// Check push result
push_result?;
push_result.map_err(|e| match e.code() {
git2::ErrorCode::NotFastForward => {
GitServiceError::BranchesDiverged(format!(
"Push failed: branch '{branch_name}' has diverged and cannot be fast-forwarded. Either merge the changes or force push."
))
}
_ => e.into(),
})?;
self.fetch_from_remote(&repo, github_token, &remote)?;
let mut branch = Self::find_branch(&repo, branch_name)?;
if !branch.get().is_remote() {
branch.set_upstream(Some(&format!("{remote_name}/{branch_name}")))?;
}
Ok(())
}
@@ -1131,12 +1183,12 @@ impl GitService {
&self,
repo: &Repository,
github_token: &str,
remote: &Remote,
) -> Result<(), GitServiceError> {
// Get the remote
let remote = repo.find_remote("origin")?;
let remote_url = remote.url().ok_or_else(|| {
GitServiceError::InvalidRepository("Remote origin has no URL".to_string())
})?;
let remote_url = remote
.url()
.ok_or_else(|| GitServiceError::InvalidRepository("Remote has no URL".to_string()))?;
// Create a temporary remote with HTTPS URL for fetching
let temp_remote_name = "temp_https_origin";
@@ -1157,14 +1209,12 @@ impl GitService {
// Configure fetch options
let mut fetch_opts = FetchOptions::new();
fetch_opts.remote_callbacks(callbacks);
let default_remote_name = self.default_remote_name(repo);
let remote_name = remote.name().unwrap_or(&default_remote_name);
// Fetch from the temporary remote
let refspec = format!("+refs/heads/*:refs/remotes/{remote_name}/*");
let fetch_result = temp_remote.fetch(
&["+refs/heads/*:refs/remotes/origin/*"],
Some(&mut fetch_opts),
None,
);
let fetch_result = temp_remote.fetch(&[&refspec], Some(&mut fetch_opts), None);
// Clean up the temporary remote
let _ = repo.remote_delete(temp_remote_name);

View File

@@ -4,7 +4,7 @@ use std::{
sync::{Arc, Mutex},
};
use git2::{BranchType, Error as GitError, Repository, WorktreeAddOptions};
use git2::{Error as GitError, Repository, WorktreeAddOptions};
use thiserror::Error;
use tracing::{debug, info, warn};
use utils::{is_wsl2, shell::get_shell_command};
@@ -43,43 +43,24 @@ impl WorktreeManager {
repo_path: &Path,
branch_name: &str,
worktree_path: &Path,
base_branch: Option<&str>,
base_branch: &str,
create_branch: bool,
) -> Result<(), WorktreeError> {
if create_branch {
let repo_path_owned = repo_path.to_path_buf();
let branch_name_owned = branch_name.to_string();
let base_branch_owned = base_branch.map(|s| s.to_string());
let base_branch_owned = base_branch.to_string();
tokio::task::spawn_blocking(move || {
let repo = Repository::open(&repo_path_owned)?;
let base_reference = if let Some(base_branch) = base_branch_owned.as_deref() {
let branch = repo.find_branch(base_branch, BranchType::Local)?;
branch.into_reference()
} else {
// Handle new repositories without any commits
match repo.head() {
Ok(head_ref) => head_ref,
Err(e)
if e.class() == git2::ErrorClass::Reference
&& e.code() == git2::ErrorCode::UnbornBranch =>
{
// Repository has no commits yet, create an initial commit
GitService::new()
.create_initial_commit(&repo)
.map_err(|_| {
GitError::from_str("Failed to create initial commit")
})?;
repo.find_reference("refs/heads/main")?
}
Err(e) => return Err(e),
}
};
// Create branch
repo.branch(&branch_name_owned, &base_reference.peel_to_commit()?, false)?;
Ok::<(), GitError>(())
let base_branch_ref =
GitService::find_branch(&repo, &base_branch_owned)?.into_reference();
repo.branch(
&branch_name_owned,
&base_branch_ref.peel_to_commit()?,
false,
)?;
Ok::<(), GitServiceError>(())
})
.await
.map_err(|e| WorktreeError::TaskJoin(format!("Task join error: {e}")))??;
@@ -324,10 +305,7 @@ impl WorktreeManager {
let repo = Repository::open(&git_repo_path).map_err(WorktreeError::Git)?;
// Find the branch reference using the branch name
let branch_ref = repo
.find_branch(&branch_name, git2::BranchType::Local)
.map_err(WorktreeError::Git)?
.into_reference();
let branch_ref = GitService::find_branch(&repo, &branch_name)?.into_reference();
// Create worktree options
let mut worktree_opts = WorktreeAddOptions::new();

View File

@@ -181,14 +181,12 @@ function CreatePrDialog({
<SelectValue placeholder="Select base branch" />
</SelectTrigger>
<SelectContent>
{branches
.filter((branch) => !branch.is_remote) // Only show local branches
.map((branch) => (
<SelectItem key={branch.name} value={branch.name}>
{branch.name}
{branch.is_current && ' (current)'}
</SelectItem>
))}
{branches.map((branch) => (
<SelectItem key={branch.name} value={branch.name}>
{branch.name}
{branch.is_current && ' (current)'}
</SelectItem>
))}
{/* Add common branches as fallback if not in the list */}
{!branches.some((b) => b.name === 'main' && !b.is_remote) && (
<SelectItem value="main">main</SelectItem>

View File

@@ -637,6 +637,7 @@ function CurrentAttempt({
(mergeInfo.hasOpenPR &&
branchStatus.remote_commits_ahead === 0) ||
((branchStatus.commits_ahead ?? 0) === 0 &&
(branchStatus.remote_commits_ahead ?? 0) === 0 &&
!pushSuccess &&
!mergeSuccess)
}