From 60b4c63078b21fbb4bf6e54f93d218d0c372a3e6 Mon Sep 17 00:00:00 2001 From: Nikita Galaiko Date: Tue, 19 Sep 2023 13:02:51 +0200 Subject: [PATCH] feat: refactor virtual branch module structure and add materialized view structs for presentation purposes The virtual branch module structure has been refactored to improve code organization and readability. The module now consists of separate files for virtual branch, remote, and files related functionality. Additionally, materialized view structs have been added for presentation purposes. These structs include `VirtualBranch`, `VirtualBranchCommit`, `VirtualBranchFile`, and `VirtualBranchHunk`. These structs provide a materialized view of the virtual branch data and are used for presentation purposes through the IPC. This refactoring improves code maintainability and provides a clearer separation of concerns between the virtual branch module and the presentation layer. --- packages/tauri/src/virtual_branches/base.rs | 16 +- packages/tauri/src/virtual_branches/files.rs | 38 +++ .../tauri/src/virtual_branches/integration.rs | 6 +- packages/tauri/src/virtual_branches/mod.rs | 10 +- packages/tauri/src/virtual_branches/remote.rs | 214 +++++++++++++++ .../{vbranch.rs => virtual.rs} | 257 ------------------ 6 files changed, 278 insertions(+), 263 deletions(-) create mode 100644 packages/tauri/src/virtual_branches/files.rs create mode 100644 packages/tauri/src/virtual_branches/remote.rs rename packages/tauri/src/virtual_branches/{vbranch.rs => virtual.rs} (87%) diff --git a/packages/tauri/src/virtual_branches/base.rs b/packages/tauri/src/virtual_branches/base.rs index f04f861f3..cf2691061 100644 --- a/packages/tauri/src/virtual_branches/base.rs +++ b/packages/tauri/src/virtual_branches/base.rs @@ -1,6 +1,7 @@ use std::time; use anyhow::{bail, Context, Result}; +use serde::Serialize; use uuid::Uuid; use crate::{ @@ -10,7 +11,20 @@ use crate::{ reader, sessions, }; -use super::{branch, delete_branch, iterator, target}; +use super::{branch, delete_branch, iterator, target, RemoteCommit}; + +#[derive(Debug, Serialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BaseBranch { + pub branch_name: String, + pub remote_name: String, + pub remote_url: String, + pub base_sha: String, + pub current_sha: String, + pub behind: u32, + pub upstream_commits: Vec, + pub recent_commits: Vec, +} pub fn get_base_branch_data( gb_repository: &gb_repository::Repository, diff --git a/packages/tauri/src/virtual_branches/files.rs b/packages/tauri/src/virtual_branches/files.rs new file mode 100644 index 000000000..1fa5ceba7 --- /dev/null +++ b/packages/tauri/src/virtual_branches/files.rs @@ -0,0 +1,38 @@ +use std::path; + +use anyhow::{Context, Result}; +use serde::Serialize; + +use crate::git::{self, diff}; + +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteBranchFile { + pub path: path::PathBuf, + pub hunks: Vec, + pub binary: bool, +} + +pub 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) +} diff --git a/packages/tauri/src/virtual_branches/integration.rs b/packages/tauri/src/virtual_branches/integration.rs index b739f41bd..da712d519 100644 --- a/packages/tauri/src/virtual_branches/integration.rs +++ b/packages/tauri/src/virtual_branches/integration.rs @@ -251,7 +251,7 @@ fn verify_head_is_clean( ) .context("failed to reset to integration commit")?; - let new_branch = super::vbranch::create_virtual_branch( + let new_branch = super::create_virtual_branch( gb_repository, &BranchCreateRequest { name: extra_commits @@ -322,11 +322,11 @@ fn verify_head_is_set( { Some(GITBUTLER_INTEGRATION_REFERENCE) => Ok(()), None => { - super::vbranch::mark_all_unapplied(gb_repository).map_err(VerifyError::Other)?; + super::mark_all_unapplied(gb_repository).map_err(VerifyError::Other)?; Err(VerifyError::DetachedHead) } Some(head_name) => { - super::vbranch::mark_all_unapplied(gb_repository).map_err(VerifyError::Other)?; + super::mark_all_unapplied(gb_repository).map_err(VerifyError::Other)?; Err(VerifyError::InvalidHead(head_name.to_string())) } } diff --git a/packages/tauri/src/virtual_branches/mod.rs b/packages/tauri/src/virtual_branches/mod.rs index 281d95296..c48888ba5 100644 --- a/packages/tauri/src/virtual_branches/mod.rs +++ b/packages/tauri/src/virtual_branches/mod.rs @@ -2,6 +2,9 @@ pub mod branch; pub use branch::Branch; pub mod target; +mod files; +pub use files::*; + mod integration; pub use integration::GITBUTLER_INTEGRATION_BRANCH_NAME; @@ -18,5 +21,8 @@ pub use iterator::BranchIterator as Iterator; #[cfg(test)] mod tests; -mod vbranch; -pub use vbranch::*; +mod r#virtual; +pub use r#virtual::*; + +mod remote; +pub use remote::*; diff --git a/packages/tauri/src/virtual_branches/remote.rs b/packages/tauri/src/virtual_branches/remote.rs new file mode 100644 index 000000000..2459121eb --- /dev/null +++ b/packages/tauri/src/virtual_branches/remote.rs @@ -0,0 +1,214 @@ +use std::{ + collections::{HashMap, HashSet}, + time, +}; + +use anyhow::{Context, Result}; +use serde::Serialize; + +use crate::{ + gb_repository, git, + project_repository::{self, LogUntil}, + reader, sessions, +}; + +use super::{branch, get_default_target, iterator::BranchIterator as Iterator, Author}; + +// this struct is a mapping to the view `RemoteBranch` type in Typescript +// found in src-tauri/src/routes/repo/[project_id]/types.ts +// +// it holds data calculated for presentation purposes of one Git branch +// with comparison data to the Target commit, determining if it is mergeable, +// and how far ahead or behind the Target it is. +// an array of them can be requested from the frontend to show in the sidebar +// Tray and should only contain branches that have not been converted into +// virtual branches yet (ie, we have no `Branch` struct persisted in our data. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteBranch { + pub sha: String, + pub name: String, + pub behind: u32, + pub upstream: Option, + 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 fn list_remote_branches( + gb_repository: &gb_repository::Repository, + project_repository: &project_repository::Repository, +) -> Result> { + // get the current target + let current_session = gb_repository + .get_or_create_current_session() + .context("failed to get or create currnt session")?; + let current_session_reader = sessions::Reader::open(gb_repository, ¤t_session) + .context("failed to open current session")?; + + let default_target = match get_default_target(¤t_session_reader) + .context("failed to get default target")? + { + Some(target) => target, + None => return Ok(vec![]), + }; + + let current_time = time::SystemTime::now(); + let too_old = time::Duration::from_secs(86_400 * 90); // 90 days (3 months) is too old + + let repo = &project_repository.git_repository; + + let main_oid = default_target.sha; + + let virtual_branches_names = Iterator::new(¤t_session_reader) + .context("failed to create branch iterator")? + .collect::, reader::Error>>() + .context("failed to read virtual branches")? + .into_iter() + .filter_map(|branch| branch.upstream) + .map(|upstream| upstream.branch().to_string()) + .collect::>(); + let mut most_recent_branches_by_hash: HashMap = HashMap::new(); + + for (branch, _) in repo.branches(None)?.flatten() { + if let Some(branch_oid) = branch.target() { + // get the branch ref + let branch_commit = repo + .find_commit(branch_oid) + .context("failed to find branch commit")?; + let branch_time = branch_commit.time(); + let seconds = branch_time + .seconds() + .try_into() + .context("failed to convert seconds")?; + let branch_time = time::UNIX_EPOCH + time::Duration::from_secs(seconds); + let duration = current_time + .duration_since(branch_time) + .context("failed to get duration")?; + if duration > too_old { + continue; + } + + let branch_name = + git::BranchName::try_from(&branch).context("could not get branch name")?; + + // skip the default target branch (both local and remote) + match branch_name { + git::BranchName::Remote(ref remote_branch_name) => { + if *remote_branch_name == default_target.branch { + continue; + } + } + git::BranchName::Local(ref local_branch_name) => { + if let Some(upstream_branch_name) = local_branch_name.remote() { + if *upstream_branch_name == default_target.branch { + continue; + } + } + } + } + + if virtual_branches_names.contains(branch_name.branch()) { + continue; + } + if branch_name.branch().eq("HEAD") { + continue; + } + if branch_name + .branch() + .eq(super::integration::GITBUTLER_INTEGRATION_BRANCH_NAME) + { + continue; + } + + match most_recent_branches_by_hash.get(&branch_oid) { + Some((_, existing_seconds)) => { + let branch_name = branch.refname().context("could not get branch name")?; + if seconds < *existing_seconds { + // this branch is older than the one we already have + continue; + } + if seconds > *existing_seconds { + most_recent_branches_by_hash.insert(branch_oid, (branch, seconds)); + continue; + } + if branch_name.starts_with("refs/remotes") { + // this branch is a remote branch + // we always prefer the remote branch if it is the same age as the local branch + most_recent_branches_by_hash.insert(branch_oid, (branch, seconds)); + continue; + } + } + None => { + // this is the first time we've seen this branch + // so we should add it to the list + most_recent_branches_by_hash.insert(branch_oid, (branch, seconds)); + } + } + } + } + + let mut most_recent_branches: Vec<(git::Branch, u64)> = + most_recent_branches_by_hash.into_values().collect(); + + // take the most recent 20 branches + most_recent_branches.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by timestamp in descending order. + let sorted_branches: Vec = most_recent_branches + .into_iter() + .map(|(branch, _)| branch) + .collect(); + let top_branches = sorted_branches.into_iter().take(20).collect::>(); // Take the first 20 entries. + + let mut branches: Vec = Vec::new(); + for branch in &top_branches { + if let Some(branch_oid) = branch.target() { + let ahead = project_repository + .log(branch_oid, LogUntil::Commit(main_oid)) + .context("failed to get ahead commits")?; + + if ahead.is_empty() { + continue; + } + + let branch_name = branch.refname().context("could not get branch name")?; + + 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()?; + + branches.push(RemoteBranch { + sha: branch_oid.to_string(), + name: branch_name.to_string(), + upstream, + behind: count_behind, + commits: ahead + .into_iter() + .map(|commit| commit_to_remote_commit(&commit)) + .collect::>>()?, + }); + } + } + Ok(branches) +} + +pub fn commit_to_remote_commit(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(), + }) +} diff --git a/packages/tauri/src/virtual_branches/vbranch.rs b/packages/tauri/src/virtual_branches/virtual.rs similarity index 87% rename from packages/tauri/src/virtual_branches/vbranch.rs rename to packages/tauri/src/virtual_branches/virtual.rs index e91f3303e..f2a99f3ac 100644 --- a/packages/tauri/src/virtual_branches/vbranch.rs +++ b/packages/tauri/src/virtual_branches/virtual.rs @@ -86,14 +86,6 @@ 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 @@ -116,60 +108,6 @@ 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 -// -// it holds data calculated for presentation purposes of one Git branch -// with comparison data to the Target commit, determining if it is mergeable, -// and how far ahead or behind the Target it is. -// an array of them can be requested from the frontend to show in the sidebar -// Tray and should only contain branches that have not been converted into -// virtual branches yet (ie, we have no `Branch` struct persisted in our data. -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoteBranch { - pub sha: String, - pub name: String, - pub behind: u32, - pub upstream: Option, - 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, -} - -#[derive(Debug, Serialize, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct BaseBranch { - pub branch_name: String, - pub remote_name: String, - pub remote_url: String, - pub base_sha: String, - pub current_sha: String, - pub behind: u32, - pub upstream_commits: Vec, - pub recent_commits: Vec, -} - #[derive(Debug, Serialize, Hash, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Author { @@ -468,201 +406,6 @@ fn unapply_all_branches( Ok(()) } -pub fn list_remote_branches( - gb_repository: &gb_repository::Repository, - project_repository: &project_repository::Repository, -) -> Result> { - // get the current target - let current_session = gb_repository - .get_or_create_current_session() - .context("failed to get or create currnt session")?; - let current_session_reader = sessions::Reader::open(gb_repository, ¤t_session) - .context("failed to open current session")?; - - let default_target = match get_default_target(¤t_session_reader) - .context("failed to get default target")? - { - Some(target) => target, - None => return Ok(vec![]), - }; - - let current_time = time::SystemTime::now(); - let too_old = time::Duration::from_secs(86_400 * 90); // 90 days (3 months) is too old - - let repo = &project_repository.git_repository; - - let main_oid = default_target.sha; - - let virtual_branches_names = Iterator::new(¤t_session_reader) - .context("failed to create branch iterator")? - .collect::, reader::Error>>() - .context("failed to read virtual branches")? - .into_iter() - .filter_map(|branch| branch.upstream) - .map(|upstream| upstream.branch().to_string()) - .collect::>(); - let mut most_recent_branches_by_hash: HashMap = HashMap::new(); - - for (branch, _) in repo.branches(None)?.flatten() { - if let Some(branch_oid) = branch.target() { - // get the branch ref - let branch_commit = repo - .find_commit(branch_oid) - .context("failed to find branch commit")?; - let branch_time = branch_commit.time(); - let seconds = branch_time - .seconds() - .try_into() - .context("failed to convert seconds")?; - let branch_time = time::UNIX_EPOCH + time::Duration::from_secs(seconds); - let duration = current_time - .duration_since(branch_time) - .context("failed to get duration")?; - if duration > too_old { - continue; - } - - let branch_name = - git::BranchName::try_from(&branch).context("could not get branch name")?; - - // skip the default target branch (both local and remote) - match branch_name { - git::BranchName::Remote(ref remote_branch_name) => { - if *remote_branch_name == default_target.branch { - continue; - } - } - git::BranchName::Local(ref local_branch_name) => { - if let Some(upstream_branch_name) = local_branch_name.remote() { - if *upstream_branch_name == default_target.branch { - continue; - } - } - } - } - - if virtual_branches_names.contains(branch_name.branch()) { - continue; - } - if branch_name.branch().eq("HEAD") { - continue; - } - if branch_name - .branch() - .eq(super::integration::GITBUTLER_INTEGRATION_BRANCH_NAME) - { - continue; - } - - match most_recent_branches_by_hash.get(&branch_oid) { - Some((_, existing_seconds)) => { - let branch_name = branch.refname().context("could not get branch name")?; - if seconds < *existing_seconds { - // this branch is older than the one we already have - continue; - } - if seconds > *existing_seconds { - most_recent_branches_by_hash.insert(branch_oid, (branch, seconds)); - continue; - } - if branch_name.starts_with("refs/remotes") { - // this branch is a remote branch - // we always prefer the remote branch if it is the same age as the local branch - most_recent_branches_by_hash.insert(branch_oid, (branch, seconds)); - continue; - } - } - None => { - // this is the first time we've seen this branch - // so we should add it to the list - most_recent_branches_by_hash.insert(branch_oid, (branch, seconds)); - } - } - } - } - - let mut most_recent_branches: Vec<(git::Branch, u64)> = - most_recent_branches_by_hash.into_values().collect(); - - // take the most recent 20 branches - most_recent_branches.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by timestamp in descending order. - let sorted_branches: Vec = most_recent_branches - .into_iter() - .map(|(branch, _)| branch) - .collect(); - let top_branches = sorted_branches.into_iter().take(20).collect::>(); // Take the first 20 entries. - - let mut branches: Vec = Vec::new(); - for branch in &top_branches { - if let Some(branch_oid) = branch.target() { - let ahead = project_repository - .log(branch_oid, LogUntil::Commit(main_oid)) - .context("failed to get ahead commits")?; - - if ahead.is_empty() { - continue; - } - - let branch_name = branch.refname().context("could not get branch name")?; - - 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()?; - - branches.push(RemoteBranch { - sha: branch_oid.to_string(), - name: branch_name.to_string(), - upstream, - behind: count_behind, - commits: ahead - .into_iter() - .map(|commit| commit_to_remote_commit(&commit)) - .collect::>>()?, - }); - } - } - Ok(branches) -} - -pub fn commit_to_remote_commit(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(), - }) -} - -pub 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) -} - fn find_base_tree<'a>( repo: &'a git::Repository, branch_commit: &'a git::Commit<'a>,