Handle post-receive hook, publish a Push activity

This commit is contained in:
fr33domlover 2019-09-09 00:27:45 +00:00
parent 3c01f4136c
commit 68e8b094a0
22 changed files with 545 additions and 73 deletions

View file

@ -36,12 +36,14 @@ Person
about Text about Text
inbox InboxId inbox InboxId
outbox OutboxId outbox OutboxId
followers FollowerSetId
UniquePersonIdent ident UniquePersonIdent ident
UniquePersonLogin login UniquePersonLogin login
UniquePersonEmail email UniquePersonEmail email
UniquePersonInbox inbox UniquePersonInbox inbox
UniquePersonOutbox outbox UniquePersonOutbox outbox
UniquePersonFollowers followers
Outbox Outbox
@ -235,8 +237,12 @@ Repo
mainBranch Text mainBranch Text
collabUser RoleId Maybe collabUser RoleId Maybe
collabAnon RoleId Maybe collabAnon RoleId Maybe
inbox InboxId
followers FollowerSetId
UniqueRepo ident sharer UniqueRepo ident sharer
UniqueRepoInbox inbox
UniqueRepoFollowers followers
Workflow Workflow
sharer SharerId sharer SharerId

View file

@ -62,6 +62,7 @@
/s/#ShrIdent/notifications NotificationsR GET POST /s/#ShrIdent/notifications NotificationsR GET POST
/s/#ShrIdent/outbox SharerOutboxR GET POST /s/#ShrIdent/outbox SharerOutboxR GET POST
/s/#ShrIdent/outbox/#OutboxItemKeyHashid SharerOutboxItemR GET /s/#ShrIdent/outbox/#OutboxItemKeyHashid SharerOutboxItemR GET
/s/#ShrIdent/followers SharerFollowersR GET
/p PeopleR GET /p PeopleR GET
@ -84,6 +85,9 @@
/s/#ShrIdent/r ReposR GET POST /s/#ShrIdent/r ReposR GET POST
/s/#ShrIdent/r/!new RepoNewR GET /s/#ShrIdent/r/!new RepoNewR GET
/s/#ShrIdent/r/#RpIdent RepoR GET PUT DELETE POST /s/#ShrIdent/r/#RpIdent RepoR GET PUT DELETE POST
/s/#ShrIdent/r/#RpIdent/inbox RepoInboxR GET POST
/s/#ShrIdent/r/#RpIdent/team RepoTeamR GET
/s/#ShrIdent/r/#RpIdent/followers RepoFollowersR GET
/s/#ShrIdent/r/#RpIdent/edit RepoEditR GET /s/#ShrIdent/r/#RpIdent/edit RepoEditR GET
/s/#ShrIdent/r/#RpIdent/s/+Texts RepoSourceR GET /s/#ShrIdent/r/#RpIdent/s/+Texts RepoSourceR GET
/s/#ShrIdent/r/#RpIdent/c RepoHeadChangesR GET /s/#ShrIdent/r/#RpIdent/c RepoHeadChangesR GET

View file

@ -0,0 +1,10 @@
Inbox
FollowerSet
Repo
inbox InboxId
followers FollowerSetId
Person
followers FollowerSetId

View file

@ -16,6 +16,7 @@
module Vervis.API module Vervis.API
( createNoteC ( createNoteC
, offerTicketC , offerTicketC
, pushCommitsC
, getFollowersCollection , getFollowersCollection
) )
where where
@ -691,6 +692,87 @@ offerTicketC shrUser summary audience offer@(Offer ticket uTarget) = runExceptT
insert_ $ InboxItemLocal ibid obiid ibiid insert_ $ InboxItemLocal ibid obiid ibiid
return remotes return remotes
pushCommitsC
:: (Entity Person, Sharer)
-> Html
-> Push URIMode
-> ShrIdent
-> RpIdent
-> Handler (Either Text OutboxItemId)
pushCommitsC (eperson, sharer) summary push shrRepo rpRepo = runExceptT $ do
let dont = Authority "dont-do.any-forwarding" Nothing
(obiid, doc, remotesHttp) <- runDBExcept $ do
(obiid, doc) <- lift $ insertToOutbox
remoteRecips <- lift $ deliverLocal obiid
federation <- getsYesod $ appFederation . appSettings
unless (federation || null remoteRecips) $
throwE "Federation disabled but remote collection members found"
remotesHttp <- lift $ deliverRemoteDB' dont obiid [] remoteRecips
return (obiid, doc, remotesHttp)
lift $ forkWorker "pushCommitsC: async HTTP delivery" $ deliverRemoteHttp dont obiid doc remotesHttp
return obiid
where
insertToOutbox :: AppDB (OutboxItemId, Doc Activity URIMode)
insertToOutbox = do
host <- getsYesod siteInstanceHost
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let shrUser = sharerIdent sharer
aud = map encodeRouteHome
[ SharerFollowersR shrUser
, RepoTeamR shrRepo rpRepo
, RepoFollowersR shrRepo rpRepo
]
activity mluAct = Doc host Activity
{ activityId = mluAct
, activityActor = encodeRouteLocal $ SharerR shrUser
, activitySummary =
Just $ TextHtml $ TL.toStrict $ renderHtml summary
, activityAudience = Audience aud [] [] [] [] []
, activitySpecific = PushActivity push
}
now <- liftIO getCurrentTime
obiid <- insert OutboxItem
{ outboxItemOutbox = personOutbox $ entityVal eperson
, outboxItemActivity = persistJSONObjectFromDoc $ activity Nothing
, outboxItemPublished = now
}
obikhid <- encodeKeyHashid obiid
let luAct = encodeRouteLocal $ SharerOutboxItemR shrUser obikhid
doc = activity $ Just luAct
update obiid [OutboxItemActivity =. persistJSONObjectFromDoc doc]
return (obiid, doc)
deliverLocal
:: OutboxItemId
-> AppDB
[ ( (InstanceId, Host)
, NonEmpty (RemoteActorId, LocalURI, LocalURI, Maybe UTCTime)
)
]
deliverLocal obiid = do
let pidAuthor = entityKey eperson
(sidRepo, repo) <- do
sid <- getKeyBy404 $ UniqueSharer shrRepo
r <- getValBy404 $ UniqueRepo rpRepo sid
return (sid, r)
(pids, remotes) <- do
(repoPids, repoRemotes) <- getRepoTeam sidRepo
(pfsPids, pfsRemotes) <-
getFollowers $ personFollowers $ entityVal eperson
(rfsPids, rfsRemotes) <- getFollowers $ repoFollowers repo
return
( L.delete pidAuthor $ union repoPids $ union pfsPids rfsPids
, repoRemotes `unionRemotes` pfsRemotes `unionRemotes` rfsRemotes
)
ibiid <- insert $ InboxItem False
insert_ $ InboxItemLocal (repoInbox repo) obiid ibiid
for_ pids $ \ pid -> do
ibid <- personInbox <$> getJust pid
ibiid <- insert $ InboxItem True
insert_ $ InboxItemLocal ibid obiid ibiid
return remotes
getFollowersCollection getFollowersCollection
:: Route App -> AppDB FollowerSetId -> Handler TypedContent :: Route App -> AppDB FollowerSetId -> Handler TypedContent
getFollowersCollection here getFsid = do getFollowersCollection here getFsid = do
@ -725,4 +807,4 @@ getFollowersCollection here getFsid = do
map (encodeRouteHome . SharerR) locals ++ map (encodeRouteHome . SharerR) locals ++
map (uncurry ObjURI . bimap E.unValue E.unValue) remotes map (uncurry ObjURI . bimap E.unValue E.unValue) remotes
} }
provideHtmlAndAP followersAP $ redirect (here, [("prettyjson", "true")]) provideHtmlAndAP followersAP $ redirectToPrettyJSON here

