Skip to content

Commit

Permalink
New crate for calculating hunk dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
mtsgrd committed Oct 21, 2024
1 parent 45467f3 commit b55825a
Show file tree
Hide file tree
Showing 16 changed files with 951 additions and 12 deletions.
20 changes: 20 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ members = [
"crates/gitbutler-stack-api",
"crates/gitbutler-stack",
"crates/gitbutler-patch-reference",
"crates/gitbutler-hunk-dependency",
]
resolver = "2"

Expand Down Expand Up @@ -92,6 +93,7 @@ gitbutler-oxidize = { path = "crates/gitbutler-oxidize" }
gitbutler-stack-api = { path = "crates/gitbutler-stack-api" }
gitbutler-stack = { path = "crates/gitbutler-stack" }
gitbutler-patch-reference = { path = "crates/gitbutler-patch-reference" }
gitbutler-hunk-dependency = { path = "crates/gitbutler-hunk-dependency" }

[profile.release]
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
Expand Down
1 change: 1 addition & 0 deletions crates/gitbutler-branch-actions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ gitbutler-oxidize.workspace = true
gitbutler-stack.workspace = true
gitbutler-stack-api.workspace = true
gitbutler-patch-reference.workspace = true
gitbutler-hunk-dependency.workspace = true
serde = { workspace = true, features = ["std"] }
bstr.workspace = true
diffy = "0.4.0"
Expand Down
13 changes: 1 addition & 12 deletions crates/gitbutler-branch-actions/src/hunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use std::{
};

use gitbutler_diff::{GitHunk, Hunk, HunkHash};
use gitbutler_hunk_dependency::locks::HunkLock;
use gitbutler_serde::BStringForFrontend;
use gitbutler_stack::StackId;
use itertools::Itertools;
use md5::Digest;
use serde::Serialize;
Expand Down Expand Up @@ -42,17 +42,6 @@ pub struct VirtualBranchHunk {
pub poisoned: bool,
}

// A hunk is locked when it depends on changes in commits that are in your
// workspace. A hunk can be locked to more than one branch if it overlaps
// with more than one committed hunk.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Copy)]
#[serde(rename_all = "camelCase")]
pub struct HunkLock {
pub branch_id: StackId,
#[serde(with = "gitbutler_serde::oid")]
pub commit_id: git2::Oid,
}

