Skip to content

Commit

Permalink
gc: implement basic GC for Git backend
Browse files Browse the repository at this point in the history
This adds an initial `jj util gc` command, which simply calls `git gc`
when using the Git backend. That should already be useful in
non-colocated repos because it's not obvious how to GC (repack) such
repos. In my own jj repo, it shrunk `.jj/repo/store/` from 2.4 GiB to
780 MiB, and `jj log --ignore-working-copy` was sped up from 157 ms to
86 ms.

I haven't added any tests because the functionality depends on having
`git` binary on the PATH, which we don't yet depend on anywhere
else. I think we'll still be able to test much of the future parts of
garbage collection without a `git` binary because the interesting
parts are about manipulating the Git repo before calling `git gc` on
it.
  • Loading branch information
martinvonz committed Dec 2, 2023
1 parent 48d586c commit 0e4e81b
Show file tree
Hide file tree
Showing 8 changed files with 56 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `jj rebase` now takes the flag `--skip-empty`, which doesn't copy over commits
that would become empty after a rebase.

* There is a new `jj util gc` command for cleaning up the repository storage.
For now, it simply runs `git gc` on the backing Git repo (when using the Git
backend).

### Fixed bugs


Expand Down
4 changes: 4 additions & 0 deletions cli/examples/custom-backend/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,8 @@ impl Backend for JitBackend {
) -> BackendResult<(CommitId, Commit)> {
self.inner.write_commit(contents, sign_with)
}

fn gc(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.inner.gc()
}
}
17 changes: 16 additions & 1 deletion cli/src/commands/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@
use std::io::Write;

use clap::Subcommand;
use jj_lib::repo::Repo;
use tracing::instrument;

use crate::cli_util::{CommandError, CommandHelper};
use crate::cli_util::{user_error, CommandError, CommandHelper};
use crate::ui::Ui;

/// Infrequently used commands such as for generating shell completions
#[derive(Subcommand, Clone, Debug)]
pub(crate) enum UtilCommand {
Completion(UtilCompletionArgs),
Gc(UtilGcArgs),
Mangen(UtilMangenArgs),
ConfigSchema(UtilConfigSchemaArgs),
}
Expand Down Expand Up @@ -56,6 +58,10 @@ pub(crate) struct UtilCompletionArgs {
zsh: bool,
}

/// Run backend-dependent garbage collection.
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct UtilGcArgs {}

/// Print a ROFF (manpage)
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct UtilMangenArgs {}
Expand Down Expand Up @@ -84,6 +90,15 @@ pub(crate) fn cmd_util(
clap_complete::generate(shell, &mut app, "jj", &mut buf);
ui.stdout_formatter().write_all(&buf)?;
}
UtilCommand::Gc(_gc_args) => {
let workspace_command = command.workspace_helper(ui)?;
workspace_command
.repo()
.store()
.backend()
.gc()
.map_err(|err| user_error(err.to_string()))?;
}
UtilCommand::Mangen(_mangen_args) => {
let mut buf = vec![];
let man = clap_mangen::Man::new(command.app().clone());
Expand Down
4 changes: 4 additions & 0 deletions lib/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,4 +533,8 @@ pub trait Backend: Send + Sync + Debug {
contents: Commit,
sign_with: Option<&mut SigningFn>,
) -> BackendResult<(CommitId, Commit)>;

/// Perform garbage collection.
// TODO: pass in the set of commits to keep here
fn gc(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
16 changes: 16 additions & 0 deletions lib/src/git_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ impl From<GitBackendError> for BackendError {
}
}

#[derive(Debug, Error)]
pub enum GitGcError {
#[error("Failed to run git gc command: {0}")]
GcCommand(std::io::Error),
}

pub struct GitBackend {
// While gix::Repository can be created from gix::ThreadSafeRepository, it's
// cheaper to cache the thread-local instance behind a mutex than creating
Expand Down Expand Up @@ -1007,6 +1013,16 @@ impl Backend for GitBackend {
self.save_extra_metadata_table(mut_table, &table_lock)?;
Ok((id, contents))
}

fn gc(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut git = std::process::Command::new("git");
git.env("GIT_DIR", self.git_repo_path());
git.args(["gc"]);
// TODO: pass output to UI layer instead of printing directly here
let mut git_gc = git.spawn().map_err(GitGcError::GcCommand)?;
git_gc.wait()?;
Ok(())
}
}

/// Write a tree conflict as a special tree with `.jjconflict-base-N` and
Expand Down
4 changes: 4 additions & 0 deletions lib/src/local_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,10 @@ impl Backend for LocalBackend {
.map_err(to_other_err)?;
Ok((id, commit))
}

fn gc(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
}

pub fn commit_to_proto(commit: &Commit) -> crate::protos::local_store::Commit {
Expand Down
4 changes: 4 additions & 0 deletions lib/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ impl Store {
})
}

pub fn backend(&self) -> &dyn Backend {
self.backend.as_ref()
}

pub fn backend_impl(&self) -> &dyn Any {
self.backend.as_any()
}
Expand Down
4 changes: 4 additions & 0 deletions lib/testutils/src/test_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,8 @@ impl Backend for TestBackend {
.insert(id.clone(), contents.clone());
Ok((id, contents))
}

fn gc(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
}

0 comments on commit 0e4e81b

Please sign in to comment.