b5fc2980a651690d4387877da815fe325c400aad
[candle] / src / main / cli.lisp
1 (in-package #:candle-cli)
2
3 (defgeneric execute-command (command args))
4
5 (defun error-and-exit (str &rest args)
6  (apply #'format *error-output* str args)
7  (sb-ext:exit :code 1))
8
9 (defmethod execute-command (command args)
10  (error-and-exit "Unknown command '~(~A~)'.  See 'candle --help'.~%" command))
11
12 (defun job-info->line (job-info)
13  (format nil "~A (~A) ~A"
14   (subseq (first job-info) 0 8)
15   (format nil "~{~2,,,'0@A/~2,,,'0@A/~A ~2,,,'0@A:~2,,,'0@A~}"
16    (utils:time-as-list (third job-info) :month :date :year :hr :min))
17   (case (second job-info)
18    (:succeeded (format nil "~c[1;32mPassed~c[0m" #\Esc #\Esc))
19    (:failed (format nil "~c[1;31mFailed~c[0m" #\Esc #\Esc))
20    (:queued "In queue")
21    (:no-candle-file "No candle file present")
22    (:in-progress "In progress"))))
23
24 (defmacro standard-cli (cmd options-in args usage remaining-args-required &rest success)
25 `(multiple-value-bind (parsed-options remaining-args error) (opera:process-arguments ,options-in ,args)
26   (cond
27    ((opera:option-present :help parsed-options)
28     (format t "~A" ,(if (eql usage :default) `(opera:usage ,cmd ,options-in) usage)))
29    ((eql error :unknown-option)
30     (error-and-exit "Unknown option: ~A.  See '~A --help'.~%" (car remaining-args) ,cmd))
31    ((eql error :required-argument-missing)
32     (error-and-exit "Missing argument for ~A.  See '~A --help'.~%" (car remaining-args) ,cmd))
33    ((and ,remaining-args-required (not remaining-args))
34     (error-and-exit "~A required.  See 'candle --help'.~%" ,remaining-args-required))
35    (t
36     ,@success))))
37
38 ;;; Section for ./candle
39
40 (defun run ()
41  (standard-cli "candle" (main-options) (cdr sb-ext:*posix-argv*) (main-usage) "Command"
42   (handler-case
43    (if
44     (and (opera:option-present :port parsed-options) (not (parse-integer (opera:option-argument :port parsed-options) :junk-allowed t)))
45     (error-and-exit "--port requires a number.  See 'candle -h'~%")
46     (let
47      ((communication:*query-port*
48        (or
49         (and
50          (opera:option-present :port parsed-options)
51          (parse-integer (opera:option-argument :port parsed-options) :junk-allowed t))
52         25004)))
53      (execute-command (intern (string-upcase (car remaining-args)) :keyword) (cdr remaining-args))))
54    (candle:candle-error (e)
55     (case (candle:candle-error-reason e)
56      (:project-does-not-exist (error-and-exit "Project does not exist~%"))
57      (:job-does-not-exist (error-and-exit "Job does not exist~%"))
58      (:invalid-project-name (error-and-exit "Project name invalid~%"))
59      (:invalid-project-uri (error-and-exit "Project uri invalid~%"))
60      (:project-name-taken (error-and-exit "Project name already taken~%"))
61      (:project-failed-to-get-branches (error-and-exit "Unable to retrieve branches from server~%"))
62      (t (error-and-exit "Unknown error occurred: ~(~S~)~%" (candle:candle-error-reason e))))))))
63
64 (defun main-options ()
65  '((:name :help :short "h" :long "help" :description "Print this usage.")
66    (:name :port :short "p" :long "port" :takes-argument t :variable-name "PORT"
67     :description "Port on which to listen for commands.  Defaults to 25004")
68    (:positional "<command>" :required t :description "Command for candle, see below")))
69
70 (defun main-usage ()
71  (opera:usage
72   "candle"
73   (main-options)
74   "Interacts with candle server.  The available commands are:
75  project   Interact with projects
76  job       Get information about jobs
77  run       Local command.  Run candle in the current working directory"))
78
79 ;;; Section for ./candle project
80
81 (defmethod execute-command ((command (eql :project)) args)
82  (standard-cli "candle project" (project-options) args (project-usage) nil
83   (let
84    ((subcommand (intern (string-upcase (car remaining-args)) :keyword)))
85    (case subcommand
86     (:delete (delete-project (cdr remaining-args)))
87     (:add (add-project (cdr remaining-args)))
88     (:show (show-project (cdr remaining-args)))
89     (:refresh (refresh-project (cdr remaining-args)))
90     (:list (list-projects))
91     (:failures (project-failures (cdr remaining-args)))
92     (t (format t "~A" (project-usage)))))))
93
94 (defun project-usage ()
95  (opera:usage
96   "candle project"
97   (project-options)
98   "Interacts with projects.  The available project subcommands are:
99  list              List all projects
100  add <name>:<src>  Add a project
101  delete <name>     Delete a project
102  show <name>       Show project branch information
103  refresh <name>    Tell the candle server to refresh the project information"))
104
105 (defun project-options ()
106  '((:name :help :short "h" :long "help" :description "Print this usage.")
107    (:positional "<subcommand>" :description "Project subcommand, see below.")))
108
109 (defun add-project (args)
110  (let
111   ((options
112    '((:name :help :short "h" :long "help" :description "Print this usage.")
113      (:positional "<name>:<src>" :description "<name> is the name of the project, which must be alphanumeric (hyphens are allowed), while <src> is the location of the repository for cloning.  This location must be accessible by the machine running candle."))))
114   (standard-cli "candle project add" options args :default "<name>:<src>"
115    (let*
116     ((project-definition (car remaining-args))
117      (pos (position #\: project-definition)))
118     (cond
119      ((not pos) (error-and-exit "Project definition ~A is not valid.  See 'candle project add --help'.~%" project-definition))
120      (t
121       (let*
122        ((name (subseq project-definition 0 pos))
123         (src (subseq project-definition (1+ pos))))
124        (communication:query `(candle:add-project ,name ,src))
125        (format t "Added project ~A at src definition ~A~%" name src))))))))
126
127 (defun delete-project (args)
128  (let
129   ((options
130    '((:name :help :short "h" :long "help" :description "Print this usage.")
131      (:positional "<name>" :description "<name> is the name of the project to delete"))))
132   (standard-cli "candle project delete" options args :default "<name>"
133     (communication:query `(candle:delete-project ,(car remaining-args)))
134     (format t "Removed project ~A~%" (car remaining-args)))))
135
136 (defun show-project (args)
137  (let
138   ((options
139    '((:name :help :short "h" :long "help" :description "Print this usage.")
140      (:positional "<name>" :description "<name> is the name of the project to show"))))
141   (standard-cli "candle project show" options args :default "<name>"
142    (let*
143     ((branch-infos (communication:query `(candle:project-branch-information ,(car remaining-args))))
144      (width (apply #'max (mapcar #'length (mapcar #'car branch-infos)))))
145     (mapcar
146      (lambda (branch-info)
147       (format t (format nil "~~~A@A: ~~A~~%" width)
148        (first branch-info)
149        (job-info->line (second branch-info))))
150      (sort branch-infos #'< :key (lambda (branch-info) (third (second branch-info)))))))))
151
152 (defun refresh-project (args)
153  (let
154   ((options
155    '((:name :help :short "h" :long "help" :description "Print this usage.")
156      (:positional "<name>" :description "<name> is the name of the project to refresh"))))
157   (standard-cli "candle project refresh" options args :default "<name>"
158    (communication:query `(candle:refresh-project ,(car remaining-args)))
159    (format t "Refreshed project ~A~%" (car remaining-args)))))
160
161 (defun list-projects ()
162  (format t "~{~A~%~}"
163   (mapcar
164    (lambda (info)
165     (format nil "~A  ~A~A"
166      (car info)
167      (cadr info)
168      (if (zerop (caddr info)) "" (format nil " (~A branches ~c[1;31mfailing~c[0m)" (caddr info) #\Esc #\Esc))))
169    (communication:query `(candle:list-projects)))))
170
171 (defun project-failures (args)
172  (let
173   ((options
174    '((:name :help :short "h" :long "help" :description "Print this usage.")
175      (:name :project :long "project" :variable-name "PROJECT" :takes-argument t :description "Restrict failures to project named by PROJECT"))))
176   (standard-cli "candle project failures" options args :default nil
177    (format t "~A"
178     (communication:query
179      `(candle:failures ,(when (opera:option-present :project parsed-options) (opera:option-argument :project parsed-options))))))))
180
181 ;;; Section for ./candle job
182
183 (defmethod execute-command ((command (eql :job)) args)
184  (standard-cli "candle job" (job-options) args (job-usage) nil
185   (let
186    ((subcommand (intern (string-upcase (car remaining-args)) :keyword)))
187    (case subcommand
188     (:list (job-list (cdr remaining-args)))
189     (:log (job-log (cdr remaining-args)))
190     (:retry (retry-job (cdr remaining-args)))
191     (t (format t "~A" (job-usage)))))))
192
193 (defun job-options ()
194  '((:name :help :short "h" :long "help" :description "Print this usage.")
195    (:positional "<subcommand>" :description "Job subcommand, see below.")))
196
197 (defun job-usage ()
198  (opera:usage
199   "candle job"
200   (project-options)
201   "Interacts with projects.  The available project subcommands are:
202  list                   List jobs
203  log <project>:<sha>    View the log for a job
204  retry <project>:<sha>  Retry a job"))
205
206 (defun job-list (args)
207  (let
208   ((options
209    '((:name :help :short "h" :long "help" :description "Print this usage.")
210      (:name :project :long "project" :variable-name "PROJECT" :takes-argument t :description "Restrict jobs to project named by PROJECT"))))
211   (standard-cli "candle job list" options args :default nil
212    (format t "~{~A~%~}"
213     (mapcar
214      #'job-info->line
215      (sort (communication:query `(candle:project-job-information ,(opera:option-argument :project parsed-options))) #'< :key #'third))))))
216
217 (defun decompose-job-definition (job-definition)
218  (let
219   ((pos (position #\: job-definition)))
220   (when
221    pos
222    (values
223     (subseq job-definition 0 pos)
224     (subseq job-definition (1+ pos))))))
225
226 (defun job-log (args)
227  (let
228   ((options
229    '((:name :help :short "h" :long "help" :description "Print this usage.")
230      (:positional "<project>:<sha>" :description "<project> is the name of the project, while <sha> is the sha of the job in question."))))
231   (standard-cli "candle job log" options args :default "<project>:<sha>"
232    (multiple-value-bind (project-name sha) (decompose-job-definition (car remaining-args))
233     (if project-name
234      (format t "~A" (communication:query `(candle:get-job-log ,project-name ,sha)))
235      (error-and-exit "Job definition ~A is not valid.  See 'candle job log --help'.~%" (car remaining-args)))))))
236
237 (defun retry-job (args)
238  (let
239   ((options
240    '((:name :help :short "h" :long "help" :description "Print this usage.")
241      (:positional "<project>:<sha>" :description "<project> is the name of the project, while <sha> is the sha of the job in question."))))
242   (standard-cli "candle job retry" options args :default "<project>:<sha>"
243    (multiple-value-bind (project-name sha) (decompose-job-definition (car remaining-args))
244     (if project-name
245      (progn
246       (communication:query `(candle:retry-job ,project-name ,sha))
247       (format t "Retrying job ~A~%" (car remaining-args)))
248      (error-and-exit "Job definition ~A is not valid.  See 'candle job log --help'.~%" (car remaining-args)))))))
249
250 ;;; Section for ./candle run
251
252 (defmethod execute-command ((command (eql :run)) args)
253  (let
254   ((options '((:name :help :short "h" :long "help" :description "Print this usage."))))
255   (standard-cli "run" options args :default nil
256    (when (not (candle:run)) (sb-ext:exit :code 1)))))