mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-27 17:55:11 +03:00
refactor: introduces a flat-file state for virtual branches
This commit is contained in:
parent
4f2dfca322
commit
d3e3e21a13
33
Cargo.lock
generated
33
Cargo.lock
generated
@ -1973,6 +1973,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml 0.8.11",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
@ -4185,7 +4186,7 @@ dependencies = [
|
||||
"siphasher 1.0.0",
|
||||
"thiserror",
|
||||
"time",
|
||||
"toml 0.8.8",
|
||||
"toml 0.8.11",
|
||||
"url",
|
||||
"walkdir",
|
||||
]
|
||||
@ -5842,14 +5843,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.8"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
|
||||
checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.21.0",
|
||||
"toml_edit 0.22.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5871,7 +5872,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
"winnow 0.5.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -5879,12 +5880,23 @@ name = "toml_edit"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
|
||||
dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992"
|
||||
dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
"winnow 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6796,6 +6808,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
@ -14,6 +14,7 @@ pretty_assertions = "1.4"
|
||||
tempfile = "3.10"
|
||||
|
||||
[dependencies]
|
||||
toml = "0.8.11"
|
||||
anyhow = "1.0.80"
|
||||
async-trait = "0.1.77"
|
||||
backoff = "0.4.0"
|
||||
|
@ -280,6 +280,7 @@ fn main() {
|
||||
virtual_branches::commands::squash_branch_commit,
|
||||
virtual_branches::commands::fetch_from_target,
|
||||
virtual_branches::commands::move_commit,
|
||||
virtual_branches::commands::save_vbranches_state,
|
||||
menu::menu_item_set_enabled,
|
||||
keys::commands::get_public_key,
|
||||
github::commands::init_device_oauth,
|
||||
|
@ -32,3 +32,5 @@ pub use r#virtual::*;
|
||||
|
||||
mod remote;
|
||||
pub use remote::*;
|
||||
|
||||
mod state;
|
||||
|
@ -22,7 +22,7 @@ pub type BranchId = Id<Branch>;
|
||||
// store. it is more or less equivalent to a git branch reference, but it is not
|
||||
// stored or accessible from the git repository itself. it is stored in our
|
||||
// session storage under the branches/ directory.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub struct Branch {
|
||||
pub id: BranchId,
|
||||
pub name: String,
|
||||
@ -31,7 +31,15 @@ pub struct Branch {
|
||||
pub upstream: Option<git::RemoteRefname>,
|
||||
// upstream_head is the last commit on we've pushed to the upstream branch
|
||||
pub upstream_head: Option<git::Oid>,
|
||||
#[serde(
|
||||
serialize_with = "serialize_u128",
|
||||
deserialize_with = "deserialize_u128"
|
||||
)]
|
||||
pub created_timestamp_ms: u128,
|
||||
#[serde(
|
||||
serialize_with = "serialize_u128",
|
||||
deserialize_with = "deserialize_u128"
|
||||
)]
|
||||
pub updated_timestamp_ms: u128,
|
||||
/// tree is the last git tree written to a session, or merge base tree if this is new. use this for delta calculation from the session data
|
||||
pub tree: git::Oid,
|
||||
@ -45,6 +53,22 @@ pub struct Branch {
|
||||
pub selected_for_changes: Option<i64>,
|
||||
}
|
||||
|
||||
fn serialize_u128<S>(x: &u128, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
s.serialize_str(&x.to_string())
|
||||
}
|
||||
|
||||
fn deserialize_u128<'de, D>(d: D) -> Result<u128, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(d)?;
|
||||
let x: u128 = s.parse().map_err(serde::de::Error::custom)?;
|
||||
Ok(x)
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
pub fn refname(&self) -> git::VirtualRefname {
|
||||
self.into()
|
||||
|
@ -14,7 +14,7 @@ use crate::{
|
||||
use super::{
|
||||
branch::BranchId,
|
||||
controller::{Controller, ControllerError},
|
||||
BaseBranch, RemoteBranchFile,
|
||||
BaseBranch, Branch, RemoteBranchFile,
|
||||
};
|
||||
|
||||
impl<E: Into<Error>> From<ControllerError<E>> for Error {
|
||||
@ -70,6 +70,36 @@ pub async fn commit_virtual_branch(
|
||||
Ok(oid)
|
||||
}
|
||||
|
||||
/// This is a test command. It retrieves the virtual branches state from the gitbutler repository (legacy state) and persists it into a flat TOML file
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle))]
|
||||
pub async fn save_vbranches_state(
|
||||
handle: AppHandle,
|
||||
project_id: &str,
|
||||
branch_ids: Vec<&str>,
|
||||
) -> Result<(), Error> {
|
||||
let project_id = project_id.parse().map_err(|_| Error::UserError {
|
||||
code: Code::Validation,
|
||||
message: "Malformed project id".to_string(),
|
||||
})?;
|
||||
|
||||
let mut ids: Vec<BranchId> = Vec::new();
|
||||
for branch_id in &branch_ids {
|
||||
let id: gitbutler_core::id::Id<Branch> =
|
||||
branch_id.parse().map_err(|_| Error::UserError {
|
||||
code: Code::Validation,
|
||||
message: "Malformed branch id".to_string(),
|
||||
})?;
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
handle
|
||||
.state::<Controller>()
|
||||
.save_vbranches_state(&project_id, ids)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
#[instrument(skip(handle))]
|
||||
pub async fn list_virtual_branches(
|
||||
|
@ -5,11 +5,10 @@ use tokio::sync::Semaphore;
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
gb_repository,
|
||||
git::{self},
|
||||
keys, project_repository,
|
||||
gb_repository, git, keys, project_repository,
|
||||
projects::{self, ProjectId},
|
||||
users,
|
||||
virtual_branches::state::{VirtualBranches, VirtualBranchesHandle},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@ -102,6 +101,33 @@ impl Controller {
|
||||
.can_apply_virtual_branch(project_id, branch_id)
|
||||
}
|
||||
|
||||
/// Retrieves the virtual branches state from the gitbutler repository (legacy state) and persists it into a flat TOML file
|
||||
pub async fn save_vbranches_state(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_ids: Vec<BranchId>,
|
||||
) -> Result<(), Error> {
|
||||
let vbranches_state = self
|
||||
.inner(project_id)
|
||||
.await
|
||||
.get_vbranches_state(project_id, branch_ids)?;
|
||||
let project = self.projects.get(project_id).map_err(Error::from)?;
|
||||
// TODO: this should be constructed somewhere else
|
||||
let state_handle = VirtualBranchesHandle::new(project.path.join(".git").as_path());
|
||||
if vbranches_state.default_target.is_some() {
|
||||
state_handle
|
||||
.set_default_target(vbranches_state.default_target.unwrap())
|
||||
.await?;
|
||||
}
|
||||
for (id, target) in vbranches_state.branch_targets {
|
||||
state_handle.set_branch_target(id, target).await?;
|
||||
}
|
||||
for (_, branch) in vbranches_state.branches {
|
||||
state_handle.set_branch(branch).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_virtual_branches(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
@ -485,6 +511,54 @@ impl ControllerInner {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Retrieves the virtual branches state from the gitbutler repository (legacy state)
|
||||
pub fn get_vbranches_state(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
branch_ids: Vec<BranchId>,
|
||||
) -> Result<VirtualBranches, Error> {
|
||||
let project = self.projects.get(project_id)?;
|
||||
let project_repository = project_repository::Repository::open(&project)?;
|
||||
let user = self.users.get_user().context("failed to get user")?;
|
||||
let gb_repository = gb_repository::Repository::open(
|
||||
&self.local_data_dir,
|
||||
&project_repository,
|
||||
user.as_ref(),
|
||||
)
|
||||
.context("failed to open gitbutler repository")?;
|
||||
let current_session = gb_repository
|
||||
.get_or_create_current_session()
|
||||
.context("failed to get or create current session")?;
|
||||
let session_reader = crate::sessions::Reader::open(&gb_repository, ¤t_session)
|
||||
.context("failed to open current session")?;
|
||||
let target_reader = super::target::Reader::new(&session_reader);
|
||||
let branch_reader = super::branch::Reader::new(&session_reader);
|
||||
|
||||
let default_target = target_reader
|
||||
.read_default()
|
||||
.context("failed to read target")?;
|
||||
|
||||
let mut branches: HashMap<BranchId, super::Branch> = HashMap::new();
|
||||
let mut branch_targets: HashMap<BranchId, super::target::Target> = HashMap::new();
|
||||
|
||||
for branch_id in branch_ids {
|
||||
let branch = branch_reader
|
||||
.read(&branch_id)
|
||||
.context("failed to read branch")?;
|
||||
branches.insert(branch_id, branch);
|
||||
let target = target_reader
|
||||
.read(&branch_id)
|
||||
.context("failed to read target")?;
|
||||
branch_targets.insert(branch_id, target);
|
||||
}
|
||||
|
||||
Ok(VirtualBranches {
|
||||
default_target: Some(default_target),
|
||||
branch_targets,
|
||||
branches,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn list_virtual_branches(
|
||||
&self,
|
||||
project_id: &ProjectId,
|
||||
|
141
gitbutler-app/src/virtual_branches/state.rs
Normal file
141
gitbutler-app/src/virtual_branches/state.rs
Normal file
@ -0,0 +1,141 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::virtual_branches::BranchId;
|
||||
|
||||
use super::{target::Target, Branch};
|
||||
|
||||
/// The state of virtual branches data, as persisted in a TOML file.
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub struct VirtualBranches {
|
||||
/// This is the target/base that is set when a repo is added to gb
|
||||
pub default_target: Option<Target>,
|
||||
/// The targets for each virtual branch
|
||||
pub branch_targets: HashMap<BranchId, Target>,
|
||||
/// The current state of the virtual branches
|
||||
pub branches: HashMap<BranchId, Branch>,
|
||||
}
|
||||
/// A handle to the state of virtual branches.
|
||||
///
|
||||
/// For all operations, if the state file does not exist, it will be created.
|
||||
pub struct VirtualBranchesHandle {
|
||||
/// The path to the file containing the virtual branches state.
|
||||
file_path: Arc<Mutex<PathBuf>>,
|
||||
}
|
||||
|
||||
impl VirtualBranchesHandle {
|
||||
/// Creates a new concurrency-safe handle to the state of virtual branches.
|
||||
pub fn new(base_path: &Path) -> Self {
|
||||
let file_path = base_path.join("virtual_branches.toml");
|
||||
Self {
|
||||
file_path: Arc::new(Mutex::new(file_path)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists the default target for the given repository.
|
||||
///
|
||||
/// Errors if the file cannot be read or written.
|
||||
pub async fn set_default_target(&self, target: Target) -> Result<()> {
|
||||
let mut virtual_branches = self.read_file().await?;
|
||||
virtual_branches.default_target = Some(target);
|
||||
self.write_file(virtual_branches).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the default target for the given repository.
|
||||
///
|
||||
/// Errors if the file cannot be read or written.
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_default_target(&self) -> Result<Option<Target>> {
|
||||
let virtual_branches = self.read_file().await?;
|
||||
Ok(virtual_branches.default_target)
|
||||
}
|
||||
|
||||
/// Sets the target for the given virtual branch.
|
||||
///
|
||||
/// Errors if the file cannot be read or written.
|
||||
pub async fn set_branch_target(&self, id: BranchId, target: Target) -> Result<()> {
|
||||
let mut virtual_branches = self.read_file().await?;
|
||||
virtual_branches.branch_targets.insert(id, target);
|
||||
self.write_file(virtual_branches).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the target for the given virtual branch.
|
||||
///
|
||||
/// Errors if the file cannot be read or written.
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_branch_target(&self, id: BranchId) -> Result<Option<Target>> {
|
||||
let virtual_branches = self.read_file().await?;
|
||||
Ok(virtual_branches.branch_targets.get(&id).cloned())
|
||||
}
|
||||
|
||||
/// Sets the state of the given virtual branch.
|
||||
///
|
||||
/// Errors if the file cannot be read or written.
|
||||
pub async fn set_branch(&self, branch: Branch) -> Result<()> {
|
||||
let mut virtual_branches = self.read_file().await?;
|
||||
virtual_branches.branches.insert(branch.id, branch);
|
||||
self.write_file(virtual_branches).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the given virtual branch.
|
||||
///
|
||||
/// Errors if the file cannot be read or written.
|
||||
#[allow(dead_code)]
|
||||
pub async fn remove_branch(&self, id: BranchId) -> Result<()> {
|
||||
let mut virtual_branches = self.read_file().await?;
|
||||
virtual_branches.branches.remove(&id);
|
||||
self.write_file(virtual_branches).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the state of the given virtual branch.
|
||||
///
|
||||
/// Errors if the file cannot be read or written.
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_branch(&self, id: BranchId) -> Result<Option<Branch>> {
|
||||
let virtual_branches = self.read_file().await?;
|
||||
Ok(virtual_branches.branches.get(&id).cloned())
|
||||
}
|
||||
|
||||
/// Reads and parses the state file.
|
||||
///
|
||||
/// If the file does not exist, it will be created.
|
||||
async fn read_file(&self) -> Result<VirtualBranches> {
|
||||
let file_path = &self.file_path.lock().await;
|
||||
if !file_path.exists() {
|
||||
write(file_path.as_path(), &VirtualBranches::default())?;
|
||||
}
|
||||
let mut file: File = File::open(file_path.as_path())?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let virtual_branches: VirtualBranches = toml::from_str(&contents)?;
|
||||
Ok(virtual_branches)
|
||||
}
|
||||
|
||||
async fn write_file(&self, virtual_branches: VirtualBranches) -> Result<()> {
|
||||
let file_path = &self.file_path.lock().await;
|
||||
write(file_path.as_path(), &virtual_branches)
|
||||
}
|
||||
}
|
||||
|
||||
fn write(file_path: &Path, virtual_branches: &VirtualBranches) -> Result<()> {
|
||||
let file_path = file_path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("bad file path"))?;
|
||||
let contents = toml::to_string(&virtual_branches)?;
|
||||
let mut file = File::create(file_path)?;
|
||||
file.write_all(contents.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
mod reader;
|
||||
mod writer;
|
||||
|
||||
use serde::{ser::SerializeStruct, Serialize, Serializer};
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{ser::SerializeStruct, Deserializer, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use reader::TargetReader as Reader;
|
||||
pub use writer::TargetWriter as Writer;
|
||||
@ -29,6 +32,29 @@ impl Serialize for Target {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Target {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TargetData {
|
||||
branch_name: String,
|
||||
remote_name: String,
|
||||
remote_url: String,
|
||||
sha: String,
|
||||
}
|
||||
let target_data: TargetData = serde::Deserialize::deserialize(d)?;
|
||||
let sha = git::Oid::from_str(&target_data.sha)
|
||||
.map_err(|x| serde::de::Error::custom(x.message()))?;
|
||||
|
||||
let target = Target {
|
||||
branch: git::RemoteRefname::new(&target_data.remote_name, &target_data.branch_name),
|
||||
remote_url: target_data.remote_url,
|
||||
sha,
|
||||
};
|
||||
Ok(target)
|
||||
}
|
||||
}
|
||||
|
||||
impl Target {
|
||||
fn try_from(reader: &crate::reader::Reader) -> Result<Target, crate::reader::Error> {
|
||||
let results = reader.batch(&["name", "branch_name", "remote", "remote_url", "sha"])?;
|
||||
|
Loading…
Reference in New Issue
Block a user