Skip to content

Commit c39a948

Browse files
committed
Adjust integration check to also recognize by matching tree.
This is relevant when all commits are equal by tree, but seem changed due to the added GitButler headers. For added safety, we also compare by commit message, date and authors, basically everything that isn't the headers.
1 parent bdab3e2 commit c39a948

File tree

3 files changed

+108
-25
lines changed

3 files changed

+108
-25
lines changed

apps/desktop/src/lib/vbranches/types.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,21 @@ export class DetailedCommit {
179179
conflicted!: boolean;
180180
// Set if a GitButler branch reference pointing to this commit exists. In the format of "refs/remotes/origin/my-branch"
181181
remoteRef?: string | undefined;
182+
// Identifies the remote commit id from which this local commit was copied. The backend figured this out by comparing
183+
// author, commit and message.
184+
copiedFromRemoteId?: string;
182185

183186
prev?: DetailedCommit;
184187
next?: DetailedCommit;
185188

186189
get status(): CommitStatus {
187190
if (this.isIntegrated) return 'integrated';
188-
if (this.isRemote && (!this.relatedTo || this.id === this.relatedTo.id))
191+
if (
192+
this.isRemote &&
193+
(!this.relatedTo ||
194+
this.id === this.relatedTo.id ||
195+
this.copiedFromRemoteId === this.relatedTo.id)
196+
)
189197
return 'localAndRemote';
190198
return 'local';
191199
}
@@ -240,9 +248,10 @@ export class Commit {
240248

241249
export type AnyCommit = DetailedCommit | Commit;
242250

243-
export function commitCompare(left: AnyCommit, right: AnyCommit): boolean {
251+
export function commitCompare(left: AnyCommit, right: DetailedCommit): boolean {
244252
if (left.id === right.id) return true;
245253
if (left.changeId && right.changeId && left.changeId === right.changeId) return true;
254+
if (right.copiedFromRemoteId && right.copiedFromRemoteId === left.id) return true;
246255
return false;
247256
}
248257

crates/gitbutler-branch-actions/src/commit.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ pub struct VirtualBranchCommit {
3636
pub change_id: Option<String>,
3737
pub is_signed: bool,
3838
pub conflicted: bool,
39+
/// The id of the remote commit from which this one was copied, as identified by
40+
/// having equal author, committer, and commit message.
41+
/// This is used by the frontend similar to the `change_id` to group matching commits.
42+
#[serde(with = "gitbutler_serde::oid_opt")]
43+
pub copied_from_remote_id: Option<git2::Oid>,
3944
pub remote_ref: Option<ReferenceName>,
4045
}
4146

@@ -45,6 +50,7 @@ pub(crate) fn commit_to_vbranch_commit(
4550
commit: &git2::Commit,
4651
is_integrated: bool,
4752
is_remote: bool,
53+
copied_from_remote_id: Option<git2::Oid>,
4854
) -> Result<VirtualBranchCommit> {
4955
let timestamp = u128::try_from(commit.time().seconds())?;
5056
let message = commit.message_bstr().to_owned();
@@ -81,6 +87,7 @@ pub(crate) fn commit_to_vbranch_commit(
8187
change_id: commit.change_id(),
8288
is_signed: commit.is_signed(),
8389
conflicted: commit.is_conflicted(),
90+
copied_from_remote_id,
8491
remote_ref,
8592
};
8693

crates/gitbutler-branch-actions/src/virtual.rs

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
use std::{
2-
borrow::Cow,
3-
collections::HashMap,
4-
path::{Path, PathBuf},
5-
vec,
6-
};
7-
81
use crate::{
92
branch_manager::BranchManagerExt,
103
commit::{commit_to_vbranch_commit, VirtualBranchCommit},
@@ -17,7 +10,7 @@ use crate::{
1710
Get, VirtualBranchesExt,
1811
};
1912
use anyhow::{anyhow, bail, Context, Result};
20-
use bstr::ByteSlice;
13+
use bstr::{BString, ByteSlice};
2114
use git2_hooks::HookResult;
2215
use gitbutler_branch::{
2316
dedup, dedup_fmt, reconcile_claims, signature, Branch, BranchId, BranchOwnershipClaims,
@@ -38,6 +31,13 @@ use gitbutler_repo::{
3831
};
3932
use gitbutler_time::time::now_since_unix_epoch_ms;
4033
use serde::Serialize;
34+
use std::collections::HashSet;
35+
use std::{
36+
borrow::Cow,
37+
collections::HashMap,
38+
path::{Path, PathBuf},
39+
vec,
40+
};
4141
use tracing::instrument;
4242

4343
// this struct is a mapping to the view `Branch` type in Typescript
@@ -313,19 +313,34 @@ pub fn list_virtual_branches_cached(
313313
))?;
314314

315315
// find upstream commits if we found an upstream reference
316-
let mut pushed_commits = HashMap::new();
317-
if let Some(upstream) = &upstram_branch_commit {
318-
let merge_base =
319-
repo.merge_base(upstream.id(), default_target.sha)
320-
.context(format!(
321-
"failed to find merge base between {} and {}",
322-
upstream.id(),
323-
default_target.sha
324-
))?;
325-
for oid in ctx.l(upstream.id(), LogUntil::Commit(merge_base))? {
326-
pushed_commits.insert(oid, true);
327-
}
328-
}
316+
let (remote_commit_ids, remote_commit_data) = upstram_branch_commit
317+
.as_ref()
318+
.map(
319+
|upstream| -> Result<(HashSet<git2::Oid>, HashMap<CommitData, git2::Oid>)> {
320+
let merge_base =
321+
repo.merge_base(upstream.id(), default_target.sha)
322+
.context(format!(
323+
"failed to find merge base between {} and {}",
324+
upstream.id(),
325+
default_target.sha
326+
))?;
327+
let remote_commit_ids =
328+
HashSet::from_iter(ctx.l(upstream.id(), LogUntil::Commit(merge_base))?);
329+
let remote_commit_data: HashMap<_, _> = remote_commit_ids
330+
.iter()
331+
.copied()
332+
.filter_map(|id| repo.find_commit(id).ok())
333+
.filter_map(|commit| {
334+
CommitData::try_from(&commit)
335+
.ok()
336+
.map(|key| (key, commit.id()))
337+
})
338+
.collect();
339+
Ok((remote_commit_ids, remote_commit_data))
340+
},
341+
)
342+
.transpose()?
343+
.unwrap_or_default();
329344

330345
let mut is_integrated = false;
331346
let mut is_remote = false;
@@ -346,15 +361,33 @@ pub fn list_virtual_branches_cached(
346361
is_remote = if is_remote {
347362
is_remote
348363
} else {
349-
pushed_commits.contains_key(&commit.id())
364+
// This can only work once we have pushed our commits to the remote.
365+
// Otherwise, even local commits created from a remote commit will look different.
366+
remote_commit_ids.contains(&commit.id())
350367
};
351368

352369
// only check for integration if we haven't already found an integration
353370
if !is_integrated {
354371
is_integrated = check_commit.is_integrated(commit)?
355372
};
356373

357-
commit_to_vbranch_commit(ctx, &branch, commit, is_integrated, is_remote)
374+
let copied_from_remote_id = if is_remote {
375+
// No need to use additional matching, the commits match already
376+
// which does the right thing.
377+
None
378+
} else {
379+
CommitData::try_from(commit)
380+
.ok()
381+
.and_then(|data| remote_commit_data.get(&data).copied())
382+
};
383+
commit_to_vbranch_commit(
384+
ctx,
385+
&branch,
386+
commit,
387+
is_integrated,
388+
is_remote || copied_from_remote_id.is_some(),
389+
copied_from_remote_id,
390+
)
358391
})
359392
.collect::<Result<Vec<_>>>()?
360393
};
@@ -427,6 +460,40 @@ pub fn list_virtual_branches_cached(
427460
Ok((branches, status.skipped_files))
428461
}
429462

463+
/// The commit-data we can use for comparison to see which remote-commit was used to craete
464+
/// a local commit from.
465+
/// Note that trees can't be used for comparison as these are typically rebased.
466+
#[derive(Hash, Eq, PartialEq)]
467+
struct CommitData {
468+
message: BString,
469+
author: gix::actor::Signature,
470+
committer: gix::actor::Signature,
471+
}
472+
473+
impl TryFrom<&git2::Commit<'_>> for CommitData {
474+
type Error = anyhow::Error;
475+
476+
fn try_from(commit: &git2::Commit<'_>) -> std::result::Result<Self, Self::Error> {
477+
Ok(CommitData {
478+
message: commit.message_raw_bytes().into(),
479+
author: git2_signature_to_gix_signature(commit.author()),
480+
committer: git2_signature_to_gix_signature(commit.committer()),
481+
})
482+
}
483+
}
484+
485+
fn git2_signature_to_gix_signature(input: git2::Signature<'_>) -> gix::actor::Signature {
486+
gix::actor::Signature {
487+
name: input.name_bytes().into(),
488+
email: input.email_bytes().into(),
489+
time: gix::date::Time {
490+
seconds: input.when().seconds(),
491+
offset: input.when().offset_minutes() * 60,
492+
sign: input.when().offset_minutes().into(),
493+
},
494+
}
495+
}
496+
430497
fn branches_with_large_files_abridged(mut branches: Vec<VirtualBranch>) -> Vec<VirtualBranch> {
431498
for branch in &mut branches {
432499
for file in &mut branch.files {

0 commit comments

Comments
 (0)