Skip to content

Commit

Permalink
cli: Add an option to record consecutive snapshots as a single op
Browse files Browse the repository at this point in the history
  • Loading branch information
necauqua committed Feb 18, 2024
1 parent 3c7aa75 commit b8a0543
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 15 deletions.
17 changes: 11 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* When creating a new workspace, the sparse patterns are now copied over from
the current workspace.

* Set config `snapshot.squash-consecutive-snapshots = true` to make consecutive
snapshots result in a single operation being recorded, effectively
automatically abandoning the intermediate snapshot operations, which helps
GC, especially if you run `jj log` in a loop for example.

### Fixed bugs

* On Windows, symlinks in the repo are now materialized as regular files in the
Expand All @@ -70,7 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
copy commit on top of a single specified revision, i.e. with one parent.
`merge` creates a new working copy commit on top of *at least* two specified
revisions, i.e. with two or more parents.

The only difference between these commands and `jj new`, which *also* creates
a new working copy commit, is that `new` can create a working copy commit on
top of any arbitrary number of revisions, so it can handle both the previous
Expand Down Expand Up @@ -222,7 +227,7 @@ Thanks to the people who made this release happen!

* `jj branch set` no longer creates a new branch. Use `jj branch create`
instead.

* `jj init --git` in an existing Git repository now errors and exits rather than
creating a second Git store.

Expand Down Expand Up @@ -386,8 +391,8 @@ Thanks to the people who made this release happen!

### New features

* The `ancestors()` revset function now takes an optional `depth` argument
to limit the depth of the ancestor set. For example, use `jj log -r
* The `ancestors()` revset function now takes an optional `depth` argument
to limit the depth of the ancestor set. For example, use `jj log -r
'ancestors(@, 5)` to view the last 5 commits.

* Support for the Watchman filesystem monitor is now bundled by default. Set
Expand Down Expand Up @@ -552,13 +557,13 @@ Thanks to the people who made this release happen!
respectively.

* `jj log` timestamp format now accepts `.utc()` to convert a timestamp to UTC.

* templates now support additional string methods `.starts_with(x)`, `.ends_with(x)`
`.remove_prefix(x)`, `.remove_suffix(x)`, and `.substr(start, end)`.

* `jj next` and `jj prev` are added, these allow you to traverse the history
in a linear style. For people coming from Sapling and `git-branchles`
see [#2126](https://github.com/martinvonz/jj/issues/2126) for
see [#2126](https://github.com/martinvonz/jj/issues/2126) for
further pending improvements.

* `jj diff --stat` has been implemented. It shows a histogram of the changes,
Expand Down
16 changes: 15 additions & 1 deletion cli/src/cli_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1433,7 +1433,21 @@ See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-workin
print_failed_git_export(ui, &failed_branches)?;
}

self.user_repo = ReadonlyUserRepo::new(tx.commit("snapshot working copy"));
const DESCRIPTION: &str = "snapshot working copy";
const TAG: &str = "snapshots";

if self.settings.squash_consecutive_snapshots() {
if let [parent_op] = tx.parent_ops() {
let meta = &parent_op.store_operation().metadata;
if meta.description == DESCRIPTION {
let tag = meta.tags.get(TAG).and_then(|s| s.parse().ok()).unwrap_or(1);
tx.replace_parent()?;
tx.set_tag(TAG.into(), (tag + 1).to_string())
}
}
}

self.user_repo = ReadonlyUserRepo::new(tx.commit(DESCRIPTION));
}
locked_ws.finish(self.user_repo.repo.op_id().clone())?;
Ok(())
Expand Down
5 changes: 5 additions & 0 deletions cli/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,11 @@
],
"description": "New files with a size in bytes above this threshold are not snapshotted, unless the threshold is 0",
"default": "1MiB"
},
"squash-consecutive-snapshots": {
"type": "boolean",
"description": "Should snapshot operation happening after another snapshot operation replace it, effectively abandoning the first snapshot and releasing it's view for garbage collection",
"default": false
}
}
},
Expand Down
28 changes: 28 additions & 0 deletions cli/tests/test_working_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,31 @@ fn test_snapshot_large_file() {
want this file to be snapshotted. Otherwise add it to your `.gitignore` file.
"###);
}

#[test]
fn test_consecutive_snapshots() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");

test_env.add_config(r#"snapshot.squash-consecutive-snapshots = true"#);

std::fs::write(repo_path.join("a"), "").unwrap();
test_env.jj_cmd_success(&repo_path, &["log"]);

std::fs::write(repo_path.join("b"), "").unwrap();
test_env.jj_cmd_success(&repo_path, &["log"]);

let stdout = test_env.jj_cmd_success(&repo_path, &["operation", "log"]);
insta::assert_snapshot!(stdout, @r###"
@ a0f9fa640725 [email protected] 2001-02-03 04:05:09.000 +07:00 - 2001-02-03 04:05:09.000 +07:00
│ snapshot working copy
│ args: jj log
│ snapshots: 2
◉ 6ac4339ad699 [email protected] 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
│ add workspace 'default'
◉ 1b0049c19762 [email protected] 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
│ initialize repo
◉ 000000000000 root()
"###);
}
4 changes: 2 additions & 2 deletions lib/src/op_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ impl Operation {
description: "".to_string(),
hostname: "".to_string(),
username: "".to_string(),
tags: HashMap::new(),
tags: BTreeMap::new(),
};
Operation {
view_id: empty_view_id,
Expand All @@ -407,7 +407,7 @@ content_hash! {
pub description: String,
pub hostname: String,
pub username: String,
pub tags: HashMap<String, String>,
pub tags: BTreeMap<String, String>,
}
}

Expand Down
6 changes: 6 additions & 0 deletions lib/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ impl UserSettings {
}
}

