packages feed

darcsden-1.1.1: src/DarcsDen/Handler/Repository.hs

{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, CPP #-}
module DarcsDen.Handler.Repository where

import Control.Monad (when)
import Control.Monad.Trans
import Data.Char (toLower, isSpace)
import Data.List (groupBy, inits, isPrefixOf, nub, partition, sortBy, elemIndex)
import Data.List.Split (wordsBy)
import Data.Map ((!))
import Data.Maybe (catMaybes, fromJust, isJust, isNothing)
import Data.Ord (comparing)
import Data.Time (getCurrentTime)
import System.Directory (setCurrentDirectory, getCurrentDirectory, createDirectory)
import System.Exit(ExitCode(..))
import System.FilePath ((</>))
import System.FilePath.Posix (isValid)
import System.IO (openFile, hClose, hGetContents, IOMode(..))
import System.Process (readProcessWithExitCode)
import Database.CouchDB (doc)
import Snap.Core

import qualified Darcs.Repository as R
import qualified Darcs.Patch.V1.Core as P
import qualified Darcs.Patch.Prim.V1.Core as Pr

#ifdef DARCS28
import Darcs.Commands.Record (record)
import Darcs.Flags (DarcsFlag(All, PatchName, Author, LookForAdds))
#else
import Darcs.UI.Commands.Optimize (doOptimizeHTTP)
import Darcs.UI.Commands.Record (record)
import Darcs.UI.Flags (DarcsFlag(All, PatchName, Author, LookForAdds))
#endif

import DarcsDen.Darcs (sanityDarcs)
import DarcsDen.Handler.Repository.Util
import DarcsDen.Handler.Repository.Browse
import DarcsDen.Handler.Repository.Changes
import DarcsDen.Handler.Repository.Forks
import DarcsDen.Settings
import DarcsDen.State.Comment
import DarcsDen.State.Issue
import DarcsDen.State.Repository
import DarcsDen.State.Session
import DarcsDen.State.User
import DarcsDen.State.Util
import DarcsDen.Util
import DarcsDen.Validate
import DarcsDen.WebUtils
import qualified DarcsDen.Pages.Repository as Page


initialize :: Page
initialize s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to create a repository." s
    redirectTo (baseUrl ++ "login")
initialize s = doPage (Page.init []) s

doInitialize :: Page
doInitialize s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to create a repository." s
    redirectTo (baseUrl ++ "login")
doInitialize s@(Session { sUser = Just n }) = validate
    [ iff
        (And (nonEmpty "name") $
            predicate "name" isSane
                "contain only alphanumeric characters, -, or _")
        (\(OK i) ->
            io "destination repository already exists" $
                fmap isNothing (getRepository (n, i ! "name")))

    -- , Or (isEmpty "bootstrap")
    --     (Predicate "bootstrap"
    --                ((`elem` ["http", "https"]) . takeWhile (/= ':'))
    --                "start with http:// or https://")

    , io "user is not valid" (fmap (/= Nothing) (getUser n))
    ]
    (\(OK r) -> do
        now <- liftIO getCurrentTime
        desc <- input "description" ""
        site <- input "website" ""
        private <- getParam "private"
        issuetracker <- getParam "issuetracker"

        let repo = Repository
                { rID = Nothing
                , rRev = Nothing
                , rName = r ! "name"
                , rOwner = n
                , rDescription = desc
                , rWebsite = site
                , rCreated = now
                , rForkOf = Nothing
                , rMembers = []
                , rIsPrivate = isJust private
                , rIssueTracker = isJust issuetracker
                , rIssueCount = 0
                , rBuiltPacks = Nothing
                }

        -- url <- input "bootstrap" ""
        -- if length url > 0
        --     then bootstrapRepository repo url
        --     else
        newRepository repo

        success "Repository created." s
        redirectTo $ baseUrl ++ n ++ "/" ++ (r ! "name"))
    (\(Invalid f) -> do
        is <- getInputs
        notify Warning s f
            >>= doPage (Page.init is))

explore :: Page
explore s = do
    page <- input "page" "1"

    let p = read page :: Int
        perpage = 500
        paginated rs =
            paginate perpage p $
                sortBy (comparing (map toLower . rName . fst)) rs

    rs <- fmap groupForks getRepositories

    let totalPages = ceiling ((fromIntegral (length rs) :: Double) / fromIntegral perpage)

    doPage (Page.explore (paginated rs) p totalPages) s
  where
    groupForks rs = foldr addFork (map (flip (,) []) roots) forks
      where
        (forks, roots) = partition (isJust . rForkOf) rs

    addFork f [] = [(f, [])]
    addFork f ((r@(Repository { rID = y }), fs):rs)
        | rForkOf f == y = ((r, (f:fs)) : rs)
        | otherwise = (r, fs) : addFork f rs

buildPacks :: User -> Repository -> Page
buildPacks _ r s = validate
    [ io "you do not own this repository" $
        return $ Just (rOwner r) == sUser s
    ]
    (\(OK _) -> (liftIO $ do
#ifdef DARCS28
       warn "Packs not supported by this darcs version" s
#else
       Right dr <- getRepo (repoDir (rOwner r) (rName r)) :: IO (Either String (R.Repository (P.Patch Pr.Prim) wR wU wT))
       od <- getCurrentDirectory
       setCurrentDirectory $ repoDir (rOwner r) (rName r)
       doOptimizeHTTP dr
       success "Packs enabled" s
       setCurrentDirectory od
       now <- getCurrentTime
       updateRepository r { rBuiltPacks = Just now}
#endif
     ) >> redirectTo (baseUrl ++ (rOwner r) ++ "/" ++ (rName r)))
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

browseRepo :: User -> Repository -> Page
browseRepo u r s = do
    Right dr <- liftIO $ getRepo (repoDir (rOwner r) (rName r)) :: Snap (Either String (R.Repository (P.Patch Pr.Prim) wR wU wT))

    member <-
        case sUser s of
            Just un ->
                return (un `elem` rMembers r)
            Nothing ->
                return False

    f <- filePath
    fs <- liftIO $ getFiles dr f
    bl <- liftIO $ getBlob dr f

    case (fs, bl) of
        (Nothing, Nothing) -> notFound
        (Just fs', _) -> do
            readme <- liftIO $ getReadme dr f

            let files =
                    map (\i -> i
                        { iPath = pathToFile (f ++ [iName i])
                        }) fs'

            doPage (Page.repo u r files (crumb f) readme member) s
        (_, Just big) | isTooLarge big ->
            doPage (Page.blob u r (crumb f) Nothing) s
        (_, Just md) | isMarkdown (last f) ->
            doPage (Page.blob u r (crumb f) (Just . Left . doMarkdown' . fromLBS $ md)) s
        (_, Just source) -> do
            source' <- liftIO $ return $! Right $! highlightBlob (last f) (strictLBS source)
            doPage
                (Page.blob u r
                    (crumb f)
                    (Just source'))
                s
  where
    filePath = do
        rq <- getRequest
        return (wordsBy (== '/') (fromBS $ rqPathInfo rq))

    crumb = map pathToRepoItem . tail . inits

    pathToRepoItem p =
        RepoItem
            { iName = last p
            , iPath = pathToFile p
            , iIsDirectory = True
            }

addFile :: User -> Repository -> Page
addFile u r s = do
   let member = case sUser s of
                   Just un -> (un `elem` rOwner r:rMembers r)
                   Nothing -> False
   if member
    then do
       f <- filePath
       doPage (Page.addFile u r (crumb f)) s
    else do
       fp <- fmap (fromBS.rqPathInfo) getRequest
       warn "You do not have permission to edit this repository" s
       redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r ++ "/browse/" ++ fp)
  where
    filePath = do
        rq <- getRequest
        return (wordsBy (== '/') (fromBS $ rqPathInfo rq))

    crumb = map pathToRepoItem . tail . inits

    pathToRepoItem p =
        RepoItem
            { iName = last p
            , iPath = pathToFile p
            , iIsDirectory = True
            }

addFilePost :: User -> Repository -> Page
addFilePost _ r s = validate [
      nonEmpty "message",
      If (And (predicate "filepath" (const $ True) "") (nonEmpty "filename"))
         (\(OK a) -> io "invalid filename" $ return $ isValid ((a!"filepath") </> (a!"filename")))
   ]
   (\(OK a) -> do
   let member = case sUser s of
                   Just un -> (un `elem` rOwner r:rMembers r)
                   Nothing -> False
   if member
    then do
       t <- input "type" "file"
       let fp = a ! "filepath"
           fn = a ! "filename"
	   msg = a ! "message"
       let Just un = sUser s
       Just email <- fmap (fmap uEmail) (getUser un)
       liftIO $ do
          od <- getCurrentDirectory
          setCurrentDirectory $ repoDir (rOwner r) (rName r)
          if t == "directory"
           then createDirectory (fp </> fn)
   	   else writeFile (fp </> fn) ""
          sanityDarcs record [All, PatchName msg, Author email, LookForAdds] []
          setCurrentDirectory od
       success (t ++ " created") s
       redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r ++ "/browse/" ++ (fp </> fn))
    else do
       warn "You do not have permission to edit this repository" s
       redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r ++ "/browse/" ++ (a!"filepath")))
   (\(Invalid f) -> do
      notify Warning s f
      fp <- input "filepath" ""
      redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r ++ "/add-file/" ++ fp))

editFile :: User -> Repository -> Page
editFile u r s = do
   let member = case sUser s of
                   Just un -> (un `elem` rOwner r:rMembers r)
                   Nothing -> False
   if member
    then do
       fp <- fmap (fromBS.rqPathInfo) getRequest
       let path' = repoDir (rOwner r) (rName r) </> fp
       mime <- getMime path'
       contents <- liftIO $ readFile path'
       f <- filePath
       Just s' <- getSession (sID s)
       doPage (Page.editFile u r (crumb f) mime contents) s'
    else do
       fp <- fmap (fromBS.rqPathInfo) getRequest
       warn "You do not have permission to edit this file" s
       redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r ++ "/browse/" ++ fp)
  where
    filePath = do
        rq <- getRequest
        return (wordsBy (== '/') (fromBS $ rqPathInfo rq))

    crumb = map pathToRepoItem . tail . inits
    
    getMime p = do
        (exc, st_out, _) <- liftIO $ readProcessWithExitCode xdgMimePath ["query", "filetype", p] ""
        if exc == ExitSuccess
         then return $ filter (not.isSpace) $ removeCharset st_out
         else do
             warn "Unable to identify file type, defaulting to plain text" s
             return $ "text/plain"

    removeCharset xs = maybe xs (flip take xs) (elemIndex ';' xs)

    pathToRepoItem p =
        RepoItem
            { iName = last p
            , iPath = pathToFile p
            , iIsDirectory = True
            }

editFilePost :: User -> Repository -> Page
editFilePost _ r s = do
   let member = case sUser s of
                   Just un -> (un `elem` rOwner r:rMembers r)
                   Nothing -> False
   if member
    then do
       ntext <- input "editor" ""
       msg <- input "message" ""
       fp <- fmap (fromBS.rqPathInfo) getRequest
       let Just un = sUser s
       Just email <- fmap (fmap uEmail) (getUser un)
       liftIO $ do
          od <- getCurrentDirectory
          setCurrentDirectory $ repoDir (rOwner r) (rName r)
          handle <- openFile fp ReadMode
          has_carriage_returns <- fmap (or.map (=='\r')) (hGetContents handle)
          hClose handle
          let ntext' = if has_carriage_returns then ntext else filter (not.(=='\r')) ntext
          writeFile fp ntext'
          sanityDarcs record [All, PatchName msg, Author email] [fp]
          setCurrentDirectory od
       success "File edited." s
       redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r ++ "/browse/" ++ fp)
    else do
       fp <- fmap (fromBS.rqPathInfo) getRequest
       warn "You do not have permission to edit this file" s
       redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r ++ "/browse/" ++ fp)

