-
Notifications
You must be signed in to change notification settings - Fork 346
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
simplify: add a command to remove redundant parents
- Loading branch information
1 parent
6d0a092
commit fb4d0c4
Showing
6 changed files
with
300 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
use std::collections::HashSet; | ||
|
||
use itertools::Itertools; | ||
use jj_lib::commit::Commit; | ||
use jj_lib::repo::Repo; | ||
use jj_lib::revset::RevsetExpression; | ||
use jj_lib::settings::UserSettings; | ||
|
||
use crate::cli_util::CommandHelper; | ||
use crate::cli_util::RevisionArg; | ||
use crate::cli_util::WorkspaceCommandTransaction; | ||
use crate::command_error::user_error; | ||
use crate::command_error::CommandError; | ||
use crate::ui::Ui; | ||
|
||
/// Simplify parent edges for the specified revision(s). | ||
/// | ||
/// Removes all redundant parent edges going into any of the specified | ||
/// revisions. By definition, this has no effect on any commit contents and | ||
/// should not cause any working copy changes or conflicts. | ||
/// | ||
/// In other words, for all (A, B, C) where A has (B, C) as parents and B has C | ||
/// as an ancestor, A will be rewritten to have only B as a parent instead of | ||
/// B+C. | ||
#[derive(clap::Args, Clone, Debug)] | ||
pub(crate) struct SimplifyArgs { | ||
/// Simplify specified revision(s) together with their trees of descendants | ||
/// (can be repeated) | ||
#[arg(long, short)] | ||
sources: Vec<RevisionArg>, | ||
/// Simplify specified revision(s) (can be repeated) | ||
#[arg(long, short)] | ||
revisions: Vec<RevisionArg>, | ||
} | ||
|
||
pub(crate) fn cmd_simplify( | ||
ui: &mut Ui, | ||
command: &CommandHelper, | ||
args: &SimplifyArgs, | ||
) -> Result<(), CommandError> { | ||
if args.sources.is_empty() && args.revisions.is_empty() { | ||
return Err(user_error("no revisions specified")); | ||
} | ||
|
||
let mut workspace_command = command.workspace_helper(ui)?; | ||
let revs = RevsetExpression::descendants( | ||
workspace_command | ||
.parse_union_revsets(&args.sources)? | ||
.expression(), | ||
) | ||
.union( | ||
workspace_command | ||
.parse_union_revsets(&args.revisions)? | ||
.expression(), | ||
); | ||
let commits: Vec<_> = workspace_command | ||
.attach_revset_evaluator(revs) | ||
.evaluate_to_commits()? | ||
.try_collect()?; | ||
workspace_command.check_rewritable(commits.iter().map(|c| c.id()))?; | ||
let orig_commits = commits.len(); | ||
|
||
let mut tx = workspace_command.start_transaction(); | ||
let mut stats = SimplifyStats::default(); | ||
for commit in commits { | ||
stats.add(simplify_commit(command.settings(), &mut tx, &commit)?); | ||
} | ||
|
||
if let Some(mut formatter) = ui.status_formatter() { | ||
if !stats.is_empty() { | ||
writeln!( | ||
formatter, | ||
"Removed {} edges from {} out of {} commits.", | ||
stats.edges, stats.commits, orig_commits | ||
)?; | ||
} | ||
} | ||
tx.finish(ui, format!("simplified {} commits", orig_commits))?; | ||
|
||
Ok(()) | ||
} | ||
|
||
#[derive(Default)] | ||
struct SimplifyStats { | ||
commits: usize, | ||
edges: usize, | ||
} | ||
|
||
impl SimplifyStats { | ||
fn for_commit(edges: usize) -> Self { | ||
Self { commits: 1, edges } | ||
} | ||
|
||
fn add(&mut self, other: SimplifyStats) { | ||
self.commits += other.commits; | ||
self.edges += other.edges; | ||
} | ||
|
||
fn is_empty(&self) -> bool { | ||
self.commits == 0 && self.edges == 0 | ||
} | ||
} | ||
|
||
fn simplify_commit( | ||
settings: &UserSettings, | ||
tx: &mut WorkspaceCommandTransaction, | ||
commit: &Commit, | ||
) -> Result<SimplifyStats, CommandError> { | ||
if commit.parent_ids().len() <= 1 { | ||
return Ok(SimplifyStats::default()); | ||
} | ||
|
||
let old_heads: HashSet<_> = commit.parent_ids().iter().cloned().collect(); | ||
let new_heads: HashSet<_> = tx | ||
.repo_mut() | ||
.index() | ||
.heads(&mut commit.parent_ids().iter()) | ||
.into_iter() | ||
.collect(); | ||
if new_heads == old_heads { | ||
return Ok(SimplifyStats::default()); | ||
} | ||
let removed = old_heads.len() - new_heads.len(); | ||
|
||
// Preserve order. | ||
let new_heads = commit | ||
.parent_ids() | ||
.iter() | ||
.filter(|&id| new_heads.contains(id)) | ||
.cloned() | ||
.collect_vec(); | ||
|
||
tx.repo_mut() | ||
.rewrite_commit(settings, commit) | ||
.set_parents(new_heads) | ||
.write()?; | ||
|
||
Ok(SimplifyStats::for_commit(removed)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// 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_no_args() { | ||
let (test_env, repo_path) = create_repo(); | ||
|
||
let stderr = test_env.jj_cmd_failure(&repo_path, &["simplify"]); | ||
insta::assert_snapshot!(stderr, @r###" | ||
Error: no revisions specified | ||
"###); | ||
} | ||
|
||
#[test] | ||
fn test_simplify_no_commits() { | ||
let (test_env, repo_path) = create_repo(); | ||
|
||
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["simplify", "-r", "root() ~ root()"]); | ||
insta::assert_snapshot!(stdout, @""); | ||
insta::assert_snapshot!(stderr, @r###" | ||
Nothing changed. | ||
"###); | ||
} | ||
|
||
#[test] | ||
fn test_simplify_immutable() { | ||
let (test_env, repo_path) = create_repo(); | ||
|
||
let stderr = test_env.jj_cmd_failure(&repo_path, &["simplify", "-r", "root()"]); | ||
insta::assert_snapshot!(stderr, @r###" | ||
Error: The root commit 000000000000 is immutable | ||
"###); | ||
} | ||
|
||
#[test] | ||
fn test_simplify_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", "-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_case(&["simplify", "-r", "@", "-r", "@-"] ; "revisions")] | ||
#[test_case(&["simplify", "-s", "@-"] ; "sources")] | ||
fn test_simplify_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 | ||
◆ | ||
"###); | ||
} | ||
} |