Introduce edit mode

asdf
This commit is contained in:
Caleb Owens 2024-08-07 15:17:58 +02:00
parent 8f51169004
commit c98421171e
9 changed files with 261 additions and 34 deletions

5
Cargo.lock generated
View File

@ -2275,8 +2275,13 @@ dependencies = [
"anyhow",
"git2",
"gitbutler-command-context",
"gitbutler-fs",
"gitbutler-reference",
"gitbutler-serde",
"gitbutler-testsupport",
"serde",
"toml 0.8.15",
"tracing",
]
[[package]]

View File

@ -35,7 +35,8 @@ resolver = "2"
[workspace.dependencies]
bstr = "1.10.0"
# Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes.
gix = { git = "https://github.com/Byron/gitoxide", rev = "d51f330e9d364c6f7b068116b59bf5c0160e47af", default-features = false, features = [] }
gix = { git = "https://github.com/Byron/gitoxide", rev = "d51f330e9d364c6f7b068116b59bf5c0160e47af", default-features = false, features = [
] }
git2 = { version = "0.18.3", features = [
"vendored-openssl",
"vendored-libgit2",
@ -49,6 +50,7 @@ anyhow = "1.0.86"
fslock = "0.2.1"
parking_lot = "0.12.3"
futures = "0.3.30"
toml = "0.8.13"
gitbutler-id = { path = "crates/gitbutler-id" }
gitbutler-git = { path = "crates/gitbutler-git" }

View File

@ -16,7 +16,7 @@ gitbutler-error.workspace = true
gitbutler-fs.workspace = true
gitbutler-diff.workspace = true
itertools = "0.13"
toml = "0.8.15"
toml.workspace = true
serde = { workspace = true, features = ["std"] }
bstr.workspace = true
md5 = "0.7.0"

View File

@ -11,4 +11,4 @@ bstr.workspace = true
anyhow = "1.0.86"
gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] }
walkdir = "2.5.0"
toml = "0.8.15"
toml.workspace = true

View File

@ -7,9 +7,14 @@ publish = false
[dependencies]
serde = { workspace = true, features = ["std"] }
tracing = "0.1.40"
git2.workspace = true
anyhow.workspace = true
toml.workspace = true
gitbutler-command-context.workspace = true
gitbutler-serde.workspace = true
gitbutler-fs.workspace = true
gitbutler-reference.workspace = true
[dev-dependencies]
gitbutler-testsupport.workspace = true

View File

