diff --git a/app/src/lib/branch/BranchIcon.svelte b/app/src/lib/branch/BranchIcon.svelte index fbc1ffd07..1681de135 100644 --- a/app/src/lib/branch/BranchIcon.svelte +++ b/app/src/lib/branch/BranchIcon.svelte @@ -1,12 +1,19 @@ + +{#if remoteBranch || localBranch} +
+
+ +
+ + {#if pr} +
+
{pr.title}
+ {#if pr.body} +
+ {@html marked.parse(pr.body, { renderer })} +
+ {/if} +
+ {/if} +
+ {#if remoteCommits} + {#each remoteCommits as commit, index (commit.id)} + + {#snippet lines(topHeightPx)} + + {/snippet} + + {/each} + {/if} + {#if localCommits} + {#each localCommits as commit, index (commit.id)} + + {#snippet lines(topHeightPx)} + + {/snippet} + + {/each} + {/if} + {#if localAndRemoteCommits} + {#each localAndRemoteCommits as commit, index (commit.id)} + + {#snippet lines(topHeightPx)} + + {/snippet} + + {/each} + {/if} +
+
+
+ { + laneWidth = e.detail / (16 * $userSettings.zoom); + lscache.set(laneWidthKey, laneWidth, 7 * 1440); // 7 day ttl + }} + /> +
+
+ {#await $selectedFile then selected} + {#if selected} + { + fileIdSelection.clear(); + }} + /> + {/if} + {/await} +
+
+{:else} +

No local or remote branch found

+{/if} + + diff --git a/app/src/lib/components/RemoteBranchPreview.svelte b/app/src/lib/components/RemoteBranchPreview.svelte deleted file mode 100644 index 6e0a6f04b..000000000 --- a/app/src/lib/components/RemoteBranchPreview.svelte +++ /dev/null @@ -1,141 +0,0 @@ - - -
-
- -
- - {#if pr} -
-
{pr.title}
- {#if pr.body} -
- {@html marked.parse(pr.body, { renderer })} -
- {/if} -
- {/if} - {#await remoteBranchService.getRemoteBranchData(branch.name) then branchData} - {#if branchData.commits && branchData.commits.length > 0} -
- {#each branchData.commits as commit, index (commit.id)} - - {/each} -
- {/if} - {/await} -
-
- { - laneWidth = e.detail / (16 * $userSettings.zoom); - lscache.set(laneWidthKey, laneWidth, 7 * 1440); // 7 day ttl - }} - /> -
-
- {#await $selectedFile then selected} - {#if selected} - { - fileIdSelection.clear(); - }} - /> - {/if} - {/await} -
-
- - diff --git a/app/src/lib/navigation/BranchItem.svelte b/app/src/lib/navigation/BranchItem.svelte index ed2925bf9..ca75eb113 100644 --- a/app/src/lib/navigation/BranchItem.svelte +++ b/app/src/lib/navigation/BranchItem.svelte @@ -10,7 +10,10 @@ function getBranchLink(b: CombinedBranch): string | undefined { if (b.vbranch) return `/${projectId}/board/`; - if (b.remoteBranch) return `/${projectId}/remote/${branch?.remoteBranch?.displayName}`; + // Here we specifically want to prefer looking at the remote branch as + // the there may be multiple remotes that share the same local branch. + if (b.branch) + return `/${projectId}/branch/${branch?.remoteBranch?.name || branch?.localBranch?.name}`; if (b.pr) return `/${projectId}/pull/${b.pr.number}`; } diff --git a/app/src/lib/navigation/Branches.svelte b/app/src/lib/navigation/Branches.svelte index 450a21559..4156ad203 100644 --- a/app/src/lib/navigation/Branches.svelte +++ b/app/src/lib/navigation/Branches.svelte @@ -67,7 +67,7 @@ if (b.pr) return false; } - if (params.includeRemote && b.remoteBranch) return true; + if (params.includeRemote && b.branch) return true; return false; }); } diff --git a/app/src/lib/vbranches/branchController.ts b/app/src/lib/vbranches/branchController.ts index 3a6c65492..118d15873 100644 --- a/app/src/lib/vbranches/branchController.ts +++ b/app/src/lib/vbranches/branchController.ts @@ -276,11 +276,18 @@ You can find them in the 'Branches' sidebar in order to resolve conflicts.`; } } - async createvBranchFromBranch(branch: string) { + /** + * + * @param branch The branch you want to create a virtual branch for. If you + * have a local branch, this should be the branch. + * @param remote Optionally sets another branch as the upstream. + */ + async createvBranchFromBranch(branch: string, remote: string | undefined = undefined) { try { await invoke('create_virtual_branch_from_branch', { projectId: this.projectId, - branch + branch, + remote }); } catch (err) { showError('Failed to create virtual branch', err); diff --git a/app/src/lib/vbranches/types.ts b/app/src/lib/vbranches/types.ts index f3ecfa69f..cf39f0a5a 100644 --- a/app/src/lib/vbranches/types.ts +++ b/app/src/lib/vbranches/types.ts @@ -315,14 +315,11 @@ export class Branch { lastCommitTimestampMs?: number | undefined; lastCommitAuthor?: string | undefined; givenName!: string; + isRemote!: boolean; get displayName(): string { return this.name.replace('refs/remotes/', '').replace('refs/heads/', ''); } - - get isRemote() { - return !!this.upstream; - } } export class BranchData { diff --git a/app/src/routes/[projectId]/+layout.svelte b/app/src/routes/[projectId]/+layout.svelte index 6c3eba5ad..7946a3ecb 100644 --- a/app/src/routes/[projectId]/+layout.svelte +++ b/app/src/routes/[projectId]/+layout.svelte @@ -4,6 +4,7 @@ import { BaseBranch, NoDefaultTarget } from '$lib/baseBranch/baseBranch'; import { BaseBranchService } from '$lib/baseBranch/baseBranchService'; import { BranchDragActionsFactory } from '$lib/branches/dragActions'; + import { getNameNormalizationServiceContext } from '$lib/branches/nameNormalizationService'; import { BranchService, createBranchServiceStore } from '$lib/branches/service'; import { CommitDragActionsFactory } from '$lib/commits/dragActions'; import NoBaseBranch from '$lib/components/NoBaseBranch.svelte'; @@ -33,6 +34,8 @@ const { data, children }: { data: LayoutData; children: Snippet } = $props(); + const nameNormalizationService = getNameNormalizationServiceContext(); + const { vbranchService, project, @@ -104,7 +107,14 @@ listServiceStore.set(ghListService); githubRepoServiceStore.set(gitHost); - branchServiceStore.set(new BranchService(vbranchService, remoteBranchService, ghListService)); + branchServiceStore.set( + new BranchService( + vbranchService, + remoteBranchService, + ghListService, + nameNormalizationService + ) + ); }); // Once on load and every time the project id changes diff --git a/app/src/routes/[projectId]/remote/[...name]/+page.svelte b/app/src/routes/[projectId]/branch/[...name]/+page.svelte similarity index 60% rename from app/src/routes/[projectId]/remote/[...name]/+page.svelte rename to app/src/routes/[projectId]/branch/[...name]/+page.svelte index c3d113baf..71db1a810 100644 --- a/app/src/routes/[projectId]/remote/[...name]/+page.svelte +++ b/app/src/routes/[projectId]/branch/[...name]/+page.svelte @@ -4,15 +4,18 @@ // - And it does NOT have a cooresponding vbranch // It may also display details about a cooresponding pr if they exist import { getBranchServiceStore } from '$lib/branches/service'; + import RemoteBranchPreview from '$lib/components/BranchPreview.svelte'; import FullviewLoading from '$lib/components/FullviewLoading.svelte'; - import RemoteBranchPreview from '$lib/components/RemoteBranchPreview.svelte'; import { page } from '$app/stores'; const branchService = getBranchServiceStore(); const branches = $derived($branchService?.branches); const error = $derived($branchService?.error); + // Search for remote branch first as there may be multiple combined branches + // which have the same local branch const branch = $derived( - $branches?.find((cb) => cb.remoteBranch?.displayName === $page.params.name) + $branches?.find((cb) => cb.remoteBranch?.name === $page.params.name) || + $branches?.find((cb) => cb.localBranch?.name === $page.params.name) ); // $: branch = $branches?.find((b) => b.displayName === $page.params.name); @@ -21,8 +24,12 @@

Error...

{:else if !$branches} -{:else if branch?.remoteBranch} - +{:else if branch?.remoteBranch || branch?.localBranch} + {:else}

Branch doesn't seem to exist

{/if} diff --git a/crates/gitbutler-branch-actions/src/actions.rs b/crates/gitbutler-branch-actions/src/actions.rs index ff1e0bc25..9669e7954 100644 --- a/crates/gitbutler-branch-actions/src/actions.rs +++ b/crates/gitbutler-branch-actions/src/actions.rs @@ -470,12 +470,13 @@ impl VirtualBranchActions { &self, project: &Project, branch: &Refname, + remote: Option, ) -> Result { let project_repository = open_with_verify(project)?; let branch_manager = project_repository.branch_manager(); let mut guard = project.exclusive_worktree_access(); branch_manager - .create_virtual_branch_from_branch(branch, guard.write_permission()) + .create_virtual_branch_from_branch(branch, remote, guard.write_permission()) .map_err(Into::into) } } diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs index e8e27495e..7e68cd82d 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs @@ -14,7 +14,7 @@ use gitbutler_commit::commit_headers::HasCommitHeaders; use gitbutler_error::error::Marker; use gitbutler_oplog::SnapshotExt; use gitbutler_project::access::WorktreeWritePermission; -use gitbutler_reference::Refname; +use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_repo::{rebase::cherry_rebase, RepoActionsExt, RepositoryExt}; use gitbutler_time::time::now_since_unix_epoch_ms; use std::borrow::Cow; @@ -36,7 +36,7 @@ impl BranchManager<'_> { let tree = commit .tree() - .context("failed to find defaut target commit tree")?; + .context("failed to find default target commit tree")?; let mut all_virtual_branches = vb_state .list_branches_in_workspace() @@ -126,20 +126,26 @@ impl BranchManager<'_> { pub fn create_virtual_branch_from_branch( &self, - upstream: &Refname, + target: &Refname, + upstream_branch: Option, perm: &mut WorktreeWritePermission, ) -> Result { // only set upstream if it's not the default target - let upstream_branch = match upstream { - Refname::Other(_) | Refname::Virtual(_) => { - // we only support local or remote branches - bail!("branch {upstream} must be a local or remote branch"); + let upstream_branch = match upstream_branch { + Some(upstream_branch) => Some(upstream_branch), + None => { + match target { + Refname::Other(_) | Refname::Virtual(_) => { + // we only support local or remote branches + bail!("branch {target} must be a local or remote branch"); + } + Refname::Remote(remote) => Some(remote.clone()), + Refname::Local(local) => local.remote().cloned(), + } } - Refname::Remote(remote) => Some(remote.clone()), - Refname::Local(local) => local.remote().cloned(), }; - let branch_name = upstream + let branch_name = target .branch() .expect("always a branch reference") .to_string(); @@ -153,21 +159,21 @@ impl BranchManager<'_> { let default_target = vb_state.get_default_target()?; - if let Refname::Remote(remote_upstream) = upstream { + if let Refname::Remote(remote_upstream) = target { if default_target.branch == *remote_upstream { bail!("cannot create a branch from default target") } } let repo = self.project_repository.repo(); - let head_reference = - repo.find_reference(&upstream.to_string()) - .map_err(|err| match err { - err if err.code() == git2::ErrorCode::NotFound => { - anyhow!("branch {upstream} was not found") - } - err => err.into(), - })?; + let head_reference = repo + .find_reference(&target.to_string()) + .map_err(|err| match err { + err if err.code() == git2::ErrorCode::NotFound => { + anyhow!("branch {target} was not found") + } + err => err.into(), + })?; let head_commit = head_reference .peel_to_commit() .context("failed to peel to commit")?; @@ -220,7 +226,7 @@ impl BranchManager<'_> { ); let branch = if let Ok(Some(mut branch)) = - vb_state.find_by_source_refname_where_not_in_workspace(upstream) + vb_state.find_by_source_refname_where_not_in_workspace(target) { branch.upstream_head = upstream_branch.is_some().then_some(head_commit.id()); branch.upstream = upstream_branch; @@ -239,7 +245,7 @@ impl BranchManager<'_> { id: BranchId::generate(), name: branch_name.clone(), notes: String::new(), - source_refname: Some(upstream.clone()), + source_refname: Some(target.clone()), upstream_head: upstream_branch.is_some().then_some(head_commit.id()), upstream: upstream_branch, tree: head_commit_tree.id(), diff --git a/crates/gitbutler-branch-actions/src/remote.rs b/crates/gitbutler-branch-actions/src/remote.rs index 23ab226a1..924a10c3d 100644 --- a/crates/gitbutler-branch-actions/src/remote.rs +++ b/crates/gitbutler-branch-actions/src/remote.rs @@ -30,6 +30,7 @@ pub struct RemoteBranch { pub given_name: String, pub last_commit_timestamp_ms: Option, pub last_commit_author: Option, + pub is_remote: bool, } #[derive(Debug, Clone, Serialize, PartialEq)] @@ -139,6 +140,7 @@ pub(crate) fn branch_to_remote_branch( .map(|t: u128| t * 1000) .ok(), last_commit_author: commit.author().name().map(std::string::ToString::to_string), + is_remote: branch.get().is_remote(), }) } diff --git a/crates/gitbutler-branch-actions/tests/extra/mod.rs b/crates/gitbutler-branch-actions/tests/extra/mod.rs index cc4d80515..5e9ac6a8c 100644 --- a/crates/gitbutler-branch-actions/tests/extra/mod.rs +++ b/crates/gitbutler-branch-actions/tests/extra/mod.rs @@ -1177,6 +1177,7 @@ fn unapply_branch() -> Result<()> { let branch_manager = project_repository.branch_manager(); let branch1_id = branch_manager.create_virtual_branch_from_branch( &Refname::from_str(&real_branch)?, + None, guard.write_permission(), )?; let contents = std::fs::read(Path::new(&project.path).join(file_path))?; @@ -1271,6 +1272,7 @@ fn apply_unapply_added_deleted_files() -> Result<()> { branch_manager .create_virtual_branch_from_branch( &Refname::from_str(&real_branch_2).unwrap(), + None, guard.write_permission(), ) .unwrap(); @@ -1281,6 +1283,7 @@ fn apply_unapply_added_deleted_files() -> Result<()> { branch_manager .create_virtual_branch_from_branch( &Refname::from_str(&real_branch_3).unwrap(), + None, guard.write_permission(), ) .unwrap(); diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/apply_virtual_branch.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/apply_virtual_branch.rs index 50dff5c3d..2c2dec127 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/apply_virtual_branch.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/apply_virtual_branch.rs @@ -94,7 +94,7 @@ async fn rebase_commit() { { // apply first vbranch again branch1_id = controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); @@ -192,7 +192,7 @@ async fn rebase_work() { { // apply first vbranch again branch1_id = controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/convert_to_real_branch.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/convert_to_real_branch.rs index 1b7988a92..fcc8f596c 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/convert_to_real_branch.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/convert_to_real_branch.rs @@ -92,7 +92,7 @@ async fn conflicting() { let branch_id = { // apply branch, it should conflict let branch_id = controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/create_virtual_branch_from_branch.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/create_virtual_branch_from_branch.rs index ac2fb9f4c..ea9039e1e 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/create_virtual_branch_from_branch.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/create_virtual_branch_from_branch.rs @@ -56,7 +56,7 @@ async fn integration() { // checkout a existing remote branch let branch_id = controller - .create_virtual_branch_from_branch(project, &branch_name) + .create_virtual_branch_from_branch(project, &branch_name, None) .await .unwrap(); @@ -151,7 +151,11 @@ async fn no_conflicts() { assert!(branches.is_empty()); let branch_id = controller - .create_virtual_branch_from_branch(project, &"refs/remotes/origin/branch".parse().unwrap()) + .create_virtual_branch_from_branch( + project, + &"refs/remotes/origin/branch".parse().unwrap(), + None, + ) .await .unwrap(); @@ -197,7 +201,11 @@ async fn conflicts_with_uncommited() { // branch should be created unapplied, because of the conflict let new_branch_id = controller - .create_virtual_branch_from_branch(project, &"refs/remotes/origin/branch".parse().unwrap()) + .create_virtual_branch_from_branch( + project, + &"refs/remotes/origin/branch".parse().unwrap(), + None, + ) .await .unwrap(); let new_branch = controller @@ -253,7 +261,11 @@ async fn conflicts_with_commited() { // branch should be created unapplied, because of the conflict let new_branch_id = controller - .create_virtual_branch_from_branch(project, &"refs/remotes/origin/branch".parse().unwrap()) + .create_virtual_branch_from_branch( + project, + &"refs/remotes/origin/branch".parse().unwrap(), + None, + ) .await .unwrap(); let new_branch = controller @@ -289,6 +301,7 @@ async fn from_default_target() { .create_virtual_branch_from_branch( project, &"refs/remotes/origin/master".parse().unwrap(), + None ) .await .unwrap_err() @@ -317,6 +330,7 @@ async fn from_non_existent_branch() { .create_virtual_branch_from_branch( project, &"refs/remotes/origin/branch".parse().unwrap(), + None ) .await .unwrap_err() @@ -355,7 +369,11 @@ async fn from_state_remote_branch() { .unwrap(); let branch_id = controller - .create_virtual_branch_from_branch(project, &"refs/remotes/origin/branch".parse().unwrap()) + .create_virtual_branch_from_branch( + project, + &"refs/remotes/origin/branch".parse().unwrap(), + None, + ) .await .unwrap(); diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/mod.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/mod.rs index e624f2886..cc98f1157 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/mod.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/mod.rs @@ -136,7 +136,7 @@ async fn resolve_conflict_flow() { let branch1_id = { // when we apply conflicted branch, it has conflict let branch1_id = controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/selected_for_changes.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/selected_for_changes.rs index 15e7d9391..7f0db5f03 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/selected_for_changes.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/selected_for_changes.rs @@ -376,7 +376,7 @@ async fn applying_first_branch() { .unwrap(); let unapplied_branch = Refname::from_str(&unapplied_branch).unwrap(); controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); diff --git a/crates/gitbutler-branch-actions/tests/virtual_branches/update_base_branch.rs b/crates/gitbutler-branch-actions/tests/virtual_branches/update_base_branch.rs index 3e77620e2..0c958fb39 100644 --- a/crates/gitbutler-branch-actions/tests/virtual_branches/update_base_branch.rs +++ b/crates/gitbutler-branch-actions/tests/virtual_branches/update_base_branch.rs @@ -54,7 +54,7 @@ mod applied_branch { { // applying the branch should produce conflict markers controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); let (branches, _) = controller.list_virtual_branches(project).await.unwrap(); @@ -124,7 +124,7 @@ mod applied_branch { { // applying the branch should produce conflict markers controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); let (branches, _) = controller.list_virtual_branches(project).await.unwrap(); @@ -200,7 +200,7 @@ mod applied_branch { { // applying the branch should produce conflict markers controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); let (branches, _) = controller.list_virtual_branches(project).await.unwrap(); @@ -273,7 +273,7 @@ mod applied_branch { { // applying the branch should produce conflict markers controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); let (branches, _) = controller.list_virtual_branches(project).await.unwrap(); @@ -346,7 +346,7 @@ mod applied_branch { { // applying the branch should produce conflict markers controller - .create_virtual_branch_from_branch(project, &unapplied_branch) + .create_virtual_branch_from_branch(project, &unapplied_branch, None) .await .unwrap(); let (branches, _) = controller.list_virtual_branches(project).await.unwrap(); @@ -767,6 +767,7 @@ mod applied_branch { .create_virtual_branch_from_branch( project, &Refname::from_str(unapplied_refname.as_str()).unwrap(), + None, ) .await .unwrap(); diff --git a/crates/gitbutler-tauri/src/virtual_branches.rs b/crates/gitbutler-tauri/src/virtual_branches.rs index 60009bb0c..9ff49a462 100644 --- a/crates/gitbutler-tauri/src/virtual_branches.rs +++ b/crates/gitbutler-tauri/src/virtual_branches.rs @@ -83,10 +83,11 @@ pub mod commands { projects: State<'_, projects::Controller>, project_id: ProjectId, branch: Refname, + remote: Option, ) -> Result { let project = projects.get(project_id)?; let branch_id = VirtualBranchActions - .create_virtual_branch_from_branch(&project, &branch) + .create_virtual_branch_from_branch(&project, &branch, remote) .await?; emit_vbranches(&windows, project_id).await; Ok(branch_id)