Skip to content

Commit 413676a

Browse files
authored
Merge pull request #10088 from Byron/fix
Fixup to_additive_hunks()
2 parents 8b18a23 + e745168 commit 413676a

File tree

4 files changed

+285
-8
lines changed

4 files changed

+285
-8
lines changed

crates/but-workspace/src/commit_engine/hunks.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ pub fn apply_hunks(
3030
for selected_hunk in hunks {
3131
let old_skips = (selected_hunk.old_start as usize)
3232
.checked_sub(old_cursor)
33-
.context("hunks must be in order from top to bottom of the file")?;
33+
.with_context(|| {
34+
format!(
35+
"`old_skips = start({start}) - cursor({old_cursor})` mut be >= 0, hunk = {selected_hunk:?}",
36+
start = selected_hunk.old_start
37+
)
38+
})?;
3439
let catchup_base_lines = old_iter.by_ref().take(old_skips);
3540
for old_line in catchup_base_lines {
3641
result_image.extend_from_slice(old_line);

crates/but-workspace/src/commit_engine/tree/mod.rs

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::commit_engine::{MoveSourceCommit, RejectionReason, apply_hunks};
1+
use crate::commit_engine::{HunkRange, MoveSourceCommit, RejectionReason, apply_hunks};
22
use crate::{DiffSpec, HunkHeader};
33
use bstr::{BStr, ByteSlice};
44
use but_core::{RepositoryExt, UnifiedDiff};
@@ -7,6 +7,7 @@ use gix::merge::tree::TreatAsUnresolved;
77
use gix::object::tree::EntryKind;
88
use gix::prelude::ObjectIdExt;
99
use std::borrow::Cow;
10+
use std::collections::BTreeMap;
1011
use std::io::Read;
1112
use std::path::Path;
1213

@@ -348,9 +349,12 @@ pub(crate) fn worktree_file_to_git_in_buf(
348349
/// hunks from `hunks_to_keep` that couldn't be associated with `worktree_hunks_no_context` because they weren't included.
349350
///
350351
/// `worktree_hunks` is the hunks with a given amount of context, usually 3, and it's used to quickly select original hunks
351-
/// without selection.
352-
/// `hunks_to_keep` indicate that they are a selection by marking the other side with `0,0`, i.e. `-1,2 +0,0` selects old `1,2`,
353-
/// and `-0,0 +2,3` selects new `2,3`.
352+
/// without sub-selection, which is needed when no sub-selections are specified for all hunks. Those with sub-selections and without
353+
/// can be mixed freely though.
354+
///
355+
/// `hunks_to_keep` indicate that they are a selection of either old or new by marking the other side with `0,0`, i.e. `-1,2 +0,0` selects *old* `1,2`,
356+
/// and `-0,0 +2,3` selects *new* `2,3`. Our job here is to rebuild the original hunk selections from that, as if the user had
357+
/// selected them in the UI, and we want to recreate the hunks that would match their selection.
354358
///
355359
/// The idea here is that `worktree_hunks_no_context` is the smallest-possible hunks that still contain the designated
356360
/// selections in the old or new image respectively. This is necessary to maintain the right order in the face of context lines.
@@ -417,7 +421,144 @@ fn to_additive_hunks(
417421
}
418422
rejected.push(sh);
419423
}
420-
(hunks_to_commit, rejected)
424+
425+
if !hunks_to_commit
426+
.iter()
427+
.zip(hunks_to_commit.iter().skip(1))
428+
.all(|(prev, next)| prev < next)
429+
{
430+
tracing::info!("Using alternative hunk algorithm for {hunks_to_commit:?}");
431+
to_additive_hunks_fallback(
432+
hunks_to_commit.into_iter().map(
433+
|HunkHeader {
434+
old_start,
435+
old_lines,
436+
new_start,
437+
new_lines,
438+
}| {
439+
if old_lines == 0 {
440+
HunkHeader {
441+
old_start: 0,
442+
old_lines: 0,
443+
new_start,
444+
new_lines,
445+
}
446+
} else {
447+
HunkHeader {
448+
old_start,
449+
old_lines,
450+
new_start: 0,
451+
new_lines: 0,
452+
}
453+
}
454+
},
455+
),
456+
worktree_hunks,
457+
worktree_hunks_no_context,
458+
)
459+
} else {
460+
(hunks_to_commit, rejected)
461+
}
462+
}
463+
464+
/// This algorithm is better when the basic one fails, but both have their merit.
465+
/// Right now this is a brute-force one or the other approach, but we could also apply them selectively.
466+
/// Note that what we really want to simulate is what the UI shows. But since the UI also doesn't really know hunks,
467+
/// we have to fiddle it together here, at least we know the hunks themselves.
468+
///
469+
/// Note that this algorithm is kind of the opposite of what people would expect if it's run where `to_additive_hunks()` works.
470+
/// But here we are… just making this work.
471+
#[allow(clippy::indexing_slicing)]
472+
fn to_additive_hunks_fallback(
473+
hunks_to_keep: impl IntoIterator<Item = HunkHeader>,
474+
worktree_hunks: &[HunkHeader],
475+
worktree_hunks_no_context: &[HunkHeader],
476+
) -> (Vec<HunkHeader>, Vec<HunkHeader>) {
477+
let mut rejected = Vec::new();
478+
479+
let mut map = BTreeMap::<HunkHeader, Vec<HunkHeader>>::new();
480+
for selected_hunk in hunks_to_keep {
481+
let sh = selected_hunk;
482+
if sh.new_range().is_null() {
483+
if let Some(wh) = worktree_hunks_no_context
484+
.iter()
485+
.find(|wh| wh.old_range().contains(sh.old_range()))
486+
{
487+
let hunks = map.entry(*wh).or_default();
488+
if let Some(existing_wh_pos) = hunks.iter_mut().position(|wh| wh.old_lines == 0) {
489+
let wh = &mut hunks[existing_wh_pos];
490+
wh.old_start = sh.old_start;
491+
wh.old_lines = sh.old_lines;
492+
493+
let iter = existing_wh_pos..hunks.len();
494+
for (prev, next) in iter.clone().zip(iter.skip(1)) {
495+
hunks[next].old_start = hunks[prev].old_range().end();
496+
}
497+
} else {
498+
hunks.push(HunkHeader {
499+
old_start: sh.old_start,
500+
old_lines: sh.old_lines,
501+
new_start: hunks
502+
.last()
503+
.and_then(|wh| {
504+
wh.new_range()
505+
.contains(HunkRange {
506+
start: wh.new_start,
507+
lines: 0,
508+
})
509+
.then_some(wh.new_range().end())
510+
})
511+
.unwrap_or(wh.new_start),
512+
new_lines: 0,
513+
});
514+
}
515+
continue;
516+
}
517+
} else if sh.old_range().is_null() {
518+
if let Some(wh) = worktree_hunks_no_context
519+
.iter()
520+
.find(|wh| wh.new_range().contains(sh.new_range()))
521+
{
522+
let hunks = map.entry(*wh).or_default();
523+
524+
if let Some(existing_wh_pos) = hunks.iter_mut().position(|wh| wh.new_lines == 0) {
525+
let wh = &mut hunks[existing_wh_pos];
526+
wh.new_start = sh.new_start;
527+
wh.new_lines = sh.new_lines;
528+
529+
let iter = existing_wh_pos..hunks.len();
530+
for (prev, next) in iter.clone().zip(iter.skip(1)) {
531+
hunks[next].new_start = hunks[prev].new_range().end();
532+
}
533+
} else {
534+
hunks.push(HunkHeader {
535+
old_start: hunks
536+
.last()
537+
.and_then(|wh| {
538+
wh.old_range()
539+
.contains(HunkRange {
540+
start: wh.old_start,
541+
lines: 0,
542+
})
543+
.then_some(wh.old_range().end())
544+
})
545+
.unwrap_or(wh.old_start),
546+
old_lines: 0,
547+
new_start: sh.new_start,
548+
new_lines: sh.new_lines,
549+
});
550+
}
551+
continue;
552+
}
553+
} else if worktree_hunks.contains(&sh) {
554+
let hunks = map.entry(sh).or_default();
555+
hunks.push(sh);
556+
continue;
557+
}
558+
rejected.push(sh);
559+
}
560+
561+
(map.into_values().flatten().collect(), rejected)
421562
}
422563

423564
#[cfg(test)]

crates/but-workspace/src/commit_engine/tree/tests.rs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
mod to_additive_hunks {
2-
use super::super::to_additive_hunks;
2+
use crate::commit_engine::tree::to_additive_hunks;
33
use crate::utils::hunk_header;
44

55
#[test]
@@ -240,4 +240,135 @@ mod to_additive_hunks {
240240
)
241241
"#);
242242
}
243+
244+
#[test]
245+
fn real_world_issue() {
246+
let wth = vec![hunk_header("-1,214", "+1,55")];
247+
let wth0 = vec![
248+
hunk_header("-4,13", "+4,0"),
249+
hunk_header("-18,19", "+5,1"),
250+
hunk_header("-38,79", "+7,3"),
251+
hunk_header("-118,64", "+11,0"),
252+
hunk_header("-183,1", "+12,1"),
253+
hunk_header("-185,15", "+14,2"),
254+
hunk_header("-201,5", "+17,5"),
255+
hunk_header("-207,1", "+23,26"),
256+
hunk_header("-209,3", "+50,3"),
257+
];
258+
259+
let actual = to_additive_hunks(
260+
[
261+
hunk_header("-0,0", "+23,26"),
262+
hunk_header("-0,0", "+50,3"),
263+
hunk_header("-207,1", "+0,0"),
264+
hunk_header("-209,3", "+0,0"),
265+
],
266+
&wth,
267+
&wth0,
268+
);
269+
insta::assert_debug_snapshot!(actual, @r#"
270+
(
271+
[
272+
HunkHeader("-207,1", "+23,26"),
273+
HunkHeader("-209,3", "+50,3"),
274+
],
275+
[],
276+
)
277+
"#);
278+
279+
let actual = to_additive_hunks(
280+
[
281+
hunk_header("-0,0", "+23,1"),
282+
hunk_header("-0,0", "+25,1"),
283+
hunk_header("-0,0", "+27,2"),
284+
hunk_header("-0,0", "+30,2"),
285+
hunk_header("-0,0", "+50,3"),
286+
hunk_header("-207,1", "+0,0"),
287+
hunk_header("-209,1", "+0,0"),
288+
hunk_header("-211,1", "+0,0"),
289+
],
290+
&wth,
291+
&wth0,
292+
);
293+
insta::assert_debug_snapshot!(actual, @r#"
294+
(
295+
[
296+
HunkHeader("-207,1", "+23,1"),
297+
HunkHeader("-208,0", "+25,1"),
298+
HunkHeader("-208,0", "+27,2"),
299+
HunkHeader("-208,0", "+30,2"),
300+
HunkHeader("-209,1", "+50,3"),
301+
HunkHeader("-211,1", "+53,0"),
302+
],
303+
[],
304+
)
305+
"#);
306+
307+
let actual = to_additive_hunks(
308+
[
309+
hunk_header("-207,1", "+0,0"),
310+
hunk_header("-209,1", "+0,0"),
311+
hunk_header("-211,1", "+0,0"),
312+
hunk_header("-0,0", "+23,1"),
313+
hunk_header("-0,0", "+25,1"),
314+
hunk_header("-0,0", "+27,2"),
315+
hunk_header("-0,0", "+30,2"),
316+
hunk_header("-0,0", "+50,3"),
317+
],
318+
&wth,
319+
&wth0,
320+
);
321+
insta::assert_debug_snapshot!(actual, @r#"
322+
(
323+
[
324+
HunkHeader("-207,1", "+23,1"),
325+
HunkHeader("-208,0", "+25,1"),
326+
HunkHeader("-208,0", "+27,2"),
327+
HunkHeader("-208,0", "+30,2"),
328+
HunkHeader("-209,1", "+50,3"),
329+
HunkHeader("-211,1", "+53,0"),
330+
],
331+
[],
332+
)
333+
"#);
334+
}
335+
336+
#[test]
337+
fn only_selections_workspace_example() {
338+
let wth = vec![hunk_header("-1,10", "+1,10")];
339+
let actual = to_additive_hunks(
340+
[
341+
// commit NOT '2,3' of the old
342+
hunk_header("-2,2", "+0,0"),
343+
// commit NOT '6,7' of the old
344+
hunk_header("-6,2", "+0,0"),
345+
// commit NOT '9' of the old
346+
hunk_header("-9,1", "+0,0"),
347+
// commit NOT '10' of the old
348+
hunk_header("-10,1", "+0,0"),
349+
// commit '11' of the new
350+
hunk_header("-0,0", "+1,1"),
351+
// commit '15,16' of the new
352+
hunk_header("-0,0", "+5,2"),
353+
// commit '19,20' of the new
354+
hunk_header("-0,0", "+9,2"),
355+
],
356+
&wth,
357+
&wth,
358+
);
359+
insta::assert_debug_snapshot!(actual, @r#"
360+
(
361+
[
362+
HunkHeader("-2,2", "+1,0"),
363+
HunkHeader("-6,2", "+1,0"),
364+
HunkHeader("-9,1", "+1,0"),
365+
HunkHeader("-10,1", "+1,0"),
366+
HunkHeader("-11,0", "+1,1"),
367+
HunkHeader("-11,0", "+5,2"),
368+
HunkHeader("-11,0", "+9,2"),
369+
],
370+
[],
371+
)
372+
"#);
373+
}
243374
}

crates/but-workspace/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ impl From<TreeChange> for DiffSpec {
122122
}
123123

124124
/// The header of a hunk that represents a change to a file.
125-
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
125+
#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
126126
#[serde(rename_all = "camelCase")]
127127
pub struct HunkHeader {
128128
/// The 1-based line number at which the previous version of the file started.

0 commit comments

Comments
 (0)