1
1
use std:: collections:: HashMap ;
2
2
use std:: fs:: { self , OpenOptions } ;
3
+ use std:: io:: prelude:: * ;
3
4
use std:: path:: { Path , PathBuf } ;
5
+
6
+ use chrono:: Utc ;
4
7
use swirl:: PerformError ;
5
- use tempfile:: { Builder , TempDir } ;
8
+ use tempfile:: TempDir ;
6
9
use url:: Url ;
7
10
8
11
use crate :: background_jobs:: Environment ;
@@ -146,7 +149,7 @@ pub struct Repository {
146
149
147
150
impl Repository {
148
151
pub fn open ( repository_config : & RepositoryConfig ) -> Result < Self , PerformError > {
149
- let checkout_path = Builder :: new ( ) . prefix ( "git" ) . tempdir ( ) ?;
152
+ let checkout_path = tempfile :: Builder :: new ( ) . prefix ( "git" ) . tempdir ( ) ?;
150
153
151
154
let repository = git2:: build:: RepoBuilder :: new ( )
152
155
. fetch_options ( Self :: fetch_options ( & repository_config. credentials ) )
@@ -203,12 +206,11 @@ impl Repository {
203
206
self . repository
204
207
. commit ( Some ( "HEAD" ) , & sig, & sig, msg, & tree, & [ & parent] ) ?;
205
208
206
- self . push ( )
209
+ self . push ( "refs/heads/master" )
207
210
}
208
211
209
- /// Push the current branch to "refs/heads/master"
210
- fn push ( & self ) -> Result < ( ) , PerformError > {
211
- let refname = "refs/heads/master" ;
212
+ /// Push the current branch to the provided refname
213
+ fn push ( & self , refspec : & str ) -> Result < ( ) , PerformError > {
212
214
let mut ref_status = Ok ( ( ) ) ;
213
215
let mut callback_called = false ;
214
216
{
@@ -217,8 +219,7 @@ impl Repository {
217
219
callbacks. credentials ( |_, user_from_url, cred_type| {
218
220
self . credentials . git2_callback ( user_from_url, cred_type)
219
221
} ) ;
220
- callbacks. push_update_reference ( |cb_refname, status| {
221
- assert_eq ! ( refname, cb_refname) ;
222
+ callbacks. push_update_reference ( |_, status| {
222
223
if let Some ( s) = status {
223
224
ref_status = Err ( format ! ( "failed to push a ref: {}" , s) . into ( ) )
224
225
}
@@ -227,7 +228,7 @@ impl Repository {
227
228
} ) ;
228
229
let mut opts = git2:: PushOptions :: new ( ) ;
229
230
opts. remote_callbacks ( callbacks) ;
230
- origin. push ( & [ refname ] , Some ( & mut opts) ) ?;
231
+ origin. push ( & [ refspec ] , Some ( & mut opts) ) ?;
231
232
}
232
233
233
234
if !callback_called {
@@ -276,6 +277,24 @@ impl Repository {
276
277
opts. remote_callbacks ( callbacks) ;
277
278
opts
278
279
}
280
+
281
+ /// Reset `HEAD` to a single commit with all the index contents, but no parent
282
+ fn squash_to_single_commit ( & self , msg : & str ) -> Result < ( ) , PerformError > {
283
+ let tree = self . repository . find_commit ( self . head_oid ( ) ?) ?. tree ( ) ?;
284
+ let sig = self . repository . signature ( ) ?;
285
+
286
+ // We cannot update an existing `update_ref`, because that requires the
287
+ // first parent of this commit to match the ref's current value.
288
+ // Instead, create the commit and then do a hard reset.
289
+ let commit = self . repository . commit ( None , & sig, & sig, msg, & tree, & [ ] ) ?;
290
+ let commit = self
291
+ . repository
292
+ . find_object ( commit, Some ( git2:: ObjectType :: Commit ) ) ?;
293
+ self . repository
294
+ . reset ( & commit, git2:: ResetType :: Hard , None ) ?;
295
+
296
+ Ok ( ( ) )
297
+ }
279
298
}
280
299
281
300
#[ swirl:: background_job]
@@ -357,3 +376,77 @@ pub fn yank(
357
376
Ok ( ( ) )
358
377
} )
359
378
}
379
+
380
+ /// Collapse the index into a single commit, archiving the current history in a snapshot branch.
381
+ #[ swirl:: background_job]
382
+ pub fn squash_index ( env : & Environment ) -> Result < ( ) , PerformError > {
383
+ let repo = env. lock_index ( ) ?;
384
+ println ! ( "Squashing the index into a single commit." ) ;
385
+
386
+ let now = Utc :: now ( ) . format ( "%Y-%m-%d" ) ;
387
+ let original_head = repo. head_oid ( ) ?. to_string ( ) ;
388
+ let msg = format ! ( "Collapse index into one commit\n \n \
389
+
390
+ Previous HEAD was {}, now on the `snapshot-{}` branch\n \n \
391
+
392
+ More information about this change can be found [online] and on [this issue].\n \n \
393
+
394
+ [online]: https://internals.rust-lang.org/t/cargos-crate-index-upcoming-squash-into-one-commit/8440\n \
395
+ [this issue]: https://github.com/rust-lang/crates-io-cargo-teams/issues/47", original_head, now) ;
396
+
397
+ repo. squash_to_single_commit ( & msg) ?;
398
+
399
+ // Shell out to git because libgit2 does not currently support push leases
400
+
401
+ let key = match & repo. credentials {
402
+ Credentials :: Ssh { key } => key,
403
+ Credentials :: Http { .. } => {
404
+ return Err ( String :: from ( "squash_index: Password auth not supported" ) . into ( ) )
405
+ }
406
+ _ => return Err ( String :: from ( "squash_index: Could not determine credentials" ) . into ( ) ) ,
407
+ } ;
408
+
409
+ // When running on production, ensure the file is created in tmpfs and not persisted to disk
410
+ #[ cfg( target_os = "linux" ) ]
411
+ let mut temp_key_file = tempfile:: Builder :: new ( ) . tempfile_in ( "/dev/shm" ) ?;
412
+
413
+ // For other platforms, default to std::env::tempdir()
414
+ #[ cfg( not( target_os = "linux" ) ) ]
415
+ let mut temp_key_file = tempfile:: Builder :: new ( ) . tempfile ( ) ?;
416
+
417
+ temp_key_file. write_all ( key. as_bytes ( ) ) ?;
418
+
419
+ let checkout_path = repo. checkout_path . path ( ) ;
420
+ let output = std:: process:: Command :: new ( "git" )
421
+ . current_dir ( checkout_path)
422
+ . env (
423
+ "GIT_SSH_COMMAND" ,
424
+ format ! (
425
+ "ssh -o StrictHostKeyChecking=accept-new -i {}" ,
426
+ temp_key_file. path( ) . display( )
427
+ ) ,
428
+ )
429
+ . args ( & [
430
+ "push" ,
431
+ // Both updates should succeed or fail together
432
+ "--atomic" ,
433
+ "origin" ,
434
+ // Overwrite master, but only if it server matches the expected value
435
+ & format ! ( "--force-with-lease=refs/heads/master:{}" , original_head) ,
436
+ // The new squashed commit is pushed to master
437
+ "HEAD:refs/heads/master" ,
438
+ // The previous value of HEAD is pushed to a snapshot branch
439
+ & format ! ( "{}:refs/heads/snapshot-{}" , original_head, now) ,
440
+ ] )
441
+ . output ( ) ?;
442
+
443
+ if !output. status . success ( ) {
444
+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
445
+ let message = format ! ( "Running git command failed with: {}" , stderr) ;
446
+ return Err ( message. into ( ) ) ;
447
+ }
448
+
449
+ println ! ( "The index has been successfully squashed." ) ;
450
+
451
+ Ok ( ( ) )
452
+ }
0 commit comments