repoChanges :: User -> Repository -> Page
repoChanges u r s = do
    p <- input "page" "1"
    let page = read p
    (patches, totalPages) <- liftIO $ getChanges (repoDir (rOwner r) (rName r)) page

    doPage (Page.changes u r patches page totalPages) s

repoChangesAtom :: User -> Repository -> Page
repoChangesAtom u r s = do
    (patches, _) <- liftIO $ getChanges (repoDir (rOwner r) (rName r)) 1
    doAtomPage (Page.changesAtom u r patches) s

-- repoPatch1 :: User -> Repository -> Page
-- repoPatch1 u r s = do
--     p <- input "id" ""
--     when (null p) (errorPage "No patch ID specified.")

--     mpatch <- liftIO $ getPatch (repoDir (rOwner r) (rName r)) p
--     case mpatch of
--       Nothing -> notFound
--       Just patch ->
--         doPage
--           (Page.patch1 u r
--            (pPatch patch)
--            (summarize (pChanges patch))
--            (filter isModification (pChanges patch)))
--           s

repoPatch :: User -> Repository -> Page
repoPatch u r s = do
    p <- input "id" ""
    when (null p) (errorPage "No patch ID specified.")
    doPage (Page.patch u r p) s

renderedpatch :: User -> Repository -> Page
renderedpatch u r s = do
  p <- input "id" ""
  when (null p) (errorPage "No patch ID specified.")
  mpatch <- liftIO $ getPatch (repoDir (rOwner r) (rName r)) p
  case mpatch of
    Nothing -> notFound
    Just patch -> -- doPage (Page.renderedpatchHtml1 u r patch) s
        doPage
          (Page.renderedpatchHtml u r
           (pPatch patch)
           (summarize (pChanges patch))
           (filter isModification (pChanges patch)))
          s