View file

@ -24,6 +24,7 @@ module Vervis.ActivityPub
, getPersonOrGroupId , getPersonOrGroupId
, getTicketTeam , getTicketTeam
, getProjectTeam , getProjectTeam
, getRepoTeam
, getFollowers , getFollowers
, unionRemotes , unionRemotes
, insertMany' , insertMany'
@ -211,6 +212,8 @@ getTicketTeam sid = do
getProjectTeam = getTicketTeam getProjectTeam = getTicketTeam
getRepoTeam = getTicketTeam
getFollowers :: FollowerSetId -> AppDB ([PersonId], [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, Maybe UTCTime))]) getFollowers :: FollowerSetId -> AppDB ([PersonId], [((InstanceId, Host), NonEmpty (RemoteActorId, LocalURI, LocalURI, Maybe UTCTime))])
getFollowers fsid = do getFollowers fsid = do
local <- selectList [FollowTarget ==. fsid] [Asc FollowPerson] local <- selectList [FollowTarget ==. fsid] [Asc FollowPerson]

View file

@ -16,6 +16,7 @@
module Vervis.Federation module Vervis.Federation
( handleSharerInbox ( handleSharerInbox
, handleProjectInbox , handleProjectInbox
, handleRepoInbox
, fixRunningDeliveries , fixRunningDeliveries
, retryOutboxDelivery , retryOutboxDelivery
) )
@ -253,6 +254,28 @@ handleProjectInbox now shrRecip prjRecip auth body = do
projectOfferTicketF now shrRecip prjRecip remoteAuthor body offer projectOfferTicketF now shrRecip prjRecip remoteAuthor body offer
_ -> return "Unsupported activity type" _ -> return "Unsupported activity type"
handleRepoInbox
:: UTCTime
-> ShrIdent
-> RpIdent
-> ActivityAuthentication
-> ActivityBody
-> ExceptT Text Handler Text
handleRepoInbox now shrRecip rpRecip auth body = do
remoteAuthor <-
case auth of
ActivityAuthLocalPerson pid ->
throwE $
"Repo inbox got local forwarded activity by pid#" <>
T.pack (show $ fromSqlKey pid)
ActivityAuthLocalProject jid ->
throwE $
"Repo inbox got local forwarded activity by jid#" <>
T.pack (show $ fromSqlKey jid)
ActivityAuthRemote ra -> return ra
case activitySpecific $ actbActivity body of
_ -> return "Unsupported activity type"
fixRunningDeliveries :: (MonadIO m, MonadLogger m, IsSqlBackend backend) => ReaderT backend m () fixRunningDeliveries :: (MonadIO m, MonadLogger m, IsSqlBackend backend) => ReaderT backend m ()
fixRunningDeliveries = do fixRunningDeliveries = do
c <- updateWhereCount [UnlinkedDeliveryRunning ==. True] [UnlinkedDeliveryRunning =. False] c <- updateWhereCount [UnlinkedDeliveryRunning ==. True] [UnlinkedDeliveryRunning =. False]

View file

@ -320,13 +320,39 @@ verifyForwardedSig hAuthor luAuthor (Verification malgo keyid input signature) =
mkauth (Left pid) = ActivityAuthLocalPerson pid mkauth (Left pid) = ActivityAuthLocalPerson pid
mkauth (Right jid) = ActivityAuthLocalProject jid mkauth (Right jid) = ActivityAuthLocalProject jid
verifyContentTypeAP :: MonadHandler m => m ()
verifyContentTypeAP = do
result <- runExceptT verifyContentTypeAP_E
case result of
Left e -> invalidArgs ["Content type error: " <> e]
Right () -> return ()
verifyContentTypeAP_E :: MonadHandler m => ExceptT Text m ()
verifyContentTypeAP_E = do
ctypes <- lookupHeaders "Content-Type"
case ctypes of
[] -> throwE "Content-Type not specified"
[x] | x == typeAS -> return ()
| x == typeAS2 -> return ()
| otherwise ->
throwE $ "Not a recognized AP Content-Type: " <>
case decodeUtf8' x of
Left _ -> T.pack (show x)
Right t -> t
_ -> throwE "More than one Content-Type specified"
where
typeAS = "application/activity+json"
typeAS2 =
"application/ld+json; \
\profile=\"https://www.w3.org/ns/activitystreams\""
authenticateActivity authenticateActivity
:: UTCTime :: UTCTime
-- -> ExceptT Text Handler (Either PersonId ActivityDetail, BL.ByteString, Object, Activity) -- -> ExceptT Text Handler (Either PersonId ActivityDetail, BL.ByteString, Object, Activity)
-> ExceptT Text Handler (ActivityAuthentication, ActivityBody) -> ExceptT Text Handler (ActivityAuthentication, ActivityBody)
authenticateActivity now = do authenticateActivity now = do
(ra, wv, body) <- do (ra, wv, body) <- do
verifyContentType verifyContentTypeAP_E
proof <- withExceptT (T.pack . displayException) $ ExceptT $ do proof <- withExceptT (T.pack . displayException) $ ExceptT $ do
timeLimit <- getsYesod $ appHttpSigTimeLimit . appSettings timeLimit <- getsYesod $ appHttpSigTimeLimit . appSettings
let requires = [hRequestTarget, hHost, hDigest] let requires = [hRequestTarget, hHost, hDigest]
@ -371,23 +397,6 @@ authenticateActivity now = do
Just a -> return a Just a -> return a
return (auth, ActivityBody body raw activity) return (auth, ActivityBody body raw activity)
where where
verifyContentType = do
ctypes <- lookupHeaders "Content-Type"
case ctypes of
[] -> throwE "Content-Type not specified"
[x] | x == typeAS -> return ()
| x == typeAS2 -> return ()
| otherwise ->
throwE $ "Not a recognized AP Content-Type: " <>
case decodeUtf8' x of
Left _ -> T.pack (show x)
Right t -> t
_ -> throwE "More than one Content-Type specified"
where
typeAS = "application/activity+json"
typeAS2 =
"application/ld+json; \
\profile=\"https://www.w3.org/ns/activitystreams\""
verifyBodyDigest = do verifyBodyDigest = do
req <- waiRequest req <- waiRequest
let headers = W.requestHeaders req let headers = W.requestHeaders req

