Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions apps/staged/src-tauri/src/branches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,10 +809,22 @@ pub async fn setup_worktree(
)
.map_err(|e| e.to_string())?
} else {
// If the branch exists on the remote (e.g. from an existing PR),
// start the new local branch from the remote tracking ref so it
// includes the PR's commits. Otherwise fall back to base_branch
// for genuinely new branches.
let remote_ref = format!("origin/{}", branch.branch_name);
let start_point = if git::remote_branch_exists(&repo_path, &branch.branch_name)
.map_err(|e| e.to_string())?
{
&remote_ref
} else {
&branch.base_branch
};
match create_worktree_with_fallback(
&repo_path,
&branch.branch_name,
&branch.base_branch,
start_point,
&desired_worktree_path,
) {
Ok(path) => path,
Expand Down Expand Up @@ -1530,10 +1542,22 @@ pub(crate) fn setup_worktree_sync(store: &Arc<Store>, branch_id: &str) -> Result
)
.map_err(|e| e.to_string())?
} else {
// If the branch exists on the remote (e.g. from an existing PR),
// start the new local branch from the remote tracking ref so it
// includes the PR's commits. Otherwise fall back to base_branch
// for genuinely new branches.
let remote_ref = format!("origin/{}", branch.branch_name);
let start_point = if crate::git::remote_branch_exists(&repo_path, &branch.branch_name)
.map_err(|e| e.to_string())?
{
&remote_ref
} else {
&branch.base_branch

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use PR ref when remote branch is absent

For PR-backed branches whose head is not on origin (notably fork PRs), remote_branch_exists is false and this path falls back to branch.base_branch, so the created worktree starts from base instead of the selected PR commits. Since project creation now stores pr_number, this produces projects that claim to target a PR but are checked out to unrelated code in this scenario.

Useful? React with 👍 / 👎.

};
match crate::git::create_worktree_at_path(
&repo_path,
&branch.branch_name,
&branch.base_branch,
start_point,
&desired_worktree_path,
) {
Ok(path) => path,
Expand Down
4 changes: 2 additions & 2 deletions apps/staged/src-tauri/src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ pub use worktree::{
create_worktree_for_existing_branch_at_path, create_worktree_from_pr,
create_worktree_from_pr_at_path, get_commits_since_base, get_full_commit_log, get_head_sha,
get_parent_commit, has_unpushed_commits, list_worktrees, project_worktree_path_for,
project_worktree_root_for, remove_worktree, reset_to_commit, switch_branch,
update_branch_from_pr, worktree_path_for, CommitInfo, UpdateFromPrResult,
project_worktree_root_for, remote_branch_exists, remove_worktree, reset_to_commit,
switch_branch, update_branch_from_pr, worktree_path_for, CommitInfo, UpdateFromPrResult,
};
7 changes: 7 additions & 0 deletions apps/staged/src-tauri/src/git/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,13 @@ pub fn branch_exists(repo: &Path, branch_name: &str) -> Result<bool, GitError> {
Ok(result.is_ok())
}

/// Check whether a remote tracking branch (`origin/<branch_name>`) exists.
pub fn remote_branch_exists(repo: &Path, branch_name: &str) -> Result<bool, GitError> {
let ref_name = format!("refs/remotes/origin/{branch_name}");
let result = cli::run(repo, &["rev-parse", "--verify", &ref_name]);
Ok(result.is_ok())
}

/// Reset HEAD to a specific commit (hard reset).
/// This discards all commits after the specified commit.
pub fn reset_to_commit(worktree: &Path, commit_sha: &str) -> Result<(), GitError> {
Expand Down
23 changes: 20 additions & 3 deletions apps/staged/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ fn list_projects(
.map_err(|e| e.to_string())
}

#[allow(clippy::too_many_arguments)]
#[tauri::command(rename_all = "camelCase")]
fn create_project(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
Expand All @@ -226,6 +227,8 @@ fn create_project(
github_repo: Option<String>,
location: Option<String>,
subpath: Option<String>,
branch_name: Option<String>,
pr_number: Option<u64>,
) -> Result<store::Project, String> {
let store = get_store(&store)?;
let trimmed = name.trim();
Expand All @@ -243,7 +246,12 @@ fn create_project(
Some("remote") => store::ProjectLocation::Remote,
_ => store::ProjectLocation::Local,
};
let inferred_branch_name = branches::infer_branch_name(trimmed);
let inferred_branch_name = branch_name
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned)
.unwrap_or_else(|| branches::infer_branch_name(trimmed));
let mut project = store::Project::named(trimmed);
project.location = project_location;
if let Some(repo) = github_repo.clone() {
Expand Down Expand Up @@ -292,21 +300,27 @@ fn create_project(

let branch_id = match project.location {
store::ProjectLocation::Local => {
let branch =
let mut branch =
store::Branch::new(&project.id, &inferred_branch_name, &effective_base)
.with_project_repo(&project_repo.id);
if let Some(pr) = pr_number {
branch = branch.with_pr(pr);
}
store.create_branch(&branch).map_err(|e| e.to_string())?;
Some(branch.id)
}
store::ProjectLocation::Remote => {
let workspace_name = branches::infer_workspace_name(&inferred_branch_name);
let branch = store::Branch::new_remote(
let mut branch = store::Branch::new_remote(
&project.id,
&inferred_branch_name,
&effective_base,
&workspace_name,
)
.with_project_repo(&project_repo.id);
if let Some(pr) = pr_number {
branch = branch.with_pr(pr);
}
store.create_branch(&branch).map_err(|e| e.to_string())?;
log::info!(
"[create_project] created remote branch={} workspace={} status=starting project={}",
Expand Down Expand Up @@ -401,6 +415,7 @@ fn list_recent_repos(
.map_err(|e| e.to_string())
}

#[allow(clippy::too_many_arguments)]
#[tauri::command(rename_all = "camelCase")]
async fn add_project_repo(
store: tauri::State<'_, Mutex<Option<Arc<Store>>>>,
Expand All @@ -410,6 +425,7 @@ async fn add_project_repo(
branch_name: Option<String>,
subpath: Option<String>,
set_as_primary: Option<bool>,
pr_number: Option<u64>,
) -> Result<store::ProjectRepo, String> {
let store = get_store(&store)?;
let repo = project_commands::add_project_repo_impl(
Expand All @@ -420,6 +436,7 @@ async fn add_project_repo(
subpath,
set_as_primary,
None,
pr_number,
)
.await?;

Expand Down
23 changes: 19 additions & 4 deletions apps/staged/src-tauri/src/project_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{blox, branches, git};
/// Core logic for adding a GitHub repository to a project.
///
/// Called by both the `add_project_repo` Tauri command and the MCP tool.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add_project_repo_impl(
store: Arc<Store>,
project_id: String,
Expand All @@ -16,6 +17,7 @@ pub(crate) async fn add_project_repo_impl(
subpath: Option<String>,
set_as_primary: Option<bool>,
reason: Option<String>,
pr_number: Option<u64>,
) -> Result<store::ProjectRepo, String> {
let project = store
.get_project(&project_id)
Expand Down Expand Up @@ -121,13 +123,26 @@ pub(crate) async fn add_project_repo_impl(
};
let branch = match project.location {
store::ProjectLocation::Local => {
store::Branch::new(&project_id, &repo.branch_name, &effective_base)
.with_project_repo(&repo.id)
let mut b = store::Branch::new(&project_id, &repo.branch_name, &effective_base)
.with_project_repo(&repo.id);
if let Some(pr) = pr_number {
b = b.with_pr(pr);
}
b
}
store::ProjectLocation::Remote => {
let ws_name = branches::resolve_project_workspace_name(&store, &project, None)?;
store::Branch::new_remote(&project_id, &repo.branch_name, &effective_base, &ws_name)
.with_project_repo(&repo.id)
let mut b = store::Branch::new_remote(
&project_id,
&repo.branch_name,
&effective_base,
&ws_name,
)
.with_project_repo(&repo.id);
if let Some(pr) = pr_number {
b = b.with_pr(pr);
}
b
}
};
store.create_branch(&branch).map_err(|e| e.to_string())?;
Expand Down
1 change: 1 addition & 0 deletions apps/staged/src-tauri/src/project_mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ impl ProjectToolsHandler {
p.subpath,
None,
p.reason,
None,
)
.await
{
Expand Down
17 changes: 14 additions & 3 deletions apps/staged/src/lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,18 @@ export function createProject(
name: string,
location: 'local' | 'remote',
githubRepo?: string,
subpath?: string
subpath?: string,
branchName?: string,
prNumber?: number
): Promise<Project> {
return invoke('create_project', { name, location, githubRepo: githubRepo ?? null, subpath });
return invoke('create_project', {
name,
location,
githubRepo: githubRepo ?? null,
subpath,
branchName: branchName ?? null,
prNumber: prNumber ?? null,
});
}

export function deleteProject(id: string): Promise<void> {
Expand All @@ -79,14 +88,16 @@ export function addProjectRepo(
githubRepo: string,
branchName?: string,
subpath?: string,
setAsPrimary?: boolean
setAsPrimary?: boolean,
prNumber?: number
): Promise<ProjectRepo> {
return invoke('add_project_repo', {
projectId,
githubRepo,
branchName: branchName ?? null,
subpath: subpath ?? null,
setAsPrimary: setAsPrimary ?? null,
prNumber: prNumber ?? null,
});
}

Expand Down
Loading