Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hunk dependencies #6077

Merged
merged 5 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ gitbutler-workspace = { path = "crates/gitbutler-workspace" }
but-debugging = { path = "crates/but-debugging" }
but-core = { path = "crates/but-core" }
but-workspace = { path = "crates/but-workspace" }
but-hunk-dependency = { path = "crates/but-hunk-dependency" }

[profile.release]
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
Expand Down
8 changes: 8 additions & 0 deletions crates/but-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@ name = "but-cli"
path = "src/main.rs"
doctest = false

[features]
# To help produce stable commits in test-cases.
testing = ["dep:gitbutler-commit"]

[dependencies]
gitbutler-command-context.workspace = true
gitbutler-project.workspace = true
gitbutler-settings.workspace = true
but-core.workspace = true
but-workspace.workspace = true
but-hunk-dependency.workspace = true

gitbutler-commit = { workspace = true, optional = true, features = ["testing"] }

clap = { version = "4.5.23", features = ["derive", "env"] }
gix.workspace = true
anyhow.workspace = true
itertools = "0.14.0"
tracing-forest = { version = "0.1.6" }
tracing-subscriber.workspace = true
tracing.workspace = true
3 changes: 3 additions & 0 deletions crates/but-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub enum Subcommands {
/// The revspec to the previous commit that the returned changes transform into current commit.
previous_commit: Option<String>,
},
/// Return the dependencies of worktree changes with the commits that last changed them.
#[clap(visible_alias = "dep")]
HunkDependency,
/// Returns the list of stacks that are currently part of the GitButler workspace.
Stacks,
StackBranches {
Expand Down
86 changes: 84 additions & 2 deletions crates/but-cli/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ fn debug_print(this: impl std::fmt::Debug) -> anyhow::Result<()> {
}

pub mod diff {
use crate::command::{debug_print, project_repo};
use crate::command::{debug_print, project_from_path, project_repo};
use gix::bstr::BString;
use itertools::Itertools;
use std::path::PathBuf;

pub fn commit_changes(
Expand Down Expand Up @@ -53,6 +55,23 @@ pub mod diff {
}
}

pub fn locks(current_dir: PathBuf) -> anyhow::Result<()> {
let project = project_from_path(current_dir)?;
let repo = gix::open(project.worktree_path())?;
let worktree_changes = but_core::diff::worktree_changes(&repo)?;
let input_stacks = but_hunk_dependency::workspace_stacks_to_input_stacks(
&repo,
&but_workspace::stacks(&project.gb_dir())?,
but_workspace::common_merge_base_with_target_branch(&project.gb_dir())?,
)?;
let ranges = but_hunk_dependency::WorkspaceRanges::try_from_stacks(input_stacks)?;
debug_print(intersect_workspace_ranges(
&repo,
ranges,
worktree_changes.changes,
)?)
}

fn unified_diff_for_changes(
repo: &gix::Repository,
changes: Vec<but_core::TreeChange>,
Expand All @@ -61,11 +80,74 @@ pub mod diff {
.into_iter()
.map(|tree_change| {
tree_change
.unified_diff(repo)
.unified_diff(repo, 3)
.map(|diff| (tree_change, diff))
})
.collect::<Result<Vec<_>, _>>()
}

fn intersect_workspace_ranges(
repo: &gix::Repository,
ranges: but_hunk_dependency::WorkspaceRanges,
worktree_changes: Vec<but_core::TreeChange>,
) -> anyhow::Result<LockInfo> {
let mut intersections_by_path = Vec::new();
let mut missed_hunks = Vec::new();
for change in worktree_changes {
let unidiff = change.unified_diff(repo, 0)?;
let but_core::UnifiedDiff::Patch { hunks } = unidiff else {
continue;
};
let mut intersections = Vec::new();
for hunk in hunks {
if let Some(hunk_ranges) =
ranges.intersection(&change.path, hunk.old_start, hunk.old_lines)
{
intersections.push(HunkIntersection {
hunk,
commit_intersections: hunk_ranges.into_iter().copied().collect(),
});
} else {
missed_hunks.push((change.path.clone(), hunk));
}
}
if !intersections.is_empty() {
intersections_by_path.push((change.path, intersections));
}
}

Ok(LockInfo {
intersections_by_path,
missed_hunks,
ranges_by_path: ranges
.ranges_by_path_map()
.iter()
.sorted_by(|a, b| a.0.cmp(b.0))
.map(|(path, ranges)| (path.to_owned(), ranges.to_vec()))
.collect(),
})
}

/// A structure that has stable content so it can be asserted on, showing the hunk-ranges that intersect with each of the input ranges.
#[derive(Debug)]
#[allow(dead_code)]
pub struct LockInfo {
/// All available ranges for a tracked path, basically all changes seen over a set of commits.
pub ranges_by_path: Vec<(BString, Vec<but_hunk_dependency::HunkRange>)>,
/// The ranges that intersected with an input hunk.
pub intersections_by_path: Vec<(BString, Vec<HunkIntersection>)>,
/// Hunks that didn't have a matching intersection, with the filepath mentioned per hunk as well.
pub missed_hunks: Vec<(BString, but_core::unified_diff::DiffHunk)>,
}

#[derive(Debug)]
#[allow(dead_code)]
pub struct HunkIntersection {
/// The hunk that was used for the intersection.
pub hunk: but_core::unified_diff::DiffHunk,
/// The hunks that touch `hunk` in the commit-diffs.
pub commit_intersections: Vec<but_hunk_dependency::HunkRange>,
}
}

pub mod stacks {
Expand Down
1 change: 1 addition & 0 deletions crates/but-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ fn main() -> Result<()> {
let _op_span = tracing::info_span!("cli-op").entered();

match args.cmd {
args::Subcommands::HunkDependency => command::diff::locks(args.current_dir),
args::Subcommands::Status { unified_diff } => {
command::diff::status(args.current_dir, unified_diff)
}
Expand Down
15 changes: 9 additions & 6 deletions crates/but-core/src/diff/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,21 +449,24 @@ impl TreeChange {
/// for obtaining a working tree to read files from disk.
/// Note that the mount of lines of context around each hunk are currently hardcoded to `3` as it *might* be relevant for creating
/// commits later.
pub fn unified_diff(&self, repo: &gix::Repository) -> anyhow::Result<UnifiedDiff> {
const CONTEXT_LINES: u32 = 3;
pub fn unified_diff(
&self,
repo: &gix::Repository,
context_lines: u32,
) -> anyhow::Result<UnifiedDiff> {
match &self.status {
TreeStatus::Deletion { previous_state } => UnifiedDiff::compute(
repo,
self.path.as_bstr(),
None,
None,
*previous_state,
CONTEXT_LINES,
context_lines,
),
TreeStatus::Addition {
state,
is_untracked: _,
} => UnifiedDiff::compute(repo, self.path.as_bstr(), None, *state, None, CONTEXT_LINES),
} => UnifiedDiff::compute(repo, self.path.as_bstr(), None, *state, None, context_lines),
TreeStatus::Modification {
state,
previous_state,
Expand All @@ -474,7 +477,7 @@ impl TreeChange {
None,
*state,
*previous_state,
CONTEXT_LINES,
context_lines,
),
TreeStatus::Rename {
previous_path,
Expand All @@ -487,7 +490,7 @@ impl TreeChange {
Some(previous_path.as_bstr()),
*state,
*previous_state,
CONTEXT_LINES,
context_lines,
),
}
}
Expand Down
33 changes: 32 additions & 1 deletion crates/but-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,25 @@ pub enum TreeStatus {
},
}

/// Like [`TreeStatus`], but distilled down to its variant.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum TreeStatusKind {
/// Something was added or scheduled to be added.
Addition,
/// Something was deleted.
Deletion,
/// A tracked entry was modified, which might mean:
///
/// * the content change, i.e. a file was changed
/// * the type changed, a file is now a symlink or something else
/// * the executable bit changed, so a file is now executable, or isn't anymore.
Modification,
/// An entry was renamed from `previous_path` to its current location.
///
/// Note that this may include any change already documented in [`Modification`](TreeStatusKind::Modification)
Rename,
}

/// Something that fully identifies the state of a [`TreeChange`].
#[derive(Debug, Clone, Copy)]
pub struct ChangeState {
Expand Down Expand Up @@ -167,7 +186,7 @@ pub struct IgnoredWorktreeChange {
status: IgnoredWorktreeTreeChangeStatus,
}

/// The type returned by [`worktree_changes()`](diff::worktree_status).
/// The type returned by [`worktree_changes()`](diff::worktree_changes).
#[derive(Debug, Clone)]
pub struct WorktreeChanges {
/// Changes that could be committed.
Expand Down Expand Up @@ -210,6 +229,18 @@ impl ModeFlags {
}
}

impl TreeStatus {
/// Learn what kind of status this is, useful if only this information is needed.
pub fn kind(&self) -> TreeStatusKind {
match self {
TreeStatus::Addition { .. } => TreeStatusKind::Addition,
TreeStatus::Deletion { .. } => TreeStatusKind::Deletion,
TreeStatus::Modification { .. } => TreeStatusKind::Modification,
TreeStatus::Rename { .. } => TreeStatusKind::Rename,
}
}
}

#[cfg(test)]
mod tests {
mod flags {
Expand Down
2 changes: 1 addition & 1 deletion crates/but-core/tests/core/diff/worktree_changes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1146,7 +1146,7 @@ fn unified_diffs(
worktree
.changes
.into_iter()
.map(|c| c.unified_diff(repo))
.map(|c| c.unified_diff(repo, 3))
.collect()
}

Expand Down
2 changes: 1 addition & 1 deletion crates/but-core/tests/core/json_samples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fn worktree_changes_unified_diffs_json_example() -> anyhow::Result<()> {
let diffs: Vec<UnifiedDiff> = but_core::diff::worktree_changes(&repo)?
.changes
.iter()
.map(|tree_change| tree_change.unified_diff(&repo))
.map(|tree_change| tree_change.unified_diff(&repo, 3))
.collect::<std::result::Result<_, _>>()?;
let actual = serde_json::to_string_pretty(&diffs)?;
insta::assert_snapshot!(actual, @r#"
Expand Down
Loading
Loading