Skip to content

Commit 8e07f95

Browse files
committed
Ensure hunks are sorted correctly when committing (#9198)
1 parent 4476f23 commit 8e07f95

File tree

4 files changed

+213
-50
lines changed

4 files changed

+213
-50
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: 70 additions & 37 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,76 +349,108 @@ 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.
357361
/// Note that the order of changes is still affected by what which selection comes first, i.e. old and new, or vice versa, if these
358362
/// selections are in the same hunk.
363+
#[allow(clippy::indexing_slicing)]
359364
fn to_additive_hunks(
360365
hunks_to_keep: impl IntoIterator<Item = HunkHeader>,
361366
worktree_hunks: &[HunkHeader],
362367
worktree_hunks_no_context: &[HunkHeader],
363368
) -> (Vec<HunkHeader>, Vec<HunkHeader>) {
364-
let mut hunks_to_commit = Vec::new();
365369
let mut rejected = Vec::new();
366-
let mut previous = HunkHeader {
367-
old_start: 1,
368-
old_lines: 0,
369-
new_start: 1,
370-
new_lines: 0,
371-
};
372-
let mut last_wh = None;
370+
371+
let mut map = BTreeMap::<HunkHeader, Vec<HunkHeader>>::new();
373372
for selected_hunk in hunks_to_keep {
374373
let sh = selected_hunk;
375374
if sh.new_range().is_null() {
376375
if let Some(wh) = worktree_hunks_no_context
377376
.iter()
378377
.find(|wh| wh.old_range().contains(sh.old_range()))
379378
{
380-
if last_wh != Some(*wh) {
381-
last_wh = Some(*wh);
382-
previous.new_start = wh.new_start;
379+
let hunks = map.entry(*wh).or_default();
380+
if let Some(existing_wh_pos) = hunks.iter_mut().position(|wh| wh.old_lines == 0) {
381+
let wh = &mut hunks[existing_wh_pos];
382+
wh.old_start = sh.old_start;
383+
wh.old_lines = sh.old_lines;
384+
385+
let iter = existing_wh_pos..hunks.len();
386+
for (prev, next) in iter.clone().zip(iter.skip(1)) {
387+
hunks[next].old_start = hunks[prev].old_range().end();
388+
}
389+
} else {
390+
hunks.push(HunkHeader {
391+
old_start: sh.old_start,
392+
old_lines: sh.old_lines,
393+
new_start: hunks
394+
.last()
395+
.and_then(|wh| {
396+
wh.new_range()
397+
.contains(HunkRange {
398+
start: wh.new_start,
399+
lines: 0,
400+
})
401+
.then_some(wh.new_range().end())
402+
})
403+
.unwrap_or(wh.new_start),
404+
new_lines: 0,
405+
});
383406
}
384-
hunks_to_commit.push(HunkHeader {
385-
old_start: sh.old_start,
386-
old_lines: sh.old_lines,
387-
new_start: previous.new_start,
388-
new_lines: 0,
389-
});
390-
previous.old_start = sh.old_range().end();
391407
continue;
392408
}
393409
} else if sh.old_range().is_null() {
394410
if let Some(wh) = worktree_hunks_no_context
395411
.iter()
396412
.find(|wh| wh.new_range().contains(sh.new_range()))
397413
{
398-
if last_wh != Some(*wh) {
399-
last_wh = Some(*wh);
400-
previous.old_start = wh.old_start;
414+
let hunks = map.entry(*wh).or_default();
415+
416+
if let Some(existing_wh_pos) = hunks.iter_mut().position(|wh| wh.new_lines == 0) {
417+
let wh = &mut hunks[existing_wh_pos];
418+
wh.new_start = sh.new_start;
419+
wh.new_lines = sh.new_lines;
420+
421+
let iter = existing_wh_pos..hunks.len();
422+
for (prev, next) in iter.clone().zip(iter.skip(1)) {
423+
hunks[next].new_start = hunks[prev].new_range().end();
424+
}
425+
} else {
426+
hunks.push(HunkHeader {
427+
old_start: hunks
428+
.last()
429+
.and_then(|wh| {
430+
wh.old_range()
431+
.contains(HunkRange {
432+
start: wh.old_start,
433+
lines: 0,
434+
})
435+
.then_some(wh.old_range().end())
436+
})
437+
.unwrap_or(wh.old_start),
438+
old_lines: 0,
439+
new_start: sh.new_start,
440+
new_lines: sh.new_lines,
441+
});
401442
}
402-
hunks_to_commit.push(HunkHeader {
403-
old_start: previous.old_start,
404-
old_lines: 0,
405-
new_start: sh.new_start,
406-
new_lines: sh.new_lines,
407-
});
408-
previous.new_start = sh.new_range().end();
409443
continue;
410444
}
411445
} else if worktree_hunks.contains(&sh) {
412-
previous.old_start = sh.old_range().end();
413-
previous.new_start = sh.new_range().end();
414-
last_wh = Some(sh);
415-
hunks_to_commit.push(sh);
446+
let hunks = map.entry(sh).or_default();
447+
hunks.push(sh);
416448
continue;
417449
}
418450
rejected.push(sh);
419451
}
420-
(hunks_to_commit, rejected)
452+
453+
(map.into_values().flatten().collect(), rejected)
421454
}
422455

423456
#[cfg(test)]

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

Lines changed: 136 additions & 11 deletions
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]
@@ -78,8 +78,7 @@ mod to_additive_hunks {
7878
), @r#"
7979
(
8080
[
81-
HunkHeader("-1,0", "+1,1"),
82-
HunkHeader("-5,2", "+2,0"),
81+
HunkHeader("-5,2", "+1,1"),
8382
HunkHeader("-7,0", "+10,1"),
8483
],
8584
[],
@@ -96,15 +95,53 @@ mod to_additive_hunks {
9695
), @r#"
9796
(
9897
[
99-
HunkHeader("-1,1", "+1,0"),
100-
HunkHeader("-2,0", "+5,2"),
98+
HunkHeader("-1,1", "+5,2"),
10199
HunkHeader("-10,1", "+7,0"),
102100
],
103101
[],
104102
)
105103
"#);
106104
}
107105

106+
#[test]
107+
fn only_selections_workspace_example() {
108+
let wth = vec![hunk_header("-1,10", "+1,10")];
109+
let actual = to_additive_hunks(
110+
[
111+
// commit NOT '2,3' of the old
112+
hunk_header("-2,2", "+0,0"),
113+
// commit NOT '6,7' of the old
114+
hunk_header("-6,2", "+0,0"),
115+
// commit NOT '9' of the old
116+
hunk_header("-9,1", "+0,0"),
117+
// commit NOT '10' of the old
118+
hunk_header("-10,1", "+0,0"),
119+
// commit '11' of the new
120+
hunk_header("-0,0", "+1,1"),
121+
// commit '15,16' of the new
122+
hunk_header("-0,0", "+5,2"),
123+
// commit '19,20' of the new
124+
hunk_header("-0,0", "+9,2"),
125+
],
126+
&wth,
127+
&wth,
128+
);
129+
insta::assert_debug_snapshot!(actual, @r#"
130+
(
131+
[
132+
HunkHeader("-2,2", "+1,0"),
133+
HunkHeader("-6,2", "+1,0"),
134+
HunkHeader("-9,1", "+1,0"),
135+
HunkHeader("-10,1", "+1,0"),
136+
HunkHeader("-11,0", "+1,1"),
137+
HunkHeader("-11,0", "+5,2"),
138+
HunkHeader("-11,0", "+9,2"),
139+
],
140+
[],
141+
)
142+
"#);
143+
}
144+
108145
#[test]
109146
fn selections_and_full_hunks() {
110147
let wth = vec![
@@ -119,16 +156,14 @@ mod to_additive_hunks {
119156
// partial match to same hunk
120157
hunk_header("-15,2", "+0,0"),
121158
hunk_header("-0,0", "+22,3"),
122-
// Last hunk isn't used
123159
],
124160
&wth,
125161
&wth,
126162
), @r#"
127163
(
128164
[
129165
HunkHeader("-1,10", "+1,10"),
130-
HunkHeader("-15,2", "+20,0"),
131-
HunkHeader("-17,0", "+22,3"),
166+
HunkHeader("-15,2", "+22,3"),
132167
],
133168
[],
134169
)
@@ -147,7 +182,6 @@ mod to_additive_hunks {
147182
// full match
148183
hunk_header("-1,10", "+1,10"),
149184
hunk_header("-15,5", "+20,5"),
150-
// Last hunk isn't used
151185
],
152186
&wth,
153187
&wth,
@@ -221,8 +255,7 @@ mod to_additive_hunks {
221255
), @r#"
222256
(
223257
[
224-
HunkHeader("-96,1", "+96,0"),
225-
HunkHeader("-97,0", "+96,2"),
258+
HunkHeader("-96,1", "+96,2"),
226259
],
227260
[],
228261
)
@@ -240,4 +273,96 @@ mod to_additive_hunks {
240273
)
241274
"#);
242275
}
276+
277+
#[test]
278+
fn real_world_issue() {
279+
let wth = vec![hunk_header("-1,214", "+1,55")];
280+
let wth0 = vec![
281+
hunk_header("-4,13", "+4,0"),
282+
hunk_header("-18,19", "+5,1"),
283+
hunk_header("-38,79", "+7,3"),
284+
hunk_header("-118,64", "+11,0"),
285+
hunk_header("-183,1", "+12,1"),
286+
hunk_header("-185,15", "+14,2"),
287+
hunk_header("-201,5", "+17,5"),
288+
hunk_header("-207,1", "+23,26"),
289+
hunk_header("-209,3", "+50,3"),
290+
];
291+
292+
let actual = to_additive_hunks(
293+
[
294+
hunk_header("-0,0", "+23,26"),
295+
hunk_header("-0,0", "+50,3"),
296+
hunk_header("-207,1", "+0,0"),
297+
hunk_header("-209,3", "+0,0"),
298+
],
299+
&wth,
300+
&wth0,
301+
);
302+
insta::assert_debug_snapshot!(actual, @r#"
303+
(
304+
[
305+
HunkHeader("-207,1", "+23,26"),
306+
HunkHeader("-209,3", "+50,3"),
307+
],
308+
[],
309+
)
310+
"#);
311+
312+
let actual = to_additive_hunks(
313+
[
314+
hunk_header("-0,0", "+23,1"),
315+
hunk_header("-0,0", "+25,1"),
316+
hunk_header("-0,0", "+27,2"),
317+
hunk_header("-0,0", "+30,2"),
318+
hunk_header("-0,0", "+50,3"),
319+
hunk_header("-207,1", "+0,0"),
320+
hunk_header("-209,1", "+0,0"),
321+
hunk_header("-211,1", "+0,0"),
322+
],
323+
&wth,
324+
&wth0,
325+
);
326+
insta::assert_debug_snapshot!(actual, @r#"
327+
(
328+
[
329+
HunkHeader("-207,1", "+23,1"),
330+
HunkHeader("-208,0", "+25,1"),
331+
HunkHeader("-208,0", "+27,2"),
332+
HunkHeader("-208,0", "+30,2"),
333+
HunkHeader("-209,1", "+50,3"),
334+
HunkHeader("-211,1", "+53,0"),
335+
],
336+
[],
337+
)
338+
"#);
339+
340+
let actual = to_additive_hunks(
341+
[
342+
hunk_header("-207,1", "+0,0"),
343+
hunk_header("-209,1", "+0,0"),
344+
hunk_header("-211,1", "+0,0"),
345+
hunk_header("-0,0", "+23,1"),
346+
hunk_header("-0,0", "+25,1"),
347+
hunk_header("-0,0", "+27,2"),
348+
hunk_header("-0,0", "+30,2"),
349+
hunk_header("-0,0", "+50,3"),
350+
],
351+
&wth,
352+
&wth0,
353+
);
354+
insta::assert_debug_snapshot!(actual, @r#"
355+
(
356+
[
357+
HunkHeader("-207,1", "+23,1"),
358+
HunkHeader("-208,0", "+25,1"),
359+
HunkHeader("-208,0", "+27,2"),
360+
HunkHeader("-208,0", "+30,2"),
361+
HunkHeader("-209,1", "+50,3"),
362+
HunkHeader("-211,1", "+53,0"),
363+
],
364+
[],
365+
)
366+
"#);
367+
}
243368
}

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)