packages feed

darcs-2.3.0: src/Darcs/Commands/ShowAuthors.lhs

%  Copyright (C) 2004-2009 David Roundy, Eric Kow, Simon Michael
%
%  This program is free software; you can redistribute it and/or modify
%  it under the terms of the GNU General Public License as published by
%  the Free Software Foundation; either version 2, or (at your option)
%  any later version.
%
%  This program is distributed in the hope that it will be useful,
%  but WITHOUT ANY WARRANTY; without even the implied warranty of
%  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%  GNU General Public License for more details.
%
%  You should have received a copy of the GNU General Public License
%  along with this program; see the file COPYING.  If not, write to
%  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
%  Boston, MA 02110-1301, USA.

\darcsCommand{show authors}
\begin{code}
{-# OPTIONS_GHC -cpp #-}
module Darcs.Commands.ShowAuthors ( show_authors ) where

import Control.Arrow ((&&&), (***))
import Data.List ( sort, sortBy, group, groupBy, isInfixOf, isPrefixOf )
import Data.Ord (comparing)
import Data.Char ( toLower, isSpace )
import Text.Regex ( Regex, mkRegexWithOpts, matchRegex )

import Darcs.Arguments ( DarcsFlag(..), working_repo_dir )
import Darcs.Commands ( DarcsCommand(..), nodefaults )
import Darcs.External ( viewDoc )
import Darcs.Hopefully ( info )
import Darcs.Repository ( amInRepository, read_repo, withRepository, ($-) )
import Darcs.Patch.Info ( pi_author )
import Darcs.Ordered ( mapRL, concatRL )
import Printer ( text )
import Data.Function (on)

show_authors_description :: String
show_authors_description = "List authors by patch count."

show_authors_help :: String
show_authors_help =
 "The `darcs show authors' command lists the authors of the current\n" ++
 "repository, sorted by the number of patches contributed.  With the\n" ++
 "--verbose option, this command simply lists the author of each patch\n" ++
 "(without aggregation or sorting).\n" ++
 "\n" ++
 "An author's name or email address may change over time.  To tell Darcs\n" ++
 "when multiple author strings refer to the same individual, create an\n" ++
 "`.authorspellings' file in the root of the working tree.  Each line in\n" ++
 "this file begins with an author's canonical name and address, and may\n" ++
 "be followed by a comma and zero or more extended regular expressions,\n" ++
 "separated by commas.  Blank lines and lines beginning with two\n" ++
 "hyphens, are ignored.\n" ++
 "\n" ++
 "Any patch with an author string that matches the canonical address or\n" ++
 "any of the associated regexps is considered to be the work of that\n" ++
 "author.  All matching is case-insensitive and partial (it can match a\n" ++
 "substring).\n" ++
 "\n" ++
 "Currently this canonicalization step is done only in `darcs show\n" ++
 "authors'.  Other commands, such as `darcs changes' use author strings\n" ++
 "verbatim.\n" ++
 "\n" ++
 "An example .authorspelling file is:\n" ++
 "\n" ++
 "    -- This is a comment.\n" ++
 "    Fred Nurk <fred@example.com>\n" ++
 "    John Snagge <snagge@bbc.co.uk>, John, snagge@, js@(si|mit).edu\n"

show_authors :: DarcsCommand
show_authors = DarcsCommand {
  command_name = "authors",
  command_help = show_authors_help,
  command_description = show_authors_description,
  command_extra_args = 0,
  command_extra_arg_help = [],
  command_command = authors_cmd,
  command_prereq = amInRepository,
  command_get_arg_possibilities = return [],
  command_argdefaults = nodefaults,
  command_advanced_options = [],
  command_basic_options = [working_repo_dir] }

authors_cmd :: [DarcsFlag] -> [String] -> IO ()
authors_cmd opts _ = withRepository opts $- \repository -> do
  patches <- read_repo repository
  spellings <- compiled_author_spellings
  let authors = mapRL (pi_author . info) $ concatRL patches
  viewDoc $ text $ unlines $
   if Verbose `elem` opts
    then authors
    else -- A list of the form ["<count> <canonical name>"]...

      -- Turn the final result into a list of strings.
      map (\ (count, name) -> show count ++ "\t" ++ name) $
      -- Sort by descending patch count.
      reverse $ sortBy (comparing fst) $
      -- Combine duplicates from a list [(count, canonized name)]
      -- with duplicates canonized names (see next comment).
      map ((sum *** head) . unzip) $
      groupBy ((==) `on` snd) $
      sortBy  (comparing snd) $
      -- Because it would take a long time to canonize "foo" into
      -- "foo <foo@bar.baz>" once per patch, the code below
      -- generates a list [(count, canonized name)].
      map (length &&& (canonize_author spellings . head)) $
      group $ sort authors

canonize_author :: [(String,[Regex])] -> String -> String
canonize_author [] a = a
canonize_author spellings a = safehead a $ canonicalsfor a
    where
      safehead x xs = if null xs then x else head xs
      canonicalsfor s = map fst $ filter (ismatch s) spellings
      ismatch s (canonical,regexps) =
          (not (null email) && (s `contains` email)) || (any (s `contains_regex`) regexps)
          where email = takeWhile (/= '>') $ drop 1 $ dropWhile (/= '<') canonical

contains :: String -> String -> Bool
a `contains` b = lower b `isInfixOf` (lower a) where lower = map toLower

contains_regex :: String -> Regex -> Bool
a `contains_regex` r = case matchRegex r a of
                         Just _ -> True
                         _ -> False

compiled_author_spellings :: IO [(String,[Regex])]
compiled_author_spellings = do
  ss <- author_spellings_from_file
  return $ map compile $ ss
    where
      compile [] = error "each author spelling should contain at least the canonical form"
      compile (canonical:pats) = (canonical, map mkregex pats)
      mkregex pat = mkRegexWithOpts pat True False

authorspellingsfile :: FilePath
authorspellingsfile = ".authorspellings"

author_spellings_from_file :: IO [[String]]
author_spellings_from_file = do
  s <- readFile -- ratify readFile: never unlinked from within darcs
         authorspellingsfile `catch` (\_ -> return "")
  let noncomments = filter (not . ("--" `isPrefixOf`)) $
                    filter (not . null) $ map strip $ lines s
  return $ map (map strip . split_on ',') noncomments

split_on :: Eq a => a -> [a] -> [[a]]
split_on e l =
    case dropWhile (e==) l of
      [] -> []
      l' -> first : split_on e rest
        where
          (first,rest) = break (e==) l'

strip :: String -> String
strip = dropWhile isSpace . reverse . dropWhile isSpace . reverse

\end{code}