mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-25 18:42:08 +03:00
more tests specific to how branches are discovered
This commit is contained in:
parent
182381dd79
commit
2d82b6e038
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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)?;
|
||||
|
@ -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,63 +19,47 @@ 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)
|
||||
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() {
|
||||
git_branches.push(GroupBranch::Remote(upstream));
|
||||
branches.push(GroupBranch::Remote(upstream));
|
||||
}
|
||||
}
|
||||
git_branches.push(GroupBranch::Local(branch));
|
||||
branches.push(GroupBranch::Local(branch));
|
||||
}
|
||||
git2::BranchType::Remote => {
|
||||
git_branches.push(GroupBranch::Remote(branch));
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
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![];
|
||||
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);
|
||||
@ -90,79 +71,68 @@ fn matches_all(branch: &BranchListing, filter: &Option<BranchListingFilter>) ->
|
||||
conditions.push(own == branch.own_branch);
|
||||
}
|
||||
return conditions.iter().all(|&x| x);
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
match res {
|
||||
Ok(branch_entry) => Some(branch_entry),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to process branch group {:?} to branch entry: {:?}",
|
||||
"Failed to process branch group {:?} to branch entry: {}",
|
||||
identity,
|
||||
branch_entry
|
||||
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 {
|
||||
let virtual_branch = group_branches.iter().find_map(|branch| match branch {
|
||||
GroupBranch::Virtual(vb) => Some(vb),
|
||||
_ => None,
|
||||
})
|
||||
.next();
|
||||
});
|
||||
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.
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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"),
|
||||
|
@ -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)]
|
||||
|
@ -129,19 +129,18 @@ impl VirtualBranchesHandle {
|
||||
&self,
|
||||
refname: &Refname,
|
||||
) -> Result<Option<Branch>> {
|
||||
self.list_all_branches().map(|branches| {
|
||||
branches.into_iter().find(|branch| {
|
||||
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() {
|
||||
if let Some(source_refname) = branch.source_refname.as_ref() {
|
||||
return source_refname.to_string() == refname.to_string();
|
||||
}
|
||||
|
||||
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>> {
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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(),
|
||||
|
@ -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"
|
||||
|
@ -95,10 +95,8 @@ 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();
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user