| 1 | ;;; magit-patch.el --- Creating and applying patches -*- 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 | ;; This library implements patch commands. |
| 26 | |
| 27 | ;;; Code: |
| 28 | |
| 29 | (require 'magit) |
| 30 | |
| 31 | (declare-function message-goto-body "message" (&optional interactive)) |
| 32 | |
| 33 | ;;; Options |
| 34 | |
| 35 | (defcustom magit-patch-save-arguments '(exclude "--stat") |
| 36 | "Control arguments used by the command `magit-patch-save'. |
| 37 | |
| 38 | `magit-patch-save' (which see) saves a diff for the changes |
| 39 | shown in the current buffer in a patch file. It may use the |
| 40 | same arguments as used in the buffer or a subset thereof, or |
| 41 | a constant list of arguments, depending on this option and |
| 42 | the prefix argument." |
| 43 | :package-version '(magit . "2.12.0") |
| 44 | :group 'magit-diff |
| 45 | :type '(choice (const :tag "Use buffer arguments" buffer) |
| 46 | (cons :tag "Use buffer arguments except" |
| 47 | (const :format "" exclude) |
| 48 | (repeat :format "%v%i\n" |
| 49 | (string :tag "Argument"))) |
| 50 | (repeat :tag "Use constant arguments" |
| 51 | (string :tag "Argument")))) |
| 52 | |
| 53 | ;;; Commands |
| 54 | |
| 55 | ;;;###autoload(autoload 'magit-patch "magit-patch" nil t) |
| 56 | (transient-define-prefix magit-patch () |
| 57 | "Create or apply patches." |
| 58 | ["Actions" |
| 59 | [("c" "Create patches" magit-patch-create) |
| 60 | ("w" "Apply patches" magit-am)] |
| 61 | [("a" "Apply plain patch" magit-patch-apply) |
| 62 | ("s" "Save diff as patch" magit-patch-save)] |
| 63 | [("r" "Request pull" magit-request-pull)]]) |
| 64 | |
| 65 | ;;;###autoload(autoload 'magit-patch-create "magit-patch" nil t) |
| 66 | (transient-define-prefix magit-patch-create (range args files) |
| 67 | "Create patches for the commits in RANGE. |
| 68 | When a single commit is given for RANGE, create a patch for the |
| 69 | changes introduced by that commit (unlike 'git format-patch' |
| 70 | which creates patches for all commits that are reachable from |
| 71 | `HEAD' but not from the specified commit)." |
| 72 | :man-page "git-format-patch" |
| 73 | :incompatible '(("--subject-prefix=" "--rfc")) |
| 74 | ["Mail arguments" |
| 75 | (6 magit-format-patch:--in-reply-to) |
| 76 | (6 magit-format-patch:--thread) |
| 77 | (6 magit-format-patch:--from) |
| 78 | (6 magit-format-patch:--to) |
| 79 | (6 magit-format-patch:--cc)] |
| 80 | ["Patch arguments" |
| 81 | (magit-format-patch:--base) |
| 82 | (magit-format-patch:--reroll-count) |
| 83 | (5 magit-format-patch:--interdiff) |
| 84 | (magit-format-patch:--range-diff) |
| 85 | (magit-format-patch:--subject-prefix) |
| 86 | ("C-m r " "RFC subject prefix" "--rfc") |
| 87 | ("C-m l " "Add cover letter" "--cover-letter") |
| 88 | (5 magit-format-patch:--cover-from-description) |
| 89 | (5 magit-format-patch:--notes) |
| 90 | (magit-format-patch:--output-directory)] |
| 91 | ["Diff arguments" |
| 92 | (magit-diff:-U) |
| 93 | (magit-diff:-M) |
| 94 | (magit-diff:-C) |
| 95 | (magit-diff:--diff-algorithm) |
| 96 | (magit:--) |
| 97 | (7 "-b" "Ignore whitespace changes" ("-b" "--ignore-space-change")) |
| 98 | (7 "-w" "Ignore all whitespace" ("-w" "--ignore-all-space"))] |
| 99 | ["Actions" |
| 100 | ("c" "Create patches" magit-patch-create)] |
| 101 | (interactive |
| 102 | (if (not (eq transient-current-command 'magit-patch-create)) |
| 103 | (list nil nil nil) |
| 104 | (cons (cond-let |
| 105 | ([revs (magit-region-values 'commit)] |
| 106 | (if (length> revs 1) |
| 107 | (concat (car (last revs)) "^.." (car revs)) |
| 108 | (format "%s^..%s" (car revs) (car revs)))) |
| 109 | ([range (magit-read-range-or-commit |
| 110 | "Create patches for range or commit")] |
| 111 | (if (string-search ".." range) |
| 112 | range |
| 113 | (format "%s^..%s" range range)))) |
| 114 | (let ((args (transient-args 'magit-patch-create))) |
| 115 | (list (seq-filter #'stringp args) |
| 116 | (cdr (assoc "--" args))))))) |
| 117 | (if (not range) |
| 118 | (transient-setup 'magit-patch-create) |
| 119 | (magit-run-git "format-patch" range args "--" files) |
| 120 | (when (member "--cover-letter" args) |
| 121 | (save-match-data |
| 122 | (find-file |
| 123 | (expand-file-name |
| 124 | (concat (and$ (transient-arg-value "--reroll-count=" args) |
| 125 | (format "v%s-" $)) |
| 126 | "0000-cover-letter.patch") |
| 127 | (let ((topdir (magit-toplevel))) |
| 128 | (if-let ((dir (transient-arg-value "--output-directory=" args))) |
| 129 | (expand-file-name dir topdir) |
| 130 | topdir)))))))) |
| 131 | |
| 132 | (transient-define-argument magit-format-patch:--in-reply-to () |
| 133 | :description "In reply to" |
| 134 | :class 'transient-option |
| 135 | :key "C-m C-r" |
| 136 | :argument "--in-reply-to=") |
| 137 | |
| 138 | (transient-define-argument magit-format-patch:--thread () |
| 139 | :description "Thread style" |
| 140 | :class 'transient-option |
| 141 | :key "C-m s " |
| 142 | :argument "--thread=" |
| 143 | :reader #'magit-format-patch-select-thread-style) |
| 144 | |
| 145 | (defun magit-format-patch-select-thread-style (&rest _ignore) |
| 146 | (magit-read-char-case "Thread style " t |
| 147 | (?d "[d]eep" "deep") |
| 148 | (?s "[s]hallow" "shallow"))) |
| 149 | |
| 150 | (transient-define-argument magit-format-patch:--base () |
| 151 | :description "Insert base commit" |
| 152 | :class 'transient-option |
| 153 | :key "C-m b " |
| 154 | :argument "--base=" |
| 155 | :reader #'magit-format-patch-select-base) |
| 156 | |
| 157 | (defun magit-format-patch-select-base (prompt initial-input history) |
| 158 | (magit-completing-read prompt (cons "auto" (magit-list-refnames)) |
| 159 | nil 'any initial-input history "auto")) |
| 160 | |
| 161 | (transient-define-argument magit-format-patch:--reroll-count () |
| 162 | :description "Reroll count" |
| 163 | :class 'transient-option |
| 164 | :key "C-m v " |
| 165 | :shortarg "-v" |
| 166 | :argument "--reroll-count=" |
| 167 | :reader #'transient-read-number-N+) |
| 168 | |
| 169 | (transient-define-argument magit-format-patch:--interdiff () |
| 170 | :description "Insert interdiff" |
| 171 | :class 'transient-option |
| 172 | :key "C-m d i" |
| 173 | :argument "--interdiff=" |
| 174 | :reader #'magit-transient-read-revision) |
| 175 | |
| 176 | (transient-define-argument magit-format-patch:--range-diff () |
| 177 | :description "Insert range-diff" |
| 178 | :class 'transient-option |
| 179 | :key "C-m d r" |
| 180 | :argument "--range-diff=" |
| 181 | :reader #'magit-format-patch-select-range-diff) |
| 182 | |
| 183 | (defun magit-format-patch-select-range-diff (prompt _initial-input _history) |
| 184 | (magit-read-range-or-commit prompt)) |
| 185 | |
| 186 | (transient-define-argument magit-format-patch:--subject-prefix () |
| 187 | :description "Subject Prefix" |
| 188 | :class 'transient-option |
| 189 | :key "C-m p " |
| 190 | :argument "--subject-prefix=") |
| 191 | |
| 192 | (transient-define-argument magit-format-patch:--cover-from-description () |
| 193 | :description "Use branch description" |
| 194 | :class 'transient-option |
| 195 | :key "C-m D " |
| 196 | :argument "--cover-from-description=" |
| 197 | :reader #'magit-format-patch-select-description-mode) |
| 198 | |
| 199 | (defun magit-format-patch-select-description-mode (&rest _ignore) |
| 200 | (magit-read-char-case "Use description as " t |
| 201 | (?m "[m]essage" "message") |
| 202 | (?s "[s]ubject" "subject") |
| 203 | (?a "[a]uto" "auto") |
| 204 | (?n "[n]othing" "none"))) |
| 205 | |
| 206 | (transient-define-argument magit-format-patch:--notes () |
| 207 | :description "Insert commentary from notes" |
| 208 | :class 'transient-option |
| 209 | :key "C-m n " |
| 210 | :argument "--notes=" |
| 211 | :reader #'magit-notes-read-ref) |
| 212 | |
| 213 | (transient-define-argument magit-format-patch:--from () |
| 214 | :description "From" |
| 215 | :class 'transient-option |
| 216 | :key "C-m C-f" |
| 217 | :argument "--from=" |
| 218 | :reader #'magit-transient-read-person) |
| 219 | |
| 220 | (transient-define-argument magit-format-patch:--to () |
| 221 | :description "To" |
| 222 | :class 'transient-option |
| 223 | :key "C-m C-t" |
| 224 | :argument "--to=" |
| 225 | :reader #'magit-transient-read-person) |
| 226 | |
| 227 | (transient-define-argument magit-format-patch:--cc () |
| 228 | :description "CC" |
| 229 | :class 'transient-option |
| 230 | :key "C-m C-c" |
| 231 | :argument "--cc=" |
| 232 | :reader #'magit-transient-read-person) |
| 233 | |
| 234 | (transient-define-argument magit-format-patch:--output-directory () |
| 235 | :description "Output directory" |
| 236 | :class 'transient-option |
| 237 | :key "C-m o " |
| 238 | :shortarg "-o" |
| 239 | :argument "--output-directory=" |
| 240 | :reader #'transient-read-existing-directory) |
| 241 | |
| 242 | ;;;###autoload(autoload 'magit-patch-apply "magit-patch" nil t) |
| 243 | (transient-define-prefix magit-patch-apply (file &rest args) |
| 244 | "Apply the patch file FILE." |
| 245 | :man-page "git-apply" |
| 246 | ["Arguments" |
| 247 | ("-i" "Also apply to index" "--index") |
| 248 | ("-c" "Only apply to index" "--cached") |
| 249 | ("-3" "Fall back on 3way merge" ("-3" "--3way"))] |
| 250 | ["Actions" |
| 251 | ("a" "Apply patch" magit-patch-apply)] |
| 252 | (interactive |
| 253 | (if (not (eq transient-current-command 'magit-patch-apply)) |
| 254 | (list nil) |
| 255 | (list (expand-file-name |
| 256 | (read-file-name "Apply patch: " |
| 257 | default-directory nil nil |
| 258 | (and$ (magit-file-at-point) |
| 259 | (file-relative-name $)))) |
| 260 | (transient-args 'magit-patch-apply)))) |
| 261 | (if (not file) |
| 262 | (transient-setup 'magit-patch-apply) |
| 263 | (magit-run-git "apply" args "--" (magit-convert-filename-for-git file)))) |
| 264 | |
| 265 | ;;;###autoload |
| 266 | (defun magit-patch-save (file &optional arg) |
| 267 | "Write current diff into patch FILE. |
| 268 | |
| 269 | What arguments are used to create the patch depends on the value |
| 270 | of `magit-patch-save-arguments' and whether a prefix argument is |
| 271 | used. |
| 272 | |
| 273 | If the value is the symbol `buffer', then use the same arguments |
| 274 | as the buffer. With a prefix argument use no arguments. |
| 275 | |
| 276 | If the value is a list beginning with the symbol `exclude', then |
| 277 | use the same arguments as the buffer except for those matched by |
| 278 | entries in the cdr of the list. The comparison is done using |
| 279 | `string-prefix-p'. With a prefix argument use the same arguments |
| 280 | as the buffer. |
| 281 | |
| 282 | If the value is a list of strings (including the empty list), |
| 283 | then use those arguments. With a prefix argument use the same |
| 284 | arguments as the buffer. |
| 285 | |
| 286 | Of course the arguments that are required to actually show the |
| 287 | same differences as those shown in the buffer are always used." |
| 288 | (interactive (list (read-file-name "Write patch file: " default-directory) |
| 289 | current-prefix-arg)) |
| 290 | (unless (derived-mode-p 'magit-diff-mode) |
| 291 | (user-error "Only diff buffers can be saved as patches")) |
| 292 | (let ((rev magit-buffer-diff-range) |
| 293 | (typearg magit-buffer-diff-typearg) |
| 294 | (args magit-buffer-diff-args) |
| 295 | (files magit-buffer-diff-files)) |
| 296 | (cond ((eq magit-patch-save-arguments 'buffer) |
| 297 | (when arg |
| 298 | (setq args nil))) |
| 299 | ((eq (car-safe magit-patch-save-arguments) 'exclude) |
| 300 | (unless arg |
| 301 | (setq args |
| 302 | (cl-set-difference args (cdr magit-patch-save-arguments) |
| 303 | :test #'equal)))) |
| 304 | ((not arg) |
| 305 | (setq args magit-patch-save-arguments))) |
| 306 | (with-temp-file file |
| 307 | (magit-git-insert "diff" rev "-p" typearg args "--" files))) |
| 308 | (magit-refresh)) |
| 309 | |
| 310 | ;;;###autoload |
| 311 | (defun magit-request-pull (url start end) |
| 312 | "Request upstream to pull from your public repository. |
| 313 | |
| 314 | URL is the url of your publicly accessible repository. |
| 315 | START is a commit that already is in the upstream repository. |
| 316 | END is the last commit, usually a branch name, which upstream |
| 317 | is asked to pull. START has to be reachable from that commit." |
| 318 | (interactive |
| 319 | (list (magit-get "remote" (magit-read-remote "Remote") "url") |
| 320 | (magit-read-branch-or-commit "Start" (magit-get-upstream-branch)) |
| 321 | (magit-read-branch-or-commit "End"))) |
| 322 | (require 'message) |
| 323 | (let ((dir default-directory)) |
| 324 | ;; mu4e changes default-directory |
| 325 | (compose-mail) |
| 326 | (setq default-directory dir)) |
| 327 | (message-goto-body) |
| 328 | (magit-git-insert "request-pull" start url end) |
| 329 | (set-buffer-modified-p nil)) |
| 330 | |
| 331 | ;;; _ |
| 332 | (provide 'magit-patch) |
| 333 | ;; Local Variables: |
| 334 | ;; read-symbol-shorthands: ( |
| 335 | ;; ("and$" . "cond-let--and$") |
| 336 | ;; ("thread$" . "cond-let--thread$") |
| 337 | ;; ("when$" . "cond-let--when$") |
| 338 | ;; ("and-let*" . "cond-let--and-let*") |
| 339 | ;; ("and-let" . "cond-let--and-let") |
| 340 | ;; ("if-let*" . "cond-let--if-let*") |
| 341 | ;; ("if-let" . "cond-let--if-let") |
| 342 | ;; ("when-let*" . "cond-let--when-let*") |
| 343 | ;; ("when-let" . "cond-let--when-let") |
| 344 | ;; ("while-let*" . "cond-let--while-let*") |
| 345 | ;; ("while-let" . "cond-let--while-let") |
| 346 | ;; ("match-string" . "match-string") |
| 347 | ;; ("match-str" . "match-string-no-properties")) |
| 348 | ;; End: |
| 349 | ;;; magit-patch.el ends here |