doomemacs/lisp/cli/upgrade.el
Henrik Lissner 615848e3a7
fix(cli): more future resistant 'doom upgrade'
In the future, should doom-core-dir or {doom-core-dir}/packages.el
change, 'doom upgrade' won't leave the repo in a broken state.
Unfortunately, this cannot retroactively prevent the issue; users will
experience this as soon as they update to b9933e6 or beyond, so users
will have to upgrade manually to overcome it:

  cd ~/.emacs.d
  git reset --hard 35a89bdfa6
  git pull origin master
  doom sync -u

Fix: #6598
Amend: b9933e6637
2022-07-31 00:58:36 +02:00

166 lines
7.1 KiB
EmacsLisp

;;; lisp/cli/upgrade.el -*- lexical-binding: t; -*-
;;; Commentary:
;;; Code:
(load! "packages")
(load! "compile")
;;
;;; Variables
(defvar doom-upgrade-url "https://github.com/doomemacs/doomemacs"
"The git repo url for Doom Emacs.")
(defvar doom-upgrade-remote "_upgrade"
"The name to use as our staging remote.")
;;
;;; Commands
(defcli! ((upgrade up))
((packages? ("-p" "--packages") "Only upgrade packages, not Doom")
(jobs ("-j" "--jobs" num) "How many CPUs to use for native compilation")
&context context)
"Updates Doom and packages.
This requires that ~/.emacs.d is a git repo, and is the equivalent of the
following shell commands:
cd ~/.emacs.d
git pull --rebase
doom clean
doom sync -u"
(let* ((force? (doom-cli-context-suppress-prompts-p context))
(sync-cmd (append '("sync" "-u") (if jobs `("-j" ,num)))))
(cond
(packages?
;; HACK It's messy to use straight to upgrade straight, due to the
;; potential for backwards incompatibility, so we staticly check if
;; Doom's `package!' declaration for straight has changed. If it has,
;; delete straight so 'doom sync' will install the new version for us.
;;
;; Clumsy, but a better solution is in the works.
(let ((recipe (doom-cli-context-get context 'straight-recipe)))
(when (and recipe (not (equal recipe (doom-upgrade--get-straight-recipe))))
(print! (item "Preparing straight for an update"))
(delete-directory (doom-path straight-base-dir "straight/repos/straight.el")
'recursive)))
(call! sync-cmd)
(print! (success "Finished upgrading Doom Emacs")))
((doom-cli-upgrade context force? force?)
;; Reload Doom's CLI & libraries, in case there were any upstream changes.
;; Major changes will still break, however
(print! (item "Reloading Doom Emacs"))
(exit! "doom" "upgrade" "-p"
(if force? "--force")
(if jobs (format "--jobs=%d" jobs))))
((print! "Doom is up-to-date!")
(call! sync-cmd)))))
;;
;;; Helpers
(defun doom-cli-upgrade (context &optional auto-accept-p force-p)
"Upgrade Doom to the latest version non-destructively."
(let ((default-directory doom-emacs-dir)
process-file-side-effects)
(print! (start "Preparing to upgrade Doom Emacs and its packages..."))
(let* (;; git name-rev may return BRANCH~X for detached HEADs and fully
;; qualified refs in some other cases, so an effort to strip out all
;; but the branch name is necessary. git symbolic-ref (or
;; `vc-git--symbolic-ref') won't work; it can't deal with submodules.
(branch (replace-regexp-in-string
"^\\(?:[^/]+/[^/]+/\\)?\\(.+\\)\\(?:~[0-9]+\\)?$" "\\1"
(cdr (sh! "git" "name-rev" "--name-only" "HEAD"))))
(target-remote (format "%s_%s" doom-upgrade-remote branch)))
(unless branch
(error (if (file-exists-p! ".git" doom-emacs-dir)
"Couldn't find Doom's .git directory. Was Doom cloned properly?"
"Couldn't detect what branch you're on. Is Doom detached?")))
;; We assume that a dirty .emacs.d is intentional and abort
(when-let (dirty (doom-upgrade--working-tree-dirty-p default-directory))
(if (not force-p)
(user-error "%s\n\n%s\n\n %s"
(format "Refusing to upgrade because %S has been modified."
(abbreviate-file-name doom-emacs-dir))
"Either stash/undo your changes or run 'doom upgrade -f' to discard local changes."
(string-join dirty "\n"))
(print! (item "You have local modifications in Doom's source. Discarding them..."))
(sh! "git" "reset" "--hard" (format "origin/%s" branch))
(sh! "git" "clean" "-ffd")))
(sh! "git" "remote" "remove" doom-upgrade-remote)
(unwind-protect
(let (result)
(or (zerop (car (sh! "git" "remote" "add" doom-upgrade-remote doom-upgrade-url)))
(error "Failed to add %s to remotes" doom-upgrade-remote))
(or (zerop (car (setq result (sh! "git" "fetch" "--force" "--tags" doom-upgrade-remote (format "%s:%s" branch target-remote)))))
(error "Failed to fetch from upstream"))
(let ((this-rev (cdr (sh! "git" "rev-parse" "HEAD")))
(new-rev (cdr (sh! "git" "rev-parse" target-remote))))
(cond
((and (null this-rev)
(null new-rev))
(error "Failed to get revisions for %s" target-remote))
((equal this-rev new-rev)
(print! (success "Doom is already up-to-date!"))
nil)
((print! (item "A new version of Doom Emacs is available!\n\n Old revision: %s (%s)\n New revision: %s (%s)\n"
(substring this-rev 0 10)
(cdr (sh! "git" "log" "-1" "--format=%cr" "HEAD"))
(substring new-rev 0 10)
(cdr (sh! "git" "log" "-1" "--format=%cr" target-remote))))
(let ((diff-url
(format "%s/compare/%s...%s"
doom-upgrade-url
this-rev
new-rev)))
(print! "Link to diff: %s" diff-url)
(when (and (not auto-accept-p)
(y-or-n-p "View the comparison diff in your browser?"))
(print! (item "Opened github in your browser."))
(browse-url diff-url)))
(if (not (or auto-accept-p
(y-or-n-p "Proceed with upgrade?")))
(ignore (print! (error "Aborted")))
(print! (start "Upgrading Doom Emacs..."))
(print-group!
(doom-compile-clean)
(doom-cli-context-put context 'straight-recipe (doom-upgrade--get-straight-recipe))
(or (and (zerop (car (sh! "git" "reset" "--hard" target-remote)))
(equal (cdr (sh! "git" "rev-parse" "HEAD")) new-rev))
(error "Failed to check out %s" (substring new-rev 0 10)))))))))
(ignore-errors
(sh! "git" "branch" "-D" target-remote)
(sh! "git" "remote" "remove" doom-upgrade-remote))))))
(defun doom-upgrade--working-tree-dirty-p (dir)
(cl-destructuring-bind (success . stdout)
(doom-call-process "git" "status" "--porcelain" "-uno")
(if (= 0 success)
(split-string stdout "\n" t)
(error "Failed to check working tree in %s" dir))))
(defun doom-upgrade--get-straight-recipe ()
(with-temp-buffer
(insert-file-contents (doom-path doom-core-dir "packages.el"))
(when (re-search-forward "(package! straight" nil t)
(goto-char (match-beginning 0))
(let ((sexp (sexp-at-point)))
(plist-put sexp :recipe
(eval (plist-get sexp :recipe)
t))))))
;;; upgrade.el ends here