Merge pull request #3687 from gitbutlerapp/snapshot-on-file-changes

snapshot on file changes
This commit is contained in:
Kiril Videlov 2024-05-04 19:47:24 +02:00 committed by GitHub
commit 81bc5b327c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 78 additions and 0 deletions

View File

@ -29,6 +29,7 @@ pub mod path;
pub mod project_repository; pub mod project_repository;
pub mod projects; pub mod projects;
pub mod reader; pub mod reader;
pub mod repo;
pub mod sessions; pub mod sessions;
pub mod snapshots; pub mod snapshots;
pub mod ssh; pub mod ssh;

View File

@ -84,6 +84,13 @@ pub struct Project {
pub omit_certificate_check: Option<bool>, pub omit_certificate_check: Option<bool>,
#[serde(default)] #[serde(default)]
pub enable_snapshots: Option<bool>, pub enable_snapshots: Option<bool>,
// The number of changed lines that will trigger a snapshot
#[serde(default = "default_snapshot_lines_threshold")]
pub snapshot_lines_threshold: usize,
}
fn default_snapshot_lines_threshold() -> usize {
20
} }
impl AsRef<Project> for Project { impl AsRef<Project> for Project {

View File

@ -0,0 +1,18 @@
use anyhow::Result;
/// The GbRepository trait provides methods which are building blocks for Gitbutler functionality.
pub trait GbRepository {
/// Returns the number of uncommitted lines of code (added plus removed) in the repository. Included untracked files.
fn changed_lines_count(&self) -> Result<usize>;
}
impl GbRepository for git2::Repository {
fn changed_lines_count(&self) -> Result<usize> {
let head_tree = self.head()?.peel_to_commit()?.tree()?;
let mut opts = git2::DiffOptions::new();
opts.include_untracked(true);
let diff = self.diff_tree_to_workdir_with_index(Some(&head_tree), Some(&mut opts));
let stats = diff?.stats()?;
Ok(stats.deletions() + stats.insertions())
}
}

View File

@ -152,6 +152,7 @@ pub enum OperationType {
ReorderCommit, ReorderCommit,
InsertBlankCommit, InsertBlankCommit,
MoveCommitFile, MoveCommitFile,
FileChanges,
#[default] #[default]
Unknown, Unknown,
} }

View File

@ -15,6 +15,7 @@ futures = "0.3.30"
tokio = { workspace = true, features = [ "macros" ] } tokio = { workspace = true, features = [ "macros" ] }
tokio-util = "0.7.10" tokio-util = "0.7.10"
tracing = "0.1.40" tracing = "0.1.40"
git2.workspace = true
backoff = "0.4.0" backoff = "0.4.0"
notify = { version = "6.0.1" } notify = { version = "6.0.1" }

View File

@ -8,12 +8,16 @@ use std::{path, time};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use gitbutler_core::projects::ProjectId; use gitbutler_core::projects::ProjectId;
use gitbutler_core::repo::GbRepository;
use gitbutler_core::sessions::SessionId; use gitbutler_core::sessions::SessionId;
use gitbutler_core::snapshots::entry::{OperationType, SnapshotDetails, Trailer};
use gitbutler_core::snapshots::snapshot;
use gitbutler_core::virtual_branches::VirtualBranches; use gitbutler_core::virtual_branches::VirtualBranches;
use gitbutler_core::{ use gitbutler_core::{
assets, deltas, gb_repository, git, project_repository, projects, reader, sessions, users, assets, deltas, gb_repository, git, project_repository, projects, reader, sessions, users,
virtual_branches, virtual_branches,
}; };
use tokio::sync::Mutex;
use tracing::instrument; use tracing::instrument;
use super::{events, Change}; use super::{events, Change};
@ -39,6 +43,9 @@ pub struct Handler {
/// A function to send events - decoupled from app-handle for testing purposes. /// A function to send events - decoupled from app-handle for testing purposes.
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
send_event: Arc<dyn Fn(Change) -> Result<()> + Send + Sync + 'static>, send_event: Arc<dyn Fn(Change) -> Result<()> + Send + Sync + 'static>,
// The number of changed lines since the value was last reset
changed_lines_count: Arc<Mutex<usize>>,
} }
impl Handler { impl Handler {
@ -63,6 +70,7 @@ impl Handler {
sessions_db, sessions_db,
deltas_db, deltas_db,
send_event: Arc::new(send_event), send_event: Arc::new(send_event),
changed_lines_count: Arc::new(Mutex::new(0)),
} }
} }
@ -278,15 +286,57 @@ impl Handler {
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
project_id: ProjectId, project_id: ProjectId,
) -> Result<()> { ) -> Result<()> {
let paths_string = paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.join(",");
let calc_deltas = tokio::task::spawn_blocking({ let calc_deltas = tokio::task::spawn_blocking({
let this = self.clone(); let this = self.clone();
move || this.calculate_deltas(paths, project_id) move || this.calculate_deltas(paths, project_id)
}); });
let changed_lines_before = *self.changed_lines_count.lock().await;
// Create a snapshot every time there are more than 20 new lines of code
let handle_snapshots = tokio::task::spawn_blocking({
let this = self.clone();
move || this.maybe_create_snapshot(project_id, changed_lines_before, paths_string)
});
// Set changed lines count to the newly observed value
*self.changed_lines_count.lock().await = handle_snapshots.await??;
self.calculate_virtual_branches(project_id).await?; self.calculate_virtual_branches(project_id).await?;
calc_deltas.await??; calc_deltas.await??;
Ok(()) Ok(())
} }
fn maybe_create_snapshot(
&self,
project_id: ProjectId,
changed_lines_before: usize,
paths: String,
) -> anyhow::Result<usize> {
let project = self
.projects
.get(&project_id)
.context("failed to get project")?;
let repo_path = project.path.as_path();
let repo = git2::Repository::init(repo_path)?;
let changed_lines = repo.changed_lines_count()? - changed_lines_before;
if changed_lines > project.snapshot_lines_threshold {
let details = SnapshotDetails {
version: Default::default(),
operation: OperationType::FileChanges,
title: OperationType::FileChanges.to_string(),
body: None,
trailers: vec![Trailer {
key: "files".to_string(),
value: paths,
}],
};
snapshot::create(&project, details)?;
}
Ok(changed_lines)
}
pub async fn git_file_change( pub async fn git_file_change(
&self, &self,
path: impl Into<PathBuf>, path: impl Into<PathBuf>,