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:
@@ -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,
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user