pub fn squash_consecutive_snapshots(&self) -> bool {
self.config
.get_bool("snapshot.squash-consecutive-snapshots")
.unwrap_or(false)
}

// separate from sign_settings as those two are needed in pretty different
// places
pub fn signing_backend(&self) -> Option<String> {
Expand Down
6 changes: 3 additions & 3 deletions lib/src/simple_op_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ fn operation_metadata_to_proto(
description: metadata.description.clone(),
hostname: metadata.hostname.clone(),
username: metadata.username.clone(),
tags: metadata.tags.clone(),
tags: metadata.tags.clone().into_iter().collect(),
}
}

Expand All @@ -371,7 +371,7 @@ fn operation_metadata_from_proto(
description: proto.description,
hostname: proto.hostname,
username: proto.username,
tags: proto.tags,
tags: proto.tags.into_iter().collect(),
}
}

Expand Down Expand Up @@ -757,7 +757,7 @@ mod tests {
description: "check out foo".to_string(),
hostname: "some.host.example.com".to_string(),
username: "someone".to_string(),
tags: hashmap! {
tags: btreemap! {
"key1".to_string() => "value1".to_string(),
"key2".to_string() => "value2".to_string(),
},
Expand Down
29 changes: 26 additions & 3 deletions lib/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use itertools::Itertools as _;

use crate::backend::Timestamp;
use crate::index::ReadonlyIndex;
use crate::op_store::OperationMetadata;
use crate::op_store::{OperationId, OperationMetadata};
use crate::operation::Operation;
use crate::repo::{MutableRepo, ReadonlyRepo, Repo, RepoLoader, RepoLoaderError};
use crate::settings::UserSettings;
Expand All @@ -32,25 +32,32 @@ pub struct Transaction {
parent_ops: Vec<Operation>,
op_metadata: OperationMetadata,
end_time: Option<Timestamp>,
old_ops: Vec<OperationId>,
}

impl Transaction {
pub fn new(mut_repo: MutableRepo, user_settings: &UserSettings) -> Transaction {
let parent_ops = vec![mut_repo.base_repo().operation().clone()];
let op_metadata = create_op_metadata(user_settings, "".to_string());
let end_time = user_settings.operation_timestamp();
let old_ops = parent_ops.iter().map(|op| op.id().clone()).collect();
Transaction {
mut_repo,
parent_ops,
op_metadata,
end_time,
old_ops,
}
}

pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
self.mut_repo.base_repo()
}

pub fn parent_ops(&self) -> &[Operation] {
&self.parent_ops
}

pub fn set_tag(&mut self, key: String, value: String) {
self.op_metadata.tags.insert(key, value);
}
Expand All @@ -74,12 +81,25 @@ impl Transaction {
let repo_loader = self.base_repo().loader();
let base_repo = repo_loader.load_at(&ancestor_op)?;
let other_repo = repo_loader.load_at(&other_op)?;
self.old_ops.push(other_op.id().clone());
self.parent_ops.push(other_op);
let merged_repo = self.mut_repo();
merged_repo.merge(&base_repo, &other_repo);
Ok(())
}

/// Replaces the parent ops with grandparent ops, effectively rewriting the
/// parent with self.
///
/// Panics if self does not have exactly one parent.
pub fn replace_parent(&mut self) -> Result<(), RepoLoaderError> {
let [parent]: [Operation; 1] = std::mem::take(&mut self.parent_ops)
.try_into()
.expect("Merge or root operations cannot replace their parent");
self.parent_ops = parent.parents().collect::<Result<_, _>>()?;
Ok(())
}

/// Writes the transaction to the operation store and publishes it.
pub fn commit(self, description: impl Into<String>) -> Arc<ReadonlyRepo> {
self.write(description).publish()
Expand Down Expand Up @@ -117,7 +137,7 @@ impl Transaction {
.index_store()
.write_index(mut_index, operation.id())
.unwrap();
UnpublishedOperation::new(base_repo.loader(), operation, view, index)
UnpublishedOperation::new(base_repo.loader(), operation, view, index, self.old_ops)
}
}

Expand Down Expand Up @@ -148,6 +168,7 @@ pub struct UnpublishedOperation {
repo_loader: RepoLoader,
data: Option<NewRepoData>,
closed: bool,
old_ops: Vec<OperationId>,
}

impl UnpublishedOperation {
Expand All @@ -156,6 +177,7 @@ impl UnpublishedOperation {
operation: Operation,
view: View,
index: Box<dyn ReadonlyIndex>,
old_ops: Vec<OperationId>,
) -> Self {
let data = Some(NewRepoData {
operation,
Expand All @@ -166,6 +188,7 @@ impl UnpublishedOperation {
repo_loader,
data,
closed: false,
old_ops,
}
}

Expand All @@ -179,7 +202,7 @@ impl UnpublishedOperation {
let _lock = self.repo_loader.op_heads_store().lock();
self.repo_loader
.op_heads_store()
.update_op_heads(data.operation.parent_ids(), data.operation.id());
.update_op_heads(&self.old_ops, data.operation.id());
}
let repo = self
.repo_loader
Expand Down

0 comments on commit b8a0543

Please sign in to comment.