packages feed

git-repair 1.20131118 → 1.20131122

raw patch · 20 files changed

+704/−188 lines, 20 filesdep +QuickCheckdep +SHAdep +optparse-applicativedep −pretty-show

Dependencies added: QuickCheck, SHA, optparse-applicative, time

Dependencies removed: pretty-show

Files

Build/Configure.hs view
@@ -7,11 +7,13 @@ import Control.Monad.IfElse  import Build.TestConfig+import Build.Version import Git.Version  tests :: [TestCase] tests =-	[ TestCase "git" $ requireCmd "git" "git --version >/dev/null"+	[ TestCase "version" getVersion+	, TestCase "git" $ requireCmd "git" "git --version >/dev/null" 	, TestCase "git version" getGitVersion 	, TestCase "nice" $ testCmd "nice" "nice true >/dev/null" 	]@@ -25,3 +27,5 @@ 	args <- getArgs 	config <- runTests ts 	writeSysConfig config+	whenM (isReleaseBuild) $+		cabalSetup "git-repair.cabal"
+ Build/Version.hs view
@@ -0,0 +1,69 @@+{- Package version determination, for configure script. -}++module Build.Version where++import Data.Maybe+import Control.Applicative+import Data.List+import System.Environment+import System.Directory+import Data.Char+import System.Process++import Build.TestConfig+import Utility.Monad+import Utility.Exception++{- Set when making an official release. (Distribution vendors should set+ - this too.) -}+isReleaseBuild :: IO Bool+isReleaseBuild = isJust <$> catchMaybeIO (getEnv "RELEASE_BUILD")++{- Version is usually based on the major version from the changelog, + - plus the date of the last commit, plus the git rev of that commit.+ - This works for autobuilds, ad-hoc builds, etc.+ -+ - If git or a git repo is not available, or something goes wrong,+ - or this is a release build, just use the version from the changelog. -}+getVersion :: Test+getVersion = do+	changelogversion <- getChangelogVersion+	version <- ifM (isReleaseBuild)+		( return changelogversion+		, catchDefaultIO changelogversion $ do+			let major = takeWhile (/= '.') changelogversion+			autoversion <- readProcess "sh"+				[ "-c"+				, "git log -n 1 --format=format:'%ci %h'| sed -e 's/-//g' -e 's/ .* /-g/'"+				] ""+			if null autoversion+				then return changelogversion+				else return $ concat [ major, ".", autoversion ]+		)+	return $ Config "packageversion" (StringConfig version)+	+getChangelogVersion :: IO String+getChangelogVersion = do+	changelog <- readFile "debian/changelog"+	let verline = takeWhile (/= '\n') changelog+	return $ middle (words verline !! 1)+  where+	middle = drop 1 . init++{- Set up cabal file with version. -}+cabalSetup :: FilePath -> IO ()+cabalSetup cabalfile = do+	version <- takeWhile (\c -> isDigit c || c == '.')+		<$> getChangelogVersion+	cabal <- readFile cabalfile+	writeFile tmpcabalfile $ unlines $ +		map (setfield "Version" version) $+		lines cabal+	renameFile tmpcabalfile cabalfile+  where+	tmpcabalfile = cabalfile++".tmp"+	setfield field value s+		| fullfield `isPrefixOf` s = fullfield ++ value+		| otherwise = s+	  where+		fullfield = field ++ ": "
+ Git/Destroyer.hs view
@@ -0,0 +1,126 @@+{- git repository destroyer+ -+ - Use with caution!+ -+ - Copyright 2013 Joey Hess <joey@kitenet.net>+ -+ - Licensed under the GNU GPL version 3 or higher.+ -}++module Git.Destroyer (+	Damage(..),+	generateDamage,+	applyDamage+) where++import Common+import Git+import Utility.QuickCheck+import Utility.FileMode++import qualified Data.ByteString as B+import Data.Word+import System.PosixCompat.Types++{- Ways to damange a git repository. -}+data Damage = Damage DamageAction 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+		, CorruptByte+			<$> nonNegative arbitraryBoundedIntegral+			<*> arbitrary+		, ScrambleFileMode <$> nonNegative arbitrarySizedIntegral+		]+	  where+		garbage = B.pack <$> arbitrary `suchThat` (not . null)++{- To select a given file in a git repository, all files in the repository+ - 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)++instance Arbitrary FileSelector where+	arbitrary = FileSelector <$> oneof+		-- An early file in the git tree, tends to be the most+		-- interesting when there are lots of files.+		[ nonNegative arbitrarySizedIntegral+		-- Totally random choice from any of the files in+		-- the git tree, to ensure good coverage.+		, nonNegative arbitraryBoundedIntegral+		]++selectFile :: [FilePath] -> FileSelector -> FilePath+selectFile sortedfs (FileSelector n) = sortedfs !! (n `mod` length sortedfs)++{- Generates random Damage. -}+generateDamage :: IO [Damage]+generateDamage = sample' (arbitrary :: Gen Damage)++{- 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)+		<$> 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 ++ ")")+  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++withSaneMode :: FilePath -> IO () -> IO ()+withSaneMode f = withModifiedFileMode f (addModes [ownerWriteMode, ownerReadMode])
Git/Fsck.hs view
@@ -17,7 +17,6 @@ import Git import Git.Command import Git.Sha-import Git.CatFile import Utility.Batch  import qualified Data.Set as S@@ -40,7 +39,7 @@ findBroken :: Bool -> Repo -> IO FsckResults findBroken batchmode r = do 	(output, fsckok) <- processTranscript command' (toCommand params') Nothing-	let objs = parseFsckOutput output+	let objs = findShas output 	badobjs <- findMissing objs r 	if S.null badobjs && not fsckok 		then return Nothing@@ -57,30 +56,23 @@  {- Finds objects that are missing from the git repsitory, or are corrupt.  -- - Note that catting a corrupt object will cause cat-file to crash;- - this is detected and it's restarted.+ - This does not use git cat-file --batch, because catting a corrupt+ - object can cause it to crash, or to report incorrect size information.a  -} findMissing :: [Sha] -> Repo -> IO MissingObjects-findMissing objs r = go objs [] =<< start+findMissing objs r = S.fromList <$> filterM (not <$$> present) objs   where-	start = catFileStart' False r-	go [] c h = do-		catFileStop h-		return $ S.fromList c-	go (o:os) c h = do-		v <- tryIO $ isNothing <$> catObjectDetails h o-		case v of-			Left _ -> do-				void $ tryIO $ catFileStop h-				go os (o:c) =<< start-			Right True -> go os (o:c) h-			Right False -> go os c h+	present o = either (const False) (const True) <$> tryIO (dump o)+	dump o = runQuiet+		[ Param "show"+		, Param (show o)+		] r -parseFsckOutput :: String -> [Sha]-parseFsckOutput = catMaybes . map extractSha . concat . map words . lines+findShas :: String -> [Sha]+findShas = catMaybes . map extractSha . concat . map words . lines  fsckParams :: Repo -> [CommandParam]-fsckParams = gitCommandLine+fsckParams = gitCommandLine $ 	[ Param "fsck" 	, Param "--no-dangling" 	, Param "--no-reflogs"
Git/Objects.hs view
@@ -9,6 +9,7 @@  import Common import Git+import Git.Sha  objectsDir :: Repo -> FilePath objectsDir r = localGitDir r </> "objects"@@ -16,12 +17,17 @@ packDir :: Repo -> FilePath packDir r = objectsDir r </> "pack" +packIdxFile :: FilePath -> FilePath+packIdxFile = flip replaceExtension "idx"+ listPackFiles :: Repo -> IO [FilePath] listPackFiles r = filter (".pack" `isSuffixOf`)  	<$> catchDefaultIO [] (dirContents $ packDir r) -packIdxFile :: FilePath -> FilePath-packIdxFile = flip replaceExtension "idx"+listLooseObjectShas :: Repo -> IO [Sha]+listLooseObjectShas r = catchDefaultIO [] $+	mapMaybe (extractSha . concat . reverse . take 2 . reverse . splitDirectories)+		<$> dirContentsRecursiveSkipping (== "pack") (objectsDir r)  looseObjectFile :: Repo -> Sha -> FilePath looseObjectFile r sha = objectsDir r </> prefix </> rest
Git/Repair.hs view
@@ -8,13 +8,13 @@ module Git.Repair ( 	runRepair, 	runRepairOf,+	successfulRepair, 	cleanCorruptObjects, 	retrieveMissingObjects, 	resetLocalBranches, 	removeTrackingBranches, 	checkIndex, 	missingIndex,-	nukeIndex, 	emptyGoodCommits, ) where @@ -35,71 +35,39 @@ import qualified Git.Branch as Branch import Utility.Tmp import Utility.Rsync+import Utility.FileMode  import qualified Data.Set as S import qualified Data.ByteString.Lazy as L import Data.Tuple.Utils -{- Given a set of bad objects found by git fsck, removes all- - corrupt objects, and returns a list of missing objects,- - which need to be found elsewhere to finish recovery.- -- - Since git fsck may crash on corrupt objects, and so not- - report the full set of corrupt or missing objects,- - this removes corrupt objects, and re-runs fsck, until it- - stabalizes.- -- - To remove corrupt objects, unpack all packs, and remove the packs- - (to handle corrupt packs), and remove loose object files.+{- Given a set of bad objects found by git fsck, which may not+ - be complete, finds and removes all corrupt objects, and+ - returns a list of missing objects, which need to be+ - found elsewhere to finish recovery.  -}-cleanCorruptObjects :: FsckResults -> Repo -> IO MissingObjects-cleanCorruptObjects mmissing r = check mmissing-  where-	check Nothing = do-		putStrLn "git fsck found a problem but no specific broken objects. Perhaps a corrupt pack file?"-		ifM (explodePacks r)-			( retry S.empty-			, return S.empty-			)-	check (Just bad)-		| S.null bad = return S.empty-		| otherwise = do-			putStrLn $ unwords -				[ "git fsck found"-				, show (S.size bad)-				, "broken objects."-				]-			exploded <- explodePacks r-			removed <- removeLoose r bad-			if exploded || removed-				then retry bad-				else return bad-	retry oldbad = do-		putStrLn "Re-running git fsck to see if it finds more problems."-		v <- findBroken False r-		case v of-			Nothing -> error $ unwords-				[ "git fsck found a problem, which was not corrected after removing"-				, show (S.size oldbad)-				, "corrupt objects."-				]-			Just newbad -> do-				removed <- removeLoose r newbad-				let s = S.union oldbad newbad-				if not removed || s == oldbad-					then return s-					else retry s+cleanCorruptObjects :: FsckResults -> Repo -> IO (Maybe MissingObjects)+cleanCorruptObjects fsckresults r = do+	void $ explodePacks r+	objs <- listLooseObjectShas r+	mapM_ (tryIO . allowRead . looseObjectFile r) objs+	bad <- findMissing objs r+	void $ removeLoose r $ S.union bad (fromMaybe S.empty fsckresults)+	-- Rather than returning the loose objects that were removed, re-run+	-- fsck. Other missing objects may have been in the packs,+	-- and this way fsck will find them.+	findBroken False r  removeLoose :: Repo -> MissingObjects -> IO Bool removeLoose r s = do-	let fs = map (looseObjectFile r) (S.toList s)-	count <- length <$> filterM doesFileExist fs-	if (count > 0)+	fs <- filterM doesFileExist (map (looseObjectFile r) (S.toList s))+	let count = length fs+	if count > 0 		then do 			putStrLn $ unwords-				[ "removing"+				[ "Removing" 				, show count-				, "corrupt loose objects"+				, "corrupt loose objects." 				] 			mapM_ nukeFile fs 			return True@@ -115,57 +83,62 @@ 			mapM_ go packs 			return True   where-	go packfile = do+	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"] r $ \h ->-				L.hPut h =<< L.readFile packfile-		nukeFile packfile-		nukeFile $ packIdxFile packfile+			pipeWrite [Param "unpack-objects", Param "-r"] r $ \h ->+				L.hPut h =<< L.readFile tmp  {- Try to retrieve a set of missing objects, from the remotes of a  - repository. Returns any that could not be retreived.- - + -  - If another clone of the repository exists locally, which might not be a  - remote of the repo being repaired, its path can be passed as a reference  - repository.+ + - Can also be run with Nothing, if it's not known which objects are+ - missing, just that some are. (Ie, fsck failed badly.)  -}-retrieveMissingObjects :: MissingObjects -> Maybe FilePath -> Repo -> IO MissingObjects+retrieveMissingObjects :: Maybe MissingObjects -> Maybe FilePath -> Repo -> IO (Maybe MissingObjects) retrieveMissingObjects missing referencerepo r-	| S.null missing = return missing+	| missing == Just S.empty = return $ Just S.empty 	| otherwise = withTmpDir "tmprepo" $ \tmpdir -> do 		unlessM (boolSystem "git" [Params "init", File tmpdir]) $ 			error $ "failed to create temp repository in " ++ tmpdir 		tmpr <- Config.read =<< Construct.fromAbsPath tmpdir 		stillmissing <- pullremotes tmpr (remotes r) fetchrefstags missing-		if S.null stillmissing-			then return stillmissing+		if stillmissing == Just S.empty+			then return $ Just S.empty 			else pullremotes tmpr (remotes r) fetchallrefs stillmissing   where 	pullremotes tmpr [] fetchrefs stillmissing = case referencerepo of 		Nothing -> return stillmissing 		Just p -> ifM (fetchfrom p fetchrefs tmpr) 			( do+				void $ explodePacks tmpr 				void $ copyObjects tmpr r-				findMissing (S.toList stillmissing) r+				case stillmissing of+					Nothing -> return $ Just S.empty+					Just s -> Just <$> findMissing (S.toList s) r 			, return stillmissing 			)-	pullremotes tmpr (rmt:rmts) fetchrefs s-		| S.null s = return s+	pullremotes tmpr (rmt:rmts) fetchrefs ms+		| ms == Just S.empty = return $ Just S.empty 		| otherwise = do-			putStrLn $ "Trying to recover missing objects from remote " ++ repoDescribe rmt+			putStrLn $ "Trying to recover missing objects from remote " ++ repoDescribe rmt ++ "." 			ifM (fetchfrom (repoLocation rmt) fetchrefs tmpr) 				( do+					void $ explodePacks tmpr 					void $ copyObjects tmpr r-					stillmissing <- findMissing (S.toList s) r-					pullremotes tmpr rmts fetchrefs stillmissing-				, do-					putStrLn $ unwords-						[ "failed to fetch from remote"-						, repoDescribe rmt-						, "(will continue without it, but making this remote available may improve recovery)"-						]-					pullremotes tmpr rmts fetchrefs s+					case ms of+						Nothing -> pullremotes tmpr rmts fetchrefs ms+						Just s -> do+							stillmissing <- findMissing (S.toList s) r+							pullremotes tmpr rmts fetchrefs (Just stillmissing)+				, pullremotes tmpr rmts fetchrefs ms 				) 	fetchfrom fetchurl ps = runBool $ 		[ Param "fetch"@@ -179,7 +152,7 @@ 	fetchallrefs = [ Param "+*:*" ]  {- Copies all objects from the src repository to the dest repository.- - This is done using rsync, so it copies all missing object, and all+ - This is done using rsync, so it copies all missing objects, and all  - objects they rely on. -} copyObjects :: Repo -> Repo -> IO Bool copyObjects srcr destr = rsync@@ -238,51 +211,44 @@ {- Gets all refs, including ones that are corrupt.  - git show-ref does not output refs to commits that are directly  - corrupted, so it is not used.+ -+ - Relies on packed refs being exploded before it's called.  -} getAllRefs :: Repo -> IO [Ref]-getAllRefs r = do-	packedrs <- mapMaybe parsePacked . lines-		<$> catchDefaultIO "" (readFile $ packedRefsFile r)-	loosers <- map toref <$> dirContentsRecursive refdir-	return $ packedrs ++ loosers+getAllRefs r = map toref <$> dirContentsRecursive refdir   where   	refdir = localGitDir r </> "refs" 	toref = Ref . relPathDirToFile (localGitDir r) +explodePackedRefsFile :: Repo -> IO ()+explodePackedRefsFile r = do+	let f = packedRefsFile r+	whenM (doesFileExist f) $ do+		rs <- mapMaybe parsePacked . lines+			<$> catchDefaultIO "" (safeReadFile f)+		forM_ rs makeref+		nukeFile f+  where+	makeref (sha, ref) = do+		let dest = localGitDir r ++ show ref+		createDirectoryIfMissing True (parentDir dest)+		unlessM (doesFileExist dest) $+			writeFile dest (show sha)+ packedRefsFile :: Repo -> FilePath packedRefsFile r = localGitDir r </> "packed-refs" -parsePacked :: String -> Maybe Ref+parsePacked :: String -> Maybe (Sha, Ref) parsePacked l = case words l of 	(sha:ref:[])-		| isJust (extractSha sha) -> Just $ Ref ref+		| isJust (extractSha sha) && Ref.legal True ref ->+			Just (Ref sha, Ref ref) 	_ -> Nothing  {- git-branch -d cannot be used to remove a branch that is directly- - pointing to a corrupt commit. However, it's tried first. -}+ - pointing to a corrupt commit. -} nukeBranchRef :: Branch -> Repo -> IO ()-nukeBranchRef b r = void $ usegit <||> byhand-  where-	usegit = runBool-		[ Param "branch"-		, Params "-r -d"-		, Param $ show $ Ref.base b-		] r-	byhand = do-		nukeFile $ localGitDir r </> show b-		whenM (doesFileExist packedrefs) $-			withTmpFile "packed-refs" $ \tmp h -> do-				ls <- lines <$> readFile packedrefs-				hPutStr h $ unlines $-					filter (not . skiprefline) ls-				hClose h-				renameFile tmp packedrefs-		return True-	skiprefline l = case parsePacked l of-		Just packedref-			| packedref == b -> True-		_ -> False-	packedrefs = packedRefsFile r+nukeBranchRef b r = nukeFile $ localGitDir r </> show 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.@@ -402,7 +368,7 @@ 	| otherwise = do 		(bad, good, cleanup) <- partitionIndex missing r 		unless (null bad) $ do-			nukeIndex r+			nukeFile (indexFile r) 			UpdateIndex.streamUpdateIndex r 				=<< (catMaybes <$> mapM reinject good) 		void cleanup@@ -414,8 +380,8 @@ 			UpdateIndex.stageFile sha blobtype file r 	reinject _ = return Nothing -nukeIndex :: Repo -> IO ()-nukeIndex r = nukeFile (localGitDir r </> "index")+indexFile :: Repo -> FilePath+indexFile r = localGitDir r </> "index"  newtype GoodCommits = GoodCommits (S.Set Sha) @@ -441,48 +407,91 @@ 		| numitems > 10 = take 10 items ++ ["(and " ++ show (numitems - 10) ++ " more)"] 		| otherwise = items +{- Fix problems that would prevent repair from working at all+ -+ - A missing or corrupt .git/HEAD makes git not treat the repository as a+ - git repo. If there is a git repo in a parent directory, it may move up+ - the tree and use that one instead. So, cannot use `git show-ref HEAD` to+ - test it.+ -+ - Explode the packed refs file, to simplify dealing with refs, and because+ - fsck can complain about bad refs in it.+ -}+preRepair :: Repo -> IO ()+preRepair g = do+	unlessM (validhead <$> catchDefaultIO "" (safeReadFile headfile)) $ do+		nukeFile headfile+		writeFile headfile "ref: refs/heads/master"+	explodePackedRefsFile g+	unless (repoIsLocalBare g) $ do+		let f = indexFile g+		void $ tryIO $ allowWrite f+  where+	headfile = localGitDir g </> "HEAD"+	validhead s = "ref: refs/" `isPrefixOf` s || isJust (extractSha s)+ {- Put it all together. -} runRepair :: Bool -> Repo -> IO (Bool, MissingObjects, [Branch]) runRepair forced g = do+	preRepair g 	putStrLn "Running git fsck ..." 	fsckresult <- findBroken False g 	if foundBroken fsckresult-		then runRepairOf fsckresult forced Nothing g+		then runRepair' fsckresult forced Nothing g 		else do 			putStrLn "No problems found." 			return (True, S.empty, [])  runRepairOf :: FsckResults -> Bool -> Maybe FilePath -> Repo -> IO (Bool, MissingObjects, [Branch]) runRepairOf fsckresult forced referencerepo g = do+	preRepair g+	runRepair' fsckresult forced referencerepo g++runRepair' :: FsckResults -> Bool -> Maybe FilePath -> Repo -> IO (Bool, MissingObjects, [Branch])+runRepair' fsckresult forced referencerepo g = do 	missing <- cleanCorruptObjects fsckresult g 	stillmissing <- retrieveMissingObjects missing referencerepo g-	if S.null stillmissing-		then if repoIsLocalBare g-			then successfulfinish stillmissing []-			else ifM (checkIndex stillmissing g)-				( successfulfinish stillmissing []-				, do-					putStrLn "No missing objects found, but the index file is corrupt!"-					if forced-						then corruptedindex-						else needforce stillmissing-				)		-		else do-			putStrLn $ unwords-				[ show (S.size stillmissing)-				, "missing objects could not be recovered!"-				]-			if forced-				then continuerepairs stillmissing-				else unsuccessfulfinish stillmissing+	case stillmissing of+		Just s+			| S.null s -> if repoIsLocalBare g+				then successfulfinish S.empty []+				else ifM (checkIndex S.empty g)+					( successfulfinish s []+					, do+						putStrLn "No missing objects found, but the index file is corrupt!"+						if forced+							then corruptedindex+							else needforce S.empty+					)+			| otherwise -> if forced+				then ifM (checkIndex s g)+					( continuerepairs s+					, corruptedindex+					)+				else do+					putStrLn $ unwords+						[ show (S.size s)+						, "missing objects could not be recovered!"+						]+					unsuccessfulfinish s+		Nothing+			| forced -> ifM (pure (repoIsLocalBare g) <||> checkIndex S.empty g)+				( do+					missing' <- cleanCorruptObjects Nothing g+					case missing' of+						Nothing -> return (False, S.empty, [])+						Just stillmissing' -> continuerepairs stillmissing'+				, corruptedindex+				)+			| otherwise -> unsuccessfulfinish S.empty   where 	continuerepairs stillmissing = do 		(remotebranches, goodcommits) <- removeTrackingBranches stillmissing emptyGoodCommits g 		unless (null remotebranches) $ 			putStrLn $ unwords-				[ "removed"+				[ "Removed" 				, show (length remotebranches)-				, "remote tracking branches that referred to missing objects"+				, "remote tracking branches that referred to missing objects." 				] 		(resetbranches, deletedbranches, _) <- resetLocalBranches stillmissing goodcommits g 		displayList (map show resetbranches)@@ -511,15 +520,18 @@ 				return (True, stillmissing, modifiedbranches) 	 	corruptedindex = do-		nukeIndex g+		nukeFile (indexFile g)+		-- The corrupted index can prevent fsck from finding other+		-- problems, so re-run repair.+		fsckresult' <- findBroken False g+		result <- runRepairOf fsckresult' forced referencerepo g 		putStrLn "Removed the corrupted index file. You should look at what files are present in your working tree and git add them back to the index when appropriate."-		return (True, S.empty, [])+		return result  	successfulfinish stillmissing modifiedbranches = do 		mapM_ putStrLn 			[ "Successfully recovered repository!"-			, "You should run \"git fsck\" to make sure, but it looks like"-			, "everything was recovered ok."+			, "You should run \"git fsck\" to make sure, but it looks like everything was recovered ok." 			] 		return (True, stillmissing, modifiedbranches) 	unsuccessfulfinish stillmissing = do@@ -532,3 +544,11 @@ 	needforce stillmissing = do 		putStrLn "To force a recovery to a usable state, retry with the --force parameter." 		return (False, stillmissing, [])++successfulRepair :: (Bool, MissingObjects, [Branch]) -> Bool+successfulRepair = fst3++safeReadFile :: FilePath -> IO String+safeReadFile f = do+	allowRead f+	readFileStrictAnyEncoding f
Makefile view
@@ -17,9 +17,10 @@ 	install -m 0644 git-repair.1 $(DESTDIR)$(PREFIX)/share/man/man1  clean:-	rm -rf git-repair dist configure Build/SysConfig.hs Setup tags-	find -name \*.o -exec rm {} \;-	find -name \*.hi -exec rm {} \;+	rm -rf git-repair git-repair-test.log \+		dist configure Build/SysConfig.hs Setup tags+	find . -name \*.o -exec rm {} \;+	find . -name \*.hi -exec rm {} \;  # Upload to hackage. hackage: clean@@ -29,3 +30,5 @@ # hothasktags chokes on some template haskell etc, so ignore errors tags: 	find . | grep -v /.git/ | grep -v /tmp/ | grep -v /dist/ | grep -v /doc/ | egrep '\.hs$$' | xargs hothasktags > tags 2>/dev/null++.PHONY: tags
Utility/FileMode.hs view
@@ -64,12 +64,19 @@ allowWrite :: FilePath -> IO () allowWrite f = modifyFileMode f $ addModes [ownerWriteMode] +{- Turns a file's owner read bit back on. -}+allowRead :: FilePath -> IO ()+allowRead f = modifyFileMode f $ addModes [ownerReadMode]+ {- Allows owner and group to read and write to a file. -}-groupWriteRead :: FilePath -> IO ()-groupWriteRead f = modifyFileMode f $ addModes+groupSharedModes :: [FileMode]+groupSharedModes = 	[ ownerWriteMode, groupWriteMode 	, ownerReadMode, groupReadMode 	]++groupWriteRead :: FilePath -> IO ()+groupWriteRead f = modifyFileMode f $ addModes groupSharedModes  checkMode :: FileMode -> FileMode -> Bool checkMode checkfor mode = checkfor `intersectFileModes` mode == checkfor
Utility/Misc.hs view
@@ -15,11 +15,15 @@ import Data.Char import Data.List import Control.Applicative+import System.Exit #ifndef mingw32_HOST_OS import System.Posix.Process (getAnyProcessStatus) import Utility.Exception #endif +import Utility.FileSystemEncoding+import Utility.Monad+ {- A version of hgetContents that is not lazy. Ensures file is   - all read before it gets closed. -} hGetContentsStrict :: Handle -> IO String@@ -29,6 +33,13 @@ readFileStrict :: FilePath -> IO String readFileStrict = readFile >=> \s -> length s `seq` return s +{-  Reads a file strictly, and using the FileSystemEncofing, 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+ {- Like break, but the item matching the condition is not included  - in the second result list.  -@@ -136,3 +147,7 @@ #else reapZombies = return () #endif++exitBool :: Bool -> IO a+exitBool False = exitFailure+exitBool True = exitSuccess
+ Utility/QuickCheck.hs view
@@ -0,0 +1,48 @@+{- QuickCheck with additional instances+ -+ - Copyright 2012 Joey Hess <joey@kitenet.net>+ -+ - Licensed under the GNU GPL version 3 or higher.+ -}++{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Utility.QuickCheck+	( module X+	, module Utility.QuickCheck+	) where++import Test.QuickCheck as X+import Data.Time.Clock.POSIX+import System.Posix.Types+import qualified Data.Map as M+import Control.Applicative++instance (Arbitrary k, Arbitrary v, Eq k, Ord k) => Arbitrary (M.Map k v) where+	arbitrary = M.fromList <$> arbitrary++{- Times before the epoch are excluded. -}+instance Arbitrary POSIXTime where+	arbitrary = nonNegative arbitrarySizedIntegral++instance Arbitrary EpochTime where+	arbitrary = nonNegative arbitrarySizedIntegral++{- Pids are never negative, or 0. -}+instance Arbitrary ProcessID where+	arbitrary = arbitrarySizedBoundedIntegral `suchThat` (> 0)++{- Inodes are never negative. -}+instance Arbitrary FileID where+	arbitrary = nonNegative arbitrarySizedIntegral++{- File sizes are never negative. -}+instance Arbitrary FileOffset where+	arbitrary = nonNegative arbitrarySizedIntegral++nonNegative :: (Num a, Ord a) => Gen a -> Gen a+nonNegative g = g `suchThat` (>= 0)++positive :: (Num a, Ord a) => Gen a -> Gen a+positive g = g `suchThat` (> 0)
+ Utility/ThreadScheduler.hs view
@@ -0,0 +1,69 @@+{- thread scheduling+ -+ - Copyright 2012, 2013 Joey Hess <joey@kitenet.net>+ - Copyright 2011 Bas van Dijk & Roel van Dijk+ -+ - Licensed under the GNU GPL version 3 or higher.+ -}++{-# LANGUAGE CPP #-}++module Utility.ThreadScheduler where++import Common++import Control.Concurrent+#ifndef mingw32_HOST_OS+import System.Posix.Signals+#ifndef __ANDROID__+import System.Posix.Terminal+#endif+#endif++newtype Seconds = Seconds { fromSeconds :: Int }+	deriving (Eq, Ord, Show)++type Microseconds = Integer++{- Runs an action repeatedly forever, sleeping at least the specified number+ - of seconds in between. -}+runEvery :: Seconds -> IO a -> IO a+runEvery n a = forever $ do+	threadDelaySeconds n+	a++threadDelaySeconds :: Seconds -> IO ()+threadDelaySeconds (Seconds n) = unboundDelay (fromIntegral n * oneSecond)++{- Like threadDelay, but not bounded by an Int.+ -+ - There is no guarantee that the thread will be rescheduled promptly when the+ - delay has expired, but the thread will never continue to run earlier than+ - specified.+ - + - Taken from the unbounded-delay package to avoid a dependency for 4 lines+ - of code.+ -}+unboundDelay :: Microseconds -> IO ()+unboundDelay time = do+	let maxWait = min time $ toInteger (maxBound :: Int)+	threadDelay $ fromInteger maxWait+	when (maxWait /= time) $ unboundDelay (time - maxWait)++{- Pauses the main thread, letting children run until program termination. -}+waitForTermination :: IO ()+waitForTermination = do+	lock <- newEmptyMVar+#ifndef mingw32_HOST_OS+	let check sig = void $+		installHandler sig (CatchOnce $ putMVar lock ()) Nothing+	check softwareTermination+#ifndef __ANDROID__+	whenM (queryTerminal stdInput) $+		check keyboardSignal+#endif+#endif+	takeMVar lock++oneSecond :: Microseconds+oneSecond = 1000000
+ debian/changelog view
@@ -0,0 +1,22 @@+git-repair (1.20131122) unstable; urgency=low++  * Added test mode, which can be used to randomly corrupt test +    repositories, in reproducible ways, which allows easy+    corruption-driven-development.+  * Improve repair code in the case where the index file is corrupt,+    and this hides other problems.+  * Write a dummy .git/HEAD if the file is missing or corrupt, as+    git otherwise will not treat the repository as a git repo.+  * Improve fsck code to find badly corrupted objects that crash git fsck+    before it can complain about them.+  * Fixed crashes on bad file encodings.+  * Can now run 10000 tests (git-repair --test -n 10000 --force)+    with 0 failures.++ -- Joey Hess <joeyh@debian.org>  Fri, 22 Nov 2013 11:16:03 -0400++git-repair (1.20131118) unstable; urgency=low++  * First release++ -- Joey Hess <joeyh@debian.org>  Mon, 18 Nov 2013 13:38:12 -0400
+ debian/compat view
@@ -0,0 +1,1 @@+9
+ debian/control view
@@ -0,0 +1,29 @@+Source: git-repair+Section: utils+Priority: optional+Build-Depends: +	debhelper (>= 9),+	ghc,+	git,+	libghc-missingh-dev,+	libghc-hslogger-dev,+	libghc-network-dev,+	libghc-ifelse-dev,+	libghc-extensible-exceptions-dev,+	libghc-unix-compat-dev,+	libghc-utf8-string-dev,+	libghc-async-dev,+	libghc-optparse-applicative-dev+Maintainer: Joey Hess <joeyh@debian.org>+Standards-Version: 3.9.4+Vcs-Git: git://git-repair.branchable.com/+Homepage: http://git-repair.branchable.com/++Package: git-repair+Architecture: any+Section: utils+Depends: ${misc:Depends}, ${shlibs:Depends}, git, rsync+Description: repair various forms of damage to git repositorie+ git-repair can repair various forms of damage to git repositories.+ .+ It is a complement to git fsck, which finds problems, but does not fix them.
+ debian/copyright view
@@ -0,0 +1,9 @@+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/+Source: native package++Files: *+Copyright: © 2013 Joey Hess <joey@kitenet.net>+License: GPL-3++ The full text of version 3 of the GPL is distributed as doc/GPL in+ this package's source, or in /usr/share/common-licenses/GPL-3 on+ Debian systems.
+ debian/manpages view
@@ -0,0 +1,1 @@+git-repair.1
+ debian/rules view
@@ -0,0 +1,10 @@+#!/usr/bin/make -f++# Avoid using cabal, as it writes to $HOME+export CABAL=./Setup++# Do use the changelog's version number, rather than making one up.+export RELEASE_BUILD=1++%:+	dh $@
doc/index.mdwn view
@@ -5,7 +5,8 @@  ## download -	git clone git://git-repair.branchable.com/ git-repair+* `git clone git://git-repair.branchable.com/ git-repair`+* from [Hackage](http://hackage.haskell.org/package/git-repair)  ## install 
git-repair.cabal view
@@ -1,5 +1,5 @@ Name: git-repair-Version: 1.20131118+Version: 1.20131122 Cabal-Version: >= 1.6 License: GPL Maintainer: Joey Hess <joey@kitenet.net>@@ -8,19 +8,22 @@ Copyright: 2013 Joey Hess License-File: GPL Build-Type: Custom-Homepage: http://git-reair.branchable.com/+Homepage: http://git-repair.branchable.com/ Category: Utility Synopsis: repairs a damanged git repisitory Description:  git-repair can repair various forms of damage to git repositories.+ .+ It is a complement to git fsck, which finds problems, but does not fix+ them.  Executable git-repair   Main-Is: git-repair.hs-  GHC-Options: -Wall+  GHC-Options: -Wall -threaded   Build-Depends: MissingH, hslogger, directory, filepath, containers, mtl,    network, extensible-exceptions, unix-compat, bytestring,-   base >= 4.5, base < 5, IfElse, pretty-show, text, process,-   utf8-string, async+   base >= 4.5, base < 5, IfElse, text, process, time, QuickCheck,+   utf8-string, async, optparse-applicative, SHA    if (! os(windows))     Build-Depends: unix
git-repair.hs view
@@ -5,34 +5,115 @@  - Licensed under the GNU GPL version 3 or higher.  -} -import System.Environment-import Data.Tuple.Utils+import Options.Applicative  import Common import qualified Git.CurrentRepo import qualified Git.Repair import qualified Git.Config+import qualified Git.Construct+import qualified Git.Destroyer+import qualified Git.Fsck+import Utility.Tmp -header :: String-header = "Usage: git-repair"+data Settings = Settings+	{ forced :: Bool+	, testMode :: Bool+	, retryTestMode :: Bool+	, numTests :: Int+	} -usage :: a-usage = error $ "bad parameters\n\n" ++ header+parseSettings :: Parser Settings+parseSettings = Settings+	<$> switch+		( long "force"+		<> help "Force repair, even if data is lost"+		)+	<*> switch+		( long "test"+		<> help "Clone local repo, damage the clone, and test repair"+		)+	<*> switch+		( long "retry"+		<> help "Retry tests in git-repair-test.log"+		)+	<*> option+		( long "numtests"+		<> short 'n'+		<> metavar "N"+		<> help "Run N tests"+		<> value 1+		) -parseArgs :: IO Bool-parseArgs = do-	args <- getArgs-	return $ or $ map parse args+main :: IO ()+main = execParser opts >>= go   where-	parse "--force" = True-	parse _ = usage+  	opts = info (helper <*> parseSettings) desc+	desc = fullDesc+		<> header "git-repair - repair a damanged git repository" +	go settings+		| testMode settings = test settings+		| retryTestMode settings = retryTest settings+		| otherwise = repair settings -main :: IO ()-main = do-	forced <- parseArgs-	+repair :: Settings -> IO ()+repair settings = do 	g <- Git.Config.read =<< Git.CurrentRepo.get-	ifM (fst3 <$> Git.Repair.runRepair forced g)+	ifM (Git.Repair.successfulRepair <$> Git.Repair.runRepair (forced settings) g) 		( exitSuccess 		, exitFailure 		)++test :: Settings -> IO ()+test settings = do+	forM_ [1 .. numTests settings] $ \n -> do+		putStrLn $ "** Test " ++ show n ++ "/" ++ show (numTests settings)+		damage <- Git.Destroyer.generateDamage+		logDamage damage+		runTest settings damage+	allOk++retryTest :: Settings -> IO ()+retryTest settings = do+	l <- map Prelude.read . lines <$> readFile logFile+	forM_ l $ \damage -> +		runTest settings damage+	allOk++runTest :: Settings -> [Git.Destroyer.Damage] -> IO ()+runTest settings damage = withTmpDir "tmprepo" $ \tmpdir -> do+	let cloneloc = tmpdir </> "clone"+	cloned <- boolSystem "git"+		[ Param "clone"+		, Param "--no-hardlinks"+		, File "."+		, File cloneloc+		]+	unless cloned $+		error $ "failed to clone this repo"+	g <- Git.Config.read =<< Git.Construct.fromPath cloneloc+	Git.Destroyer.applyDamage damage g+	repairstatus <- catchMaybeIO $ Git.Repair.successfulRepair+		<$> Git.Repair.runRepair (forced settings) g+	case repairstatus of+		Just True -> testResult repairstatus +			. Just . not . Git.Fsck.foundBroken+			=<< Git.Fsck.findBroken False g+		_ -> testResult repairstatus Nothing++-- Pass test result and fsck result+testResult :: (Maybe Bool) -> (Maybe Bool) -> IO ()+testResult (Just True) (Just True) = putStrLn "** repair succeeded"+testResult (Just True) (Just False) = error "** repair succeeded, but final fsck failed"+testResult _ _ = error "** repair failed"++allOk :: IO ()+allOk = do+	putStrLn ""+	putStrLn "All tests ok!"++logDamage :: [Git.Destroyer.Damage] -> IO ()+logDamage damage = appendFile logFile $ show damage ++ "\n"++logFile :: FilePath+logFile = "git-repair-test.log"