mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-22 19:14:31 +03:00
Introduce edit mode
asdf
This commit is contained in:
parent
8f51169004
commit
c98421171e
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -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]]
|
||||
|
@ -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" }
|
||||
|
@ -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"
|
||||
|
@ -6,9 +6,9 @@ authors = ["GitButler <gitbutler@gitbutler.com>"]
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["std"]}
|
||||
serde = { workspace = true, features = ["std"] }
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user