1
1
//! Functions for applying patches to a work directory.
2
+ use super :: SourceError ;
3
+ use crate :: system_tools:: { SystemTools , Tool } ;
4
+ use itertools:: Itertools ;
5
+ use std:: process:: { Command , Output } ;
2
6
use std:: {
3
7
collections:: HashSet ,
4
- io:: { BufRead , BufReader , Write } ,
8
+ ffi:: OsStr ,
9
+ io:: { BufRead , BufReader } ,
5
10
path:: { Path , PathBuf } ,
6
11
process:: Stdio ,
7
12
} ;
8
13
9
- use super :: SourceError ;
10
- use crate :: system_tools:: { SystemTools , Tool } ;
11
-
12
14
fn parse_patch_file < P : AsRef < Path > > ( patch_file : P ) -> std:: io:: Result < HashSet < PathBuf > > {
13
15
let file = fs_err:: File :: open ( patch_file. as_ref ( ) ) ?;
14
16
let reader = BufReader :: new ( file) ;
@@ -115,6 +117,19 @@ pub(crate) fn apply_patches(
115
117
work_dir : & Path ,
116
118
recipe_dir : & Path ,
117
119
) -> Result < ( ) , SourceError > {
120
+ // Early out to avoid unnecessary work
121
+ if patches. is_empty ( ) {
122
+ return Ok ( ( ) ) ;
123
+ }
124
+
125
+ // Ensure that the working directory is a valid git directory.
126
+ let git_dir = work_dir. join ( ".git" ) ;
127
+ let _dot_git_dir = if !git_dir. exists ( ) {
128
+ Some ( TempDotGit :: setup ( work_dir) ?)
129
+ } else {
130
+ None
131
+ } ;
132
+
118
133
for patch_path_relative in patches {
119
134
let patch_file_path = recipe_dir. join ( patch_path_relative) ;
120
135
@@ -124,60 +139,137 @@ pub(crate) fn apply_patches(
124
139
return Err ( SourceError :: PatchNotFound ( patch_file_path) ) ;
125
140
}
126
141
127
- // Read the patch content into a string. This also normalizes line endings to LF.
128
- let patch_content_for_stdin =
129
- fs_err:: read_to_string ( & patch_file_path) . map_err ( SourceError :: Io ) ?;
130
-
131
142
let strip_level = guess_strip_level ( & patch_file_path, work_dir) ?;
132
143
133
- let mut cmd_builder = system_tools
134
- . call ( Tool :: Git )
135
- . map_err ( SourceError :: GitNotFound ) ?;
136
-
137
- cmd_builder
138
- . current_dir ( work_dir)
139
- . arg ( "apply" )
140
- . arg ( format ! ( "-p{}" , strip_level) )
141
- . arg ( "--ignore-space-change" )
142
- . arg ( "--ignore-whitespace" )
143
- . arg ( "--recount" )
144
- . stdin ( Stdio :: piped ( ) )
145
- . stdout ( Stdio :: piped ( ) )
146
- . stderr ( Stdio :: piped ( ) ) ;
147
-
148
- let mut child_process = cmd_builder. spawn ( ) . map_err ( SourceError :: Io ) ?;
149
-
150
- // Write the patch content to the child process's stdin.
151
- {
152
- if let Some ( mut child_stdin) = child_process. stdin . take ( ) {
153
- child_stdin
154
- . write_all ( patch_content_for_stdin. as_bytes ( ) )
155
- . map_err ( SourceError :: Io ) ?;
156
- } else {
157
- return Err ( SourceError :: Io ( std:: io:: Error :: new (
158
- std:: io:: ErrorKind :: Other ,
159
- "Failed to obtain stdin handle for git apply" ,
160
- ) ) ) ;
144
+ struct GitApplyAttempt {
145
+ command : Command ,
146
+ output : Output ,
147
+ }
148
+
149
+ let mut outputs = Vec :: new ( ) ;
150
+ for try_extra_flag in [ None , Some ( "--recount" ) ] {
151
+ let mut cmd_builder = system_tools
152
+ . call ( Tool :: Git )
153
+ . map_err ( SourceError :: GitNotFound ) ?;
154
+ cmd_builder
155
+ . current_dir ( work_dir)
156
+ . arg ( "apply" )
157
+ . arg ( format ! ( "-p{}" , strip_level) )
158
+ . arg ( "--verbose" )
159
+ . arg ( "--ignore-space-change" )
160
+ . arg ( "--ignore-whitespace" )
161
+ . args ( try_extra_flag. into_iter ( ) )
162
+ . arg ( patch_file_path. as_os_str ( ) )
163
+ . stdout ( Stdio :: piped ( ) )
164
+ . stderr ( Stdio :: piped ( ) ) ;
165
+
166
+ tracing:: debug!(
167
+ "Running: {} {}" ,
168
+ cmd_builder. get_program( ) . to_string_lossy( ) ,
169
+ cmd_builder
170
+ . get_args( )
171
+ . map( OsStr :: to_string_lossy)
172
+ . format( " " )
173
+ ) ;
174
+
175
+ let output = cmd_builder. output ( ) . map_err ( SourceError :: Io ) ?;
176
+ outputs. push ( GitApplyAttempt {
177
+ command : cmd_builder,
178
+ output : output. clone ( ) ,
179
+ } ) ;
180
+
181
+ if outputs
182
+ . last ( )
183
+ . expect ( "we just added an entry" )
184
+ . output
185
+ . status
186
+ . success ( )
187
+ {
188
+ break ;
161
189
}
162
190
}
163
191
164
- let output = child_process. wait_with_output ( ) . map_err ( SourceError :: Io ) ?;
192
+ // Check if the last output was successful, if not, we report all the errors.
193
+ let last_output = outputs. last ( ) . expect ( "we just added at least one entry" ) ;
194
+ if !last_output. output . status . success ( ) {
195
+ return Err ( SourceError :: PatchFailed ( format ! (
196
+ "{}\n `git apply` failed with a combination of flags.\n \n {}" ,
197
+ patch_path_relative. display( ) ,
198
+ outputs
199
+ . into_iter( )
200
+ . map(
201
+ |GitApplyAttempt {
202
+ output, command, ..
203
+ } | {
204
+ let stderr = String :: from_utf8_lossy( & output. stderr) ;
205
+ format!(
206
+ "With the che command:\n \n \t {} {}The output was:\n \n \t {}\n \n " ,
207
+ command. get_program( ) . to_string_lossy( ) ,
208
+ command. get_args( ) . map( OsStr :: to_string_lossy) . format( " " ) ,
209
+ stderr. lines( ) . format( "\n \t " )
210
+ )
211
+ }
212
+ )
213
+ . format( "\n \n " )
214
+ ) ) ) ;
215
+ }
165
216
166
- if !output. status . success ( ) {
167
- eprintln ! (
168
- "Failed to apply patch: {}" ,
169
- patch_file_path. to_string_lossy( )
170
- ) ;
171
- eprintln ! ( "Stdout: {}" , String :: from_utf8_lossy( & output. stdout) ) ;
172
- eprintln ! ( "Stderr: {}" , String :: from_utf8_lossy( & output. stderr) ) ;
173
- return Err ( SourceError :: PatchFailed (
174
- patch_file_path. to_string_lossy ( ) . to_string ( ) ,
175
- ) ) ;
217
+ // Sometimes git apply will skip the contents of a patch. This usually is *not* what we
218
+ // want, so we detect this behavior and return an error.
219
+ let stderr = String :: from_utf8_lossy ( & last_output. output . stderr ) ;
220
+ let skipped_patch = stderr
221
+ . lines ( )
222
+ . any ( |line| line. starts_with ( "Skipped patch " ) ) ;
223
+ if skipped_patch {
224
+ return Err ( SourceError :: PatchFailed ( format ! (
225
+ "{}\n `git apply` seems to have skipped some of the contents of the patch. The output of the command is:\n \n \t {}\n \n The command was invoked with:\n \n \t {} {}" ,
226
+ patch_path_relative. display( ) ,
227
+ stderr. lines( ) . format( "\n \t " ) ,
228
+ last_output. command. get_program( ) . to_string_lossy( ) ,
229
+ last_output
230
+ . command
231
+ . get_args( )
232
+ . map( OsStr :: to_string_lossy)
233
+ . format( " " )
234
+ ) ) ) ;
176
235
}
177
236
}
178
237
Ok ( ( ) )
179
238
}
180
239
240
+ /// A temporary .git directory that contains the bare minimum files and
241
+ /// directories needed for git to function as if the directory that contains
242
+ /// the .git directory is a proper git repository.
243
+ struct TempDotGit {
244
+ path : PathBuf ,
245
+ }
246
+
247
+ impl TempDotGit {
248
+ /// Creates a temporary .git directory in the specified root directory.
249
+ fn setup ( root : & Path ) -> std:: io:: Result < Self > {
250
+ // Initialize a temporary .git directory
251
+ let dot_git = root. join ( ".git" ) ;
252
+ fs_err:: create_dir ( & dot_git) ?;
253
+ let dot_git = TempDotGit { path : dot_git } ;
254
+
255
+ // Add the minimum number of files and directories to the .git directory that are needed for
256
+ // git to work
257
+ fs_err:: create_dir ( dot_git. path . join ( "objects" ) ) ?;
258
+ fs_err:: create_dir ( dot_git. path . join ( "refs" ) ) ?;
259
+ fs_err:: write ( dot_git. path . join ( "HEAD" ) , "ref: refs/heads/main" ) ?;
260
+
261
+ Ok ( dot_git)
262
+ }
263
+ }
264
+
265
+ impl Drop for TempDotGit {
266
+ fn drop ( & mut self ) {
267
+ fs_err:: remove_dir_all ( & self . path ) . unwrap_or_else ( |e| {
268
+ eprintln ! ( "Failed to remove temporary .git directory: {}" , e) ;
269
+ } ) ;
270
+ }
271
+ }
272
+
181
273
#[ cfg( test) ]
182
274
mod tests {
183
275
use crate :: source:: copy_dir:: CopyDir ;
@@ -294,4 +386,27 @@ mod tests {
294
386
let cmake_list = fs_err:: read_to_string ( & cmake_list) . unwrap ( ) ;
295
387
assert ! ( cmake_list. contains( "cmake_minimum_required(VERSION 3.12)" ) ) ;
296
388
}
389
+
390
+ #[ test]
391
+ fn test_apply_git_patch_in_git_ignored ( ) {
392
+ let ( tempdir, _) = setup_patch_test_dir ( ) ;
393
+
394
+ // Initialize a temporary .git directory at the root of the temporary directory. This makes
395
+ // git take the working directory is in a git repository.
396
+ let _temp_dot_git = TempDotGit :: setup ( tempdir. path ( ) ) . unwrap ( ) ;
397
+
398
+ // Apply the patches in the working directory
399
+ apply_patches (
400
+ & SystemTools :: new ( ) ,
401
+ & [ PathBuf :: from ( "0001-increase-minimum-cmake-version.patch" ) ] ,
402
+ & tempdir. path ( ) . join ( "workdir" ) ,
403
+ & tempdir. path ( ) . join ( "patches" ) ,
404
+ )
405
+ . expect ( "Patch 0001-increase-minimum-cmake-version.patch should apply successfully" ) ;
406
+
407
+ // Read the cmake list file and make sure that it contains `cmake_minimum_required(VERSION 3.12)`
408
+ let cmake_list = tempdir. path ( ) . join ( "workdir/CMakeLists.txt" ) ;
409
+ let cmake_list = fs_err:: read_to_string ( & cmake_list) . unwrap ( ) ;
410
+ assert ! ( cmake_list. contains( "cmake_minimum_required(VERSION 3.12)" ) ) ;
411
+ }
297
412
}
0 commit comments