forked from mirrors/jj
squash: move the core functionality to jj_lib
This allows 'squash' to be executed more easily from a programmatic context
This commit is contained in:
parent
85f1ac37c6
commit
4f15ca41bf
2 changed files with 162 additions and 99 deletions
|
@ -16,9 +16,9 @@ use itertools::Itertools as _;
|
|||
use jj_lib::commit::Commit;
|
||||
use jj_lib::commit::CommitIteratorExt;
|
||||
use jj_lib::matchers::Matcher;
|
||||
use jj_lib::merged_tree::MergedTree;
|
||||
use jj_lib::object_id::ObjectId;
|
||||
use jj_lib::repo::Repo;
|
||||
use jj_lib::rewrite;
|
||||
use jj_lib::settings::UserSettings;
|
||||
use tracing::instrument;
|
||||
|
||||
|
@ -196,12 +196,6 @@ pub fn move_diff(
|
|||
tx.base_workspace_helper()
|
||||
.check_rewritable(sources.iter().chain(std::iter::once(destination)).ids())?;
|
||||
|
||||
struct SourceCommit<'a> {
|
||||
commit: &'a Commit,
|
||||
parent_tree: MergedTree,
|
||||
selected_tree: MergedTree,
|
||||
abandon: bool,
|
||||
}
|
||||
let mut source_commits = vec![];
|
||||
for source in sources {
|
||||
let parent_tree = source.parent_tree(tx.repo())?;
|
||||
|
@ -227,105 +221,52 @@ from the source will be moved into the destination.
|
|||
let selected_tree_id =
|
||||
diff_selector.select(&parent_tree, &source_tree, matcher, format_instructions)?;
|
||||
let selected_tree = tx.repo().store().get_root_tree(&selected_tree_id)?;
|
||||
let abandon = !keep_emptied && selected_tree.id() == source_tree.id();
|
||||
if !abandon && selected_tree_id == parent_tree.id() {
|
||||
// Nothing selected from this commit. If it's abandoned (i.e. already empty), we
|
||||
// still include it so `jj squash` can be used for abandoning an empty commit in
|
||||
// the middle of a stack.
|
||||
continue;
|
||||
}
|
||||
// TODO: Do we want to optimize the case of moving to the parent commit (`jj
|
||||
// squash -r`)? The source tree will be unchanged in that case.
|
||||
source_commits.push(SourceCommit {
|
||||
commit: source,
|
||||
parent_tree,
|
||||
|
||||
source_commits.push(rewrite::CommitToSquash {
|
||||
commit: source.clone(),
|
||||
selected_tree,
|
||||
abandon,
|
||||
parent_tree,
|
||||
});
|
||||
}
|
||||
if source_commits.is_empty() {
|
||||
if diff_selector.is_interactive() {
|
||||
return Err(user_error("No changes selected"));
|
||||
}
|
||||
|
||||
if let [only_path] = path_arg {
|
||||
if no_rev_arg
|
||||
&& tx
|
||||
.base_workspace_helper()
|
||||
.parse_revset(ui, &RevisionArg::from(only_path.to_owned()))
|
||||
.is_ok()
|
||||
{
|
||||
writeln!(
|
||||
ui.warning_default(),
|
||||
"The argument {only_path:?} is being interpreted as a path. To specify a \
|
||||
revset, pass -r {only_path:?} instead."
|
||||
)?;
|
||||
let repo_path = tx.base_workspace_helper().repo_path().to_owned();
|
||||
match rewrite::squash_commits(
|
||||
settings,
|
||||
tx.repo_mut(),
|
||||
&source_commits,
|
||||
destination,
|
||||
keep_emptied,
|
||||
|abandoned_commits| match description {
|
||||
SquashedDescription::Exact(description) => Ok(description),
|
||||
SquashedDescription::UseDestination => Ok(destination.description().to_owned()),
|
||||
SquashedDescription::Combine => {
|
||||
let abandoned_commits = abandoned_commits.iter().map(|c| &c.commit).collect_vec();
|
||||
combine_messages(&repo_path, &abandoned_commits, destination, settings)
|
||||
}
|
||||
},
|
||||
)? {
|
||||
rewrite::SquashResult::NoChanges => {
|
||||
if diff_selector.is_interactive() {
|
||||
return Err(user_error("No changes selected"));
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
if let [only_path] = path_arg {
|
||||
if no_rev_arg
|
||||
&& tx
|
||||
.base_workspace_helper()
|
||||
.parse_revset(ui, &RevisionArg::from(only_path.to_owned()))
|
||||
.is_ok()
|
||||
{
|
||||
writeln!(
|
||||
ui.warning_default(),
|
||||
"The argument {only_path:?} is being interpreted as a path. To specify a \
|
||||
revset, pass -r {only_path:?} instead."
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for source in &source_commits {
|
||||
if source.abandon {
|
||||
tx.repo_mut()
|
||||
.record_abandoned_commit(source.commit.id().clone());
|
||||
} else {
|
||||
let source_tree = source.commit.tree()?;
|
||||
// Apply the reverse of the selected changes onto the source
|
||||
let new_source_tree = source_tree.merge(&source.selected_tree, &source.parent_tree)?;
|
||||
tx.repo_mut()
|
||||
.rewrite_commit(settings, source.commit)
|
||||
.set_tree_id(new_source_tree.id().clone())
|
||||
.write()?;
|
||||
Ok(())
|
||||
}
|
||||
rewrite::SquashResult::NewCommit(_) => Ok(()),
|
||||
}
|
||||
|
||||
let mut rewritten_destination = destination.clone();
|
||||
if sources
|
||||
.iter()
|
||||
.any(|source| tx.repo().index().is_ancestor(source.id(), destination.id()))
|
||||
{
|
||||
// If we're moving changes to a descendant, first rebase descendants onto the
|
||||
// rewritten sources. Otherwise it will likely already have the content
|
||||
// changes we're moving, so applying them will have no effect and the
|
||||
// changes will disappear.
|
||||
let rebase_map = tx.repo_mut().rebase_descendants_return_map(settings)?;
|
||||
let rebased_destination_id = rebase_map.get(destination.id()).unwrap().clone();
|
||||
rewritten_destination = tx.repo().store().get_commit(&rebased_destination_id)?;
|
||||
}
|
||||
// Apply the selected changes onto the destination
|
||||
let mut destination_tree = rewritten_destination.tree()?;
|
||||
for source in &source_commits {
|
||||
destination_tree = destination_tree.merge(&source.parent_tree, &source.selected_tree)?;
|
||||
}
|
||||
let description = match description {
|
||||
SquashedDescription::Exact(description) => description,
|
||||
SquashedDescription::UseDestination => destination.description().to_owned(),
|
||||
SquashedDescription::Combine => {
|
||||
let abandoned_commits = source_commits
|
||||
.iter()
|
||||
.filter_map(|source| source.abandon.then_some(source.commit))
|
||||
.collect_vec();
|
||||
combine_messages(
|
||||
tx.base_workspace_helper().repo_path(),
|
||||
&abandoned_commits,
|
||||
destination,
|
||||
settings,
|
||||
)?
|
||||
}
|
||||
};
|
||||
let mut predecessors = vec![destination.id().clone()];
|
||||
predecessors.extend(
|
||||
source_commits
|
||||
.iter()
|
||||
.map(|source| source.commit.id().clone()),
|
||||
);
|
||||
tx.repo_mut()
|
||||
.rewrite_commit(settings, &rewritten_destination)
|
||||
.set_tree_id(destination_tree.id().clone())
|
||||
.set_predecessors(predecessors)
|
||||
.set_description(description)
|
||||
.write()?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -830,3 +830,125 @@ pub fn move_commits(
|
|||
num_skipped_rebases,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct CommitToSquash {
|
||||
pub commit: Commit,
|
||||
pub selected_tree: MergedTree,
|
||||
pub parent_tree: MergedTree,
|
||||
}
|
||||
|
||||
impl CommitToSquash {
|
||||
/// Returns true if the selection contains all changes in the commit.
|
||||
fn is_full_selection(&self) -> bool {
|
||||
&self.selected_tree.id() == self.commit.tree_id()
|
||||
}
|
||||
|
||||
/// Returns true if the selection matches the parent tree (contains no
|
||||
/// changes from the commit).
|
||||
///
|
||||
/// Both `is_full_selection()` and `is_empty_selection()`
|
||||
/// can be true if the commit is itself empty.
|
||||
fn is_empty_selection(&self) -> bool {
|
||||
self.selected_tree.id() == self.parent_tree.id()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SquashResult {
|
||||
/// No inputs contained actual changes.
|
||||
NoChanges,
|
||||
/// Destination was rewritten.
|
||||
NewCommit(Commit),
|
||||
}
|
||||
|
||||
/// Squash `sources` into `destination` and return a CommitBuilder for the
|
||||
/// resulting commit. Caller is responsible for setting the description and
|
||||
/// finishing the commit.
|
||||
pub fn squash_commits<E>(
|
||||
settings: &UserSettings,
|
||||
repo: &mut MutableRepo,
|
||||
sources: &[CommitToSquash],
|
||||
destination: &Commit,
|
||||
keep_emptied: bool,
|
||||
description_fn: impl FnOnce(&[&CommitToSquash]) -> Result<String, E>,
|
||||
) -> Result<SquashResult, E>
|
||||
where
|
||||
E: From<BackendError>,
|
||||
{
|
||||
struct SourceCommit<'a> {
|
||||
commit: &'a CommitToSquash,
|
||||
abandon: bool,
|
||||
}
|
||||
let mut source_commits = vec![];
|
||||
for source in sources {
|
||||
let abandon = !keep_emptied && source.is_full_selection();
|
||||
if !abandon && source.is_empty_selection() {
|
||||
// Nothing selected from this commit. If it's abandoned (i.e. already empty), we
|
||||
// still include it so `jj squash` can be used for abandoning an empty commit in
|
||||
// the middle of a stack.
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Do we want to optimize the case of moving to the parent commit (`jj
|
||||
// squash -r`)? The source tree will be unchanged in that case.
|
||||
source_commits.push(SourceCommit {
|
||||
commit: source,
|
||||
abandon,
|
||||
});
|
||||
}
|
||||
|
||||
if source_commits.is_empty() {
|
||||
return Ok(SquashResult::NoChanges);
|
||||
}
|
||||
|
||||
let mut abandoned_commits = vec![];
|
||||
for source in &source_commits {
|
||||
if source.abandon {
|
||||
repo.record_abandoned_commit(source.commit.commit.id().clone());
|
||||
abandoned_commits.push(source.commit);
|
||||
} else {
|
||||
let source_tree = source.commit.commit.tree()?;
|
||||
// Apply the reverse of the selected changes onto the source
|
||||
let new_source_tree =
|
||||
source_tree.merge(&source.commit.selected_tree, &source.commit.parent_tree)?;
|
||||
repo.rewrite_commit(settings, &source.commit.commit)
|
||||
.set_tree_id(new_source_tree.id().clone())
|
||||
.write()?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut rewritten_destination = destination.clone();
|
||||
if sources.iter().any(|source| {
|
||||
repo.index()
|
||||
.is_ancestor(source.commit.id(), destination.id())
|
||||
}) {
|
||||
// If we're moving changes to a descendant, first rebase descendants onto the
|
||||
// rewritten sources. Otherwise it will likely already have the content
|
||||
// changes we're moving, so applying them will have no effect and the
|
||||
// changes will disappear.
|
||||
let rebase_map = repo.rebase_descendants_return_map(settings)?;
|
||||
let rebased_destination_id = rebase_map.get(destination.id()).unwrap().clone();
|
||||
rewritten_destination = repo.store().get_commit(&rebased_destination_id)?;
|
||||
}
|
||||
// Apply the selected changes onto the destination
|
||||
let mut destination_tree = rewritten_destination.tree()?;
|
||||
for source in &source_commits {
|
||||
destination_tree =
|
||||
destination_tree.merge(&source.commit.parent_tree, &source.commit.selected_tree)?;
|
||||
}
|
||||
let mut predecessors = vec![destination.id().clone()];
|
||||
predecessors.extend(
|
||||
source_commits
|
||||
.iter()
|
||||
.map(|source| source.commit.commit.id().clone()),
|
||||
);
|
||||
|
||||
let destination = repo
|
||||
.rewrite_commit(settings, &rewritten_destination)
|
||||
.set_tree_id(destination_tree.id().clone())
|
||||
.set_predecessors(predecessors)
|
||||
.set_description(description_fn(&abandoned_commits)?)
|
||||
.write()?;
|
||||
|
||||
Ok(SquashResult::NewCommit(destination))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue