packages feed

pr-tools-0.2.0.0: src/PRTools/ContentHash.hs

module PRTools.ContentHash where

import Data.Hashable (hash)
import Data.List (isPrefixOf)
import System.Process (readProcess)
import PRTools.Config (trimTrailing)

-- Generate a hash from the patch content, excluding metadata
-- Uses the merge-base to ensure consistent comparison point
generatePatchHash :: String -> String -> IO String
generatePatchHash baseCommit headCommit = do
  -- Find the actual merge base between the base branch and the commit
  mergeBase <- trimTrailing <$> readProcess "git" ["merge-base", baseCommit, headCommit] ""
  patchContent <- readProcess "git" ["diff", mergeBase, headCommit] ""
  let normalizedPatch = normalizePatch patchContent
  return $ take 12 $ show $ abs $ hash normalizedPatch

-- Normalize patch content by removing line numbers and normalizing whitespace
normalizePatch :: String -> String
normalizePatch patch = 
  let patchLines = lines patch
      -- Remove @@ line number headers and file headers, keep only actual changes
      contentLines = filter (not . isPatchHeader) patchLines
      -- Normalize whitespace but preserve structure
      normalized = map normalizeWhitespace contentLines
      -- Remove empty lines that might vary between diffs
      nonEmpty = filter (not . null . trim) normalized
  in unlines nonEmpty
  where
    trim = dropWhile (`elem` " \t") . reverse . dropWhile (`elem` " \t") . reverse

-- Check if a line is a patch header that should be ignored for content comparison
isPatchHeader :: String -> Bool
isPatchHeader line = 
  "@@" `isPrefixOf` line ||
  "diff --git" `isPrefixOf` line ||
  "index " `isPrefixOf` line ||
  "--- " `isPrefixOf` line ||
  "+++ " `isPrefixOf` line

-- Normalize whitespace while preserving the +/- prefix structure
normalizeWhitespace :: String -> String
normalizeWhitespace line
  | "+" `isPrefixOf` line = "+" ++ (unwords . words . drop 1) line
  | "-" `isPrefixOf` line = "-" ++ (unwords . words . drop 1) line
  | otherwise = unwords . words $ line

-- Debug function to compare patch content (for troubleshooting)
debugPatchDifference :: String -> String -> String -> IO ()
debugPatchDifference baseCommit oldCommit newCommit = do
  -- Use merge-base for consistent comparison
  oldMergeBase <- trimTrailing <$> readProcess "git" ["merge-base", baseCommit, oldCommit] ""
  newMergeBase <- trimTrailing <$> readProcess "git" ["merge-base", baseCommit, newCommit] ""
  
  putStrLn $ "Old merge base: " ++ oldMergeBase
  putStrLn $ "New merge base: " ++ newMergeBase
  
  oldPatch <- readProcess "git" ["diff", oldMergeBase, oldCommit] ""
  newPatch <- readProcess "git" ["diff", newMergeBase, newCommit] ""
  let oldNormalized = normalizePatch oldPatch
  let newNormalized = normalizePatch newPatch
  putStrLn "=== OLD PATCH (normalized) ==="
  putStrLn oldNormalized
  putStrLn "=== NEW PATCH (normalized) ==="
  putStrLn newNormalized
  putStrLn "=== PATCHES EQUAL? ==="
  putStrLn $ show (oldNormalized == newNormalized)