Skip to content

Commit

Permalink
simplify-parents: add a command to remove redundant parents
Browse files Browse the repository at this point in the history
  • Loading branch information
torquestomp committed Oct 7, 2024
1 parent 24ff802 commit dbd7b39
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 0 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* Added diff options to ignore whitespace when comparing lines. Whitespace
changes are still highlighted.

* New command `jj simplify-parents` will remove redundant parent edges.

### Fixed bugs


Expand Down Expand Up @@ -106,6 +108,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
content.

* `jj git fetch -b <remote-git-branch-name>` will now warn if the branch(es)

### Fixed bugs

* Update working copy before reporting changes. This prevents errors during reporting
from leaving the working copy in a stale state.

* `jj git fetch -b <remote-git-branch-name>` will now warn if the branch(es)
can not be found in any of the specified/configured remotes.

* `jj split` now lets the user select all changes in interactive mode. This may be used
Expand Down
5 changes: 5 additions & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ mod restore;
mod root;
mod run;
mod show;
mod simplify_parents;
mod sparse;
mod split;
mod squash;
Expand Down Expand Up @@ -145,6 +146,7 @@ enum Command {
// TODO: Flesh out.
Run(run::RunArgs),
Show(show::ShowArgs),
SimplifyParents(simplify_parents::SimplifyParentsArgs),
#[command(subcommand)]
Sparse(sparse::SparseCommand),
Split(split::SplitArgs),
Expand Down Expand Up @@ -232,6 +234,9 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
Command::Revert(_args) => revert(),
Command::Root(args) => root::cmd_root(ui, command_helper, args),
Command::Run(args) => run::cmd_run(ui, command_helper, args),
Command::SimplifyParents(args) => {
simplify_parents::cmd_simplify_parents(ui, command_helper, args)
}
Command::Show(args) => show::cmd_show(ui, command_helper, args),
Command::Sparse(args) => sparse::cmd_sparse(ui, command_helper, args),
Command::Split(args) => split::cmd_split(ui, command_helper, args),
Expand Down
119 changes: 119 additions & 0 deletions cli/src/commands/simplify_parents.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use std::collections::HashSet;

use itertools::Itertools;
use jj_lib::backend::BackendResult;
use jj_lib::revset::RevsetExpression;
use jj_lib::rewrite::CommitRewriter;
use jj_lib::settings::UserSettings;

use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::command_error::CommandError;
use crate::ui::Ui;

/// Simplify parent edges for the specified revision(s).
///
/// Removes all parents of each of the specified revisions that are also
/// indirect ancestors of the same revisions through other parents. This has no
/// effect on any revision's contents, including the working copy.
///
/// In other words, for all (A, B, C) where A has (B, C) as parents and C is an
/// ancestor of B, A will be rewritten to have only B as a parent instead of
/// B+C.
#[derive(clap::Args, Clone, Debug)]
#[command(group = clap::ArgGroup::new("revision-args").multiple(true).required(true))]
pub(crate) struct SimplifyParentsArgs {
/// Simplify specified revision(s) together with their trees of descendants
/// (can be repeated)
#[arg(long, short, group = "revision-args")]
source: Vec<RevisionArg>,
/// Simplify specified revision(s) (can be repeated)
#[arg(long, short, group = "revision-args")]
revisions: Vec<RevisionArg>,
}

pub(crate) fn cmd_simplify_parents(
ui: &mut Ui,
command: &CommandHelper,
args: &SimplifyParentsArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let revs = RevsetExpression::descendants(
workspace_command
.parse_union_revsets(ui, &args.source)?
.expression(),
)
.union(
workspace_command
.parse_union_revsets(ui, &args.revisions)?
.expression(),
);
let commit_ids = workspace_command
.attach_revset_evaluator(revs)
.evaluate_to_commit_ids()?
.collect_vec();
workspace_command.check_rewritable(&commit_ids)?;
let commit_ids_set: HashSet<_> = commit_ids.iter().cloned().collect();
let num_orig_commits = commit_ids.len();

let mut tx = workspace_command.start_transaction();
let mut stats = SimplifyStats::default();
tx.repo_mut()
.transform_descendants(command.settings(), commit_ids, |rewriter| {
if commit_ids_set.contains(rewriter.old_commit().id()) {
simplify_commit_parents(command.settings(), rewriter, &mut stats)?;
}

Ok(())
})?;

if let Some(mut formatter) = ui.status_formatter() {
if !stats.is_empty() {
writeln!(
formatter,
"Removed {} edges from {} out of {} commits.",
stats.edges, stats.commits, num_orig_commits
)?;
}
}
tx.finish(ui, format!("simplify {} commits", num_orig_commits))?;

Ok(())
}

#[derive(Default)]
struct SimplifyStats {
commits: usize,
edges: usize,
}

impl SimplifyStats {
fn is_empty(&self) -> bool {
self.commits == 0 && self.edges == 0
}
}

fn simplify_commit_parents(
settings: &UserSettings,
mut rewriter: CommitRewriter,
stats: &mut SimplifyStats,
) -> BackendResult<()> {
if rewriter.old_commit().parent_ids().len() <= 1 {
return Ok(());
}

let num_old_heads = rewriter.new_parents().len();
rewriter.simplify_ancestor_merge();
let num_new_heads = rewriter.new_parents().len();

if rewriter.parents_changed() {
rewriter.reparent(settings)?.write()?;

if num_new_heads < num_old_heads {
stats.commits += 1;
stats.edges += num_old_heads - num_new_heads;
}
}

Ok(())
}
19 changes: 19 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ This document contains the help content for the `jj` command-line program.
* [`jj restore`↴](#jj-restore)
* [`jj root`↴](#jj-root)
* [`jj show`↴](#jj-show)
* [`jj simplify-parents`↴](#jj-simplify-parents)
* [`jj sparse`↴](#jj-sparse)
* [`jj sparse edit`↴](#jj-sparse-edit)
* [`jj sparse list`↴](#jj-sparse-list)
Expand Down Expand Up @@ -138,6 +139,7 @@ To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutor
* `restore`Restore paths from another revision
* `root`Show the current workspace root directory
* `show`Show commit description and changes in a revision
* `simplify-parents`Simplify parent edges for the specified revision(s)
* `sparse`Manage which paths from the working-copy commit are present in the working copy
* `split`Split a revision in two
* `squash`Move changes from a revision into another revision
Expand Down Expand Up @@ -1844,6 +1846,23 @@ Show commit description and changes in a revision
## `jj simplify-parents`
Simplify parent edges for the specified revision(s).
Removes all parents of each of the specified revisions that are also indirect ancestors of the same revisions through other parents. This has no effect on any revision's contents, including the working copy.
In other words, for all (A, B, C) where A has (B, C) as parents and C is an ancestor of B, A will be rewritten to have only B as a parent instead of B+C.
**Usage:** `jj simplify-parents <--source <SOURCE>|--revisions <REVISIONS>>`
###### **Options:**
* `-s`, `--source <SOURCE>` — Simplify specified revision(s) together with their trees of descendants (can be repeated)
* `-r`, `--revisions <REVISIONS>` — Simplify specified revision(s) (can be repeated)
## `jj sparse`
Manage which paths from the working-copy commit are present in the working copy
Expand Down
1 change: 1 addition & 0 deletions cli/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ mod test_revset_output;
mod test_root;
mod test_shell_completion;
mod test_show_command;
mod test_simplify_parents_command;
mod test_sparse_command;
mod test_split_command;
mod test_squash_command;
Expand Down
180 changes: 180 additions & 0 deletions cli/tests/test_simplify_parents_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2024 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::path::Path;
use std::path::PathBuf;

use test_case::test_case;

use crate::common::TestEnvironment;

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

(test_env, repo_path)
}

fn create_commit(test_env: &TestEnvironment, repo_path: &Path, name: &str, parents: &[&str]) {
let mut args = vec!["new", "-m", name];
args.extend(parents);
test_env.jj_cmd_ok(repo_path, &args);

std::fs::write(repo_path.join(name), format!("{name}\n")).unwrap();
test_env.jj_cmd_ok(repo_path, &["bookmark", "create", name]);
}

#[test]
fn test_simplify_parents_no_args() {
let (test_env, repo_path) = create_repo();

let stderr = test_env.jj_cmd_cli_error(&repo_path, &["simplify-parents"]);
insta::assert_snapshot!(stderr, @r###"
error: the following required arguments were not provided:
<--source <SOURCE>|--revisions <REVISIONS>>
Usage: jj simplify-parents <--source <SOURCE>|--revisions <REVISIONS>>
For more information, try '--help'.
"###);
}

#[test]
fn test_simplify_parents_no_commits() {
let (test_env, repo_path) = create_repo();

let (stdout, stderr) =
test_env.jj_cmd_ok(&repo_path, &["simplify-parents", "-r", "root() ~ root()"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Nothing changed.
"###);
}

#[test]
fn test_simplify_parents_immutable() {
let (test_env, repo_path) = create_repo();

let stderr = test_env.jj_cmd_failure(&repo_path, &["simplify-parents", "-r", "root()"]);
insta::assert_snapshot!(stderr, @r###"
Error: The root commit 000000000000 is immutable
"###);
}

#[test]
fn test_simplify_parents_no_change() {
let (test_env, repo_path) = create_repo();

create_commit(&test_env, &repo_path, "a", &["root()"]);
create_commit(&test_env, &repo_path, "b", &["a"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "-T", "description"]);
insta::assert_snapshot!(stdout, @r###"
@ b
○ a
"###);

let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["simplify-parents", "-s", "@-"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Nothing changed.
"###);

let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "-T", "description"]);
insta::assert_snapshot!(stdout, @r###"
@ b
○ a
"###);
}

#[test]
fn test_simplify_parents_no_change_diamond() {
let (test_env, repo_path) = create_repo();

create_commit(&test_env, &repo_path, "a", &["root()"]);
create_commit(&test_env, &repo_path, "b", &["a"]);
create_commit(&test_env, &repo_path, "c", &["a"]);
create_commit(&test_env, &repo_path, "d", &["b", "c"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "-T", "description"]);
insta::assert_snapshot!(stdout, @r###"
@ d
├─╮
│ ○ c
○ │ b
├─╯
○ a
"###);

let (stdout, stderr) =
test_env.jj_cmd_ok(&repo_path, &["simplify-parents", "-r", "all() ~ root()"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Nothing changed.
"###);

let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "-T", "description"]);
insta::assert_snapshot!(stdout, @r###"
@ d
├─╮
│ ○ c
○ │ b
├─╯
○ a
"###);
}

#[test_case(&["simplify-parents", "-r", "@", "-r", "@-"] ; "revisions")]
#[test_case(&["simplify-parents", "-s", "@-"] ; "sources")]
fn test_simplify_parents_redundant_parent(args: &[&str]) {
let (test_env, repo_path) = create_repo();

create_commit(&test_env, &repo_path, "a", &["root()"]);
create_commit(&test_env, &repo_path, "b", &["a"]);
create_commit(&test_env, &repo_path, "c", &["a", "b"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "-T", "description"]);
insta::allow_duplicates! {
insta::assert_snapshot!(stdout, @r###"
@ c
├─╮
│ ○ b
├─╯
○ a
"###);
}

let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, args);
insta::allow_duplicates! {
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Removed 1 edges from 1 out of 3 commits.
Working copy now at: royxmykx 0ac2063b c | c
Parent commit : zsuskuln 1394f625 b | b
"###);
}

let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "-T", "description"]);
insta::allow_duplicates! {
insta::assert_snapshot!(stdout, @r###"
@ c
○ b
○ a
"###);
}
}

0 comments on commit dbd7b39

Please sign in to comment.