Skip to content

Commit 282519e

Browse files
committed
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.
1 parent 5cdbadc commit 282519e

File tree

13 files changed

+280
-29
lines changed

13 files changed

+280
-29
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ keyring = "2.3.3"
4747
anyhow = "1.0.86"
4848
fslock = "0.2.1"
4949
parking_lot = "0.12.3"
50+
futures = "0.3.30"
5051

5152
gitbutler-id = { path = "crates/gitbutler-id" }
5253
gitbutler-git = { path = "crates/gitbutler-git" }

crates/gitbutler-branch-actions/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ regex = "1.10"
3232
git2-hooks = "0.3"
3333
url = { version = "2.5.2", features = ["serde"] }
3434
md5 = "0.7.0"
35-
futures = "0.3"
35+
futures.workspace = true
3636
itertools = "0.13"
3737
gitbutler-command-context.workspace = true
3838
gitbutler-project.workspace = true

crates/gitbutler-branch-actions/tests/fixtures/for-listing.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env bash
22
set -eu -o pipefail
3+
CLI=${1:?The first argument is the GitButler CLI}
34

45
git init remote
56
(cd remote
@@ -15,4 +16,10 @@ git clone remote single-branch-no-vbranch-multi-remote
1516
git fetch other-origin
1617
)
1718

