diff --git a/gitbutler-app/src/bin.rs b/gitbutler-app/src/bin.rs index 860fd0e65..38e597b16 100644 --- a/gitbutler-app/src/bin.rs +++ b/gitbutler-app/src/bin.rs @@ -214,6 +214,7 @@ fn main() { virtual_branches::commands::cherry_pick_onto_virtual_branch, virtual_branches::commands::amend_virtual_branch, virtual_branches::commands::list_remote_branches, + virtual_branches::commands::get_remote_branch_data, virtual_branches::commands::squash_branch_commit, virtual_branches::commands::fetch_from_target, menu::menu_item_set_enabled, diff --git a/gitbutler-app/src/virtual_branches/commands.rs b/gitbutler-app/src/virtual_branches/commands.rs index 9054e80ce..2284b7981 100644 --- a/gitbutler-app/src/virtual_branches/commands.rs +++ b/gitbutler-app/src/virtual_branches/commands.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use crate::watcher; use anyhow::Context; use tauri::{AppHandle, Manager}; @@ -519,6 +521,28 @@ pub async fn list_remote_branches( Ok(branches) } +#[tauri::command(async)] +#[instrument(skip(handle))] +pub async fn get_remote_branch_data( + handle: tauri::AppHandle, + project_id: &str, + refname: &str, +) -> Result { + let project_id = project_id.parse().map_err(|_| Error::UserError { + code: Code::Validation, + message: "Malformed project id".to_string(), + })?; + let refname = git::Refname::from_str(refname).map_err(|_| Error::UserError { + code: Code::Validation, + message: "Malformed refname".to_string(), + })?; + let branch_data = handle + .state::() + .get_remote_branch_data(&project_id, &refname) + .await?; + Ok(branch_data) +} + #[tauri::command(async)] #[instrument(skip(handle))] pub async fn squash_branch_commit( diff --git a/gitbutler-app/src/virtual_branches/controller.rs b/gitbutler-app/src/virtual_branches/controller.rs index 76cdf8e70..f923569c3 100644 --- a/gitbutler-app/src/virtual_branches/controller.rs +++ b/gitbutler-app/src/virtual_branches/controller.rs @@ -14,8 +14,8 @@ use crate::{ use super::{ branch::{BranchId, Ownership}, errors::{ - self, FetchFromTargetError, GetBaseBranchDataError, IsRemoteBranchMergableError, - ListRemoteBranchesError, + self, FetchFromTargetError, GetBaseBranchDataError, GetRemoteBranchDataError, + IsRemoteBranchMergableError, ListRemoteBranchesError, }, target_to_base_branch, BaseBranch, RemoteBranchFile, }; @@ -313,6 +313,16 @@ impl Controller { .list_remote_branches(project_id) } + pub async fn get_remote_branch_data( + &self, + project_id: &ProjectId, + refname: &git::Refname, + ) -> Result> { + self.inner(project_id) + .await + .get_remote_branch_data(project_id, refname) + } + pub async fn squash( &self, project_id: &ProjectId, @@ -808,6 +818,25 @@ impl ControllerInner { .map_err(ControllerError::Action) } + pub fn get_remote_branch_data( + &self, + project_id: &ProjectId, + refname: &git::Refname, + ) -> Result> { + let project = self.projects.get(project_id).map_err(Error::from)?; + let project_repository = + project_repository::Repository::open(&project).map_err(Error::from)?; + let user = self.users.get_user().map_err(Error::from)?; + let gb_repository = gb_repository::Repository::open( + &self.local_data_dir, + &project_repository, + user.as_ref(), + ) + .context("failed to open gitbutler repository")?; + super::get_branch_data(&gb_repository, &project_repository, refname) + .map_err(ControllerError::Action) + } + pub async fn squash( &self, project_id: &ProjectId, diff --git a/gitbutler-app/src/virtual_branches/errors.rs b/gitbutler-app/src/virtual_branches/errors.rs index 7045d7f1a..92c37eb7a 100644 --- a/gitbutler-app/src/virtual_branches/errors.rs +++ b/gitbutler-app/src/virtual_branches/errors.rs @@ -751,6 +751,26 @@ pub enum ListRemoteBranchesError { Other(#[from] anyhow::Error), } +#[derive(Debug, thiserror::Error)] +pub enum GetRemoteBranchDataError { + #[error("default target not set")] + DefaultTargetNotSet(DefaultTargetNotSetError), + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl From for Error { + fn from(value: GetRemoteBranchDataError) -> Self { + match value { + GetRemoteBranchDataError::DefaultTargetNotSet(error) => error.into(), + GetRemoteBranchDataError::Other(error) => { + tracing::error!(?error, "get remote branch data error"); + Error::Unknown + } + } + } +} + impl From for Error { fn from(value: ListRemoteBranchesError) -> Self { match value { diff --git a/gitbutler-app/src/virtual_branches/remote.rs b/gitbutler-app/src/virtual_branches/remote.rs index 8831c8bb0..da5781f85 100644 --- a/gitbutler-app/src/virtual_branches/remote.rs +++ b/gitbutler-app/src/virtual_branches/remote.rs @@ -27,6 +27,16 @@ pub struct RemoteBranch { pub commits: Vec, } +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RemoteBranchData { + pub sha: git::Oid, + pub name: git::Refname, + pub upstream: Option, + pub behind: u32, + pub commits: Vec, +} + #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct RemoteCommit { @@ -66,6 +76,43 @@ pub fn list_remote_branches( Ok(remote_branches) } +pub fn get_branch_data( + gb_repository: &gb_repository::Repository, + project_repository: &project_repository::Repository, + refname: &git::Refname, +) -> Result { + let default_target = gb_repository + .default_target() + .context("failed to get default target")? + .ok_or_else(|| { + errors::GetRemoteBranchDataError::DefaultTargetNotSet( + errors::DefaultTargetNotSetError { + project_id: project_repository.project().id, + }, + ) + })?; + + let branch = project_repository + .git_repository + .find_branch(refname) + .context(format!("failed to find branch with refname {refname}"))?; + + let branch_data = branch_to_remote_branch(project_repository, &branch, default_target.sha) + .context("failed to get branch data")?; + + branch_data + .ok_or_else(|| { + errors::GetRemoteBranchDataError::Other(anyhow::anyhow!("no data found for branch")) + }) + .map(|branch_data| RemoteBranchData { + sha: branch_data.sha, + name: branch_data.name, + upstream: branch_data.upstream, + behind: branch_data.behind, + commits: branch_data.commits, + }) +} + pub fn branch_to_remote_branch( project_repository: &project_repository::Repository, branch: &git::Branch, diff --git a/gitbutler-ui/src/lib/components/RemoteBranchPreview.svelte b/gitbutler-ui/src/lib/components/RemoteBranchPreview.svelte index e857204f0..1c958a691 100644 --- a/gitbutler-ui/src/lib/components/RemoteBranchPreview.svelte +++ b/gitbutler-ui/src/lib/components/RemoteBranchPreview.svelte @@ -5,6 +5,7 @@ import ScrollableContainer from './ScrollableContainer.svelte'; import CommitCard from '$lib/components/CommitCard.svelte'; import { type SettingsStore, SETTINGS_CONTEXT } from '$lib/settings/userSettings'; + import { getRemoteBranchData } from '$lib/stores/remoteBranches'; import { Ownership } from '$lib/vbranches/ownership'; import lscache from 'lscache'; import { marked } from 'marked'; @@ -59,18 +60,20 @@ {/if} - {#if branch.commits && branch.commits.length > 0} -
- {#each branch.commits as commit (commit.id)} - - {/each} -
- {/if} + {#await getRemoteBranchData({ projectId, refname: branch.name }) then branchData} + {#if branchData.commits && branchData.commits.length > 0} +
+ {#each branchData.commits as commit (commit.id)} + + {/each} +
+ {/if} + {/await} ) { this.branches$ = combineLatest([baseBranch$, this.reload$, head$, fetches$]).pipe( - switchMap(() => getRemoteBranchesData({ projectId })), + switchMap(() => listRemoteBranches({ projectId })), map((branches) => branches.filter((b) => b.ahead != 0)), shareReplay(1), catchError((e) => { @@ -42,9 +42,7 @@ export class RemoteBranchService { } } -export async function getRemoteBranchesData(params: { - projectId: string; -}): Promise { +async function listRemoteBranches(params: { projectId: string }): Promise { const branches = plainToInstance( RemoteBranch, await invoke('list_remote_branches', params) @@ -52,3 +50,15 @@ export async function getRemoteBranchesData(params: { return branches; } + +export async function getRemoteBranchData(params: { + projectId: string; + refname: string; +}): Promise { + const branchData = plainToInstance( + RemoteBranchData, + await invoke('get_remote_branch_data', params) + ); + + return branchData; +} diff --git a/gitbutler-ui/src/lib/vbranches/types.ts b/gitbutler-ui/src/lib/vbranches/types.ts index e31b9fd35..3c0e0dde6 100644 --- a/gitbutler-ui/src/lib/vbranches/types.ts +++ b/gitbutler-ui/src/lib/vbranches/types.ts @@ -216,6 +216,40 @@ export class RemoteBranch { } } +export class RemoteBranchData { + sha!: string; + name!: string; + upstream?: string; + behind!: number; + @Type(() => RemoteCommit) + commits!: RemoteCommit[]; + isMergeable!: boolean | undefined; + + get ahead(): number { + return this.commits.length; + } + + get lastCommitTs(): Date | undefined { + return this.commits[0]?.createdAt; + } + + get firstCommitAt(): Date { + return this.commits[this.commits.length - 1].createdAt; + } + + get authors(): Author[] { + const allAuthors = this.commits.map((commit) => commit.author); + const uniqueAuthors = allAuthors.filter( + (author, index) => allAuthors.findIndex((a) => a.email == author.email) == index + ); + return uniqueAuthors; + } + + get displayName(): string { + return this.name.replace('refs/remotes/', '').replace('origin/', '').replace('refs/heads/', ''); + } +} + export class BaseBranch { branchName!: string; remoteName!: string;