config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

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