@@ -35,6 +35,7 @@ def fake_subprocess(
3535 squawk_result = None ,
3636 git_exists_on_branch = False ,
3737 git_branch_valid = True ,
38+ git_fetch_succeeds = False ,
3839):
3940 """Return a side_effect function that dispatches based on the command."""
4041 alembic_res = alembic_result or make_result (stdout = "CREATE TABLE foo (id int);\n " )
@@ -44,7 +45,11 @@ def side_effect(cmd, **kwargs):
4445 if cmd [0 ] == "git" :
4546 if "rev-parse" in cmd :
4647 return make_result (returncode = 0 if git_branch_valid else 1 )
47- return make_result (returncode = 0 if git_exists_on_branch else 1 )
48+ if "fetch" in cmd :
49+ return make_result (returncode = 0 if git_fetch_succeeds else 1 )
50+ if "cat-file" in cmd :
51+ return make_result (returncode = 0 if git_exists_on_branch else 1 )
52+ return make_result (returncode = 1 )
4853 if cmd [0 ] == "alembic" :
4954 return alembic_res
5055 if cmd [0 ] == "squawk" :
@@ -381,3 +386,98 @@ def upgrade():
381386 assert main () == 0
382387 # No git call, just alembic + squawk = 2 calls
383388 assert mock_run .call_count == 2
389+
390+
391+ def test_origin_branch_shallow_fetch_succeeds (repo ):
392+ """In CI shallow clones, origin/main may not exist locally; the hook should fetch it."""
393+ path = write_migration (
394+ repo ,
395+ "016_shallow.py" ,
396+ """
397+ revision = 'sha001'
398+ down_revision = 'prev001'
399+
400+ from alembic import op
401+
402+ def upgrade():
403+ op.execute("CREATE TABLE foo (id int)")
404+ """ ,
405+ )
406+ with (
407+ patch ("sys.argv" , ["squawk-alembic" , "--diff-branch" , "origin/main" , path ]),
408+ patch (
409+ "subprocess.run" ,
410+ side_effect = fake_subprocess (
411+ git_branch_valid = False ,
412+ git_fetch_succeeds = True ,
413+ git_exists_on_branch = False ,
414+ ),
415+ ) as mock_run ,
416+ ):
417+ assert main () == 0
418+ # git rev-parse (fail) + git fetch + git cat-file + alembic + squawk = 5 calls
419+ assert mock_run .call_count == 5
420+ assert mock_run .call_args_list [0 ][0 ][0 ][0 ] == "git"
421+ assert "fetch" in mock_run .call_args_list [1 ][0 ][0 ]
422+ assert "cat-file" in mock_run .call_args_list [2 ][0 ][0 ]
423+
424+
425+ def test_origin_branch_shallow_fetch_fails (repo , capsys ):
426+ """When both rev-parse and fetch fail, the hook should error."""
427+ path = write_migration (
428+ repo ,
429+ "017_fetch_fail.py" ,
430+ """
431+ revision = 'ff001'
432+ down_revision = 'prev001'
433+
434+ from alembic import op
435+
436+ def upgrade():
437+ op.execute("CREATE TABLE foo (id int)")
438+ """ ,
439+ )
440+ with (
441+ patch ("sys.argv" , ["squawk-alembic" , "--diff-branch" , "origin/main" , path ]),
442+ patch (
443+ "subprocess.run" ,
444+ side_effect = fake_subprocess (
445+ git_branch_valid = False ,
446+ git_fetch_succeeds = False ,
447+ ),
448+ ) as mock_run ,
449+ ):
450+ assert main () == 1
451+ # git rev-parse (fail) + git fetch (fail) = 2 calls
452+ assert mock_run .call_count == 2
453+ captured = capsys .readouterr ()
454+ assert "not found in git" in captured .err
455+
456+
457+ def test_non_origin_branch_no_fetch_attempted (repo , capsys ):
458+ """Non-origin branches should not trigger a fetch attempt."""
459+ path = write_migration (
460+ repo ,
461+ "018_no_fetch.py" ,
462+ """
463+ revision = 'nf001'
464+ down_revision = 'prev001'
465+
466+ from alembic import op
467+
468+ def upgrade():
469+ op.execute("CREATE TABLE foo (id int)")
470+ """ ,
471+ )
472+ with (
473+ patch ("sys.argv" , ["squawk-alembic" , "--diff-branch" , "main" , path ]),
474+ patch (
475+ "subprocess.run" ,
476+ side_effect = fake_subprocess (git_branch_valid = False ),
477+ ) as mock_run ,
478+ ):
479+ assert main () == 1
480+ # Only git rev-parse (fail), no fetch attempted
481+ assert mock_run .call_count == 1
482+ captured = capsys .readouterr ()
483+ assert "not found in git" in captured .err
0 commit comments