first stab at resurecting git sync

This commit is contained in:
Kiril Videlov 2024-05-25 23:07:40 +02:00
parent f71b76ccb4
commit 762bb31dea
5 changed files with 223 additions and 4 deletions

View File

@ -29,6 +29,7 @@ pub mod projects;
pub mod reader;
pub mod ssh;
pub mod storage;
pub mod synchronize;
pub mod time;
pub mod types;
pub mod users;

View File

@ -81,6 +81,8 @@ pub trait Oplog {
///
/// This is useful to show what has changed in this particular snapshot
fn snapshot_diff(&self, sha: String) -> Result<HashMap<PathBuf, FileDiff>>;
/// Gets the sha of the last snapshot commit if present.
fn oplog_head(&self) -> Result<Option<String>>;
}
impl Oplog for Project {
@ -625,6 +627,10 @@ impl Oplog for Project {
let hunks = hunks_by_filepath(None, &diff)?;
Ok(hunks)
}
fn oplog_head(&self) -> Result<Option<String>> {
let oplog_state = OplogHandle::new(&self.gb_dir());
oplog_state.get_oplog_head()
}
}
fn restore_conflicts_tree(

View File

@ -0,0 +1,192 @@
use std::time;
use crate::id::Id;
use crate::ops::oplog::Oplog;
use crate::{
git::{self, Oid},
project_repository,
projects::{self, CodePushState},
users,
};
use anyhow::{Context, Result};
use itertools::Itertools;
pub async fn sync_with_gitbutler(
project_repository: &project_repository::Repository,
user: &users::User,
projects: &projects::Controller,
) -> Result<()> {
let project = project_repository.project();
let vb_state = project.virtual_branches();
let default_target = vb_state.get_default_target()?;
let gb_code_last_commit = project
.gitbutler_code_push_state
.as_ref()
.map(|state| &state.id)
.copied();
// Push target
push_target(
projects,
project_repository,
&default_target,
gb_code_last_commit,
project.id,
user,
12,
)
.await?;
// Push all refs
push_all_refs(project_repository, user, project.id)?;
// Push Oplog head
let oplog_refspec = project_repository
.project()
.oplog_head()?
.map(|sha| format!("+{}:refs/gitbutler/oplog/oplog", sha));
if let Some(oplog_refspec) = oplog_refspec {
let x = project_repository.push_to_gitbutler_server(Some(user), &[&oplog_refspec]);
println!("\n\n\nHERE: {:?}", x?);
}
Ok(())
}
async fn push_target(
projects: &projects::Controller,
project_repository: &project_repository::Repository,
default_target: &crate::virtual_branches::target::Target,
gb_code_last_commit: Option<Oid>,
project_id: Id<projects::Project>,
user: &users::User,
batch_size: usize,
) -> Result<(), project_repository::RemoteError> {
let ids = batch_rev_walk(
&project_repository.git_repository,
batch_size,
default_target.sha,
gb_code_last_commit,
)?;
tracing::info!(
%project_id,
batches=%ids.len(),
"batches left to push",
);
let id_count = ids.len();
for (idx, id) in ids.iter().enumerate().rev() {
let refspec = format!("+{}:refs/push-tmp/{}", id, project_id);
project_repository.push_to_gitbutler_server(Some(user), &[&refspec])?;
update_project(projects, project_id, *id).await?;
tracing::info!(
%project_id,
i = id_count.saturating_sub(idx),
total = id_count,
"project batch pushed",
);
}
project_repository.push_to_gitbutler_server(
Some(user),
&[&format!("+{}:refs/{}", default_target.sha, project_id)],
)?;
//TODO: remove push-tmp ref
tracing::info!(
%project_id,
"project target ref fully pushed",
);
Ok(())
}
fn batch_rev_walk(
repo: &crate::git::Repository,
batch_size: usize,
from: Oid,
until: Option<Oid>,
) -> Result<Vec<Oid>> {
let mut revwalk = repo.revwalk().context("failed to create revwalk")?;
revwalk
.push(from.into())
.context(format!("failed to push {}", from))?;
if let Some(oid) = until {
revwalk
.hide(oid.into())
.context(format!("failed to hide {}", oid))?;
}
let mut oids = Vec::new();
oids.push(from);
let from = from.into();
for batch in &revwalk.chunks(batch_size) {
let Some(oid) = batch.last() else { continue };
let oid = oid.context("failed to get oid")?;
if oid != from {
oids.push(oid.into());
}
}
Ok(oids)
}
fn collect_refs(
project_repository: &project_repository::Repository,
) -> anyhow::Result<Vec<git::Refname>> {
Ok(project_repository
.git_repository
.references_glob("refs/*")?
.flatten()
.filter_map(|r| r.name())
.collect::<Vec<_>>())
}
fn push_all_refs(
project_repository: &project_repository::Repository,
user: &users::User,
project_id: Id<projects::Project>,
) -> Result<(), project_repository::RemoteError> {
let gb_references = collect_refs(project_repository)?;
let all_refs: Vec<_> = gb_references
.iter()
.filter(|r| {
matches!(
r,
git::Refname::Remote(_) | git::Refname::Virtual(_) | git::Refname::Local(_)
)
})
.map(|r| format!("+{}:{}", r, r))
.collect();
let all_refs: Vec<_> = all_refs.iter().map(String::as_str).collect();
let anything_pushed = project_repository.push_to_gitbutler_server(Some(user), &all_refs)?;
if anything_pushed {
tracing::info!(
%project_id,
"refs pushed",
);
}
Ok(())
}
async fn update_project(
projects: &projects::Controller,
project_id: Id<projects::Project>,
id: Oid,
) -> Result<(), project_repository::RemoteError> {
projects
.update(&projects::UpdateRequest {
id: project_id,
gitbutler_code_push_state: Some(CodePushState {
id,
timestamp: time::SystemTime::now(),
}),
..Default::default()
})
.await
.context("failed to update last push")?;
Ok(())
}

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::{Context, Result};
use futures::executor::block_on;
use gitbutler_core::projects::{self, Project, ProjectId};
use gitbutler_core::{assets, virtual_branches};
use gitbutler_core::{assets, users, virtual_branches};
use tauri::{AppHandle, Manager};
use tracing::instrument;
@ -75,11 +75,13 @@ pub struct Watchers {
fn handler_from_app(app: &AppHandle) -> anyhow::Result<gitbutler_watcher::Handler> {
let projects = app.state::<projects::Controller>().inner().clone();
let users = app.state::<users::Controller>().inner().clone();
let vbranches = app.state::<virtual_branches::Controller>().inner().clone();
let assets_proxy = app.state::<assets::Proxy>().inner().clone();
Ok(gitbutler_watcher::Handler::new(
projects,
users,
vbranches,
assets_proxy,
{

View File

@ -5,8 +5,9 @@ use anyhow::{Context, Result};
use gitbutler_core::ops::entry::{OperationType, SnapshotDetails};
use gitbutler_core::ops::oplog::Oplog;
use gitbutler_core::projects::ProjectId;
use gitbutler_core::synchronize::sync_with_gitbutler;
use gitbutler_core::virtual_branches::VirtualBranches;
use gitbutler_core::{assets, git, project_repository, projects, virtual_branches};
use gitbutler_core::{assets, git, project_repository, projects, users, virtual_branches};
use tracing::instrument;
use super::{events, Change};
@ -22,6 +23,7 @@ pub struct Handler {
// the tauri app, assuming that such application would not be `Send + Sync` everywhere and thus would
// need extra protection.
projects: projects::Controller,
users: users::Controller,
vbranch_controller: virtual_branches::Controller,
assets_proxy: assets::Proxy,
@ -35,12 +37,14 @@ impl Handler {
#[allow(clippy::too_many_arguments)]
pub fn new(
projects: projects::Controller,
users: users::Controller,
vbranch_controller: virtual_branches::Controller,
assets_proxy: assets::Proxy,
send_event: impl Fn(Change) -> Result<()> + Send + Sync + 'static,
) -> Self {
Handler {
projects,
users,
vbranch_controller,
assets_proxy,
send_event: Arc::new(send_event),
@ -185,8 +189,22 @@ impl Handler {
}
Ok(())
}
async fn gitbutler_oplog_change(&self, _project_id: ProjectId) -> Result<()> {
// TODO: Queue up pushing of data here, if configured
/// Invoked whenever there's a new oplog entry.
/// If synchronizing with GitButler's servers is enabled it will push Oplog refs
async fn gitbutler_oplog_change(&self, project_id: ProjectId) -> Result<()> {
let project = self
.projects
.get(&project_id)
.context("failed to get project")?;
if project.is_sync_enabled() && project.has_code_url() {
if let Some(user) = self.users.get_user()? {
let repository = project_repository::Repository::open(&project)
.context("failed to open project repository for project")?;
return sync_with_gitbutler(&repository, &user, &self.projects).await;
}
}
Ok(())
}
}