git.mcksp
    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))))