Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 54 additions & 57 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion crates/but-hunk-dependency/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ gitbutler-serde.workspace = true

# TODO: remove once not needed anymore
git2.workspace = true
gitbutler-repo.workspace = true
gitbutler-oxidize.workspace = true

# For `ui` module
Expand Down
48 changes: 23 additions & 25 deletions crates/but-hunk-dependency/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,8 @@
//! so in that case it should be fine to fallback to using the first parent.
mod input;

use anyhow::Context;
use but_core::{TreeChange, UnifiedDiff};
use gitbutler_oxidize::{ObjectIdExt, OidExt};
use gitbutler_repo::logging::{LogUntil, RepositoryExt};
use gitbutler_oxidize::ObjectIdExt;
use gix::prelude::ObjectIdExt as _;
use gix::trace;
pub use input::{InputCommit, InputDiffHunk, InputFile, InputStack};
Expand Down Expand Up @@ -206,34 +204,34 @@ pub fn tree_changes_to_input_files(

/// Traverse all commits from `tip` down to `common_merge_base`, but omit merges.
// TODO: the algorithm should be able to deal with merges, just like `jj absorb` or `git absorb`.
// TODO: Needs ahead-behind in `gix` to remove `git2` completely.
fn commits_in_stack_base_to_tip_without_merge_bases(
tip: gix::Id<'_>,
// TODO: implement in `gix` - need actual rev-walk with excludes, and possibly ahead-behind.
git2_repo: &git2::Repository,
common_merge_base: gix::ObjectId,
) -> anyhow::Result<Vec<gix::ObjectId>> {
let tip = tip.detach().to_git2();
let common_merge_base = common_merge_base.to_git2();
let commit_ids = git2_repo
.l(tip, LogUntil::Commit(common_merge_base), false)
.context("failed to list commits")?
.into_iter()
.rev()
.filter_map(move |commit_id| {
let commit = git2_repo.find_commit(commit_id).ok()?;
if commit.parent_count() == 1 {
return Some(commit_id.to_gix());
}

// TODO: probably to be reviewed as it basically doesn't give access to the
// first (base) commit in a branch that forked off target-sha.
let has_integrated_parent = commit.parent_ids().any(|id| {
git2_repo
.graph_ahead_behind(id, common_merge_base)
.is_ok_and(|(number_commits_ahead, _)| number_commits_ahead == 0)
});
let commit_ids: Vec<_> = tip
.ancestors()
.first_parent_only()
.with_hidden(Some(common_merge_base))
.all()?
.collect::<Result<_, _>>()?;
let commit_ids = commit_ids.into_iter().rev().filter_map(|info| {
let commit = info.id().object().ok()?.into_commit();
let commit = commit.decode().ok()?;
if commit.parents.len() == 1 {
return Some(info.id);
}

(!has_integrated_parent).then_some(commit_id.to_gix())
// TODO: probably to be reviewed as it basically doesn't give access to the
// first (base) commit in a branch that forked off target-sha.
let has_integrated_parent = commit.parents().any(|id| {
git2_repo
.graph_ahead_behind(id.to_git2(), common_merge_base.to_git2())
.is_ok_and(|(number_commits_ahead, _)| number_commits_ahead == 0)
});

(!has_integrated_parent).then_some(info.id)
});
Ok(commit_ids.collect())
}
2 changes: 0 additions & 2 deletions crates/but-workspace/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ bstr.workspace = true
git2.workspace = true
but-core.workspace = true
but-rebase.workspace = true
gitbutler-id.workspace = true
gix = { workspace = true, features = ["worktree-mutation"] }
gitbutler-stack.workspace = true
gitbutler-command-context.workspace = true
Expand All @@ -29,7 +28,6 @@ gitbutler-serde.workspace = true
itertools = "0.14"
url = { version = "2.5.4", features = ["serde"] }
md5 = "0.7.0"
but-status.workspace = true

# For virtual branches metadata
gitbutler-fs.workspace = true
Expand Down
91 changes: 75 additions & 16 deletions crates/but-workspace/src/branch_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use anyhow::{Context, bail};
use but_core::RefMetadata;
use gitbutler_command_context::CommandContext;
use gitbutler_error::error::Code;
use gitbutler_oxidize::{ObjectIdExt as _, OidExt};
use gitbutler_oxidize::OidExt;
use gix::date::parse::TimeBuf;
use gix::prelude::ObjectIdExt;
use gix::reference::Category;
use gix::remote::Direction;
Expand Down Expand Up @@ -174,23 +175,15 @@ pub fn branch_details_v3(

let mut authors = HashSet::new();
let (mut commits, upstream_commits) = {
let repo = git2::Repository::open(repo.path())?;

let commits = local_commits(
&repo,
integration_branch_id.to_git2(),
branch_id.to_git2(),
&mut authors,
)?;
let commits = local_commits_gix(branch_id, integration_branch_id.detach(), &mut authors)?;

let upstream_commits = if let Some(remote_tracking_branch) = remote_tracking_branch.as_mut()
{
let remote_id = remote_tracking_branch.peel_to_id_in_place()?;
upstream_commits(
&repo,
remote_id.to_git2(),
integration_branch_id.to_git2(),
branch_id.to_git2(),
upstream_commits_gix(
remote_id,
integration_branch_id.detach(),
branch_id.detach(),
&mut authors,
)?
} else {
Expand Down Expand Up @@ -299,10 +292,40 @@ fn upstream_commits(
.collect())
}

/// Traverse all commits that are reachable from the first parent of `upstream_id`, but not in `integration_branch_id` nor in `branch_id`.
/// While at it, collect the commiter and author of each commit into `authors`.
fn upstream_commits_gix(
upstream_id: gix::Id<'_>,
integration_branch_id: gix::ObjectId,
branch_id: gix::ObjectId,
authors: &mut HashSet<ui::Author>,
) -> anyhow::Result<Vec<UpstreamCommit>> {
let revwalk = upstream_id
.ancestors()
.with_hidden([branch_id, integration_branch_id])
.first_parent_only()
.all()?;
let mut out = Vec::new();
for info in revwalk {
let info = info?;
let commit = info.id().object()?.into_commit();
let commit = commit.decode()?;
let author: ui::Author = commit.author().into();
let commiter: ui::Author = commit.committer().into();
authors.insert(author.clone());
authors.insert(commiter);
out.push(UpstreamCommit {
id: info.id,
message: commit.message.into(),
created_at: i128::from(commit.time().seconds) * 1000,
author,
});
}
Ok(out)
}

/// Traverse all commits that are reachable from the first parent of `branch_id`, but not in `integration_branch`, and store all
/// commit authors and committers in `authors` while at it.
///
// TODO: rewrite with `gix` and obtain is_conflicted information.
fn local_commits(
repository: &git2::Repository,
integration_branch_id: git2::Oid,
Expand Down Expand Up @@ -334,3 +357,39 @@ fn local_commits(
})
.collect())
}

/// Traverse all commits that are reachable from the first parent of `branch_id`, but not in `integration_branch`, and store all
/// commit authors and committers in `authors` while at it.
fn local_commits_gix(
branch_id: gix::Id<'_>,
integration_branch_id: gix::ObjectId,
authors: &mut HashSet<ui::Author>,
) -> anyhow::Result<Vec<ui::Commit>> {
let revwalk = branch_id
.ancestors()
.with_hidden([integration_branch_id])
.first_parent_only()
.all()?;

let mut out = Vec::new();
for info in revwalk {
let info = info?;
let commit = but_core::Commit::from_id(info.id())?;

let mut buf = TimeBuf::default();
let author: ui::Author = commit.author.to_ref(&mut buf).into();
let commiter: ui::Author = commit.committer.to_ref(&mut buf).into();
authors.insert(author.clone());
authors.insert(commiter);
out.push(ui::Commit {
id: info.id,
parent_ids: commit.parents.iter().cloned().collect(),
message: commit.message.clone(),
has_conflicts: commit.is_conflicted(),
state: CommitState::LocalAndRemote(info.id),
created_at: i128::from(commit.committer.time.seconds) * 1000,
author,
});
}
Ok(out)
}
Loading
Loading