| 1 | ;;; magit-extras.el --- Additional functionality for Magit -*- lexical-binding:t -*- |
| 2 | |
| 3 | ;; Copyright (C) 2008-2026 The Magit Project Contributors |
| 4 | |
| 5 | ;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> |
| 6 | ;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev> |
| 7 | |
| 8 | ;; SPDX-License-Identifier: GPL-3.0-or-later |
| 9 | |
| 10 | ;; Magit is free software: you can redistribute it and/or modify it |
| 11 | ;; under the terms of the GNU General Public License as published by |
| 12 | ;; the Free Software Foundation, either version 3 of the License, or |
| 13 | ;; (at your option) any later version. |
| 14 | ;; |
| 15 | ;; Magit is distributed in the hope that it will be useful, but WITHOUT |
| 16 | ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
| 17 | ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public |
| 18 | ;; License for more details. |
| 19 | ;; |
| 20 | ;; You should have received a copy of the GNU General Public License |
| 21 | ;; along with Magit. If not, see <https://www.gnu.org/licenses/>. |
| 22 | |
| 23 | ;;; Commentary: |
| 24 | |
| 25 | ;; Additional functionality for Magit. |
| 26 | |
| 27 | ;;; Code: |
| 28 | |
| 29 | (require 'magit) |
| 30 | |
| 31 | ;; For `magit-project-status'. |
| 32 | (declare-function vc-git-command "vc-git" |
| 33 | (buffer okstatus file-or-list &rest flags)) |
| 34 | |
| 35 | (defvar project-prefix-map) |
| 36 | (defvar project-switch-commands) |
| 37 | |
| 38 | (defgroup magit-extras nil |
| 39 | "Additional functionality for Magit." |
| 40 | :group 'magit-extensions) |
| 41 | |
| 42 | ;;; Git Tools |
| 43 | ;;;; Git-Mergetool |
| 44 | |
| 45 | ;;;###autoload(autoload 'magit-git-mergetool "magit-extras" nil t) |
| 46 | (transient-define-prefix magit-git-mergetool (file args &optional transient) |
| 47 | "Resolve conflicts in FILE using \"git mergetool --gui\". |
| 48 | With a prefix argument allow changing ARGS using a transient |
| 49 | popup. See info node `(magit) Ediffing' for information about |
| 50 | alternative commands." |
| 51 | :man-page "git-mergetool" |
| 52 | ["Settings" |
| 53 | ("-t" magit-git-mergetool:--tool) |
| 54 | ("=t" magit-merge.guitool) |
| 55 | ("=T" magit-merge.tool) |
| 56 | ("-r" magit-mergetool.hideResolved) |
| 57 | ("-b" magit-mergetool.keepBackup) |
| 58 | ("-k" magit-mergetool.keepTemporaries) |
| 59 | ("-w" magit-mergetool.writeToTemp)] |
| 60 | ["Actions" |
| 61 | (" m" "Invoke mergetool" magit-git-mergetool)] |
| 62 | (interactive |
| 63 | (if (and (not (eq transient-current-command 'magit-git-mergetool)) |
| 64 | current-prefix-arg) |
| 65 | (list nil nil t) |
| 66 | (list (magit-read-unmerged-file "Resolve") |
| 67 | (transient-args 'magit-git-mergetool)))) |
| 68 | (if transient |
| 69 | (transient-setup 'magit-git-mergetool) |
| 70 | (magit-run-git-async "mergetool" "--gui" args "--" file))) |
| 71 | |
| 72 | (transient-define-infix magit-git-mergetool:--tool () |
| 73 | :description "Override mergetool" |
| 74 | :class 'transient-option |
| 75 | :shortarg "-t" |
| 76 | :argument "--tool=" |
| 77 | :reader #'magit--read-mergetool) |
| 78 | |
| 79 | (transient-define-infix magit-merge.guitool () |
| 80 | :class 'magit--git-variable |
| 81 | :variable "merge.guitool" |
| 82 | :global t |
| 83 | :reader #'magit--read-mergetool) |
| 84 | |
| 85 | (transient-define-infix magit-merge.tool () |
| 86 | :class 'magit--git-variable |
| 87 | :variable "merge.tool" |
| 88 | :global t |
| 89 | :reader #'magit--read-mergetool) |
| 90 | |
| 91 | (defun magit--read-mergetool (prompt _initial-input history) |
| 92 | (let ((choices nil) |
| 93 | (lines (cdr (magit-git-lines "mergetool" "--tool-help")))) |
| 94 | (while (string-prefix-p "\t\t" (car lines)) |
| 95 | (push (substring (pop lines) 2) choices)) |
| 96 | (setq choices (nreverse choices)) |
| 97 | (magit-completing-read (or prompt "Select mergetool") |
| 98 | choices nil t nil history))) |
| 99 | |
| 100 | (transient-define-infix magit-mergetool.hideResolved () |
| 101 | :class 'magit--git-variable:boolean |
| 102 | :variable "mergetool.hideResolved" |
| 103 | :default "false" |
| 104 | :global t) |
| 105 | |
| 106 | (transient-define-infix magit-mergetool.keepBackup () |
| 107 | :class 'magit--git-variable:boolean |
| 108 | :variable "mergetool.keepBackup" |
| 109 | :default "true" |
| 110 | :global t) |
| 111 | |
| 112 | (transient-define-infix magit-mergetool.keepTemporaries () |
| 113 | :class 'magit--git-variable:boolean |
| 114 | :variable "mergetool.keepTemporaries" |
| 115 | :default "false" |
| 116 | :global t) |
| 117 | |
| 118 | (transient-define-infix magit-mergetool.writeToTemp () |
| 119 | :class 'magit--git-variable:boolean |
| 120 | :variable "mergetool.writeToTemp" |
| 121 | :default "false" |
| 122 | :global t) |
| 123 | |
| 124 | ;;;; Git-Gui |
| 125 | |
| 126 | ;;;###autoload |
| 127 | (defun magit-run-git-gui-blame (commit filename &optional linenum) |
| 128 | "Run \"git gui blame\" on the given FILENAME and COMMIT. |
| 129 | Interactively run it for the current file and the `HEAD', with a |
| 130 | prefix or when the current file cannot be determined let the user |
| 131 | choose. When the current buffer is visiting FILENAME instruct |
| 132 | blame to center around the line point is on." |
| 133 | (interactive |
| 134 | (let (revision filename) |
| 135 | (when (or current-prefix-arg |
| 136 | (progn |
| 137 | (setq revision "HEAD") |
| 138 | (not (setq filename (magit-file-relative-name nil 'tracked))))) |
| 139 | (setq revision (magit-read-branch-or-commit "Blame from revision")) |
| 140 | (setq filename (magit-read-file-from-rev revision "Blame file"))) |
| 141 | (list revision filename |
| 142 | (and (equal filename |
| 143 | (ignore-errors |
| 144 | (magit-file-relative-name buffer-file-name))) |
| 145 | (line-number-at-pos))))) |
| 146 | (magit-with-toplevel |
| 147 | (magit-process-git 0 "gui" "blame" |
| 148 | (and linenum (list (format "--line=%d" linenum))) |
| 149 | commit |
| 150 | filename))) |
| 151 | |
| 152 | ;;;; Gitk |
| 153 | |
| 154 | (defcustom magit-gitk-executable |
| 155 | (or (and (eq system-type 'windows-nt) |
| 156 | (let ((exe (magit-git-string |
| 157 | "-c" "alias.X=!x() { which \"$1\" | cygpath -mf -; }; x" |
| 158 | "X" "gitk.exe"))) |
| 159 | (and exe (file-executable-p exe) exe))) |
| 160 | (executable-find "gitk") "gitk") |
| 161 | "The Gitk executable." |
| 162 | :group 'magit-extras |
| 163 | :set-after '(magit-git-executable) |
| 164 | :type 'string) |
| 165 | |
| 166 | ;;;###autoload |
| 167 | (defun magit-run-git-gui () |
| 168 | "Run \"git gui\" for the current git repository." |
| 169 | (interactive) |
| 170 | (magit-with-toplevel (magit-process-git 0 "gui"))) |
| 171 | |
| 172 | ;;;###autoload |
| 173 | (defun magit-run-gitk () |
| 174 | "Run `gitk' in the current repository." |
| 175 | (interactive) |
| 176 | (magit-process-file magit-gitk-executable nil 0)) |
| 177 | |
| 178 | ;;;###autoload |
| 179 | (defun magit-run-gitk-branches () |
| 180 | "Run `gitk --branches' in the current repository." |
| 181 | (interactive) |
| 182 | (magit-process-file magit-gitk-executable nil 0 nil "--branches")) |
| 183 | |
| 184 | ;;;###autoload |
| 185 | (defun magit-run-gitk-all () |
| 186 | "Run `gitk --all' in the current repository." |
| 187 | (interactive) |
| 188 | (magit-process-file magit-gitk-executable nil 0 nil "--all")) |
| 189 | |
| 190 | ;;; Emacs Tools |
| 191 | |
| 192 | ;;;###autoload |
| 193 | (defun magit-project-status () |
| 194 | "Run `magit-status' in the current project's root. |
| 195 | |
| 196 | To teach `project-switch-project' about this command, you have to |
| 197 | add something like this to your configuration: |
| 198 | |
| 199 | (keymap-set project-prefix-map \"m\" #\\='magit-project-status) |
| 200 | (add-to-list \\='project-switch-commands |
| 201 | \\='(magit-project-status \"Magit\") t) |
| 202 | |
| 203 | Also see `magit-project-dispatch'." |
| 204 | (interactive) |
| 205 | (if (fboundp 'project-root) |
| 206 | (magit-status-setup-buffer (project-root (project-current t))) |
| 207 | (user-error "`magit-project-status' requires `project' 0.3.0 or greater"))) |
| 208 | |
| 209 | ;;;###autoload |
| 210 | (defun magit-project-dispatch () |
| 211 | "Run `magit-dispatch' in the current project's root. |
| 212 | |
| 213 | Note that for `magit-dispatch' to operate in the selected project, |
| 214 | the current buffer's `default-directory' must be located in the |
| 215 | selected repository. To achieve that, a Dired buffer is created. |
| 216 | |
| 217 | To teach `project-switch-project' about this command, you have to |
| 218 | add something like this to your configuration: |
| 219 | |
| 220 | (keymap-set project-prefix-map \"M\" #\\='magit-project-dispatch) |
| 221 | (add-to-list \\='project-switch-commands |
| 222 | \\='(magit-project-dispatch \"Magit Dispatch\") t) |
| 223 | |
| 224 | Also see `magit-project-status'." |
| 225 | (interactive) |
| 226 | (if (fboundp 'project-root) |
| 227 | (let ((dir (project-root (project-current t)))) |
| 228 | (find-file dir) |
| 229 | (transient-setup 'magit-dispatch)) |
| 230 | (user-error "`magit-project-status' requires `project' 0.3.0 or greater"))) |
| 231 | |
| 232 | ;;; Shift Selection |
| 233 | |
| 234 | (defun magit--turn-on-shift-select-mode-p () |
| 235 | (and shift-select-mode |
| 236 | this-command-keys-shift-translated |
| 237 | (not mark-active) |
| 238 | (not (eq (car-safe transient-mark-mode) 'only)))) |
| 239 | |
| 240 | ;;;###autoload |
| 241 | (defun magit-previous-line (&optional arg try-vscroll) |
| 242 | "Like `previous-line' but with Magit-specific shift-selection. |
| 243 | |
| 244 | Magit's selection mechanism is based on the region but selects an |
| 245 | area that is larger than the region. This causes `previous-line' |
| 246 | when invoked while holding the shift key to move up one line and |
| 247 | thereby select two lines. When invoked inside a hunk body this |
| 248 | command does not move point on the first invocation and thereby |
| 249 | it only selects a single line. Which inconsistency you prefer |
| 250 | is a matter of preference." |
| 251 | (declare (interactive-only |
| 252 | "use `forward-line' with negative argument instead.")) |
| 253 | (interactive "p\np") |
| 254 | (unless arg (setq arg 1)) |
| 255 | (let ((stay (or (magit-diff-inside-hunk-body-p) |
| 256 | (magit-section-position-in-heading-p)))) |
| 257 | (if (and stay (= arg 1) (magit--turn-on-shift-select-mode-p)) |
| 258 | (push-mark nil nil t) |
| 259 | (with-no-warnings |
| 260 | (handle-shift-selection) |
| 261 | (previous-line (if stay (max (1- arg) 1) arg) try-vscroll))))) |
| 262 | |
| 263 | ;;;###autoload |
| 264 | (defun magit-next-line (&optional arg try-vscroll) |
| 265 | "Like `next-line' but with Magit-specific shift-selection. |
| 266 | |
| 267 | Magit's selection mechanism is based on the region but selects |
| 268 | an area that is larger than the region. This causes `next-line' |
| 269 | when invoked while holding the shift key to move down one line |
| 270 | and thereby select two lines. When invoked inside a hunk body |
| 271 | this command does not move point on the first invocation and |
| 272 | thereby it only selects a single line. Which inconsistency you |
| 273 | prefer is a matter of preference." |
| 274 | (declare (interactive-only forward-line)) |
| 275 | (interactive "p\np") |
| 276 | (unless arg (setq arg 1)) |
| 277 | (let ((stay (or (magit-diff-inside-hunk-body-p) |
| 278 | (magit-section-position-in-heading-p)))) |
| 279 | (if (and stay (= arg 1) (magit--turn-on-shift-select-mode-p)) |
| 280 | (push-mark nil nil t) |
| 281 | (with-no-warnings |
| 282 | (handle-shift-selection) |
| 283 | (next-line (if stay (max (1- arg) 1) arg) try-vscroll))))) |
| 284 | |
| 285 | ;;; Clean |
| 286 | |
| 287 | ;;;###autoload |
| 288 | (defun magit-clean (&optional arg) |
| 289 | "Remove untracked files from the working tree. |
| 290 | With a prefix argument also remove ignored files, |
| 291 | with two prefix arguments remove ignored files only. |
| 292 | \n(git clean -f -d [-x|-X])" |
| 293 | (interactive "p") |
| 294 | (when (yes-or-no-p (format "Remove %s files? " |
| 295 | (pcase arg |
| 296 | (1 "untracked") |
| 297 | (4 "untracked and ignored") |
| 298 | (_ "ignored")))) |
| 299 | (magit-run-before-change-functions nil "clean") |
| 300 | (magit-run-git "clean" "-f" "-d" (pcase arg (4 "-x") (16 "-X"))))) |
| 301 | |
| 302 | (put 'magit-clean 'disabled t) |
| 303 | |
| 304 | ;;; ChangeLog |
| 305 | |
| 306 | ;;;###autoload |
| 307 | (defun magit-generate-changelog (&optional amending) |
| 308 | "Insert ChangeLog entries into the current buffer. |
| 309 | |
| 310 | The entries are generated from the diff being committed. |
| 311 | If prefix argument, AMENDING, is non-nil, include changes |
| 312 | in HEAD as well as staged changes in the diff to check." |
| 313 | (interactive "P") |
| 314 | (unless (magit-commit-message-buffer) |
| 315 | (user-error "No commit in progress")) |
| 316 | (require 'diff-mode) ; `diff-add-log-current-defuns'. |
| 317 | (require 'vc-git) ; `vc-git-diff'. |
| 318 | (require 'add-log) ; `change-log-insert-entries'. |
| 319 | (setq default-directory (magit-toplevel)) |
| 320 | (let ((rev1 (if amending "HEAD^1" "HEAD")) |
| 321 | (rev2 nil)) |
| 322 | ;; Magit may have updated the files without notifying vc, but |
| 323 | ;; `diff-add-log-current-defuns' relies on vc being up-to-date. |
| 324 | (mapc #'vc-file-clearprops (magit-staged-files)) |
| 325 | (change-log-insert-entries |
| 326 | (with-temp-buffer |
| 327 | (vc-git-command (current-buffer) 1 nil |
| 328 | "diff-index" "--exit-code" "--patch" |
| 329 | (and (magit-anything-staged-p) "--cached") |
| 330 | rev1 "--") |
| 331 | ;; `diff-find-source-location' consults these vars. |
| 332 | (defvar diff-vc-revisions) |
| 333 | (setq-local diff-vc-revisions (list rev1 rev2)) |
| 334 | (setq-local diff-vc-backend 'Git) |
| 335 | (diff-add-log-current-defuns))))) |
| 336 | |
| 337 | ;;;###autoload |
| 338 | (defun magit-add-change-log-entry (&optional whoami file-name other-window) |
| 339 | "Find change log file and add date entry and item for current change. |
| 340 | This differs from `add-change-log-entry' (which see) in that |
| 341 | it acts on the current hunk in a Magit buffer instead of on |
| 342 | a position in a file-visiting buffer." |
| 343 | (interactive (list current-prefix-arg |
| 344 | (prompt-for-change-log-name))) |
| 345 | (pcase-let ((`(,buf ,pos) (magit-diff-visit-file--noselect))) |
| 346 | (magit--with-temp-position buf pos |
| 347 | (let ((add-log-buffer-file-name-function #'magit-buffer-file-name)) |
| 348 | (add-change-log-entry whoami file-name other-window))))) |
| 349 | |
| 350 | ;;;###autoload |
| 351 | (defun magit-add-change-log-entry-other-window (&optional whoami file-name) |
| 352 | "Find change log file in other window and add entry and item. |
| 353 | This differs from `add-change-log-entry-other-window' (which see) |
| 354 | in that it acts on the current hunk in a Magit buffer instead of |
| 355 | on a position in a file-visiting buffer." |
| 356 | (interactive (and current-prefix-arg |
| 357 | (list current-prefix-arg |
| 358 | (prompt-for-change-log-name)))) |
| 359 | (magit-add-change-log-entry whoami file-name t)) |
| 360 | |
| 361 | ;;; Edit Line Commit |
| 362 | |
| 363 | ;;;###autoload |
| 364 | (defun magit-edit-line-commit (&optional type) |
| 365 | "Edit the commit that added the current line. |
| 366 | |
| 367 | With a prefix argument edit the commit that removes the line, |
| 368 | if any. The commit is determined using \"git blame\" and made |
| 369 | editable using \"git rebase --interactive\" if it is reachable |
| 370 | from `HEAD', or by checking out the commit (or a branch that |
| 371 | points at it) otherwise." |
| 372 | (interactive (list (and current-prefix-arg 'removal))) |
| 373 | (let* ((chunk (magit-current-blame-chunk (or type 'addition))) |
| 374 | (rev (oref chunk orig-rev))) |
| 375 | (if (string-match-p "\\`0\\{40,\\}\\'" rev) |
| 376 | (message "This line has not been committed yet") |
| 377 | (let ((rebase (magit-rev-ancestor-p rev "HEAD")) |
| 378 | (file (expand-file-name (oref chunk orig-file) |
| 379 | (magit-toplevel)))) |
| 380 | (if rebase |
| 381 | (let ((magit--rebase-published-symbol 'edit-published)) |
| 382 | (magit-rebase-edit-commit rev (magit-rebase-arguments))) |
| 383 | (magit--checkout (or (magit-rev-branch rev) rev))) |
| 384 | (unless (and buffer-file-name |
| 385 | (file-equal-p file buffer-file-name)) |
| 386 | (let ((blame-type (and magit-blame-mode magit-blame-type))) |
| 387 | (if rebase |
| 388 | (set-process-sentinel |
| 389 | magit-this-process |
| 390 | (lambda (process event) |
| 391 | (magit-sequencer-process-sentinel process event) |
| 392 | (when (eq (process-status process) 'exit) |
| 393 | (find-file file) |
| 394 | (when blame-type |
| 395 | (magit-blame--pre-blame-setup blame-type) |
| 396 | (magit-blame--run (magit-blame-arguments)))))) |
| 397 | (find-file file) |
| 398 | (when blame-type |
| 399 | (magit-blame--pre-blame-setup blame-type) |
| 400 | (magit-blame--run (magit-blame-arguments)))))))))) |
| 401 | |
| 402 | (put 'magit-edit-line-commit 'disabled t) |
| 403 | |
| 404 | ;;;###autoload |
| 405 | (defun magit-diff-edit-hunk-commit () |
| 406 | "From a hunk, edit the respective commit and visit the file. |
| 407 | |
| 408 | First visit the file being modified by the hunk at the correct |
| 409 | location using `magit-diff-visit-file'. This actually visits a |
| 410 | blob. When point is on a diff header, not within an individual |
| 411 | hunk, then this visits the blob the first hunk is about. |
| 412 | |
| 413 | Then invoke `magit-edit-line-commit', which uses an interactive |
| 414 | rebase to make the commit editable, or if that is not possible |
| 415 | because the commit is not reachable from `HEAD' by checking out |
| 416 | that commit directly. This also causes the actual worktree file |
| 417 | to be visited. |
| 418 | |
| 419 | Neither the blob nor the file buffer are killed when finishing |
| 420 | the rebase. If that is undesirable, then it might be better to |
| 421 | use `magit-rebase-edit-commit' instead of this command." |
| 422 | (interactive) |
| 423 | (let ((magit-diff-visit-previous-blob nil)) |
| 424 | (with-current-buffer |
| 425 | (magit-diff-visit-file--internal nil #'pop-to-buffer-same-window) |
| 426 | (magit-edit-line-commit)))) |
| 427 | |
| 428 | (put 'magit-diff-edit-hunk-commit 'disabled t) |
| 429 | |
| 430 | ;;; Reshelve |
| 431 | |
| 432 | (defcustom magit-reshelve-since-committer-only nil |
| 433 | "Whether `magit-reshelve-since' changes only the committer dates. |
| 434 | Otherwise the author dates are also changed." |
| 435 | :package-version '(magit . "3.0.0") |
| 436 | :group 'magit-commands |
| 437 | :type 'boolean) |
| 438 | |
| 439 | ;;;###autoload |
| 440 | (defun magit-reshelve-since (commit keyid) |
| 441 | "Change the author and committer dates of the commits since COMMIT. |
| 442 | |
| 443 | Ask the user for the first reachable commit whose dates should |
| 444 | be changed. Then read the new date for that commit. The initial |
| 445 | minibuffer input and the previous history element offer good |
| 446 | values. The next commit will be created one minute later and so |
| 447 | on. |
| 448 | |
| 449 | This command is only intended for interactive use and should only |
| 450 | be used on highly rearranged and unpublished history. |
| 451 | |
| 452 | If KEYID is non-nil, then use that to sign all reshelved commits. |
| 453 | Interactively use the value of the \"--gpg-sign\" option in the |
| 454 | list returned by `magit-rebase-arguments'." |
| 455 | (interactive (list nil |
| 456 | (transient-arg-value "--gpg-sign=" |
| 457 | (magit-rebase-arguments)))) |
| 458 | (let* ((current (or (magit-get-current-branch) |
| 459 | (user-error "Refusing to reshelve detached head"))) |
| 460 | (backup (concat "refs/original/refs/heads/" current))) |
| 461 | (cond |
| 462 | ((not commit) |
| 463 | (when (and (magit-ref-p backup) |
| 464 | (not (magit-y-or-n-p |
| 465 | (format "Backup ref %s already exists. Override? " |
| 466 | backup)))) |
| 467 | (user-error "Abort")) |
| 468 | (magit-log-select |
| 469 | (lambda (rev) |
| 470 | (magit-reshelve-since rev keyid)) |
| 471 | "Type %p on a commit to reshelve it and the commits above it,")) |
| 472 | (t |
| 473 | (cl-flet ((adjust (time offset) |
| 474 | (format-time-string |
| 475 | "%F %T %z" |
| 476 | (+ (floor time) |
| 477 | (* offset 60) |
| 478 | (- (car (decode-time time))))))) |
| 479 | (let* ((start (concat commit "^")) |
| 480 | (range (concat start ".." current)) |
| 481 | (time-rev (adjust (float-time (string-to-number |
| 482 | (magit-rev-format "%at" start))) |
| 483 | 1)) |
| 484 | (time-now (adjust (float-time) |
| 485 | (- (string-to-number |
| 486 | (magit-git-string "rev-list" "--count" |
| 487 | range)))))) |
| 488 | (push time-rev magit--reshelve-history) |
| 489 | (let ((date (floor |
| 490 | (float-time |
| 491 | (date-to-time |
| 492 | (read-string "Date for first commit: " |
| 493 | time-now 'magit--reshelve-history)))))) |
| 494 | (with-environment-variables (("FILTER_BRANCH_SQUELCH_WARNING" "1")) |
| 495 | (magit-with-toplevel |
| 496 | (magit-run-git-async |
| 497 | "filter-branch" "--force" "--env-filter" |
| 498 | (format |
| 499 | "case $GIT_COMMIT in %s\nesac" |
| 500 | (mapconcat |
| 501 | (lambda (rev) |
| 502 | (prog1 |
| 503 | (concat |
| 504 | (format "%s) " rev) |
| 505 | (and (not magit-reshelve-since-committer-only) |
| 506 | (format "export GIT_AUTHOR_DATE=\"%s\"; " date)) |
| 507 | (format "export GIT_COMMITTER_DATE=\"%s\";;" date)) |
| 508 | (incf date 60))) |
| 509 | (magit-git-lines "rev-list" "--reverse" range) |
| 510 | " ")) |
| 511 | (and keyid |
| 512 | (list "--commit-filter" |
| 513 | (format "git commit-tree --gpg-sign=%s \"$@\";" |
| 514 | keyid))) |
| 515 | range "--")) |
| 516 | (set-process-sentinel |
| 517 | magit-this-process |
| 518 | (lambda (process event) |
| 519 | (when (memq (process-status process) '(exit signal)) |
| 520 | (if (> (process-exit-status process) 0) |
| 521 | (magit-process-sentinel process event) |
| 522 | (process-put process 'inhibit-refresh t) |
| 523 | (magit-process-sentinel process event) |
| 524 | (magit-run-git "update-ref" "-d" backup))))))))))))) |
| 525 | |
| 526 | ;;; Revision Stack |
| 527 | |
| 528 | (defvar magit-revision-stack nil) |
| 529 | |
| 530 | (defcustom magit-pop-revision-stack-format |
| 531 | '("[%N: %h] " |
| 532 | "%N: %cs %H\n %s\n" |
| 533 | "\\[\\([0-9]+\\)[]:]") |
| 534 | "Control how `magit-pop-revision-stack' inserts a revision. |
| 535 | |
| 536 | The command `magit-pop-revision-stack' inserts a representation |
| 537 | of the revision last pushed to the `magit-revision-stack' into |
| 538 | the current buffer. It inserts text at point and/or near the end |
| 539 | of the buffer, and removes the consumed revision from the stack. |
| 540 | |
| 541 | The entries on the stack have the format (HASH TOPLEVEL) and this |
| 542 | option has the format (POINT-FORMAT EOB-FORMAT INDEX-REGEXP), all |
| 543 | of which may be nil or a string (though either one of EOB-FORMAT |
| 544 | or POINT-FORMAT should be a string, and if INDEX-REGEXP is |
| 545 | non-nil, then the two formats should be too). |
| 546 | |
| 547 | First INDEX-REGEXP is used to find the previously inserted entry, |
| 548 | by searching backward from point. The first submatch must match |
| 549 | the index number. That number is incremented by one, and becomes |
| 550 | the index number of the entry to be inserted. If you don't want |
| 551 | to number the inserted revisions, then use nil for INDEX-REGEXP. |
| 552 | |
| 553 | If INDEX-REGEXP is non-nil, then both POINT-FORMAT and EOB-FORMAT |
| 554 | should contain \"%N\", which is replaced with the number that was |
| 555 | determined in the previous step. |
| 556 | |
| 557 | Both formats, if non-nil and after removing %N, are then expanded |
| 558 | using \"git show --format=FORMAT ...\" inside TOPLEVEL. |
| 559 | |
| 560 | The expansion of POINT-FORMAT is inserted at point, and the |
| 561 | expansion of EOB-FORMAT is inserted at the end of the buffer (if |
| 562 | the buffer ends with a comment, then it is inserted right before |
| 563 | that)." |
| 564 | :package-version '(magit . "3.2.0") |
| 565 | :group 'magit-commands |
| 566 | :type '(list (choice (string :tag "Insert at point format") |
| 567 | (cons (string :tag "Insert at point format") |
| 568 | (repeat (string :tag "Argument to git show"))) |
| 569 | (const :tag "Don't insert at point" nil)) |
| 570 | (choice (string :tag "Insert at eob format") |
| 571 | (cons (string :tag "Insert at eob format") |
| 572 | (repeat (string :tag "Argument to git show"))) |
| 573 | (const :tag "Don't insert at eob" nil)) |
| 574 | (choice (regexp :tag "Find index regexp") |
| 575 | (const :tag "Don't number entries" nil)))) |
| 576 | |
| 577 | (defcustom magit-copy-revision-abbreviated nil |
| 578 | "Whether to save abbreviated revision to `kill-ring' and `magit-revision-stack'." |
| 579 | :package-version '(magit . "3.0.0") |
| 580 | :group 'magit-miscellaneous |
| 581 | :type 'boolean) |
| 582 | |
| 583 | ;;;###autoload |
| 584 | (defun magit-pop-revision-stack (rev toplevel) |
| 585 | "Insert a representation of a revision into the current buffer. |
| 586 | |
| 587 | Pop a revision from the `magit-revision-stack' and insert it into |
| 588 | the current buffer according to `magit-pop-revision-stack-format'. |
| 589 | Revisions can be put on the stack using `magit-copy-section-value' |
| 590 | and `magit-copy-buffer-revision'. |
| 591 | |
| 592 | If the stack is empty or with a prefix argument, instead read a |
| 593 | revision in the minibuffer. By using the minibuffer history this |
| 594 | allows selecting an item which was popped earlier or to insert an |
| 595 | arbitrary reference or revision without first pushing it onto the |
| 596 | stack. |
| 597 | |
| 598 | When reading the revision from the minibuffer, then it might not |
| 599 | be possible to guess the correct repository. When this command |
| 600 | is called inside a repository (e.g., while composing a commit |
| 601 | message), then that repository is used. Otherwise (e.g., while |
| 602 | composing an email) then the repository recorded for the top |
| 603 | element of the stack is used (even though we insert another |
| 604 | revision). If not called inside a repository and with an empty |
| 605 | stack, or with two prefix arguments, then read the repository in |
| 606 | the minibuffer too." |
| 607 | (interactive |
| 608 | (if (or current-prefix-arg (not magit-revision-stack)) |
| 609 | (let ((default-directory |
| 610 | (or (and (not (= (prefix-numeric-value current-prefix-arg) 16)) |
| 611 | (or (magit-toplevel) |
| 612 | (cadr (car magit-revision-stack)))) |
| 613 | (magit-read-repository)))) |
| 614 | (list (magit-read-branch-or-commit "Insert revision") |
| 615 | default-directory)) |
| 616 | (push (caar magit-revision-stack) magit-revision-history) |
| 617 | (pop magit-revision-stack))) |
| 618 | (unless rev |
| 619 | (user-error "Revision stack is empty")) |
| 620 | (pcase-let ((`(,pnt-format ,eob-format ,idx-format) |
| 621 | magit-pop-revision-stack-format)) |
| 622 | (let ((default-directory toplevel) |
| 623 | (idx (and idx-format |
| 624 | (if (save-excursion |
| 625 | (re-search-backward idx-format nil t)) |
| 626 | (number-to-string (1+ (string-to-number (match-str 1)))) |
| 627 | "1"))) |
| 628 | (pnt-args nil) |
| 629 | (eob-args nil)) |
| 630 | (when (listp pnt-format) |
| 631 | (pcase-setq `(,pnt-format . ,pnt-args) pnt-format)) |
| 632 | (when (listp eob-format) |
| 633 | (pcase-setq `(,eob-format . ,eob-args) eob-format)) |
| 634 | (when pnt-format |
| 635 | (when idx-format |
| 636 | (setq pnt-format (string-replace "%N" idx pnt-format))) |
| 637 | (when (and (bolp) comment-start (looking-at comment-start)) |
| 638 | (save-excursion (insert ?\n))) |
| 639 | (magit-rev-insert-format pnt-format rev pnt-args) |
| 640 | (delete-char -1)) |
| 641 | (when eob-format |
| 642 | (when idx-format |
| 643 | (setq eob-format (string-replace "%N" idx eob-format))) |
| 644 | (git-commit--insert-trailer-1 |
| 645 | (with-temp-buffer |
| 646 | (magit-rev-insert-format eob-format rev eob-args) |
| 647 | (string-trim (buffer-string))) |
| 648 | t))))) |
| 649 | |
| 650 | ;;;###autoload |
| 651 | (defun magit-copy-section-value (arg) |
| 652 | "Save the value of the current section for later use. |
| 653 | |
| 654 | Save the section value to the `kill-ring', and, provided that |
| 655 | the current section is a commit, branch, or tag section, push |
| 656 | the (referenced) revision to the `magit-revision-stack' for use |
| 657 | with `magit-pop-revision-stack'. |
| 658 | |
| 659 | When `magit-copy-revision-abbreviated' is non-nil, save the |
| 660 | abbreviated revision to the `kill-ring' and the |
| 661 | `magit-revision-stack'. |
| 662 | |
| 663 | When the current section is a branch or a tag, and a prefix |
| 664 | argument is used, then save the revision at its tip to the |
| 665 | `kill-ring' instead of the reference name. |
| 666 | |
| 667 | When the region is active, then save that to the `kill-ring', |
| 668 | like `kill-ring-save' would, instead of behaving as described |
| 669 | above. If a prefix argument is used and the region is within |
| 670 | a hunk, then strip the diff marker column and keep only either |
| 671 | the added or removed lines, depending on the sign of the prefix |
| 672 | argument." |
| 673 | (interactive "P") |
| 674 | (cond-let* |
| 675 | ((and arg |
| 676 | (magit-section-internal-region-p) |
| 677 | (magit-section-match 'hunk)) |
| 678 | (kill-new |
| 679 | (thread-last (buffer-substring-no-properties |
| 680 | (region-beginning) |
| 681 | (region-end)) |
| 682 | (replace-regexp-in-string |
| 683 | (format "^\\%c.*\n?" (if (< (prefix-numeric-value arg) 0) ?+ ?-)) |
| 684 | "") |
| 685 | (replace-regexp-in-string "^[ +-]" ""))) |
| 686 | (deactivate-mark)) |
| 687 | ((use-region-p) |
| 688 | (call-interactively #'copy-region-as-kill)) |
| 689 | ([section (magit-current-section)] |
| 690 | [value (oref section value)] |
| 691 | (magit-section-case |
| 692 | ((branch commit module-commit tag) |
| 693 | (let ((default-directory default-directory) ref) |
| 694 | (magit-section-case |
| 695 | ((branch tag) |
| 696 | (setq ref value)) |
| 697 | (module-commit |
| 698 | (setq default-directory |
| 699 | (file-name-as-directory |
| 700 | (expand-file-name (magit-section-parent-value section) |
| 701 | (magit-toplevel)))))) |
| 702 | (setq value (magit-rev-parse |
| 703 | (and magit-copy-revision-abbreviated "--short") |
| 704 | value)) |
| 705 | (push (list value default-directory) magit-revision-stack) |
| 706 | (kill-new (message "%s" (or (and current-prefix-arg ref) |
| 707 | value))))) |
| 708 | (t (kill-new (message "%s" value))))))) |
| 709 | |
| 710 | ;;;###autoload |
| 711 | (defun magit-copy-buffer-revision () |
| 712 | "Save the revision of the current buffer for later use. |
| 713 | |
| 714 | Save the revision shown in the current buffer to the `kill-ring' |
| 715 | and push it to the `magit-revision-stack'. |
| 716 | |
| 717 | This command is mainly intended for use in `magit-revision-mode' |
| 718 | buffers, the only buffers where it is always unambiguous exactly |
| 719 | which revision should be saved. |
| 720 | |
| 721 | Most other Magit buffers usually show more than one revision, in |
| 722 | some way or another, so this command has to select one of them, |
| 723 | and that choice might not always be the one you think would have |
| 724 | been the best pick. |
| 725 | |
| 726 | In such buffers it is often more useful to save the value of |
| 727 | the current section instead, using `magit-copy-section-value'. |
| 728 | |
| 729 | When the region is active, then save that to the `kill-ring', |
| 730 | like `kill-ring-save' would, instead of behaving as described |
| 731 | above. |
| 732 | |
| 733 | When `magit-copy-revision-abbreviated' is non-nil, save the |
| 734 | abbreviated revision to the `kill-ring' and the |
| 735 | `magit-revision-stack'." |
| 736 | (interactive) |
| 737 | (cond-let* |
| 738 | ((use-region-p) |
| 739 | (call-interactively #'copy-region-as-kill)) |
| 740 | ([rev (or magit-buffer-revision |
| 741 | (cl-case major-mode |
| 742 | (magit-diff-mode |
| 743 | (if (string-match "\\.\\.\\.?\\(.+\\)" |
| 744 | magit-buffer-diff-range) |
| 745 | (match-str 1 magit-buffer-diff-range) |
| 746 | magit-buffer-diff-range)) |
| 747 | (magit-status-mode "HEAD")))] |
| 748 | [_(magit-commit-p rev)] |
| 749 | (setq rev (magit-rev-parse |
| 750 | (and magit-copy-revision-abbreviated "--short") |
| 751 | rev)) |
| 752 | (push (list rev default-directory) magit-revision-stack) |
| 753 | (kill-new (message "%s" rev))))) |
| 754 | |
| 755 | ;;; Buffer Switching |
| 756 | |
| 757 | ;;;###autoload |
| 758 | (defun magit-display-repository-buffer (buffer) |
| 759 | "Display a Magit buffer belonging to the current Git repository. |
| 760 | The buffer is displayed using `magit-display-buffer', which see." |
| 761 | (interactive (list (magit--read-repository-buffer |
| 762 | "Display magit buffer: "))) |
| 763 | (magit-display-buffer (get-buffer buffer))) |
| 764 | |
| 765 | ;;;###autoload |
| 766 | (defun magit-switch-to-repository-buffer (buffer) |
| 767 | "Switch to a Magit buffer belonging to the current Git repository." |
| 768 | (interactive (list (magit--read-repository-buffer |
| 769 | "Switch to magit buffer: "))) |
| 770 | (switch-to-buffer buffer)) |
| 771 | |
| 772 | ;;;###autoload |
| 773 | (defun magit-switch-to-repository-buffer-other-window (buffer) |
| 774 | "Switch to a Magit buffer belonging to the current Git repository." |
| 775 | (interactive (list (magit--read-repository-buffer |
| 776 | "Switch to magit buffer in another window: "))) |
| 777 | (switch-to-buffer-other-window buffer)) |
| 778 | |
| 779 | ;;;###autoload |
| 780 | (defun magit-switch-to-repository-buffer-other-frame (buffer) |
| 781 | "Switch to a Magit buffer belonging to the current Git repository." |
| 782 | (interactive (list (magit--read-repository-buffer |
| 783 | "Switch to magit buffer in another frame: "))) |
| 784 | (switch-to-buffer-other-frame buffer)) |
| 785 | |
| 786 | (defun magit--read-repository-buffer (prompt) |
| 787 | (if-let ((topdir (magit-rev-parse-safe "--show-toplevel"))) |
| 788 | (read-buffer |
| 789 | prompt (magit-get-mode-buffer 'magit-status-mode) t |
| 790 | (pcase-lambda (`(,_ . ,buf)) |
| 791 | (and buf |
| 792 | (with-current-buffer buf |
| 793 | (and (or (derived-mode-p 'magit-mode |
| 794 | 'magit-repolist-mode |
| 795 | 'magit-submodule-list-mode |
| 796 | 'git-rebase-mode) |
| 797 | (and buffer-file-name |
| 798 | (string-match-p git-commit-filename-regexp |
| 799 | buffer-file-name))) |
| 800 | (equal (magit-rev-parse-safe "--show-toplevel") |
| 801 | topdir)))))) |
| 802 | (user-error "Not inside a Git repository"))) |
| 803 | |
| 804 | ;;; Miscellaneous |
| 805 | |
| 806 | ;;;###autoload |
| 807 | (defun magit-abort-dwim () |
| 808 | "Abort current operation. |
| 809 | Depending on the context, this will abort a merge, a rebase, a |
| 810 | patch application, a cherry-pick, a revert, or a bisect." |
| 811 | (interactive) |
| 812 | (cond ((magit-merge-in-progress-p) (magit-merge-abort)) |
| 813 | ((magit-rebase-in-progress-p) (magit-rebase-abort)) |
| 814 | ((magit-am-in-progress-p) (magit-am-abort)) |
| 815 | ((magit-sequencer-in-progress-p) (magit-sequencer-abort)) |
| 816 | ((magit-bisect-in-progress-p) (magit-bisect-reset)))) |
| 817 | |
| 818 | ;;;###autoload |
| 819 | (defun magit-back-to-indentation () |
| 820 | "Move point to the first non-whitespace character on this line. |
| 821 | In Magit diffs, also skip over - and + at the beginning of the line." |
| 822 | (interactive "^") |
| 823 | (beginning-of-line 1) |
| 824 | (when (and (magit-section-match 'hunk) |
| 825 | (looking-at (if (oref (magit-current-section) combined) |
| 826 | "^ ?[-+]+" |
| 827 | "^[-+]"))) |
| 828 | (goto-char (match-end 0))) |
| 829 | (skip-syntax-forward " " (line-end-position)) |
| 830 | (backward-prefix-chars)) |
| 831 | |
| 832 | (defun magit-insert-nested-repositories () |
| 833 | "Insert repositories below the current repository. |
| 834 | |
| 835 | If the current repository has submodules, then do nothing (in this case |
| 836 | the expectation is that all nested repositories are submodules and that |
| 837 | they are being listed using a function such as `magit-insert-modules'). |
| 838 | Otherwise look for nested repositories once, using \"find\", and cache |
| 839 | the result. That cache can be invalidated using `magit-zap-caches'. |
| 840 | |
| 841 | Note that \"find\" will be run once in every repository, including in |
| 842 | huge repositories, which don't actually have any nested repositories. |
| 843 | This may be highly inefficient and you might have to implement your own |
| 844 | version of this function, which is more suited to your situation. This |
| 845 | function is mostly intended as a proof-of-concept." |
| 846 | (unless (magit-list-module-names) |
| 847 | (unless (magit-repository-local-exists-p 'nested-repos) |
| 848 | (magit-repository-local-set |
| 849 | 'nested-repos |
| 850 | (mapcar (##file-name-directory (substring % 2)) |
| 851 | (process-lines "find" "-name" ".git" "-mindepth" "2")))) |
| 852 | (when-let ((repos (magit-repository-local-get 'nested-repos))) |
| 853 | (magit-insert-section (repos repos t) |
| 854 | (magit-insert-heading |
| 855 | (format "%s (%s)" |
| 856 | (propertize "Nested repositories" |
| 857 | 'font-lock-face 'magit-section-heading) |
| 858 | (length repos))) |
| 859 | (magit-insert-section-body |
| 860 | (magit--insert-modules-overview nil repos)))))) |
| 861 | |
| 862 | ;;; _ |
| 863 | (provide 'magit-extras) |
| 864 | ;; Local Variables: |
| 865 | ;; read-symbol-shorthands: ( |
| 866 | ;; ("and$" . "cond-let--and$") |
| 867 | ;; ("thread$" . "cond-let--thread$") |
| 868 | ;; ("when$" . "cond-let--when$") |
| 869 | ;; ("and-let*" . "cond-let--and-let*") |
| 870 | ;; ("and-let" . "cond-let--and-let") |
| 871 | ;; ("if-let*" . "cond-let--if-let*") |
| 872 | ;; ("if-let" . "cond-let--if-let") |
| 873 | ;; ("when-let*" . "cond-let--when-let*") |
| 874 | ;; ("when-let" . "cond-let--when-let") |
| 875 | ;; ("while-let*" . "cond-let--while-let*") |
| 876 | ;; ("while-let" . "cond-let--while-let") |
| 877 | ;; ("match-string" . "match-string") |
| 878 | ;; ("match-str" . "match-string-no-properties")) |
| 879 | ;; End: |
| 880 | ;;; magit-extras.el ends here |