add ability to create add projects in CLI and create vbranches

This allows actual GitButler setups and more complex tests, which
are also added here.
This commit is contained in:
Sebastian Thiel 2024-07-27 19:15:02 +02:00
parent 5cdbadce4f
commit 282519eca5
No known key found for this signature in database
GPG Key ID: 9CB5EE7895E8268B
13 changed files with 280 additions and 29 deletions

5
Cargo.lock generated
View File

@ -2058,8 +2058,13 @@ dependencies = [
"anyhow",
"chrono",
"clap",
"dirs-next",
"futures",
"gitbutler-branch",
"gitbutler-branch-actions",
"gitbutler-oplog",
"gitbutler-project",
"gitbutler-reference",
"gix",
]

View File

@ -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" }

View File

@ -32,7 +32,7 @@ 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

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -eu -o pipefail
CLI=${1:?The first argument is the GitButler CLI}
git init remote
(cd remote
@ -15,4 +16,10 @@ git clone remote single-branch-no-vbranch-multi-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
)

View File

@ -9,7 +9,7 @@ fn on_main_single_branch_no_vbranch() -> Result<()> {
let branch = &list[0];
assert_eq!(branch.name, "main", "short names are used");
assert_eq!(branch.remotes, &["origin"]);
assert_eq!(branch.remotes, ["origin"]);
assert_eq!(branch.virtual_branch, None);
assert_eq!(
branch.authors,
@ -26,12 +26,31 @@ fn on_main_single_branch_no_vbranch_multiple_remotes() -> Result<()> {
let branch = &list[0];
assert_eq!(branch.name, "main");
assert_eq!(branch.remotes, &["other-origin", "origin"]);
assert_eq!(branch.remotes, ["other-origin", "origin"]);
assert_eq!(branch.virtual_branch, None);
assert_eq!(branch.authors, []);
Ok(())
}
#[test]
fn one_vbranch_on_integration() -> Result<()> {
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
.virtual_branch
.as_ref()
.map(|v| v.given_name.as_str()),
Some("virtual")
);
assert_eq!(branch.authors, []);
Ok(())
}
fn project_ctx(name: &str) -> anyhow::Result<ProjectRepository> {
gitbutler_testsupport::read_only::fixture("for-listing.sh", name)
}

View File

@ -12,7 +12,12 @@ path = "src/main.rs"
[dependencies]
gitbutler-oplog.workspace = true
gitbutler-project.workspace = true
gitbutler-reference.workspace = true
gitbutler-branch-actions.workspace = true
gitbutler-branch.workspace = true
gix.workspace = true
clap = { version = "4.5.9", features = ["derive"] }
futures.workspace = true
dirs-next = "2.0.0"
clap = { version = "4.5.9", features = ["derive", "env"] }
anyhow = "1.0.86"
chrono = "0.4.10"

View File

@ -13,10 +13,74 @@ pub struct Args {
#[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 {
/// 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 {

View File

@ -1,3 +1,81 @@
pub mod vbranch {
use crate::command::debug_print;
use futures::executor::block_on;
use gitbutler_branch::{BranchCreateRequest, VirtualBranchesHandle};
use gitbutler_branch_actions::VirtualBranchActions;
use gitbutler_project::Project;
pub fn list(project: Project) -> anyhow::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) -> anyhow::Result<()> {
debug_print(block_on(VirtualBranchActions.create_virtual_branch(
&project,
&BranchCreateRequest {
name: Some(branch_name),
..Default::default()
},
))?)
}
}
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;
@ -21,3 +99,55 @@ pub mod snapshot {
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(())
}

View File

@ -1,10 +1,7 @@
use anyhow::{Context, Result};
use std::path::PathBuf;
use gitbutler_project::Project;
use anyhow::Result;
mod args;
use crate::args::snapshot;
use crate::args::{project, snapshot, vbranch};
use args::Args;
mod command;
@ -12,24 +9,45 @@ mod command;
fn main() -> Result<()> {
let args: Args = clap::Parser::parse();
let project = project_from_path(args.current_dir)?;
match args.cmd {
args::Subcommands::Snapshot(snapshot::Platform { cmd }) => match cmd {
Some(snapshot::SubCommands::Restore { snapshot_id }) => {
command::snapshot::restore(project, snapshot_id)
args::Subcommands::Branch(vbranch::Platform { cmd }) => {
let project = command::prepare::project_from_path(args.current_dir)?;
match cmd {
Some(vbranch::SubCommands::Create { name }) => {
command::vbranch::create(project, name)
}
None => command::vbranch::list(project),
}
}
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)
}
None => command::snapshot::list(project),
},
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),
}
}
}
}
fn project_from_path(path: PathBuf) -> 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()
})
}

View File

@ -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"

View File

@ -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"] }

View File

@ -88,7 +88,9 @@ pub mod read_only {
path.is_file(),
"Expecting driver to be located at {path:?} - we also assume a certain crate location"
);
path
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>`)

View File

@ -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"