/// Lifecycle
impl VirtualBranchHunk {
pub(crate) fn gen_id(new_start: u32, new_lines: u32) -> String {
Expand Down
25 changes: 25 additions & 0 deletions crates/gitbutler-hunk-dependency/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "gitbutler-hunk-dependency"
version = "0.0.0"
edition = "2021"
authors = ["GitButler <[email protected]>"]
publish = false

[dependencies]
anyhow = "1.0.86"
git2.workspace = true
gix = { workspace = true, features = [] }
gitbutler-diff.workspace = true
gitbutler-reference.workspace = true
gitbutler-serde.workspace = true
gitbutler-stack.workspace = true
gitbutler-id.workspace = true
itertools = "0.13"
serde = { workspace = true, features = ["std"] }
bstr.workspace = true
tokio.workspace = true
uuid = { workspace = true, features = ["v4", "fast-rng"] }

[[test]]
name = "blame"
path = "tests/mod.rs"
76 changes: 76 additions & 0 deletions crates/gitbutler-hunk-dependency/src/diff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use anyhow::{anyhow, Context};

#[derive(Debug, PartialEq, Clone)]
pub struct InputDiff {
pub old_start: i32,
pub old_lines: i32,
pub new_start: i32,
pub new_lines: i32,
}

impl InputDiff {
pub fn net_lines(&self) -> i32 {
self.new_lines - self.old_lines
}
}

fn count_context_lines<I, S>(iter: I) -> i32
where
I: Iterator<Item = S>,
S: AsRef<str>,
{
iter.take_while(|line| {
let line_ref = line.as_ref(); // Convert to &str
!line_ref.starts_with('-') && !line_ref.starts_with('+')
})
.fold(0i32, |acc, _| acc + 1)
}

impl TryFrom<String> for InputDiff {
fn try_from(value: String) -> Result<Self, anyhow::Error> {
parse_diff(value)
}

type Error = anyhow::Error;
}

impl TryFrom<&str> for InputDiff {
fn try_from(value: &str) -> Result<Self, anyhow::Error> {
parse_diff(value)
}

type Error = anyhow::Error;
}

fn parse_diff(value: impl AsRef<str>) -> Result<InputDiff, anyhow::Error> {
let value = value.as_ref();
let header = value.lines().next().context("No header found")?;
if !header.starts_with("@@") {
return Err(anyhow!("Malformed undiff"));
}
let parts: Vec<&str> = header.split_whitespace().collect();
let (old_start, old_lines) = parse_header(parts[1]);
let (new_start, new_lines) = parse_header(parts[2]);
let head_context_lines = count_context_lines(value.lines().skip(1).take(3));
let tail_context_lines = count_context_lines(value.rsplit_terminator('\n').take(3));
let context_lines = head_context_lines + tail_context_lines;

Ok(InputDiff {
old_start: old_start + head_context_lines,
old_lines: old_lines - context_lines,
new_start: new_start + head_context_lines,
new_lines: new_lines - context_lines,
})
}

fn parse_header(hunk_info: &str) -> (i32, i32) {
let hunk_info = hunk_info.trim_start_matches(&['-', '+'][..]); // Remove the leading '-' or '+'
let parts: Vec<&str> = hunk_info.split(',').collect();
let start = parts[0].parse().unwrap();
let lines = if parts.len() > 1 {
parts[1].parse().unwrap()
} else {
1
};
(start, lines)
}
24 changes: 24 additions & 0 deletions crates/gitbutler-hunk-dependency/src/hunk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use gitbutler_stack::StackId;

#[derive(Debug, PartialEq, Clone)]
pub struct HunkRange {
pub stack_id: StackId,
pub commit_id: git2::Oid,
pub start: i32,
pub lines: i32,
pub line_shift: i32,
}

impl HunkRange {
fn end(&self) -> i32 {
self.start + self.lines - 1
}

pub fn intersects(&self, start: i32, lines: i32) -> bool {
self.end() >= start && self.start < start + lines
}

pub fn contains(&self, start: i32, lines: i32) -> bool {
start > self.start && start + lines <= self.end()
}
}
6 changes: 6 additions & 0 deletions crates/gitbutler-hunk-dependency/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod diff;
pub(crate) mod hunk;
pub mod locks;
pub(crate) mod path;
pub mod stack;
pub mod workspace;
69 changes: 69 additions & 0 deletions crates/gitbutler-hunk-dependency/src/locks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use std::{collections::HashMap, path::PathBuf};

use gitbutler_diff::{Hunk, HunkHash};
use gitbutler_stack::StackId;
use itertools::Itertools;
use serde::Serialize;

use crate::workspace::{InputStack, WorkspaceRanges};

// Type defined in gitbutler-branch-actions and can't be imported here.
type BranchStatus = HashMap<PathBuf, Vec<gitbutler_diff::GitHunk>>;

// A hunk is locked when it depends on changes in commits that are in your
// workspace. A hunk can be locked to more than one branch if it overlaps
// with more than one committed hunk.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Copy)]
#[serde(rename_all = "camelCase")]
pub struct HunkLock {
// TODO: Rename this stack_id.
pub branch_id: StackId,
#[serde(with = "gitbutler_serde::oid")]
pub commit_id: git2::Oid,
}

pub struct HunkDependencyOptions<'a> {
// Uncommitted changes in workspace.
pub workdir: &'a BranchStatus,

/// A nested map of committed diffs per stack, commit, and path.
pub stacks: Vec<InputStack>,
}

/// Returns a map from hunk hash to hunk locks.
///
/// To understand if any uncommitted changes depend on (intersect) any existing
/// changes we first transform the branch specific line numbers to global ones,
/// then look for places they intersect.
///
/// TODO: Change terminology to talk about dependencies instead of locks.
pub fn compute_hunk_locks(
options: HunkDependencyOptions,
) -> anyhow::Result<HashMap<HunkHash, Vec<HunkLock>>> {
let HunkDependencyOptions { workdir, stacks } = options;

// Transforms local line numbers to global line numbers.
let workspace_ranges = WorkspaceRanges::new(stacks);

let mut result = HashMap::new();

for (path, workspace_hunks) in workdir {
for hunk in workspace_hunks {
let hunk_dependencies =
workspace_ranges.intersection(path, hunk.old_start as i32, hunk.old_lines as i32);
let hash = Hunk::hash_diff(&hunk.diff_lines);
let locks = hunk_dependencies
.iter()
.map(|dependency| HunkLock {
commit_id: dependency.commit_id,
branch_id: dependency.stack_id,
})
.collect_vec();

if !locks.is_empty() {
result.insert(hash, locks);
}
}
}
Ok(result)
}
Loading

0 comments on commit b55825a

Please sign in to comment.