@ -1,38 +1,123 @@
use std::{fs, path::PathBuf};
use anyhow::{bail, Context, Result};
use gitbutler_command_context::CommandContext;
use gitbutler_reference::ReferenceName;
use serde::{Deserialize, Serialize};
/// Operating Modes:
/// Gitbutler currently has two main operating modes:
/// - `in workspace mode`: When the app is on the gitbutler/integration branch.
/// This is when normal operations can be performed.
/// - `outside workspace mode`: When the user has left the gitbutler/integration
/// branch to perform regular git commands.
/// The reference the app will checkout when the workspace is open
pub const INTEGRATION_BRANCH_REF: &str = "refs/heads/gitbutler/integration";
/// The reference the app will checkout when in edit mode
pub const EDIT_BRANCH_REF: &str = "refs/heads/gitbutler/edit";
const INTEGRATION_BRANCH_REF: &str = "refs/heads/gitbutler/integration";
fn edit_mode_metadata_path(ctx: &CommandContext) -> PathBuf {
ctx.project().gb_dir().join("edit_mode_metadata.toml")
}
pub fn in_open_workspace_mode(ctx: &CommandContext) -> Result<bool> {
let head_ref = ctx.repository().head().context("failed to get head")?;
let head_ref_name = head_ref.name().context("failed to get head name")?;
#[doc(hidden)]
pub fn read_edit_mode_metadata(ctx: &CommandContext) -> Result<EditModeMetadata> {
let edit_mode_metadata = fs::read_to_string(edit_mode_metadata_path(ctx).as_path())
.context("Failed to read edit mode metadata")?;
Ok(head_ref_name == INTEGRATION_BRANCH_REF)
toml::from_str(&edit_mode_metadata).context("Failed to parse edit mode metadata")
}
#[doc(hidden)]
pub fn write_edit_mode_metadata(
ctx: &CommandContext,
edit_mode_metadata: &EditModeMetadata,
) -> Result<()> {
let serialized_edit_mode_metadata =
toml::to_string(edit_mode_metadata).context("Failed to serialize edit mode metadata")?;
gitbutler_fs::write(
edit_mode_metadata_path(ctx).as_path(),
serialized_edit_mode_metadata,
)
.context("Failed to write edit mode metadata")?;
Ok(())
}
/// Holds relevant state required to switch to and from edit mode
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct EditModeMetadata {
/// The sha of the commit getting edited.
#[serde(with = "gitbutler_serde::oid")]
pub editee_commit_sha: git2::Oid,
/// The ref of the vbranch which owns this commit.
pub editee_branch: ReferenceName,
}
#[derive(PartialEq)]
pub enum OperatingMode {
/// The typical app state when its on the gitbutler/integration branch
OpenWorkspace,
/// When the user has chosen to leave the gitbutler/integration branch
OutsideWorkspace,
/// When the app is off of gitbutler/integration and in edit mode
Edit(EditModeMetadata),
}
pub fn operating_mode(ctx: &CommandContext) -> OperatingMode {
let Ok(head_ref) = ctx.repository().head() else {
return OperatingMode::OutsideWorkspace;
};
let Some(head_ref_name) = head_ref.name() else {
return OperatingMode::OutsideWorkspace;
};
if head_ref_name == INTEGRATION_BRANCH_REF {
OperatingMode::OpenWorkspace
} else if head_ref_name == EDIT_BRANCH_REF {
let edit_mode_metadata = read_edit_mode_metadata(ctx);
match edit_mode_metadata {
Ok(edit_mode_metadata) => OperatingMode::Edit(edit_mode_metadata),
Err(error) => {
tracing::warn!(
"Failed to open in edit mode, falling back to outside workspace {}",
error
);
OperatingMode::OutsideWorkspace
}
}
} else {
OperatingMode::OutsideWorkspace
}
}
pub fn in_open_workspace_mode(ctx: &CommandContext) -> bool {
operating_mode(ctx) == OperatingMode::OpenWorkspace
}
pub fn assure_open_workspace_mode(ctx: &CommandContext) -> Result<()> {
if in_open_workspace_mode(ctx)? {
if in_open_workspace_mode(ctx) {
Ok(())
} else {
bail!("Unexpected state: cannot perform operation on non-integration branch")
bail!("Expected to be in open workspace mode")
}
}
pub fn in_outside_workspace_mode(ctx: &CommandContext) -> Result<bool> {
in_open_workspace_mode(ctx).map(|open_mode| !open_mode)
pub fn in_edit_mode(ctx: &CommandContext) -> bool {
matches!(operating_mode(ctx), OperatingMode::Edit(_))
}
pub fn assure_edit_mode(ctx: &CommandContext) -> Result<EditModeMetadata> {
match operating_mode(ctx) {
OperatingMode::Edit(edit_mode_metadata) => Ok(edit_mode_metadata),
_ => bail!("Expected to be in edit mode"),
}
}
pub fn in_outside_workspace_mode(ctx: &CommandContext) -> bool {
operating_mode(ctx) == OperatingMode::OutsideWorkspace
}
pub fn assure_outside_workspace_mode(ctx: &CommandContext) -> Result<()> {
if in_outside_workspace_mode(ctx)? {
if in_outside_workspace_mode(ctx) {
Ok(())
} else {
bail!("Unexpected state: cannot perform operation on integration branch")
bail!("Expected to be in outside workspace mode")
}
}

View File

@ -1,4 +1,5 @@
use gitbutler_command_context::CommandContext;
use gitbutler_operating_modes::{write_edit_mode_metadata, EditModeMetadata};
/// Creates a branch from the head commit
fn create_and_checkout_branch(ctx: &CommandContext, branch_name: &str) {
@ -16,6 +17,17 @@ fn create_and_checkout_branch(ctx: &CommandContext, branch_name: &str) {
.unwrap();
}
fn create_edit_mode_metadata(ctx: &CommandContext) {
write_edit_mode_metadata(
ctx,
&EditModeMetadata {
editee_branch: "asdf".into(),
editee_commit_sha: git2::Oid::zero(),
},
)
.unwrap();
}
mod operating_modes {
mod open_workspace_mode {
use gitbutler_operating_modes::{assure_open_workspace_mode, in_open_workspace_mode};
@ -30,10 +42,21 @@ mod operating_modes {
create_and_checkout_branch(ctx, "gitbutler/integration");
let in_open_workspace = in_open_workspace_mode(ctx).unwrap();
let in_open_workspace = in_open_workspace_mode(ctx);
assert!(in_open_workspace);
}
#[test]
fn in_open_workspace_mode_false_when_in_gitbutler_edit() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/edit");
let in_open_workspace = in_open_workspace_mode(ctx);
assert!(!in_open_workspace);
}
#[test]
fn in_open_workspace_mode_false_when_on_other_branches() {
let suite = Suite::default();
@ -41,7 +64,7 @@ mod operating_modes {
create_and_checkout_branch(ctx, "testeroni");
let in_open_workspace = in_open_workspace_mode(ctx).unwrap();
let in_open_workspace = in_open_workspace_mode(ctx);
assert!(!in_open_workspace);
}
@ -55,6 +78,16 @@ mod operating_modes {
assert!(assure_open_workspace_mode(ctx).is_ok());
}
#[test]
fn assure_open_workspace_mode_err_when_on_gitbutler_edit() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/edit");
assert!(assure_open_workspace_mode(ctx).is_err());
}
#[test]
fn assure_open_workspace_mode_err_when_on_other_branch() {
let suite = Suite::default();
@ -70,32 +103,44 @@ mod operating_modes {
use gitbutler_operating_modes::{assure_outside_workspace_mode, in_outside_workspace_mode};
use gitbutler_testsupport::{Case, Suite};
use crate::create_and_checkout_branch;
use crate::{create_and_checkout_branch, create_edit_mode_metadata};
#[test]
fn in_outside_workspace_mode_true_when_in_gitbutler_integration() {
fn in_outside_workspace_mode_true_when_in_other_branches() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "testeroni");
let in_outside_workspace = in_outside_workspace_mode(ctx).unwrap();
let in_outside_workspace = in_outside_workspace_mode(ctx);
assert!(in_outside_workspace);
}
#[test]
fn in_outside_workspace_mode_false_when_on_other_branches() {
fn in_outside_workspace_mode_false_when_on_gitbutler_edit() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/edit");
create_edit_mode_metadata(ctx);
let in_outside_worskpace = in_outside_workspace_mode(ctx);
assert!(!in_outside_worskpace);
}
#[test]
fn in_outside_workspace_mode_false_when_on_gitbutler_integration() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/integration");
let in_outside_worskpace = in_outside_workspace_mode(ctx).unwrap();
let in_outside_worskpace = in_outside_workspace_mode(ctx);
assert!(!in_outside_worskpace);
}
#[test]
fn assure_outside_workspace_mode_ok_when_on_gitbutler_integration() {
fn assure_outside_workspace_mode_ok_when_on_other_branches() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
@ -105,7 +150,18 @@ mod operating_modes {
}
#[test]
fn assure_outside_workspace_mode_err_when_on_other_branch() {
fn assure_outside_workspace_mode_err_when_on_gitbutler_edit() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/edit");
create_edit_mode_metadata(ctx);
assert!(assure_outside_workspace_mode(ctx).is_err());
}
#[test]
fn assure_outside_workspace_mode_err_when_on_gitbutler_integration() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
@ -114,4 +170,78 @@ mod operating_modes {
assert!(assure_outside_workspace_mode(ctx).is_err());
}
}
mod edit_mode {
use gitbutler_operating_modes::{assure_edit_mode, in_edit_mode};
use gitbutler_testsupport::{Case, Suite};
use crate::{create_and_checkout_branch, create_edit_mode_metadata};
#[test]
fn in_edit_mode_true_when_in_edit_mode() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/edit");
create_edit_mode_metadata(ctx);
let in_edit_mode = in_edit_mode(ctx);
assert!(in_edit_mode);
}
#[test]
fn in_edit_mode_false_when_in_edit_mode_with_no_metadata() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/edit");
let in_edit_mode = in_edit_mode(ctx);
assert!(!in_edit_mode);
}
#[test]
fn in_edit_mode_false_when_on_other_branches() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "testeroni");
create_edit_mode_metadata(ctx);
let in_edit_mode = in_edit_mode(ctx);
assert!(!in_edit_mode);
}
#[test]
fn assert_edit_mode_ok_when_in_edit_mode() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/edit");
create_edit_mode_metadata(ctx);
assert!(assure_edit_mode(ctx).is_ok());
}
#[test]
fn assert_edit_mode_err_when_in_edit_mode_with_no_metadata() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "gitbutler/edit");
assert!(assure_edit_mode(ctx).is_err());
}
#[test]
fn assert_edit_mode_err_when_on_other_branches() {
let suite = Suite::default();
let Case { ctx, .. } = &suite.new_case();
create_and_checkout_branch(ctx, "testeroni");
create_edit_mode_metadata(ctx);
assert!(assure_edit_mode(ctx).is_err());
}
}
}