View file

@ -99,6 +99,8 @@ editRepoAForm sid (Entity rid repo) = Repo
) )
<*> aopt selectRole "User role" (Just $ repoCollabUser repo) <*> aopt selectRole "User role" (Just $ repoCollabUser repo)
<*> aopt selectRole "Guest role" (Just $ repoCollabAnon repo) <*> aopt selectRole "Guest role" (Just $ repoCollabAnon repo)
<*> pure (repoInbox repo)
<*> pure (repoFollowers repo)
where where
selectProject' = selectProjectForExisting (repoSharer repo) rid selectProject' = selectProjectForExisting (repoSharer repo) rid
selectRole = selectRole =

View file

@ -604,6 +604,7 @@ instance AccountDB AccountPersistDB' where
Right sid -> do Right sid -> do
ibid <- insert Inbox ibid <- insert Inbox
obid <- insert Outbox obid <- insert Outbox
fsid <- insert FollowerSet
let defTime = UTCTime (ModifiedJulianDay 0) 0 let defTime = UTCTime (ModifiedJulianDay 0) 0
person = Person person = Person
{ personIdent = sid { personIdent = sid
@ -618,6 +619,7 @@ instance AccountDB AccountPersistDB' where
, personAbout = "" , personAbout = ""
, personInbox = ibid , personInbox = ibid
, personOutbox = obid , personOutbox = obid
, personFollowers = fsid
} }
pid <- insert person pid <- insert person
return $ Right $ Entity pid person return $ Right $ Entity pid person
@ -738,6 +740,8 @@ instance YesodBreadcrumbs App where
SharerOutboxItemR shr hid -> ( "#" <> keyHashidText hid SharerOutboxItemR shr hid -> ( "#" <> keyHashidText hid
, Just $ SharerOutboxR shr , Just $ SharerOutboxR shr
) )
SharerFollowersR shr -> ("Followers", Just $ SharerR shr)
ActorKey1R -> ("Actor Key 1", Nothing) ActorKey1R -> ("Actor Key 1", Nothing)
ActorKey2R -> ("Actor Key 2", Nothing) ActorKey2R -> ("Actor Key 2", Nothing)

View file

