Skip to content

Commit 71aa090

Browse files
committed
Use gix for branch listing
1 parent e42fcd4 commit 71aa090

File tree

7 files changed

+355
-327
lines changed

7 files changed

+355
-327
lines changed

Cargo.lock

Lines changed: 195 additions & 228 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ resolver = "2"
3434

3535
[workspace.dependencies]
3636
# Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes.
37-
gix = { git = "https://github.com/Byron/gitoxide", rev = "12313f2720bb509cb8fa5d7033560823beafb91c", default-features = false, features = [] }
37+
gix = { git = "https://github.com/Byron/gitoxide", rev = "29898e3010bd3332418c683f2ac96aff5c8e72fa", default-features = false, features = [] }
3838
git2 = { version = "0.18.3", features = [
3939
"vendored-openssl",
4040
"vendored-libgit2",

crates/gitbutler-branch-actions/benches/branches.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,14 @@ pub fn benchmark_list_branches(c: &mut Criterion) {
3838
] {
3939
let mut group = c.benchmark_group(bench_name);
4040
let project = fixture_project(repo_name, script_name);
41+
let ctx = CommandContext::open(&project).unwrap();
4142
group.throughput(Throughput::Elements(num_references));
4243
group
4344
.bench_function("no filter", |b| {
44-
b.iter(|| {
45-
let ctx = CommandContext::open(&project).unwrap();
46-
list_branches(black_box(&ctx), None, None)
47-
})
45+
b.iter(|| list_branches(black_box(&ctx), None, None))
4846
})
4947
.bench_function("name-filter rejecting all", |b| {
50-
b.iter(|| {
51-
let ctx = CommandContext::open(&project).unwrap();
52-
list_branches(black_box(&ctx), None, Some(vec!["not available".into()]))
53-
})
48+
b.iter(|| list_branches(black_box(&ctx), None, Some(vec!["not available".into()])))
5449
});
5550
}
5651
}

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

Lines changed: 104 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,65 @@
1+
use crate::VirtualBranchesExt;
2+
use anyhow::{Context, Result};
3+
use bstr::{BStr, BString, ByteSlice};
14
use core::fmt;
5+
use gitbutler_branch::{Branch as GitButlerBranch, BranchId, ReferenceExtGix, Target};
6+
use gitbutler_command_context::CommandContext;
7+
use gitbutler_reference::normalize_branch_name;
8+
use gix::prelude::ObjectIdExt;
9+
use gix::reference::Category;
10+
use serde::{Deserialize, Serialize};
11+
use std::borrow::Cow;
12+
use std::collections::BTreeSet;
213
use std::{
314
cmp::max,
415
collections::{HashMap, HashSet},
516
fmt::Debug,
617
vec,
718
};
819

9-
use anyhow::{Context, Result};
10-
use bstr::{BString, ByteSlice};
11-
use gitbutler_branch::{Branch as GitButlerBranch, BranchId, ReferenceExt, Target};
12-
use gitbutler_command_context::CommandContext;
13-
use gitbutler_reference::normalize_branch_name;
14-
use serde::{Deserialize, Serialize};
15-
16-
use crate::VirtualBranchesExt;
17-
1820
/// Returns a list of branches associated with this project.
1921
pub fn list_branches(
2022
ctx: &CommandContext,
2123
filter: Option<BranchListingFilter>,
2224
filter_branch_names: Option<Vec<BranchIdentity>>,
2325
) -> Result<Vec<BranchListing>> {
26+
let mut repo = gix::open(ctx.repository().path())?;
27+
repo.object_cache_size_if_unset(1024 * 1024);
2428
let has_filter = filter.is_some();
2529
let filter = filter.unwrap_or_default();
2630
let vb_handle = ctx.project().virtual_branches();
31+
let platform = repo.references()?;
2732
let mut branches: Vec<GroupBranch> = vec![];
28-
for (branch, branch_type) in ctx.repository().branches(None)?.filter_map(Result::ok) {
33+
for reference in platform.all()?.filter_map(Result::ok) {
2934
// Loosely match on branch names
3035
if let Some(branch_names) = &filter_branch_names {
31-
let has_matching_name = branch_names.iter().any(|branch_name| {
32-
if let Ok(Some(name)) = branch.name() {
33-
name.ends_with(&branch_name.0)
34-
} else {
35-
false
36-
}
37-
});
36+
let has_matching_name = branch_names
37+
.iter()
38+
.any(|branch_name| reference.name().as_bstr().ends_with_str(&branch_name.0));
3839

3940
if !has_matching_name {
4041
continue;
4142
}
4243
}
4344

44-
match branch_type {
45-
git2::BranchType::Local => {
46-
branches.push(GroupBranch::Local(branch));
47-
}
48-
git2::BranchType::Remote => {
49-
branches.push(GroupBranch::Remote(branch));
50-
}
45+
let is_local_branch = match reference.name().category() {
46+
Some(Category::LocalBranch) => true,
47+
Some(Category::RemoteBranch) => false,
48+
_ => continue,
5149
};
50+
branches.push(if is_local_branch {
51+
GroupBranch::Local(reference)
52+
} else {
53+
GroupBranch::Remote(reference)
54+
});
5255
}
5356

5457
let virtual_branches = vb_handle.list_all_branches()?;
5558

5659
for branch in virtual_branches {
5760
branches.push(GroupBranch::Virtual(branch));
5861
}
59-
let mut branches = combine_branches(branches, ctx, vb_handle.get_default_target()?)?;
62+
let mut branches = combine_branches(branches, &repo, vb_handle.get_default_target()?)?;
6063

6164
// Apply the filter
6265
branches.retain(|branch| !has_filter || matches_all(branch, filter));
@@ -111,11 +114,11 @@ fn matches_all(branch: &BranchListing, filter: BranchListingFilter) -> bool {
111114

112115
fn combine_branches(
113116
group_branches: Vec<GroupBranch>,
114-
ctx: &CommandContext,
117+
repo: &gix::Repository,
115118
target_branch: Target,
116119
) -> Result<Vec<BranchListing>> {
117-
let repo = ctx.repository();
118-
let remotes = repo.remotes()?;
120+
let remotes = repo.remote_names();
121+
let packed = repo.refs.cached_packed_buffer()?;
119122

120123
// Group branches by identity
121124
let mut groups: HashMap<BranchIdentity, Vec<GroupBranch>> = HashMap::new();
@@ -134,8 +137,14 @@ fn combine_branches(
134137
Ok(groups
135138
.into_iter()
136139
.filter_map(|(identity, group_branches)| {
137-
let res =
138-
branch_group_to_branch(&identity, group_branches, repo, &remotes, &target_branch);
140+
let res = branch_group_to_branch(
141+
&identity,
142+
group_branches,
143+
repo,
144+
packed.as_ref().map(|p| &***p),
145+
&remotes,
146+
&target_branch,
147+
);
139148
match res {
140149
Ok(branch_entry) => branch_entry,
141150
Err(err) => {
@@ -155,17 +164,22 @@ fn combine_branches(
155164
fn branch_group_to_branch(
156165
identity: &BranchIdentity,
157166
group_branches: Vec<GroupBranch>,
158-
repo: &git2::Repository,
159-
remotes: &git2::string_array::StringArray,
167+
repo: &gix::Repository,
168+
packed: Option<&gix::refs::packed::Buffer>,
169+
remotes: &BTreeSet<Cow<'_, BStr>>,
160170
target: &Target,
161171
) -> Result<Option<BranchListing>> {
162-
let mut vbranches = group_branches
163-
.iter()
164-
.filter_map(|branch| match branch {
165-
GroupBranch::Virtual(vb) => Some(vb),
166-
_ => None,
167-
})
168-
.collect::<Vec<_>>();
172+
let (local_branches, remote_branches, mut vbranches) =
173+
group_branches
174+
.into_iter()
175+
.fold((Vec::new(), Vec::new(), Vec::new()), |mut acc, item| {
176+
match item {
177+
GroupBranch::Local(branch) => acc.0.push(branch),
178+
GroupBranch::Remote(branch) => acc.1.push(branch),
179+
GroupBranch::Virtual(branch) => acc.2.push(branch),
180+
}
181+
acc
182+
});
169183

170184
let virtual_branch = if vbranches.len() > 1 {
171185
vbranches.sort_by_key(|virtual_branch| virtual_branch.updated_timestamp_ms);
@@ -174,25 +188,10 @@ fn branch_group_to_branch(
174188
vbranches.first()
175189
};
176190

177-
let remote_branches: Vec<&git2::Branch> = group_branches
178-
.iter()
179-
.filter_map(|branch| match branch {
180-
GroupBranch::Remote(gb) => Some(gb),
181-
_ => None,
182-
})
183-
.collect();
184-
let local_branches: Vec<&git2::Branch> = group_branches
185-
.iter()
186-
.filter_map(|branch| match branch {
187-
GroupBranch::Local(gb) => Some(gb),
188-
_ => None,
189-
})
190-
.collect();
191-
192191
if virtual_branch.is_none()
193192
&& local_branches
194193
.iter()
195-
.any(|b| b.get().given_name(remotes).as_deref().ok() == Some(target.branch.branch()))
194+
.any(|b| b.name().given_name(remotes).as_deref().ok() == Some(target.branch.branch()))
196195
{
197196
return Ok(None);
198197
}
@@ -204,12 +203,11 @@ fn branch_group_to_branch(
204203
in_workspace: branch.in_workspace,
205204
});
206205

206+
// TODO(ST): keep the type alive, don't reduce to BString
207207
let mut remotes: Vec<BString> = Vec::new();
208208
for branch in remote_branches.iter() {
209-
if let Some(name) = branch.get().name() {
210-
if let Ok(remote_name) = repo.branch_remote_name(name) {
211-
remotes.push(remote_name.as_bstr().into());
212-
}
209+
if let Some(remote_name) = branch.remote_name(gix::remote::Direction::Fetch) {
210+
remotes.push(remote_name.as_bstr().into());
213211
}
214212
}
215213

@@ -218,21 +216,22 @@ fn branch_group_to_branch(
218216
// The head commit for which we calculate statistics.
219217
// If there is a virtual branch let's get it's head. Alternatively, pick the first local branch and use it's head.
220218
// If there are no local branches, pick the first remote branch.
221-
let head = if let Some(vbranch) = virtual_branch {
222-
Some(vbranch.head)
223-
} else if let Some(branch) = local_branches.first().cloned() {
224-
branch.get().peel_to_commit().ok().map(|c| c.id())
225-
} else if let Some(branch) = remote_branches.first().cloned() {
226-
branch.get().peel_to_commit().ok().map(|c| c.id())
219+
let head_commit = if let Some(vbranch) = virtual_branch {
220+
Some(git2_to_gix_object_id(vbranch.head).attach(repo))
221+
} else if let Some(mut branch) = local_branches.into_iter().next() {
222+
branch.peel_to_id_in_place_packed(packed).ok()
223+
} else if let Some(mut branch) = remote_branches.into_iter().next() {
224+
branch.peel_to_id_in_place_packed(packed).ok()
227225
} else {
228226
None
229227
}
230228
.context("Could not get any valid reference in order to build branch stats")?;
231229

232-
let head_commit = repo.find_commit(head)?;
233-
// If this was a virtual branch and there was never any remote set, use the virtual branch name as the identity
230+
let head = gix_to_git2_oid(head_commit.detach());
231+
let head_commit = head_commit.object()?.try_into_commit()?;
232+
let head_commit = head_commit.decode()?;
234233
let last_modified_ms = max(
235-
(head_commit.time().seconds() * 1000) as u128,
234+
(head_commit.time().seconds * 1000) as u128,
236235
virtual_branch.map_or(0, |x| x.updated_timestamp_ms),
237236
);
238237
let last_commiter = head_commit.author().into();
@@ -248,30 +247,37 @@ fn branch_group_to_branch(
248247
}))
249248
}
250249

250+
fn gix_to_git2_oid(id: gix::ObjectId) -> git2::Oid {
251+
git2::Oid::from_bytes(id.as_bytes()).expect("always valid")
252+
}
253+
254+
fn git2_to_gix_object_id(id: git2::Oid) -> gix::ObjectId {
255+
gix::ObjectId::try_from(id.as_bytes()).expect("git2 oid is always valid")
256+
}
257+
251258
/// A sum type of branch that can be a plain git branch or a virtual branch
252259
#[allow(clippy::large_enum_variant)]
253260
enum GroupBranch<'a> {
254-
Local(git2::Branch<'a>),
255-
Remote(git2::Branch<'a>),
261+
Local(gix::Reference<'a>),
262+
Remote(gix::Reference<'a>),
256263
Virtual(GitButlerBranch),
257264
}
258265

259266
impl fmt::Debug for GroupBranch<'_> {
260267
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
261268
match self {
262-
GroupBranch::Local(branch) | GroupBranch::Remote(branch) => {
263-
let reference = branch.get();
264-
let target = reference
265-
.target()
266-
.expect("Failed to reference target in debug formatting");
267-
let name = reference
268-
.name()
269-
.expect("Failed to get reference name in debug");
270-
formatter
271-
.debug_struct("GroupBranch::Local/Remote")
272-
.field("0", &&format!("id: {}, name: {}", target, name).as_str())
273-
.finish()
274-
}
269+
GroupBranch::Local(branch) | GroupBranch::Remote(branch) => formatter
270+
.debug_struct("GroupBranch::Local/Remote")
271+
.field(
272+
"0",
273+
&format!(
274+
"id: {:?}, name: {}",
275+
branch.target(),
276+
branch.name().as_bstr()
277+
)
278+
.as_str(),
279+
)
280+
.finish(),
275281
GroupBranch::Virtual(branch) => formatter
276282
.debug_struct("GroupBranch::Virtal")
277283
.field("0", branch)
@@ -284,10 +290,11 @@ impl GroupBranch<'_> {
284290
/// A name identifier for the branch. When multiple branches (e.g. virtual, local, remote) have the same identity,
285291
/// they are grouped together under the same `Branch` entry.
286292
/// `None` means an identity could not be obtained, which makes this branch odd enough to ignore.
287-
fn identity(&self, remotes: &git2::string_array::StringArray) -> Option<BranchIdentity> {
293+
fn identity(&self, remotes: &BTreeSet<Cow<'_, BStr>>) -> Option<BranchIdentity> {
288294
match self {
289-
GroupBranch::Local(branch) => branch.get().given_name(remotes).ok(),
290-
GroupBranch::Remote(branch) => branch.get().given_name(remotes).ok(),
295+
GroupBranch::Local(branch) | GroupBranch::Remote(branch) => {
296+
branch.name().given_name(remotes).ok()
297+
}
291298
// The identity of a Virtual branch is derived from the source refname, upstream or the branch given name, in that order
292299
GroupBranch::Virtual(branch) => {
293300
let name_from_source = branch.source_refname.as_ref().and_then(|n| n.branch());
@@ -354,7 +361,7 @@ pub struct BranchListing {
354361
/// The head of interest for the branch group, used for calculating branch statistics.
355362
/// If there is a virtual branch, a local branch and remote branches, the head is determined in the following order:
356363
/// 1. The head of the virtual branch
357-
/// 2. The head of the local branch
364+
/// 2. The head of the first local branch
358365
/// 3. The head of the first remote branch
359366
#[serde(skip)]
360367
pub head: git2::Oid,
@@ -363,6 +370,7 @@ pub struct BranchListing {
363370
/// Represents a "commit author" or "signature", based on the data from the git history
364371
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
365372
pub struct Author {
373+
// TODO(ST): use `BString` here to not degenerate information
366374
/// The name of the author as configured in the git config
367375
pub name: Option<String>,
368376
/// The email of the author as configured in the git config
@@ -403,6 +411,15 @@ impl From<git2::Signature<'_>> for Author {
403411
}
404412
}
405413

414+
impl From<gix::actor::SignatureRef<'_>> for Author {
415+
fn from(value: gix::actor::SignatureRef<'_>) -> Self {
416+
Author {
417+
name: Some(value.name.to_string()),
418+
email: Some(value.email.to_string()),
419+
}
420+
}
421+
}
422+
406423
/// Represents a reference to an associated virtual branch
407424
#[derive(Debug, Clone, Serialize, PartialEq)]
408425
#[serde(rename_all = "camelCase")]

crates/gitbutler-branch-actions/tests/virtual_branches/list.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ fn one_branch_on_integration_multiple_remotes() -> Result<()> {
125125
&list[0],
126126
ExpectedBranchListing {
127127
identity: "main".into(),
128-
remotes: vec!["other-remote", "origin"],
128+
remotes: vec!["origin", "other-remote"],
129129
virtual_branch_given_name: Some("main"),
130130
virtual_branch_in_workspace: true,
131131
has_local: true,

crates/gitbutler-branch/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bstr::ByteSlice;
66
mod branch_ext;
77
pub use branch_ext::BranchExt;
88
mod reference_ext;
9-
pub use reference_ext::ReferenceExt;
9+
pub use reference_ext::{ReferenceExt, ReferenceExtGix};
1010
mod dedup;
1111
pub use dedup::{dedup, dedup_fmt};
1212
mod file_ownership;

0 commit comments

Comments
 (0)