View File

@ -14,7 +14,7 @@ itertools = "0.13"
strum = { version = "0.26", features = ["derive"] }
tracing = "0.1.40"
gix = { workspace = true, features = ["dirwalk", "credentials", "parallel"] }
toml = "0.8.15"
toml.workspace = true
gitbutler-project.workspace = true
gitbutler-branch.workspace = true
gitbutler-serde.workspace = true

View File

@ -92,7 +92,7 @@ impl Handler {
fn calculate_virtual_branches(&self, project_id: ProjectId) -> Result<()> {
let ctx = self.open_command_context(project_id)?;
// Skip if we're not on the open workspace mode
if !in_open_workspace_mode(&ctx)? {
if !in_open_workspace_mode(&ctx) {
return Ok(());
}
@ -124,7 +124,7 @@ impl Handler {
fn recalculate_everything(&self, paths: Vec<PathBuf>, project_id: ProjectId) -> Result<()> {
let ctx = self.open_command_context(project_id)?;
// Skip if we're not on the open workspace mode
if !in_open_workspace_mode(&ctx)? {
if !in_open_workspace_mode(&ctx) {
return Ok(());
}
@ -174,7 +174,7 @@ impl Handler {
// If the user has left gitbutler/integration, we want to delete the reference.
// TODO: why do we want to do this?
if in_outside_workspace_mode(&ctx)? {
if in_outside_workspace_mode(&ctx) {
let mut integration_reference = ctx.repository().find_reference(
&Refname::from(LocalRefname::new("gitbutler/integration", None))
.to_string(),