@ -17,8 +17,10 @@ module Vervis.Handler.Inbox
( getInboxR ( getInboxR
, getSharerInboxR , getSharerInboxR
, getProjectInboxR , getProjectInboxR
, getRepoInboxR
, postSharerInboxR , postSharerInboxR
, postProjectInboxR , postProjectInboxR
, postRepoInboxR
, getPublishR , getPublishR
, getSharerOutboxR , getSharerOutboxR
, getSharerOutboxItemR , getSharerOutboxItemR
@ -283,6 +285,15 @@ getProjectInboxR shr prj = getInbox here getInboxId
j <- getValBy404 $ UniqueProject prj sid j <- getValBy404 $ UniqueProject prj sid
return $ projectInbox j return $ projectInbox j
getRepoInboxR :: ShrIdent -> RpIdent -> Handler TypedContent
getRepoInboxR shr rp = getInbox here getInboxId
where
here = RepoInboxR shr rp
getInboxId = do
sid <- getKeyBy404 $ UniqueSharer shr
r <- getValBy404 $ UniqueRepo rp sid
return $ repoInbox r
postSharerInboxR :: ShrIdent -> Handler () postSharerInboxR :: ShrIdent -> Handler ()
postSharerInboxR shrRecip = do postSharerInboxR shrRecip = do
federation <- getsYesod $ appFederation . appSettings federation <- getsYesod $ appFederation . appSettings
@ -326,6 +337,21 @@ postProjectInboxR shrRecip prjRecip = do
Left _ -> sendResponseStatus badRequest400 () Left _ -> sendResponseStatus badRequest400 ()
Right _ -> return () Right _ -> return ()
postRepoInboxR :: ShrIdent -> RpIdent -> Handler ()
postRepoInboxR shrRecip rpRecip = do
federation <- getsYesod $ appFederation . appSettings
unless federation badMethod
contentTypes <- lookupHeaders "Content-Type"
now <- liftIO getCurrentTime
result <- runExceptT $ do
(auth, body) <- authenticateActivity now
(actbObject body,) <$>
handleRepoInbox now shrRecip rpRecip auth body
recordActivity now result contentTypes
case result of
Left _ -> sendResponseStatus badRequest400 ()
Right _ -> return ()
{- {-
jsonField :: (FromJSON a, ToJSON a) => Field Handler a jsonField :: (FromJSON a, ToJSON a) => Field Handler a
jsonField = checkMMap fromTextarea toTextarea textareaField jsonField = checkMMap fromTextarea toTextarea textareaField

View file

@ -137,7 +137,7 @@ getPerson shr sharer person = do
, actorSummary = Nothing , actorSummary = Nothing
, actorInbox = encodeRouteLocal $ SharerInboxR shr , actorInbox = encodeRouteLocal $ SharerInboxR shr
, actorOutbox = Just $ encodeRouteLocal $ SharerOutboxR shr , actorOutbox = Just $ encodeRouteLocal $ SharerOutboxR shr
, actorFollowers = Nothing , actorFollowers = Just $ encodeRouteLocal $ SharerFollowersR shr
, actorPublicKeys = , actorPublicKeys =
[ Left $ encodeRouteLocal ActorKey1R [ Left $ encodeRouteLocal ActorKey1R
, Left $ encodeRouteLocal ActorKey2R , Left $ encodeRouteLocal ActorKey2R

View file

@ -34,14 +34,18 @@ module Vervis.Handler.Repo
, deleteRepoDevR , deleteRepoDevR
, postRepoDevR , postRepoDevR
, getDarcsDownloadR , getDarcsDownloadR
, getRepoTeamR
, getRepoFollowersR
, getHighlightStyleR , getHighlightStyleR
, postPostReceiveR , postPostReceiveR
) )
where where
import Control.Exception hiding (Handler)
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (logWarn) import Control.Monad.Logger (logWarn)
import Data.Bifunctor
import Data.Git.Graph import Data.Git.Graph
import Data.Git.Harder import Data.Git.Harder
import Data.Git.Named (RefName (..)) import Data.Git.Named (RefName (..))
@ -49,16 +53,16 @@ import Data.Git.Ref (toHex)
import Data.Git.Repository import Data.Git.Repository
import Data.Git.Storage (withRepo) import Data.Git.Storage (withRepo)
import Data.Git.Storage.Object (Object (..)) import Data.Git.Storage.Object (Object (..))
import Data.Git.Types (Blob (..), Commit (..), Person (..), entName) import Data.Git.Types (Blob (..), Person (..), entName)
import Data.Graph.Inductive.Graph (noNodes) import Data.Graph.Inductive.Graph (noNodes)
import Data.Graph.Inductive.Query.Topsort import Data.Graph.Inductive.Query.Topsort
import Data.List (inits) import Data.List (inits)
import Data.Text (Text, unpack) import Data.Text (Text, unpack)
import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import Data.Traversable (for) import Data.Traversable (for)
import Database.Esqueleto hiding (delete, (%)) import Database.Persist
import Database.Persist (delete) import Database.Persist.Sql
import Data.Hourglass (timeConvert) import Data.Hourglass (timeConvert)
import Formatting (sformat, stext, (%)) import Formatting (sformat, stext, (%))
import System.Directory import System.Directory
@ -73,45 +77,62 @@ import Yesod.Form.Functions (runFormPost)
import Yesod.Form.Types (FormResult (..)) import Yesod.Form.Types (FormResult (..))
import Yesod.Persist.Core (runDB, getBy404) import Yesod.Persist.Core (runDB, getBy404)
import qualified Data.ByteString.Lazy as BL (ByteString)
import qualified Data.CaseInsensitive as CI (foldedCase) import qualified Data.CaseInsensitive as CI (foldedCase)
import qualified Data.DList as D import qualified Data.DList as D
import qualified Data.Set as S (member) import qualified Data.Set as S (member)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With) import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With)
import qualified Database.Esqueleto as E
import Data.MediaType
import Web.ActivityPub hiding (Repo)
import Yesod.ActivityPub
import Yesod.FedURI
import Yesod.Hashids
import Yesod.MonadSite
import Yesod.RenderSource
import qualified Web.ActivityPub as AP
import Data.ByteString.Char8.Local (takeLine) import Data.ByteString.Char8.Local (takeLine)
import Data.Either.Local
import Data.Git.Local import Data.Git.Local
import Database.Persist.Local
import Text.FilePath.Local (breakExt) import Text.FilePath.Local (breakExt)
import Yesod.Persist.Local
import qualified Data.Git.Local as G (createRepo)
import qualified Darcs.Local.Repository as D (createRepo)
import Vervis.API
import Vervis.Form.Repo import Vervis.Form.Repo
import Vervis.Foundation import Vervis.Foundation
import Vervis.Handler.Repo.Darcs import Vervis.Handler.Repo.Darcs
import Vervis.Handler.Repo.Git import Vervis.Handler.Repo.Git
import Vervis.Path import Vervis.Path
import Data.MediaType
import Vervis.Model import Vervis.Model
import Vervis.Model.Ident import Vervis.Model.Ident
import Vervis.Model.Repo import Vervis.Model.Repo
import Vervis.Paginate import Vervis.Paginate
import Vervis.Readme import Vervis.Readme
import Yesod.RenderSource
import Vervis.Settings import Vervis.Settings
import Vervis.SourceTree import Vervis.SourceTree
import Vervis.Style import Vervis.Style
import Vervis.Widget.Repo import Vervis.Widget.Repo
import Vervis.Widget.Sharer import Vervis.Widget.Sharer
import qualified Darcs.Local.Repository as D (createRepo)
import qualified Data.ByteString.Lazy as BL (ByteString)
import qualified Data.Git.Local as G (createRepo)
import qualified Vervis.Formatting as F import qualified Vervis.Formatting as F
import qualified Vervis.Hook as H
getReposR :: ShrIdent -> Handler Html getReposR :: ShrIdent -> Handler Html
getReposR user = do getReposR user = do
repos <- runDB $ select $ from $ \ (sharer, repo) -> do repos <- runDB $ E.select $ E.from $ \ (sharer, repo) -> do
where_ $ E.where_ $
sharer ^. SharerIdent ==. val user &&. sharer E.^. SharerIdent E.==. E.val user E.&&.
sharer ^. SharerId ==. repo ^. RepoSharer sharer E.^. SharerId E.==. repo E.^. RepoSharer
orderBy [asc $ repo ^. RepoIdent] E.orderBy [E.asc $ repo E.^. RepoIdent]
return $ repo ^. RepoIdent return $ repo E.^. RepoIdent
defaultLayout $(widgetFile "repo/list") defaultLayout $(widgetFile "repo/list")
postReposR :: ShrIdent -> Handler Html postReposR :: ShrIdent -> Handler Html
@ -137,6 +158,8 @@ postReposR user = do
(rp2text $ nrpIdent nrp) (rp2text $ nrpIdent nrp)
pid <- requireAuthId pid <- requireAuthId
runDB $ do runDB $ do
ibid <- insert Inbox
fsid <- insert FollowerSet
let repo = Repo let repo = Repo
{ repoIdent = nrpIdent nrp { repoIdent = nrpIdent nrp
, repoSharer = sid , repoSharer = sid
@ -146,6 +169,8 @@ postReposR user = do
, repoMainBranch = "master" , repoMainBranch = "master"
, repoCollabUser = Nothing , repoCollabUser = Nothing
, repoCollabAnon = Nothing , repoCollabAnon = Nothing
, repoInbox = ibid
, repoFollowers = fsid
} }
rid <- insert repo rid <- insert repo
let collab = RepoCollab let collab = RepoCollab
@ -175,14 +200,30 @@ selectRepo shar repo = do
Entity _rid r <- getBy404 $ UniqueRepo repo sid Entity _rid r <- getBy404 $ UniqueRepo repo sid
return r return r
getRepoR :: ShrIdent -> RpIdent -> Handler Html getRepoR :: ShrIdent -> RpIdent -> Handler TypedContent
getRepoR shar repo = do getRepoR shr rp = do
repository <- runDB $ selectRepo shar repo repo <- runDB $ selectRepo shr rp
case repoVcs repository of encodeRouteLocal <- getEncodeRouteLocal
VCSDarcs -> getDarcsRepoSource repository shar repo [] encodeRouteHome <- getEncodeRouteHome
VCSGit -> let repoAP = AP.Repo
getGitRepoSource { AP.repoActor = Actor
repository shar repo (repoMainBranch repository) [] { actorId = encodeRouteLocal $ RepoR shr rp
, actorType = ActorTypeRepo
, actorUsername = Nothing
, actorName = Just $ rp2text rp
, actorSummary = repoDesc repo
, actorInbox = encodeRouteLocal $ RepoInboxR shr rp
, actorOutbox = Nothing
, actorFollowers =
Just $ encodeRouteLocal $ RepoFollowersR shr rp
, actorPublicKeys = []
}
, AP.repoTeam = encodeRouteLocal $ RepoTeamR shr rp
}
dir = case repoVcs repo of
VCSDarcs -> []
VCSGit -> [repoMainBranch repo]
provideHtmlAndAP repoAP $ redirect $ RepoSourceR shr rp dir
putRepoR :: ShrIdent -> RpIdent -> Handler Html putRepoR :: ShrIdent -> RpIdent -> Handler Html
putRepoR shr rp = do putRepoR shr rp = do
@ -293,15 +334,15 @@ getRepoDevsR shr rp = do
Entity s _ <- getBy404 $ UniqueSharer shr Entity s _ <- getBy404 $ UniqueSharer shr
Entity r _ <- getBy404 $ UniqueRepo rp s Entity r _ <- getBy404 $ UniqueRepo rp s
return r return r
select $ from $ \ (collab `InnerJoin` E.select $ E.from $ \ (collab `E.InnerJoin`
person `InnerJoin` person `E.InnerJoin`
sharer `LeftOuterJoin` sharer `E.LeftOuterJoin`
role) -> do role) -> do
on $ collab ^. RepoCollabRole ==. role ?. RoleId E.on $ collab E.^. RepoCollabRole E.==. role E.?. RoleId
on $ person ^. PersonIdent ==. sharer ^. SharerId E.on $ person E.^. PersonIdent E.==. sharer E.^. SharerId
on $ collab ^. RepoCollabPerson ==. person ^. PersonId E.on $ collab E.^. RepoCollabPerson E.==. person E.^. PersonId
where_ $ collab ^. RepoCollabRepo ==. val rid E.where_ $ collab E.^. RepoCollabRepo E.==. E.val rid
return (sharer, role ?. RoleIdent) return (sharer, role E.?. RoleIdent)
defaultLayout $(widgetFile "repo/collab/list") defaultLayout $(widgetFile "repo/collab/list")
postRepoDevsR :: ShrIdent -> RpIdent -> Handler Html postRepoDevsR :: ShrIdent -> RpIdent -> Handler Html
@ -377,6 +418,53 @@ postRepoDevR shr rp dev = do
Just "DELETE" -> deleteRepoDevR shr rp dev Just "DELETE" -> deleteRepoDevR shr rp dev
_ -> notFound _ -> notFound
getRepoTeamR :: ShrIdent -> RpIdent -> Handler TypedContent
getRepoTeamR shr rp = do
memberShrs <- runDB $ do
sid <- getKeyBy404 $ UniqueSharer shr
_rid <- getKeyBy404 $ UniqueRepo rp sid
id_ <-
requireEitherAlt
(getKeyBy $ UniquePersonIdent sid)
(getKeyBy $ UniqueGroup sid)
"Found sharer that is neither person nor group"
"Found sharer that is both person and group"
case id_ of
Left pid -> return [shr]
Right gid -> do
pids <-
map (groupMemberPerson . entityVal) <$>
selectList [GroupMemberGroup ==. gid] []
sids <-
map (personIdent . entityVal) <$>
selectList [PersonId <-. pids] []
map (sharerIdent . entityVal) <$>
selectList [SharerId <-. sids] []
let here = RepoTeamR shr rp
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
let team = Collection
{ collectionId = encodeRouteLocal here
, collectionType = CollectionTypeUnordered
, collectionTotalItems = Just $ length memberShrs
, collectionCurrent = Nothing
, collectionFirst = Nothing
, collectionLast = Nothing
, collectionItems = map (encodeRouteHome . SharerR) memberShrs
}
provideHtmlAndAP team $ redirectToPrettyJSON here
getRepoFollowersR :: ShrIdent -> RpIdent -> Handler TypedContent
getRepoFollowersR shr rp = getFollowersCollection here getFsid
where
here = RepoFollowersR shr rp
getFsid = do
sid <- getKeyBy404 $ UniqueSharer shr
r <- getValBy404 $ UniqueRepo rp sid
return $ repoFollowers r
getHighlightStyleR :: Text -> Handler TypedContent getHighlightStyleR :: Text -> Handler TypedContent
getHighlightStyleR styleName = getHighlightStyleR styleName =
case lookup (unpack styleName) highlightingStyles of case lookup (unpack styleName) highlightingStyles of
@ -384,5 +472,108 @@ getHighlightStyleR styleName =
Just style -> Just style ->
return $ TypedContent typeCss $ toContent $ styleToCss style return $ TypedContent typeCss $ toContent $ styleToCss style
postPostReceiveR :: Handler () postPostReceiveR :: Handler Text
postPostReceiveR = error "TODO post-receive handler not implemented yet" postPostReceiveR = do
push <- requireCheckJsonBody
(pushAP, shr, rp) <- push2ap push
user <- runDB $ do
p <- getJustEntity $ toSqlKey $ H.pushUser push
s <- getJust $ personIdent $ entityVal p
return (p, s)
let shrUser = sharerIdent $ snd user
summary <- do
let mbranch = H.pushBranch push
total = pushCommitsTotal pushAP
withUrlRenderer
[hamlet|
<p>
<a href=@{SharerR shrUser}>#{shr2text shrUser}
\ pushed #{total} #
\ #{commitsText mbranch total} to repo #
<a href=@{RepoR shr rp}>#{rp2text rp}</a>^{branchText shr rp mbranch}.
|]
eid <- pushCommitsC user summary pushAP shr rp
case eid of
Left e -> liftIO $ throwIO $ userError $ T.unpack e
Right obiid -> do
renderUrl <- askUrlRender
obikhid <- encodeKeyHashid obiid
return $
"Push activity published: " <>
renderUrl (SharerOutboxItemR shrUser obikhid)
where
push2ap (H.Push secret _ sharer repo mbranch mbefore after early mlate) = do
encodeRouteLocal <- getEncodeRouteLocal
let shr = text2shr sharer
rp = text2rp repo
commit2ap' = commit2ap shr rp
(commitsLast, commitsFirst) <-
runDB $ case mlate of
Nothing -> (,) <$> traverse commit2ap' early <*> pure Nothing
Just (_omitted, late) ->
(,) <$> traverse commit2ap' late
<*> (Just <$> traverse commit2ap' early)
return
( Push
{ pushCommitsLast = commitsLast
, pushCommitsFirst = commitsFirst
, pushCommitsTotal =
case mlate of
Nothing -> length early
Just (omitted, late) ->
length early + omitted + length late
, pushTarget =
encodeRouteLocal $
case mbranch of
Nothing -> RepoR shr rp
Just b -> RepoBranchR shr rp b
, pushHashBefore = mbefore
, pushHashAfter = after
}
, shr
, rp
)
where
commit2ap shr rp (H.Commit (wauthor, wtime) mcommitted hash title desc) = do
encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome
author <- authorByEmail wauthor
mcommitter <- traverse (authorByEmail . fst) mcommitted
return Commit
{ commitId = encodeRouteLocal $ RepoPatchR shr rp hash
, commitRepository = encodeRouteLocal $ RepoR shr rp
, commitAuthor = second (encodeRouteHome . SharerR) author
, commitCommitter =
second (encodeRouteHome . SharerR) <$> mcommitter
, commitTitle = title
, commitHash = Hash $ encodeUtf8 hash
, commitDescription =
if T.null desc
then Nothing
else Just desc
, commitWritten = wtime
, commitCommitted = snd <$> mcommitted
}
where
authorByEmail (H.Author name email) = do
mperson <- getValBy $ UniquePersonEmail email
case mperson of
Nothing -> return $ Left $ Author name email
Just person ->
Right . sharerIdent <$> getJust (personIdent person)
commitsText :: Maybe a -> Int -> Text
commitsText Nothing n =
if n > 1
then "patches"
else "patch"
commitsText (Just _) n =
if n > 1
then "commits"
else "commit"
--branchText :: ShrIdent -> RpIdent -> Maybe Text -> HtmlUrl (Route App)
branchText _ _ Nothing = const mempty
branchText shr rp (Just branch) =
[hamlet|
, branch #
<a href=@{RepoBranchR shr rp branch}>#{branch}
|]

View file

@ -48,7 +48,7 @@ import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With) import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With)
import Data.MediaType import Data.MediaType
import Web.ActivityPub import Web.ActivityPub hiding (Repo)
import Yesod.ActivityPub import Yesod.ActivityPub
import Yesod.FedURI import Yesod.FedURI
import Yesod.RenderSource import Yesod.RenderSource

View file

@ -59,7 +59,7 @@ import qualified Data.Text as T
import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With) import qualified Data.Text.Lazy.Encoding as L (decodeUtf8With)
import Data.MediaType import Data.MediaType
import Web.ActivityPub hiding (Commit, Author) import Web.ActivityPub hiding (Commit, Author, Repo)
import Yesod.ActivityPub import Yesod.ActivityPub
import Yesod.FedURI import Yesod.FedURI
import Yesod.RenderSource import Yesod.RenderSource

