├── .dockerignore ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── cmd └── post-receive │ ├── main.go │ └── utils.go ├── components ├── avatar.tsx ├── base.tsx ├── body.tsx ├── button.tsx ├── center.tsx ├── code.tsx ├── code │ ├── gutter.tsx │ ├── load.ts │ ├── maps.ts │ └── pre.tsx ├── commit │ ├── commit.tsx │ └── diff.tsx ├── divider.tsx ├── dropbox.tsx ├── dropdown.tsx ├── feed │ ├── commit-event.tsx │ └── create-repository-event.tsx ├── header.tsx ├── heading.tsx ├── image.tsx ├── input.tsx ├── issue │ ├── comment.tsx │ └── write.tsx ├── label.tsx ├── link.tsx ├── list.tsx ├── new │ ├── button.tsx │ ├── head.tsx │ ├── organization.tsx │ └── repository.tsx ├── profile │ ├── org.tsx │ ├── repos.tsx │ └── user.tsx ├── progress.tsx ├── relative.tsx ├── repository │ ├── branch.tsx │ ├── empty.tsx │ ├── layout.tsx │ ├── path.tsx │ └── tree.tsx ├── settings │ ├── field.tsx │ └── left.tsx ├── skeleton.tsx ├── split.tsx ├── svgs │ ├── add.tsx │ ├── arrow.tsx │ ├── file.tsx │ ├── folder.tsx │ ├── git.tsx │ ├── logo.tsx │ ├── no-data.tsx │ ├── no-events.tsx │ ├── notfound.tsx │ └── pencil.tsx ├── tabs.tsx └── text.tsx ├── data ├── CascadiaCode.woff2 └── o2.ini ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── _document.tsx ├── feed.tsx ├── login.tsx ├── new.tsx ├── organization.tsx ├── register.tsx ├── repository │ ├── blob.tsx │ ├── commit.tsx │ ├── commits.tsx │ ├── issue.tsx │ ├── issues.tsx │ ├── new.tsx │ ├── repository.tsx │ ├── settings.tsx │ └── tree.tsx ├── settings │ ├── privacy.tsx │ └── settings.tsx └── user.tsx ├── pkg ├── auth │ ├── is.go │ ├── login.go │ ├── middleware.go │ ├── register.go │ └── token.go ├── data │ ├── base.go │ ├── compose.go │ └── with.go ├── git │ ├── blob.go │ ├── branch.go │ ├── command.go │ ├── commit.go │ ├── commits.go │ ├── hooks.go │ ├── init.go │ ├── path.go │ ├── refs.go │ ├── repository.go │ └── tree.go ├── images │ └── picture.go ├── log │ └── log.go ├── middleware │ ├── charset.go │ ├── debug.go │ ├── pex.go │ ├── repo.go │ ├── resource.go │ └── scheme.go ├── models │ ├── action.go │ ├── base.go │ ├── init.go │ ├── issue.go │ ├── issue_comment.go │ ├── organization.go │ ├── permission.go │ ├── repository.go │ └── user.go ├── pex │ └── pex.go ├── render │ ├── ogp.go │ └── render.go └── store │ ├── config.go │ ├── cwd.go │ ├── database.go │ ├── hooks.go │ └── log.go ├── quercia.config.js ├── routes ├── datas │ ├── blob.go │ └── repository.go ├── git │ ├── cache_headers.go │ ├── get_service_type.go │ ├── git_command.go │ ├── internal_error.go │ ├── packets.go │ ├── refs.go │ └── rpc.go ├── index.go ├── login.go ├── logout.go ├── new-org.go ├── new-repo.go ├── new.go ├── picture.go ├── profile.go ├── register.go ├── repository │ ├── blob.go │ ├── commit.go │ ├── commits.go │ ├── issue.go │ ├── issues.go │ ├── new.go │ ├── repository.go │ ├── settings.go │ └── tree.go ├── settings │ ├── privacy.go │ └── settings.go └── shared │ └── 404.go ├── tsconfig.json ├── types ├── data.ts ├── index.d.ts ├── mdown.ts ├── repository.ts ├── theme.ts ├── time.ts └── xtend.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | screens 3 | data/repos -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | frontend: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: volta-cli/action@v1 13 | 14 | - run: yarn install 15 | - run: yarn build 16 | 17 | - name: Upload quercia build artifacts 18 | uses: actions/upload-artifact@v1 19 | with: 20 | name: __quercia 21 | path: __quercia 22 | 23 | build: 24 | needs: frontend 25 | strategy: 26 | matrix: 27 | platform: [ubuntu-latest, macos-latest, windows-latest] 28 | runs-on: ${{ matrix.platform }} 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: actions/setup-go@v2 32 | 33 | - name: Download previous quercia build 34 | uses: actions/download-artifact@v1 35 | with: 36 | name: __quercia 37 | 38 | - name: Build pkger asset file 39 | run: | 40 | go get -v -t -d ./... 41 | go get -d -v github.com/markbates/pkger@v0.16.0 42 | go get github.com/markbates/pkger/cmd/pkger 43 | rm -rf __quercia/*/server 44 | rm data/CascadiaCode.woff2 45 | pkger 46 | 47 | - name: Build the go binary 48 | run: go build -ldflags="-s -w" -o o2 49 | 50 | - name: Create Release 51 | id: create_release 52 | uses: actions/create-release@v1 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | with: 56 | tag_name: ${{ github.ref }} 57 | release_name: Release ${{ github.ref }} 58 | draft: false 59 | prerelease: false 60 | 61 | - name: Upload Release Asset 62 | id: upload-release-asset 63 | uses: actions/upload-release-asset@v1 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | with: 67 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 68 | asset_path: ./o2 69 | asset_name: o2-${{ matrix.platform }} 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __quercia 2 | node_modules 3 | bins 4 | data/repos/* 5 | data/pictures/* 6 | data/*.log 7 | pkged.go 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine AS builder 2 | 3 | RUN apk update && apk add --no-cache git ca-certificates build-base && update-ca-certificates 4 | 5 | WORKDIR /app 6 | 7 | COPY . . 8 | 9 | RUN go mod download 10 | RUN go mod verify 11 | RUN go get -v -t -d ./... && \ 12 | go get -d -v github.com/markbates/pkger@v0.16.0 && \ 13 | go build -o /go/bin/pkger github.com/markbates/pkger/cmd/pkger && \ 14 | rm -rf __quercia/*/server && \ 15 | pkger 16 | 17 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-w -s' -o /go/bin/o2 *.go 18 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-w -s' -o /go/bin/o2-post-receive cmd/post-receive/*.go 19 | 20 | RUN mkdir -p /data/repos 21 | 22 | FROM alpine:latest 23 | 24 | RUN apk update && apk add --no-cache git 25 | 26 | WORKDIR / 27 | COPY --from=builder /data /data 28 | COPY --from=builder /go/bin/o2 /bin/o2 29 | COPY --from=builder /go/bin/o2-post-receive /bin/o2-post-receive 30 | 31 | ENTRYPOINT ["/bin/o2", "--debug"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: o2 cmds 2 | 3 | run: cmds 4 | PATH=$$PATH:$$PWD/bins go run main.go 5 | 6 | o2: 7 | go build main.go 8 | 9 | cmds: 10 | go build -o bins/o2-post-receive cmd/post-receive/*.go 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | o2 - self-hosted git web app 3 |

4 | 5 | > The project is still in early development, and while we try to provide the 6 | > best support you may encounter bugs and rough edges. Be sure to always report 7 | > anything you find so that we can improve your experience! 8 | 9 | ### Setting up 10 | 11 | There is currently no official documentation, but if you want to get a glimpse 12 | at what `o2` is capable of you can try running it via the CI built docker image. 13 | To get a quick start you can use this 14 | [`docker-compose.yml`](https://github.com/lucat1/o2/blob/master/docker-compose.yml) 15 | example file. 16 | 17 | ### screenshots 18 | 19 | ![login](https://user-images.githubusercontent.com/29304787/82896559-7492c100-9f56-11ea-992a-e631dd75e5a8.png) 20 | ![profile](https://user-images.githubusercontent.com/29304787/82896565-75c3ee00-9f56-11ea-9354-91a3b5eaeb71.png) 21 | ![new](https://user-images.githubusercontent.com/29304787/82896566-765c8480-9f56-11ea-92a8-48d3caad4574.png) 22 | ![tree](https://user-images.githubusercontent.com/29304787/82896572-778db180-9f56-11ea-9005-92ee284735f8.png) 23 | ![blob](https://user-images.githubusercontent.com/29304787/82896569-76f51b00-9f56-11ea-9774-c596ce190a18.png) 24 | ![commits](https://user-images.githubusercontent.com/29304787/82896574-78264800-9f56-11ea-994d-90307a3d881d.png) 25 | ![commit](https://user-images.githubusercontent.com/29304787/82896573-778db180-9f56-11ea-9408-37335fee5966.png) 26 | -------------------------------------------------------------------------------- /cmd/post-receive/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "os" 7 | "path" 8 | "time" 9 | 10 | "github.com/lucat1/o2/pkg/log" 11 | "github.com/lucat1/o2/pkg/models" 12 | "github.com/lucat1/o2/pkg/store" 13 | ) 14 | 15 | func init() { 16 | flag.Parse() 17 | logsPath := os.Getenv("LOGSPATH") 18 | 19 | // initialize the logger, store and database 20 | store.InitConfig() 21 | store.InitLogs() 22 | 23 | // store logs only in a file. Don't print anything to the client 24 | file, err := os.OpenFile(logsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 25 | if err != nil { 26 | log.Fatal().Err(err).Msg("Could not open log file (post-receive)") 27 | } 28 | log.Output(file) 29 | 30 | store.InitDatabase() 31 | } 32 | 33 | func main() { 34 | gitDir := os.Getenv("GIT_DIR") 35 | dir := path.Join(store.GetCwd(), gitDir) 36 | 37 | // read stdin data 38 | previous, next, ref := parseStdin() 39 | 40 | log.Info(). 41 | Str("dir", dir). 42 | Strs("arguments", os.Args). 43 | Str("previous", previous). 44 | Str("next", next). 45 | Str("ref", ref). 46 | Msg("Called post-receive") 47 | 48 | // get commits 49 | repo := findRepository(dir) 50 | dbRepo := findDatabaseRepository(dir) 51 | commits := findCommits(repo, previous, next) 52 | log.Info(). 53 | Strs("commits", commitsHashes(commits)). 54 | Msg("Found commits") 55 | 56 | raw, _ := json.Marshal(map[string]interface{}{ 57 | "commits": commits.Commits, 58 | "more": commits.Next, 59 | }) 60 | // push the action to the database 61 | event := models.Event{ 62 | Time: time.Now(), 63 | Type: models.CommitEvent, 64 | Resource: dbRepo.UUID, 65 | Data: raw, 66 | } 67 | 68 | if err := event.Insert(); err != nil { 69 | log.Fatal().Err(err).Msg("Could not save event in the database") 70 | } 71 | 72 | log.Info().Msg("Successfully saved event in databases") 73 | } 74 | -------------------------------------------------------------------------------- /cmd/post-receive/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "path" 7 | 8 | "github.com/lucat1/o2/pkg/git" 9 | "github.com/lucat1/o2/pkg/log" 10 | "github.com/lucat1/o2/pkg/models" 11 | uuid "github.com/satori/go.uuid" 12 | ) 13 | 14 | func parseStdin() (string, string, string) { 15 | scanner := bufio.NewScanner(os.Stdin) 16 | // scan every byte 17 | scanner.Split(bufio.ScanBytes) 18 | 19 | i := 0 20 | res := []string{"", "", ""} 21 | 22 | // split by spaces 23 | for scanner.Scan() { 24 | char := scanner.Text() 25 | if char == " " { 26 | i++ 27 | continue 28 | } 29 | 30 | res[i] += char 31 | } 32 | 33 | if err := scanner.Err(); err != nil { 34 | log.Fatal().Err(err).Msg("Could not read input from stdin") 35 | } 36 | 37 | return res[0], res[1], res[2] 38 | } 39 | 40 | func findRepository(path string) *git.Repository { 41 | return &git.Repository{Path: path} 42 | } 43 | 44 | func findDatabaseRepository(dir string) models.Repository { 45 | rawUUID := path.Base(dir) 46 | id, err := uuid.FromString(rawUUID) 47 | if err != nil { 48 | log.Fatal().Err(err).Msg("Could not parse UUID") 49 | } 50 | 51 | repo, err := models.GetRepositoryByUUID(id) 52 | if err != nil { 53 | log.Fatal(). 54 | Err(err). 55 | Str("uuid", id.String()). 56 | Msg("Error while querying the DB to find a repository") 57 | } 58 | 59 | return repo 60 | } 61 | 62 | func findCommits(repo *git.Repository, prev, next string) git.Commits { 63 | branch := prev + ".." + next 64 | // if the previos commit didn't exist (first push) 65 | // we can just log until the `next` 66 | if prev == "0000000000000000000000000000000000000000" { 67 | branch = next 68 | } 69 | 70 | commits, err := repo.Branch(branch).Commits(0, 10) 71 | if err != nil { 72 | log.Fatal(). 73 | Err(err). 74 | Str("repo", repo.Path). 75 | Str("prev", prev). 76 | Str("next", next). 77 | Str("branch", branch). 78 | Msg("Could not find pushed commits") 79 | } 80 | 81 | return commits 82 | } 83 | 84 | func commitsHashes(commits git.Commits) (res []string) { 85 | for _, commit := range commits.Commits { 86 | res = append(res, commit.Commit) 87 | } 88 | 89 | return 90 | } 91 | -------------------------------------------------------------------------------- /components/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Flex } from 'rebass' 3 | 4 | import { navigate } from '@quercia/quercia' 5 | 6 | import Relative from './relative' 7 | import Dropdown from './dropdown' 8 | import Divider from './divider' 9 | 10 | import Image from './image' 11 | import { List, Item } from './list' 12 | 13 | import { LoggedUser } from '../types/data' 14 | 15 | const Avatar: React.FC = ({ picture, name }) => { 16 | const [open, setOpen] = React.useState(false) 17 | const go = React.useCallback((url: string) => { 18 | setOpen(false) 19 | navigate(url) 20 | }, []) 21 | 22 | return ( 23 | 24 | setOpen(true)} 37 | onKeyUp={e => e.keyCode == 13 && setOpen(!open)} 38 | alt='Your profile picture' 39 | src={'/picture/' + picture} 40 | /> 41 | setOpen(false)} 46 | > 47 | 48 | go(`/${name}`)}>Your profile 49 | 50 | go('/settings')}>Settings 51 | 52 | go('/new')}>New 53 | 54 | go('/logout')}>Logout 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export default Avatar 62 | -------------------------------------------------------------------------------- /components/base.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Box, BoxProps, FlexProps } from 'rebass' 3 | import merge from '../types/xtend' 4 | 5 | const Base: React.FC = props => ( 6 | 19 | ) 20 | 21 | export default Base 22 | -------------------------------------------------------------------------------- /components/body.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Flex, FlexProps } from 'rebass' 3 | import merge from '../types/xtend' 4 | 5 | const Body: React.FC = props => ( 6 | 18 | ) 19 | 20 | export default Body 21 | -------------------------------------------------------------------------------- /components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { variant } from 'styled-system' 3 | import { Button as RebassButton, ButtonProps } from 'rebass' 4 | import merge from '../types/xtend' 5 | 6 | const PrimaryButton: React.FC = props => ( 7 | 50 | ) 51 | 52 | const Button: React.FC = props => { 53 | const sx = variant({ 54 | variants: { 55 | 'sm': { 56 | minWidth: '1.25rem', 57 | py: 1 58 | }, 59 | 60 | 'md': { 61 | minWidth: '5.5rem' 62 | }, 63 | 64 | 'secondary': { 65 | borderColor: 'bg.3', 66 | color: 'fg.5', 67 | bg: 'transparent' 68 | }, 69 | 70 | 'md-secondary': { 71 | borderColor: 'bg.3', 72 | color: 'fg.5', 73 | bg: 'transparent', 74 | minWidth: '5.5rem' 75 | }, 76 | 'sm-secondary': { 77 | minWidth: '1.25rem', 78 | borderColor: 'bg.3', 79 | color: 'fg.5', 80 | bg: 'transparent' 81 | } 82 | } 83 | })(props) 84 | 85 | return ( 86 | 91 | ) 92 | } 93 | 94 | export default Button 95 | -------------------------------------------------------------------------------- /components/center.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Flex, FlexProps } from 'rebass' 3 | import merge from '../types/xtend' 4 | 5 | const Center: React.FC = props => ( 6 | 17 | ) 18 | 19 | export default Center 20 | -------------------------------------------------------------------------------- /components/code.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import merge from '../types/xtend' 3 | 4 | import Text, { TextProps } from './text' 5 | 6 | const Code: React.FC = props => { 7 | return ( 8 | 20 | ) 21 | } 22 | 23 | export default Code 24 | -------------------------------------------------------------------------------- /components/code/gutter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import merge from '../../types/xtend' 3 | 4 | import { TextProps } from '../text' 5 | import Pre from './pre' 6 | 7 | const Gutter: React.FC = props => ( 8 |
23 | )
24 | 
25 | export default Gutter
26 | 


--------------------------------------------------------------------------------
/components/code/load.ts:
--------------------------------------------------------------------------------
 1 | import { reqScript } from '@quercia/quercia'
 2 | 
 3 | import { aliases, dependencies } from './maps'
 4 | 
 5 | const loaded: Set = new Set()
 6 | 
 7 | export const lang = (l: string): string => aliases[l] || l
 8 | 
 9 | const load = async (l: string) => {
10 |   const language = lang(l)
11 |   if (language == undefined || loaded.has(language)) {
12 |     return
13 |   }
14 | 
15 |   let deps: string | string[] = dependencies[language]
16 |   if (!Array.isArray(deps)) {
17 |     deps = [deps]
18 |   }
19 | 
20 |   await Promise.all(deps.map(load))
21 | 
22 |   await reqScript(
23 |     `https://unpkg.com/prismjs@1.20.0/components/prism-${language}.min.js`
24 |   )
25 |   loaded.add(language)
26 | }
27 | 
28 | export default load
29 | 


--------------------------------------------------------------------------------
/components/code/pre.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import merge from '../../types/xtend'
 3 | 
 4 | import Text, { TextProps } from '../text'
 5 | 
 6 | const Pre: React.FC = props => (
 7 |   
57 | )
58 | 
59 | // `
60 | 
61 | export default Pre
62 | 


--------------------------------------------------------------------------------
/components/commit/commit.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Flex } from 'rebass'
 3 | import format from 'tinydate'
 4 | 
 5 | import Container from '../base'
 6 | import Image from '../image'
 7 | import Link from '../link'
 8 | import Text from '../text'
 9 | 
10 | import { Commit as ICommit } from '../../types/data'
11 | 
12 | const Commit: React.FC<{ commit: ICommit; base: string }> = ({
13 |   commit,
14 |   base
15 | }) => (
16 |   
25 |     
26 |       
31 |         {
41 |       
42 |       
53 |         
54 |           {commit?.subject}
55 |         
56 | 
57 |         
58 |           
59 |             {commit?.author?.name}
60 |           
61 |           
62 |             commited on
63 |           
64 |           
65 |             {format('{DD} {MM} {YYYY}')(new Date(commit?.author?.date))}
66 |           
67 |         
68 |       
69 |     
70 | 
71 |     
72 |       {commit?.abbrv_tree}
73 |     
74 |   
75 | )
76 | 
77 | export default Commit
78 | 


--------------------------------------------------------------------------------
/components/divider.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Box, BoxProps } from 'rebass'
 3 | import merge from '../types/xtend'
 4 | 
 5 | const Divider: React.FC = props => (
 6 |   
12 | )
13 | 
14 | export default Divider
15 | 


--------------------------------------------------------------------------------
/components/dropbox.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Box, ButtonProps } from 'rebass'
 3 | import merge from '../types/xtend'
 4 | 
 5 | import Arrow from './svgs/arrow'
 6 | import Button from './button'
 7 | 
 8 | const Dropbox: React.FC = ({
 9 |   children,
10 |   open,
11 |   ...props
12 | }) => (
13 |   
24 | )
25 | 
26 | export default Dropbox
27 | 


--------------------------------------------------------------------------------
/components/dropdown.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Box, BoxProps } from 'rebass'
 3 | import merge from '../types/xtend'
 4 | import useOnClickOutside from 'use-onclickoutside'
 5 | 
 6 | const Dropdown: React.FC void
 9 | }> = ({ open, onClose, ...props }) => {
10 |   const ref = React.useRef()
11 |   useOnClickOutside(ref, onClose)
12 | 
13 |   return (
14 |     
43 |   )
44 | }
45 | 
46 | export default Dropdown
47 | 


--------------------------------------------------------------------------------
/components/feed/commit-event.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Flex } from 'rebass'
 3 | import format from 'tinydate'
 4 | 
 5 | import Heading from '../heading'
 6 | import Link from '../link'
 7 | import Text from '../text'
 8 | import Commit from '../commit/commit'
 9 | 
10 | import { CommitEvent as Event } from '../../types/data'
11 | 
12 | const CommitEvent: React.FC<{ event: Event }> = ({ event }) => {
13 |   const base = '/' + event.owner + '/' + event.name
14 |   return (
15 |     
16 |       
17 |         {format('on the {DD} {MM} {YYYY} at {HH}:{mm}:{ss}')(
18 |           new Date(event.time)
19 |         )}
20 |       
21 | 
22 |       
23 |         {event.data.commits.length} commit
24 |         {event.data.commits.length > 1 ? 's' : ''}{' '}
25 |         {event.data.commits.length > 1 ? 'have' : 'has'} been pushed to{' '}
26 |         
27 |           {event.owner}/{event.name}
28 |         
29 |         :
30 |       
31 | 
32 |       
33 |         
34 |         
35 |           {event.data.commits.map(commit => (
36 |             
37 |           ))}
38 |         
39 |       
40 | 
41 |       {event.data.more && and more}
42 |     
43 |   )
44 | }
45 | 
46 | export default CommitEvent
47 | 


--------------------------------------------------------------------------------
/components/feed/create-repository-event.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Flex } from 'rebass'
 3 | import format from 'tinydate'
 4 | 
 5 | import Heading from '../heading'
 6 | import Link from '../link'
 7 | import Text from '../text'
 8 | 
 9 | import { CreateRepositoryEvent as Event } from '../../types/data'
10 | 
11 | const CreateRepositoryEvent: React.FC<{ event: Event }> = ({ event }) => {
12 |   const base = '/' + event.owner + '/' + event.name
13 |   return (
14 |     
15 |       
16 |         {format('on the {DD} {MM} {YYYY} at {HH}:{mm}:{ss}')(
17 |           new Date(event.time)
18 |         )}
19 |       
20 | 
21 |       
22 |         {event.owner} created a new
23 |         repository at{' '}
24 |         
25 |           {event.owner}/{event.name}
26 |         
27 |       
28 |     
29 |   )
30 | }
31 | 
32 | export default CreateRepositoryEvent
33 | 


--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Flex } from 'rebass'
 3 | 
 4 | import { navigate } from '@quercia/quercia'
 5 | 
 6 | import Body from './body'
 7 | import Link from './link'
 8 | import Avatar from './avatar'
 9 | import Logo from './svgs/logo'
10 | import Button from './button'
11 | 
12 | import { Base } from '../types/data'
13 | 
14 | const Header: React.FC> = ({ account }) => {
15 |   return (
16 |     
27 |       
28 |         
29 |            navigate('/')} />
30 |         
31 |         {account ? (
32 |           
33 |         ) : (
34 |           
35 |             
36 |               Login
37 |             
38 |             
39 |               
40 |             
41 |           
42 |         )}
43 |       
44 |     
45 |   )
46 | }
47 | 
48 | export default Header
49 | 


--------------------------------------------------------------------------------
/components/heading.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import merge from '../types/xtend'
 3 | 
 4 | import Text, { TextProps } from './text'
 5 | 
 6 | const Heading: React.FC = props => {
 7 |   return (
 8 |     
14 |   )
15 | }
16 | 
17 | export default Heading
18 | 


--------------------------------------------------------------------------------
/components/image.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Image as RebassImage, ImageProps } from 'rebass'
 3 | import merge from '../types/xtend'
 4 | import { SSG } from '@quercia/quercia'
 5 | 
 6 | import Skeleton from './skeleton'
 7 | 
 8 | const Image: React.FC = ({ src, alt, ...props }) => {
 9 |   const sx = Object.assign(
10 |     { borderRadius: 'lg', flexGrow: 0, flexShrink: 0 },
11 |     props.sx
12 |   )
13 | 
14 |   if (SSG) {
15 |     return 
16 |   }
17 | 
18 |   const [loaded, setLoaded] = React.useState(false)
19 |   const [hidden, setHidden] = React.useState(false)
20 | 
21 |   React.useEffect(() => {
22 |     // reset values when the src prop changes
23 |     setLoaded(false)
24 |     setHidden(false)
25 |   }, [src])
26 | 
27 |   return (
28 |     
29 |       
53 |   )
54 | }
55 | 
56 | export default Image
57 | 


--------------------------------------------------------------------------------
/components/input.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Input as RebassInput, InputProps } from '@rebass/forms'
 3 | import { keyframes } from '@emotion/css'
 4 | import merge from '../types/xtend'
 5 | 
 6 | const animation = keyframes({
 7 |   '100': {
 8 |     background: 'transparent',
 9 |     color: 'inherit'
10 |   }
11 | })
12 | 
13 | const Label: React.FC = React.forwardRef((props, ref) => (
14 |   
40 | ))
41 | 
42 | export default Label
43 | 


--------------------------------------------------------------------------------
/components/issue/comment.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Flex } from 'rebass'
 3 | 
 4 | import Link from '../link'
 5 | import Image from '../image'
 6 | import Container from '../base'
 7 | 
 8 | const Comment: React.FC<{ picture: string; name: string }> = ({
 9 |   children,
10 |   picture,
11 |   name
12 | }) => (
13 |   
14 |     
15 |       
16 |     
17 |     
18 |       {children}
19 |     
20 |   
21 | )
22 | 
23 | export default Comment
24 | 


--------------------------------------------------------------------------------
/components/issue/write.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from 'react'
 2 | import { Flex, Box } from 'rebass'
 3 | import { Textarea } from '@rebass/forms'
 4 | import { usePage } from '@quercia/quercia'
 5 | 
 6 | import Comment from './comment'
 7 | import Text from '../text'
 8 | import Divider from '../divider'
 9 | 
10 | const Write = React.forwardRef((_, ref) => {
11 |   const { account } = usePage()[1]
12 | 
13 |   return (
14 |     
15 |       
16 |         
17 |           {!account ? 'please sign in to comment' : 'about to comment'}:
18 |         
19 |       
20 |       
21 |