|
|
- ;;; magit-wip.el --- commit snapshots to work-in-progress refs -*- lexical-binding: t -*-
-
- ;; Copyright (C) 2010-2019 The Magit Project Contributors
- ;;
- ;; You should have received a copy of the AUTHORS.md file which
- ;; lists all contributors. If not, see http://magit.vc/authors.
-
- ;; Author: Jonas Bernoulli <jonas@bernoul.li>
- ;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
-
- ;; Magit is free software; you can redistribute it and/or modify it
- ;; under the terms of the GNU General Public License as published by
- ;; the Free Software Foundation; either version 3, or (at your option)
- ;; any later version.
- ;;
- ;; Magit is distributed in the hope that it will be useful, but WITHOUT
- ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
- ;; License for more details.
- ;;
- ;; You should have received a copy of the GNU General Public License
- ;; along with Magit. If not, see http://www.gnu.org/licenses.
-
- ;;; Commentary:
-
- ;; This library defines tree global modes which automatically commit
- ;; snapshots to branch-specific work-in-progress refs before and after
- ;; making changes, and two commands which can be used to do so on
- ;; demand.
-
- ;;; Code:
-
- (eval-when-compile
- (require 'subr-x))
-
- (require 'magit-core)
- (require 'magit-log)
-
- ;;; Options
-
- (defgroup magit-wip nil
- "Automatically commit to work-in-progress refs."
- :link '(info-link "(magit)Wip Modes")
- :group 'magit-modes
- :group 'magit-essentials)
-
- (defgroup magit-wip-legacy nil
- "It is better to not use these modes individually."
- :link '(info-link "(magit)Legacy Wip Modes")
- :group 'magit-wip)
-
- (defcustom magit-wip-mode-lighter " Wip"
- "Lighter for Magit-Wip mode."
- :package-version '(magit . "2.90.0")
- :group 'magit-wip
- :type 'string)
-
- (defcustom magit-wip-after-save-local-mode-lighter ""
- "Lighter for Magit-Wip-After-Save-Local mode."
- :package-version '(magit . "2.1.0")
- :group 'magit-wip-legacy
- :type 'string)
-
- (defcustom magit-wip-after-apply-mode-lighter ""
- "Lighter for Magit-Wip-After-Apply mode."
- :package-version '(magit . "2.1.0")
- :group 'magit-wip-legacy
- :type 'string)
-
- (defcustom magit-wip-before-change-mode-lighter ""
- "Lighter for Magit-Wip-Before-Change mode."
- :package-version '(magit . "2.1.0")
- :group 'magit-wip-legacy
- :type 'string)
-
- (defcustom magit-wip-initial-backup-mode-lighter ""
- "Lighter for Magit-Wip-Initial Backup mode."
- :package-version '(magit . "2.1.0")
- :group 'magit-wip-legacy
- :type 'string)
-
- (defcustom magit-wip-merge-branch nil
- "Whether to merge the current branch into its wip ref.
-
- If non-nil and the current branch has new commits, then it is
- merged into the wip ref before creating a new wip commit. This
- makes it easier to inspect wip history and the wip commits are
- never garbage collected.
-
- If nil and the current branch has new commits, then the wip ref
- is reset to the tip of the branch before creating a new wip
- commit. With this setting wip commits are eventually garbage
- collected. This is currently the default."
- :package-version '(magit . "2.90.0")
- :group 'magit-wip
- :type 'boolean)
-
- (defcustom magit-wip-namespace "refs/wip/"
- "Namespace used for work-in-progress refs.
- The wip refs are named \"<namespace/>index/<branchref>\"
- and \"<namespace/>wtree/<branchref>\". When snapshots
- are created while the `HEAD' is detached then \"HEAD\"
- is used as `branch-ref'."
- :package-version '(magit . "2.1.0")
- :group 'magit-wip
- :type 'string)
-
- ;;; Modes
-
- (define-minor-mode magit-wip-mode
- "Save uncommitted changes to work-in-progress refs.
-
- Whenever appropriate (i.e. when dataloss would be a possibility
- otherwise) this mode causes uncommitted changes to be committed
- to dedicated work-in-progress refs.
-
- For historic reasons this mode is implemented on top of four
- other `magit-wip-*' modes, which can also be used individually,
- if you want finer control over when the wip refs are updated;
- but that is discouraged."
- :package-version '(magit . "2.90.0")
- :lighter magit-wip-mode-lighter
- :global t
- (let ((arg (if magit-wip-mode 1 -1)))
- (magit-wip-after-save-mode arg)
- (magit-wip-after-apply-mode arg)
- (magit-wip-before-change-mode arg)
- (magit-wip-initial-backup-mode arg)))
-
- (define-minor-mode magit-wip-after-save-local-mode
- "After saving, also commit to a worktree work-in-progress ref.
-
- After saving the current file-visiting buffer this mode also
- commits the changes to the worktree work-in-progress ref for
- the current branch.
-
- This mode should be enabled globally by turning on the globalized
- variant `magit-wip-after-save-mode'."
- :package-version '(magit . "2.1.0")
- :lighter magit-wip-after-save-local-mode-lighter
- (if magit-wip-after-save-local-mode
- (if (and buffer-file-name (magit-inside-worktree-p t))
- (add-hook 'after-save-hook 'magit-wip-commit-buffer-file t t)
- (setq magit-wip-after-save-local-mode nil)
- (user-error "Need a worktree and a file"))
- (remove-hook 'after-save-hook 'magit-wip-commit-buffer-file t)))
-
- (defun magit-wip-after-save-local-mode-turn-on ()
- (and buffer-file-name
- (magit-inside-worktree-p t)
- (magit-file-tracked-p buffer-file-name)
- (magit-wip-after-save-local-mode)))
-
- ;;;###autoload
- (define-globalized-minor-mode magit-wip-after-save-mode
- magit-wip-after-save-local-mode magit-wip-after-save-local-mode-turn-on
- :package-version '(magit . "2.1.0")
- :group 'magit-wip)
-
- (defun magit-wip-commit-buffer-file (&optional msg)
- "Commit visited file to a worktree work-in-progress ref.
-
- Also see `magit-wip-after-save-mode' which calls this function
- automatically whenever a buffer visiting a tracked file is saved."
- (interactive)
- (--when-let (magit-wip-get-ref)
- (magit-with-toplevel
- (let ((file (file-relative-name buffer-file-name)))
- (magit-wip-commit-worktree
- it (list file)
- (format (cond (msg)
- ((called-interactively-p 'any)
- "wip-save %s after save")
- (t
- "autosave %s after save"))
- file))))))
-
- ;;;###autoload
- (define-minor-mode magit-wip-after-apply-mode
- "Commit to work-in-progress refs.
-
- After applying a change using any \"apply variant\"
- command (apply, stage, unstage, discard, and reverse) commit the
- affected files to the current wip refs. For each branch there
- may be two wip refs; one contains snapshots of the files as found
- in the worktree and the other contains snapshots of the entries
- in the index."
- :package-version '(magit . "2.1.0")
- :group 'magit-wip
- :lighter magit-wip-after-apply-mode-lighter
- :global t)
-
- (defun magit-wip-commit-after-apply (&optional files msg)
- (when magit-wip-after-apply-mode
- (magit-wip-commit files msg)))
-
- ;;;###autoload
- (define-minor-mode magit-wip-before-change-mode
- "Commit to work-in-progress refs before certain destructive changes.
-
- Before invoking a revert command or an \"apply variant\"
- command (apply, stage, unstage, discard, and reverse) commit the
- affected tracked files to the current wip refs. For each branch
- there may be two wip refs; one contains snapshots of the files
- as found in the worktree and the other contains snapshots of the
- entries in the index.
-
- Only changes to files which could potentially be affected by the
- command which is about to be called are committed."
- :package-version '(magit . "2.1.0")
- :group 'magit-wip
- :lighter magit-wip-before-change-mode-lighter
- :global t)
-
- (defun magit-wip-commit-before-change (&optional files msg)
- (when magit-wip-before-change-mode
- (magit-with-toplevel
- (magit-wip-commit files msg))))
-
- (define-minor-mode magit-wip-initial-backup-mode
- "Before saving a buffer for the first time, commit to a wip ref."
- :package-version '(magit . "2.90.0")
- :group 'magit-wip
- :lighter magit-wip-initial-backup-mode-lighter
- :global t
- (if magit-wip-initial-backup-mode
- (add-hook 'before-save-hook 'magit-wip-commit-initial-backup)
- (remove-hook 'before-save-hook 'magit-wip-commit-initial-backup)))
-
- (defvar-local magit-wip-buffer-backed-up nil)
- (put 'magit-wip-buffer-backed-up 'permanent-local t)
-
- ;;;###autoload
- (defun magit-wip-commit-initial-backup ()
- "Before saving, commit current file to a worktree wip ref.
-
- The user has to add this function to `before-save-hook'.
-
- Commit the current state of the visited file before saving the
- current buffer to that file. This backs up the same version of
- the file as `backup-buffer' would, but stores the backup in the
- worktree wip ref, which is also used by the various Magit Wip
- modes, instead of in a backup file as `backup-buffer' would.
-
- This function ignores the variables that affect `backup-buffer'
- and can be used along-side that function, which is recommended
- because this function only backs up files that are tracked in
- a Git repository."
- (when (and (not magit-wip-buffer-backed-up)
- buffer-file-name
- (magit-inside-worktree-p t)
- (magit-file-tracked-p buffer-file-name))
- (let ((magit-save-repository-buffers nil))
- (magit-wip-commit-buffer-file "autosave %s before save"))
- (setq magit-wip-buffer-backed-up t)))
-
- ;;; Core
-
- (defun magit-wip-commit (&optional files msg)
- "Commit all tracked files to the work-in-progress refs.
-
- Interactively, commit all changes to all tracked files using
- a generic commit message. With a prefix-argument the commit
- message is read in the minibuffer.
-
- Non-interactively, only commit changes to FILES using MSG as
- commit message."
- (interactive (list nil (if current-prefix-arg
- (magit-read-string "Wip commit message")
- "wip-save tracked files")))
- (--when-let (magit-wip-get-ref)
- (magit-wip-commit-index it files msg)
- (magit-wip-commit-worktree it files msg)))
-
- (defun magit-wip-commit-index (ref files msg)
- (let* ((wipref (magit--wip-index-ref ref))
- (parent (magit-wip-get-parent ref wipref))
- (tree (magit-git-string "write-tree")))
- (magit-wip-update-wipref ref wipref tree parent files msg "index")))
-
- (defun magit-wip-commit-worktree (ref files msg)
- (let* ((wipref (magit--wip-wtree-ref ref))
- (parent (magit-wip-get-parent ref wipref))
- (tree (magit-with-temp-index parent "--reset"
- (if files
- (magit-call-git "add" "--" files)
- (magit-with-toplevel
- (magit-call-git "add" "-u" ".")))
- (magit-git-string "write-tree"))))
- (magit-wip-update-wipref ref wipref tree parent files msg "worktree")))
-
- (defun magit-wip-update-wipref (ref wipref tree parent files msg start-msg)
- (cond
- ((and (not (equal parent wipref))
- (or (not magit-wip-merge-branch)
- (not (magit-rev-verify wipref))))
- (setq start-msg (concat "start autosaving " start-msg))
- (magit-update-ref wipref start-msg
- (magit-git-string "commit-tree" "--no-gpg-sign"
- "-p" parent "-m" start-msg
- (concat parent "^{tree}")))
- (setq parent wipref))
- ((and magit-wip-merge-branch
- (or (not (magit-rev-ancestor-p ref wipref))
- (not (magit-rev-ancestor-p
- (concat (magit-git-string "log" "--format=%H"
- "-1" "--merges" wipref)
- "^2")
- ref))))
- (setq start-msg (format "merge %s into %s" ref start-msg))
- (magit-update-ref wipref start-msg
- (magit-git-string "commit-tree" "--no-gpg-sign"
- "-p" wipref "-p" ref
- "-m" start-msg
- (concat ref "^{tree}")))
- (setq parent wipref)))
- (when (magit-git-failure "diff-tree" "--quiet" parent tree "--" files)
- (unless (and msg (not (= (aref msg 0) ?\s)))
- (let ((len (length files)))
- (setq msg (concat
- (cond ((= len 0) "autosave tracked files")
- ((> len 1) (format "autosave %s files" len))
- (t (concat "autosave "
- (file-relative-name (car files)
- (magit-toplevel)))))
- msg))))
- (magit-update-ref wipref msg
- (magit-git-string "commit-tree" "--no-gpg-sign"
- "-p" parent "-m" msg tree))))
-
- (defun magit-wip-get-ref ()
- (let ((ref (or (magit-git-string "symbolic-ref" "HEAD") "HEAD")))
- (and (magit-rev-verify ref)
- ref)))
-
- (defun magit-wip-get-parent (ref wipref)
- (if (and (magit-rev-verify wipref)
- (equal (magit-git-string "merge-base" wipref ref)
- (magit-rev-verify ref)))
- wipref
- ref))
-
- (defun magit--wip-index-ref (&optional ref)
- (magit--wip-ref "index/" ref))
-
- (defun magit--wip-wtree-ref (&optional ref)
- (magit--wip-ref "wtree/" ref))
-
- (defun magit--wip-ref (namespace &optional ref)
- (concat magit-wip-namespace namespace
- (or (and ref (string-prefix-p "refs/" ref) ref)
- (when-let ((branch (or ref (magit-get-current-branch))))
- (concat "refs/heads/" branch))
- "HEAD")))
-
- (defun magit-wip-maybe-add-commit-hook ()
- (when (and magit-wip-merge-branch
- (magit-wip-any-enabled-p))
- (add-hook 'git-commit-post-finish-hook 'magit-wip-commit nil t)))
-
- (defun magit-wip-any-enabled-p ()
- (or magit-wip-mode
- magit-wip-after-save-local-mode
- magit-wip-after-save-mode
- magit-wip-after-apply-mode
- magit-wip-before-change-mode
- magit-wip-initial-backup-mode))
-
- ;;; Log
-
- (defun magit-wip-log-index (args files)
- "Show log for the index wip ref of the current branch."
- (interactive (magit-log-arguments))
- (magit-log-setup-buffer (list (magit--wip-index-ref)) args files))
-
- (defun magit-wip-log-worktree (args files)
- "Show log for the worktree wip ref of the current branch."
- (interactive (magit-log-arguments))
- (magit-log-setup-buffer (list (magit--wip-wtree-ref)) args files))
-
- (defun magit-wip-log-current (branch args files count)
- "Show log for the current branch and its wip refs.
- With a negative prefix argument only show the worktree wip ref.
- The absolute numeric value of the prefix argument controls how
- many \"branches\" of each wip ref are shown."
- (interactive
- (nconc (list (or (magit-get-current-branch) "HEAD"))
- (magit-log-arguments)
- (list (prefix-numeric-value current-prefix-arg))))
- (magit-wip-log branch args files count))
-
- (defun magit-wip-log (branch args files count)
- "Show log for a branch and its wip refs.
- With a negative prefix argument only show the worktree wip ref.
- The absolute numeric value of the prefix argument controls how
- many \"branches\" of each wip ref are shown."
- (interactive
- (nconc (list (magit-completing-read
- "Log branch and its wip refs"
- (-snoc (magit-list-local-branch-names) "HEAD")
- nil t nil 'magit-revision-history
- (or (magit-branch-at-point)
- (magit-get-current-branch)
- "HEAD")))
- (magit-log-arguments)
- (list (prefix-numeric-value current-prefix-arg))))
- (magit-log-setup-buffer (nconc (list branch)
- (magit-wip-log-get-tips
- (magit--wip-wtree-ref branch)
- (abs count))
- (and (>= count 0)
- (magit-wip-log-get-tips
- (magit--wip-index-ref branch)
- (abs count))))
- args files))
-
- (defun magit-wip-log-get-tips (wipref count)
- (when-let ((reflog (magit-git-lines "reflog" wipref)))
- (let (tips)
- (while (and reflog (> count 1))
- (setq reflog (cl-member "^[^ ]+ [^:]+: restart autosaving"
- reflog :test #'string-match-p))
- (when (and (cadr reflog)
- (string-match "^[^ ]+ \\([^:]+\\)" (cadr reflog)))
- (push (match-string 1 (cadr reflog)) tips))
- (setq reflog (cddr reflog))
- (cl-decf count))
- (cons wipref (nreverse tips)))))
-
- ;;; _
- (provide 'magit-wip)
- ;;; magit-wip.el ends here
|