View file

@ -16,6 +16,7 @@
module Vervis.Handler.Sharer module Vervis.Handler.Sharer
( getSharersR ( getSharersR
, getSharerR , getSharerR
, getSharerFollowersR
) )
where where
@ -30,6 +31,10 @@ import Yesod.Core.Content (TypedContent)
import Yesod.Core.Handler (redirect, notFound) import Yesod.Core.Handler (redirect, notFound)
import Yesod.Persist.Core (runDB, getBy404) import Yesod.Persist.Core (runDB, getBy404)
import Database.Persist.Local
import Yesod.Persist.Local
import Vervis.API
import Vervis.Foundation import Vervis.Foundation
import Vervis.Handler.Person import Vervis.Handler.Person
import Vervis.Handler.Group import Vervis.Handler.Group
@ -64,3 +69,21 @@ getSharerR shr = do
case ent of case ent of
Left (Entity _ p) -> getPerson shr s p Left (Entity _ p) -> getPerson shr s p
Right (Entity _ g) -> getGroup shr g Right (Entity _ g) -> getGroup shr g
getSharerFollowersR :: ShrIdent -> Handler TypedContent
getSharerFollowersR shr = getFollowersCollection here getFsid
where
here = SharerFollowersR shr
getFsid = do
sid <- getKeyBy404 $ UniqueSharer shr
mval <- runMaybeT
$ Left <$> MaybeT (getValBy $ UniquePersonIdent sid)
<|> Right <$> MaybeT (getValBy $ UniqueGroup sid)
case mval of
Nothing -> do
$logWarn $ "Found non-person non-group sharer: " <> shr2text shr
notFound
Just val ->
case val of
Left person -> return $ personFollowers person
Right _group -> notFound