editRepo :: User -> Repository -> Page
editRepo u r s = validate
    [ io "you do not own this repository" $
        return $ Just (rOwner r) == sUser s
    ]
    (\(OK _) -> do
        ms <- mapM getUser (rMembers r)

        let members = map fromJust . filter (/= Nothing) $ ms

        doPage (Page.edit u r members []) s)
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

doEditRepo :: User -> Repository -> Page
doEditRepo _ r s = validate
    [ io "you do not own this repository" $
        return $ Just (rOwner r) == sUser s

    , nonEmpty "name"

    , predicate "name" isSane
        "contain only alphanumeric characters, -, or _"
    ]
    (\(OK i) -> do
        removed <- removeMembers r (rMembers r)

        toAdd <- input "add-members" ""
        added <- addMembers removed (map strip . wordsBy (== ',') $ toAdd)
        new <- rename added (i ! "name")

        desc <- input "description" (rDescription r)
        site <- input "website" (rWebsite r)
        private <- getParam "private"
        issuetracker <- getParam "issuetracker"
        updateRepository
            new { rDescription = desc
                , rWebsite = site
                , rIsPrivate = isJust private
                , rIssueTracker = isJust issuetracker
                }

        success "Repository updated." s

        redirectTo (baseUrl ++ (rOwner r) ++ "/" ++ (i ! "name") ++ "/edit"))
    (\(Invalid f) -> do
        notify Warning s f
        redirectTo (baseUrl ++ (rOwner r) ++ "/" ++ (rName r) ++ "/edit"))
  where
    rename r' n
        = if rName r' /= n
             then do
                 res <- renameRepository n r'
                 case res of
                     Nothing -> do
                         warn "There was an error renaming the repository." s
                         return r'
                     Just r'' -> return r''
             else return r'

    addMembers r' [] = return r'
    addMembers r' (m:ms) = do
        user <- getUser m
        case user of
             Just (User { uName = un }) -> do
                 done <- addMember r' un
                 case done of
                     Just r'' ->
                         addMembers r'' ms
                     Nothing -> do
                         warn ("There was an error adding member " ++ un ++ ".") s
                         return r'
             _ -> do
                 warn ("Could not add member " ++ m ++ "; user does not exist.") s
                 addMembers r' ms

    removeMembers r' [] = return r'
    removeMembers r' (m:ms) = do
        remove <- getParam (toBS $ "remove-" ++ show m)
        maybe (removeMembers r' ms) (\_ -> do
            removed <- removeMember r m
            flip maybe (flip removeMembers ms) (do
                user <- getUser m
                case user of
                     Just u' -> do
                         warn ("There was an error removing member " ++ uName u' ++ ".") s
                         removeMembers r' ms
                     Nothing -> do
                         warn failDunno s
                         removeMembers r' ms) removed) remove

    failDunno = unwords
        [ "There was an error removing a member that doesn't exist."
        , "So I can't tell you who it was. Way to go."
        ]


deleteRepo :: User -> Repository -> Page
deleteRepo u r s = validate
    [ io "you do not own this repository" $
        return $ Just (rOwner r) == sUser s
    ]
    (\(OK _) -> doPage (Page.delete u r) s)
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

doDeleteRepo :: User -> Repository -> Page
doDeleteRepo _ r s = validate
    [ io "you do not own this repository" $
        return $ Just (rOwner r) == sUser s
    ]
    (\(OK _) -> do
        destroyed <- destroyRepository r

        if destroyed
           then do
               success "Repository deleted." s
               redirectTo baseUrl
           else do
               warn "Repository deletion failed." s
               redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r))
    (\(Invalid f) -> do
        notify Warning s f
        redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r))

forkRepo :: User -> Repository -> Page
forkRepo _ _ s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to fork a repository." s
    redirectTo baseUrl
forkRepo u r s@(Session { sUser = Just n }) = validate
    [ io "destination repository already exists" $
        fmap isNothing (getOwnerRepository (n, rName r))
    ]
    (\(OK _) -> do
        forked <- forkRepository n (rName r) r
        success "Repository forked." s
        redirectTo (baseUrl ++ n ++ "/" ++ rName forked))
    (\(Invalid _) -> doPage (Page.fork u r (rName r)) s)

forkRepoAs :: User -> Repository -> Page
forkRepoAs _ _ s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to fork a repository." s
    redirectTo baseUrl
forkRepoAs u r s@(Session { sUser = Just n }) = validate
    [ iff (nonEmpty "name") $ \(OK i) ->
        io "destination repository already exists" $
            fmap isNothing (getRepository (n, i ! "name"))
    ]
    (\(OK i) -> do
        forked <- forkRepository n (i ! "name") r
        success "Repository forked." s
        redirectTo (baseUrl ++ n ++ "/" ++ rName forked))
    (\(Invalid _) -> do
        name <- input "name" (rName r)
        doPage (Page.fork u r name) s)

repoPatches :: User -> Repository -> Page
repoPatches u r s = do
    fs <- getRepositoryForks (fromJust $ rID r)
    upstream <- liftIO $ getUpstreamChanges r
    downstream <- liftIO $ mapM getDownstreamChanges fs

    ownPrivFs <- fmap (filter isPrivateFork) (getOwnerRepositories (uName u))
    ownPrivForks <- liftIO $ mapM getDownstreamChanges ownPrivFs

    let changes     = filter (canView (sUser s) . fRepo) $ catMaybes (upstream : downstream)
        privChanges = filter (canView (sUser s) . fRepo) $ catMaybes ownPrivForks

    doPage (Page.patches u r changes privChanges) s
  where
    isPrivateFork (Repository { rIsPrivate = True, rForkOf = f }) = f == rID r
    isPrivateFork _ = False

repoMerge :: User -> Repository -> Page
repoMerge _ r s = validate
    [ io "you do not own this repository" $
        return $ canMerge (sUser s) r
    ]
    (\(OK _) -> do
        is <- getInputs
        let ps = map (\(n, _) ->
                       let split = wordsBy (== ':') n
                       in (doc (split !! 1), split !! 2))
                     (filter (\(n, _) -> "merge:" `isPrefixOf` n) is)
            gps = groupBy (\a b -> fst a == fst b) ps
            groupedPatches = map (\r'@((k, _):_) -> (k, map snd r')) gps

        merge <- mapM (\(r', ps') -> do
                          Just f <- getRepositoryByID r'
                          liftIO $ mergePatches r f ps' s) groupedPatches

        when (and merge) (success "Patches merged!" s >> return ())

        redirectTo (baseUrl ++ rOwner r ++ "/" ++ rName r ++ "/patches"))
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

repoIssuesOpen :: User -> Repository -> Page
repoIssuesOpen u r s = do
    issues <- fmap (sortBy (flip $ comparing iUpdated)) $ getIssuesOpen r
    doPage (Page.issues u r "open issues" issues) s

repoIssuesClosed :: User -> Repository -> Page
repoIssuesClosed u r s = do
  issues <- fmap (sortBy (flip $ comparing iUpdated)) $ getIssuesClosed r
  doPage (Page.issues u r "closed issues" issues) s
  
repoIssuesAll :: User -> Repository -> Page
repoIssuesAll u r s = do
  issues <- fmap (sortBy (flip $ comparing iUpdated)) $ getIssuesAll r
  doPage (Page.issues u r "all issues" issues) s

repoIssuesTag :: User -> Repository -> Page
repoIssuesTag u r s = validate
    [ nonEmpty "tag"
    ]
    (\(OK is) -> do
        let tags = map (wordsBy (== '^')) $ wordsBy (== '~') (is ! "tag")

        issues <- orFind tags

        doPage
            (Page.issuesByTags u r
                (sortBy (flip $ comparing iUpdated) issues)
                tags)
            s)
    (\(Invalid f) -> do
        notify Warning s f
        redirectTo (repoURL r ++ "/issues"))
  where
    andFind :: [String] -> Snap [Issue]
    andFind (ft:ts) = do
        issues <- getIssuesByTag r ft
        return (foldr (\t i -> filter ((t `elem`) . iTags) i) issues ts)
    andFind _ = error "andFind: no tags"

    orFind :: [[String]] -> Snap [Issue]
    orFind as = do
        issues <- mapM andFind as
        return (nub (concat issues))

repoIssue :: User -> Repository -> Page
repoIssue u r s = validate
    [ numeric "number"
    ]
    (\(OK is) -> do
        mi <- getIssue (fromJust (rID r)) (read $ is ! "number")
        case mi of
            Just i -> do
                cs <- fmap (sortBy (comparing cUpdated)) $ getComments i
                doPage (Page.issue u r i cs) s
            Nothing -> notFound)
    (\(Invalid f) -> do
        notify Warning s f
        redirectTo (repoURL r))

newIssue :: User -> Repository -> Page
newIssue _ _ s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to create an issue." s
    redirectTo baseUrl
newIssue u r s =
    doPage (Page.newIssue u r) s

doNewIssue :: User -> Repository -> Page
doNewIssue _ _ s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to create an issue." s
    redirectTo baseUrl
doNewIssue _ r s@(Session { sUser = Just un }) = validate
    [ nonEmpty "summary"
    , Predicate "description" (const True) "be provided"
    , Predicate "tags" (const True) "be provided"
    ]
    (\(OK is) -> do
        now <- liftIO getCurrentTime
        i <- addIssue Issue
            { iID = Nothing
            , iRev = Nothing
            , iNumber = rIssueCount r + 1
            , iSummary = is ! "summary"
            , iOwner = un
            , iDescription = is ! "description"
            , iTags = map strip $ wordsBy (== ',') (is ! "tags")
            , iCreated = now
            , iUpdated = now
            , iIsClosed = False
            , iRepository = fromJust (rID r)
            }

        updateRepository r
            { rIssueCount = rIssueCount r + 1
            }

        redirectTo (issueURL r i))
    (\(Invalid f) -> do
        notify Warning s f
        redirectTo (repoURL r ++ "/new-issue"))

repoComment :: User -> Repository -> Page
repoComment _ _ s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to comment on an issue." s
    redirectTo baseUrl
repoComment _ r s@(Session { sUser = Just un }) = validate
    [ iff (numeric "number") $ \(OK is) -> io "issue does not exist" $
        fmap isJust (getIssue (fromJust (rID r)) (read $ is ! "number"))
    ]
    (\(OK is) -> do
        Just i <- getIssue (fromJust (rID r)) (read $ is ! "number")
        submit <- input "submit" ""
        summary <- input "summary" ""
        description <- input "description" ""
        c <- input "comment" ""
        ts <- fmap (map strip . wordsBy (== ',')) $ input "tags" ""

        let closed =
                case submit of
                    "and close" -> True
                    "and reopen" -> False
                    _ -> iIsClosed i

            issueChanged = or
                [ iSummary i /= summary
                , iDescription i /= description
                , not (null (diffTags (iTags i) ts))
                , iIsClosed i /= closed
                ]

            diffTags ots nts = concat
                [ map AddTag (filter (`notElem` ots) nts)
                , map RemoveTag (filter (`notElem` nts) ots)
                ]

            changes = concat
                [ if iSummary i /= summary
                    then [Summary (iSummary i) summary]
                    else []
                , if iDescription i /= description
                    then [Description (iDescription i) description]
                    else []
                , if iIsClosed i /= closed then [Closed closed] else []
                , if iTags i /= ts then diffTags (iTags i) ts else []
                ]

        if not issueChanged && null c
            then do
                warn "no changes and no comment; do something!" s
                redirectTo (issueURL r i)
            else do

        now <- liftIO getCurrentTime
        addComment Comment
            { cID = Nothing
            , cRev = Nothing
            , cBody = c
            , cChanges = changes
            , cAuthor = un
            , cIssue = fromJust (iID i)
            , cCreated = now
            , cUpdated = now
            }

        mni <-
            if issueChanged
                then updateIssue i
                    { iSummary = summary
                    , iDescription = description
                    , iTags = ts
                    , iIsClosed = closed
                    , iUpdated = now
                    }
                else return (Just i)

        case mni of
            Nothing -> do
                warn "issue could not be updated" s
                redirectTo (issueURL r i)
            Just ni -> do
                if not (null c)
                    then success "comment added" s
                    else success "issue updated" s

                redirectTo (issueURL r ni))
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)