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