View file

@ -42,6 +42,7 @@ import Data.Git.Graph
import Data.Git.Harder import Data.Git.Harder
import Data.Graph.Inductive.Graph -- (noNodes) import Data.Graph.Inductive.Graph -- (noNodes)
import Data.Graph.Inductive.Query.Topsort import Data.Graph.Inductive.Query.Topsort
import Data.Int
import Data.List.NonEmpty (NonEmpty, nonEmpty) import Data.List.NonEmpty (NonEmpty, nonEmpty)
import Data.Text (Text) import Data.Text (Text)
import Data.Time.Clock import Data.Time.Clock
@ -49,6 +50,7 @@ import Data.Time.Clock.POSIX
import Data.Word import Data.Word
import GHC.Generics import GHC.Generics
import Network.HTTP.Client import Network.HTTP.Client
import Network.HTTP.Types.Header
import System.Directory import System.Directory
import System.Environment import System.Environment
import System.Exit import System.Exit
@ -57,6 +59,7 @@ import System.IO
import Text.Email.Aeson.Instances () import Text.Email.Aeson.Instances ()
import Text.Email.Validate import Text.Email.Validate
import Time.Types import Time.Types
import Yesod.Core.Content
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString.Base16 as B16
@ -122,9 +125,12 @@ instance ToJSON Commit
data Push = Push data Push = Push
{ pushSecret :: Text { pushSecret :: Text
, pushUser :: Int64
, pushSharer :: Text , pushSharer :: Text
, pushRepo :: Text , pushRepo :: Text
, pushBranch :: Maybe Text , pushBranch :: Maybe Text
, pushBefore :: Maybe Text
, pushAfter :: Text
, pushInit :: NonEmpty Commit , pushInit :: NonEmpty Commit
, pushLast :: Maybe (Int, NonEmpty Commit) , pushLast :: Maybe (Int, NonEmpty Commit)
} }
@ -148,10 +154,11 @@ writeHookConfig config = do
reportNewCommits :: Config -> Text -> Text -> IO () reportNewCommits :: Config -> Text -> Text -> IO ()
reportNewCommits config sharer repo = do reportNewCommits config sharer repo = do
user <- read <$> getEnv "VERVIS_SSH_USER"
manager <- newManager defaultManagerSettings manager <- newManager defaultManagerSettings
withRepo "." $ loop manager withRepo "." $ loop user manager
where where
loop manager git = do loop user manager git = do
eof <- isEOF eof <- isEOF
unless eof $ do unless eof $ do
result <- runExceptT $ do result <- runExceptT $ do
@ -205,9 +212,12 @@ reportNewCommits config sharer repo = do
return (eNE, Just (middle, lNE)) return (eNE, Just (middle, lNE))
let push = Push let push = Push
{ pushSecret = configSecret config { pushSecret = configSecret config
, pushUser = user
, pushSharer = sharer , pushSharer = sharer
, pushRepo = repo , pushRepo = repo
, pushBranch = Just branch , pushBranch = Just branch
, pushBefore = old <$ moldRef
, pushAfter = new
, pushInit = early , pushInit = early
, pushLast = late , pushLast = late
} }
@ -219,6 +229,7 @@ reportNewCommits config sharer repo = do
req <- requestFromURI $ uriFromObjURI uri req <- requestFromURI $ uriFromObjURI uri
let req' = let req' =
setRequestCheckStatus $ setRequestCheckStatus $
consHeader hContentType typeJson $
req { method = "POST" req { method = "POST"
, requestBody = RequestBodyLBS $ encode push , requestBody = RequestBodyLBS $ encode push
} }
@ -227,10 +238,11 @@ reportNewCommits config sharer repo = do
case result of case result of
Left e -> TIO.hPutStrLn stderr $ "HOOK ERROR: " <> e Left e -> TIO.hPutStrLn stderr $ "HOOK ERROR: " <> e
Right _resp -> return () Right _resp -> return ()
loop manager git loop user manager git
where where
adaptErr :: HttpException -> Text adaptErr :: HttpException -> Text
adaptErr = T.pack . displayException adaptErr = T.pack . displayException
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
parseRef t = parseRef t =
if t == nullRef if t == nullRef
then return Nothing then return Nothing

