github-backup-1.20120126: github-backup.hs
{- github-backup
-
- Copyright 2012 Joey Hess <joey@kitenet.net>
-
- Licensed under the GNU GPL version 3 or higher.
-}
module Main where
import System.Environment
import Control.Exception (bracket)
import System.Posix.Directory (changeWorkingDirectory)
import Text.Show.Pretty
import qualified Github.Data.Definitions as Github
import qualified Github.Repos as Github
import qualified Github.Repos.Forks as Github
import qualified Github.PullRequests as Github
import qualified Github.Repos.Watching as Github
import qualified Github.Issues as Github
import qualified Github.Issues.Comments
import qualified Github.Issues.Milestones
import Common
import qualified Git
import qualified Git.Construct
import qualified Git.Config
import qualified Git.Types
import qualified Git.Command
import qualified Git.Ref
import qualified Git.Branch
import qualified Git.Queue
usage :: String
usage = "usage: github-backup [directory]"
getLocalRepo :: [String] -> IO Git.Repo
getLocalRepo [] = Git.Construct.fromCwd
getLocalRepo (d:[]) = Git.Construct.fromPath d
getLocalRepo _ = error usage
-- A github user and repo.
data GithubUserRepo = GithubUserRepo String String
instance Show (GithubUserRepo) where
show (GithubUserRepo user remote) = "git://github.com/" ++ user ++ "/" ++ remote ++ ".git"
toGithubUserRepo :: Github.Repo -> GithubUserRepo
toGithubUserRepo r = GithubUserRepo
(Github.githubUserLogin $ Github.repoOwner r)
(Github.repoName r)
remoteFor :: Github.Repo -> String
remoteFor r = "github_" ++
(Github.githubUserLogin $ Github.repoOwner r) ++ "_" ++
(Github.repoName r)
showRepo :: Github.Repo -> String
showRepo = show . toGithubUserRepo
runRepoWith :: (String -> String -> b -> IO (Either Github.Error v)) -> b -> Github.Repo -> IO (Maybe v)
runRepoWith a b = run (\user repo -> a user repo b) . toGithubUserRepo
runRepo :: (String -> String -> IO (Either Github.Error v)) -> Github.Repo -> IO (Maybe v)
runRepo a = run a . toGithubUserRepo
run :: (String -> String -> IO (Either Github.Error v)) -> GithubUserRepo -> IO (Maybe v)
run a r@(GithubUserRepo user repo) = handle =<< a user repo
where
handle (Right v) = return $ Just v
handle (Left e) = do
hPutStrLn stderr $ "problem accessing API for " ++ show r ++ ": " ++ show e
return Nothing
{- Finds already configured remotes that use github.
- Fetches from them because it would be surprising if a github backup
- didn't do so, and this is the only place the remote names are available. -}
gitHubRemotes :: Git.Repo -> IO [Github.Repo]
gitHubRemotes r = do
let (repos, remotes) = unzip $ gitHubUserRepos r
when (null remotes) $ error "no github remotes found"
forM_ repos $ \repo ->
Git.Command.runBool "fetch" [Param $ fromJust $ Git.Types.remoteName repo] r
catMaybes <$> mapM (run Github.userRepo) remotes
gitHubUserRepos :: Git.Repo -> [(Git.Repo, GithubUserRepo)]
gitHubUserRepos = mapMaybe check . Git.Types.remotes
where
check r@Git.Repo { Git.Types.location = Git.Types.Url u } =
headMaybe $ mapMaybe (checkurl r $ show u) gitHubUrlPrefixes
check _ = Nothing
checkurl r u prefix
| prefix `isPrefixOf` u && length bits == 2 =
Just $ (r,
GithubUserRepo (bits !! 0)
(dropdotgit $ bits !! 1))
| otherwise = Nothing
where
rest = drop (length prefix) u
bits = split "/" rest
dropdotgit s
| ".git" `isSuffixOf` s = take (length s - length ".git") s
| otherwise = s
{- All known prefixes for urls to github repos. -}
gitHubUrlPrefixes :: [String]
gitHubUrlPrefixes =
[ "git@github.com:"
, "git://github.com/"
, "https://github.com/"
, "http://github.com/"
, "ssh://git@github.com/~/"
]
{- Recursively look for forks, and forks of forks, of the repos.
- Note that this guards against cycles in the fork tree, although
- presumably that should never happen. -}
findForks :: [Github.Repo] -> IO [Github.Repo]
findForks = findForks' []
findForks' :: [Github.Repo] -> [Github.Repo] -> IO [Github.Repo]
findForks' _ [] = return []
findForks' done rs = do
forks <- filter new . concat . catMaybes <$>
mapM (runRepo Github.forksFor) rs
forks' <- findForks' (done++rs) forks
return $ nub $ forks ++ forks'
where
new r = not $ r `elem` rs || r `elem` done
{- Adds new forks, fetching from them. -}
addForks :: Git.Repo -> [Github.Repo] -> IO ()
addForks _ [] = putStrLn "no new forks"
addForks r forks = do
putStrLn $ "new forks: " ++ unwords (map showRepo forks)
forM_ forks $ \fork -> do
Git.Command.runBool "remote"
[ Param "add"
, Param "-f"
, Param $ remoteFor fork
, Param $ Github.repoGitUrl fork
] r
onGithubBranch :: Git.Repo -> IO () -> IO ()
onGithubBranch r a = bracket prep cleanup (const a)
where
prep = do
oldbranch <- Git.Branch.current r
exists <- Git.Ref.matching (Git.Ref $ "refs/heads/" ++ branchname) r
if null exists
then Git.Command.run "checkout"
[Param "--orphan", Param branchname] r
else Git.Command.run "checkout"
[Param branchname] r
return oldbranch
cleanup Nothing = return ()
cleanup (Just oldbranch)
| name == branchname = return ()
| otherwise = Git.Command.run "checkout" [Param name] r
where
name = show $ Git.Ref.base oldbranch
branchname = "github"
commitFiles :: Git.Repo -> [FilePath] -> IO ()
commitFiles _ [] = return ()
commitFiles r files = do
mass "add" [Param "-f"]
mass "commit" [Param "-m", Param "github-backup"]
where
mass subcommand params = do
let q = Git.Queue.add Git.Queue.new subcommand params files
_ <- Git.Queue.flush q r
return ()
saveMetaData :: Show a => Github.Repo -> String -> a -> IO FilePath
saveMetaData repo file val = do
let file' = remoteFor repo </> file
createDirectoryIfMissing True (parentDir file')
writeFile file' (ppShow val)
return file'
gatherMetaData :: Github.Repo -> IO [FilePath]
gatherMetaData repo = do
putStrLn $ "gathering metadata for " ++ showRepo repo
concat <$> mapM (\a -> a repo)
[ pullRequests
, watchers
, issues
, milestones
]
pullRequests :: Github.Repo -> IO [FilePath]
pullRequests repo = runRepo Github.pullRequestsFor repo >>= collect
where
collect Nothing = return []
collect (Just rs) = forM rs $ \r -> do
let n = Github.pullRequestNumber r
details <- runRepoWith Github.pullRequest n repo
saveMetaData repo ("pullrequest" </> show n) details
watchers :: Github.Repo -> IO [FilePath]
watchers repo = runRepo Github.watchersFor repo >>= collect
where
collect Nothing = return []
collect (Just ws) = do
f <- saveMetaData repo "watchers" $ sort ws
return [f]
issues :: Github.Repo -> IO [FilePath]
issues repo = runRepoWith Github.issuesForRepo [] repo >>= collect
where
collect Nothing = return []
collect (Just is) = concat <$> forM is get
get i = do
let n = Github.issueNumber i
f <- saveMetaData repo ("issue" </> show n) i
fs <- issueComments n repo
return $ f:fs
issueComments :: Int -> Github.Repo -> IO [FilePath]
issueComments n repo = runRepoWith Github.Issues.Comments.comments n repo >>= collect
where
collect Nothing = return []
collect (Just cs) = forM cs $ \c -> do
let i = Github.issueCommentId c
saveMetaData repo ("issue" </> show n ++ "_comment" </> show i) c
milestones :: Github.Repo -> IO [FilePath]
milestones repo = runRepo Github.Issues.Milestones.milestones repo >>= collect
where
collect Nothing = return []
collect (Just ms) = forM ms $ \m -> do
let n = Github.milestoneNumber m
saveMetaData repo ("milestone" </> show n) m
main :: IO ()
main = do
r <- Git.Config.read =<< getLocalRepo =<< getArgs
changeWorkingDirectory $ Git.repoLocation r
remotes <- gitHubRemotes r
forks <- findForks remotes
addForks r forks
onGithubBranch r $
concat <$> forM (forks ++ remotes) gatherMetaData
>>= commitFiles r