diff --git a/CHANGELOG.md b/CHANGELOG.md index f94d5805df..c56746211c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 * Fixed another file conflict resolution issue where `jj status` would disagree diff --git a/cli/examples/custom-backend/main.rs b/cli/examples/custom-backend/main.rs index aee6b603b8..438d7d16e4 100644 --- a/cli/examples/custom-backend/main.rs +++ b/cli/examples/custom-backend/main.rs @@ -170,4 +170,8 @@ impl Backend for JitBackend { ) -> BackendResult<(CommitId, Commit)> { self.inner.write_commit(contents, sign_with) } + + fn gc(&self) -> Result<(), Box> { + self.inner.gc() + } } diff --git a/cli/src/commands/util.rs b/cli/src/commands/util.rs index 3a80de123d..43d68ebc12 100644 --- a/cli/src/commands/util.rs +++ b/cli/src/commands/util.rs @@ -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), } @@ -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 {} @@ -72,6 +78,7 @@ pub(crate) fn cmd_util( ) -> Result<(), CommandError> { match subcommand { UtilCommand::Completion(args) => cmd_util_completion(ui, command, args), + UtilCommand::Gc(args) => cmd_util_gc(ui, command, args), UtilCommand::Mangen(args) => cmd_util_mangen(ui, command, args), UtilCommand::ConfigSchema(args) => cmd_util_config_schema(ui, command, args), } @@ -96,6 +103,17 @@ fn cmd_util_completion( Ok(()) } +fn cmd_util_gc( + ui: &mut Ui, + command: &CommandHelper, + _args: &UtilGcArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let store = workspace_command.repo().store(); + store.gc().map_err(|err| user_error(err.to_string()))?; + Ok(()) +} + fn cmd_util_mangen( ui: &mut Ui, command: &CommandHelper, diff --git a/docs/git-compatibility.md b/docs/git-compatibility.md index 4a33c2513e..51375bb6cd 100644 --- a/docs/git-compatibility.md +++ b/docs/git-compatibility.md @@ -130,8 +130,8 @@ repos may require you to deal with more involved Jujutsu and Git concepts. * In co-located repos with a very large number of branches or other refs, `jj` commands can get noticeably slower because of the automatic `jj git import` - executed on each command. This can be mitigated by occasionally running `git - pack-refs --all` to speed up the import. + executed on each command. This can be mitigated by occasionally running `jj util + gc` to speed up the import (that command includes packing the Git refs). * Git tools will have trouble with revisions that contain conflicted files. While `jj` renders these files with conflict markers in the working copy, they are diff --git a/lib/src/backend.rs b/lib/src/backend.rs index a81c183eaf..f0b2bf8cd4 100644 --- a/lib/src/backend.rs +++ b/lib/src/backend.rs @@ -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>; } diff --git a/lib/src/git_backend.rs b/lib/src/git_backend.rs index e0126a93a8..47866e5ddf 100644 --- a/lib/src/git_backend.rs +++ b/lib/src/git_backend.rs @@ -18,6 +18,7 @@ use std::any::Any; use std::fmt::{Debug, Error, Formatter}; use std::io::{Cursor, Read}; use std::path::Path; +use std::process::ExitStatus; use std::sync::{Arc, Mutex, MutexGuard}; use std::{fs, str}; @@ -94,6 +95,14 @@ impl From for BackendError { } } +#[derive(Debug, Error)] +pub enum GitGcError { + #[error("Failed to run git gc command: {0}")] + GcCommand(#[source] std::io::Error), + #[error("git gc command exited with an error: {0}")] + GcCommandErrorStatus(ExitStatus), +} + 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 @@ -1007,6 +1016,18 @@ impl Backend for GitBackend { self.save_extra_metadata_table(mut_table, &table_lock)?; Ok((id, contents)) } + + fn gc(&self) -> Result<(), Box> { + 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 status = git.status().map_err(GitGcError::GcCommand)?; + if !status.success() { + return Err(Box::new(GitGcError::GcCommandErrorStatus(status))); + } + Ok(()) + } } /// Write a tree conflict as a special tree with `.jjconflict-base-N` and diff --git a/lib/src/local_backend.rs b/lib/src/local_backend.rs index 09574b5924..c704d23e44 100644 --- a/lib/src/local_backend.rs +++ b/lib/src/local_backend.rs @@ -297,6 +297,10 @@ impl Backend for LocalBackend { .map_err(to_other_err)?; Ok((id, commit)) } + + fn gc(&self) -> Result<(), Box> { + Ok(()) + } } pub fn commit_to_proto(commit: &Commit) -> crate::protos::local_store::Commit { diff --git a/lib/src/store.rs b/lib/src/store.rs index f4810bd364..81e002ab77 100644 --- a/lib/src/store.rs +++ b/lib/src/store.rs @@ -265,4 +265,8 @@ impl Store { pub fn tree_builder(self: &Arc, base_tree_id: TreeId) -> TreeBuilder { TreeBuilder::new(self.clone(), base_tree_id) } + + pub fn gc(&self) -> Result<(), Box> { + self.backend.gc() + } } diff --git a/lib/testutils/src/test_backend.rs b/lib/testutils/src/test_backend.rs index 59ab5037dc..c0657194e2 100644 --- a/lib/testutils/src/test_backend.rs +++ b/lib/testutils/src/test_backend.rs @@ -292,4 +292,8 @@ impl Backend for TestBackend { .insert(id.clone(), contents.clone()); Ok((id, contents)) } + + fn gc(&self) -> Result<(), Box> { + Ok(()) + } }