(in-package #:candle-cli) (defgeneric execute-command (command args)) (defun error-and-exit (str &rest args) (apply #'format *error-output* str args) (sb-ext:exit :code 1)) (defmethod execute-command (command args) (error-and-exit "Unknown command '~(~A~)'. See 'candle --help'.~%" command)) (defun job-info->line (job-info) (format nil "~A:~A (~A) ~A" (first job-info) (subseq (second job-info) 0 8) (format nil "~{~2,,,'0@A/~2,,,'0@A/~A ~2,,,'0@A:~2,,,'0@A~}" (utils:time-as-list (fourth job-info) :month :date :year :hr :min)) (case (third job-info) (:succeeded (format nil "~c[1;32mPassed~c[0m" #\Esc #\Esc)) (:failed (format nil "~c[1;31mFailed~c[0m" #\Esc #\Esc)) (:queued "In queue") (:no-candle-file "No candle file present") (:in-progress "In progress")))) (defmacro standard-cli (cmd options-in args usage remaining-args-required &rest success) `(multiple-value-bind (parsed-options remaining-args error) (opera:process-arguments ,options-in ,args) (cond ((opera:option-present :help parsed-options) (format t "~A" ,(if (eql usage :default) `(opera:usage ,cmd ,options-in) usage))) ((eql error :unknown-option) (error-and-exit "Unknown option: ~A. See '~A --help'.~%" (car remaining-args) ,cmd)) ((eql error :required-argument-missing) (error-and-exit "Missing argument for ~A. See '~A --help'.~%" (car remaining-args) ,cmd)) ((and ,remaining-args-required (not remaining-args)) (error-and-exit "~A required. See 'candle --help'.~%" ,remaining-args-required)) (t ,@success)))) ;;; Section for ./candle (defun run () (standard-cli "candle" (main-options) (cdr sb-ext:*posix-argv*) (main-usage) "Command" (handler-case (if (and (opera:option-present :port parsed-options) (not (parse-integer (opera:option-argument :port parsed-options) :junk-allowed t))) (error-and-exit "--port requires a number. See 'candle -h'~%") (let ((communication:*query-port* (or (and (opera:option-present :port parsed-options) (parse-integer (opera:option-argument :port parsed-options) :junk-allowed t)) 25004))) (execute-command (intern (string-upcase (car remaining-args)) :keyword) (cdr remaining-args)))) (candle:candle-error (e) (case (candle:candle-error-reason e) (:project-does-not-exist (error-and-exit "Project does not exist~%")) (:job-does-not-exist (error-and-exit "Job does not exist~%")) (:invalid-project-name (error-and-exit "Project name invalid~%")) (:invalid-project-uri (error-and-exit "Project uri invalid~%")) (:project-name-taken (error-and-exit "Project name already taken~%")) (:project-failed-to-get-branches (error-and-exit "Unable to retrieve branches from server~%")) (t (error-and-exit "Unknown error occurred: ~(~S~)~%" (candle:candle-error-reason e)))))))) (defun main-options () '((:name :help :short "h" :long "help" :description "Print this usage.") (:name :port :short "p" :long "port" :takes-argument t :variable-name "PORT" :description "Port on which to listen for commands. Defaults to 25004") (:positional "" :required t :description "Command for candle, see below"))) (defun main-usage () (opera:usage "candle" (main-options) "Interacts with candle server. The available commands are: project Interact with projects job Get information about jobs run Local command. Run candle in the current working directory")) ;;; Section for ./candle project (defmethod execute-command ((command (eql :project)) args) (standard-cli "candle project" (project-options) args (project-usage) nil (let ((subcommand (intern (string-upcase (car remaining-args)) :keyword))) (case subcommand (:delete (delete-project (cdr remaining-args))) (:add (add-project (cdr remaining-args))) (:show (show-project (cdr remaining-args))) (:refresh (refresh-project (cdr remaining-args))) (:list (list-projects)) (:failures (project-failures (cdr remaining-args))) (t (format t "~A" (project-usage))))))) (defun project-usage () (opera:usage "candle project" (project-options) "Interacts with projects. The available project subcommands are: list List all projects add : Add a project delete Delete a project show Show project branch information refresh Tell the candle server to refresh the project information")) (defun project-options () '((:name :help :short "h" :long "help" :description "Print this usage.") (:positional "" :description "Project subcommand, see below."))) (defun add-project (args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.") (:positional ":" :description " is the name of the project, which must be alphanumeric (hyphens are allowed), while is the location of the repository for cloning. This location must be accessible by the machine running candle.")))) (standard-cli "candle project add" options args :default ":" (let* ((project-definition (car remaining-args)) (pos (position #\: project-definition))) (cond ((not pos) (error-and-exit "Project definition ~A is not valid. See 'candle project add --help'.~%" project-definition)) (t (let* ((name (subseq project-definition 0 pos)) (src (subseq project-definition (1+ pos)))) (communication:query `(candle:add-project ,name ,src)) (format t "Added project ~A at src definition ~A~%" name src)))))))) (defun delete-project (args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.") (:positional "" :description " is the name of the project to delete")))) (standard-cli "candle project delete" options args :default "" (communication:query `(candle:delete-project ,(car remaining-args))) (format t "Removed project ~A~%" (car remaining-args))))) (defun show-project (args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.") (:positional "" :description " is the name of the project to show")))) (standard-cli "candle project show" options args :default "" (let* ((branch-infos (communication:query `(candle:project-branch-information ,(car remaining-args)))) (width (apply #'max (mapcar #'length (mapcar #'car branch-infos))))) (mapcar (lambda (branch-info) (format t (format nil "~~~A@A: ~~A~~%" width) (first branch-info) (job-info->line (second branch-info)))) (sort branch-infos #'< :key (lambda (branch-info) (fourth (second branch-info))))))))) (defun refresh-project (args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.") (:positional "" :description " is the name of the project to refresh")))) (standard-cli "candle project refresh" options args :default "" (communication:query `(candle:refresh-project ,(car remaining-args))) (format t "Refreshed project ~A~%" (car remaining-args))))) (defun list-projects () (format t "~{~A~%~}" (mapcar (lambda (info) (format nil "~A ~A~A" (car info) (cadr info) (if (zerop (caddr info)) "" (format nil " (~A branches ~c[1;31mfailing~c[0m)" (caddr info) #\Esc #\Esc)))) (communication:query `(candle:list-projects))))) (defun project-failures (args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.") (:name :project :long "project" :variable-name "PROJECT" :takes-argument t :description "Restrict failures to project named by PROJECT")))) (standard-cli "candle project failures" options args :default nil (format t "~A" (communication:query `(candle:failures ,(when (opera:option-present :project parsed-options) (opera:option-argument :project parsed-options)))))))) ;;; Section for ./candle job (defmethod execute-command ((command (eql :job)) args) (standard-cli "candle job" (job-options) args (job-usage) nil (let ((subcommand (intern (string-upcase (car remaining-args)) :keyword))) (case subcommand (:list (job-list (cdr remaining-args))) (:log (job-log (cdr remaining-args))) (:retry (retry-job (cdr remaining-args))) (t (format t "~A" (job-usage))))))) (defun job-options () '((:name :help :short "h" :long "help" :description "Print this usage.") (:positional "" :description "Job subcommand, see below."))) (defun job-usage () (opera:usage "candle job" (project-options) "Interacts with projects. The available project subcommands are: list List jobs log : View the log for a job retry : Retry a job")) (defun job-list (args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.") (:name :project :long "project" :variable-name "PROJECT" :takes-argument t :description "Restrict jobs to project named by PROJECT")))) (standard-cli "candle job list" options args :default nil (format t "~{~A~%~}" (mapcar #'job-info->line (sort (communication:query `(candle:project-job-information ,(opera:option-argument :project parsed-options))) #'< :key #'fourth)))))) (defun decompose-job-definition (job-definition) (let ((pos (position #\: job-definition))) (when pos (values (subseq job-definition 0 pos) (subseq job-definition (1+ pos)))))) (defun job-log (args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.") (:positional ":" :description " is the name of the project, while is the sha of the job in question.")))) (standard-cli "candle job log" options args :default ":" (multiple-value-bind (project-name sha) (decompose-job-definition (car remaining-args)) (if project-name (format t "~A" (communication:query `(candle:get-job-log ,project-name ,sha))) (error-and-exit "Job definition ~A is not valid. See 'candle job log --help'.~%" (car remaining-args))))))) (defun retry-job (args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.") (:positional ":" :description " is the name of the project, while is the sha of the job in question.")))) (standard-cli "candle job retry" options args :default ":" (multiple-value-bind (project-name sha) (decompose-job-definition (car remaining-args)) (if project-name (progn (communication:query `(candle:retry-job ,project-name ,sha)) (format t "Retrying job ~A~%" (car remaining-args))) (error-and-exit "Job definition ~A is not valid. See 'candle job log --help'.~%" (car remaining-args))))))) ;;; Section for ./candle run (defmethod execute-command ((command (eql :run)) args) (let ((options '((:name :help :short "h" :long "help" :description "Print this usage.")))) (standard-cli "run" options args :default nil (when (not (candle:run)) (sb-ext:exit :code 1)))))