@@ -3,12 +3,13 @@ use clap::crate_version;
3
3
use cli_table:: Table ;
4
4
use cli_table:: { print_stderr, WithTitle } ;
5
5
use include_dir:: { include_dir, Dir } ;
6
- use miette:: { bail, Result } ;
6
+ use miette:: { bail, IntoDiagnostic , Result } ;
7
7
use nix:: sys:: signal;
8
8
use nix:: unistd:: Pid ;
9
9
use once_cell:: sync:: Lazy ;
10
10
use serde:: Deserialize ;
11
11
use sha2:: Digest ;
12
+ use similar:: { ChangeTag , TextDiff } ;
12
13
use std:: collections:: HashMap ;
13
14
use std:: io:: Write ;
14
15
use std:: os:: unix:: { fs:: PermissionsExt , process:: CommandExt } ;
@@ -154,14 +155,6 @@ impl Devenv {
154
155
std:: fs:: create_dir_all ( & target) . expect ( "Failed to create target directory" ) ;
155
156
}
156
157
157
- // fails if any of the required files already exists
158
- for filename in REQUIRED_FILES {
159
- let file_path = target. join ( filename) ;
160
- if file_path. exists ( ) && !EXISTING_REQUIRED_FILES . contains ( & filename) {
161
- bail ! ( "File already exists {}" , file_path. display( ) ) ;
162
- }
163
- }
164
-
165
158
for filename in REQUIRED_FILES {
166
159
info ! ( "Creating {}" , filename) ;
167
160
@@ -183,7 +176,15 @@ impl Devenv {
183
176
} )
184
177
. expect ( "Failed to append to existing file" ) ;
185
178
} else {
186
- std:: fs:: write ( & target_path, path. contents ( ) ) . expect ( "Failed to write file" ) ;
179
+ if target_path. exists ( ) && !EXISTING_REQUIRED_FILES . contains ( & filename) {
180
+ if let Some ( utf8_contents) = path. contents_utf8 ( ) {
181
+ confirm_overwrite ( & target_path, utf8_contents. to_string ( ) ) ?;
182
+ } else {
183
+ bail ! ( "Failed to read file contents as UTF-8" ) ;
184
+ }
185
+ } else {
186
+ std:: fs:: write ( & target_path, path. contents ( ) ) . expect ( "Failed to write file" ) ;
187
+ }
187
188
}
188
189
}
189
190
@@ -200,6 +201,120 @@ impl Devenv {
200
201
Ok ( ( ) )
201
202
}
202
203
204
+ pub async fn generate (
205
+ & mut self ,
206
+ description : Option < String > ,
207
+ host : & str ,
208
+ exclude : Vec < PathBuf > ,
209
+ disable_telemetry : bool ,
210
+ ) -> Result < ( ) > {
211
+ let client = reqwest:: Client :: new ( ) ;
212
+ let mut request = client
213
+ . post ( host)
214
+ . query ( & [ ( "disable_telemetry" , disable_telemetry) ] )
215
+ . header ( reqwest:: header:: USER_AGENT , crate_version ! ( ) ) ;
216
+
217
+ let ( asyncwriter, asyncreader) = tokio:: io:: duplex ( 256 * 1024 ) ;
218
+ let streamreader = tokio_util:: io:: ReaderStream :: new ( asyncreader) ;
219
+
220
+ let ( body_sender, body) = match description {
221
+ Some ( desc) => {
222
+ request = request. query ( & [ ( "q" , desc) ] ) ;
223
+ ( None , None )
224
+ }
225
+ None => {
226
+ let git_output = std:: process:: Command :: new ( "git" )
227
+ . args ( [ "ls-files" , "-z" ] )
228
+ . output ( )
229
+ . map_err ( |_| {
230
+ miette:: miette!( "Failed to get list of files from git ls-files" )
231
+ } ) ?;
232
+
233
+ let files = String :: from_utf8_lossy ( & git_output. stdout )
234
+ . split ( '\0' )
235
+ . filter ( |s| !s. is_empty ( ) )
236
+ . filter ( |s| !binaryornot:: is_binary ( s) . unwrap_or ( false ) )
237
+ . map ( PathBuf :: from)
238
+ . collect :: < Vec < _ > > ( ) ;
239
+
240
+ if files. is_empty ( ) {
241
+ warn ! ( "No files found. Are you in a git repository?" ) ;
242
+ return Ok ( ( ) ) ;
243
+ }
244
+
245
+ if let Some ( stderr) = String :: from_utf8 ( git_output. stderr ) . ok ( ) {
246
+ if !stderr. is_empty ( ) {
247
+ warn ! ( "{}" , & stderr) ;
248
+ }
249
+ }
250
+
251
+ let body = reqwest:: Body :: wrap_stream ( streamreader) ;
252
+
253
+ request = request
254
+ . body ( body)
255
+ . header ( reqwest:: header:: CONTENT_TYPE , "application/x-tar" ) ;
256
+
257
+ ( Some ( tokio_tar:: Builder :: new ( asyncwriter) ) , Some ( files) )
258
+ }
259
+ } ;
260
+
261
+ info ! ( "Generating devenv.nix and devenv.yaml, this should take about a minute ..." ) ;
262
+
263
+ let response_future = request. send ( ) ;
264
+
265
+ let tar_task = async {
266
+ if let ( Some ( mut builder) , Some ( files) ) = ( body_sender, body) {
267
+ for path in files {
268
+ if path. is_file ( ) && !exclude. iter ( ) . any ( |exclude| path. starts_with ( exclude) ) {
269
+ builder. append_path ( & path) . await ?;
270
+ }
271
+ }
272
+ builder. finish ( ) . await ?;
273
+ }
274
+ Ok :: < ( ) , std:: io:: Error > ( ( ) )
275
+ } ;
276
+
277
+ let ( response, _) = tokio:: join!( response_future, tar_task) ;
278
+
279
+ let response = response. into_diagnostic ( ) ?;
280
+ let status = response. status ( ) ;
281
+ if !status. is_success ( ) {
282
+ let error_text = & response
283
+ . text ( )
284
+ . await
285
+ . unwrap_or_else ( |_| "No error details available" . to_string ( ) ) ;
286
+ bail ! (
287
+ "Failed to generate (HTTP {}): {}" ,
288
+ & status. as_u16( ) ,
289
+ match serde_json:: from_str:: <serde_json:: Value >( error_text) {
290
+ Ok ( json) => json[ "message" ]
291
+ . as_str( )
292
+ . map( String :: from)
293
+ . unwrap_or_else( || error_text. clone( ) ) ,
294
+ Err ( _) => error_text. clone( ) ,
295
+ }
296
+ ) ;
297
+ }
298
+
299
+ let response_json: GenerateResponse = response. json ( ) . await . expect ( "Failed to parse JSON." ) ;
300
+
301
+ confirm_overwrite ( Path :: new ( "devenv.nix" ) , response_json. devenv_nix ) ?;
302
+ confirm_overwrite ( Path :: new ( "devenv.yaml" ) , response_json. devenv_yaml ) ?;
303
+
304
+ info ! (
305
+ "{}" ,
306
+ indoc:: formatdoc!( "
307
+ Generated devenv.nix and devenv.yaml 🎉
308
+
309
+ Treat these as templates and open an issue at https://github.com/cachix/devenv/issues if you think we can do better!
310
+
311
+ Start by running:
312
+
313
+ $ devenv shell
314
+ " ) ) ;
315
+ Ok ( ( ) )
316
+ }
317
+
203
318
pub fn inputs_add ( & mut self , name : & str , url : & str , follows : & [ String ] ) -> Result < ( ) > {
204
319
self . config . add_input ( name, url, follows) ;
205
320
self . config . write ( ) ;
@@ -893,6 +1008,46 @@ impl Devenv {
893
1008
}
894
1009
}
895
1010
1011
+ fn confirm_overwrite ( file : & Path , contents : String ) -> Result < ( ) > {
1012
+ if std:: fs:: metadata ( file) . is_ok ( ) {
1013
+ // first output the old version and propose new changes
1014
+ let before = std:: fs:: read_to_string ( file) . expect ( "Failed to read file" ) ;
1015
+
1016
+ let diff = TextDiff :: from_lines ( & before, & contents) ;
1017
+
1018
+ println ! ( "\n Changes that will be made to {}:" , file. to_string_lossy( ) ) ;
1019
+ for change in diff. iter_all_changes ( ) {
1020
+ let sign = match change. tag ( ) {
1021
+ ChangeTag :: Delete => "\x1b [31m-\x1b [0m" ,
1022
+ ChangeTag :: Insert => "\x1b [32m+\x1b [0m" ,
1023
+ ChangeTag :: Equal => " " ,
1024
+ } ;
1025
+ print ! ( "{}{}" , sign, change) ;
1026
+ }
1027
+
1028
+ let confirm = dialoguer:: Confirm :: new ( )
1029
+ . with_prompt ( format ! (
1030
+ "{} already exists. Do you want to overwrite it?" ,
1031
+ file. to_string_lossy( )
1032
+ ) )
1033
+ . interact ( )
1034
+ . into_diagnostic ( ) ?;
1035
+
1036
+ if confirm {
1037
+ std:: fs:: write ( file, contents) . into_diagnostic ( ) ?;
1038
+ }
1039
+ } else {
1040
+ std:: fs:: write ( file, contents) . into_diagnostic ( ) ?;
1041
+ }
1042
+ Ok ( ( ) )
1043
+ }
1044
+
1045
+ #[ derive( Deserialize ) ]
1046
+ struct GenerateResponse {
1047
+ devenv_nix : String ,
1048
+ devenv_yaml : String ,
1049
+ }
1050
+
896
1051
pub struct DevEnv {
897
1052
output : Vec < u8 > ,
898
1053
gc_root : PathBuf ,
0 commit comments