@@ -27,6 +27,21 @@ fn selected(text: &str, module: &str, version: &str) -> String {
2727 . join ( "\n " )
2828}
2929
30+ /// Whether some line of `b` pins the same module and version as a line of
31+ /// `a` with a different hash. Merging the two would leave go.sum with both.
32+ fn conflicts ( a : & str , b : & str ) -> bool {
33+ b. lines ( ) . any ( |line| {
34+ let fields: Vec < _ > = line. split_whitespace ( ) . collect ( ) ;
35+ a. lines ( ) . any ( |other| {
36+ let other: Vec < _ > = other. split_whitespace ( ) . collect ( ) ;
37+ fields. len ( ) == 3
38+ && other. len ( ) == 3
39+ && other[ ..2 ] == fields[ ..2 ]
40+ && other[ 2 ] != fields[ 2 ]
41+ } )
42+ } )
43+ }
44+
3045pub ( super ) async fn save (
3146 root : & Path ,
3247 base : & str ,
@@ -38,6 +53,12 @@ pub(super) async fn save(
3853 if entry. module == module && entry. version . as_deref ( ) == Some ( version) {
3954 if let Some ( path) = backup ( root, & entry) {
4055 let saved = selected ( & read ( & path) . await ?, module, version) ;
56+ if conflicts ( & sums, & saved) {
57+ return Err ( format ! (
58+ "go.sum conflicts with the checksum saved in {} for {module} {version}" ,
59+ path. display( )
60+ ) ) ;
61+ }
4162 if let Some ( merged) = reinsert_lines ( & sums, & saved) {
4263 sums = merged;
4364 }
@@ -49,6 +70,12 @@ pub(super) async fn save(
4970 }
5071 let path = root. join ( base) . join ( NAME ) ;
5172 let old = read ( & path) . await ?;
73+ if conflicts ( & old, & sums) {
74+ return Err ( format ! (
75+ "go.sum conflicts with the checksum saved in {} for {module} {version}" ,
76+ path. display( )
77+ ) ) ;
78+ }
5279 if let Some ( new) = reinsert_lines ( & old, & sums) {
5380 tokio:: fs:: create_dir_all ( root. join ( base) )
5481 . await
@@ -105,6 +132,12 @@ pub(super) async fn restore(
105132 continue ;
106133 }
107134 let saved = selected ( & read ( & backup) . await ?, & entry. module , version) ;
135+ if conflicts ( & saved, & saved) {
136+ return Err ( format ! (
137+ "conflicting saved checksums in {}" ,
138+ backup. display( )
139+ ) ) ;
140+ }
108141 for line in saved. lines ( ) {
109142 let fields: Vec < _ > = line. split_whitespace ( ) . collect ( ) ;
110143 if fields. len ( ) != 3 || !fields[ 2 ] . starts_with ( "h1:" ) {
@@ -236,6 +269,56 @@ mod tests {
236269 assert_eq ! ( read( & root. join( "go.sum" ) ) . await . unwrap( ) , conflict) ;
237270 }
238271
272+ /// Re-pinning over a backup that saved a different hash refuses instead
273+ /// of keeping both, which a rollback after tidy would write to go.sum.
274+ #[ tokio:: test]
275+ async fn save_refuses_a_checksum_conflicting_with_the_backup ( ) {
276+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
277+ let root = tmp. path ( ) ;
278+ tokio:: fs:: write ( root. join ( "go.mod" ) , "module consumer\n go 1.21\n " )
279+ . await
280+ . unwrap ( ) ;
281+ tokio:: fs:: write ( root. join ( "go.sum" ) , SUMS ) . await . unwrap ( ) ;
282+ save ( root, GO_PATCHES_DIR , MODULE , VERSION ) . await . unwrap ( ) ;
283+ let backup = root. join ( GO_PATCHES_DIR ) . join ( NAME ) ;
284+ let saved = read ( & backup) . await . unwrap ( ) ;
285+ tokio:: fs:: write ( root. join ( "go.sum" ) , SUMS . replace ( "h1:aaaa" , "h1:zzzz" ) )
286+ . await
287+ . unwrap ( ) ;
288+ assert ! ( save( root, GO_PATCHES_DIR , MODULE , VERSION )
289+ . await
290+ . unwrap_err( )
291+ . contains( "conflicts" ) ) ;
292+ assert_eq ! ( read( & backup) . await . unwrap( ) , saved) ;
293+ }
294+
295+ #[ tokio:: test]
296+ async fn restore_refuses_a_backup_with_conflicting_lines ( ) {
297+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
298+ let root = tmp. path ( ) ;
299+ tokio:: fs:: write (
300+ root. join ( "go.mod" ) ,
301+ "module consumer\n go 1.21\n require example.com/upstream v1.0.0\n " ,
302+ )
303+ . await
304+ . unwrap ( ) ;
305+ tokio:: fs:: write ( root. join ( "go.sum" ) , SUMS ) . await . unwrap ( ) ;
306+ ensure_replace_entry ( root, MODULE , VERSION , GO_PATCHES_DIR , false )
307+ . await
308+ . unwrap ( ) ;
309+ let backup = root. join ( GO_PATCHES_DIR ) . join ( NAME ) ;
310+ let both = format ! ( "{SUMS}{}" , SUMS . replace( "h1:aaaa" , "h1:zzzz" ) ) ;
311+ tokio:: fs:: write ( & backup, both) . await . unwrap ( ) ;
312+ tokio:: fs:: write ( root. join ( "go.sum" ) , "" ) . await . unwrap ( ) ;
313+ assert ! (
314+ drop_replace_entry( root, MODULE , ReplaceOwner :: GoPatches , false )
315+ . await
316+ . unwrap_err( )
317+ . contains( "conflicting" )
318+ ) ;
319+ assert_eq ! ( read( & root. join( "go.sum" ) ) . await . unwrap( ) , "" ) ;
320+ }
321+
239322 #[ tokio:: test]
240323 async fn dry_run_drop_restores_nothing ( ) {
241324 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
0 commit comments