Files
converge/test/GitCommitSpec.hs
T
jbrechtel 25b8f24906 Fix two git operation bugs
- gitCommitAll: abort commit when staging (git add -A) fails, e.g.
  due to a stale .git/index.lock.  Previously the failure was ignored
  and commit proceeded on an empty or partial index.

- gitPull: remove explicit branch argument from git pull --rebase.
  Passing both remote and branch can conflict with the branch's
  configured tracking ref, causing 'Cannot rebase onto multiple
  branches'.  Now uses just --rebase <remote>, letting git resolve
  the tracking branch from config.

- Test: index.lock scenario verifies commit is aborted on staging
  failure.
2026-05-13 22:51:09 -04:00

94 lines
4.1 KiB
Haskell

module GitCommitSpec (spec) where
import Converge (Options (..), RepoConfig (..), defaultOptions, defaultRepoConfig, gitCommitAll, syncRepo)
import qualified Data.Text as T
import System.Exit (ExitCode (..))
import System.FilePath ((</>))
import Test.Hspec
import TestHelper
spec :: Spec
spec = do
describe "gitCommitAll" $ do
it "creates a commit after a file is changed" $ do
logger <- testLogger
withTempGitRepo $ \repo -> do
countBefore <- commitCount repo
countBefore `shouldBe` 1 -- initial commit from helper
writeFile (repo </> "hello.txt") "hello world"
(code, _, _) <- gitCommitAll logger repo
code `shouldBe` ExitSuccess
countAfter <- commitCount repo
countAfter `shouldBe` (countBefore + 1)
it "includes the changed file in the commit" $ do
logger <- testLogger
withTempGitRepo $ \repo -> do
writeFile (repo </> "data.txt") "some data"
(code, _, _) <- gitCommitAll logger repo
code `shouldBe` ExitSuccess
(_, logOut, _) <- runGitIn repo ["log", "-1", "--name-only", "--oneline"]
logOut `shouldSatisfy` ("data.txt" `T.isInfixOf`)
it "does not create a new commit when there are no changes" $ do
logger <- testLogger
withTempGitRepo $ \repo -> do
countBefore <- commitCount repo
(code, _, _) <- gitCommitAll logger repo
-- git commit exits non-zero when there is nothing to commit
code `shouldSatisfy` (/= ExitSuccess)
countAfter <- commitCount repo
countAfter `shouldBe` countBefore
it "creates only one commit when called multiple times with the same staged changes" $ do
logger <- testLogger
withTempGitRepo $ \repo -> do
writeFile (repo </> "single.txt") "single change"
(code1, _, _) <- gitCommitAll logger repo
code1 `shouldBe` ExitSuccess
-- Second call: nothing left to commit
countBefore2 <- commitCount repo
(code2, _, _) <- gitCommitAll logger repo
code2 `shouldSatisfy` (/= ExitSuccess)
countAfter2 <- commitCount repo
countAfter2 `shouldBe` countBefore2
it "aborts commit when staging fails (e.g. index.lock exists)" $ do
logger <- testLogger
withTempGitRepo $ \repo -> do
-- Create a lock file to make git add -A fail
writeFile (repo </> ".git" </> "index.lock") "locked"
writeFile (repo </> "new.txt") "new file"
countBefore <- commitCount repo
(code, _, err) <- gitCommitAll logger repo
-- Staging fails, so commit should not happen
code `shouldSatisfy` (/= ExitSuccess)
err `shouldSatisfy` ("index.lock" `T.isInfixOf`)
commitCount repo `shouldReturn` countBefore
describe "startup sync" $ do
it "commits and pushes uncommitted changes on startup" $ do
logger <- testLogger
withTempGitRepoWithRemote $ \repo -> do
-- Leave a file uncommitted (simulating state before converge starts)
writeFile (repo </> "uncommitted.txt") "pending change"
countBefore <- commitCount repo
countBefore `shouldBe` 1
let cfg = defaultRepoConfig{rcPath = repo}
let opts =
defaultOptions
{ optRepos = [cfg]
, optNotifyCmd = "true" -- avoid firing notify-send
}
syncRepo opts logger cfg
countAfter <- commitCount repo
countAfter `shouldBe` 2
-- Verify the commit was pushed to remote
(_, remoteLog, _) <- runGitIn repo ["log", "origin/main", "--oneline", "--name-only"]
remoteLog `shouldSatisfy` ("uncommitted.txt" `T.isInfixOf`)