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 | (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) () (display-page (:title "profile") (:form :action "/logout" :method "post" (:input :type "submit" :value "logout")))) (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/src" :method :get) (path) (display-page (:title (format nil "~A/~A" user repo)) (: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) (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/src?path=~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" *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)))) |