more tests specific to how branches are discovered

This commit is contained in:
Sebastian Thiel 2024-07-30 16:57:55 +02:00
parent 182381dd79
commit 2d82b6e038
No known key found for this signature in database
GPG Key ID: 9CB5EE7895E8268B
16 changed files with 326 additions and 239 deletions

2
Cargo.lock generated
View File

@ -2014,7 +2014,6 @@ dependencies = [
"anyhow",
"bstr",
"diffy",
"futures",
"git2",
"git2-hooks",
"gitbutler-branch",
@ -2451,7 +2450,6 @@ version = "0.0.0"
dependencies = [
"anyhow",
"backoff",
"futures",
"gitbutler-branch-actions",
"gitbutler-command-context",
"gitbutler-error",

View File

@ -33,7 +33,6 @@ regex = "1.10"
git2-hooks = "0.3"
url = { version = "2.5.2", features = ["serde"] }
md5 = "0.7.0"
futures.workspace = true
itertools = "0.13"
gitbutler-command-context.workspace = true
gitbutler-project.workspace = true

View File

@ -386,7 +386,7 @@ pub(crate) fn update_base_branch(
let non_commited_files =
gitbutler_diff::trees(ctx.repository(), &branch_head_tree, &branch_tree)?;
if non_commited_files.is_empty() {
// if there are no commited files, then the branch is fully merged
// if there are no commited files, then the branch is fully merged,
// and we can delete it.
vb_state.mark_as_not_in_workspace(branch.id)?;
ctx.delete_branch_reference(&branch)?;

View File

@ -6,13 +6,10 @@ use std::{
use anyhow::{Context, Result};
use bstr::{BString, ByteSlice};
use gitbutler_branch::{
Branch as GitButlerBranch, BranchId, ReferenceExt, Target, VirtualBranchesHandle,
};
use gitbutler_branch::{Branch as GitButlerBranch, BranchId, ReferenceExt, Target};
use gitbutler_command_context::CommandContext;
use gitbutler_reference::normalize_branch_name;
use gitbutler_repo::RepoActionsExt;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::VirtualBranchesExt;
@ -22,147 +19,120 @@ pub fn list_branches(
ctx: &CommandContext,
filter: Option<BranchListingFilter>,
) -> Result<Vec<BranchListing>> {
let has_filter = filter.is_some();
let filter = filter.unwrap_or_default();
let vb_handle = ctx.project().virtual_branches();
// The definition of "own_branch" is based if the current user made the first commit on the branch
// However, because getting that info is both expensive and also we cant filter ahead of time,
// here we assume that all of the "own_branches" will be local.
let branch_filter = filter
.as_ref()
.and_then(|filter| match filter.own_branches {
Some(true) => Some(git2::BranchType::Local),
_ => None,
});
let mut git_branches: Vec<GroupBranch> = vec![];
for result in ctx.repository().branches(branch_filter)? {
match result {
Ok((branch, branch_type)) => match branch_type {
git2::BranchType::Local => {
if branch_filter
.map(|branch_type| branch_type == git2::BranchType::Local)
.unwrap_or(false)
{
// If we had an "own_branch" filter, we skipped getting the remote branches, however we still want the remote
// tracking branches for the ones that are local
if let Ok(upstream) = branch.upstream() {
git_branches.push(GroupBranch::Remote(upstream));
}
let own_branches = filter.own_branches.unwrap_or_default();
let mut branches: Vec<GroupBranch> = vec![];
for (branch, branch_type) in ctx
.repository()
.branches(own_branches.then_some(git2::BranchType::Local))?
.filter_map(Result::ok)
{
match branch_type {
git2::BranchType::Local => {
if own_branches {
// If we had an "own_branch" filter, we skipped getting the remote branches, however we still want the remote
// tracking branches for the ones that are local
if let Ok(upstream) = branch.upstream() {
branches.push(GroupBranch::Remote(upstream));
}
git_branches.push(GroupBranch::Local(branch));
}
git2::BranchType::Remote => {
git_branches.push(GroupBranch::Remote(branch));
}
},
Err(_) => {
continue;
branches.push(GroupBranch::Local(branch));
}
}
git2::BranchType::Remote => {
branches.push(GroupBranch::Remote(branch));
}
};
}
// virtual branches from the application state
let virtual_branches = ctx
.project()
.virtual_branches()
.list_all_branches()?
.into_iter();
for branch in vb_handle.list_all_branches()? {
branches.push(GroupBranch::Virtual(branch));
}
let mut branches = combine_branches(branches, ctx, vb_handle.get_default_target()?)?;
let branches = combine_branches(git_branches, virtual_branches, ctx, &vb_handle)?;
// Apply the filter
let branches: Vec<BranchListing> = branches
.into_iter()
.filter(|branch| matches_all(branch, &filter))
.sorted_by(|a, b| b.updated_at.cmp(&a.updated_at))
.collect();
branches.retain(|branch| !has_filter || matches_all(branch, filter));
branches.sort_by(|a, b| a.updated_at.cmp(&b.updated_at).reverse());
Ok(branches)
}
fn matches_all(branch: &BranchListing, filter: &Option<BranchListingFilter>) -> bool {
if let Some(filter) = filter {
let mut conditions: Vec<bool> = vec![];
if let Some(applied) = filter.applied {
if let Some(vb) = branch.virtual_branch.as_ref() {
conditions.push(applied == vb.in_workspace);
} else {
conditions.push(!applied);
}
fn matches_all(branch: &BranchListing, filter: BranchListingFilter) -> bool {
let mut conditions = vec![];
if let Some(applied) = filter.applied {
if let Some(vb) = branch.virtual_branch.as_ref() {
conditions.push(applied == vb.in_workspace);
} else {
conditions.push(!applied);
}
if let Some(own) = filter.own_branches {
conditions.push(own == branch.own_branch);
}
return conditions.iter().all(|&x| x);
} else {
true
}
if let Some(own) = filter.own_branches {
conditions.push(own == branch.own_branch);
}
return conditions.iter().all(|&x| x);
}
fn combine_branches(
mut group_branches: Vec<GroupBranch>,
virtual_branches: impl Iterator<Item = GitButlerBranch>,
group_branches: Vec<GroupBranch>,
ctx: &CommandContext,
vb_handle: &VirtualBranchesHandle,
target_branch: Target,
) -> Result<Vec<BranchListing>> {
let repo = ctx.repository();
for branch in virtual_branches {
group_branches.push(GroupBranch::Virtual(branch));
}
let remotes = repo.remotes()?;
let target_branch = vb_handle.get_default_target()?;
// Group branches by identity
let mut groups: HashMap<Option<String>, Vec<&GroupBranch>> = HashMap::new();
for branch in group_branches.iter() {
let identity = branch.identity(&remotes);
let mut groups: HashMap<String, Vec<GroupBranch>> = HashMap::new();
for branch in group_branches {
let Some(identity) = branch.identity(&remotes) else {
continue;
};
// Skip branches that should not be listed, e.g. the target 'main' or the gitbutler technical branches like 'gitbutler/integration'
if !should_list_git_branch(&identity, &target_branch) {
continue;
}
if let Some(group) = groups.get_mut(&identity) {
group.push(branch);
} else {
groups.insert(identity, vec![branch]);
}
groups.entry(identity).or_default().push(branch);
}
let (local_author, _committer) = ctx.signatures()?;
// Convert to Branch entries for the API response, filtering out any errors
let branches: Vec<BranchListing> = groups
.iter()
Ok(groups
.into_iter()
.filter_map(|(identity, group_branches)| {
let branch_entry = branch_group_to_branch(
identity.clone(),
group_branches.clone(),
let res = branch_group_to_branch(
&identity,
group_branches,
repo,
&local_author,
target_branch.sha,
);
if branch_entry.is_err() {
tracing::warn!(
"Failed to process branch group {:?} to branch entry: {:?}",
identity,
branch_entry
);
match res {
Ok(branch_entry) => Some(branch_entry),
Err(err) => {
tracing::warn!(
"Failed to process branch group {:?} to branch entry: {}",
identity,
err
);
None
}
}
branch_entry.ok()
})
.collect();
Ok(branches)
.collect())
}
/// Converts a group of branches with the same identity into a single branch entry
fn branch_group_to_branch(
identity: Option<String>,
group_branches: Vec<&GroupBranch>,
identity: &str,
group_branches: Vec<GroupBranch>,
repo: &git2::Repository,
local_author: &git2::Signature,
target_sha: git2::Oid,
) -> Result<BranchListing> {
let virtual_branch = group_branches
.iter()
.filter_map(|branch| match branch {
GroupBranch::Virtual(vb) => Some(vb),
_ => None,
})
.next();
let virtual_branch = group_branches.iter().find_map(|branch| match branch {
GroupBranch::Virtual(vb) => Some(vb),
_ => None,
});
let remote_branches: Vec<&git2::Branch> = group_branches
.iter()
.filter_map(|branch| match branch {
@ -209,12 +179,6 @@ fn branch_group_to_branch(
.context("Could not get any valid reference in order to build branch stats")?;
// If this was a virtual branch and there was never any remote set, use the virtual branch name as the identity
let identity = identity.unwrap_or(
virtual_branch
.map(|vb| normalize_branch_name(&vb.name))
.transpose()?
.unwrap_or_default(),
);
let last_modified_ms = max(
(repo.find_commit(head)?.time().seconds() * 1000) as u128,
virtual_branch.map_or(0, |x| x.updated_timestamp_ms),
@ -240,7 +204,7 @@ fn branch_group_to_branch(
});
BranchListing {
name: identity,
name: identity.to_owned(),
remotes,
virtual_branch: virtual_branch_reference,
number_of_commits: commits.len(),
@ -251,7 +215,7 @@ fn branch_group_to_branch(
}
} else {
BranchListing {
name: identity,
name: identity.to_owned(),
remotes,
virtual_branch: virtual_branch_reference,
number_of_commits: 0,
@ -264,7 +228,7 @@ fn branch_group_to_branch(
Ok(branch)
}
/// A sum type of a branch that can be a plain git branch or a virtual branch
/// A sum type of branch that can be a plain git branch or a virtual branch
#[allow(clippy::large_enum_variant)]
enum GroupBranch<'a> {
Local(git2::Branch<'a>),
@ -273,8 +237,9 @@ enum GroupBranch<'a> {
}
impl GroupBranch<'_> {
/// A name identifier for the branch. When multiple branches (e.g. virtual, local, reomte) have the same identity,
/// A name identifier for the branch. When multiple branches (e.g. virtual, local, remote) have the same identity,
/// they are grouped together under the same `Branch` entry.
/// `None` means an identity could not be obtained, which makes this branch odd enough to ignore.
fn identity(&self, remotes: &git2::string_array::StringArray) -> Option<String> {
match self {
GroupBranch::Local(branch) => branch.get().given_name(remotes).ok(),
@ -284,8 +249,8 @@ impl GroupBranch<'_> {
let name_from_source = branch.source_refname.as_ref().and_then(|n| n.branch());
let name_from_upstream = branch.upstream.as_ref().map(|n| n.branch());
let rich_name = branch.name.clone();
let rich_name = &normalize_branch_name(&rich_name).ok()?;
let identity = name_from_source.unwrap_or(name_from_upstream.unwrap_or(rich_name));
let rich_name = normalize_branch_name(&rich_name).ok()?;
let identity = name_from_source.unwrap_or(name_from_upstream.unwrap_or(&rich_name));
Some(identity.to_string())
}
}
@ -294,24 +259,23 @@ impl GroupBranch<'_> {
/// Determines if a branch should be listed in the UI.
/// This excludes the target branch as well as gitbutler specific branches.
fn should_list_git_branch(identity: &Option<String>, target: &Target) -> bool {
// Exclude the target branch
if identity == &Some(target.branch.branch().to_owned()) {
fn should_list_git_branch(identity: &str, target: &Target) -> bool {
if identity == target.branch.branch() {
return false;
}
// Exclude gitbutler technical branches (not useful for the user)
if identity == &Some("gitbutler/integration".to_string())
|| identity == &Some("gitbutler/target".to_string())
|| identity == &Some("gitbutler/oplog".to_string())
|| identity == &Some("HEAD".to_string())
{
return false;
}
true
let is_technical = [
"gitbutler/integration",
"gitbutler/target",
"gitbutler/oplog",
"HEAD",
]
.contains(&identity);
!is_technical
}
/// A filter that can be applied to the branch listing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BranchListingFilter {
/// If the value is true, the listing will only include branches that have the same author as the current user.

View File

@ -21,7 +21,7 @@ git init remote
git add . && git commit -m "init"
)
export GITBUTLER_CLI_DATA_DIR=./user/gitbutler/app-data
export GITBUTLER_CLI_DATA_DIR=../user/gitbutler/app-data
git clone remote one-vbranch-on-integration
(cd one-vbranch-on-integration
$CLI project add --switch-to-integration "$(git rev-parse --symbolic-full-name @{u})"
@ -38,3 +38,26 @@ git clone remote one-vbranch-on-integration-one-commit
$CLI branch commit virtual -m "virtual branch change in index and worktree"
)
git clone remote two-vbranches-on-integration-one-applied
(cd two-vbranches-on-integration-one-applied
$CLI project add --switch-to-integration "$(git rev-parse --symbolic-full-name @{u})"
$CLI branch create virtual
echo change >> file
echo in-index > new && git add new
tick
$CLI branch commit virtual -m "commit in initially applied virtual branch"
$CLI branch create --set-default other
echo new > new-file
$CLI branch unapply virtual
)
git clone remote a-vbranch-named-like-target-branch-short-name
(cd a-vbranch-named-like-target-branch-short-name
$CLI project add --switch-to-integration "$(git rev-parse --symbolic-full-name @{u})"
$CLI branch create --set-default main
echo change >> file
echo in-index > new && git add new
tick
$CLI branch commit main -m "virtual branch change in index and worktree"
)

View File

@ -1,5 +1,5 @@
use anyhow::Result;
use gitbutler_branch_actions::{list_branches, Author};
use gitbutler_branch_actions::{list_branches, Author, BranchListingFilter};
use gitbutler_command_context::CommandContext;
#[test]
@ -47,9 +47,87 @@ fn one_vbranch_on_integration_one_commit() -> Result<()> {
Ok(())
}
#[test]
fn two_vbranches_on_integration_one_commit() -> Result<()> {
init_env();
let ctx = project_ctx("two-vbranches-on-integration-one-applied")?;
// let list = list_branches(&ctx, None)?;
// assert_eq!(list.len(), 2, "all branches are listed");
let list = list_branches(
&ctx,
Some(BranchListingFilter {
own_branches: Some(true),
applied: Some(true),
}),
)?;
assert_eq!(list.len(), 1, "only one of these is applied");
let branch = &list[0];
assert_eq!(branch.name, "other");
assert!(branch.remotes.is_empty(), "no remote is associated yet");
assert_eq!(
branch
.virtual_branch
.as_ref()
.map(|v| v.given_name.as_str()),
Some("other")
);
assert_eq!(
branch.number_of_commits, 0,
"this one has only pending changes in the worktree"
);
assert_eq!(branch.authors, []);
assert!(
branch.own_branch,
"empty branches are always considered owned (or something the user is involved in)"
);
let list = list_branches(
&ctx,
Some(BranchListingFilter {
own_branches: Some(true),
applied: Some(false),
}),
)?;
assert_eq!(list.len(), 1, "only one of these is *not* applied");
let branch = &list[0];
assert_eq!(branch.name, "virtual");
assert!(branch.remotes.is_empty(), "no remote is associated yet");
assert_eq!(
branch
.virtual_branch
.as_ref()
.map(|v| v.given_name.as_str()),
Some("virtual")
);
assert_eq!(branch.number_of_commits, 1, "here we have a commit");
assert_eq!(branch.authors, [default_author()]);
assert!(
branch.own_branch,
"the current user (as identified by signature) created the commit"
);
Ok(())
}
#[test]
fn one_feature_branch_and_one_vbranch_on_integration_one_commit() -> Result<()> {
init_env();
let ctx = project_ctx("a-vbranch-named-like-target-branch-short-name")?;
let list = list_branches(&ctx, None)?;
assert_eq!(
list.len(),
0,
"Strange, one is definitely there and it seems valid to name vbranches\
after the target branch but it's filtered out here"
);
Ok(())
}
/// This function affects all tests, but those who care should just call it, assuming
/// they all care for the same default value.
/// If not, they should be placed in their own integration test or run with `#[serial_test:serial]`.
/// For `list_branches` it's needed as it compares the current author with commit authors to determine ownership.
fn init_env() {
for (name, value) in [
("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000"),

View File

@ -14,9 +14,14 @@ pub type BranchId = Id<Branch>;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Branch {
pub id: BranchId,
/// A user-specified name with no restrictions.
/// It will be normalized except to be a valid [ref-name](Branch::refname()) if named `refs/gitbutler/<normalize(name)>`.
pub name: String,
pub notes: String,
/// If set, this means this virtual branch was originally created from `Some(branch)`.
/// It can be *any* branch.
pub source_refname: Option<Refname>,
/// The local tracking branch, holding the state of the remote.
pub upstream: Option<RemoteRefname>,
// upstream_head is the last commit on we've pushed to the upstream branch
#[serde(with = "gitbutler_serde::serde::oid_opt", default)]
@ -54,7 +59,8 @@ pub struct Branch {
/// but the old `applied` property will have remained false.
#[serde(default = "default_true")]
pub applied: bool,
/// This is the new metric for determining whether the branch is in the workspace
/// This is the new metric for determining whether the branch is in the workspace, which means it's applied
/// and its effects are available to the user.
#[serde(default = "default_true")]
pub in_workspace: bool,
#[serde(default)]

View File

@ -129,19 +129,18 @@ impl VirtualBranchesHandle {
&self,
refname: &Refname,
) -> Result<Option<Branch>> {
self.list_all_branches().map(|branches| {
branches.into_iter().find(|branch| {
if branch.in_workspace {
return false;
}
let branches = self.list_all_branches()?;
Ok(branches.into_iter().find(|branch| {
if branch.in_workspace {
return false;
}
if let Some(source_refname) = branch.source_refname.clone() {
return source_refname.to_string() == refname.to_string();
}
if let Some(source_refname) = branch.source_refname.as_ref() {
return source_refname.to_string() == refname.to_string();
}
false
})
})
false
}))
}
/// Gets the state of the given virtual branch.
@ -163,15 +162,9 @@ impl VirtualBranchesHandle {
/// Gets the state of the given virtual branch returning `Some(branch)` or `None`
/// if that branch doesn't exist.
pub fn try_branch_in_workspace(&self, id: BranchId) -> Result<Option<Branch>> {
if let Some(branch) = self.try_branch(id)? {
if branch.in_workspace && !branch.is_old_unapplied() {
Ok(Some(branch))
} else {
Ok(None)
}
} else {
Ok(None)
}
Ok(self
.try_branch(id)?
.filter(|branch| branch.in_workspace && !branch.is_old_unapplied()))
}
/// Gets the state of the given virtual branch returning `Some(branch)` or `None`
@ -181,7 +174,7 @@ impl VirtualBranchesHandle {
Ok(virtual_branches.branches.get(&id).cloned())
}
/// Lists all branches in vbranches.toml
/// Lists all branches in `virtual_branches.toml`.
///
/// Errors if the file cannot be read or written.
pub fn list_all_branches(&self) -> Result<Vec<Branch>> {

View File

@ -38,6 +38,11 @@ pub mod vbranch {
/// The name of the new default virtual branch.
name: String,
},
/// Remove a branch from the workspace.
Unapply {
/// The name of the virtual branch to unapply.
name: String,
},
/// Create a new commit to named virtual branch with all changes currently in the worktree or staging area assigned to it.
Commit {
/// The commit message
@ -48,6 +53,9 @@ pub mod vbranch {
},
/// Create a new virtual branch
Create {
/// Also make this branch the default branch, so it is considered the owner of new edits.
#[clap(short = 'd', long)]
set_default: bool,
/// The name of the virtual branch to create
name: String,
},

View File

@ -12,32 +12,47 @@ pub mod vbranch {
let branches = VirtualBranchesHandle::new(project.gb_dir()).list_all_branches()?;
for vbranch in branches {
println!(
"{active} {id} {name} {upstream}",
"{active} {id} {name} {upstream} {default}",
active = if vbranch.applied { "✔️" } else { "" },
id = vbranch.id,
name = vbranch.name,
upstream = vbranch
.upstream
.map_or_else(Default::default, |b| b.to_string())
.map_or_else(Default::default, |b| b.to_string()),
default = if vbranch.in_workspace { "🌟" } else { "" }
);
}
Ok(())
}
pub fn create(project: Project, branch_name: String) -> Result<()> {
debug_print(VirtualBranchActions.create_virtual_branch(
pub fn unapply(project: Project, branch_name: String) -> Result<()> {
let branch = branch_by_name(&project, &branch_name)?;
debug_print(VirtualBranchActions.convert_to_real_branch(&project, branch.id)?)
}
pub fn create(project: Project, branch_name: String, set_default: bool) -> Result<()> {
let new = VirtualBranchActions.create_virtual_branch(
&project,
&BranchCreateRequest {
name: Some(branch_name),
..Default::default()
},
)?)
)?;
if set_default {
let new = VirtualBranchesHandle::new(project.gb_dir()).get_branch(new)?;
set_default_branch(&project, &new)?;
}
debug_print(new)
}
pub fn set_default(project: Project, branch_name: String) -> Result<()> {
let branch = branch_by_name(&project, &branch_name)?;
set_default_branch(&project, &branch)
}
fn set_default_branch(project: &Project, branch: &Branch) -> Result<()> {
VirtualBranchActions.update_virtual_branch(
&project,
project,
BranchUpdateRequest {
id: branch.id,
name: None,

View File

@ -14,14 +14,17 @@ fn main() -> Result<()> {
args::Subcommands::Branch(vbranch::Platform { cmd }) => {
let project = command::prepare::project_from_path(args.current_dir)?;
match cmd {
Some(vbranch::SubCommands::Unapply { name }) => {
command::vbranch::unapply(project, name)
}
Some(vbranch::SubCommands::SetDefault { name }) => {
command::vbranch::set_default(project, name)
}
Some(vbranch::SubCommands::Commit { message, name }) => {
command::vbranch::commit(project, name, message)
}
Some(vbranch::SubCommands::Create { name }) => {
command::vbranch::create(project, name)
Some(vbranch::SubCommands::Create { set_default, name }) => {
command::vbranch::create(project, name, set_default)
}
None => command::vbranch::list(project),
}

View File

@ -1,2 +1,73 @@
mod repository;
pub use repository::CommandContext;
use anyhow::Result;
use gitbutler_project::Project;
pub struct CommandContext {
/// The git repository of the `project` itself.
git_repository: git2::Repository,
/// Metadata about the project, typically stored with GitButler application data.
project: Project,
}
impl CommandContext {
/// Open the repository identified by `project` and perform some checks.
pub fn open(project: &Project) -> Result<Self> {
let repo = git2::Repository::open(&project.path)?;
// XXX(qix-): This is a temporary measure to disable GC on the project repository.
// XXX(qix-): We do this because the internal repository we use to store the "virtual"
// XXX(qix-): refs and information use Git's alternative-objects mechanism to refer
// XXX(qix-): to the project repository's objects. However, the project repository
// XXX(qix-): has no knowledge of these refs, and will GC them away (usually after
// XXX(qix-): about 2 weeks) which will corrupt the internal repository.
// XXX(qix-):
// XXX(qix-): We will ultimately move away from an internal repository for a variety
// XXX(qix-): of reasons, but for now, this is a simple, short-term solution that we
// XXX(qix-): can clean up later on. We're aware this isn't ideal.
if let Ok(config) = repo.config().as_mut() {
let should_set = match config.get_bool("gitbutler.didSetPrune") {
Ok(false) => true,
Ok(true) => false,
Err(err) => {
tracing::warn!(
"failed to get gitbutler.didSetPrune for repository at {}; cannot disable gc: {}",
project.path.display(),
err
);
false
}
};
if should_set {
if let Err(error) = config
.set_str("gc.pruneExpire", "never")
.and_then(|()| config.set_bool("gitbutler.didSetPrune", true))
{
tracing::warn!(
"failed to set gc.auto to false for repository at {}; cannot disable gc: {}",
project.path.display(),
error
);
}
}
} else {
tracing::warn!(
"failed to get config for repository at {}; cannot disable gc",
project.path.display()
);
}
Ok(Self {
git_repository: repo,
project: project.clone(),
})
}
pub fn project(&self) -> &Project {
&self.project
}
/// Return the [`project`](Self::project) repository.
pub fn repository(&self) -> &git2::Repository {
&self.git_repository
}
}

View File

@ -1,73 +0,0 @@
use anyhow::Result;
use gitbutler_project::Project;
pub struct CommandContext {
git_repository: git2::Repository,
project: Project,
}
impl CommandContext {
pub fn open(project: &Project) -> Result<Self> {
let repo = git2::Repository::open(&project.path)?;
// XXX(qix-): This is a temporary measure to disable GC on the project repository.
// XXX(qix-): We do this because the internal repository we use to store the "virtual"
// XXX(qix-): refs and information use Git's alternative-objects mechanism to refer
// XXX(qix-): to the project repository's objects. However, the project repository
// XXX(qix-): has no knowledge of these refs, and will GC them away (usually after
// XXX(qix-): about 2 weeks) which will corrupt the internal repository.
// XXX(qix-):
// XXX(qix-): We will ultimately move away from an internal repository for a variety
// XXX(qix-): of reasons, but for now, this is a simple, short-term solution that we
// XXX(qix-): can clean up later on. We're aware this isn't ideal.
if let Ok(config) = repo.config().as_mut() {
let should_set = match config.get_bool("gitbutler.didSetPrune") {
Ok(false) => true,
Ok(true) => false,
Err(err) => {
tracing::warn!(
"failed to get gitbutler.didSetPrune for repository at {}; cannot disable gc: {}",
project.path.display(),
err
);
false
}
};
if should_set {
if let Err(error) = config
.set_str("gc.pruneExpire", "never")
.and_then(|()| config.set_bool("gitbutler.didSetPrune", true))
{
tracing::warn!(
"failed to set gc.auto to false for repository at {}; cannot disable gc: {}",
project.path.display(),
error
);
}
}
} else {
tracing::warn!(
"failed to get config for repository at {}; cannot disable gc",
project.path.display()
);
}
Ok(Self {
git_repository: repo,
project: project.clone(),
})
}
pub fn set_project(&mut self, project: &Project) {
self.project = project.clone();
}
pub fn project(&self) -> &Project {
&self.project
}
pub fn repository(&self) -> &git2::Repository {
&self.git_repository
}
}

View File

@ -67,6 +67,11 @@ impl FromStr for Refname {
// Alternatively, `git2` also has support for respecting refspecs.
let value = value.strip_prefix("refs/remotes/").unwrap();
// TODO(ST): the remote name cannot be assumed to *not* contain slashes, but the refspec
// would be '+refs/heads/*:refs/remotes/multi/slash/remote/*' which allows to extract
// the right remote name. However, for that we need the local branch, which
// has the remote name configured in plain text. Technically, it doesn't even have
// to match the refspec, so this abstraction is very dangerous.
if let Some((remote, branch)) = value.split_once('/') {
Ok(Self {
remote: remote.to_string(),

View File

@ -14,7 +14,6 @@ gitbutler-sync.workspace = true
gitbutler-oplog.workspace = true
thiserror.workspace = true
anyhow = "1.0.86"
futures.workspace = true
tokio = { workspace = true, features = ["macros"] }
tokio-util = "0.7.11"
tracing = "0.1.40"

View File

@ -95,9 +95,7 @@ pub fn watch_in_background(
// across await points. Further, there is a fair share of `sync` IO happening
// as well, so nothing can really be done here.
task::spawn_blocking(move || {
futures::executor::block_on(async move {
handler.handle(event).ok();
});
handler.handle(event).ok();
});
Ok(())
};