View file

@ -1008,6 +1008,48 @@ changes hLocal ctx =
"summary" "summary"
-- 129 -- 129
, addFieldPrimRequired "TicketDependency" defaultTime "created" , addFieldPrimRequired "TicketDependency" defaultTime "created"
-- 130
, addFieldRefRequired'
"Repo"
FollowerSet130
(Just $ do
rids <- selectKeysList ([] :: [Filter Repo130]) []
for_ rids $ \ rid -> do
fsid <- insert FollowerSet130
update rid [Repo130Followers =. fsid]
)
"followers"
"FollowerSet"
-- 131
, addUnique "Repo" $ Unique "UniqueRepoFollowers" ["followers"]
-- 132
, addFieldRefRequired'
"Repo"
Inbox130
(Just $ do
rids <- selectKeysList ([] :: [Filter Repo130]) []
for_ rids $ \ rid -> do
ibid <- insert Inbox130
update rid [Repo130Inbox =. ibid]
)
"inbox"
"Inbox"
-- 133
, addUnique "Repo" $ Unique "UniqueRepoInbox" ["inbox"]
-- 134
, addFieldRefRequired'
"Person"
FollowerSet130
(Just $ do
pids <- selectKeysList ([] :: [Filter Person130]) []
for_ pids $ \ pid -> do
fsid <- insert FollowerSet130
update pid [Person130Followers =. fsid]
)
"followers"
"FollowerSet"
-- 135
, addUnique "Person" $ Unique "UniquePersonFollowers" ["followers"]
] ]
migrateDB migrateDB

View file

@ -118,6 +118,10 @@ module Vervis.Migration.Model
, Ticket127Generic (..) , Ticket127Generic (..)
, TicketDependency127Generic (..) , TicketDependency127Generic (..)
, TicketDependency127 , TicketDependency127
, Inbox130Generic (..)
, FollowerSet130Generic (..)
, Repo130
, Person130
) )
where where
@ -239,3 +243,6 @@ makeEntitiesMigration "20190624"
makeEntitiesMigration "127" makeEntitiesMigration "127"
$(modelFile "migrations/2019_07_11.model") $(modelFile "migrations/2019_07_11.model")
makeEntitiesMigration "130"
$(modelFile "migrations/2019_09_06.model")

View file

@ -42,6 +42,7 @@ import Network.SSH.Channel
import Network.SSH.Crypto import Network.SSH.Crypto
import Network.SSH.Session import Network.SSH.Session
import System.Directory (doesFileExist, doesDirectoryExist) import System.Directory (doesFileExist, doesDirectoryExist)
import System.Environment
import System.FilePath ((</>)) import System.FilePath ((</>))
import System.Process (CreateProcess (..), StdStream (..), createProcess, proc) import System.Process (CreateProcess (..), StdStream (..), createProcess, proc)
@ -263,6 +264,8 @@ runAction repoDir _wantReply action =
can <- canPushTo sharer repo can <- canPushTo sharer repo
if can if can
then whenGitRepoExists True repoPath $ do then whenGitRepoExists True repoPath $ do
pid <- authId <$> askAuthDetails
liftIO $ setEnv "VERVIS_SSH_USER" (show $ fromSqlKey pid)
execute "git-receive-pack" [repoPath] execute "git-receive-pack" [repoPath]
return ARProcess return ARProcess
else return $ ARFail "You can't push to this repository" else return $ ARFail "You can't push to this repository"

