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