magit-merge.el (12236B)
1 ;;; magit-merge.el --- Merge functionality -*- 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 merge commands. 26 27 ;;; Code: 28 29 (require 'magit) 30 (require 'magit-diff) 31 32 (declare-function magit-git-push "magit-push" (branch target args)) 33 34 ;;; Commands 35 36 ;;;###autoload (autoload 'magit-merge "magit" nil t) 37 (transient-define-prefix magit-merge () 38 "Merge branches." 39 :man-page "git-merge" 40 :incompatible '(("--ff-only" "--no-ff")) 41 ["Arguments" 42 :if-not magit-merge-in-progress-p 43 ("-f" "Fast-forward only" "--ff-only") 44 ("-n" "No fast-forward" "--no-ff") 45 (magit-merge:--strategy) 46 (5 magit-merge:--strategy-option) 47 (5 "-b" "Ignore changes in amount of whitespace" "-Xignore-space-change") 48 (5 "-w" "Ignore whitespace when comparing lines" "-Xignore-all-space") 49 (5 magit-diff:--diff-algorithm :argument "-Xdiff-algorithm=") 50 (5 magit:--gpg-sign)] 51 ["Actions" 52 :if-not magit-merge-in-progress-p 53 [("m" "Merge" magit-merge-plain) 54 ("e" "Merge and edit message" magit-merge-editmsg) 55 ("n" "Merge but don't commit" magit-merge-nocommit) 56 ("a" "Absorb" magit-merge-absorb)] 57 [("p" "Preview merge" magit-merge-preview) 58 "" 59 ("s" "Squash merge" magit-merge-squash) 60 ("i" "Dissolve" magit-merge-into)]] 61 ["Actions" 62 :if magit-merge-in-progress-p 63 ("m" "Commit merge" magit-commit-create) 64 ("a" "Abort merge" magit-merge-abort)]) 65 66 (defun magit-merge-arguments () 67 (transient-args 'magit-merge)) 68 69 (transient-define-argument magit-merge:--strategy () 70 :description "Strategy" 71 :class 'transient-option 72 ;; key for merge and rebase: "-s" 73 ;; key for cherry-pick and revert: "=s" 74 ;; shortarg for merge and rebase: "-s" 75 ;; shortarg for cherry-pick and revert: none 76 :key "-s" 77 :argument "--strategy=" 78 :choices '("resolve" "recursive" "octopus" "ours" "subtree")) 79 80 (transient-define-argument magit-merge:--strategy-option () 81 :description "Strategy Option" 82 :class 'transient-option 83 :key "-X" 84 :argument "--strategy-option=" 85 :choices '("ours" "theirs" "patience")) 86 87 ;;;###autoload 88 (defun magit-merge-plain (rev &optional args nocommit) 89 "Merge commit REV into the current branch; using default message. 90 91 Unless there are conflicts or a prefix argument is used create a 92 merge commit using a generic commit message and without letting 93 the user inspect the result. With a prefix argument pretend the 94 merge failed to give the user the opportunity to inspect the 95 merge. 96 97 \(git merge --no-edit|--no-commit [ARGS] REV)" 98 (interactive (list (magit-read-other-branch-or-commit "Merge") 99 (magit-merge-arguments) 100 current-prefix-arg)) 101 (magit-merge-assert) 102 (magit-run-git-async "merge" (if nocommit "--no-commit" "--no-edit") args rev)) 103 104 ;;;###autoload 105 (defun magit-merge-editmsg (rev &optional args) 106 "Merge commit REV into the current branch; and edit message. 107 Perform the merge and prepare a commit message but let the user 108 edit it. 109 \n(git merge --edit --no-ff [ARGS] REV)" 110 (interactive (list (magit-read-other-branch-or-commit "Merge") 111 (magit-merge-arguments))) 112 (magit-merge-assert) 113 (cl-pushnew "--no-ff" args :test #'equal) 114 (apply #'magit-run-git-with-editor "merge" "--edit" 115 (append (delete "--ff-only" args) 116 (list rev)))) 117 118 ;;;###autoload 119 (defun magit-merge-nocommit (rev &optional args) 120 "Merge commit REV into the current branch; pretending it failed. 121 Pretend the merge failed to give the user the opportunity to 122 inspect the merge and change the commit message. 123 \n(git merge --no-commit --no-ff [ARGS] REV)" 124 (interactive (list (magit-read-other-branch-or-commit "Merge") 125 (magit-merge-arguments))) 126 (magit-merge-assert) 127 (cl-pushnew "--no-ff" args :test #'equal) 128 (magit-run-git-async "merge" "--no-commit" args rev)) 129 130 ;;;###autoload 131 (defun magit-merge-into (branch &optional args) 132 "Merge the current branch into BRANCH and remove the former. 133 134 Before merging, force push the source branch to its push-remote, 135 provided the respective remote branch already exists, ensuring 136 that the respective pull-request (if any) won't get stuck on some 137 obsolete version of the commits that are being merged. Finally 138 if `forge-branch-pullreq' was used to create the merged branch, 139 then also remove the respective remote branch." 140 (interactive 141 (list (magit-read-other-local-branch 142 (format "Merge `%s' into" 143 (or (magit-get-current-branch) 144 (magit-rev-parse "HEAD"))) 145 nil 146 (and-let* ((upstream (magit-get-upstream-branch)) 147 (upstream (cdr (magit-split-branch-name upstream)))) 148 (and (magit-branch-p upstream) upstream))) 149 (magit-merge-arguments))) 150 (let ((current (magit-get-current-branch)) 151 (head (magit-rev-parse "HEAD"))) 152 (when (zerop (magit-call-git "checkout" branch)) 153 (if current 154 (magit--merge-absorb current args) 155 (magit-run-git-with-editor "merge" args head))))) 156 157 ;;;###autoload 158 (defun magit-merge-absorb (branch &optional args) 159 "Merge BRANCH into the current branch and remove the former. 160 161 Before merging, force push the source branch to its push-remote, 162 provided the respective remote branch already exists, ensuring 163 that the respective pull-request (if any) won't get stuck on some 164 obsolete version of the commits that are being merged. Finally 165 if `forge-branch-pullreq' was used to create the merged branch, 166 then also remove the respective remote branch." 167 (interactive (list (magit-read-other-local-branch "Absorb branch") 168 (magit-merge-arguments))) 169 (magit--merge-absorb branch args)) 170 171 (defun magit--merge-absorb (branch args) 172 (when (equal branch (magit-main-branch)) 173 (unless (yes-or-no-p 174 (format "Do you really want to merge `%s' into another branch? " 175 branch)) 176 (user-error "Abort"))) 177 (if-let ((target (magit-get-push-branch branch t))) 178 (progn 179 (magit-git-push branch target (list "--force-with-lease")) 180 (set-process-sentinel 181 magit-this-process 182 (lambda (process event) 183 (when (memq (process-status process) '(exit signal)) 184 (if (not (zerop (process-exit-status process))) 185 (magit-process-sentinel process event) 186 (process-put process 'inhibit-refresh t) 187 (magit-process-sentinel process event) 188 (magit--merge-absorb-1 branch args)))))) 189 (magit--merge-absorb-1 branch args))) 190 191 (defun magit--merge-absorb-1 (branch args) 192 (if-let ((pr (magit-get "branch" branch "pullRequest"))) 193 (magit-run-git-async 194 "merge" args "-m" 195 (format "Merge branch '%s'%s [#%s]" 196 branch 197 (let ((current (magit-get-current-branch))) 198 (if (equal current (magit-main-branch)) 199 "" 200 (format " into %s" current))) 201 pr) 202 branch) 203 (magit-run-git-async "merge" args "--no-edit" branch)) 204 (set-process-sentinel 205 magit-this-process 206 (lambda (process event) 207 (when (memq (process-status process) '(exit signal)) 208 (if (> (process-exit-status process) 0) 209 (magit-process-sentinel process event) 210 (process-put process 'inhibit-refresh t) 211 (magit-process-sentinel process event) 212 (magit-branch-maybe-delete-pr-remote branch) 213 (magit-branch-unset-pushRemote branch) 214 (magit-run-git "branch" "-D" branch)))))) 215 216 ;;;###autoload 217 (defun magit-merge-squash (rev) 218 "Squash commit REV into the current branch; don't create a commit. 219 \n(git merge --squash REV)" 220 (interactive (list (magit-read-other-branch-or-commit "Squash"))) 221 (magit-merge-assert) 222 (magit-run-git-async "merge" "--squash" rev)) 223 224 ;;;###autoload 225 (defun magit-merge-preview (rev) 226 "Preview result of merging REV into the current branch." 227 (interactive (list (magit-read-other-branch-or-commit "Preview merge"))) 228 (magit-merge-preview-setup-buffer rev)) 229 230 ;;;###autoload 231 (defun magit-merge-abort () 232 "Abort the current merge operation. 233 \n(git merge --abort)" 234 (interactive) 235 (unless (file-exists-p (expand-file-name "MERGE_HEAD" (magit-gitdir))) 236 (user-error "No merge in progress")) 237 (magit-confirm 'abort-merge) 238 (magit-run-git-async "merge" "--abort")) 239 240 (defun magit-checkout-stage (file arg) 241 "During a conflict checkout and stage side, or restore conflict." 242 (interactive 243 (let ((file (magit-completing-read "Checkout file" 244 (magit-tracked-files) nil nil nil 245 'magit-read-file-hist 246 (magit-current-file)))) 247 (cond ((member file (magit-unmerged-files)) 248 (list file (magit-checkout-read-stage file))) 249 ((yes-or-no-p (format "Restore conflicts in %s? " file)) 250 (list file "--merge")) 251 (t 252 (user-error "Quit"))))) 253 (pcase (cons arg (cddr (car (magit-file-status file)))) 254 ((or `("--ours" ?D ,_) 255 '("--ours" ?U ?A) 256 `("--theirs" ,_ ?D) 257 '("--theirs" ?A ?U)) 258 (magit-run-git "rm" "--" file)) 259 (_ (if (equal arg "--merge") 260 ;; This fails if the file was deleted on one 261 ;; side. And we cannot do anything about it. 262 (magit-run-git "checkout" "--merge" "--" file) 263 (magit-call-git "checkout" arg "--" file) 264 (magit-run-git "add" "-u" "--" file))))) 265 266 ;;; Utilities 267 268 (defun magit-merge-in-progress-p () 269 (file-exists-p (expand-file-name "MERGE_HEAD" (magit-gitdir)))) 270 271 (defun magit--merge-range (&optional head) 272 (unless head 273 (setq head (magit-get-shortname 274 (car (magit-file-lines 275 (expand-file-name "MERGE_HEAD" (magit-gitdir))))))) 276 (and head 277 (concat (magit-git-string "merge-base" "--octopus" "HEAD" head) 278 ".." head))) 279 280 (defun magit-merge-assert () 281 (or (not (magit-anything-modified-p t)) 282 (magit-confirm 'merge-dirty 283 "Merging with dirty worktree is risky. Continue"))) 284 285 (defun magit-checkout-read-stage (file) 286 (magit-read-char-case (format "For %s checkout: " file) t 287 (?o "[o]ur stage" "--ours") 288 (?t "[t]heir stage" "--theirs") 289 (?c (if magit-verbose-messages "restore [c]onflict" "[c]onflict") 290 "--merge"))) 291 292 ;;; Sections 293 294 (defvar-keymap magit-unmerged-section-map 295 :doc "Keymap for `unmerged' sections." 296 :parent magit-log-section-map) 297 298 (defun magit-insert-merge-log () 299 "Insert section for the on-going merge. 300 Display the heads that are being merged. 301 If no merge is in progress, do nothing." 302 (when (magit-merge-in-progress-p) 303 (let* ((heads (mapcar #'magit-get-shortname 304 (magit-file-lines 305 (expand-file-name "MERGE_HEAD" (magit-gitdir))))) 306 (range (magit--merge-range (car heads)))) 307 (magit-insert-section (unmerged range) 308 (magit-insert-heading 309 (format "Merging %s:" (mapconcat #'identity heads ", "))) 310 (magit--insert-log nil 311 range 312 (let ((args magit-buffer-log-args)) 313 (unless (member "--decorate=full" magit-buffer-log-args) 314 (push "--decorate=full" args)) 315 args)))))) 316 317 ;;; _ 318 (provide 'magit-merge) 319 ;;; magit-merge.el ends here