19+
export GITBUTLER_CLI_DATA_DIR=./git/gitbutler/app-data
20+
git clone remote one-vbranch-on-integration
21+
(cd one-vbranch-on-integration
22+
$CLI project add --switch-to-integration "$(git rev-parse --symbolic-full-name @{u})"
23+
$CLI branch create virtual
24+
)
1825

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fn on_main_single_branch_no_vbranch() -> Result<()> {
99

1010
let branch = &list[0];
1111
assert_eq!(branch.name, "main", "short names are used");
12-
assert_eq!(branch.remotes, &["origin"]);
12+
assert_eq!(branch.remotes, ["origin"]);
1313
assert_eq!(branch.virtual_branch, None);
1414
assert_eq!(
1515
branch.authors,
@@ -26,12 +26,31 @@ fn on_main_single_branch_no_vbranch_multiple_remotes() -> Result<()> {
2626

2727
let branch = &list[0];
2828
assert_eq!(branch.name, "main");
29-
assert_eq!(branch.remotes, &["other-origin", "origin"]);
29+
assert_eq!(branch.remotes, ["other-origin", "origin"]);
3030
assert_eq!(branch.virtual_branch, None);
3131
assert_eq!(branch.authors, []);
3232
Ok(())
3333
}
3434

35+
#[test]
36+
fn one_vbranch_on_integration() -> Result<()> {
37+
let list = list_branches(&project_ctx("one-vbranch-on-integration")?, None)?;
38+
assert_eq!(list.len(), 1);
39+
40+
let branch = &list[0];
41+
assert_eq!(branch.name, "virtual");
42+
assert!(branch.remotes.is_empty(), "no remote is associated yet");
43+
assert_eq!(
44+
branch
45+
.virtual_branch
46+
.as_ref()
47+
.map(|v| v.given_name.as_str()),
48+
Some("virtual")
49+
);
50+
assert_eq!(branch.authors, []);
51+
Ok(())
52+
}
53+
3554
fn project_ctx(name: &str) -> anyhow::Result<ProjectRepository> {
3655
gitbutler_testsupport::read_only::fixture("for-listing.sh", name)
3756
}

crates/gitbutler-cli/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ path = "src/main.rs"
1212
[dependencies]
1313
gitbutler-oplog.workspace = true
1414
gitbutler-project.workspace = true
15+
gitbutler-reference.workspace = true
16+
gitbutler-branch-actions.workspace = true
17+
gitbutler-branch.workspace = true
1518
gix.workspace = true
16-
clap = { version = "4.5.9", features = ["derive"] }
19+
futures.workspace = true
20+
dirs-next = "2.0.0"
21+
clap = { version = "4.5.9", features = ["derive", "env"] }
1722
anyhow = "1.0.86"
1823
chrono = "0.4.10"

crates/gitbutler-cli/src/args.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,74 @@ pub struct Args {
1313

1414
#[derive(Debug, clap::Subcommand)]
1515
pub enum Subcommands {
16+
/// List and manipulate virtual branches.
17+
#[clap(visible_alias = "branches")]
18+
Branch(vbranch::Platform),
19+
/// List and manipulate projects.
20+
#[clap(visible_alias = "projects")]
21+
Project(project::Platform),
1622
/// List and restore snapshots.
23+
#[clap(visible_alias = "snapshots")]
1724
Snapshot(snapshot::Platform),
1825
}
1926

27+
pub mod vbranch {
28+
#[derive(Debug, clap::Parser)]
29+
pub struct Platform {
30+
#[clap(subcommand)]
31+
pub cmd: Option<SubCommands>,
32+
}
33+
34+
#[derive(Debug, clap::Subcommand)]
35+
pub enum SubCommands {
36+
/// Create a new virtual branch
37+
Create {
38+
/// The name of the virtual branch to create
39+
name: String,
40+
},
41+
}
42+
}
43+
44+
pub mod project {
45+
use gitbutler_reference::RemoteRefname;
46+
use std::path::PathBuf;
47+
48+
#[derive(Debug, clap::Parser)]
49+
pub struct Platform {
50+
/// The location of the directory to contain app data.
51+
///
52+
/// Defaults to the standard location on this platform if unset.
53+
#[clap(short = 'd', long, env = "GITBUTLER_CLI_DATA_DIR")]
54+
pub app_data_dir: Option<PathBuf>,
55+
/// A suffix like `dev` to refer to projects of the development version of the application.
56+
///
57+
/// The production version is used if unset.
58+
#[clap(short = 's', long)]
59+
pub app_suffix: Option<String>,
60+
#[clap(subcommand)]
61+
pub cmd: Option<SubCommands>,
62+
}
63+
64+
#[derive(Debug, clap::Subcommand)]
65+
pub enum SubCommands {
66+
/// Add the given Git repository as project for use with GitButler.
67+
Add {
68+
/// The long name of the remote reference to track, like `refs/remotes/origin/main`,
69+
/// when switching to the integration branch.
70+
#[clap(short = 's', long)]
71+
switch_to_integration: Option<RemoteRefname>,
72+
/// The path at which the repository worktree is located.
73+
#[clap(default_value = ".", value_name = "REPOSITORY")]
74+
path: PathBuf,
75+
},
76+
/// Switch back to the integration branch for use of virtual branches.
77+
SwitchToIntegration {
78+
/// The long name of the remote reference to track, like `refs/remotes/origin/main`.
79+
remote_ref_name: RemoteRefname,
80+
},
81+
}
82+
}
83+
2084
pub mod snapshot {
2185
#[derive(Debug, clap::Parser)]
2286
pub struct Platform {

crates/gitbutler-cli/src/command.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,81 @@
1+
pub mod vbranch {
2+
use crate::command::debug_print;
3+
use futures::executor::block_on;
4+
use gitbutler_branch::{BranchCreateRequest, VirtualBranchesHandle};
5+
use gitbutler_branch_actions::VirtualBranchActions;
6+
use gitbutler_project::Project;
7+
8+
pub fn list(project: Project) -> anyhow::Result<()> {
9+
let branches = VirtualBranchesHandle::new(project.gb_dir()).list_all_branches()?;
10+
for vbranch in branches {
11+
println!(
12+
"{active} {id} {name} {upstream}",
13+
active = if vbranch.applied { "✔️" } else { "⛌" },
14+
id = vbranch.id,
15+
name = vbranch.name,
16+
upstream = vbranch
17+
.upstream
18+
.map_or_else(Default::default, |b| b.to_string())
19+
);
20+
}
21+
Ok(())
22+
}
23+
24+
pub fn create(project: Project, branch_name: String) -> anyhow::Result<()> {
25+
debug_print(block_on(VirtualBranchActions.create_virtual_branch(
26+
&project,
27+
&BranchCreateRequest {
28+
name: Some(branch_name),
29+
..Default::default()
30+
},
31+
))?)
32+
}
33+
}
34+
35+
pub mod project {
36+
use crate::command::debug_print;
37+
use anyhow::{Context, Result};
38+
use futures::executor::block_on;
39+
use gitbutler_branch_actions::VirtualBranchActions;
40+
use gitbutler_project::Project;
41+
use gitbutler_reference::RemoteRefname;
42+
use std::path::PathBuf;
43+
44+
pub fn list(ctrl: gitbutler_project::Controller) -> Result<()> {
45+
for project in ctrl.list()? {
46+
println!(
47+
"{id} {name} {path}",
48+
id = project.id,
49+
name = project.title,
50+
path = project.path.display()
51+
);
52+
}
53+
Ok(())
54+
}
55+
56+
pub fn add(
57+
ctrl: gitbutler_project::Controller,
58+
path: PathBuf,
59+
refname: Option<RemoteRefname>,
60+
) -> Result<()> {
61+
let path = gix::discover(path)?
62+
.work_dir()
63+
.context("Only non-bare repositories can be added")?
64+
.to_owned()
65+
.canonicalize()?;
66+
let project = ctrl.add(path)?;
67+
if let Some(refname) = refname {
68+
block_on(VirtualBranchActions.set_base_branch(&project, &refname))?;
69+
};
70+
debug_print(project)
71+
}
72+
73+
pub fn switch_to_integration(project: Project, refname: RemoteRefname) -> Result<()> {
74+
debug_print(block_on(
75+
VirtualBranchActions.set_base_branch(&project, &refname),
76+
)?)
77+
}
78+
}
179
pub mod snapshot {
280
use anyhow::Result;
381
use gitbutler_oplog::OplogExt;
@@ -21,3 +99,55 @@ pub mod snapshot {
2199
Ok(())
22100
}
23101
}
102+
103+
pub mod prepare {
104+
use anyhow::{bail, Context};
105+
use gitbutler_project::Project;
106+
use std::path::PathBuf;
107+
108+
pub fn project_from_path(path: PathBuf) -> anyhow::Result<Project> {
109+
let worktree_dir = gix::discover(path)?
110+
.work_dir()
111+
.context("Bare repositories aren't supported")?
112+
.to_owned();
113+
Ok(Project {
114+
path: worktree_dir,
115+
..Default::default()
116+
})
117+
}
118+
119+
pub fn project_controller(
120+
app_suffix: Option<String>,
121+
app_data_dir: Option<PathBuf>,
122+
) -> anyhow::Result<gitbutler_project::Controller> {
123+
let path = if let Some(dir) = app_data_dir {
124+
std::fs::create_dir_all(&dir)
125+
.context("Failed to assure the designated data-dir exists")?;
126+
dir
127+
} else {
128+
dirs_next::data_dir()
129+
.map(|dir| {
130+
dir.join(format!(
131+
"com.gitbutler.app{}",
132+
app_suffix
133+
.map(|mut suffix| {
134+
suffix.insert(0, '.');
135+
suffix
136+
})
137+
.unwrap_or_default()
138+
))
139+
})
140+
.context("no data-directory available on this platform")?
141+
};
142+
if !path.is_dir() {
143+
bail!("Path '{}' must be a valid directory", path.display());
144+
}
145+
eprintln!("Using projects from '{}'", path.display());
146+
Ok(gitbutler_project::Controller::from_path(path))
147+
}
148+
}
149+
150+
fn debug_print(this: impl std::fmt::Debug) -> anyhow::Result<()> {
151+
eprintln!("{:#?}", this);
152+
Ok(())
153+
}

crates/gitbutler-cli/src/main.rs

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,53 @@
1-
use anyhow::{Context, Result};
2-
use std::path::PathBuf;
3-
4-
use gitbutler_project::Project;
1+
use anyhow::Result;
52

63
mod args;
7-
use crate::args::snapshot;
4+
use crate::args::{project, snapshot, vbranch};
85
use args::Args;
96

107
mod command;
118

129
fn main() -> Result<()> {
1310
let args: Args = clap::Parser::parse();
1411

15-
let project = project_from_path(args.current_dir)?;
1612
match args.cmd {
17-
args::Subcommands::Snapshot(snapshot::Platform { cmd }) => match cmd {
18-
Some(snapshot::SubCommands::Restore { snapshot_id }) => {
19-
command::snapshot::restore(project, snapshot_id)
13+
args::Subcommands::Branch(vbranch::Platform { cmd }) => {
14+
let project = command::prepare::project_from_path(args.current_dir)?;
15+
match cmd {
16+
Some(vbranch::SubCommands::Create { name }) => {
17+
command::vbranch::create(project, name)
18+
}
19+
None => command::vbranch::list(project),
20+
}
21+
}
22+
args::Subcommands::Project(project::Platform {
23+
app_data_dir,
24+
app_suffix,
25+
cmd,
26+
}) => match cmd {
27+
Some(project::SubCommands::SwitchToIntegration { remote_ref_name }) => {
28+
let project = command::prepare::project_from_path(args.current_dir)?;
29+
command::project::switch_to_integration(project, remote_ref_name)
30+
}
31+
Some(project::SubCommands::Add {
32+
switch_to_integration,
33+
path,
34+
}) => {
35+
let ctrl = command::prepare::project_controller(app_suffix, app_data_dir)?;
36+
command::project::add(ctrl, path, switch_to_integration)
37+
}
38+
None => {
39+
let ctrl = command::prepare::project_controller(app_suffix, app_data_dir)?;
40+
command::project::list(ctrl)
2041
}
21-
None => command::snapshot::list(project),
2242
},
43+
args::Subcommands::Snapshot(snapshot::Platform { cmd }) => {
44+
let project = command::prepare::project_from_path(args.current_dir)?;
45+
match cmd {
46+
Some(snapshot::SubCommands::Restore { snapshot_id }) => {
47+
command::snapshot::restore(project, snapshot_id)
48+
}
49+
None => command::snapshot::list(project),
50+
}
51+
}
2352
}
2453
}
25-
26-
fn project_from_path(path: PathBuf) -> Result<Project> {
27-
let worktree_dir = gix::discover(path)?
28-
.work_dir()
29-
.context("Bare repositories aren't supported")?
30-
.to_owned();
31-
Ok(Project {
32-
path: worktree_dir,
33-
..Default::default()
34-
})
35-
}

0 commit comments

Comments
 (0)