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