diff --git a/packages/tauri/src/app.rs b/packages/tauri/src/app.rs index be4d72784..3399f12a6 100644 --- a/packages/tauri/src/app.rs +++ b/packages/tauri/src/app.rs @@ -366,6 +366,7 @@ impl App { let diff = Self::diff_hunks_to_string(diff); Ok(diff) } + pub fn git_commit_diff( &self, project_id: &str, diff --git a/packages/tauri/src/git/diff.rs b/packages/tauri/src/git/diff.rs index 0d2d989f9..d3a31f902 100644 --- a/packages/tauri/src/git/diff.rs +++ b/packages/tauri/src/git/diff.rs @@ -1,12 +1,13 @@ use std::{collections::HashMap, path}; use anyhow::{Context, Result}; +use serde::Serialize; use crate::git; use super::Repository; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Serialize)] pub struct Hunk { pub old_start: usize, pub old_lines: usize, diff --git a/packages/tauri/src/main.rs b/packages/tauri/src/main.rs index c0c550da6..65ab9c89d 100644 --- a/packages/tauri/src/main.rs +++ b/packages/tauri/src/main.rs @@ -8,7 +8,7 @@ use futures::future::join_all; use tauri::{generate_context, Manager}; use tracing::instrument; -use gitbutler::{virtual_branches::VirtualBranchCommit, *}; +use gitbutler::*; use crate::{error::Error, git, project_repository::activity}; @@ -366,7 +366,7 @@ async fn git_remote_branches_data( .map(|commit| { let proxy = proxy.clone(); async move { - VirtualBranchCommit { + virtual_branches::RemoteCommit { author: virtual_branches::Author { gravatar_url: proxy .proxy(&commit.author.gravatar_url) diff --git a/packages/tauri/src/virtual_branches/base.rs b/packages/tauri/src/virtual_branches/base.rs index 8a2fd79f4..b7f548463 100644 --- a/packages/tauri/src/virtual_branches/base.rs +++ b/packages/tauri/src/virtual_branches/base.rs @@ -451,7 +451,7 @@ pub fn target_to_base_branch( .log(oid, project_repository::LogUntil::Commit(target.sha)) .context("failed to get upstream commits")? .iter() - .map(|c| super::commit_to_vbranch_commit(project_repository, target, c, None)) + .map(|commit| super::commit_to_remote_commit(&project_repository.git_repository, commit)) .collect::>>()?; // get some recent commits @@ -459,7 +459,7 @@ pub fn target_to_base_branch( .log(target.sha, LogUntil::Take(20)) .context("failed to get recent commits")? .iter() - .map(|c| super::commit_to_vbranch_commit(project_repository, target, c, None)) + .map(|commit| super::commit_to_remote_commit(&project_repository.git_repository, commit)) .collect::>>()?; let base = super::BaseBranch { @@ -576,7 +576,8 @@ pub fn create_virtual_branch_from_branch( // do a diff between the head of this branch and the target base let diff = diff::trees(&project_repository.git_repository, &merge_tree, &tree) .context("failed to diff trees")?; - let hunks_by_filepath = super::hunks_by_filepath(project_repository, &diff); + let hunks_by_filepath = + super::virtual_hunks_by_filepath(&project_repository.git_repository, &diff); // assign ownership to the branch for hunk in hunks_by_filepath.values().flatten() { diff --git a/packages/tauri/src/virtual_branches/controller.rs b/packages/tauri/src/virtual_branches/controller.rs index 5af124532..db9f33472 100644 --- a/packages/tauri/src/virtual_branches/controller.rs +++ b/packages/tauri/src/virtual_branches/controller.rs @@ -349,7 +349,7 @@ impl Controller { .recent_commits .into_iter() .map(|commit| async move { - super::VirtualBranchCommit { + super::RemoteCommit { author: super::Author { gravatar_url: self .assets_proxy @@ -373,7 +373,7 @@ impl Controller { .upstream_commits .into_iter() .map(|commit| async move { - super::VirtualBranchCommit { + super::RemoteCommit { author: super::Author { gravatar_url: self .assets_proxy diff --git a/packages/tauri/src/virtual_branches/vbranch.rs b/packages/tauri/src/virtual_branches/vbranch.rs index 9e1550662..da05dead3 100644 --- a/packages/tauri/src/virtual_branches/vbranch.rs +++ b/packages/tauri/src/virtual_branches/vbranch.rs @@ -64,7 +64,6 @@ pub struct VirtualBranchCommit { pub created_at: u128, pub author: Author, pub is_remote: bool, - // only present if is_remote is false pub files: Vec, pub is_integrated: bool, } @@ -88,6 +87,14 @@ pub struct VirtualBranchFile { pub binary: bool, } +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteBranchFile { + pub path: path::PathBuf, + pub hunks: Vec, + pub binary: bool, +} + // this struct is a mapping to the view `Hunk` type in Typescript // found in src-tauri/src/routes/repo/[project_id]/types.ts // it holds a materialized view for presentation purposes of one entry of the @@ -110,6 +117,19 @@ pub struct VirtualBranchHunk { pub locked: bool, } +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteBranchHunk { + pub id: String, + pub diff: String, + pub modified_at: u128, + pub file_path: path::PathBuf, + pub hash: String, + pub start: usize, + pub end: usize, + pub binary: bool, +} + // this struct is a mapping to the view `RemoteBranch` type in Typescript // found in src-tauri/src/routes/repo/[project_id]/types.ts // @@ -127,7 +147,17 @@ pub struct RemoteBranch { pub behind: u32, pub upstream: Option, pub mergeable: bool, - pub commits: Vec, + pub commits: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteCommit { + pub id: String, + pub description: String, + pub created_at: u128, + pub author: Author, + pub files: Vec, } #[derive(Debug, Serialize, PartialEq, Clone)] @@ -139,8 +169,8 @@ pub struct BaseBranch { pub base_sha: String, pub current_sha: String, pub behind: u32, - pub upstream_commits: Vec, - pub recent_commits: Vec, + pub upstream_commits: Vec, + pub recent_commits: Vec, } #[derive(Debug, Serialize, Hash, Clone, PartialEq, Eq)] @@ -572,74 +602,91 @@ pub fn list_remote_branches( let mut branches: Vec = Vec::new(); for branch in &top_branches { - let branch_name = branch.refname().context("could not get branch name")?; - match branch.target() { - Some(branch_oid) => { - // get the branch ref - let branch_commit = repo - .find_commit(branch_oid) - .context("failed to find branch commit")?; + if let Some(branch_oid) = branch.target() { + let ahead = project_repository + .log(branch_oid, LogUntil::Commit(main_oid)) + .context("failed to get ahead commits")?; - let count_behind = project_repository - .distance(main_oid, branch_oid) - .context("failed to get behind count")?; - - let ahead = project_repository - .log(branch_oid, LogUntil::Commit(main_oid)) - .context("failed to get ahead commits")?; - let count_ahead = ahead.len(); - - let upstream = branch - .upstream() - .ok() - .map(|upstream_branch| git::RemoteBranchName::try_from(&upstream_branch)) - .transpose()?; - - if count_ahead > 0 { - if let Ok(base_tree) = find_base_tree(repo, &branch_commit, &target_commit) { - // determine if this tree is mergeable - let branch_tree = branch_commit.tree()?; - let mergeable = !repo - .merge_trees(&base_tree, &branch_tree, &wd_tree)? - .has_conflicts(); - - branches.push(RemoteBranch { - sha: branch_oid.to_string(), - name: branch_name.to_string(), - upstream, - behind: count_behind, - mergeable, - commits: ahead - .into_iter() - .map(|commit| { - commit_to_vbranch_commit( - project_repository, - &default_target, - &commit, - None, - ) - }) - .collect::>>()?, - }); - }; - } - } - None => { - // this is a detached head - branches.push(RemoteBranch { - sha: "".to_string(), - name: branch_name.to_string(), - behind: 0, - upstream: None, - mergeable: false, - commits: vec![], - }); + if ahead.is_empty() { + continue; } + + let branch_name = branch.refname().context("could not get branch name")?; + // get the branch ref + let branch_commit = repo + .find_commit(branch_oid) + .context("failed to find branch commit")?; + + let count_behind = project_repository + .distance(main_oid, branch_oid) + .context("failed to get behind count")?; + + let upstream = branch + .upstream() + .ok() + .map(|upstream_branch| git::RemoteBranchName::try_from(&upstream_branch)) + .transpose()?; + + let base_tree = find_base_tree(repo, &branch_commit, &target_commit)?; + // determine if this tree is mergeable + let branch_tree = branch_commit.tree()?; + let mergeable = !repo + .merge_trees(&base_tree, &branch_tree, &wd_tree)? + .has_conflicts(); + + branches.push(RemoteBranch { + sha: branch_oid.to_string(), + name: branch_name.to_string(), + upstream, + behind: count_behind, + mergeable, + commits: ahead + .into_iter() + .map(|commit| commit_to_remote_commit(repo, &commit)) + .collect::>>()?, + }); } } Ok(branches) } +pub fn commit_to_remote_commit( + repository: &git::Repository, + commit: &git::Commit, +) -> Result { + Ok(RemoteCommit { + id: commit.id().to_string(), + description: commit.message().unwrap_or_default().to_string(), + created_at: commit.time().seconds().try_into().unwrap(), + author: commit.author().into(), + files: list_remote_commit_files(repository, commit)?, + }) +} + +fn list_remote_commit_files( + repository: &git::Repository, + commit: &git::Commit, +) -> Result> { + if commit.parent_count() == 0 { + return Ok(vec![]); + } + let parent = commit.parent(0).context("failed to get parent commit")?; + let commit_tree = commit.tree().context("failed to get commit tree")?; + let parent_tree = parent.tree().context("failed to get parent tree")?; + let diff = diff::trees(repository, &parent_tree, &commit_tree)?; + + let files = diff + .into_iter() + .map(|(file_path, hunks)| RemoteBranchFile { + path: file_path.clone(), + hunks: hunks.clone(), + binary: hunks.iter().any(|h| h.binary), + }) + .collect::>(); + + Ok(files) +} + pub fn get_wd_tree(repo: &git::Repository) -> Result { let mut index = repo.index()?; index.add_all(["*"], git2::IndexAddOption::DEFAULT, None)?; @@ -819,7 +866,8 @@ fn calculate_non_commited_files( ) .context("failed to diff trees")?; - let non_commited_hunks_by_filepath = super::hunks_by_filepath(project_repository, &diff); + let non_commited_hunks_by_filepath = + super::virtual_hunks_by_filepath(&project_repository.git_repository, &diff); let file_hunks = files .iter() @@ -910,7 +958,7 @@ fn calculate_non_commited_files( Ok(vfiles) } -fn list_commit_files( +fn list_virtual_commit_files( project_repository: &project_repository::Repository, commit: &git::Commit, ) -> Result> { @@ -925,8 +973,8 @@ fn list_commit_files( &parent_tree, &commit_tree, )?; - let hunks_by_filepath = hunks_by_filepath(project_repository, &diff); - Ok(hunks_to_files( + let hunks_by_filepath = virtual_hunks_by_filepath(&project_repository.git_repository, &diff); + Ok(virtual_hunks_to_virtual_files( project_repository, &hunks_by_filepath .values() @@ -952,11 +1000,8 @@ pub fn commit_to_vbranch_commit( None => true, }; - let files = if is_remote { - vec![] - } else { - list_commit_files(repository, commit).context("failed to list commit files")? - }; + let files = + list_virtual_commit_files(repository, commit).context("failed to list commit files")?; let is_integrated = is_commit_integrated(repository, target, commit)?; @@ -1242,8 +1287,8 @@ fn diff_hash(diff: &str) -> String { format!("{:x}", md5::compute(addition)) } -pub fn hunks_by_filepath( - project_repository: &project_repository::Repository, +pub fn virtual_hunks_by_filepath( + repository: &git::Repository, diff: &HashMap>, ) -> HashMap> { let mut mtimes: HashMap = HashMap::new(); @@ -1253,7 +1298,7 @@ pub fn hunks_by_filepath( .iter() .map(|hunk| VirtualBranchHunk { id: format!("{}-{}", hunk.new_start, hunk.new_start + hunk.new_lines), - modified_at: get_mtime(&mut mtimes, &project_repository.path().join(file_path)), + modified_at: get_mtime(&mut mtimes, &repository.path().join(file_path)), file_path: file_path.clone(), diff: hunk.diff.clone(), start: hunk.new_start, @@ -1369,7 +1414,8 @@ fn get_non_applied_status( .collect::>() }) .collect::>(); - let hunks_by_filepath = hunks_by_filepath(project_repository, &diff); + let hunks_by_filepath = + virtual_hunks_by_filepath(&project_repository.git_repository, &diff); let hunks_by_filepath = hunks_by_filepath .values() .flatten() @@ -1409,7 +1455,8 @@ fn get_applied_status( ) .context("failed to diff")?; - let mut hunks_by_filepath = hunks_by_filepath(project_repository, &diff); + let mut hunks_by_filepath = + virtual_hunks_by_filepath(&project_repository.git_repository, &diff); // sort by order, so that the default branch is first (left in the ui) virtual_branches.sort_by(|a, b| a.order.cmp(&b.order)); @@ -1584,7 +1631,7 @@ fn get_applied_status( Ok(files_by_branch) } -fn hunks_to_files( +fn virtual_hunks_to_virtual_files( project_repository: &project_repository::Repository, hunks: &[VirtualBranchHunk], ) -> Vec { @@ -1619,7 +1666,7 @@ fn group_virtual_hunks( hunks_by_branch .iter() .map(|(branch, hunks)| { - let mut files = hunks_to_files(project_repository, hunks); + let mut files = virtual_hunks_to_virtual_files(project_repository, hunks); files.sort_by(|a, b| { branch .ownership diff --git a/packages/ui/src/lib/vbranches/branchController.ts b/packages/ui/src/lib/vbranches/branchController.ts index f7f73e393..8c4a00c2d 100644 --- a/packages/ui/src/lib/vbranches/branchController.ts +++ b/packages/ui/src/lib/vbranches/branchController.ts @@ -1,4 +1,4 @@ -import type { Branch, BranchData, BaseBranch, WritableReloadable } from './types'; +import type { Branch, RemoteBranch, BaseBranch, WritableReloadable } from './types'; import * as toasts from '$lib/toasts'; import { invoke } from '$lib/ipc'; @@ -6,7 +6,7 @@ export class BranchController { constructor( readonly projectId: string, readonly virtualBranchStore: WritableReloadable, - readonly remoteBranchStore: WritableReloadable, + readonly remoteBranchStore: WritableReloadable, readonly targetBranchStore: WritableReloadable ) {} diff --git a/packages/ui/src/lib/vbranches/branchStoresCache.ts b/packages/ui/src/lib/vbranches/branchStoresCache.ts index eeeb9774a..a109ba229 100644 --- a/packages/ui/src/lib/vbranches/branchStoresCache.ts +++ b/packages/ui/src/lib/vbranches/branchStoresCache.ts @@ -1,5 +1,5 @@ import { asyncWritable, type Readable } from '@square/svelte-store'; -import { BaseBranch, Branch, BranchData, type WritableReloadable } from './types'; +import { BaseBranch, Branch, RemoteBranch, type WritableReloadable } from './types'; import { plainToInstance } from 'class-transformer'; import { invoke } from '$lib/ipc'; import { isDelete, isInsert, type Delta } from '$lib/api/ipc/deltas'; @@ -36,13 +36,13 @@ export function getRemoteBranchStore(projectId: string, asyncStores: Readable getRemoteBranchesData({ projectId }), async (newRemotes) => newRemotes, { reloadable: true, trackState: true } - ) as WritableReloadable; + ) as WritableReloadable; } export function getBaseBranchStore(projectId: string, asyncStores: Readable[]) { return asyncWritable( asyncStores, - async () => getBaseBranchData({ projectId }), + async () => getBaseBranch({ projectId }), async (newBaseBranch) => newBaseBranch, { reloadable: true, trackState: true } ) as WritableReloadable; @@ -56,11 +56,13 @@ export async function listVirtualBranches(params: { projectId: string }): Promis return result; } -export async function getRemoteBranchesData(params: { projectId: string }): Promise { - return plainToInstance(BranchData, await invoke('git_remote_branches_data', params)); +export async function getRemoteBranchesData(params: { + projectId: string; +}): Promise { + return plainToInstance(RemoteBranch, await invoke('git_remote_branches_data', params)); } -export async function getBaseBranchData(params: { projectId: string }): Promise { +export async function getBaseBranch(params: { projectId: string }): Promise { const baseBranch = plainToInstance(BaseBranch, await invoke('get_base_branch_data', params)); if (baseBranch) { // The rust code performs a fetch when get_base_branch_data is invoked diff --git a/packages/ui/src/lib/vbranches/types.ts b/packages/ui/src/lib/vbranches/types.ts index 53202bd95..08a25fe09 100644 --- a/packages/ui/src/lib/vbranches/types.ts +++ b/packages/ui/src/lib/vbranches/types.ts @@ -57,6 +57,27 @@ export class Commit { files!: File[]; } +export class RemoteCommit { + id!: string; + author!: Author; + description!: string; + @Transform((obj) => new Date(obj.value)) + createdAt!: Date; + @Type(() => RemoteFile) + files!: RemoteFile[]; +} + +export class RemoteHunk { + diff!: string; +} + +export class RemoteFile { + path!: string; + @Type(() => RemoteHunk) + hunks!: RemoteHunk[]; + binary!: boolean; +} + export class Author { email!: string; name!: string; @@ -65,14 +86,14 @@ export class Author { } // TODO: For consistency change Ts suffix to At, and return milliseconds from back end -export class BranchData { +export class RemoteBranch { sha!: string; name!: string; behind!: number; upstream?: string; mergeable!: boolean; - @Type(() => Commit) - commits!: Commit[]; + @Type(() => RemoteCommit) + commits!: RemoteCommit[]; ahead(): number { return this.commits.length; @@ -98,10 +119,10 @@ export class BaseBranch { baseSha!: string; currentSha!: string; behind!: number; - @Type(() => Commit) - upstreamCommits!: Commit[]; - @Type(() => Commit) - recentCommits!: Commit[]; + @Type(() => RemoteCommit) + upstreamCommits!: RemoteCommit[]; + @Type(() => RemoteCommit) + recentCommits!: RemoteCommit[]; fetchedAt!: Date; get repoBaseUrl(): string { diff --git a/packages/ui/src/routes/repo/[projectId]/CommitCard.svelte b/packages/ui/src/routes/repo/[projectId]/CommitCard.svelte index 1b00c06da..ad0e2e484 100644 --- a/packages/ui/src/routes/repo/[projectId]/CommitCard.svelte +++ b/packages/ui/src/routes/repo/[projectId]/CommitCard.svelte @@ -1,5 +1,5 @@ diff --git a/packages/ui/src/routes/repo/[projectId]/Tray.svelte b/packages/ui/src/routes/repo/[projectId]/Tray.svelte index 302d653f0..5996a24f5 100644 --- a/packages/ui/src/routes/repo/[projectId]/Tray.svelte +++ b/packages/ui/src/routes/repo/[projectId]/Tray.svelte @@ -1,6 +1,6 @@