diff options
Diffstat (limited to 'src/lib/git/mod.rs')
| -rw-r--r-- | src/lib/git/mod.rs | 237 |
1 files changed, 236 insertions, 1 deletions
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs index ca7aa3f..c5b5f2e 100644 --- a/src/lib/git/mod.rs +++ b/src/lib/git/mod.rs | |||
| @@ -417,6 +417,11 @@ impl RepoActions for Repo { | |||
| 417 | ))?) | 417 | ))?) |
| 418 | .context(format!("failed to find commit {}", &commit))?; | 418 | .context(format!("failed to find commit {}", &commit))?; |
| 419 | let mut options = git2::EmailCreateOptions::default(); | 419 | let mut options = git2::EmailCreateOptions::default(); |
| 420 | // Explicitly set a/b prefixes so that the user's `diff.noprefix` git | ||
| 421 | // config (or any other prefix override) does not affect the generated | ||
| 422 | // patch. Patches must use standard a/b prefixes to be parseable by | ||
| 423 | // git2::Diff::from_buffer when applying them. | ||
| 424 | options.diff_options().old_prefix("a/").new_prefix("b/"); | ||
| 420 | if let Some((n, total)) = series_count { | 425 | if let Some((n, total)) = series_count { |
| 421 | options.subject_prefix(format!("PATCH {n}/{total}")); | 426 | options.subject_prefix(format!("PATCH {n}/{total}")); |
| 422 | } | 427 | } |
| @@ -695,9 +700,10 @@ impl RepoActions for Repo { | |||
| 695 | // let mut apply_opts = git2::ApplyOptions::new(); | 700 | // let mut apply_opts = git2::ApplyOptions::new(); |
| 696 | // apply_opts.check(false); | 701 | // apply_opts.check(false); |
| 697 | let mut existing_index = self.git_repo.index()?; | 702 | let mut existing_index = self.git_repo.index()?; |
| 703 | let normalized_content = normalize_diff_prefix(&patch.content); | ||
| 698 | let mut index = self.git_repo.apply_to_tree( | 704 | let mut index = self.git_repo.apply_to_tree( |
| 699 | &parent_tree, | 705 | &parent_tree, |
| 700 | &git2::Diff::from_buffer(patch.content.as_bytes())?, | 706 | &git2::Diff::from_buffer(normalized_content.as_bytes())?, |
| 701 | // Some(&mut apply_opts), | 707 | // Some(&mut apply_opts), |
| 702 | None, | 708 | None, |
| 703 | )?; | 709 | )?; |
| @@ -1067,6 +1073,98 @@ impl SignatureData { | |||
| 1067 | } | 1073 | } |
| 1068 | } | 1074 | } |
| 1069 | 1075 | ||
| 1076 | /// Normalize a patch that uses no-prefix diff format (produced by libgit2 with | ||
| 1077 | /// `--no-prefix`, e.g. `diff --git src/foo.rs src/foo.rs`) to the standard | ||
| 1078 | /// `a/`/`b/` prefix format that `git2::Diff::from_buffer` requires. | ||
| 1079 | /// | ||
| 1080 | /// If the patch already uses standard prefixes this function is a no-op. | ||
| 1081 | fn normalize_diff_prefix(content: &str) -> String { | ||
| 1082 | // Detect whether any diff header is missing the a/b prefix. | ||
| 1083 | // A standard header looks like: diff --git a/<path> b/<path> | ||
| 1084 | // A no-prefix header looks like: diff --git <path> <path> | ||
| 1085 | let needs_normalization = content | ||
| 1086 | .lines() | ||
| 1087 | .filter(|l| l.starts_with("diff --git ")) | ||
| 1088 | .any(|l| { | ||
| 1089 | let rest = &l["diff --git ".len()..]; | ||
| 1090 | !rest.starts_with("a/") | ||
| 1091 | }); | ||
| 1092 | |||
| 1093 | if !needs_normalization { | ||
| 1094 | return content.to_string(); | ||
| 1095 | } | ||
| 1096 | |||
| 1097 | let mut out = String::with_capacity(content.len() + 64); | ||
| 1098 | for line in content.lines() { | ||
| 1099 | if let Some(rest) = line.strip_prefix("diff --git ") { | ||
| 1100 | // rest is "<old-path> <new-path>". For paths without spaces the | ||
| 1101 | // split is unambiguous. For paths with spaces we rely on the fact | ||
| 1102 | // that old == new for renames-in-place; we find the midpoint by | ||
| 1103 | // trying each space as a split and checking old == new. | ||
| 1104 | let (old, new) = split_diff_git_paths(rest); | ||
| 1105 | out.push_str("diff --git a/"); | ||
| 1106 | out.push_str(old); | ||
| 1107 | out.push_str(" b/"); | ||
| 1108 | out.push_str(new); | ||
| 1109 | } else if let Some(rest) = line.strip_prefix("--- ") { | ||
| 1110 | if rest == "/dev/null" { | ||
| 1111 | out.push_str(line); | ||
| 1112 | } else { | ||
| 1113 | out.push_str("--- a/"); | ||
| 1114 | out.push_str(rest); | ||
| 1115 | } | ||
| 1116 | } else if let Some(rest) = line.strip_prefix("+++ ") { | ||
| 1117 | if rest == "/dev/null" { | ||
| 1118 | out.push_str(line); | ||
| 1119 | } else { | ||
| 1120 | out.push_str("+++ b/"); | ||
| 1121 | out.push_str(rest); | ||
| 1122 | } | ||
| 1123 | } else { | ||
| 1124 | out.push_str(line); | ||
| 1125 | } | ||
| 1126 | out.push('\n'); | ||
| 1127 | } | ||
| 1128 | // Preserve trailing newline behaviour of the original. | ||
| 1129 | if !content.ends_with('\n') && out.ends_with('\n') { | ||
| 1130 | out.pop(); | ||
| 1131 | } | ||
| 1132 | out | ||
| 1133 | } | ||
| 1134 | |||
| 1135 | /// Split the path portion of a `diff --git <old> <new>` line into (old, new). | ||
| 1136 | /// | ||
| 1137 | /// For paths without spaces this is trivial. For paths with spaces we try each | ||
| 1138 | /// space as a candidate split point and return the first one where the two | ||
| 1139 | /// halves are equal (which is always the case for non-rename patches). If no | ||
| 1140 | /// equal split is found we fall back to splitting at the first space. | ||
| 1141 | fn split_diff_git_paths(rest: &str) -> (&str, &str) { | ||
| 1142 | // Fast path: no spaces → split at the single space | ||
| 1143 | if let Some(pos) = rest.find(' ') { | ||
| 1144 | let (old, new_with_space) = rest.split_at(pos); | ||
| 1145 | let new = &new_with_space[1..]; | ||
| 1146 | if !old.contains(' ') { | ||
| 1147 | return (old, new); | ||
| 1148 | } | ||
| 1149 | // Paths contain spaces: try each space as a split point. | ||
| 1150 | let bytes = rest.as_bytes(); | ||
| 1151 | for (i, &b) in bytes.iter().enumerate() { | ||
| 1152 | if b == b' ' { | ||
| 1153 | let candidate_old = &rest[..i]; | ||
| 1154 | let candidate_new = &rest[i + 1..]; | ||
| 1155 | if candidate_old == candidate_new { | ||
| 1156 | return (candidate_old, candidate_new); | ||
| 1157 | } | ||
| 1158 | } | ||
| 1159 | } | ||
| 1160 | // Fallback: split at first space | ||
| 1161 | (old, new) | ||
| 1162 | } else { | ||
| 1163 | // No space at all — malformed, return as-is | ||
| 1164 | (rest, rest) | ||
| 1165 | } | ||
| 1166 | } | ||
| 1167 | |||
| 1070 | fn extract_description_from_patch(patch: &nostr::Event) -> Result<String> { | 1168 | fn extract_description_from_patch(patch: &nostr::Event) -> Result<String> { |
| 1071 | if let Ok(desc) = tag_value(patch, "description") { | 1169 | if let Ok(desc) = tag_value(patch, "description") { |
| 1072 | return Ok(desc); | 1170 | return Ok(desc); |
| @@ -1138,6 +1236,142 @@ mod tests { | |||
| 1138 | 1236 | ||
| 1139 | use super::*; | 1237 | use super::*; |
| 1140 | 1238 | ||
| 1239 | mod normalize_diff_prefix { | ||
| 1240 | use super::*; | ||
| 1241 | |||
| 1242 | #[test] | ||
| 1243 | fn no_op_when_already_prefixed() { | ||
| 1244 | let patch = "\ | ||
| 1245 | diff --git a/src/foo.rs b/src/foo.rs\n\ | ||
| 1246 | index ce01362..a21e91c 100644\n\ | ||
| 1247 | --- a/src/foo.rs\n\ | ||
| 1248 | +++ b/src/foo.rs\n\ | ||
| 1249 | @@ -1 +1 @@\n\ | ||
| 1250 | -hello\n\ | ||
| 1251 | +world\n"; | ||
| 1252 | assert_eq!(normalize_diff_prefix(patch), patch); | ||
| 1253 | } | ||
| 1254 | |||
| 1255 | #[test] | ||
| 1256 | fn adds_prefix_to_no_prefix_diff() { | ||
| 1257 | let patch_no_prefix = "\ | ||
| 1258 | diff --git src/foo.rs src/foo.rs\n\ | ||
| 1259 | index ce01362..a21e91c 100644\n\ | ||
| 1260 | --- src/foo.rs\n\ | ||
| 1261 | +++ src/foo.rs\n\ | ||
| 1262 | @@ -1 +1 @@\n\ | ||
| 1263 | -hello\n\ | ||
| 1264 | +world\n"; | ||
| 1265 | let expected = "\ | ||
| 1266 | diff --git a/src/foo.rs b/src/foo.rs\n\ | ||
| 1267 | index ce01362..a21e91c 100644\n\ | ||
| 1268 | --- a/src/foo.rs\n\ | ||
| 1269 | +++ b/src/foo.rs\n\ | ||
| 1270 | @@ -1 +1 @@\n\ | ||
| 1271 | -hello\n\ | ||
| 1272 | +world\n"; | ||
| 1273 | assert_eq!(normalize_diff_prefix(patch_no_prefix), expected); | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | #[test] | ||
| 1277 | fn preserves_dev_null_for_new_file() { | ||
| 1278 | let patch_no_prefix = "\ | ||
| 1279 | diff --git src/new.rs src/new.rs\n\ | ||
| 1280 | new file mode 100644\n\ | ||
| 1281 | index 0000000..a21e91c\n\ | ||
| 1282 | --- /dev/null\n\ | ||
| 1283 | +++ src/new.rs\n\ | ||
| 1284 | @@ -0,0 +1 @@\n\ | ||
| 1285 | +hello\n"; | ||
| 1286 | let expected = "\ | ||
| 1287 | diff --git a/src/new.rs b/src/new.rs\n\ | ||
| 1288 | new file mode 100644\n\ | ||
| 1289 | index 0000000..a21e91c\n\ | ||
| 1290 | --- /dev/null\n\ | ||
| 1291 | +++ b/src/new.rs\n\ | ||
| 1292 | @@ -0,0 +1 @@\n\ | ||
| 1293 | +hello\n"; | ||
| 1294 | assert_eq!(normalize_diff_prefix(patch_no_prefix), expected); | ||
| 1295 | } | ||
| 1296 | |||
| 1297 | #[test] | ||
| 1298 | fn preserves_dev_null_for_deleted_file() { | ||
| 1299 | let patch_no_prefix = "\ | ||
| 1300 | diff --git src/old.rs src/old.rs\n\ | ||
| 1301 | deleted file mode 100644\n\ | ||
| 1302 | index a21e91c..0000000\n\ | ||
| 1303 | --- src/old.rs\n\ | ||
| 1304 | +++ /dev/null\n\ | ||
| 1305 | @@ -1 +0,0 @@\n\ | ||
| 1306 | -hello\n"; | ||
| 1307 | let expected = "\ | ||
| 1308 | diff --git a/src/old.rs b/src/old.rs\n\ | ||
| 1309 | deleted file mode 100644\n\ | ||
| 1310 | index a21e91c..0000000\n\ | ||
| 1311 | --- a/src/old.rs\n\ | ||
| 1312 | +++ /dev/null\n\ | ||
| 1313 | @@ -1 +0,0 @@\n\ | ||
| 1314 | -hello\n"; | ||
| 1315 | assert_eq!(normalize_diff_prefix(patch_no_prefix), expected); | ||
| 1316 | } | ||
| 1317 | |||
| 1318 | #[test] | ||
| 1319 | fn handles_multiple_files() { | ||
| 1320 | let patch_no_prefix = "\ | ||
| 1321 | diff --git src/foo.rs src/foo.rs\n\ | ||
| 1322 | index ce01362..a21e91c 100644\n\ | ||
| 1323 | --- src/foo.rs\n\ | ||
| 1324 | +++ src/foo.rs\n\ | ||
| 1325 | @@ -1 +1 @@\n\ | ||
| 1326 | -hello\n\ | ||
| 1327 | +world\n\ | ||
| 1328 | diff --git src/bar.rs src/bar.rs\n\ | ||
| 1329 | index 1234567..abcdef0 100644\n\ | ||
| 1330 | --- src/bar.rs\n\ | ||
| 1331 | +++ src/bar.rs\n\ | ||
| 1332 | @@ -1 +1 @@\n\ | ||
| 1333 | -foo\n\ | ||
| 1334 | +baz\n"; | ||
| 1335 | let expected = "\ | ||
| 1336 | diff --git a/src/foo.rs b/src/foo.rs\n\ | ||
| 1337 | index ce01362..a21e91c 100644\n\ | ||
| 1338 | --- a/src/foo.rs\n\ | ||
| 1339 | +++ b/src/foo.rs\n\ | ||
| 1340 | @@ -1 +1 @@\n\ | ||
| 1341 | -hello\n\ | ||
| 1342 | +world\n\ | ||
| 1343 | diff --git a/src/bar.rs b/src/bar.rs\n\ | ||
| 1344 | index 1234567..abcdef0 100644\n\ | ||
| 1345 | --- a/src/bar.rs\n\ | ||
| 1346 | +++ b/src/bar.rs\n\ | ||
| 1347 | @@ -1 +1 @@\n\ | ||
| 1348 | -foo\n\ | ||
| 1349 | +baz\n"; | ||
| 1350 | assert_eq!(normalize_diff_prefix(patch_no_prefix), expected); | ||
| 1351 | } | ||
| 1352 | |||
| 1353 | #[test] | ||
| 1354 | fn libgit2_can_parse_normalized_no_prefix_diff() { | ||
| 1355 | // Verify that after normalization, git2::Diff::from_buffer succeeds | ||
| 1356 | let patch_no_prefix = "\ | ||
| 1357 | diff --git src/foo.rs src/foo.rs\n\ | ||
| 1358 | index ce01362..a21e91c 100644\n\ | ||
| 1359 | --- src/foo.rs\n\ | ||
| 1360 | +++ src/foo.rs\n\ | ||
| 1361 | @@ -1 +1 @@\n\ | ||
| 1362 | -hello\n\ | ||
| 1363 | +world\n"; | ||
| 1364 | let normalized = normalize_diff_prefix(patch_no_prefix); | ||
| 1365 | let result = git2::Diff::from_buffer(normalized.as_bytes()); | ||
| 1366 | assert!( | ||
| 1367 | result.is_ok(), | ||
| 1368 | "libgit2 failed to parse normalized diff: {:?}", | ||
| 1369 | result.err() | ||
| 1370 | ); | ||
| 1371 | assert_eq!(result.unwrap().deltas().count(), 1); | ||
| 1372 | } | ||
| 1373 | } | ||
| 1374 | |||
| 1141 | mod git_config_item_local { | 1375 | mod git_config_item_local { |
| 1142 | use super::*; | 1376 | use super::*; |
| 1143 | 1377 | ||
| @@ -2609,4 +2843,5 @@ mod tests { | |||
| 2609 | Ok(()) | 2843 | Ok(()) |
| 2610 | } | 2844 | } |
| 2611 | } | 2845 | } |
| 2846 | |||
| 2612 | } | 2847 | } |