Merged origin/master into Collapsable lane

This commit is contained in:
Pavel Laptev 2024-02-11 22:09:15 +01:00 committed by GitButler
commit 3427a3e8c5
8 changed files with 187 additions and 19 deletions

View File

@ -214,6 +214,7 @@ fn main() {
virtual_branches::commands::cherry_pick_onto_virtual_branch, virtual_branches::commands::cherry_pick_onto_virtual_branch,
virtual_branches::commands::amend_virtual_branch, virtual_branches::commands::amend_virtual_branch,
virtual_branches::commands::list_remote_branches, virtual_branches::commands::list_remote_branches,
virtual_branches::commands::get_remote_branch_data,
virtual_branches::commands::squash_branch_commit, virtual_branches::commands::squash_branch_commit,
virtual_branches::commands::fetch_from_target, virtual_branches::commands::fetch_from_target,
menu::menu_item_set_enabled, menu::menu_item_set_enabled,

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use crate::watcher; use crate::watcher;
use anyhow::Context; use anyhow::Context;
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
@ -519,6 +521,28 @@ pub async fn list_remote_branches(
Ok(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<super::RemoteBranchData, Error> {
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::<Controller>()
.get_remote_branch_data(&project_id, &refname)
.await?;
Ok(branch_data)
}
#[tauri::command(async)] #[tauri::command(async)]
#[instrument(skip(handle))] #[instrument(skip(handle))]
pub async fn squash_branch_commit( pub async fn squash_branch_commit(

View File

@ -14,8 +14,8 @@ use crate::{
use super::{ use super::{
branch::{BranchId, Ownership}, branch::{BranchId, Ownership},
errors::{ errors::{
self, FetchFromTargetError, GetBaseBranchDataError, IsRemoteBranchMergableError, self, FetchFromTargetError, GetBaseBranchDataError, GetRemoteBranchDataError,
ListRemoteBranchesError, IsRemoteBranchMergableError, ListRemoteBranchesError,
}, },
target_to_base_branch, BaseBranch, RemoteBranchFile, target_to_base_branch, BaseBranch, RemoteBranchFile,
}; };
@ -313,6 +313,16 @@ impl Controller {
.list_remote_branches(project_id) .list_remote_branches(project_id)
} }
pub async fn get_remote_branch_data(
&self,
project_id: &ProjectId,
refname: &git::Refname,
) -> Result<super::RemoteBranchData, ControllerError<GetRemoteBranchDataError>> {
self.inner(project_id)
.await
.get_remote_branch_data(project_id, refname)
}
pub async fn squash( pub async fn squash(
&self, &self,
project_id: &ProjectId, project_id: &ProjectId,
@ -808,6 +818,25 @@ impl ControllerInner {
.map_err(ControllerError::Action) .map_err(ControllerError::Action)
} }
pub fn get_remote_branch_data(
&self,
project_id: &ProjectId,
refname: &git::Refname,
) -> Result<super::RemoteBranchData, ControllerError<GetRemoteBranchDataError>> {
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( pub async fn squash(
&self, &self,
project_id: &ProjectId, project_id: &ProjectId,

View File

@ -751,6 +751,26 @@ pub enum ListRemoteBranchesError {
Other(#[from] anyhow::Error), 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<GetRemoteBranchDataError> 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<ListRemoteBranchesError> for Error { impl From<ListRemoteBranchesError> for Error {
fn from(value: ListRemoteBranchesError) -> Self { fn from(value: ListRemoteBranchesError) -> Self {
match value { match value {

View File

@ -27,6 +27,16 @@ pub struct RemoteBranch {
pub commits: Vec<RemoteCommit>, pub commits: Vec<RemoteCommit>,
} }
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RemoteBranchData {
pub sha: git::Oid,
pub name: git::Refname,
pub upstream: Option<git::RemoteRefname>,
pub behind: u32,
pub commits: Vec<RemoteCommit>,
}
#[derive(Debug, Clone, PartialEq, Serialize)] #[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RemoteCommit { pub struct RemoteCommit {
@ -66,6 +76,43 @@ pub fn list_remote_branches(
Ok(remote_branches) Ok(remote_branches)
} }
pub fn get_branch_data(
gb_repository: &gb_repository::Repository,
project_repository: &project_repository::Repository,
refname: &git::Refname,
) -> Result<super::RemoteBranchData, errors::GetRemoteBranchDataError> {
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( pub fn branch_to_remote_branch(
project_repository: &project_repository::Repository, project_repository: &project_repository::Repository,
branch: &git::Branch, branch: &git::Branch,

View File

@ -5,6 +5,7 @@
import ScrollableContainer from './ScrollableContainer.svelte'; import ScrollableContainer from './ScrollableContainer.svelte';
import CommitCard from '$lib/components/CommitCard.svelte'; import CommitCard from '$lib/components/CommitCard.svelte';
import { type SettingsStore, SETTINGS_CONTEXT } from '$lib/settings/userSettings'; import { type SettingsStore, SETTINGS_CONTEXT } from '$lib/settings/userSettings';
import { getRemoteBranchData } from '$lib/stores/remoteBranches';
import { Ownership } from '$lib/vbranches/ownership'; import { Ownership } from '$lib/vbranches/ownership';
import lscache from 'lscache'; import lscache from 'lscache';
import { marked } from 'marked'; import { marked } from 'marked';
@ -59,18 +60,20 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if branch.commits && branch.commits.length > 0} {#await getRemoteBranchData({ projectId, refname: branch.name }) then branchData}
<div class="flex w-full flex-col gap-y-2"> {#if branchData.commits && branchData.commits.length > 0}
{#each branch.commits as commit (commit.id)} <div class="flex w-full flex-col gap-y-2">
<CommitCard {#each branchData.commits as commit (commit.id)}
{commit} <CommitCard
{projectId} {commit}
{selectedFiles} {projectId}
commitUrl={base?.commitUrl(commit.id)} {selectedFiles}
/> commitUrl={base?.commitUrl(commit.id)}
{/each} />
</div> {/each}
{/if} </div>
{/if}
{/await}
</div> </div>
</ScrollableContainer> </ScrollableContainer>
<Resizer <Resizer

View File

@ -1,6 +1,6 @@
import { invoke } from '$lib/backend/ipc'; import { invoke } from '$lib/backend/ipc';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import { RemoteBranch } from '$lib/vbranches/types'; import { RemoteBranch, RemoteBranchData } from '$lib/vbranches/types';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { import {
BehaviorSubject, BehaviorSubject,
@ -25,7 +25,7 @@ export class RemoteBranchService {
baseBranch$: Observable<any> baseBranch$: Observable<any>
) { ) {
this.branches$ = combineLatest([baseBranch$, this.reload$, head$, fetches$]).pipe( this.branches$ = combineLatest([baseBranch$, this.reload$, head$, fetches$]).pipe(
switchMap(() => getRemoteBranchesData({ projectId })), switchMap(() => listRemoteBranches({ projectId })),
map((branches) => branches.filter((b) => b.ahead != 0)), map((branches) => branches.filter((b) => b.ahead != 0)),
shareReplay(1), shareReplay(1),
catchError((e) => { catchError((e) => {
@ -42,9 +42,7 @@ export class RemoteBranchService {
} }
} }
export async function getRemoteBranchesData(params: { async function listRemoteBranches(params: { projectId: string }): Promise<RemoteBranch[]> {
projectId: string;
}): Promise<RemoteBranch[]> {
const branches = plainToInstance( const branches = plainToInstance(
RemoteBranch, RemoteBranch,
await invoke<any[]>('list_remote_branches', params) await invoke<any[]>('list_remote_branches', params)
@ -52,3 +50,15 @@ export async function getRemoteBranchesData(params: {
return branches; return branches;
} }
export async function getRemoteBranchData(params: {
projectId: string;
refname: string;
}): Promise<RemoteBranchData> {
const branchData = plainToInstance(
RemoteBranchData,
await invoke<any>('get_remote_branch_data', params)
);
return branchData;
}

View File

@ -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 { export class BaseBranch {
branchName!: string; branchName!: string;
remoteName!: string; remoteName!: string;