@@ -9,8 +9,31 @@ use crate::common::{TestContext, cmd_snapshot, git_cmd};
99
1010mod common;
1111
12- /// Helper function to create a local git repository with hooks
12+ const BASE_TIMESTAMP : u64 = 1_000_000_000 ;
13+ const INCREMENTING_STEP_SECS : u64 = 100 ;
14+ const FIXED_STEP_SECS : u64 = 0 ;
15+
16+ /// Helper function to create a local git repository with hooks and incrementing timestamps.
1317fn create_local_git_repo ( context : & TestContext , repo_name : & str , tags : & [ & str ] ) -> Result < String > {
18+ create_local_git_repo_with_timestamps ( context, repo_name, tags, INCREMENTING_STEP_SECS )
19+ }
20+
21+ /// Like `create_local_git_repo`, but all commits and tags share a single fixed timestamp.
22+ /// Simulates mirror repos where all tags are imported simultaneously.
23+ fn create_local_git_repo_fixed_ts (
24+ context : & TestContext ,
25+ repo_name : & str ,
26+ tags : & [ & str ] ,
27+ ) -> Result < String > {
28+ create_local_git_repo_with_timestamps ( context, repo_name, tags, FIXED_STEP_SECS )
29+ }
30+
31+ fn create_local_git_repo_with_timestamps (
32+ context : & TestContext ,
33+ repo_name : & str ,
34+ tags : & [ & str ] ,
35+ timestamp_step_secs : u64 ,
36+ ) -> Result < String > {
1437 let repo_dir = context. home_dir ( ) . child ( format ! ( "test-repos/{repo_name}" ) ) ;
1538 repo_dir. create_dir_all ( ) ?;
1639
@@ -20,24 +43,6 @@ fn create_local_git_repo(context: &TestContext, repo_name: &str, tags: &[&str])
2043 . arg ( "init" )
2144 . assert ( )
2245 . success ( ) ;
23- git_cmd ( & repo_dir)
24- . arg ( "config" )
25- . arg ( "user.name" )
26- . arg ( "Prek Test" )
27- . assert ( )
28- . success ( ) ;
29- git_cmd ( & repo_dir)
30- . arg ( "config" )
31- . arg ( "user.email" )
32- . arg ( "test@prek.dev" )
33- . assert ( )
34- . success ( ) ;
35- git_cmd ( & repo_dir)
36- . arg ( "config" )
37- . arg ( "core.autocrlf" )
38- . arg ( "false" )
39- . assert ( )
40- . success ( ) ;
4146
4247 // Create .pre-commit-hooks.yaml
4348 repo_dir
@@ -55,7 +60,7 @@ fn create_local_git_repo(context: &TestContext, repo_name: &str, tags: &[&str])
5560
5661 git_cmd ( & repo_dir) . arg ( "add" ) . arg ( "." ) . assert ( ) . success ( ) ;
5762
58- let mut timestamp = 1_000_000_000 ;
63+ let mut timestamp = BASE_TIMESTAMP ;
5964
6065 git_cmd ( & repo_dir)
6166 . arg ( "commit" )
@@ -68,7 +73,7 @@ fn create_local_git_repo(context: &TestContext, repo_name: &str, tags: &[&str])
6873
6974 // Create tags
7075 for tag in tags {
71- timestamp += 100 ;
76+ timestamp += timestamp_step_secs ;
7277 git_cmd ( & repo_dir)
7378 . arg ( "commit" )
7479 . arg ( "-m" )
@@ -89,7 +94,7 @@ fn create_local_git_repo(context: &TestContext, repo_name: &str, tags: &[&str])
8994 . success ( ) ;
9095 }
9196
92- timestamp += 100 ;
97+ timestamp += timestamp_step_secs ;
9398 // Add an extra commit to the tip
9499 git_cmd ( & repo_dir)
95100 . arg ( "commit" )
@@ -1330,6 +1335,174 @@ fn auto_update_freeze_toml() -> Result<()> {
13301335 Ok ( ( ) )
13311336}
13321337
1338+ #[ test]
1339+ fn auto_update_equal_timestamp_tags_picks_highest_version ( ) -> Result < ( ) > {
1340+ let context = TestContext :: new ( ) ;
1341+ context. init_project ( ) ;
1342+
1343+ let repo_path = create_local_git_repo_fixed_ts (
1344+ & context,
1345+ "mirror-repo" ,
1346+ & [ "v1.0.0" , "v1.0.1" , "v1.0.2" , "v1.0.3" , "v1.0.4" , "v1.0.5" ] ,
1347+ ) ?;
1348+
1349+ context. write_pre_commit_config ( & indoc:: formatdoc! { r"
1350+ repos:
1351+ - repo: {}
1352+ rev: v1.0.3
1353+ hooks:
1354+ - id: test-hook
1355+ " , repo_path} ) ;
1356+
1357+ context. git_add ( "." ) ;
1358+
1359+ let filters = context. filters ( ) ;
1360+ cmd_snapshot ! ( filters. clone( ) , context. auto_update( ) . arg( "--cooldown-days" ) . arg( "0" ) , @r#"
1361+ success: true
1362+ exit_code: 0
1363+ ----- stdout -----
1364+ [[HOME]/test-repos/mirror-repo] updating v1.0.3 -> v1.0.5
1365+
1366+ ----- stderr -----
1367+ "# ) ;
1368+
1369+ insta:: with_settings!(
1370+ { filters => filters. clone( ) } ,
1371+ {
1372+ assert_snapshot!( context. read( PRE_COMMIT_CONFIG_YAML ) , @r#"
1373+ repos:
1374+ - repo: [HOME]/test-repos/mirror-repo
1375+ rev: v1.0.5
1376+ hooks:
1377+ - id: test-hook
1378+ "# ) ;
1379+ }
1380+ ) ;
1381+
1382+ Ok ( ( ) )
1383+ }
1384+
1385+ // When all tags share a timestamp and some are non-semver (e.g. "latest", "stable"),
1386+ // semver tags should be preferred and sorted highest-first.
1387+ #[ test]
1388+ fn auto_update_equal_timestamp_prefers_semver_over_nonsemver ( ) -> Result < ( ) > {
1389+ let context = TestContext :: new ( ) ;
1390+ context. init_project ( ) ;
1391+
1392+ let repo_path = create_local_git_repo_fixed_ts (
1393+ & context,
1394+ "mixed-tags-repo" ,
1395+ & [ "v1.0.0" , "latest" , "v2.0.0" , "stable" ] ,
1396+ ) ?;
1397+
1398+ context. write_pre_commit_config ( & indoc:: formatdoc! { r"
1399+ repos:
1400+ - repo: {}
1401+ rev: v1.0.0
1402+ hooks:
1403+ - id: test-hook
1404+ " , repo_path} ) ;
1405+
1406+ context. git_add ( "." ) ;
1407+
1408+ let filters = context. filters ( ) ;
1409+
1410+ cmd_snapshot ! ( filters. clone( ) , context. auto_update( ) . arg( "--cooldown-days" ) . arg( "0" ) , @r#"
1411+ success: true
1412+ exit_code: 0
1413+ ----- stdout -----
1414+ [[HOME]/test-repos/mixed-tags-repo] updating v1.0.0 -> v2.0.0
1415+
1416+ ----- stderr -----
1417+ "# ) ;
1418+
1419+ insta:: with_settings!(
1420+ { filters => filters. clone( ) } ,
1421+ {
1422+ assert_snapshot!( context. read( PRE_COMMIT_CONFIG_YAML ) , @r#"
1423+ repos:
1424+ - repo: [HOME]/test-repos/mixed-tags-repo
1425+ rev: v2.0.0
1426+ hooks:
1427+ - id: test-hook
1428+ "# ) ;
1429+ }
1430+ ) ;
1431+
1432+ Ok ( ( ) )
1433+ }
1434+
1435+ // When tags span multiple timestamp groups, the newest group should be selected first.
1436+ // Within an equal-timestamp group, semver tiebreaker picks the highest version.
1437+ #[ test]
1438+ fn auto_update_mixed_timestamps_with_equal_subgroups ( ) -> Result < ( ) > {
1439+ let context = TestContext :: new ( ) ;
1440+ context. init_project ( ) ;
1441+
1442+ // Create base repo with v1.0.x tags at incrementing timestamps.
1443+ let repo_path = create_local_git_repo ( & context, "mixed-ts-repo" , & [ "v1.0.0" , "v1.0.1" ] ) ?;
1444+
1445+ // Add a second group of tags sharing a single newer timestamp
1446+ // (must be in the past so the cooldown filter doesn't exclude them).
1447+ let newer_ts = "1500000000 +0000" ;
1448+ for tag in & [ "v2.0.1" , "v2.0.0" ] {
1449+ git_cmd ( & repo_path)
1450+ . arg ( "commit" )
1451+ . arg ( "-m" )
1452+ . arg ( format ! ( "Release {tag}" ) )
1453+ . arg ( "--allow-empty" )
1454+ . env ( "GIT_AUTHOR_DATE" , newer_ts)
1455+ . env ( "GIT_COMMITTER_DATE" , newer_ts)
1456+ . assert ( )
1457+ . success ( ) ;
1458+ git_cmd ( & repo_path)
1459+ . arg ( "tag" )
1460+ . arg ( tag)
1461+ . arg ( "-m" )
1462+ . arg ( tag)
1463+ . env ( "GIT_AUTHOR_DATE" , newer_ts)
1464+ . env ( "GIT_COMMITTER_DATE" , newer_ts)
1465+ . assert ( )
1466+ . success ( ) ;
1467+ }
1468+
1469+ context. write_pre_commit_config ( & indoc:: formatdoc! { r"
1470+ repos:
1471+ - repo: {}
1472+ rev: v1.0.0
1473+ hooks:
1474+ - id: test-hook
1475+ " , repo_path} ) ;
1476+
1477+ context. git_add ( "." ) ;
1478+
1479+ let filters = context. filters ( ) ;
1480+
1481+ cmd_snapshot ! ( filters. clone( ) , context. auto_update( ) . arg( "--cooldown-days" ) . arg( "0" ) , @r#"
1482+ success: true
1483+ exit_code: 0
1484+ ----- stdout -----
1485+ [[HOME]/test-repos/mixed-ts-repo] updating v1.0.0 -> v2.0.1
1486+
1487+ ----- stderr -----
1488+ "# ) ;
1489+
1490+ insta:: with_settings!(
1491+ { filters => filters. clone( ) } ,
1492+ {
1493+ assert_snapshot!( context. read( PRE_COMMIT_CONFIG_YAML ) , @r#"
1494+ repos:
1495+ - repo: [HOME]/test-repos/mixed-ts-repo
1496+ rev: v2.0.1
1497+ hooks:
1498+ - id: test-hook
1499+ "# ) ;
1500+ }
1501+ ) ;
1502+
1503+ Ok ( ( ) )
1504+ }
1505+
13331506#[ test]
13341507fn auto_update_freeze_toml_with_comment ( ) -> Result < ( ) > {
13351508 let context = TestContext :: new ( ) ;
0 commit comments