View file

@ -31,6 +31,7 @@ module Web.ActivityPub
, Owner (..) , Owner (..)
, PublicKey (..) , PublicKey (..)
, Actor (..) , Actor (..)
, Repo (..)
, Project (..) , Project (..)
, CollectionType (..) , CollectionType (..)
, Collection (..) , Collection (..)
@ -175,22 +176,25 @@ instance (ActivityPub a, UriMode u) => ToJSON (Doc a u) where
context [t] = "@context" .= t context [t] = "@context" .= t
context ts = "@context" .= ts context ts = "@context" .= ts
data ActorType = ActorTypePerson | ActorTypeProject | ActorTypeOther Text data ActorType =
ActorTypePerson | ActorTypeRepo | ActorTypeProject | ActorTypeOther Text
deriving Eq deriving Eq
instance FromJSON ActorType where instance FromJSON ActorType where
parseJSON = withText "ActorType" $ pure . parse parseJSON = withText "ActorType" $ pure . parse
where where
parse t parse t
| t == "Person" = ActorTypePerson | t == "Person" = ActorTypePerson
| t == "Project" = ActorTypeProject | t == "Repository" = ActorTypeRepo
| otherwise = ActorTypeOther t | t == "Project" = ActorTypeProject
| otherwise = ActorTypeOther t
instance ToJSON ActorType where instance ToJSON ActorType where
toJSON = error "toJSON ActorType" toJSON = error "toJSON ActorType"
toEncoding at = toEncoding at =
toEncoding $ case at of toEncoding $ case at of
ActorTypePerson -> "Person" ActorTypePerson -> "Person"
ActorTypeRepo -> "Repository"
ActorTypeProject -> "Project" ActorTypeProject -> "Project"
ActorTypeOther t -> t ActorTypeOther t -> t
@ -307,6 +311,24 @@ instance ActivityPub Actor where
<> "followers" .=? (ObjURI authority <$> followers) <> "followers" .=? (ObjURI authority <$> followers)
<> "publicKey" `pair` encodePublicKeySet authority pkeys <> "publicKey" `pair` encodePublicKeySet authority pkeys
data Repo u = Repo
{ repoActor :: Actor u
, repoTeam :: LocalURI
}
instance ActivityPub Repo where
jsonldContext _ = [as2Context, secContext, forgeContext, extContext]
parseObject o = do
(h, a) <- parseObject o
unless (actorType a == ActorTypeRepo) $
fail "Actor type isn't Repository"
fmap (h,) $
Repo a
<$> withAuthorityO h (o .:| "team")
toSeries authority (Repo actor team)
= toSeries authority actor
<> "team" .= ObjURI authority team
data Project u = Project data Project u = Project
{ projectActor :: Actor u { projectActor :: Actor u
, projectTeam :: LocalURI , projectTeam :: LocalURI
@ -1021,10 +1043,11 @@ encodeOffer authority actor (Offer obj target)
<> "target" .= target <> "target" .= target
data Push u = Push data Push u = Push
{ pushCommits :: NonEmpty (Commit u) { pushCommitsLast :: NonEmpty (Commit u)
, pushCommitsFirst :: Maybe (NonEmpty (Commit u))
, pushCommitsTotal :: Int , pushCommitsTotal :: Int
, pushTarget :: LocalURI , pushTarget :: LocalURI
, pushHashBefore :: Text , pushHashBefore :: Maybe Text
, pushHashAfter :: Text , pushHashAfter :: Text
} }
@ -1033,23 +1056,25 @@ parsePush a o = do
c <- o .: "object" c <- o .: "object"
Push Push
<$> (traverse (withAuthorityT a . parseObject) =<< c .: "items") <$> (traverse (withAuthorityT a . parseObject) =<< c .: "items")
<*> (traverse (traverse $ withAuthorityT a . parseObject) =<< c .:? "earlyItems")
<*> c .: "totalItems" <*> c .: "totalItems"
<*> withAuthorityO a (o .: "target") <*> withAuthorityO a (o .: "target")
<*> o .: "hashBefore" <*> o .:? "hashBefore"
<*> o .: "hashAfter" <*> o .: "hashAfter"
encodePush :: UriMode u => Authority u -> Push u -> Series encodePush :: UriMode u => Authority u -> Push u -> Series
encodePush a (Push commits total target before after) encodePush a (Push lateCommits earlyCommits total target before after)
= "object" `pair` pairs = "object" `pair` pairs
( "type" .= ("OrderedCollection" :: Text) ( "type" .= ("OrderedCollection" :: Text)
<> pair <> pair "items" (objectList lateCommits)
"items" <> maybe mempty (pair "earlyItems" . objectList) earlyCommits
(listEncoding (pairs . toSeries a) (NE.toList commits))
<> "totalItems" .= total <> "totalItems" .= total
) )
<> "target" .= ObjURI a target <> "target" .= ObjURI a target
<> "hashBefore" .= before <> "hashBefore" .=? before
<> "hashAfter" .= after <> "hashAfter" .= after
where
objectList items = listEncoding (pairs . toSeries a) (NE.toList items)
data Reject u = Reject data Reject u = Reject
{ rejectObject :: ObjURI u { rejectObject :: ObjURI u

View file

@ -16,7 +16,7 @@ $# <http://creativecommons.org/publicdomain/zero/1.0/>.
<tr> <tr>
<th>Collaborator <th>Collaborator
<th>Role <th>Role
$forall (Entity _sid sharer, Value mrl) <- devs $forall (Entity _sid sharer, E.Value mrl) <- devs
<tr> <tr>
<td>^{sharerLinkW sharer} <td>^{sharerLinkW sharer}
<td> <td>

View file

@ -15,7 +15,7 @@ $# <http://creativecommons.org/publicdomain/zero/1.0/>.
<p>These are the repositories shared by #{shr2text user}. <p>These are the repositories shared by #{shr2text user}.
<ul> <ul>
$forall Value repo <- repos $forall E.Value repo <- repos
<li> <li>
<a href=@{RepoR user repo}>#{rp2text repo} <a href=@{RepoR user repo}>#{rp2text repo}
<li> <li>