(require 'compile)
(require 'seq)
(require 'magit-process nil t)
(autoload 'ein:jupyter-running-notebook-directory "ein-jupyter")
(defconst ein:gat-status-cd 7 "gat exits 7 if requiring a change directory.")
(defcustom ein:gat-python-command (if (equal system-type 'windows-nt)
(or (executable-find "py")
(executable-find "pythonw")
"python")
"python")
"Python executable name."
:type (append '(choice)
(let (result)
(dolist (py '("python" "python3" "pythonw" "py") result)
(setq result (append result `((const :tag ,py ,py))))))
'((string :tag "Other")))
:group 'ein)
(defsubst ein:gat-shell-command (command)
(string-trim (shell-command-to-string (concat "2>/dev/null " command))))
(defcustom ein:gat-version
(ein:gat-shell-command "gat --project - --region - --zone - version")
"Currently, aws or gce."
:type 'string
:group 'ein)
(defconst ein:gat-required-version "0.0.4-pre")
(defvar ein:gat-machine-history nil
"History of user entered machine type.")
(defcustom ein:gat-vendor
(ein:gat-shell-command "gat --project - --region - --zone - vendor")
"Currently, aws or gce."
:type '(choice (const :tag "aws" "aws") (const :tag "gce" "gce"))
:group 'ein
:set (lambda (symbol value)
(setq ein:gat-machine-history nil)
(set-default symbol value)))
(defcustom ein:gat-gce-zone (ein:gat-shell-command "gcloud config get-value compute/zone")
"gcloud project zone."
:type 'string
:group 'ein)
(defcustom ein:gat-gce-region (ein:gat-shell-command "gcloud config get-value compute/region")
"gcloud project region."
:type 'string
:group 'ein)
(defcustom ein:gat-aws-region (ein:gat-shell-command "aws configure get region")
"gcloud project region."
:type 'string
:group 'ein)
(defcustom ein:gat-gce-project (ein:gat-shell-command "gcloud config get-value core/project")
"gcloud project id."
:type 'string
:group 'ein)
(defcustom ein:gat-aws-machine-types (split-string (ein:gat-shell-command "aws ec2 describe-instance-type-offerings --location-type=region --page-size=1000 --filter Name=location,Values=us-east-2 --query 'sort_by(InstanceTypeOfferings, &InstanceType)[].InstanceType' --output text"))
"gcloud machine types."
:type '(repeat string)
:group 'ein)
(defcustom ein:gat-gce-machine-types (split-string (ein:gat-shell-command (format "gcloud compute machine-types list --filter=\"zone:%s\" --format=\"value[terminator=' '](name)\"" ein:gat-gce-zone)))
"gcloud machine types."
:type '(repeat string)
:group 'ein)
(defcustom ein:gat-gpu-types (split-string "nvidia-tesla-t4 nvidia-tesla-v100")
"Gat gpu types."
:type '(repeat string)
:group 'ein)
(defcustom ein:gat-base-images '("dickmao/tensorflow-gpu"
"dickmao/scipy-gpu"
"dickmao/pytorch-gpu")
"Known https://hub.docker.com/u/jupyter images."
:type '(repeat (string :tag "FROM-appropriate docker image"))
:group 'ein)
(defvar ein:gat-previous-worktree nil)
(defvar ein:gat-urls nil)
(defconst ein:gat-master-worktree "master")
(defvar ein:gat-current-worktree ein:gat-master-worktree)
(defvar ein:gat-disksizegb-history '("default")
"History of user entered disk size.")
(defvar ein:gat-gpus-history '("0")
"History of user entered gpu count.")
(defvar ein:gat-gpu-type-history nil
"History of user entered gpu types.")
(defvar ein:gat-keyname-history nil
"History of user entered aws ssh keyname.")
(defvar ein:gat-preemptible-history nil
"History of preemptible opt-in.")
(defun ein:gat-where-am-i (&optional print-message)
(interactive "p")
(let ((from-end (cl-search "/.gat" default-directory :from-end)))
(cond ((and (string= major-mode "magit-process-mode")
(string-prefix-p "ein-gat:" (buffer-name)))
(aprog1 default-directory
(when print-message
(message it))))
((string= major-mode "ein:ipynb-mode")
(aprog1 (directory-file-name (file-name-directory (buffer-file-name)))
(when print-message
(message it))))
((file-directory-p
(concat (file-name-as-directory default-directory) ".gat"))
(aprog1 default-directory
(when print-message
(message it))))
(from-end
(aprog1 (file-name-as-directory
(cl-subseq default-directory 0 from-end))
(when print-message
(message it))))
(t
(if-let ((notebook-dir (ein:jupyter-running-notebook-directory))
(notebook (ein:get-notebook))
(where (directory-file-name
(concat (file-name-as-directory notebook-dir)
(file-name-directory (ein:$notebook-notebook-path notebook))))))
(aprog1 where
(when print-message
(message it)))
(prog1 nil
(when print-message
(message "nowhere"))))))))
(cl-defun ein:gat-jupyter-login (ipynb-name notebook-dir callback &rest args &key public-ip-address)
(if public-ip-address
(let ((url-or-port (ein:url (format "http://%s:8888" public-ip-address))))
(setf (alist-get (intern url-or-port) ein:gat-urls) notebook-dir)
(ein:login url-or-port
(lambda (buffer url-or-port)
(pop-to-buffer buffer)
(ein:notebook-open url-or-port ipynb-name nil callback))))
(ein:log 'error "ein:gat-jupyter-login: no public ip address")))
(defun ein:gat-process-filter (proc string)
"Copied `magit-process-filter' with added wrinkle of `ansi-color'.
Advising `insert' in `magit-process-filter' is a little sus, and
moreover, how would I avoid messing `magit-process-filter' of
other processes?"
(with-current-buffer (process-buffer proc)
(let ((inhibit-read-only t))
(goto-char (process-mark proc))
(when-let ((ret-pos (cl-position ?\r string :from-end t)))
(cl-callf substring string (1+ ret-pos))
(delete-region (line-beginning-position) (point)))
(insert (propertize (ansi-color-filter-apply string) 'magit-section
(process-get proc 'section)))
(set-marker (process-mark proc) (point)))))
(cl-defun ein:gat-chain (buffer callback exec &rest args &key public-ip-address notebook-dir &allow-other-keys)
(declare (indent 0))
(let* ((default-directory (or notebook-dir (ein:gat-where-am-i)))
(default-process-coding-system (magit--process-coding-system))
(magit-inhibit-refresh t)
(_ (awhen (getenv "GAT_APPLICATION_CREDENTIALS")
(push (concat "GOOGLE_APPLICATION_CREDENTIALS=" it) process-environment)))
(activate-with-editor-mode
(when (string= (car exec) with-editor-emacsclient-executable)
(lambda () (when (string= (buffer-name) (car (last exec)))
(with-editor-mode 1)))))
(process (let ((magit-buffer-name-format "%xein-gat%v: %t%x"))
(apply #'magit-start-process exec))))
(when activate-with-editor-mode
(add-hook 'find-file-hook activate-with-editor-mode))
(with-editor-set-process-filter process #'ein:gat-process-filter)
(set-process-sentinel
process
(lambda (proc event)
(let* ((gat-status (process-exit-status proc))
(process-buf (process-buffer proc))
(section (process-get proc 'section))
(gat-status-cd-p (= gat-status ein:gat-status-cd))
worktree-dir new-public-ip-address)
(when activate-with-editor-mode
(remove-hook 'find-file-hook activate-with-editor-mode))
(let ((magit-process-display-mode-line-error
(if gat-status-cd-p nil magit-process-display-mode-line-error))
(magit-process-raise-error
(if gat-status-cd-p nil magit-process-raise-error))
(short-circuit (lambda (&rest _args) (when gat-status-cd-p 0))))
(add-function :before-until (symbol-function 'process-exit-status)
short-circuit)
(unwind-protect
(magit-process-sentinel proc event)
(remove-function (symbol-function 'process-exit-status) short-circuit)))
(cond
((or (zerop gat-status) gat-status-cd-p)
(alet (and (bufferp process-buf)
(with-current-buffer process-buf
(when (integer-or-marker-p (oref section content))
(buffer-substring-no-properties (oref section content)
(oref section end)))))
(when it
(when gat-status-cd-p
(setq worktree-dir (when (string-match "^cd\\s-+\\(\\S-+\\)" it)
(string-trim (match-string 1 it)))))
(when-let ((last-line (car (last (split-string (string-trim it) "\n")))))
(setq new-public-ip-address
(when (string-match "^\\([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\\)" last-line)
(string-trim (match-string 1 last-line))))))
(when callback
(when (buffer-live-p buffer)
(set-buffer buffer))
(let ((magit-process-popup-time 0))
(apply callback
(append
(when worktree-dir
`(:worktree-dir ,worktree-dir))
(when-let ((address (or new-public-ip-address
public-ip-address)))
`(:public-ip-address ,address))))))))
(t
(ein:log 'error "ein:gat-chain: %s exited %s"
(car exec) (process-exit-status proc)))))))
process))
(defun ein:gat--path (archepath worktree-dir)
"Form relative path from ARCHEPATH root, WORKTREE-DIR subroot, ARCHEPATH leaf.
With WORKTREE-DIR of 3/4/1/2/.gat/fantab,
1/2/eager.ipynb -> 1/2/.gat/fantab/eager.ipynb
1/2/.gat/fubar/subdir/eager.ipynb -> 1/2/.gat/fantab/subdir/eager.ipynb
With WORKTREE-DIR of /home/dick/gat/test-repo2
.gat/getout/eager.ipynb -> eager.ipynb
"
(when-let ((root (directory-file-name (or (awhen (cl-search ".gat/" archepath :from-end)
(cl-subseq archepath 0 it))
(file-name-directory archepath)
""))))
(if (zerop (length root))
(concat (replace-regexp-in-string
"^\\./" ""
(file-name-as-directory
(cl-subseq worktree-dir
(or (cl-search ".gat/" worktree-dir :from-end)
(length worktree-dir)))))
(file-name-nondirectory archepath))
(concat (file-name-as-directory
(cl-subseq worktree-dir
(cl-search root worktree-dir :from-end)))
(or (awhen (string-match "\\(\\.gat/[^/]+/\\)" archepath)
(cl-subseq archepath (+ it (length (match-string 1 archepath)))))
(file-name-nondirectory archepath))))))
(defun ein:gat-zone ()
(interactive)
(cl-case (intern ein:gat-vendor)
(gce ein:gat-gce-zone)
(otherwise "-")))
(defun ein:gat-region ()
(interactive)
(cl-case (intern ein:gat-vendor)
(aws ein:gat-aws-region)
(gce ein:gat-gce-region)
(otherwise (or ein:gat-aws-region ein:gat-gce-region))))
(defun ein:gat-project ()
(interactive)
(cl-case (intern ein:gat-vendor)
(gce ein:gat-gce-project)
(otherwise "-")))
(defun ein:gat-machine-types ()
(interactive)
(cl-case (intern ein:gat-vendor)
(aws ein:gat-aws-machine-types)
(gce ein:gat-gce-machine-types)
(otherwise (or ein:gat-aws-machine-types ein:gat-gce-machine-types))))
(defsubst ein:gat-need-upgrade ()
(version-list-< (version-to-list ein:gat-version)
(version-to-list ein:gat-required-version)))
(defmacro ein:gat-install-gat (&rest body)
`(if (and (executable-find "gat")
(not (ein:gat-need-upgrade)))
(progn ,@body)
(if (zerop (length (ein:gat-region)))
(ein:log 'error "ein:gat-install-gat: no cloud utilities detected")
(ein:log 'info "ein:gat-install-gat: %s gat..."
(if (executable-find "gat") "Upgrading" "Installing"))
(let* ((orig-buf (current-buffer))
(bufname "*gat-install*")
(dir (make-temp-file "gat-install" t))
(commands `(,(format "cd %s" dir)
,(format "git clone --depth=1 --single-branch --branch=%s https://github.com/dickmao/gat.git" (if noninteractive "dev" ein:gat-required-version))
"make -C gat install"))
(compile (format "bash -ex -c '%s'" (mapconcat #'identity commands "; ")))
(callback (lambda (_buf msg)
(when (cl-search "finished" msg)
(with-current-buffer orig-buf
(custom-set-default
'ein:gat-version
(ein:gat-shell-command
"gat --project - --region - --zone - version"))
,@body)))))
(let ((compilation-scroll-output t))
(compilation-start compile nil (lambda (&rest _args) bufname)))
(with-current-buffer bufname
(add-hook 'compilation-finish-functions callback nil t))))))
(defun ein:gat-edit (&optional _refresh)
(interactive "P")
(ein:gat-install-gat
(if-let ((default-directory (ein:gat-where-am-i))
(gat-chain-args `("gat" nil "--project" ,(ein:gat-project)
"--region" ,(ein:gat-region) "--zone" ,(ein:gat-zone))))
(if (special-variable-p 'magit-process-popup-time)
(let ((magit-process-popup-time -1)
(notebook (ein:get-notebook)))
(ein:gat-chain
(current-buffer)
(cl-function
(lambda (&rest args &key worktree-dir &allow-other-keys)
(if notebook
(ein:notebook-open
(ein:$notebook-url-or-port notebook)
(ein:gat--path (ein:$notebook-notebook-path notebook)
worktree-dir)
(ein:$notebook-kernelspec notebook))
(cd worktree-dir))))
(append gat-chain-args
(list "edit"
(alet (ein:gat-elicit-worktree t)
(setq ein:gat-previous-worktree ein:gat-current-worktree)
(setq ein:gat-current-worktree it))))))
(error "ein:gat-create: magit not installed"))
(message "ein:gat-edit: not a notebook buffer"))))
(defun ein:gat-create (&optional _refresh)
(interactive "P")
(ein:gat-install-gat
(if-let ((default-directory (ein:gat-where-am-i))
(notebook (ein:get-notebook))
(gat-chain-args `("gat" nil "--project" ,(ein:gat-project)
"--region" ,(ein:gat-region) "--zone" " -")))
(if (special-variable-p 'magit-process-popup-time)
(let ((magit-process-popup-time 0))
(ein:gat-chain
(current-buffer)
(cl-function
(lambda (&rest args &key worktree-dir &allow-other-keys)
(ein:notebook-open
(ein:$notebook-url-or-port notebook)
(ein:gat--path (ein:$notebook-notebook-path notebook)
worktree-dir)
(ein:$notebook-kernelspec notebook))))
(append gat-chain-args
(list "create"
(alet (ein:gat-elicit-worktree nil)
(setq ein:gat-previous-worktree ein:gat-current-worktree)
(setq ein:gat-current-worktree it))))))
(error "ein:gat-create: magit not installed"))
(message "ein:gat-create: not a notebook buffer"))))
(defun ein:gat-run-local-batch (&optional refresh)
(interactive "P")
(ein:gat--run nil t refresh))
(defun ein:gat-run-local (&optional refresh)
(interactive "P")
(ein:gat--run nil nil refresh))
(defun ein:gat-run-remote-batch (&optional refresh)
(interactive "P")
(ein:gat--run t t refresh))
(defun ein:gat-run-remote (&optional refresh)
(interactive "P")
(ein:gat--run t nil refresh))
(defun ein:gat-hash-password (raw-password)
(let ((gat-hash-password-python
(format "%s - <<EOF
from notebook.auth import passwd
print(passwd('%s', 'sha1'))
EOF
" ein:gat-python-command raw-password)))
(ein:gat-shell-command gat-hash-password-python)))
(defun ein:gat-crib-password ()
(let* ((gat-crib-password-python
(format "%s - <<EOF
from traitlets.config.application import Application
from traitlets import Unicode
class NotebookApp(Application):
password = Unicode(u'', config=True,)
app = NotebookApp()
app.load_config_file('jupyter_notebook_config.py', '~/.jupyter')
print(app.password)
EOF
" ein:gat-python-command))
(config-dir
(elt (assoc-default
'config
(ein:json-read-from-string (ein:gat-shell-command "jupyter --paths --json")))
0))
(config-json (expand-file-name "jupyter_notebook_config.json" config-dir))
(config-py (expand-file-name "jupyter_notebook_config.py" config-dir))
result)
(when (file-exists-p config-py)
(setq result
(awhen (ein:gat-shell-command gat-crib-password-python)
(unless (zerop (length it)) it))))
(unless (stringp result)
(when (file-exists-p config-json)
(-let* (((&alist 'NotebookApp (&alist 'password))
(json-read-file config-json)))
(setq result password))))
result))
(defun ein:gat-kaggle-env (var json-key)
(when-let ((val (or (getenv var)
(let ((json (expand-file-name "kaggle.json" "~/.kaggle")))
(when (file-exists-p json)
(assoc-default json-key (json-read-file json)))))))
(format "--env %s=%s" var val)))
(defun ein:gat--run (remote-p batch-p refresh)
(unless with-editor-emacsclient-executable
(error "Could not determine emacsclient"))
(ein:gat-install-gat
(-if-let* ((ipynb-name
(if (string= major-mode "ein:ipynb-mode")
(file-name-nondirectory (buffer-file-name))
(awhen (aand (ein:get-notebook) (ein:$notebook-notebook-name it)) it)))
(callback
(if (string= major-mode "ein:ipynb-mode")
(apply-partially (lambda (buffer*
_notebook _created
&rest _args)
(when (buffer-live-p buffer*)
(kill-buffer-if-not-modified buffer*)))
(current-buffer))
#'ignore))
(default-directory (ein:gat-where-am-i))
(password (if (or batch-p (not remote-p))
""
(or (ein:gat-crib-password)
(let ((new-password
(read-passwd "Enter new password for remote server [none]: " t)))
(if (zerop (length new-password))
new-password
(let ((hashed (ein:gat-hash-password new-password)))
(if (cl-search ":" hashed)
hashed
(prog1 nil
(ein:log 'error "ein:gat--run: %s %s"
"Could not hash" new-password)))))))))
(gat-chain-args `("gat" nil
"--project" ,(ein:gat-project)
"--region" ,(ein:gat-region)
"--zone" ,(ein:gat-zone)))
(common-options (append '("--bespoke")
'("--user" "root")
'("--env" "GRANT_SUDO=1")
(awhen (ein:gat-kaggle-env "KAGGLE_USERNAME" 'username)
(split-string it))
(awhen (ein:gat-kaggle-env "KAGGLE_KEY" 'key)
(split-string it))
(awhen (ein:gat-kaggle-env "KAGGLE_NULL" 'null)
(split-string it))))
(gat-chain-run (if remote-p
(append '("run-remote")
common-options
`("--vendor" ,ein:gat-vendor)
`("--machine" ,(ein:gat-elicit-machine))
`(,@(when (string= (ein:gat-elicit-preemptible) "y")
(list "--spot")))
`(,@(awhen (ein:gat-elicit-disksizegb)
(list "--disksizegb"
(number-to-string it))))
`(,@(when (string= ein:gat-vendor "aws")
(list "--keyname"
(ein:gat-elicit-keyname))))
`(,@(-when-let* ((gce-p (string= ein:gat-vendor "gce"))
(gpus (ein:gat-elicit-gpus))
(nonzero (not (zerop gpus))))
(list "--gpus"
(number-to-string gpus)
"--gpu"
(ein:gat-elicit-gpu-type)))))
(append '("run-local") common-options)))
(now (truncate (float-time)))
(gat-log-exec (append gat-chain-args
(list "log" "--after" (format "%s" now)
"--vendor" ein:gat-vendor
"--until" "is running at:"
"--nextunit" "shutdown.service")))
(command (cond (batch-p
(format "start.sh jupyter nbconvert --ExecutePreprocessor.timeout=21600 --to notebook --execute %s" ipynb-name))
((zerop (length password))
(format "start-notebook.sh --NotebookApp.token=''"))
(t
(format "start-notebook.sh --NotebookApp.password='%s'" password))))
(last-known-buffer (current-buffer)))
(progn
(unless (or (file-directory-p
(concat (file-name-as-directory default-directory) ".gat"))
(member ".gat" (split-string default-directory "/")))
(let* ((command (format "gat --project %s --region %s --zone %s create"
(ein:gat-project) (ein:gat-region) (ein:gat-zone)))
(retcode (shell-command command)))
(unless (zerop retcode)
(error "ein:gat-where-am-i: \"%s\" exited with %d" command retcode))))
(cl-destructuring-bind (pre-docker . post-docker) (ein:gat-dockerfiles-state)
(if (or refresh (null pre-docker))
(if (fboundp 'magit-with-editor)
(magit-with-editor
(let* ((dockerfile (format "Dockerfile.%s" (file-name-sans-extension ipynb-name)))
(base-image (ein:gat-elicit-base-image))
(_ (with-temp-file dockerfile
(insert (format "FROM %s\nCOPY --chown=jovyan:users ./%s .\n"
base-image ipynb-name))))
(my-editor (when (and (boundp 'server-name)
(server-running-p server-name))
`("-s" ,server-name))))
(ein:gat-chain
last-known-buffer
(apply-partially
#'ein:gat-chain
last-known-buffer
(when remote-p
(apply-partially
#'ein:gat-chain
last-known-buffer
(unless batch-p
(apply-partially #'ein:gat-jupyter-login ipynb-name default-directory callback))
gat-log-exec))
(append gat-chain-args gat-chain-run (list "--dockerfile" dockerfile "--command" command)))
`(,with-editor-emacsclient-executable nil ,@my-editor ,dockerfile))))
(error "ein:gat--run: magit not installed"))
(if (special-variable-p 'magit-process-popup-time)
(let ((magit-process-popup-time 0))
(ein:gat-chain
last-known-buffer
(when remote-p
(apply-partially
#'ein:gat-chain
last-known-buffer
(unless batch-p
(apply-partially #'ein:gat-jupyter-login ipynb-name default-directory callback))
gat-log-exec))
(append gat-chain-args gat-chain-run (list "--dockerfile" pre-docker "--command" command))))
(error "ein:gat--run: magit not installed")))))
(message "ein:gat--run: aborting"))))
(defun ein:gat-elicit-base-image ()
"Using a defcustom as HIST is suspect but pithy."
(ein:completing-read
"FROM image: " ein:gat-base-images nil 'confirm
nil 'ein:gat-base-images (car ein:gat-base-images)))
(defun ein:gat-elicit-preemptible ()
(interactive)
(let ((kind (cl-case (intern ein:gat-vendor)
(gce "Preemptible")
(otherwise "Spot")))
(default (or (car ein:gat-preemptible-history) "n")))
(ein:completing-read
(format "%s [%s]: " kind default)
(split-string "y n")
nil t nil
'ein:gat-preemptible-history default)))
(defun ein:gat-elicit-keyname ()
(interactive)
(ein:completing-read
(format "Keyname%s: " (aif (car ein:gat-keyname-history)
(format " [%s]" it) ""))
nil nil nil nil
'ein:gat-keyname-history (car ein:gat-keyname-history)))
(defun ein:gat-elicit-machine ()
(interactive)
(let ((machine ""))
(while (zerop (length machine))
(setq machine (ein:completing-read
(format "Machine Type%s: " (aif (car ein:gat-machine-history)
(format " [%s]" it) ""))
(append (seq-uniq ein:gat-machine-history)
(seq-remove (lambda (x) (member x ein:gat-machine-history))
(cl-copy-list (ein:gat-machine-types))))
nil t nil 'ein:gat-machine-history
(car ein:gat-machine-history))))
machine))
(defun ein:gat-elicit-gpu-type ()
(interactive)
(let ((types ein:gat-gpu-types))
(ein:completing-read
(format "GPU%s: " (aif (car ein:gat-gpu-type-history)
(format " [%s]" it) ""))
(append (seq-uniq ein:gat-gpu-type-history)
(seq-remove (lambda (x) (member x ein:gat-gpu-type-history))
(cl-copy-list types)))
nil t nil 'ein:gat-gpu-type-history
(car (or ein:gat-gpu-type-history types)))))
(defun ein:gat-elicit-gpus ()
(interactive)
(cl-loop for answer =
(string-to-number
(ein:completing-read
(format "Number GPUs%s: "
(format " [%s]" (or (car ein:gat-gpus-history) "0")))
'("0") nil nil nil
'ein:gat-gpus-history (car ein:gat-gpus-history)))
until (>= answer 0)
finally return answer))
(defun ein:gat-elicit-worktree (extant)
(let ((already (split-string
(ein:gat-shell-command
(format "gat --project %s --region %s --zone %s list"
(ein:gat-project) (ein:gat-region) (ein:gat-zone))))))
(if extant
(ein:completing-read
"Experiment: " already nil t nil nil
ein:gat-previous-worktree)
(read-string "New experiment: "))))
(defun ein:gat-elicit-disksizegb ()
"Return nil for default [currently max(8, 6 + image size)]."
(interactive)
(cl-loop with answer
do (setq answer (ein:completing-read
(format "Disk GiB%s: "
(format " [%s]"
(or (car ein:gat-disksizegb-history)
"default")))
'("default") nil nil nil
'ein:gat-disksizegb-history
(car ein:gat-disksizegb-history)))
if (string= answer "default")
do (setq answer nil)
else
do (setq answer (string-to-number answer))
end
until (or (null answer) (> answer 0))
finally return answer))
(defun ein:gat-dockerfiles-state ()
"Return cons of (pre-Dockerfile . post-Dockerfile).
Pre-Dockerfile is Dockerfile.<notebook> if extant, else Dockerfile."
(-if-let* ((default-directory (ein:gat-where-am-i))
(notebook-name (cond ((string= major-mode "ein:ipynb-mode")
(file-name-nondirectory (buffer-file-name)))
(t
(aand (ein:get-notebook) (ein:$notebook-notebook-name it)))))
(dockers (directory-files (file-name-as-directory default-directory)
nil "^Dockerfile")))
(let* ((pre-docker-p (lambda (f) (or (string= f (format "Dockerfile.%s" (file-name-sans-extension notebook-name)))
(string= f "Dockerfile"))))
(pre-docker (seq-find pre-docker-p (sort (cl-copy-list dockers) #'string>)))
(post-docker-p (lambda (f) (string= f (format "%s.gat" pre-docker))))
(post-docker (and (stringp pre-docker) (seq-find post-docker-p (sort (cl-copy-list dockers) #'string>)))))
`(,pre-docker . ,post-docker))
'(nil)))
(provide 'ein-gat)