diff --git a/apps/desktop/src/lib/vbranches/types.ts b/apps/desktop/src/lib/vbranches/types.ts index 4eb42c159..18e94e948 100644 --- a/apps/desktop/src/lib/vbranches/types.ts +++ b/apps/desktop/src/lib/vbranches/types.ts @@ -188,6 +188,19 @@ export class DetailedCommit { // author, commit and message. copiedFromRemoteId?: string; + /** + * + * Represents the remote commit id of this patch. + * This field is set if: + * - The commit has been pushed + * - The commit has been copied from a remote commit (when applying a remote branch) + * + * The `remoteCommitId` may be the same as the `id` or it may be different if the commit has been rebased or updated. + * + * Note: This makes both the `isRemote` and `copiedFromRemoteId` fields redundant, but they are kept for compatibility. + */ + remoteCommitId?: string; + prev?: DetailedCommit; next?: DetailedCommit; diff --git a/crates/gitbutler-branch-actions/src/commit.rs b/crates/gitbutler-branch-actions/src/commit.rs index d1febd716..4dbfc8773 100644 --- a/crates/gitbutler-branch-actions/src/commit.rs +++ b/crates/gitbutler-branch-actions/src/commit.rs @@ -25,6 +25,7 @@ pub struct VirtualBranchCommit { pub description: BStringForFrontend, pub created_at: u128, pub author: Author, + /// Dont use, favor `remote_commit_id` instead pub is_remote: bool, pub files: Vec, pub is_integrated: bool, @@ -39,6 +40,16 @@ pub struct VirtualBranchCommit { /// This is used by the frontend similar to the `change_id` to group matching commits. #[serde(with = "gitbutler_serde::oid_opt")] pub copied_from_remote_id: Option, + /// Represents the remote commit id of this patch. + /// This field is set if: + /// - The commit has been pushed + /// - The commit has been copied from a remote commit (when applying a remote branch) + /// + /// The `remote_commit_id` may be the same as the `id` or it may be different if the commit has been rebased or updated. + /// + /// Note: This makes both the `is_remote` and `copied_from_remote_id` fields redundant, but they are kept for compatibility. + #[serde(with = "gitbutler_serde::oid_opt")] + pub remote_commit_id: Option, } pub(crate) fn commit_to_vbranch_commit( @@ -48,6 +59,7 @@ pub(crate) fn commit_to_vbranch_commit( is_integrated: bool, is_remote: bool, copied_from_remote_id: Option, + remote_commit_id: Option, ) -> Result { let timestamp = u128::try_from(commit.time().seconds())?; let message = commit.message_bstr().to_owned(); @@ -76,6 +88,7 @@ pub(crate) fn commit_to_vbranch_commit( is_signed: commit.is_signed(), conflicted: commit.is_conflicted(), copied_from_remote_id, + remote_commit_id: remote_commit_id.or(copied_from_remote_id), }; Ok(commit) diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index fc008b115..4357c7d75 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -380,6 +380,7 @@ pub fn list_virtual_branches_cached( is_integrated, is_remote, copied_from_remote_id, + None, // remote_commit_id is only used inside PatchSeries ) }) .collect::>>()? @@ -420,7 +421,13 @@ pub fn list_virtual_branches_cached( let refname = branch.refname()?.into(); // TODO: Error out here once this API is stable - let series = match stack_series(ctx, &branch, &default_target, &check_commit) { + let series = match stack_series( + ctx, + &branch, + &default_target, + &check_commit, + remote_commit_data, + ) { Ok(series) => series, Err(e) => { tracing::warn!("failed to compute stack series: {:?}", e); @@ -472,6 +479,7 @@ fn stack_series( branch: &Stack, default_target: &Target, check_commit: &IsCommitIntegrated, + remote_commit_data: HashMap, ) -> Result> { let mut api_series: Vec = vec![]; let stack_series = branch.list_series(ctx)?; @@ -487,13 +495,23 @@ fn stack_series( for patch in series.clone().local_commits { let commit = commit_by_oid_or_change_id(&patch, ctx, branch.head(), default_target)?; let is_integrated = check_commit.is_integrated(&commit)?; + let copied_from_remote_id = CommitData::try_from(&commit) + .ok() + .and_then(|data| remote_commit_data.get(&data).copied()); + let remote_commit_id = commit.change_id().and_then(|change_id| { + series + .remote_commit_ids_by_change_id + .get(&change_id) + .cloned() + }); let vcommit = commit_to_vbranch_commit( ctx, branch, &commit, is_integrated, series.remote(&patch), - None, + copied_from_remote_id, + remote_commit_id, )?; patches.push(vcommit); } @@ -508,7 +526,8 @@ fn stack_series( &commit, is_integrated, true, // per definition - None, + None, // per definition + Some(commit.id()), )?; upstream_patches.push(vcommit); } diff --git a/crates/gitbutler-stack-api/src/series.rs b/crates/gitbutler-stack-api/src/series.rs index 6eecfe1f4..da4da0d82 100644 --- a/crates/gitbutler-stack-api/src/series.rs +++ b/crates/gitbutler-stack-api/src/series.rs @@ -1,11 +1,12 @@ use gitbutler_patch_reference::{CommitOrChangeId, PatchReference}; +use std::collections::HashMap; /// Series or (patch) Series is a set of patches (commits) that are dependent on each other. /// This is effectively a sub-branch within a (series) stack. /// The difference from a branch is that only the patches (commits) unique to the series are included. /// /// The `pushed` status, as well as the `remote_reference` can be obtained from the methods on `head` (PatchReference). -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Series { /// The GitButler-managed head reference for this series. It points to a commit ID or a change ID in the stack. /// This head may or may not be part of the commits that are in the series @@ -19,6 +20,9 @@ pub struct Series { /// If the branch/series have never been pushed, this list will be empty. /// Topologically ordered, the first entry is the newest in the series. pub remote_commits: Vec, + /// The commit IDs of the remote commits that are part of this series, grouped by change id. + /// Since we dont have a change_id to commit_id index, this is used to determine + pub remote_commit_ids_by_change_id: HashMap, } impl Series { diff --git a/crates/gitbutler-stack-api/src/stack_ext.rs b/crates/gitbutler-stack-api/src/stack_ext.rs index a75e341b7..b8e53e28f 100644 --- a/crates/gitbutler-stack-api/src/stack_ext.rs +++ b/crates/gitbutler-stack-api/src/stack_ext.rs @@ -24,6 +24,7 @@ use crate::heads::add_head; use crate::heads::get_head; use crate::heads::remove_head; use crate::series::Series; +use std::collections::HashMap; /// A (series) Stack represents multiple "branches" that are dependent on each other in series. /// @@ -457,6 +458,7 @@ impl StackExt for Stack { .collect_vec(); let mut remote_patches: Vec = vec![]; + let mut remote_commit_ids_by_change_id: HashMap = HashMap::new(); if let Some(remote_name) = default_target.push_remote_name.as_ref() { if head.pushed(remote_name, ctx).unwrap_or_default() { let head_commit = repo @@ -466,17 +468,24 @@ impl StackExt for Stack { repo.log(head_commit.id(), LogUntil::Commit(merge_base))? .iter() .rev() - .map(|c| match c.change_id() { - Some(change_id) => CommitOrChangeId::ChangeId(change_id.to_string()), - None => CommitOrChangeId::CommitId(c.id().to_string()), - }) - .for_each(|c| remote_patches.push(c)); + .for_each(|c| { + let commit_or_change_id = match c.change_id() { + Some(change_id) => { + remote_commit_ids_by_change_id + .insert(change_id.to_string(), c.id()); + CommitOrChangeId::ChangeId(change_id.to_string()) + } + None => CommitOrChangeId::CommitId(c.id().to_string()), + }; + remote_patches.push(commit_or_change_id); + }); } }; all_series.push(Series { head: head.clone(), local_commits: local_patches, remote_commits: remote_patches, + remote_commit_ids_by_change_id, }); previous_head = head_commit; }