11//! Functions for applying patches to a work directory.
22use std:: {
3- io :: Write ,
4- ops :: Deref ,
3+ collections :: HashSet ,
4+ io :: { BufRead , BufReader , Write } ,
55 path:: { Path , PathBuf } ,
66 process:: Stdio ,
77} ;
88
9- use gitpatch:: Patch ;
10-
119use super :: SourceError ;
1210use crate :: system_tools:: { SystemTools , Tool } ;
1311
12+ fn parse_patch_file < P : AsRef < Path > > ( patch_file : P ) -> std:: io:: Result < HashSet < PathBuf > > {
13+ let file = fs_err:: File :: open ( patch_file. as_ref ( ) ) ?;
14+ let reader = BufReader :: new ( file) ;
15+ let mut affected_files = HashSet :: new ( ) ;
16+
17+ // Common patch file patterns
18+ let unified_pattern = "--- " ;
19+ let git_pattern = "diff --git " ;
20+ let traditional_pattern = "Index: " ;
21+ let mut is_git = false ;
22+ for line in reader. lines ( ) {
23+ let line = line?;
24+
25+ if let Some ( git_line) = line. strip_prefix ( git_pattern) {
26+ is_git = true ;
27+ if let Some ( file_path) = extract_git_file_path ( git_line) {
28+ affected_files. insert ( file_path) ;
29+ }
30+ } else if let Some ( unified_line) = line. strip_prefix ( unified_pattern) {
31+ if is_git || unified_line. contains ( "/dev/null" ) {
32+ continue ;
33+ }
34+ if let Some ( file_path) = clean_file_path ( unified_line) {
35+ affected_files. insert ( file_path) ;
36+ }
37+ } else if let Some ( traditional_line) = line. strip_prefix ( traditional_pattern) {
38+ if let Some ( file_path) = clean_file_path ( traditional_line) {
39+ affected_files. insert ( file_path) ;
40+ }
41+ }
42+ }
43+
44+ Ok ( affected_files)
45+ }
46+
47+ fn clean_file_path ( path_str : & str ) -> Option < PathBuf > {
48+ let path = path_str. trim ( ) ;
49+
50+ // Handle timestamp in unified diff format (file.txt\t2023-05-10 10:00:00)
51+ let path = path. split ( '\t' ) . next ( ) . unwrap_or ( path) ;
52+
53+ // Skip /dev/null entries
54+ if path. is_empty ( ) || path == "/dev/null" {
55+ return None ;
56+ }
57+
58+ Some ( PathBuf :: from ( path) )
59+ }
60+
61+ fn extract_git_file_path ( content : & str ) -> Option < PathBuf > {
62+ // Format: "a/file.txt b/file.txt"
63+ let parts: Vec < & str > = content. split ( ' ' ) . collect ( ) ;
64+ if parts. len ( ) >= 2 {
65+ let a_file = parts[ 0 ] ;
66+ if a_file. starts_with ( "a/" ) && a_file != "a/dev/null" {
67+ return Some ( PathBuf :: from ( & a_file) ) ;
68+ }
69+ }
70+
71+ None
72+ }
73+
1474/// We try to guess the "strip level" for a patch application. This is done by checking
1575/// what files are present in the work directory and comparing them to the paths in the patch.
1676///
@@ -21,21 +81,22 @@ use crate::system_tools::{SystemTools, Tool};
2181/// But in our work directory, we only have `contents/file.c`. In this case, we can guess that the
2282/// strip level is 2 and we can apply the patch successfully.
2383fn guess_strip_level ( patch : & Path , work_dir : & Path ) -> Result < usize , std:: io:: Error > {
24- let text = fs_err:: read_to_string ( patch) ?;
25- let Ok ( patches) = Patch :: from_multiple ( & text) else {
26- return Ok ( 1 ) ;
27- } ;
84+ let patched_files = parse_patch_file ( patch) . map_err ( |e| {
85+ std:: io:: Error :: new (
86+ std:: io:: ErrorKind :: InvalidData ,
87+ format ! ( "Failed to parse patch file: {}" , e) ,
88+ )
89+ } ) ?;
2890
2991 // Try to guess the strip level by checking if the path exists in the work directory
30- for p in patches {
31- let path = PathBuf :: from ( p. old . path . deref ( ) ) ;
92+ for file in patched_files {
3293 // This means the patch is creating an entirely new file so we can't guess the strip level
33- if path == Path :: new ( "/dev/null" ) {
94+ if file == Path :: new ( "/dev/null" ) {
3495 continue ;
3596 }
36- for strip_level in 0 ..path . components ( ) . count ( ) {
97+ for strip_level in 0 ..file . components ( ) . count ( ) {
3798 let mut new_path = work_dir. to_path_buf ( ) ;
38- new_path. extend ( path . components ( ) . skip ( strip_level) ) ;
99+ new_path. extend ( file . components ( ) . skip ( strip_level) ) ;
39100 if new_path. exists ( ) {
40101 return Ok ( strip_level) ;
41102 }
@@ -122,7 +183,6 @@ mod tests {
122183 use crate :: source:: copy_dir:: CopyDir ;
123184
124185 use super :: * ;
125- use gitpatch:: Patch ;
126186 use line_ending:: LineEnding ;
127187 use tempfile:: TempDir ;
128188
@@ -139,13 +199,31 @@ mod tests {
139199 continue ;
140200 }
141201
142- let ps = fs_err:: read_to_string ( & patch_path) . unwrap ( ) ;
143- let parsed = Patch :: from_multiple ( & ps) ;
202+ let parsed = parse_patch_file ( & patch_path) ;
203+ if let Err ( e) = & parsed {
204+ eprintln ! ( "Failed to parse patch: {} {}" , patch_path. display( ) , e) ;
205+ }
144206
145207 println ! ( "Parsing patch: {} {}" , patch_path. display( ) , parsed. is_ok( ) ) ;
146208 }
147209 }
148210
211+ #[ test]
212+ fn get_affected_files ( ) {
213+ let manifest_dir = PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) ) ;
214+ let patches_dir = manifest_dir. join ( "test-data/patch_application/patches" ) ;
215+
216+ let patched_paths = parse_patch_file ( patches_dir. join ( "test.patch" ) ) . unwrap ( ) ;
217+ assert_eq ! ( patched_paths. len( ) , 1 ) ;
218+ assert ! ( patched_paths. contains( & PathBuf :: from( "a/text.md" ) ) ) ;
219+
220+ let patched_paths =
221+ parse_patch_file ( patches_dir. join ( "0001-increase-minimum-cmake-version.patch" ) )
222+ . unwrap ( ) ;
223+ assert_eq ! ( patched_paths. len( ) , 1 ) ;
224+ assert ! ( patched_paths. contains( & PathBuf :: from( "a/CMakeLists.txt" ) ) ) ;
225+ }
226+
149227 fn setup_patch_test_dir ( ) -> ( TempDir , PathBuf ) {
150228 let manifest_dir = PathBuf :: from ( env ! ( "CARGO_MANIFEST_DIR" ) ) ;
151229 let patch_test_dir = manifest_dir. join ( "test-data/patch_application" ) ;
0 commit comments