1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
| (in-package #:gitbrn)
(defmacro display-page ((&key title) &body body)
`(spinneret:with-html-string
(:doctype)
(:html
(:head
(:title
(if ,title
(format nil "gitbrn.com | ~A" ,title)
"gitbrn.com"))
(:link :type "text/css"
:rel "stylesheet"
:href "/static/assets/style.css"))
(:body
(:div :class "container" (navbar))
(:div :class "container mt16" ,@body))
)))
(easy-routes:defroute index ("/" :method :get) ()
(display-page (:title "index")
(:div
(:div "repositories")
(:hr)
(:table
(:tbody
(:tr
(:td :class "repo-name" (:a :href "/mcksp/gitbrn" "gitbrn"))
(:td "friendly git forge")))))))
(defvar *db* (sqlite:connect (merge-pathnames #p"proj/gitbrn/sql.db" (user-homedir-pathname))))
(easy-routes:defroute discover ("/discover" :method :get) ()
(display-page (:title "discover")
(:div "wip 🚧")))
(easy-routes:defroute register ("/register" :method :get) ()
(display-page (:title "register")
(:div
(:form :action "/register" :method "post"
(:input :type "text" :name "username")
(:input :type "password" :name "password")
(:input :type "submit" :value "register")))))
(easy-routes:defroute create-user ("/register" :method :post) (&post username password)
(let ((user-id (insert-user username password)))
(if user-id
(progn
(hunchentoot:set-cookie "session" :value (save-session user-id))
(hunchentoot:redirect "/"))
(hunchentoot:redirect "/register"))))
(defun insert-user (username password)
(let ((hash (bcrypt:encode (bcrypt:make-password password :cost 14)))
(user-id (uuid-str)))
(handler-case
(progn
(sqlite:execute-non-query *db*
"insert into users (id, username, hash_pass, created_at)
values (?, ?, ?, datetime())" user-id username hash)
user-id)
(error (e) nil))))
(easy-routes:defroute profile ("/profile" :method :get) ()
(let ((query (sqlite:execute-to-list *db* "select user_id, key from ssh_keys where user_id = ?" (user-id *current-user*))))
(display-page (:title "profile")
(:div
(:form :action "/ssh" :method "post"
(:input :type "text" :name "key" :value
(format nil "~A" (if (null query) "" (second (first query)))))
(:input :type "submit" :value "submit"))
(:form :action "/logout" :method "post"
(:input :type "submit" :value "logout"))))))
(easy-routes:defroute create-ssh ("/ssh" :method :post) (&post key)
(insert-key key)
(hunchentoot:redirect "/profile"))
(defun insert-key(key)
(let ((query (sqlite:execute-to-list *db* "select user_id, key from ssh_keys where user_id = ?" (user-id *current-user*))))
(if (null query)
(sqlite:execute-non-query *db*
"insert into ssh_keys (id, user_id, key, created_at)
values (?, ?, ?, datetime())" (uuid-str) (user-id *current-user*) key)
(sqlite:execute-non-query *db*
"update ssh_keys set key = ? where user_id = ?" key (user-id *current-user*)))))
(easy-routes:defroute logout ("/logout" :method :post) ()
(progn
;; TODO delete session from db
(hunchentoot:set-cookie "session" :value nil)
(hunchentoot:redirect "/")))
(defun save-session (user-id)
(let ((session (uuid-str)))
(progn
(sqlite:execute-non-query *db* "insert into sessions (token, user_id) values (?, ?)" session user-id)
session)))
(easy-routes:defroute login ("/login" :method :get) ()
(display-page (:title "login")
(:form :action "/login" :method "post"
(:input :type "text" :name "username")
(:input :type "password" :name "password")
(:input :type "submit" :value "login"))))
(easy-routes:defroute login-user ("/login" :method :post) (&post username password)
(let ((hash (sqlite:execute-single *db* "select hash_pass from users where username = ?" username))
(user-id (sqlite:execute-single *db* "select id from users where username = ?" username)))
;; TODO query for user_id and hash in one query
(if (bcrypt:password= password hash)
(progn
(hunchentoot:set-cookie "session" :value (save-session user-id))
(hunchentoot:redirect "/"))
(hunchentoot:redirect "/login"))))
(easy-routes:defroute repo ("/:user/:repo" :method :get) ()
(display-page (:title (format nil "~A/~A" user repo))
(:div
(repo-header user repo)
(repo-listing "")
(:div :class "pt8")
(:hr)
(:pre (git-to-string (tree-path "readme"))))))
(easy-routes:defroute repo-file ("/:user/:repo/tree/*path-list" :method :get) ()
(let ((path (format nil "~{~A~^/~}" path-list)))
(display-page (:title (format nil "~A/~A:~A" user repo path))
(:div
(repo-header user repo)
(:hr)
(repo-listing path)))))
(defun repo-header (user repo)
(spinneret:with-html
(:div :class "pb8"
(:a :href (format nil "/~A/~A" user repo)
(:span user)(:span "/")(:b repo))
(:span " - chill git forge"))))
(defun repo-listing (path)
(spinneret:with-html
(let ((tree (tree-path path)))
(if (is-dir tree)
(:div :class "gaps-y" (dolist (file (sort-tree (tree-path path)))
(file-item file)))
(if (cl-git:binary-p tree)
(:span "this is binary file")
(let ((txt (git-to-string tree)))
(:table (:tbody (:tr
(:td :class "line-nums"
(:code (loop for line-num from 1 to (length (str:lines txt))
do (:div (:a :id (format nil "L~a" line-num)
:href (format nil "#L~a" line-num)
(format nil "~a~%" line-num))))))
(:td :class "file-content" (:code (:pre txt))))))))))))
(defun git-to-string (tree)
(if (null tree)
"file not found"
(flexi-streams:octets-to-string (cl-git:blob-content tree))))
(defun filename (path)
(let ((file (file-namestring path)))
(if (str:empty? file)
(first (last (pathname-directory path)))
file)))
(defun file-item (file)
(spinneret:with-html
(:div :class "file-item"
(if (is-dir file)
(:img :class "icon pr8" :src "/static/assets/dir.png")
(:img :class "icon pr8" :src "/static/assets/text.png"))
(:a :href (format nil "/mcksp/gitbrn/tree/~a" (cl-git:filename file))
(:span (format nil "~A" (filename (cl-git:filename file))))))))
(defun is-dir (tree)
(or (typep tree 'cl-git:tree-tree) (typep tree 'cl-git:tree)))
(defun sort-tree (tree)
(stable-sort (cl-git:tree-directory tree) 'compare-tree-obj))
(defun compare-tree-obj (a b)
(let ((dira (is-dir a))
(dirb (is-dir b)))
(and (not dirb) dira)))
(defun repo-tree ()
(cl-git:commit-tree
(cl-git:target
(cl-git:repository-head
(cl-git:open-repository
(merge-pathnames #p"proj/gitbrn" (user-homedir-pathname)))))))
(defun tree-dirs (tree dirs)
(if (not dirs)
tree
(tree-dirs (tree-dir tree (first dirs)) (cdr dirs))))
(defun tree-dir (tree dir)
(first (cl-git:tree-directory tree (pathname (format nil "~a/" dir)))))
(defun tree-file (tree file)
(first (remove-if
(lambda (obj) (not (exact-match obj file)))
(cl-git:tree-directory tree (pathname (format nil "~a" file))))))
(defun tree-path (path)
(let ((dir (tree-dirs (repo-tree) (cdr (pathname-directory path))))
(filename (file-namestring path)))
(if (str:empty? filename)
dir
(tree-file dir filename))))
(defun exact-match (obj file)
(let ((objname (file-namestring (cl-git:filename obj))))
(string= objname file)))
(defun navbar ()
(spinneret:with-html (:div :id "navbar"
(:div
(:a :href "/" "gitbrn.com"))
(:div :class "gaps-x b"
(:a :href "/" "repos")
(:a :href "/discover" "discover")
(if *current-user*
(:a :href "/profile" (user-username *current-user*))
(:span (:a :href "/login" "login")
(:a :href "/register" "register")))))))
;; TODO figure out how to return list of elements to apply to div
(defun uuid-str ()
(string-downcase (format nil "~w" (uuid:make-v4-uuid))))
|