git-repair 1.20140115 → 1.20140227
raw patch · 30 files changed
+404/−197 lines, 30 files
Files
- Git.hs +1/−0
- Git/Branch.hs +10/−10
- Git/CatFile.hs +4/−4
- Git/Command.hs +10/−13
- Git/Construct.hs +2/−1
- Git/Destroyer.hs +85/−62
- Git/FilePath.hs +13/−8
- Git/Fsck.hs +10/−11
- Git/LsTree.hs +2/−2
- Git/Objects.hs +1/−1
- Git/Ref.hs +15/−10
- Git/RefLog.hs +1/−1
- Git/Repair.hs +38/−27
- Git/Sha.hs +4/−0
- Git/Types.hs +7/−3
- Git/UpdateIndex.hs +28/−9
- Utility/Directory.hs +18/−2
- Utility/Env.hs +18/−0
- Utility/FileMode.hs +1/−3
- Utility/Misc.hs +8/−1
- Utility/Path.hs +62/−22
- Utility/PosixFiles.hs +33/−0
- Utility/QuickCheck.hs +5/−1
- Utility/Tmp.hs +3/−2
- debian/changelog +9/−0
- debian/control +1/−1
- doc/index.mdwn +1/−1
- git-repair.1 +12/−0
- git-repair.cabal +1/−1
- git-repair.hs +1/−1
Git.hs view
@@ -13,6 +13,7 @@ module Git ( Repo(..), Ref(..),+ fromRef, Branch, Sha, Tag,
Git/Branch.hs view
@@ -28,7 +28,7 @@ case v of Nothing -> return Nothing Just branch -> - ifM (null <$> pipeReadStrict [Param "show-ref", Param $ show branch] r)+ ifM (null <$> pipeReadStrict [Param "show-ref", Param $ fromRef branch] r) ( return Nothing , return v )@@ -36,7 +36,7 @@ {- The current branch, which may not really exist yet. -} currentUnsafe :: Repo -> IO (Maybe Git.Ref) currentUnsafe r = parse . firstLine- <$> pipeReadStrict [Param "symbolic-ref", Param $ show Git.Ref.headRef] r+ <$> pipeReadStrict [Param "symbolic-ref", Param $ fromRef Git.Ref.headRef] r where parse l | null l = Nothing@@ -51,7 +51,7 @@ where diffs = pipeReadStrict [ Param "log"- , Param (show origbranch ++ ".." ++ show newbranch)+ , Param (fromRef origbranch ++ ".." ++ fromRef newbranch) , Params "--oneline -n1" ] repo @@ -74,7 +74,7 @@ where no_ff = return False do_ff to = do- run [Param "update-ref", Param $ show branch, Param $ show to] repo+ run [Param "update-ref", Param $ fromRef branch, Param $ fromRef to] repo return True findbest c [] = return $ Just c findbest c (r:rs)@@ -104,14 +104,14 @@ ifM (cancommit tree) ( do sha <- getSha "commit-tree" $ pipeWriteRead- (map Param $ ["commit-tree", show tree] ++ ps)+ (map Param $ ["commit-tree", fromRef tree] ++ ps) (Just $ flip hPutStr message) repo update branch sha repo return $ Just sha , return Nothing ) where- ps = concatMap (\r -> ["-p", show r]) parentrefs+ ps = concatMap (\r -> ["-p", fromRef r]) parentrefs cancommit tree | allowempty = return True | otherwise = case parentrefs of@@ -130,8 +130,8 @@ update :: Branch -> Sha -> Repo -> IO () update branch sha = run [ Param "update-ref"- , Param $ show branch- , Param $ show sha+ , Param $ fromRef branch+ , Param $ fromRef sha ] {- Checks out a branch, creating it if necessary. -}@@ -140,7 +140,7 @@ [ Param "checkout" , Param "-q" , Param "-B"- , Param $ show $ Git.Ref.base branch+ , Param $ fromRef $ Git.Ref.base branch ] {- Removes a branch. -}@@ -149,5 +149,5 @@ [ Param "branch" , Param "-q" , Param "-D"- , Param $ show $ Git.Ref.base branch+ , Param $ fromRef $ Git.Ref.base branch ]
Git/CatFile.hs view
@@ -50,7 +50,7 @@ {- Reads a file from a specified branch. -} catFile :: CatFileHandle -> Branch -> FilePath -> IO L.ByteString catFile h branch file = catObject h $ Ref $- show branch ++ ":" ++ toInternalGitPath file+ fromRef branch ++ ":" ++ toInternalGitPath file {- Uses a running git cat-file read the content of an object. - Objects that do not exist will have "" returned. -}@@ -60,7 +60,7 @@ catObjectDetails :: CatFileHandle -> Ref -> IO (Maybe (L.ByteString, Sha, ObjectType)) catObjectDetails (CatFileHandle hdl _) object = CoProcess.query hdl send receive where- query = show object+ query = fromRef object send to = hPutStrLn to query receive from = do header <- hGetLine from@@ -72,8 +72,8 @@ _ -> dne | otherwise -> dne _- | header == show object ++ " missing" -> dne- | otherwise -> error $ "unknown response from git cat-file " ++ show (header, object)+ | header == fromRef object ++ " missing" -> dne+ | otherwise -> error $ "unknown response from git cat-file " ++ show (header, query) readcontent objtype bytes from sha = do content <- S.hGet from bytes eatchar '\n' from
Git/Command.hs view
@@ -18,32 +18,29 @@ #ifdef mingw32_HOST_OS import Git.FilePath #endif+import Utility.Batch {- Constructs a git command line operating on the specified repo. -} gitCommandLine :: [CommandParam] -> Repo -> [CommandParam] gitCommandLine params r@(Repo { location = l@(Local _ _ ) }) = setdir : settree ++ gitGlobalOpts r ++ params where- setdir = Param $ "--git-dir=" ++ gitpath (gitdir l)+ setdir = Param $ "--git-dir=" ++ gitdir l settree = case worktree l of Nothing -> []- Just t -> [Param $ "--work-tree=" ++ gitpath t]-#ifdef mingw32_HOST_OS- -- despite running on windows, msysgit wants a unix-formatted path- gitpath s- | isAbsolute s = "/" ++ dropDrive (toInternalGitPath s)- | otherwise = s-#else- gitpath = id-#endif+ Just t -> [Param $ "--work-tree=" ++ t] gitCommandLine _ repo = assertLocal repo $ error "internal" {- Runs git in the specified repo. -} runBool :: [CommandParam] -> Repo -> IO Bool runBool params repo = assertLocal repo $- boolSystemEnv "git"- (gitCommandLine params repo)- (gitEnv repo)+ boolSystemEnv "git" (gitCommandLine params repo) (gitEnv repo)++{- Runs git in batch mode. -}+runBatch :: BatchCommandMaker -> [CommandParam] -> Repo -> IO Bool+runBatch batchmaker params repo = assertLocal repo $ do+ let (cmd, params') = batchmaker ("git", gitCommandLine params repo)+ boolSystemEnv cmd params' (gitEnv repo) {- Runs git in the specified repo, throwing an error if it fails. -} run :: [CommandParam] -> Repo -> IO ()
Git/Construct.hs view
@@ -33,6 +33,7 @@ import Git.Types import Git import Git.Remote+import Git.FilePath import qualified Git.Url as Url import Utility.UserInfo @@ -57,7 +58,7 @@ - specified. -} fromAbsPath :: FilePath -> IO Repo fromAbsPath dir- | isAbsolute dir = ifM (doesDirectoryExist dir') ( ret dir' , hunt )+ | absoluteGitPath dir = ifM (doesDirectoryExist dir') ( ret dir' , hunt ) | otherwise = error $ "internal error, " ++ dir ++ " is not absolute" where
Git/Destroyer.hs view
@@ -2,7 +2,7 @@ - - Use with caution! -- - Copyright 2013 Joey Hess <joey@kitenet.net>+ - Copyright 2013, 2014 Joey Hess <joey@kitenet.net> - - Licensed under the GNU GPL version 3 or higher. -}@@ -17,39 +17,41 @@ import Git import Utility.QuickCheck import Utility.FileMode+import Utility.Tmp import qualified Data.ByteString as B import Data.Word import System.PosixCompat.Types {- Ways to damange a git repository. -}-data Damage = Damage DamageAction FileSelector+data Damage+ = Empty FileSelector+ | Delete FileSelector+ | Reverse FileSelector+ | AppendGarbage FileSelector B.ByteString+ | PrependGarbage FileSelector B.ByteString+ | CorruptByte FileSelector Int Word8+ | ScrambleFileMode FileSelector FileMode+ | SwapFiles FileSelector FileSelector deriving (Read, Show) instance Arbitrary Damage where- arbitrary = Damage <$> arbitrary <*> arbitrary--data DamageAction- = Empty- | Delete- | Reverse- | AppendGarbage B.ByteString- | PrependGarbage B.ByteString- | CorruptByte Int Word8- | ScrambleFileMode FileMode- deriving (Read, Show)--instance Arbitrary DamageAction where arbitrary = oneof- [ pure Empty- , pure Delete- , pure Reverse- , AppendGarbage <$> garbage- , PrependGarbage <$> garbage+ [ Empty <$> arbitrary+ , Delete <$> arbitrary+ , Reverse <$> arbitrary+ , AppendGarbage <$> arbitrary <*> garbage+ , PrependGarbage <$> arbitrary <*> garbage , CorruptByte- <$> nonNegative arbitraryBoundedIntegral+ <$> arbitrary+ <*> nonNegative arbitraryBoundedIntegral <*> arbitrary- , ScrambleFileMode <$> nonNegative arbitrarySizedIntegral+ , ScrambleFileMode+ <$> arbitrary+ <*> nonNegative arbitrarySizedIntegral+ , SwapFiles+ <$> arbitrary+ <*> arbitrary ] where garbage = B.pack <$> arbitrary `suchThat` (not . null)@@ -58,7 +60,7 @@ - are enumerated, sorted, and this is used as an index - into the list. (Wrapping around if higher than the length.) -} data FileSelector = FileSelector Int- deriving (Read, Show)+ deriving (Read, Show, Eq) instance Arbitrary FileSelector where arbitrary = FileSelector <$> oneof@@ -80,47 +82,68 @@ {- Applies Damage to a Repo, in a reproducible fashion - (as long as the Repo contains the same files each time). -} applyDamage :: [Damage] -> Repo -> IO ()-applyDamage l r = do- contents <- sort . filter (not . skipped . takeFileName)+applyDamage ds r = do+ contents <- sort . filter (not . skipped) <$> dirContentsRecursive (localGitDir r)- forM_ l $ \(Damage action fileselector) -> do- let f = selectFile contents fileselector- -- Symlinks might be dangling, so are skipped.- -- If the file was already removed by a previous Damage,- -- it's skipped.- whenM (doesFileExist f) $- applyDamageAction action f- `catchIO` \e -> error ("Failed to apply " ++ show action ++ " " ++ show f ++ ": " ++ show e ++ "(total damage: " ++ show l ++ ")")+ forM_ ds $ \d -> do+ let withfile s a = do+ let f = selectFile contents s+ -- Symlinks might be dangling, so are skipped.+ -- If the file was already removed by a previous Damage,+ -- it's skipped.+ whenM (doesFileExist f) $+ a f `catchIO` \e -> error ("Failed to apply damage " ++ show d ++ " to " ++ show f ++ ": " ++ show e ++ "(total damage: " ++ show ds ++ ")")+ case d of+ Empty s -> withfile s $ \f ->+ withSaneMode f $ do+ nukeFile f+ writeFile f ""+ Reverse s -> withfile s $ \f ->+ withSaneMode f $+ B.writeFile f =<< B.reverse <$> B.readFile f+ Delete s -> withfile s $ nukeFile+ AppendGarbage s garbage ->+ withfile s $ \f ->+ withSaneMode f $+ B.appendFile f garbage+ PrependGarbage s garbage ->+ withfile s $ \f ->+ withSaneMode f $ do+ b <- B.readFile f+ B.writeFile f $ B.concat [garbage, b]+ -- When the byte is past the end of the+ -- file, wrap around. Does nothing to empty file.+ CorruptByte s n garbage ->+ withfile s $ \f ->+ withSaneMode f $ do+ b <- B.readFile f+ let len = B.length b+ unless (len == 0) $ do+ let n' = n `mod` len+ let (prefix, rest) = B.splitAt n' b+ B.writeFile f $ B.concat+ [prefix+ , B.singleton garbage+ , B.drop 1 rest+ ]+ ScrambleFileMode s mode ->+ withfile s $ \f ->+ setFileMode f mode+ SwapFiles a b ->+ withfile a $ \fa ->+ withfile b $ \fb -> + unless (fa == fb) $+ withTmpFile "swap" $ \tmp _ -> do+ moveFile fa tmp+ moveFile fb fa+ moveFile tmp fa where- -- A broken .git/config is not recoverable.- skipped f = f `elem` [ "config" ]--applyDamageAction :: DamageAction -> FilePath -> IO ()-applyDamageAction Empty f = withSaneMode f $ do- nukeFile f- writeFile f ""-applyDamageAction Reverse f = withSaneMode f $- B.writeFile f =<< B.reverse <$> B.readFile f-applyDamageAction Delete f = nukeFile f-applyDamageAction (AppendGarbage garbage) f = withSaneMode f $- B.appendFile f garbage-applyDamageAction (PrependGarbage garbage) f = withSaneMode f $ do- b <- B.readFile f- B.writeFile f $ B.concat [garbage, b]--- When the byte is past the end of the file, wrap around.--- Does nothing to empty file.-applyDamageAction (CorruptByte n garbage) f = withSaneMode f $ do- b <- B.readFile f- let len = B.length b- unless (len == 0) $ do- let n' = n `mod` len- let (prefix, rest) = B.splitAt n' b- B.writeFile f $ B.concat- [prefix- , B.singleton garbage- , B.drop 1 rest- ]-applyDamageAction (ScrambleFileMode mode) f = setFileMode f mode+ -- A broken .git/config is not recoverable.+ -- Don't damage hook scripts, to avoid running arbitrary code. ;)+ skipped f = or+ [ takeFileName f == "config"+ , "hooks" `isPrefixOf` f+ ] withSaneMode :: FilePath -> IO () -> IO () withSaneMode f = withModifiedFileMode f (addModes [ownerWriteMode, ownerReadMode])
Git/FilePath.hs view
@@ -20,12 +20,15 @@ asTopFilePath, InternalGitPath, toInternalGitPath,- fromInternalGitPath+ fromInternalGitPath,+ absoluteGitPath ) where import Common import Git +import qualified System.FilePath.Posix+ {- A FilePath, relative to the top of the git repository. -} newtype TopFilePath = TopFilePath { getTopFilePath :: FilePath } deriving (Show)@@ -48,8 +51,7 @@ - it internally. - - On Windows, git uses '/' to separate paths stored in the repository,- - despite Windows using '\'. Also, git on windows dislikes paths starting- - with "./" or ".\".+ - despite Windows using '\'. - -} type InternalGitPath = String@@ -58,11 +60,7 @@ #ifndef mingw32_HOST_OS toInternalGitPath = id #else-toInternalGitPath p =- let p' = replace "\\" "/" p- in if "./" `isPrefixOf` p'- then dropWhile (== '/') (drop 1 p')- else p'+toInternalGitPath = replace "\\" "/" #endif fromInternalGitPath :: InternalGitPath -> FilePath@@ -71,3 +69,10 @@ #else fromInternalGitPath = replace "/" "\\" #endif++{- isAbsolute on Windows does not think "/foo" or "\foo" is absolute,+ - so try posix paths.+ -}+absoluteGitPath :: FilePath -> Bool+absoluteGitPath p = isAbsolute p ||+ System.FilePath.Posix.isAbsolute (toInternalGitPath p)
Git/Fsck.hs view
@@ -20,7 +20,7 @@ import Git.Command import Git.Sha import Utility.Batch-import qualified Git.BuildVersion+import qualified Git.Version import qualified Data.Set as S @@ -40,12 +40,14 @@ -} findBroken :: Bool -> Repo -> IO FsckResults findBroken batchmode r = do- let (command, params) = ("git", fsckParams r)+ supportsNoDangling <- (>= Git.Version.normalize "1.7.10")+ <$> Git.Version.installed+ let (command, params) = ("git", fsckParams supportsNoDangling r) (command', params') <- if batchmode then toBatchCommand (command, params) else return (command, params) (output, fsckok) <- processTranscript command' (toCommand params') Nothing- let objs = findShas output+ let objs = findShas supportsNoDangling output badobjs <- findMissing objs r if S.null badobjs && not fsckok then return FsckFailed@@ -72,24 +74,21 @@ where dump = runQuiet [ Param "show"- , Param (show s)+ , Param (fromRef s) ] r -findShas :: String -> [Sha]-findShas = catMaybes . map extractSha . concat . map words . filter wanted . lines+findShas :: Bool -> String -> [Sha]+findShas supportsNoDangling = catMaybes . map extractSha . concat . map words . filter wanted . lines where wanted l | supportsNoDangling = True | otherwise = not ("dangling " `isPrefixOf` l) -fsckParams :: Repo -> [CommandParam]-fsckParams = gitCommandLine $ map Param $ catMaybes+fsckParams :: Bool -> Repo -> [CommandParam]+fsckParams supportsNoDangling = gitCommandLine $ map Param $ catMaybes [ Just "fsck" , if supportsNoDangling then Just "--no-dangling" else Nothing , Just "--no-reflogs" ]--supportsNoDangling :: Bool-supportsNoDangling = not $ Git.BuildVersion.older "1.7.10"
Git/LsTree.hs view
@@ -38,13 +38,13 @@ <$> pipeNullSplitZombie (lsTreeParams t) repo lsTreeParams :: Ref -> [CommandParam]-lsTreeParams t = [ Params "ls-tree --full-tree -z -r --", File $ show t ]+lsTreeParams t = [ Params "ls-tree --full-tree -z -r --", File $ fromRef t ] {- Lists specified files in a tree. -} lsTreeFiles :: Ref -> [FilePath] -> Repo -> IO [TreeItem] lsTreeFiles t fs repo = map parseLsTree <$> pipeNullSplitStrict ps repo where- ps = [Params "ls-tree --full-tree -z --", File $ show t] ++ map File fs+ ps = [Params "ls-tree --full-tree -z --", File $ fromRef t] ++ map File fs {- Parses a line of ls-tree output. - (The --long format is not currently supported.) -}
Git/Objects.hs view
@@ -32,4 +32,4 @@ looseObjectFile :: Repo -> Sha -> FilePath looseObjectFile r sha = objectsDir r </> prefix </> rest where- (prefix, rest) = splitAt 2 (show sha)+ (prefix, rest) = splitAt 2 (fromRef sha)
Git/Ref.hs view
@@ -11,6 +11,7 @@ import Git import Git.Command import Git.Sha+import Git.Types import Data.Char (chr) @@ -19,12 +20,12 @@ {- Converts a fully qualified git ref into a user-visible string. -} describe :: Ref -> String-describe = show . base+describe = fromRef . base {- Often git refs are fully qualified (eg: refs/heads/master). - Converts such a fully qualified ref into a base ref (eg: master). -} base :: Ref -> Ref-base = Ref . remove "refs/heads/" . remove "refs/remotes/" . show+base = Ref . remove "refs/heads/" . remove "refs/remotes/" . fromRef where remove prefix s | prefix `isPrefixOf` s = drop (length prefix) s@@ -34,13 +35,13 @@ - it under the directory. -} under :: String -> Ref -> Ref under dir r = Ref $ dir ++ "/" ++- (reverse $ takeWhile (/= '/') $ reverse $ show r)+ (reverse $ takeWhile (/= '/') $ reverse $ fromRef r) {- Given a directory such as "refs/remotes/origin", and a ref such as - refs/heads/master, yields a version of that ref under the directory, - such as refs/remotes/origin/master. -} underBase :: String -> Ref -> Ref-underBase dir r = Ref $ dir ++ "/" ++ show (base r)+underBase dir r = Ref $ dir ++ "/" ++ fromRef (base r) {- A Ref that can be used to refer to a file in the repository, as staged - in the index.@@ -51,6 +52,10 @@ fileRef :: FilePath -> Ref fileRef f = Ref $ ":./" ++ f +{- Converts a Ref to refer to the content of the Ref on a given date. -}+dateRef :: Ref -> RefDate -> Ref+dateRef (Ref r) (RefDate d) = Ref $ r ++ "@" ++ d+ {- A Ref that can be used to refer to a file in the repository as it - appears in a given Ref. -} fileFromRef :: Ref -> FilePath -> Ref@@ -59,12 +64,12 @@ {- Checks if a ref exists. -} exists :: Ref -> Repo -> IO Bool exists ref = runBool- [Param "show-ref", Param "--verify", Param "-q", Param $ show ref]+ [Param "show-ref", Param "--verify", Param "-q", Param $ fromRef ref] {- The file used to record a ref. (Git also stores some refs in a - packed-refs file.) -} file :: Ref -> Repo -> FilePath-file ref repo = localGitDir repo </> show ref+file ref repo = localGitDir repo </> fromRef ref {- Checks if HEAD exists. It generally will, except for in a repository - that was just created. -}@@ -79,17 +84,17 @@ where showref = pipeReadStrict [Param "show-ref", Param "--hash", -- get the hash- Param $ show branch]+ Param $ fromRef branch] process [] = Nothing process s = Just $ Ref $ firstLine s {- List of (shas, branches) matching a given ref or refs. -} matching :: [Ref] -> Repo -> IO [(Sha, Branch)]-matching refs repo = matching' (map show refs) repo+matching refs repo = matching' (map fromRef refs) repo {- Includes HEAD in the output, if asked for it. -} matchingWithHEAD :: [Ref] -> Repo -> IO [(Sha, Branch)]-matchingWithHEAD refs repo = matching' ("--head" : map show refs) repo+matchingWithHEAD refs repo = matching' ("--head" : map fromRef refs) repo {- List of (shas, branches) matching a given ref or refs. -} matching' :: [String] -> Repo -> IO [(Sha, Branch)]@@ -109,7 +114,7 @@ {- Gets the sha of the tree a ref uses. -} tree :: Ref -> Repo -> IO (Maybe Sha) tree ref = extractSha <$$> pipeReadStrict- [ Param "rev-parse", Param (show ref ++ ":") ]+ [ Param "rev-parse", Param (fromRef ref ++ ":") ] {- Checks if a String is a legal git ref name. -
Git/RefLog.hs view
@@ -18,5 +18,5 @@ [ Param "log" , Param "-g" , Param "--format=%H"- , Param (show b)+ , Param (fromRef b) ]
Git/Repair.hs view
@@ -75,24 +75,35 @@ return True else return False +{- Explodes all pack files, and deletes them.+ -+ - First moves all pack files to a temp dir, before unpacking them each in+ - turn.+ -+ - This is because unpack-objects will not unpack a pack file if it's in the+ - git repo.+ -+ - Also, this prevents unpack-objects from possibly looking at corrupt+ - pack files to see if they contain an object, while unpacking a+ - non-corrupt pack file.+ -} explodePacks :: Repo -> IO Bool-explodePacks r = do- packs <- listPackFiles r- if null packs- then return False- else do- putStrLn "Unpacking all pack files."- mapM_ go packs- return True+explodePacks r = go =<< listPackFiles r where- go packfile = withTmpFileIn (localGitDir r) "pack" $ \tmp _ -> do- moveFile packfile tmp- nukeFile $ packIdxFile packfile- allowRead tmp- -- May fail, if pack file is corrupt.- void $ tryIO $- pipeWrite [Param "unpack-objects", Param "-r"] r $ \h ->+ go [] = return False+ go packs = withTmpDir "packs" $ \tmpdir -> do+ putStrLn "Unpacking all pack files."+ forM_ packs $ \packfile -> do+ moveFile packfile (tmpdir </> takeFileName packfile)+ nukeFile $ packIdxFile packfile+ forM_ packs $ \packfile -> do+ let tmp = tmpdir </> takeFileName packfile+ allowRead tmp+ -- May fail, if pack file is corrupt.+ void $ tryIO $+ pipeWrite [Param "unpack-objects", Param "-r"] r $ \h -> L.hPut h =<< L.readFile tmp+ return True {- Try to retrieve a set of missing objects, from the remotes of a - repository. Returns any that could not be retreived.@@ -168,7 +179,7 @@ resetLocalBranches missing goodcommits r = go [] [] goodcommits =<< filter islocalbranch <$> getAllRefs r where- islocalbranch b = "refs/heads/" `isPrefixOf` show b+ islocalbranch b = "refs/heads/" `isPrefixOf` fromRef b go changed deleted gcs [] = return (changed, deleted, gcs) go changed deleted gcs (b:bs) = do (mc, gcs') <- findUncorruptedCommit missing gcs b r@@ -185,12 +196,12 @@ nukeBranchRef b r void $ runBool [ Param "branch"- , Param (show $ Ref.base b)- , Param (show c)+ , Param (fromRef $ Ref.base b)+ , Param (fromRef c) ] r isTrackingBranch :: Ref -> Bool-isTrackingBranch b = "refs/remotes/" `isPrefixOf` show b+isTrackingBranch b = "refs/remotes/" `isPrefixOf` fromRef b {- To deal with missing objects that cannot be recovered, removes - any branches (filtered by a predicate) that reference them@@ -231,10 +242,10 @@ nukeFile f where makeref (sha, ref) = do- let dest = localGitDir r </> show ref+ let dest = localGitDir r </> fromRef ref createDirectoryIfMissing True (parentDir dest) unlessM (doesFileExist dest) $- writeFile dest (show sha)+ writeFile dest (fromRef sha) packedRefsFile :: Repo -> FilePath packedRefsFile r = localGitDir r </> "packed-refs"@@ -249,7 +260,7 @@ {- git-branch -d cannot be used to remove a branch that is directly - pointing to a corrupt commit. -} nukeBranchRef :: Branch -> Repo -> IO ()-nukeBranchRef b r = nukeFile $ localGitDir r </> show b+nukeBranchRef b r = nukeFile $ localGitDir r </> fromRef b {- Finds the most recent commit to a branch that does not need any - of the missing objects. If the input branch is good as-is, returns it.@@ -268,7 +279,7 @@ [ Param "log" , Param "-z" , Param "--format=%H"- , Param (show branch)+ , Param (fromRef branch) ] r let branchshas = catMaybes $ map extractSha ls reflogshas <- RefLog.get branch r@@ -297,7 +308,7 @@ [ Param "log" , Param "-z" , Param "--format=%H %T"- , Param (show commit)+ , Param (fromRef commit) ] r let committrees = map parse ls if any isNothing committrees || null committrees@@ -501,9 +512,9 @@ , "remote tracking branches that referred to missing objects." ] (resetbranches, deletedbranches, _) <- resetLocalBranches stillmissing goodcommits g- displayList (map show resetbranches)+ displayList (map fromRef resetbranches) "Reset these local branches to old versions before the missing objects were committed:"- displayList (map show deletedbranches)+ displayList (map fromRef deletedbranches) "Deleted these local branches, which could not be recovered due to missing objects:" deindexedfiles <- rewriteIndex g displayList deindexedfiles@@ -519,7 +530,7 @@ Just curr -> when (any (== curr) modifiedbranches) $ do putStrLn $ unwords [ "You currently have"- , show curr+ , fromRef curr , "checked out. You may have staged changes in the index that can be committed to recover the lost state of this branch!" ] putStrLn "Successfully recovered repository!"
Git/Sha.hs view
@@ -37,3 +37,7 @@ nullSha :: Ref nullSha = Ref $ replicate shaSize '0'++{- Git's magic empty tree. -}+emptyTree :: Ref+emptyTree = Ref "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
Git/Types.hs view
@@ -47,15 +47,19 @@ {- A git ref. Can be a sha1, or a branch or tag name. -} newtype Ref = Ref String- deriving (Eq, Ord)+ deriving (Eq, Ord, Read, Show) -instance Show Ref where- show (Ref v) = v+fromRef :: Ref -> String+fromRef (Ref s) = s {- Aliases for Ref. -} type Branch = Ref type Sha = Ref type Tag = Ref++{- A date in the format described in gitrevisions. Includes the+ - braces, eg, "{yesterday}" -}+newtype RefDate = RefDate String {- Types of objects that can be stored in git. -} data ObjectType = BlobObject | CommitObject | TreeObject
Git/UpdateIndex.hs view
@@ -11,6 +11,9 @@ Streamer, pureStreamer, streamUpdateIndex,+ streamUpdateIndex',+ startUpdateIndex,+ stopUpdateIndex, lsTree, updateIndexLine, stageFile,@@ -25,6 +28,9 @@ import Git.FilePath import Git.Sha +import Control.Exception (bracket)+import System.Process (std_in)+ {- Streamers are passed a callback and should feed it lines in the form - read by update-index, and generated by ls-tree. -} type Streamer = (String -> IO ()) -> IO ()@@ -35,17 +41,30 @@ {- Streams content into update-index from a list of Streamers. -} streamUpdateIndex :: Repo -> [Streamer] -> IO ()-streamUpdateIndex repo as = pipeWrite params repo $ \h -> do+streamUpdateIndex repo as = bracket (startUpdateIndex repo) stopUpdateIndex $+ (\h -> forM_ as $ streamUpdateIndex' h)++data UpdateIndexHandle = UpdateIndexHandle ProcessHandle Handle++streamUpdateIndex' :: UpdateIndexHandle -> Streamer -> IO ()+streamUpdateIndex' (UpdateIndexHandle _ h) a = a $ \s -> do+ hPutStr h s+ hPutStr h "\0"++startUpdateIndex :: Repo -> IO UpdateIndexHandle+startUpdateIndex repo = do+ (Just h, _, _, p) <- createProcess (gitCreateProcess params repo)+ { std_in = CreatePipe } fileEncoding h- forM_ as (stream h)- hClose h+ return $ UpdateIndexHandle p h where params = map Param ["update-index", "-z", "--index-info"]- stream h a = a (streamer h)- streamer h s = do- hPutStr h s- hPutStr h "\0" +stopUpdateIndex :: UpdateIndexHandle -> IO Bool+stopUpdateIndex (UpdateIndexHandle p h) = do+ hClose h+ checkSuccessProcess p+ {- A streamer that adds the current tree for a ref. Useful for eg, copying - and modifying branches. -} lsTree :: Ref -> Repo -> Streamer@@ -60,7 +79,7 @@ - a given file with a given sha. -} updateIndexLine :: Sha -> BlobType -> TopFilePath -> String updateIndexLine sha filetype file =- show filetype ++ " blob " ++ show sha ++ "\t" ++ indexPath file+ show filetype ++ " blob " ++ fromRef sha ++ "\t" ++ indexPath file stageFile :: Sha -> BlobType -> FilePath -> Repo -> IO Streamer stageFile sha filetype file repo = do@@ -71,7 +90,7 @@ unstageFile :: FilePath -> Repo -> IO Streamer unstageFile file repo = do p <- toTopFilePath file repo- return $ pureStreamer $ "0 " ++ show nullSha ++ "\t" ++ indexPath p+ return $ pureStreamer $ "0 " ++ fromRef nullSha ++ "\t" ++ indexPath p {- A streamer that adds a symlink to the index. -} stageSymlink :: FilePath -> Sha -> Repo -> IO Streamer
Utility/Directory.hs view
@@ -1,6 +1,6 @@ {- directory manipulation -- - Copyright 2011 Joey Hess <joey@kitenet.net>+ - Copyright 2011-2014 Joey Hess <joey@kitenet.net> - - Licensed under the GNU GPL version 3 or higher. -}@@ -10,7 +10,6 @@ module Utility.Directory where import System.IO.Error-import System.PosixCompat.Files import System.Directory import Control.Exception (throw) import Control.Monad@@ -19,10 +18,12 @@ import Control.Applicative import System.IO.Unsafe (unsafeInterleaveIO) +import Utility.PosixFiles import Utility.SafeCommand import Utility.Tmp import Utility.Exception import Utility.Monad+import Utility.Applicative dirCruft :: FilePath -> Bool dirCruft "." = True@@ -72,6 +73,21 @@ , skip ) _ -> skip++{- Gets the directory tree from a point, recursively and lazily,+ - with leaf directories **first**, skipping any whose basenames+ - match the skipdir. Does not follow symlinks. -}+dirTreeRecursiveSkipping :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+dirTreeRecursiveSkipping skipdir topdir = go [] [topdir]+ where+ go c [] = return c+ go c (dir:dirs)+ | skipdir (takeFileName dir) = go c dirs+ | otherwise = unsafeInterleaveIO $ do+ subdirs <- go c+ =<< filterM (isDirectory <$$> getSymbolicLinkStatus)+ =<< catchDefaultIO [] (dirContents dir)+ go (subdirs++[dir]) dirs {- Moves one filename to another. - First tries a rename, but falls back to moving across devices if needed. -}
Utility/Env.hs view
@@ -61,3 +61,21 @@ #else unsetEnv _ = return False #endif++{- Adds the environment variable to the input environment. If already+ - present in the list, removes the old value.+ -+ - This does not really belong here, but Data.AssocList is for some reason+ - buried inside hxt.+ -}+addEntry :: Eq k => k -> v -> [(k, v)] -> [(k, v)]+addEntry k v l = ( (k,v) : ) $! delEntry k l++addEntries :: Eq k => [(k, v)] -> [(k, v)] -> [(k, v)]+addEntries = foldr (.) id . map (uncurry addEntry) . reverse++delEntry :: Eq k => k -> [(k, v)] -> [(k, v)]+delEntry _ [] = []+delEntry k (x@(k1,_) : rest)+ | k == k1 = rest+ | otherwise = ( x : ) $! delEntry k rest
Utility/FileMode.hs view
@@ -133,10 +133,8 @@ - as writeFile. -} writeFileProtected :: FilePath -> String -> IO ()-writeFileProtected file content = do- h <- openFile file WriteMode+writeFileProtected file content = withFile file WriteMode $ \h -> do void $ tryIO $ modifyFileMode file $ removeModes [groupReadMode, otherReadMode] hPutStr h content- hClose h
Utility/Misc.hs view
@@ -33,12 +33,19 @@ readFileStrict :: FilePath -> IO String readFileStrict = readFile >=> \s -> length s `seq` return s -{- Reads a file strictly, and using the FileSystemEncofing, so it will+{- Reads a file strictly, and using the FileSystemEncoding, so it will - never crash on a badly encoded file. -} readFileStrictAnyEncoding :: FilePath -> IO String readFileStrictAnyEncoding f = withFile f ReadMode $ \h -> do fileEncoding h hClose h `after` hGetContentsStrict h++{- Writes a file, using the FileSystemEncoding so it will never crash+ - on a badly encoded content string. -}+writeFileAnyEncoding :: FilePath -> String -> IO ()+writeFileAnyEncoding f content = withFile f WriteMode $ \h -> do+ fileEncoding h+ hPutStr h content {- Like break, but the item matching the condition is not included - in the second result list.
Utility/Path.hs view
@@ -1,6 +1,6 @@ {- path manipulation -- - Copyright 2010-2013 Joey Hess <joey@kitenet.net>+ - Copyright 2010-2014 Joey Hess <joey@kitenet.net> - - Licensed under the GNU GPL version 3 or higher. -}@@ -21,28 +21,60 @@ import Data.Char import qualified System.FilePath.Posix as Posix #else-import qualified "MissingH" System.Path as MissingH import System.Posix.Files #endif +import qualified "MissingH" System.Path as MissingH import Utility.Monad import Utility.UserInfo -{- Makes a path absolute if it's not already.+{- Simplifies a path, removing any ".." or ".", and removing the trailing+ - path separator.+ -+ - On Windows, preserves whichever style of path separator might be used in+ - the input FilePaths. This is done because some programs in Windows+ - demand a particular path separator -- and which one actually varies!+ -+ - This does not guarantee that two paths that refer to the same location,+ - and are both relative to the same location (or both absolute) will+ - yeild the same result. Run both through normalise from System.FilePath+ - to ensure that.+ -}+simplifyPath :: FilePath -> FilePath+simplifyPath path = dropTrailingPathSeparator $ + joinDrive drive $ joinPath $ norm [] $ splitPath path'+ where+ (drive, path') = splitDrive path++ norm c [] = reverse c+ norm c (p:ps)+ | p' == ".." = norm (drop 1 c) ps+ | p' == "." = norm c ps+ | otherwise = norm (p:c) ps+ where+ p' = dropTrailingPathSeparator p++{- Makes a path absolute.+ - - The first parameter is a base directory (ie, the cwd) to use if the path - is not already absolute. -- - On Unix, collapses and normalizes ".." etc in the path. May return Nothing- - if the path cannot be normalized.- -- - MissingH's absNormPath does not work on Windows, so on Windows- - no normalization is done.+ - Does not attempt to deal with edge cases or ensure security with+ - untrusted inputs. -}-absNormPath :: FilePath -> FilePath -> Maybe FilePath+absPathFrom :: FilePath -> FilePath -> FilePath+absPathFrom dir path = simplifyPath (combine dir path)++{- On Windows, this converts the paths to unix-style, in order to run+ - MissingH's absNormPath on them. Resulting path will use / separators. -}+absNormPathUnix :: FilePath -> FilePath -> Maybe FilePath #ifndef mingw32_HOST_OS-absNormPath dir path = MissingH.absNormPath dir path+absNormPathUnix dir path = MissingH.absNormPath dir path #else-absNormPath dir path = Just $ combine dir path+absNormPathUnix dir path = todos <$> MissingH.absNormPath (fromdos dir) (fromdos path)+ where+ fromdos = replace "\\" "/"+ todos = replace "/" "\\" #endif {- Returns the parent directory of a path.@@ -72,13 +104,13 @@ - are all equivilant. -} dirContains :: FilePath -> FilePath -> Bool-dirContains a b = a == b || a' == b' || (a'++[pathSeparator]) `isPrefixOf` b'+dirContains a b = a == b || a' == b' || (addTrailingPathSeparator a') `isPrefixOf` b' where- norm p = fromMaybe "" $ absNormPath p "." a' = norm a b' = norm b+ norm = normalise . simplifyPath -{- Converts a filename into a normalized, absolute path.+{- Converts a filename into an absolute path. - - Unlike Directory.canonicalizePath, this does not require the path - already exists. -}@@ -87,13 +119,6 @@ cwd <- getCurrentDirectory return $ absPathFrom cwd file -{- Converts a filename into a normalized, absolute path- - from the specified cwd. -}-absPathFrom :: FilePath -> FilePath -> FilePath-absPathFrom cwd file = fromMaybe bad $ absNormPath cwd file- where- bad = error $ "unable to normalize " ++ file- {- Constructs a relative path from the CWD to a file. - - For example, assuming CWD is /tmp/foo/bar:@@ -105,7 +130,7 @@ {- Constructs a relative path from a directory to a file. -- - Both must be absolute, and normalized (eg with absNormpath).+ - Both must be absolute, and cannot contain .. etc. (eg use absPath first). -} relPathDirToFile :: FilePath -> FilePath -> FilePath relPathDirToFile from to = join s $ dotdots ++ uncommon@@ -252,3 +277,18 @@ | c == '.' = c | isSpace c || isPunctuation c || isSymbol c || isControl c || c == '/' = '_' | otherwise = c++{- Similar to splitExtensions, but knows that some things in FilePaths+ - after a dot are too long to be extensions. -}+splitShortExtensions :: FilePath -> (FilePath, [String])+splitShortExtensions = splitShortExtensions' 5 -- enough for ".jpeg"+splitShortExtensions' :: Int -> FilePath -> (FilePath, [String])+splitShortExtensions' maxextension = go []+ where+ go c f+ | len > 0 && len <= maxextension && not (null base) = + go (ext:c) base+ | otherwise = (f, c)+ where+ (base, ext) = splitExtension f+ len = length ext
+ Utility/PosixFiles.hs view
@@ -0,0 +1,33 @@+{- POSIX files (and compatablity wrappers).+ -+ - This is like System.PosixCompat.Files, except with a fixed rename.+ -+ - Copyright 2014 Joey Hess <joey@kitenet.net>+ -+ - Licensed under the GNU GPL version 3 or higher.+ -}++{-# LANGUAGE CPP #-}++module Utility.PosixFiles (+ module X,+ rename+) where++import System.PosixCompat.Files as X hiding (rename)++#ifndef mingw32_HOST_OS+import System.Posix.Files (rename)+#else+import qualified System.Win32.File as Win32+#endif++{- System.PosixCompat.Files.rename on Windows calls renameFile,+ - so cannot rename directories. + -+ - Instead, use Win32 moveFile, which can. It needs to be told to overwrite+ - any existing file. -}+#ifdef mingw32_HOST_OS+rename :: FilePath -> FilePath -> IO ()+rename src dest = Win32.moveFileEx src dest Win32.mOVEFILE_REPLACE_EXISTING+#endif
Utility/QuickCheck.hs view
@@ -1,6 +1,6 @@ {- QuickCheck with additional instances -- - Copyright 2012 Joey Hess <joey@kitenet.net>+ - Copyright 2012-2014 Joey Hess <joey@kitenet.net> - - Licensed under the GNU GPL version 3 or higher. -}@@ -17,10 +17,14 @@ import Data.Time.Clock.POSIX import System.Posix.Types import qualified Data.Map as M+import qualified Data.Set as S import Control.Applicative instance (Arbitrary k, Arbitrary v, Eq k, Ord k) => Arbitrary (M.Map k v) where arbitrary = M.fromList <$> arbitrary++instance (Arbitrary v, Eq v, Ord v) => Arbitrary (S.Set v) where+ arbitrary = S.fromList <$> arbitrary {- Times before the epoch are excluded. -} instance Arbitrary POSIXTime where
Utility/Tmp.hs view
@@ -13,10 +13,11 @@ import System.IO import System.Directory import Control.Monad.IfElse+import System.FilePath import Utility.Exception-import System.FilePath import Utility.FileSystemEncoding+import Utility.PosixFiles type Template = String @@ -30,7 +31,7 @@ (tmpfile, handle) <- openTempFile dir (base ++ ".tmp") hClose handle a tmpfile content- renameFile tmpfile file+ rename tmpfile file {- Runs an action with a tmp file located in the system's tmp directory - (or in "." if there is none) then removes the file. -}
debian/changelog view
@@ -1,9 +1,18 @@+git-repair (1.20140227) unstable; urgency=medium++ * Optimise unpacking of pack files, and avoid repeated error+ messages about corrupt pack files.+ * Add swapping 2 files test case.++ -- Joey Hess <joeyh@debian.org> Thu, 27 Feb 2014 11:56:27 -0400+ git-repair (1.20140115) unstable; urgency=medium * Support old git versions from before git fsck --no-dangling was implemented. * Fix bug in packed refs file exploding code that caused a .gitrefs directory to be created instead of .git/refs+ * Check git version at run time. -- Joey Hess <joeyh@debian.org> Wed, 15 Jan 2014 16:53:30 -0400
debian/control view
@@ -17,7 +17,7 @@ libghc-async-dev, libghc-optparse-applicative-dev Maintainer: Joey Hess <joeyh@debian.org>-Standards-Version: 3.9.4+Standards-Version: 3.9.5 Vcs-Git: git://git-repair.branchable.com/ Homepage: http://git-repair.branchable.com/
doc/index.mdwn view
@@ -44,7 +44,7 @@ try to clean up either the reflog or the stash. Also note that the `--force` option never touches tags, even if they are no-longer usable due to missing data, so fack may also find problems with+longer usable due to missing data, so fsck may also find problems with tags. Since this command unpacks all packs in the repository, you may want to
git-repair.1 view
@@ -28,6 +28,18 @@ .PP Since this command unpacks all packs in the repository, you may want to run \fBgit gc\fP afterwards.+.SH TESTING+git-repair is able to test itself, by making a temporary copy+of the git reposiory it's run in, damaging it in random ways, and checking+that it can repair it so that git fsck reports no problems.+.PP+This is done using the --test parameter and associated --numtests and+--retry parameters.+.PP+Note that the testing will sometimes find repositories that can only be+repaired when --force is used; this is expected. On the other hand,+if it fails to recover a repository with --force, then the testing has+found a bug! .PP .SH AUTHOR Joey Hess <joey@kitenet.net>
git-repair.cabal view
@@ -1,5 +1,5 @@ Name: git-repair-Version: 1.20140115+Version: 1.20140227 Cabal-Version: >= 1.6 License: GPL Maintainer: Joey Hess <joey@kitenet.net>
git-repair.hs view
@@ -52,8 +52,8 @@ desc = fullDesc <> header "git-repair - repair a damanged git repository" go settings- | testMode settings = test settings | retryTestMode settings = retryTest settings+ | testMode settings = test settings | otherwise = repair settings repair :: Settings -> IO ()