History

Reproduce unified diff issue

A change on 2x context lines will have two much context from the top.

Sebastian Thiel committed 1 year ago

Commit 5e642981 parent dab97f72 files · +193 −34Browse files

Unified · Split

2 files
  1. gix-diff/src/blob/unified_diff.rs +74 −34
  2. gix-diff/tests/diff/blob/unified_diff.rs +119 −0
gix-diff/src/blob/unified_diff.rs+74−34
@@ -77,6 +77,10 @@ pub(super) mod _impl { · 76 unmodified lines
77 77
78 use super::{ConsumeHunk, ContextSize, NewlineSeparator};78 use super::{ConsumeHunk, ContextSize, NewlineSeparator};
79 79
80 const CONTEXT: char = ' ';
81 const ADDITION: char = '+';
82 const REMOVAL: char = '-';
83
80 /// A [`Sink`] that creates a textual diff in the format typically output by git or `gnu-diff` if the `-u` option is used,84 /// A [`Sink`] that creates a textual diff in the format typically output by git or `gnu-diff` if the `-u` option is used,
81 /// and passes it in full to a consumer.85 /// and passes it in full to a consumer.
82 pub struct UnifiedDiff<'a, T, D>86 pub struct UnifiedDiff<'a, T, D>
@@ -88,18 +92,25 @@ pub(super) mod _impl { · 5 unmodified lines
88 after: &'a [Token],92 after: &'a [Token],
89 interner: &'a Interner<T>,93 interner: &'a Interner<T>,
90 94
91 pos: u32,95 /// The 0-based start position in the 'before' tokens for the accumulated hunk for display in the header.
92 before_hunk_start: u32,96 before_hunk_start: u32,
93 after_hunk_start: u32,97 /// The size of the accumulated 'before' hunk in lines for display in the header.
94 before_hunk_len: u32,98 before_hunk_len: u32,
99 /// The 0-based start position in the 'after' tokens for the accumulated hunk for display in the header.
100 after_hunk_start: u32,
101 /// The size of the accumulated 'after' hunk in lines.
95 after_hunk_len: u32,102 after_hunk_len: u32,
103 // An index into `before` and the context line to print next,
104 // or `None` if this value was never computed to be the correct starting point for an accumulated hunk.
105 ctx_pos: Option<u32>,
106
96 /// Symmetrical context before and after the changed hunk.107 /// Symmetrical context before and after the changed hunk.
97 ctx_size: u32,108 ctx_size: u32,
109 newline: NewlineSeparator<'a>,
98 110
99 buffer: Vec<u8>,111 buffer: Vec<u8>,
100 header_buf: String,112 header_buf: String,
101 delegate: D,113 delegate: D,
102 newline: NewlineSeparator<'a>,
103 114
104 err: Option<std::io::Error>,115 err: Option<std::io::Error>,
105 }116 }
@@ -122,19 +133,22 @@ pub(super) mod _impl { · 16 unmodified lines
122 context_size: ContextSize,133 context_size: ContextSize,
123 ) -> Self {134 ) -> Self {
124 Self {135 Self {
136 interner: &input.interner,
137 before: &input.before,
138 after: &input.after,
139
125 before_hunk_start: 0,140 before_hunk_start: 0,
126 after_hunk_start: 0,
127 before_hunk_len: 0,141 before_hunk_len: 0,
128 after_hunk_len: 0,142 after_hunk_len: 0,
143 after_hunk_start: 0,
144 ctx_pos: None,
145
146 ctx_size: context_size.symmetrical,
147 newline: newline_separator,
148
129 buffer: Vec::with_capacity(8),149 buffer: Vec::with_capacity(8),
130 header_buf: String::new(),150 header_buf: String::new(),
131 delegate: consume_hunk,151 delegate: consume_hunk,
132 interner: &input.interner,
133 before: &input.before,
134 after: &input.after,
135 pos: 0,
136 ctx_size: context_size.symmetrical,
137 newline: newline_separator,
138 152
139 err: None,153 err: None,
140 }154 }
@@ -158,23 +172,25 @@ pub(super) mod _impl { · 17 unmodified lines
158 }172 }
159 }173 }
160 174
161 fn flush(&mut self) -> std::io::Result<()> {175 fn flush_accumulated_hunk(&mut self) -> std::io::Result<()> {
162 if self.before_hunk_len == 0 && self.after_hunk_len == 0 {176 if self.nothing_to_flush() {
163 return Ok(());177 return Ok(());
164 }178 }
165 179
166 let end = (self.pos + self.ctx_size).min(self.before.len() as u32);180 let ctx_pos = self.ctx_pos.expect("has been set if we started a hunk");
167 self.update_pos(end, end);181 let end = (ctx_pos + self.ctx_size).min(self.before.len() as u32);
182 self.print_context_and_update_pos(ctx_pos..end, end);
168 183
184 let hunk_start = self.before_hunk_start + 1;
185 let hunk_end = self.after_hunk_start + 1;
169 self.header_buf.clear();186 self.header_buf.clear();
170
171 std::fmt::Write::write_fmt(187 std::fmt::Write::write_fmt(
172 &mut self.header_buf,188 &mut self.header_buf,
173 format_args!(189 format_args!(
174 "@@ -{},{} +{},{} @@{nl}",190 "@@ -{},{} +{},{} @@{nl}",
175 self.before_hunk_start + 1,191 hunk_start,
176 self.before_hunk_len,192 self.before_hunk_len,
177 self.after_hunk_start + 1,193 hunk_end,
178 self.after_hunk_len,194 self.after_hunk_len,
179 nl = match self.newline {195 nl = match self.newline {
180 NewlineSeparator::AfterHeaderAndLine(nl) | NewlineSeparator::AfterHeaderAndWhenNeeded(nl) => {196 NewlineSeparator::AfterHeaderAndLine(nl) | NewlineSeparator::AfterHeaderAndWhenNeeded(nl) => {
@@ -185,26 +201,35 @@ pub(super) mod _impl { · 4 unmodified lines
185 )201 )
186 .map_err(|err| std::io::Error::new(ErrorKind::Other, err))?;202 .map_err(|err| std::io::Error::new(ErrorKind::Other, err))?;
187 self.delegate.consume_hunk(203 self.delegate.consume_hunk(
188 self.before_hunk_start + 1,204 hunk_start,
189 self.before_hunk_len,205 self.before_hunk_len,
190 self.after_hunk_start + 1,206 hunk_end,
191 self.after_hunk_len,207 self.after_hunk_len,
192 &self.header_buf,208 &self.header_buf,
193 &self.buffer,209 &self.buffer,
194 )?;210 )?;
195 self.buffer.clear();211
196 self.before_hunk_len = 0;212 self.reset_hunks();
197 self.after_hunk_len = 0;
198 Ok(())213 Ok(())
199 }214 }
200 215
201 fn update_pos(&mut self, print_to: u32, move_to: u32) {216 fn print_context_and_update_pos(&mut self, print: Range<u32>, move_to: u32) {
202 self.print_tokens(&self.before[self.pos as usize..print_to as usize], ' ');217 self.print_tokens(&self.before[print.start as usize..print.end as usize], CONTEXT);
203 let len = print_to - self.pos;218 let len = print.end - print.start;
204 self.pos = move_to;219 self.ctx_pos = Some(move_to);
205 self.before_hunk_len += len;220 self.before_hunk_len += len;
206 self.after_hunk_len += len;221 self.after_hunk_len += len;
207 }222 }
223
224 fn reset_hunks(&mut self) {
225 self.buffer.clear();
226 self.before_hunk_len = 0;
227 self.after_hunk_len = 0;
228 }
229
230 fn nothing_to_flush(&self) -> bool {
231 self.before_hunk_len == 0 && self.after_hunk_len == 0
232 }
208 }233 }
209 234
210 impl<T, D> Sink for UnifiedDiff<'_, T, D>235 impl<T, D> Sink for UnifiedDiff<'_, T, D>
@@ -218,24 +243,39 @@ pub(super) mod _impl { · 7 unmodified lines
218 if self.err.is_some() {243 if self.err.is_some() {
219 return;244 return;
220 }245 }
221 if before.start - self.pos > 2 * self.ctx_size {246 let start_next_hunk = self
222 if let Err(err) = self.flush() {247 .ctx_pos
248 .is_some_and(|ctx_pos| before.start - ctx_pos > 2 * self.ctx_size);
249 if start_next_hunk {
250 if let Err(err) = self.flush_accumulated_hunk() {
223 self.err = Some(err);251 self.err = Some(err);
224 return;252 return;
225 }253 }
226 self.pos = before.start - self.ctx_size;254 let ctx_pos = before.start - self.ctx_size;
227 self.before_hunk_start = self.pos;255 self.ctx_pos = Some(ctx_pos);
256 self.before_hunk_start = ctx_pos;
228 self.after_hunk_start = after.start - self.ctx_size;257 self.after_hunk_start = after.start - self.ctx_size;
229 }258 }
230 self.update_pos(before.start, before.end);259 let ctx_pos = match self.ctx_pos {
260 None => {
261 // TODO: can this be made so the code above does the job?
262 let ctx_pos = before.start.saturating_sub(self.ctx_size);
263 self.before_hunk_start = ctx_pos;
264 self.after_hunk_start = after.start.saturating_sub(self.ctx_size);
265 ctx_pos
266 }
267 Some(pos) => pos,
268 };
269 self.print_context_and_update_pos(ctx_pos..before.start, before.end);
231 self.before_hunk_len += before.end - before.start;270 self.before_hunk_len += before.end - before.start;
232 self.after_hunk_len += after.end - after.start;271 self.after_hunk_len += after.end - after.start;
233 self.print_tokens(&self.before[before.start as usize..before.end as usize], '-');272
234 self.print_tokens(&self.after[after.start as usize..after.end as usize], '+');273 self.print_tokens(&self.before[before.start as usize..before.end as usize], REMOVAL);
274 self.print_tokens(&self.after[after.start as usize..after.end as usize], ADDITION);
235 }275 }
236 276
237 fn finish(mut self) -> Self::Out {277 fn finish(mut self) -> Self::Out {
238 if let Err(err) = self.flush() {278 if let Err(err) = self.flush_accumulated_hunk() {
239 self.err = Some(err);279 self.err = Some(err);
240 }280 }
241 if let Some(err) = self.err {281 if let Some(err) = self.err {
gix-diff/tests/diff/blob/unified_diff.rs+119−0
@@ -170,6 +170,125 @@ fn context_overlap_by_one_line_move_down() -> crate::Result { · 169 unmodified lines
170 Ok(())170 Ok(())
171}171}
172 172
173#[test]
174fn added_on_top_keeps_context_correctly_sized() -> crate::Result {
175 let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10";
176 let b = "1\n2\n3\n4\n4.5\n5\n6\n7\n8\n9\n10";
177
178 let a = gix_diff::blob::sources::lines_with_terminator(a);
179 let b = gix_diff::blob::sources::lines_with_terminator(b);
180 let interner = gix_diff::blob::intern::InternedInput::new(a, b);
181
182 let actual = gix_diff::blob::diff(
183 Algorithm::Myers,
184 &interner,
185 UnifiedDiff::new(
186 &interner,
187 String::new(),
188 NewlineSeparator::AfterHeaderAndWhenNeeded("\n"),
189 ContextSize::symmetrical(3),
190 ),
191 )?;
192 // TODO: fix this
193 insta::assert_snapshot!(actual, @r"
194 @@ -2,6 +2,7 @@
195 2
196 3
197 4
198 +4.5
199 5
200 6
201 7
202 ");
203
204 let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10";
205 let b = "1\n2\n3\n4\n5\n6\n6.5\n7\n8\n9\n10";
206
207 let a = gix_diff::blob::sources::lines_with_terminator(a);
208 let b = gix_diff::blob::sources::lines_with_terminator(b);
209 let interner = gix_diff::blob::intern::InternedInput::new(a, b);
210
211 let actual = gix_diff::blob::diff(
212 Algorithm::Myers,
213 &interner,
214 UnifiedDiff::new(
215 &interner,
216 String::new(),
217 NewlineSeparator::AfterHeaderAndWhenNeeded("\n"),
218 ContextSize::symmetrical(3),
219 ),
220 )?;
221
222 insta::assert_snapshot!(actual, @r"
223 @@ -4,6 +4,7 @@
224 4
225 5
226 6
227 +6.5
228 7
229 8
230 9
231 ");
232 let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10";
233 let b = "1\n2\n3\n3.5\n4\n5\n6\n7\n8\n9\n10";
234
235 let a = gix_diff::blob::sources::lines_with_terminator(a);
236 let b = gix_diff::blob::sources::lines_with_terminator(b);
237 let interner = gix_diff::blob::intern::InternedInput::new(a, b);
238
239 let actual = gix_diff::blob::diff(
240 Algorithm::Myers,
241 &interner,
242 UnifiedDiff::new(
243 &interner,
244 String::new(),
245 NewlineSeparator::AfterHeaderAndWhenNeeded("\n"),
246 ContextSize::symmetrical(3),
247 ),
248 )?;
249
250 insta::assert_snapshot!(actual, @r"
251 @@ -1,6 +1,7 @@
252 1
253 2
254 3
255 +3.5
256 4
257 5
258 6
259 ");
260
261 // From the end, for good measure
262 let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10";
263 let b = "1\n2\n3\n4\n5\n6\n7\n7.5\n8\n9\n10";
264
265 let a = gix_diff::blob::sources::lines_with_terminator(a);
266 let b = gix_diff::blob::sources::lines_with_terminator(b);
267 let interner = gix_diff::blob::intern::InternedInput::new(a, b);
268
269 let actual = gix_diff::blob::diff(
270 Algorithm::Myers,
271 &interner,
272 UnifiedDiff::new(
273 &interner,
274 String::new(),
275 NewlineSeparator::AfterHeaderAndWhenNeeded("\n"),
276 ContextSize::symmetrical(3),
277 ),
278 )?;
279 insta::assert_snapshot!(actual, @r"
280 @@ -5,6 +5,7 @@
281 5
282 6
283 7
284 +7.5
285 8
286 9
287 10
288 ");
289 Ok(())
290}
291
173#[test]292#[test]
174fn removed_modified_added_with_newlines_in_tokens() -> crate::Result {293fn removed_modified_added_with_newlines_in_tokens() -> crate::Result {
175 let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10";294 let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10";