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 88% rename from packages/tauri/src/virtual_branches/vbranch.rs rename to packages/tauri/src/virtual_branches/virtual.rs index bad4d8a3d..bc37c89e7 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>,