refactor: introduces a flat-file state for virtual branches

This commit is contained in:
Kiril Videlov 2024-03-16 18:07:51 +01:00 committed by Kiril Videlov
parent 4f2dfca322
commit d3e3e21a13
9 changed files with 332 additions and 12 deletions

33
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -32,3 +32,5 @@ pub use r#virtual::*;
mod remote;
pub use remote::*;
mod state;

View File

@ -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()

View File

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

View File

@ -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, &current_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,

View 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(())
}

View File

@ -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"])?;