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