mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-30 01:17:37 +03:00
Merge pull request #4509 from Byron/git2-to-gix
This commit is contained in:
commit
2d0fa99452
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,8 @@
|
||||
# will have compiled rust files and executables
|
||||
target/
|
||||
generated-archives/
|
||||
generated-do-not-edit/
|
||||
|
||||
|
||||
# editors
|
||||
.idea
|
||||
|
186
Cargo.lock
generated
186
Cargo.lock
generated
@ -847,6 +847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -861,6 +862,18 @@ dependencies = [
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.1"
|
||||
@ -1033,6 +1046,21 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
|
||||
dependencies = [
|
||||
"crc-catalog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc-catalog"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
@ -1396,17 +1424,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
@ -1417,16 +1434,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
@ -1600,6 +1607,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@ -2045,9 +2058,15 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dirs-next",
|
||||
"futures",
|
||||
"gitbutler-branch",
|
||||
"gitbutler-branch-actions",
|
||||
"gitbutler-diff",
|
||||
"gitbutler-oplog",
|
||||
"gitbutler-project",
|
||||
"pager",
|
||||
"gitbutler-reference",
|
||||
"gix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2247,6 +2266,7 @@ dependencies = [
|
||||
"gitbutler-time",
|
||||
"gitbutler-url",
|
||||
"gitbutler-user",
|
||||
"gix",
|
||||
"log",
|
||||
"resolve-path",
|
||||
"serde",
|
||||
@ -2376,8 +2396,10 @@ dependencies = [
|
||||
"gitbutler-storage",
|
||||
"gitbutler-url",
|
||||
"gitbutler-user",
|
||||
"gix-testtools",
|
||||
"keyring",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
@ -2448,7 +2470,7 @@ dependencies = [
|
||||
"gix-date",
|
||||
"gix-diff",
|
||||
"gix-dir",
|
||||
"gix-discover",
|
||||
"gix-discover 0.33.0",
|
||||
"gix-features",
|
||||
"gix-filter",
|
||||
"gix-fs",
|
||||
@ -2466,7 +2488,7 @@ dependencies = [
|
||||
"gix-path",
|
||||
"gix-pathspec",
|
||||
"gix-prompt",
|
||||
"gix-ref",
|
||||
"gix-ref 0.45.0",
|
||||
"gix-refspec",
|
||||
"gix-revision",
|
||||
"gix-revwalk",
|
||||
@ -2570,7 +2592,7 @@ dependencies = [
|
||||
"gix-features",
|
||||
"gix-glob",
|
||||
"gix-path",
|
||||
"gix-ref",
|
||||
"gix-ref 0.45.0",
|
||||
"gix-sec",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
@ -2641,7 +2663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c975679aa00dd2d757bfd3ddb232e8a188c0094c3306400575a0813858b1365"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-discover",
|
||||
"gix-discover 0.33.0",
|
||||
"gix-fs",
|
||||
"gix-ignore",
|
||||
"gix-index",
|
||||
@ -2654,6 +2676,22 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-discover"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc27c699b63da66b50d50c00668bc0b7e90c3a382ef302865e891559935f3dbf"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"dunce",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-path",
|
||||
"gix-ref 0.44.1",
|
||||
"gix-sec",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-discover"
|
||||
version = "0.33.0"
|
||||
@ -2665,7 +2703,7 @@ dependencies = [
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-path",
|
||||
"gix-ref",
|
||||
"gix-ref 0.45.0",
|
||||
"gix-sec",
|
||||
"thiserror",
|
||||
]
|
||||
@ -2956,6 +2994,28 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-ref"
|
||||
version = "0.44.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3394a2997e5bc6b22ebc1e1a87b41eeefbcfcff3dbfa7c4bd73cb0ac8f1f3e2e"
|
||||
dependencies = [
|
||||
"gix-actor",
|
||||
"gix-date",
|
||||
"gix-features",
|
||||
"gix-fs",
|
||||
"gix-hash",
|
||||
"gix-lock",
|
||||
"gix-object",
|
||||
"gix-path",
|
||||
"gix-tempfile",
|
||||
"gix-utils",
|
||||
"gix-validate",
|
||||
"memmap2",
|
||||
"thiserror",
|
||||
"winnow 0.6.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-ref"
|
||||
version = "0.45.0"
|
||||
@ -3057,9 +3117,37 @@ dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"signal-hook",
|
||||
"signal-hook-registry",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-testtools"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33fd7cd1816d78db635003c9e3fc667a1671689c678de2b92ce7c71ed2d58686"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"crc",
|
||||
"fastrand 2.1.0",
|
||||
"fs_extra",
|
||||
"gix-discover 0.32.0",
|
||||
"gix-fs",
|
||||
"gix-ignore",
|
||||
"gix-index",
|
||||
"gix-lock",
|
||||
"gix-tempfile",
|
||||
"gix-worktree",
|
||||
"io-close",
|
||||
"is_ci",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.3",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"winnow 0.6.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix-trace"
|
||||
version = "0.1.9"
|
||||
@ -3782,6 +3870,16 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-close"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
@ -3818,6 +3916,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_ci"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
@ -4579,9 +4683,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.66"
|
||||
version = "0.10.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
@ -4664,16 +4768,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "pager"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2599211a5c97fbbb1061d3dc751fa15f404927e4846e07c643287d6d1f462880"
|
||||
dependencies = [
|
||||
"errno 0.2.8",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.15.10"
|
||||
@ -5666,7 +5760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"errno 0.3.9",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys 0.3.8",
|
||||
@ -5680,7 +5774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno 0.3.9",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"windows-sys 0.52.0",
|
||||
@ -6072,6 +6166,16 @@ dependencies = [
|
||||
"dirs 5.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.2"
|
||||
|
@ -47,6 +47,7 @@ keyring = "2.3.3"
|
||||
anyhow = "1.0.86"
|
||||
fslock = "0.2.1"
|
||||
parking_lot = "0.12.3"
|
||||
futures = "0.3.30"
|
||||
|
||||
gitbutler-id = { path = "crates/gitbutler-id" }
|
||||
gitbutler-git = { path = "crates/gitbutler-git" }
|
||||
|
@ -32,17 +32,13 @@ regex = "1.10"
|
||||
git2-hooks = "0.3"
|
||||
url = { version = "2.5.2", features = ["serde"] }
|
||||
md5 = "0.7.0"
|
||||
futures = "0.3"
|
||||
futures.workspace = true
|
||||
itertools = "0.13"
|
||||
gitbutler-command-context.workspace = true
|
||||
gitbutler-project.workspace = true
|
||||
urlencoding = "2.1.3"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
|
||||
[[test]]
|
||||
name = "virtual"
|
||||
path = "tests/virtual_branches/mod.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
once_cell = "1.19"
|
||||
pretty_assertions = "1.4"
|
||||
|
@ -13,12 +13,12 @@ use gitbutler_branch::Target;
|
||||
use gitbutler_branch::VirtualBranchesHandle;
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
|
||||
use crate::{VirtualBranch, VirtualBranchesExt};
|
||||
use gitbutler_reference::normalize_branch_name;
|
||||
use gitbutler_repo::RepoActionsExt;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{VirtualBranch, VirtualBranchesExt};
|
||||
|
||||
/// Returns a list of branches associated with this project.
|
||||
// TODO: Implement pagination for this thing
|
||||
// TODO: The results should be sortedb by updated_at
|
||||
@ -70,7 +70,7 @@ pub fn list_branches(
|
||||
.list_all_branches()?
|
||||
.into_iter();
|
||||
|
||||
let branches = combine_branches(git_branches, virtual_branches, ctx.repo(), &vb_handle)?;
|
||||
let branches = combine_branches(git_branches, virtual_branches, ctx, &vb_handle)?;
|
||||
// Apply the filter
|
||||
let branches: Vec<BranchListing> = branches
|
||||
.into_iter()
|
||||
@ -101,9 +101,10 @@ fn matches_all(branch: &BranchListing, filter: &Option<BranchListingFilter>) ->
|
||||
fn combine_branches(
|
||||
mut group_branches: Vec<GroupBranch>,
|
||||
virtual_branches: impl Iterator<Item = GitButlerBranch>,
|
||||
repo: &git2::Repository,
|
||||
ctx: &ProjectRepository,
|
||||
vb_handle: &VirtualBranchesHandle,
|
||||
) -> Result<Vec<BranchListing>> {
|
||||
let repo = ctx.repo();
|
||||
for branch in virtual_branches {
|
||||
group_branches.push(GroupBranch::Virtual(branch));
|
||||
}
|
||||
@ -123,11 +124,7 @@ fn combine_branches(
|
||||
groups.insert(identity, vec![branch]);
|
||||
}
|
||||
}
|
||||
let config = repo.config()?;
|
||||
let local_author = Author {
|
||||
name: config.get_string("user.name").ok(),
|
||||
email: config.get_string("user.email").ok(),
|
||||
};
|
||||
let (local_author, _committer) = ctx.signatures()?;
|
||||
|
||||
// Convert to Branch entries for the API response, filtering out any errors
|
||||
let branches: Vec<BranchListing> = groups
|
||||
@ -157,7 +154,7 @@ fn branch_group_to_branch(
|
||||
identity: Option<String>,
|
||||
group_branches: Vec<&GroupBranch>,
|
||||
repo: &git2::Repository,
|
||||
local_author: &Author,
|
||||
local_author: &git2::Signature,
|
||||
) -> Result<BranchListing> {
|
||||
let virtual_branch = group_branches
|
||||
.iter()
|
||||
@ -233,13 +230,13 @@ fn branch_group_to_branch(
|
||||
commits.push(commit);
|
||||
}
|
||||
|
||||
let mut own_branch = commits
|
||||
.iter()
|
||||
.any(|commit| local_author == &commit.author().into());
|
||||
// If there are no commits (i.e. virtual branch only) it is considered the users own
|
||||
if commits.is_empty() {
|
||||
own_branch = true;
|
||||
}
|
||||
let own_branch = commits.is_empty()
|
||||
|| commits.iter().any(|commit| {
|
||||
let commit_author = commit.author();
|
||||
local_author.name_bytes() == commit_author.name_bytes()
|
||||
&& local_author.email_bytes() == commit_author.email_bytes()
|
||||
});
|
||||
|
||||
let last_modified_ms = max(
|
||||
last_commit_time_ms as u128,
|
||||
|
@ -42,4 +42,4 @@ mod branch;
|
||||
mod commit;
|
||||
mod hunk;
|
||||
|
||||
pub use branch::{list_branches, BranchListing, BranchListingFilter};
|
||||
pub use branch::{list_branches, Author, BranchListing, BranchListingFilter};
|
||||
|
@ -61,6 +61,8 @@ pub struct VirtualBranch {
|
||||
pub upstream: Option<RemoteBranch>, // the upstream branch where this branch pushes to, if any
|
||||
pub upstream_name: Option<String>, // the upstream branch where this branch will push to on next push
|
||||
pub base_current: bool, // is this vbranch based on the current base branch? if false, this needs to be manually merged with conflicts
|
||||
/// The hunks (as `[(file, [hunks])]`) which are uncommitted but assigned to this branch.
|
||||
/// This makes them committable.
|
||||
pub ownership: BranchOwnershipClaims,
|
||||
pub updated_at: u128,
|
||||
pub selected_for_changes: bool,
|
||||
|
39
crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh
vendored
Normal file
39
crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu -o pipefail
|
||||
CLI=${1:?The first argument is the GitButler CLI}
|
||||
|
||||
git init remote
|
||||
(cd remote
|
||||
echo first > file
|
||||
git add . && git commit -m "init"
|
||||
)
|
||||
|
||||
git clone remote single-branch-no-vbranch
|
||||
|
||||
git clone remote single-branch-no-vbranch-one-commit
|
||||
(cd single-branch-no-vbranch-one-commit
|
||||
echo change >> file && git add . && git commit -m "local change"
|
||||
)
|
||||
|
||||
git clone remote single-branch-no-vbranch-multi-remote
|
||||
(cd single-branch-no-vbranch-multi-remote
|
||||
git remote add other-origin ../remote
|
||||
git fetch other-origin
|
||||
)
|
||||
|
||||
export GITBUTLER_CLI_DATA_DIR=./git/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})"
|
||||
$CLI branch create virtual
|
||||
)
|
||||
|
||||
git clone remote one-vbranch-on-integration-one-commit
|
||||
(cd one-vbranch-on-integration-one-commit
|
||||
$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
|
||||
$CLI branch commit virtual -m "virtual branch change in index and worktree"
|
||||
)
|
||||
|
133
crates/gitbutler-branch-actions/tests/virtual_branches/list.rs
Normal file
133
crates/gitbutler-branch-actions/tests/virtual_branches/list.rs
Normal file
@ -0,0 +1,133 @@
|
||||
use anyhow::Result;
|
||||
use gitbutler_branch_actions::{list_branches, Author};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main", "short names are used");
|
||||
assert_eq!(branch.remotes, ["origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(branch.number_of_commits, 0);
|
||||
assert_eq!(
|
||||
branch.authors,
|
||||
[],
|
||||
"there is no local commit, so no authors are known"
|
||||
);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch_multiple_remotes() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch-multi-remote")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main");
|
||||
assert_eq!(branch.remotes, ["other-origin", "origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(branch.number_of_commits, 0);
|
||||
assert_eq!(branch.authors, []);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_main_single_branch_no_vbranch_one_commit() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("single-branch-no-vbranch-one-commit")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "main");
|
||||
assert_eq!(branch.remotes, ["origin"]);
|
||||
assert_eq!(branch.virtual_branch, None);
|
||||
assert_eq!(
|
||||
branch.number_of_commits, 0,
|
||||
"local-only commits aren't detected"
|
||||
);
|
||||
assert_eq!(
|
||||
branch.authors,
|
||||
[],
|
||||
"and thus there is no ownership information"
|
||||
);
|
||||
assert!(branch.own_branch);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_vbranch_on_integration() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("one-vbranch-on-integration")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
let branch = &list[0];
|
||||
assert_eq!(branch.name, "virtual");
|
||||
assert!(branch.remotes.is_empty(), "no remote is associated yet");
|
||||
assert_eq!(branch.number_of_commits, 0);
|
||||
assert_eq!(
|
||||
branch
|
||||
.virtual_branch
|
||||
.as_ref()
|
||||
.map(|v| v.given_name.as_str()),
|
||||
Some("virtual")
|
||||
);
|
||||
assert_eq!(branch.authors, []);
|
||||
assert!(branch.own_branch, "zero commits means user owns the branch");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_vbranch_on_integration_one_commit() -> Result<()> {
|
||||
init_env();
|
||||
let list = list_branches(&project_ctx("one-vbranch-on-integration-one-commit")?, None)?;
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
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, "one commit created on vbranch");
|
||||
assert_eq!(branch.authors, [default_author()]);
|
||||
assert!(branch.own_branch);
|
||||
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]`.
|
||||
fn init_env() {
|
||||
for (name, value) in [
|
||||
("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000"),
|
||||
("GIT_AUTHOR_EMAIL", "author@example.com"),
|
||||
("GIT_AUTHOR_NAME", "author"),
|
||||
("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000"),
|
||||
("GIT_COMMITTER_EMAIL", "committer@example.com"),
|
||||
("GIT_COMMITTER_NAME", "committer"),
|
||||
] {
|
||||
std::env::set_var(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn default_author() -> Author {
|
||||
Author {
|
||||
name: Some("author".into()),
|
||||
email: Some("author@example.com".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn project_ctx(name: &str) -> anyhow::Result<ProjectRepository> {
|
||||
gitbutler_testsupport::read_only::fixture("for-listing.sh", name)
|
||||
}
|
@ -65,6 +65,7 @@ mod create_virtual_branch_from_branch;
|
||||
mod delete_virtual_branch;
|
||||
mod init;
|
||||
mod insert_blank_commit;
|
||||
mod list;
|
||||
mod move_commit_file;
|
||||
mod move_commit_to_vbranch;
|
||||
mod oplog;
|
||||
|
@ -12,9 +12,13 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
gitbutler-oplog.workspace = true
|
||||
gitbutler-project.workspace = true
|
||||
clap = "4.5.9"
|
||||
gitbutler-reference.workspace = true
|
||||
gitbutler-branch-actions.workspace = true
|
||||
gitbutler-branch.workspace = true
|
||||
gitbutler-diff.workspace = true
|
||||
gix.workspace = true
|
||||
futures.workspace = true
|
||||
dirs-next = "2.0.0"
|
||||
clap = { version = "4.5.9", features = ["derive", "env"] }
|
||||
anyhow = "1.0.86"
|
||||
chrono = "0.4.10"
|
||||
|
||||
[target."cfg(unix)".dependencies]
|
||||
pager = "0.16.1"
|
||||
|
123
crates/gitbutler-cli/src/args.rs
Normal file
123
crates/gitbutler-cli/src/args.rs
Normal file
@ -0,0 +1,123 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(name = "gitbutler-cli", about = "A CLI for GitButler", version = option_env!("GIX_VERSION"))]
|
||||
pub struct Args {
|
||||
/// Run as if gitbutler-cli was started in PATH instead of the current working directory.
|
||||
#[clap(short = 'C', long, default_value = ".", value_name = "PATH")]
|
||||
pub current_dir: PathBuf,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub cmd: Subcommands,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum Subcommands {
|
||||
/// List and manipulate virtual branches.
|
||||
#[clap(visible_alias = "branches")]
|
||||
Branch(vbranch::Platform),
|
||||
/// List and manipulate projects.
|
||||
#[clap(visible_alias = "projects")]
|
||||
Project(project::Platform),
|
||||
/// List and restore snapshots.
|
||||
#[clap(visible_alias = "snapshots")]
|
||||
Snapshot(snapshot::Platform),
|
||||
}
|
||||
|
||||
pub mod vbranch {
|
||||
#[derive(Debug, clap::Parser)]
|
||||
pub struct Platform {
|
||||
#[clap(subcommand)]
|
||||
pub cmd: Option<SubCommands>,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum SubCommands {
|
||||
/// Make the named branch the default so all worktree or index changes are associated with it automatically.
|
||||
SetDefault {
|
||||
/// The name of the new default virtual branch.
|
||||
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
|
||||
#[clap(short = 'm', long)]
|
||||
message: String,
|
||||
/// The name of the virtual to commit all staged and unstaged changes to.
|
||||
name: String,
|
||||
},
|
||||
/// Create a new virtual branch
|
||||
Create {
|
||||
/// The name of the virtual branch to create
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub mod project {
|
||||
use gitbutler_reference::RemoteRefname;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
pub struct Platform {
|
||||
/// The location of the directory to contain app data.
|
||||
///
|
||||
/// Defaults to the standard location on this platform if unset.
|
||||
#[clap(short = 'd', long, env = "GITBUTLER_CLI_DATA_DIR")]
|
||||
pub app_data_dir: Option<PathBuf>,
|
||||
/// A suffix like `dev` to refer to projects of the development version of the application.
|
||||
///
|
||||
/// The production version is used if unset.
|
||||
#[clap(short = 's', long)]
|
||||
pub app_suffix: Option<String>,
|
||||
#[clap(subcommand)]
|
||||
pub cmd: Option<SubCommands>,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum SubCommands {
|
||||
/// Add the given Git repository as project for use with GitButler.
|
||||
Add {
|
||||
/// The long name of the remote reference to track, like `refs/remotes/origin/main`,
|
||||
/// when switching to the integration branch.
|
||||
#[clap(short = 's', long)]
|
||||
switch_to_integration: Option<RemoteRefname>,
|
||||
/// The path at which the repository worktree is located.
|
||||
#[clap(default_value = ".", value_name = "REPOSITORY")]
|
||||
path: PathBuf,
|
||||
},
|
||||
/// Switch back to the integration branch for use of virtual branches.
|
||||
SwitchToIntegration {
|
||||
/// The long name of the remote reference to track, like `refs/remotes/origin/main`.
|
||||
remote_ref_name: RemoteRefname,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub mod snapshot {
|
||||
#[derive(Debug, clap::Parser)]
|
||||
pub struct Platform {
|
||||
#[clap(subcommand)]
|
||||
pub cmd: Option<SubCommands>,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum SubCommands {
|
||||
/// Restores the state of the working direcory as well as virtual branches to a given snapshot.
|
||||
Restore {
|
||||
/// The snapshot to restore
|
||||
snapshot_id: String,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn clap() {
|
||||
use clap::CommandFactory;
|
||||
Args::command().debug_assert();
|
||||
}
|
||||
}
|
237
crates/gitbutler-cli/src/command.rs
Normal file
237
crates/gitbutler-cli/src/command.rs
Normal file
@ -0,0 +1,237 @@
|
||||
pub mod vbranch {
|
||||
use crate::command::debug_print;
|
||||
use anyhow::bail;
|
||||
use anyhow::Result;
|
||||
use futures::executor::block_on;
|
||||
use gitbutler_branch::{
|
||||
Branch, BranchCreateRequest, BranchUpdateRequest, VirtualBranchesHandle,
|
||||
};
|
||||
use gitbutler_branch_actions::VirtualBranchActions;
|
||||
use gitbutler_project::Project;
|
||||
|
||||
pub fn list(project: Project) -> Result<()> {
|
||||
let branches = VirtualBranchesHandle::new(project.gb_dir()).list_all_branches()?;
|
||||
for vbranch in branches {
|
||||
println!(
|
||||
"{active} {id} {name} {upstream}",
|
||||
active = if vbranch.applied { "✔️" } else { "⛌" },
|
||||
id = vbranch.id,
|
||||
name = vbranch.name,
|
||||
upstream = vbranch
|
||||
.upstream
|
||||
.map_or_else(Default::default, |b| b.to_string())
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create(project: Project, branch_name: String) -> Result<()> {
|
||||
debug_print(block_on(VirtualBranchActions.create_virtual_branch(
|
||||
&project,
|
||||
&BranchCreateRequest {
|
||||
name: Some(branch_name),
|
||||
..Default::default()
|
||||
},
|
||||
))?)
|
||||
}
|
||||
|
||||
pub fn set_default(project: Project, branch_name: String) -> Result<()> {
|
||||
let branch = branch_by_name(&project, &branch_name)?;
|
||||
block_on(VirtualBranchActions.update_virtual_branch(
|
||||
&project,
|
||||
BranchUpdateRequest {
|
||||
id: branch.id,
|
||||
name: None,
|
||||
notes: None,
|
||||
ownership: None,
|
||||
order: None,
|
||||
upstream: None,
|
||||
selected_for_changes: Some(true),
|
||||
allow_rebasing: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn commit(project: Project, branch_name: String, message: String) -> Result<()> {
|
||||
let branch = branch_by_name(&project, &branch_name)?;
|
||||
let (info, skipped) = block_on(VirtualBranchActions.list_virtual_branches(&project))?;
|
||||
|
||||
if !skipped.is_empty() {
|
||||
eprintln!(
|
||||
"{} files could not be processed (binary or large size)",
|
||||
skipped.len()
|
||||
)
|
||||
}
|
||||
|
||||
let populated_branch = info
|
||||
.iter()
|
||||
.find(|b| b.id == branch.id)
|
||||
.expect("A populated branch exists for a branch we can list");
|
||||
if populated_branch.ownership.claims.is_empty() {
|
||||
bail!(
|
||||
"Branch '{branch_name}' has no change to commit{hint}",
|
||||
hint = {
|
||||
let candidate_names = info
|
||||
.iter()
|
||||
.filter_map(|b| (!b.ownership.claims.is_empty()).then_some(b.name.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
let mut candidates = candidate_names.join(", ");
|
||||
if !candidate_names.is_empty() {
|
||||
candidates = format!(
|
||||
". {candidates} {have} changes.",
|
||||
have = if candidate_names.len() == 1 {
|
||||
"has"
|
||||
} else {
|
||||
"have"
|
||||
}
|
||||
)
|
||||
};
|
||||
candidates
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let run_hooks = false;
|
||||
debug_print(block_on(VirtualBranchActions.create_commit(
|
||||
&project,
|
||||
branch.id,
|
||||
&message,
|
||||
Some(&populated_branch.ownership),
|
||||
run_hooks,
|
||||
))?)
|
||||
}
|
||||
|
||||
pub fn branch_by_name(project: &Project, name: &str) -> Result<Branch> {
|
||||
let mut found: Vec<_> = VirtualBranchesHandle::new(project.gb_dir())
|
||||
.list_all_branches()?
|
||||
.into_iter()
|
||||
.filter(|b| b.name == name)
|
||||
.collect();
|
||||
if found.is_empty() {
|
||||
bail!("No virtual branch named '{name}'");
|
||||
} else if found.len() > 1 {
|
||||
bail!("Found more than one virtual branch named '{name}'");
|
||||
}
|
||||
Ok(found.pop().expect("present"))
|
||||
}
|
||||
}
|
||||
|
||||
pub mod project {
|
||||
use crate::command::debug_print;
|
||||
use anyhow::{Context, Result};
|
||||
use futures::executor::block_on;
|
||||
use gitbutler_branch_actions::VirtualBranchActions;
|
||||
use gitbutler_project::Project;
|
||||
use gitbutler_reference::RemoteRefname;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn list(ctrl: gitbutler_project::Controller) -> Result<()> {
|
||||
for project in ctrl.list()? {
|
||||
println!(
|
||||
"{id} {name} {path}",
|
||||
id = project.id,
|
||||
name = project.title,
|
||||
path = project.path.display()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add(
|
||||
ctrl: gitbutler_project::Controller,
|
||||
path: PathBuf,
|
||||
refname: Option<RemoteRefname>,
|
||||
) -> Result<()> {
|
||||
let path = gix::discover(path)?
|
||||
.work_dir()
|
||||
.context("Only non-bare repositories can be added")?
|
||||
.to_owned()
|
||||
.canonicalize()?;
|
||||
let project = ctrl.add(path)?;
|
||||
if let Some(refname) = refname {
|
||||
block_on(VirtualBranchActions.set_base_branch(&project, &refname))?;
|
||||
};
|
||||
debug_print(project)
|
||||
}
|
||||
|
||||
pub fn switch_to_integration(project: Project, refname: RemoteRefname) -> Result<()> {
|
||||
debug_print(block_on(
|
||||
VirtualBranchActions.set_base_branch(&project, &refname),
|
||||
)?)
|
||||
}
|
||||
}
|
||||
pub mod snapshot {
|
||||
use anyhow::Result;
|
||||
use gitbutler_oplog::OplogExt;
|
||||
use gitbutler_project::Project;
|
||||
|
||||
pub fn list(project: Project) -> Result<()> {
|
||||
let snapshots = project.list_snapshots(100, None)?;
|
||||
for snapshot in snapshots {
|
||||
let ts = chrono::DateTime::from_timestamp(snapshot.created_at.seconds(), 0);
|
||||
let details = snapshot.details;
|
||||
if let (Some(ts), Some(details)) = (ts, details) {
|
||||
println!("{} {} {}", ts, snapshot.commit_id, details.operation);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore(project: Project, snapshot_id: String) -> Result<()> {
|
||||
let _guard = project.try_exclusive_access()?;
|
||||
project.restore_snapshot(snapshot_id.parse()?)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod prepare {
|
||||
use anyhow::{bail, Context};
|
||||
use gitbutler_project::Project;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn project_from_path(path: PathBuf) -> anyhow::Result<Project> {
|
||||
let worktree_dir = gix::discover(path)?
|
||||
.work_dir()
|
||||
.context("Bare repositories aren't supported")?
|
||||
.to_owned();
|
||||
Ok(Project {
|
||||
path: worktree_dir,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn project_controller(
|
||||
app_suffix: Option<String>,
|
||||
app_data_dir: Option<PathBuf>,
|
||||
) -> anyhow::Result<gitbutler_project::Controller> {
|
||||
let path = if let Some(dir) = app_data_dir {
|
||||
std::fs::create_dir_all(&dir)
|
||||
.context("Failed to assure the designated data-dir exists")?;
|
||||
dir
|
||||
} else {
|
||||
dirs_next::data_dir()
|
||||
.map(|dir| {
|
||||
dir.join(format!(
|
||||
"com.gitbutler.app{}",
|
||||
app_suffix
|
||||
.map(|mut suffix| {
|
||||
suffix.insert(0, '.');
|
||||
suffix
|
||||
})
|
||||
.unwrap_or_default()
|
||||
))
|
||||
})
|
||||
.context("no data-directory available on this platform")?
|
||||
};
|
||||
if !path.is_dir() {
|
||||
bail!("Path '{}' must be a valid directory", path.display());
|
||||
}
|
||||
eprintln!("Using projects from '{}'", path.display());
|
||||
Ok(gitbutler_project::Controller::from_path(path))
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_print(this: impl std::fmt::Debug) -> anyhow::Result<()> {
|
||||
eprintln!("{:#?}", this);
|
||||
Ok(())
|
||||
}
|
@ -1,76 +1,59 @@
|
||||
use anyhow::Result;
|
||||
use gitbutler_oplog::OplogExt;
|
||||
|
||||
use clap::{arg, Command};
|
||||
use gitbutler_project::Project;
|
||||
#[cfg(not(windows))]
|
||||
use pager::Pager;
|
||||
mod args;
|
||||
use crate::args::{project, snapshot, vbranch};
|
||||
use args::Args;
|
||||
|
||||
fn cli() -> Command {
|
||||
Command::new("gitbutler-cli")
|
||||
.about("A CLI tool for GitButler")
|
||||
.arg(arg!(-C <path> "Run as if gitbutler-cli was started in <path> instead of the current working directory."))
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.allow_external_subcommands(true)
|
||||
.subcommand(
|
||||
Command::new("snapshot")
|
||||
.about("List and restore snapshots.")
|
||||
.subcommand(Command::new("restore")
|
||||
.about("Restores the state of the working direcory as well as virtual branches to a given snapshot.")
|
||||
.arg(arg!(<SNAPSHOT_ID> "The snapshot to restore"))),
|
||||
)
|
||||
}
|
||||
mod command;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(not(windows))]
|
||||
Pager::new().setup();
|
||||
let matches = cli().get_matches();
|
||||
let args: Args = clap::Parser::parse();
|
||||
|
||||
let cwd = std::env::current_dir()?.to_string_lossy().to_string();
|
||||
let repo_dir = matches.get_one::<String>("path").unwrap_or(&cwd);
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("snapshot", sub_matches)) => match sub_matches.subcommand() {
|
||||
Some(("restore", sub_matches)) => {
|
||||
let snapshot_id = sub_matches
|
||||
.get_one::<String>("SNAPSHOT_ID")
|
||||
.expect("required");
|
||||
restore_snapshot(repo_dir, snapshot_id)?;
|
||||
match args.cmd {
|
||||
args::Subcommands::Branch(vbranch::Platform { cmd }) => {
|
||||
let project = command::prepare::project_from_path(args.current_dir)?;
|
||||
match cmd {
|
||||
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)
|
||||
}
|
||||
None => command::vbranch::list(project),
|
||||
}
|
||||
_ => {
|
||||
list_snapshots(repo_dir)?;
|
||||
}
|
||||
args::Subcommands::Project(project::Platform {
|
||||
app_data_dir,
|
||||
app_suffix,
|
||||
cmd,
|
||||
}) => match cmd {
|
||||
Some(project::SubCommands::SwitchToIntegration { remote_ref_name }) => {
|
||||
let project = command::prepare::project_from_path(args.current_dir)?;
|
||||
command::project::switch_to_integration(project, remote_ref_name)
|
||||
}
|
||||
Some(project::SubCommands::Add {
|
||||
switch_to_integration,
|
||||
path,
|
||||
}) => {
|
||||
let ctrl = command::prepare::project_controller(app_suffix, app_data_dir)?;
|
||||
command::project::add(ctrl, path, switch_to_integration)
|
||||
}
|
||||
None => {
|
||||
let ctrl = command::prepare::project_controller(app_suffix, app_data_dir)?;
|
||||
command::project::list(ctrl)
|
||||
}
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_snapshots(repo_dir: &str) -> Result<()> {
|
||||
let project = project_from_path(repo_dir);
|
||||
let snapshots = project.list_snapshots(100, None)?;
|
||||
for snapshot in snapshots {
|
||||
let ts = chrono::DateTime::from_timestamp(snapshot.created_at.seconds(), 0);
|
||||
let details = snapshot.details;
|
||||
if let (Some(ts), Some(details)) = (ts, details) {
|
||||
println!("{} {} {}", ts, snapshot.commit_id, details.operation);
|
||||
args::Subcommands::Snapshot(snapshot::Platform { cmd }) => {
|
||||
let project = command::prepare::project_from_path(args.current_dir)?;
|
||||
match cmd {
|
||||
Some(snapshot::SubCommands::Restore { snapshot_id }) => {
|
||||
command::snapshot::restore(project, snapshot_id)
|
||||
}
|
||||
None => command::snapshot::list(project),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_snapshot(repo_dir: &str, snapshot_id: &str) -> Result<()> {
|
||||
let project = project_from_path(repo_dir);
|
||||
let _guard = project.try_exclusive_access()?;
|
||||
project.restore_snapshot(snapshot_id.parse()?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn project_from_path(repo_dir: &str) -> Project {
|
||||
Project {
|
||||
path: std::path::PathBuf::from(repo_dir),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ tokio = { workspace = true, optional = true, features = [
|
||||
] }
|
||||
uuid = { workspace = true, features = ["v4", "fast-rng"] }
|
||||
rand = "0.8.5"
|
||||
futures = "0.3.30"
|
||||
futures.workspace = true
|
||||
sysinfo = "0.30.13"
|
||||
gix-path = "0.10.9"
|
||||
|
||||
|
@ -7,9 +7,10 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
git2.workspace = true
|
||||
gix.workspace = true
|
||||
anyhow = "1.0.86"
|
||||
bstr = "1.9.1"
|
||||
tokio = { workspace = true, features = [ "rt-multi-thread", "rt", "macros" ] }
|
||||
tokio = { workspace = true, features = [ "rt-multi-thread", "rt", "macros", "sync" ] }
|
||||
gitbutler-git.workspace = true
|
||||
tracing = "0.1.40"
|
||||
tempfile = "3.10"
|
||||
|
@ -1,8 +1,11 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use gitbutler_branch::{Branch, BranchId};
|
||||
use bstr::ByteSlice;
|
||||
use gitbutler_branch::{
|
||||
Branch, BranchId, GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL,
|
||||
GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME,
|
||||
};
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_commit::commit_headers::CommitHeadersV2;
|
||||
use gitbutler_error::error::Code;
|
||||
@ -45,6 +48,7 @@ pub trait RepoActionsExt {
|
||||
branch_name: &str,
|
||||
askpass: Option<Option<BranchId>>,
|
||||
) -> Result<()>;
|
||||
fn signatures(&self) -> Result<(git2::Signature, git2::Signature)>;
|
||||
}
|
||||
|
||||
impl RepoActionsExt for ProjectRepository {
|
||||
@ -229,7 +233,7 @@ impl RepoActionsExt for ProjectRepository {
|
||||
parents: &[&git2::Commit],
|
||||
commit_headers: Option<CommitHeadersV2>,
|
||||
) -> Result<git2::Oid> {
|
||||
let (author, committer) = signatures(self).context("failed to get signatures")?;
|
||||
let (author, committer) = self.signatures().context("failed to get signatures")?;
|
||||
self.repo()
|
||||
.commit_with_signature(
|
||||
None,
|
||||
@ -424,25 +428,43 @@ impl RepoActionsExt for ProjectRepository {
|
||||
|
||||
Err(anyhow!("authentication failed")).context(Code::ProjectGitAuth)
|
||||
}
|
||||
|
||||
fn signatures(&self) -> Result<(git2::Signature, git2::Signature)> {
|
||||
let repo = gix::open(self.repo().path())?;
|
||||
|
||||
let default_actor = gix::actor::SignatureRef {
|
||||
name: GITBUTLER_INTEGRATION_COMMIT_AUTHOR_NAME.into(),
|
||||
email: GITBUTLER_INTEGRATION_COMMIT_AUTHOR_EMAIL.into(),
|
||||
time: Default::default(),
|
||||
};
|
||||
let author = repo.author().transpose()?.unwrap_or(default_actor);
|
||||
let config: Config = self.repo().into();
|
||||
let committer = if config.user_real_comitter()? {
|
||||
repo.committer().transpose()?.unwrap_or(default_actor)
|
||||
} else {
|
||||
default_actor
|
||||
};
|
||||
|
||||
Ok((
|
||||
actor_to_git2_signature(author)?,
|
||||
actor_to_git2_signature(committer)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn signatures(project_repo: &ProjectRepository) -> Result<(git2::Signature, git2::Signature)> {
|
||||
let config: Config = project_repo.repo().into();
|
||||
|
||||
let author = match (config.user_name()?, config.user_email()?) {
|
||||
(None, Some(email)) => git2::Signature::now(&email, &email)?,
|
||||
(Some(name), None) => git2::Signature::now(&name, &format!("{}@example.com", &name))?,
|
||||
(Some(name), Some(email)) => git2::Signature::now(&name, &email)?,
|
||||
_ => git2::Signature::now("GitButler", "gitbutler@gitbutler.com")?,
|
||||
};
|
||||
|
||||
let comitter = if config.user_real_comitter()? {
|
||||
author.clone()
|
||||
} else {
|
||||
git2::Signature::now("GitButler", "gitbutler@gitbutler.com")?
|
||||
};
|
||||
|
||||
Ok((author, comitter))
|
||||
fn actor_to_git2_signature(
|
||||
actor: gix::actor::SignatureRef<'_>,
|
||||
) -> Result<git2::Signature<'static>> {
|
||||
Ok(git2::Signature::now(
|
||||
actor
|
||||
.name
|
||||
.to_str()
|
||||
.with_context(|| format!("Could not process actor name: {}", actor.name))?,
|
||||
actor
|
||||
.email
|
||||
.to_str()
|
||||
.with_context(|| format!("Could not process actor email: {}", actor.email))?,
|
||||
)?)
|
||||
}
|
||||
|
||||
type OidFilter = dyn Fn(&git2::Commit) -> Result<bool>;
|
||||
|
@ -27,7 +27,7 @@ backtrace = { version = "0.3.72", optional = true }
|
||||
console-subscriber = "0.3.0"
|
||||
dirs = "5.0.1"
|
||||
fslock.workspace = true
|
||||
futures = "0.3"
|
||||
futures.workspace = true
|
||||
git2.workspace = true
|
||||
once_cell = "1.19"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
|
@ -1,6 +1,7 @@
|
||||
pub mod commands {
|
||||
use crate::error::Error;
|
||||
use anyhow::{anyhow, Context};
|
||||
use futures::executor::block_on;
|
||||
use gitbutler_branch::BranchOwnershipClaims;
|
||||
use gitbutler_branch::{BranchCreateRequest, BranchId, BranchUpdateRequest};
|
||||
use gitbutler_branch_actions::{BaseBranch, BranchListing};
|
||||
@ -40,7 +41,7 @@ pub mod commands {
|
||||
let oid = VirtualBranchActions
|
||||
.create_commit(&project, branch, message, ownership.as_ref(), run_hooks)
|
||||
.await?;
|
||||
emit_vbranches(&windows, project_id).await;
|
||||
block_on(emit_vbranches(&windows, project_id));
|
||||
Ok(oid.to_string())
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ git2.workspace = true
|
||||
tempfile = "3.10.1"
|
||||
keyring.workspace = true
|
||||
serde_json = "1.0"
|
||||
gix-testtools = "0.15.0"
|
||||
parking_lot.workspace = true
|
||||
gitbutler-branch-actions = { path = "../gitbutler-branch-actions" }
|
||||
gitbutler-repo = { path = "../gitbutler-repo" }
|
||||
gitbutler-command-context.workspace = true
|
||||
|
@ -61,6 +61,78 @@ pub fn init_opts_bare() -> git2::RepositoryInitOptions {
|
||||
opts
|
||||
}
|
||||
|
||||
pub mod read_only {
|
||||
use gitbutler_command_context::ProjectRepository;
|
||||
use gitbutler_project::{Project, ProjectId};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
static DRIVER: Lazy<PathBuf> = Lazy::new(|| {
|
||||
let mut cargo = std::process::Command::new(env!("CARGO"));
|
||||
let res = cargo
|
||||
.args(["build", "-p=gitbutler-cli"])
|
||||
.status()
|
||||
.expect("cargo should run fine");
|
||||
assert!(res.success(), "cargo invocation should be successful");
|
||||
|
||||
let path = Path::new("../../target")
|
||||
.join("debug")
|
||||
.join(if cfg!(windows) {
|
||||
"gitbutler-cli.exe"
|
||||
} else {
|
||||
"gitbutler-cli"
|
||||
});
|
||||
assert!(
|
||||
path.is_file(),
|
||||
"Expecting driver to be located at {path:?} - we also assume a certain crate location"
|
||||
);
|
||||
path.canonicalize().expect(
|
||||
"canonicalization works as the CWD is valid and there are no symlinks to resolve",
|
||||
)
|
||||
});
|
||||
|
||||
/// Execute the script at `script_name.sh` (assumed to be located in `tests/fixtures/<script_name>`)
|
||||
/// and make the command-line application available to it. That way the script can perform GitButler
|
||||
/// operations and leave relevant files around statically.
|
||||
/// Use `project_directory` to define where the project is located within the directory containing
|
||||
/// the output of `script_name`.
|
||||
///
|
||||
/// Returns the project that is strictly for read-only use.
|
||||
pub fn fixture(
|
||||
script_name: &str,
|
||||
project_directory: &str,
|
||||
) -> anyhow::Result<ProjectRepository> {
|
||||
static IS_VALID_PROJECT: Lazy<Mutex<BTreeSet<(String, String)>>> =
|
||||
Lazy::new(|| Mutex::new(Default::default()));
|
||||
|
||||
let root = gix_testtools::scripted_fixture_read_only_with_args(
|
||||
script_name,
|
||||
Some(DRIVER.display().to_string()),
|
||||
)
|
||||
.expect("script execution always succeeds");
|
||||
|
||||
let mut is_valid_guard = IS_VALID_PROJECT.lock();
|
||||
let was_inserted =
|
||||
is_valid_guard.insert((script_name.to_owned(), project_directory.to_owned()));
|
||||
let project_worktree_dir = root.join(project_directory);
|
||||
// Assure the project is valid the first time.
|
||||
let project = if was_inserted {
|
||||
let tmp = tempfile::TempDir::new()?;
|
||||
gitbutler_project::Controller::from_path(tmp.path()).add(project_worktree_dir)?
|
||||
} else {
|
||||
Project {
|
||||
id: ProjectId::generate(),
|
||||
title: project_directory.to_owned(),
|
||||
path: project_worktree_dir,
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
ProjectRepository::open(&project)
|
||||
}
|
||||
}
|
||||
|
||||
/// A secrets store to prevent secrets to be written into the systems own store.
|
||||
///
|
||||
/// Note that this can't be used if secrets themselves are under test as it' doesn't act
|
||||
|
@ -14,7 +14,7 @@ gitbutler-sync.workspace = true
|
||||
gitbutler-oplog.workspace = true
|
||||
thiserror.workspace = true
|
||||
anyhow = "1.0.86"
|
||||
futures = "0.3.30"
|
||||
futures.workspace = true
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
tokio-util = "0.7.11"
|
||||
tracing = "0.1.40"
|
||||
|
Loading…
Reference in New Issue
Block a user