| 1 | ;;; magit-branch.el --- Branch support -*- 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 support for branches. It defines commands |
| 26 | ;; for creating, checking out, manipulating, and configuring branches. |
| 27 | ;; Commands defined here are mainly concerned with branches as |
| 28 | ;; pointers, commands that deal with what a branch points at, are |
| 29 | ;; defined elsewhere. |
| 30 | |
| 31 | ;;; Code: |
| 32 | |
| 33 | (require 'magit) |
| 34 | (require 'magit-reset) |
| 35 | |
| 36 | ;;; Options |
| 37 | |
| 38 | (defcustom magit-branch-read-upstream-first t |
| 39 | "Whether to read upstream before name of new branch when creating a branch. |
| 40 | |
| 41 | `nil' Read the branch name first. |
| 42 | `t' Read the upstream first. |
| 43 | `fallback' Read the upstream first, but if it turns out that the chosen |
| 44 | value is not a valid upstream (because it cannot be resolved |
| 45 | as an existing revision), then treat it as the name of the |
| 46 | new branch and continue by reading the upstream next." |
| 47 | :package-version '(magit . "2.2.0") |
| 48 | :group 'magit-commands |
| 49 | :type '(choice (const :tag "Read branch name first" nil) |
| 50 | (const :tag "Read upstream first" t) |
| 51 | (const :tag "Read upstream first, with fallback" fallback))) |
| 52 | |
| 53 | (defcustom magit-branch-name-suggestions nil |
| 54 | "List of names and/or prefixes suggested when naming a new branch." |
| 55 | :package-version '(magit . "4.6.0") |
| 56 | :group 'magit-commands |
| 57 | :type '(repeat string) |
| 58 | :safe (##and (listp %) (all #'stringp %))) |
| 59 | |
| 60 | (defcustom magit-branch-prefer-remote-upstream nil |
| 61 | "Whether to favor remote upstreams when creating new branches. |
| 62 | |
| 63 | When a new branch is created, then the branch, commit, or stash |
| 64 | at point is suggested as the default starting point of the new |
| 65 | branch, or if there is no such revision at point the current |
| 66 | branch. In either case the user may choose another starting |
| 67 | point. |
| 68 | |
| 69 | If the chosen starting point is a branch, then it may also be set |
| 70 | as the upstream of the new branch, depending on the value of the |
| 71 | Git variable `branch.autoSetupMerge'. By default this is done |
| 72 | for remote branches, but not for local branches. |
| 73 | |
| 74 | You might prefer to always use some remote branch as upstream. |
| 75 | If the chosen starting point is (1) a local branch, (2) whose |
| 76 | name matches a member of the value of this option, (3) the |
| 77 | upstream of that local branch is a remote branch with the same |
| 78 | name, and (4) that remote branch can be fast-forwarded to the |
| 79 | local branch, then the chosen branch is used as starting point, |
| 80 | but its own upstream is used as the upstream of the new branch. |
| 81 | |
| 82 | Members of this option's value are treated as branch names that |
| 83 | have to match exactly unless they contain a character that makes |
| 84 | them invalid as a branch name. Recommended characters to use |
| 85 | to trigger interpretation as a regexp are \"*\" and \"^\". Some |
| 86 | other characters which you might expect to be invalid, actually |
| 87 | are not, e.g., \".+$\" are all perfectly valid. More precisely, |
| 88 | if \"git check-ref-format --branch STRING\" exits with a non-zero |
| 89 | status, then treat STRING as a regexp. |
| 90 | |
| 91 | Assuming the chosen branch matches these conditions you would end |
| 92 | up with with e.g.: |
| 93 | |
| 94 | feature --upstream--> origin/master |
| 95 | |
| 96 | instead of |
| 97 | |
| 98 | feature --upstream--> master --upstream--> origin/master |
| 99 | |
| 100 | Which you prefer is a matter of personal preference. If you do |
| 101 | prefer the former, then you should add branches such as \"master\", |
| 102 | \"next\", and \"maint\" to the value of this options." |
| 103 | :package-version '(magit . "2.4.0") |
| 104 | :group 'magit-commands |
| 105 | :type '(repeat string)) |
| 106 | |
| 107 | (defcustom magit-branch-adjust-remote-upstream-alist nil |
| 108 | "Alist of upstreams to be used when branching from remote branches. |
| 109 | |
| 110 | When creating a local branch from an ephemeral branch located |
| 111 | on a remote, e.g., a feature or hotfix branch, then that remote |
| 112 | branch should usually not be used as the upstream branch, since |
| 113 | the push-remote already allows accessing it and having both the |
| 114 | upstream and the push-remote reference the same related branch |
| 115 | would be wasteful. Instead a branch like \"maint\" or \"master\" |
| 116 | should be used as the upstream. |
| 117 | |
| 118 | This option allows specifying the branch that should be used as |
| 119 | the upstream when branching certain remote branches. The value |
| 120 | is an alist of the form ((UPSTREAM . RULE)...). The first |
| 121 | element is used whose UPSTREAM exists and whose RULE matches |
| 122 | the name of the new branch. Subsequent elements are ignored. |
| 123 | |
| 124 | UPSTREAM is the branch to be used as the upstream for branches |
| 125 | specified by RULE. It can be a local or a remote branch. |
| 126 | |
| 127 | RULE can either be a regular expression, matching branches whose |
| 128 | upstream should be the one specified by UPSTREAM. Or it can be |
| 129 | a list of the only branches that should *not* use UPSTREAM; all |
| 130 | other branches will. Matching is done after stripping the remote |
| 131 | part of the name of the branch that is being branched from. |
| 132 | |
| 133 | If you use a finite set of non-ephemeral branches across all your |
| 134 | repositories, then you might use something like: |
| 135 | |
| 136 | ((\"origin/master\" . (\"master\" \"next\" \"maint\"))) |
| 137 | |
| 138 | Or if the names of all your ephemeral branches contain a slash, |
| 139 | at least in some repositories, then a good value could be: |
| 140 | |
| 141 | ((\"origin/master\" . \"/\")) |
| 142 | |
| 143 | Of course you can also fine-tune: |
| 144 | |
| 145 | ((\"origin/maint\" . \"\\\\\\=`hotfix/\") |
| 146 | (\"origin/master\" . \"\\\\\\=`feature/\")) |
| 147 | |
| 148 | UPSTREAM can be a local branch: |
| 149 | |
| 150 | ((\"master\" . (\"master\" \"next\" \"maint\"))) |
| 151 | |
| 152 | Because the main branch is no longer almost always named \"master\" |
| 153 | you should also account for other common names: |
| 154 | |
| 155 | ((\"main\" . (\"main\" \"master\" \"next\" \"maint\")) |
| 156 | (\"master\" . (\"main\" \"master\" \"next\" \"maint\"))) |
| 157 | |
| 158 | If you use remote branches as UPSTREAM, then you might also want |
| 159 | to set `magit-branch-prefer-remote-upstream' to a non-nil value. |
| 160 | However, I recommend that you use local branches as UPSTREAM." |
| 161 | :package-version '(magit . "2.9.0") |
| 162 | :group 'magit-commands |
| 163 | :type '(alist :key-type (string :tag "Use upstream") |
| 164 | :value-type (choice :tag "For branches" ;??? |
| 165 | (regexp :tag "Matching") |
| 166 | (repeat :tag "Except" |
| 167 | (string :tag "Branch"))))) |
| 168 | |
| 169 | (defcustom magit-branch-rename-push-target t |
| 170 | "Whether the push-remote setup is preserved when renaming a branch. |
| 171 | |
| 172 | The command `magit-branch-rename' renames a branch named OLD to |
| 173 | NEW. This option controls how much of the push-remote setup is |
| 174 | preserved when doing so. |
| 175 | |
| 176 | When nil, then preserve nothing and unset `branch.OLD.pushRemote'. |
| 177 | |
| 178 | When `local-only', then set `branch.NEW.pushRemote' to the same |
| 179 | value as `branch.OLD.pushRemote'. |
| 180 | |
| 181 | When t, then additionally rename the branch named OLD on the |
| 182 | remote specified by `branch.OLD.pushRemote' to NEW, provided |
| 183 | OLD exists on that remote and unless NEW already exists on the |
| 184 | remote. |
| 185 | |
| 186 | When `forge-only' and the `forge' package is available, then |
| 187 | behave like `t', iff the remote points to a repository on a |
| 188 | forge (currently Github or Gitlab), otherwise like `local-only'." |
| 189 | :package-version '(magit . "2.90.0") |
| 190 | :group 'magit-commands |
| 191 | :type '(choice |
| 192 | (const :tag "Don't preserve push-remote setup" nil) |
| 193 | (const :tag "Preserve push-remote setup" local-only) |
| 194 | (const :tag "... and rename corresponding branch on remote" t) |
| 195 | (const :tag "... but only if remote is on a forge" forge-only))) |
| 196 | |
| 197 | (defcustom magit-branch-direct-configure t |
| 198 | "Whether the command `magit-branch' shows Git variables. |
| 199 | When set to nil, no variables are displayed by this transient |
| 200 | command, instead the sub-transient `magit-branch-configure' |
| 201 | has to be used to view and change branch related variables." |
| 202 | :package-version '(magit . "2.7.0") |
| 203 | :group 'magit-commands |
| 204 | :type 'boolean) |
| 205 | |
| 206 | (defcustom magit-published-branches '("origin/master") |
| 207 | "List of branches that are considered to be published." |
| 208 | :package-version '(magit . "2.13.0") |
| 209 | :group 'magit-commands |
| 210 | :type '(repeat string)) |
| 211 | |
| 212 | ;;; Commands |
| 213 | |
| 214 | ;;;###autoload(autoload 'magit-branch "magit" nil t) |
| 215 | (transient-define-prefix magit-branch (branch) |
| 216 | "Add, configure or remove a branch." |
| 217 | :man-page "git-branch" |
| 218 | :refresh-suffixes t |
| 219 | [:if (##and magit-branch-direct-configure (transient-scope)) |
| 220 | :description (##concat |
| 221 | (propertize "Configure " 'face 'transient-heading) |
| 222 | (propertize (transient-scope) 'face 'magit-branch-local)) |
| 223 | ("d" magit-branch.<branch>.description) |
| 224 | ("u" magit-branch.<branch>.merge/remote) |
| 225 | ("r" magit-branch.<branch>.rebase) |
| 226 | ("p" magit-branch.<branch>.pushRemote)] |
| 227 | [:if-non-nil magit-branch-direct-configure |
| 228 | :description "Configure repository defaults" |
| 229 | ("R" magit-pull.rebase) |
| 230 | ("P" magit-remote.pushDefault) |
| 231 | ("B" "Update default branch" magit-update-default-branch |
| 232 | :inapt-if-not magit-get-some-remote)] |
| 233 | ["Arguments" |
| 234 | (6 "-m" "Automatically merge conflicting local modifications" "--merge") |
| 235 | (7 "-r" "Recurse submodules when checking out an existing branch" |
| 236 | "--recurse-submodules")] |
| 237 | [["Checkout" |
| 238 | ("b" "branch/revision" magit-checkout) |
| 239 | ("l" "local branch" magit-branch-checkout) |
| 240 | (6 "o" "new orphan" magit-branch-orphan) |
| 241 | (0 "r" "remote ref" magit-checkout-remote-ref)] |
| 242 | ["" |
| 243 | ("c" "new branch" magit-branch-and-checkout) |
| 244 | ("s" "new spin-off" magit-branch-spinoff) |
| 245 | (5 "w" "new worktree" magit-worktree-checkout)] |
| 246 | ["Create" |
| 247 | ("n" "new branch" magit-branch-create) |
| 248 | ("S" "new spin-out" magit-branch-spinout) |
| 249 | (5 "W" "new worktree" magit-worktree-branch)] |
| 250 | ["Do" |
| 251 | ("C" "configure..." magit-branch-configure) |
| 252 | ("m" "rename" magit-branch-rename) |
| 253 | ("x" "reset" magit-branch-reset) |
| 254 | ("k" "delete" magit-branch-delete)] |
| 255 | ["" |
| 256 | (7 "h" "shelve" magit-branch-shelve) |
| 257 | (7 "H" "unshelve" magit-branch-unshelve)]] |
| 258 | (interactive (list (magit-get-current-branch))) |
| 259 | (transient-setup 'magit-branch nil nil :scope branch)) |
| 260 | |
| 261 | (defun magit-branch-arguments () |
| 262 | (transient-args 'magit-branch)) |
| 263 | |
| 264 | ;;;###autoload |
| 265 | (defun magit-checkout (commit &optional args) |
| 266 | "Checkout COMMIT, updating the index and the working tree. |
| 267 | If COMMIT is a local branch, then that becomes the current |
| 268 | branch. If it is something else, then `HEAD' becomes detached. |
| 269 | Checkout fails if the working tree or the staging area contain |
| 270 | changes. |
| 271 | \n(git checkout COMMIT)." |
| 272 | (declare (interactive-only magit--checkout)) |
| 273 | (interactive (list (magit-read-other-branch-or-commit "Checkout") |
| 274 | (magit-branch-arguments))) |
| 275 | (when (string-match "\\`heads/\\(.+\\)" commit) |
| 276 | (setq commit (match-str 1 commit))) |
| 277 | (magit-run-git-async "checkout" args commit)) |
| 278 | |
| 279 | (defun magit--checkout (rev &optional args) |
| 280 | (when (string-match "\\`heads/\\(.+\\)" rev) |
| 281 | (setq rev (match-str 1 rev))) |
| 282 | (magit-call-git "checkout" args rev)) |
| 283 | |
| 284 | ;;;###autoload |
| 285 | (defun magit-branch-create (branch start-point) |
| 286 | "Create BRANCH at branch or revision START-POINT." |
| 287 | (declare (interactive-only magit-call-git)) |
| 288 | (interactive (magit-branch-read-args "Create branch")) |
| 289 | (magit-run-git-async "branch" branch start-point) |
| 290 | (set-process-sentinel |
| 291 | magit-this-process |
| 292 | (lambda (process event) |
| 293 | (when (memq (process-status process) '(exit signal)) |
| 294 | (magit-branch-maybe-adjust-upstream branch start-point) |
| 295 | (magit-process-sentinel process event))))) |
| 296 | |
| 297 | ;;;###autoload |
| 298 | (defun magit-branch-and-checkout (branch start-point &optional args) |
| 299 | "Create and checkout BRANCH at branch or revision START-POINT." |
| 300 | (declare (interactive-only magit-call-git)) |
| 301 | (interactive (append (magit-branch-read-args "Create and checkout branch") |
| 302 | (list (magit-branch-arguments)))) |
| 303 | (if (string-match-p "^stash@{[0-9]+}$" start-point) |
| 304 | (magit-run-git "stash" "branch" branch start-point) |
| 305 | (magit-run-git-async "checkout" args "-b" branch start-point) |
| 306 | (set-process-sentinel |
| 307 | magit-this-process |
| 308 | (lambda (process event) |
| 309 | (when (memq (process-status process) '(exit signal)) |
| 310 | (magit-branch-maybe-adjust-upstream branch start-point) |
| 311 | (magit-process-sentinel process event)))))) |
| 312 | |
| 313 | ;;;###autoload |
| 314 | (defun magit-branch-or-checkout (arg &optional start-point) |
| 315 | "Hybrid between `magit-checkout' and `magit-branch-and-checkout'. |
| 316 | |
| 317 | Ask the user for an existing branch or revision. If the user |
| 318 | input actually can be resolved as a branch or revision, then |
| 319 | check that out, just like `magit-checkout' would. |
| 320 | |
| 321 | Otherwise create and checkout a new branch using the input as |
| 322 | its name. Before doing so read the starting-point for the new |
| 323 | branch. This is similar to what `magit-branch-and-checkout' |
| 324 | does." |
| 325 | (declare (interactive-only magit-call-git)) |
| 326 | (interactive |
| 327 | (let ((arg (magit-read-other-branch-or-commit "Checkout"))) |
| 328 | (list arg |
| 329 | (and (not (magit-commit-p arg)) |
| 330 | (magit-read-starting-point "Create and checkout branch" arg))))) |
| 331 | (when (string-match "\\`heads/\\(.+\\)" arg) |
| 332 | (setq arg (match-str 1 arg))) |
| 333 | (if start-point |
| 334 | (with-suppressed-warnings ((interactive-only magit-branch-and-checkout)) |
| 335 | (magit-branch-and-checkout arg start-point)) |
| 336 | (magit--checkout arg) |
| 337 | (magit-refresh))) |
| 338 | |
| 339 | ;;;###autoload |
| 340 | (defun magit-branch-checkout (branch &optional start-point) |
| 341 | "Checkout an existing or new local branch. |
| 342 | |
| 343 | Read a branch name from the user offering all local branches and |
| 344 | a subset of remote branches as candidates. Omit remote branches |
| 345 | for which a local branch by the same name exists from the list |
| 346 | of candidates. The user can also enter a completely new branch |
| 347 | name. |
| 348 | |
| 349 | - If the user selects an existing local branch, then check that |
| 350 | out. |
| 351 | |
| 352 | - If the user selects a remote branch, then create and checkout |
| 353 | a new local branch with the same name. Configure the selected |
| 354 | remote branch as push target. |
| 355 | |
| 356 | - If the user enters a new branch name, then create and check |
| 357 | that out, after also reading the starting-point from the user. |
| 358 | |
| 359 | In the latter two cases the upstream is also set. Whether it is |
| 360 | set to the chosen START-POINT or something else depends on the |
| 361 | value of `magit-branch-adjust-remote-upstream-alist', just like |
| 362 | when using `magit-branch-and-checkout'." |
| 363 | (declare (interactive-only magit-call-git)) |
| 364 | (interactive |
| 365 | (let* ((current (magit-get-current-branch)) |
| 366 | (local (magit-list-local-branch-names)) |
| 367 | (remote (seq-filter (##and (string-match "[^/]+/" %) |
| 368 | (not (member (substring % (match-end 0)) |
| 369 | (cons "HEAD" local)))) |
| 370 | (magit-list-remote-branch-names))) |
| 371 | (choices (nconc (delete current local) remote)) |
| 372 | (atpoint (magit-branch-at-point)) |
| 373 | (choice (magit-completing-read |
| 374 | "Checkout branch" choices |
| 375 | nil nil nil 'magit-revision-history |
| 376 | (or (car (member atpoint choices)) |
| 377 | (and atpoint |
| 378 | (car (member (and (string-match "[^/]+/" atpoint) |
| 379 | (substring atpoint (match-end 0))) |
| 380 | choices))))))) |
| 381 | (cond ((member choice remote) |
| 382 | (list (and (string-match "[^/]+/" choice) |
| 383 | (substring choice (match-end 0))) |
| 384 | choice)) |
| 385 | ((member choice local) |
| 386 | (list choice)) |
| 387 | ((list choice (magit-read-starting-point "Create" choice)))))) |
| 388 | (cond |
| 389 | ((not start-point) |
| 390 | (magit--checkout branch (magit-branch-arguments)) |
| 391 | (magit-refresh)) |
| 392 | (t |
| 393 | (when (and (magit-anything-modified-p t) |
| 394 | (not (transient-arg-value "--merge" (magit-branch-arguments)))) |
| 395 | (user-error "Cannot checkout when there are uncommitted changes")) |
| 396 | (magit-run-git-async "checkout" (magit-branch-arguments) |
| 397 | "-b" branch start-point) |
| 398 | (set-process-sentinel |
| 399 | magit-this-process |
| 400 | (lambda (process event) |
| 401 | (when (memq (process-status process) '(exit signal)) |
| 402 | (magit-branch-maybe-adjust-upstream branch start-point) |
| 403 | (when (magit-remote-branch-p start-point) |
| 404 | (pcase-let ((`(,remote . ,remote-branch) |
| 405 | (magit-split-branch-name start-point))) |
| 406 | (when (and (equal branch remote-branch) |
| 407 | (not (equal remote (magit-get "remote.pushDefault")))) |
| 408 | (magit-set remote "branch" branch "pushRemote")))) |
| 409 | (magit-process-sentinel process event))))))) |
| 410 | |
| 411 | (defun magit-branch-maybe-adjust-upstream (branch start-point) |
| 412 | (when-let ((upstream |
| 413 | (or (and (magit-get-upstream-branch branch) |
| 414 | (magit-get-indirect-upstream-branch start-point)) |
| 415 | (and (magit-remote-branch-p start-point) |
| 416 | (let ((name (cdr (magit-split-branch-name start-point)))) |
| 417 | (seq-some |
| 418 | (pcase-lambda (`(,upstream . ,rule)) |
| 419 | (and (magit-branch-p upstream) |
| 420 | (if (listp rule) |
| 421 | (not (member name rule)) |
| 422 | (string-match-p rule name)) |
| 423 | upstream)) |
| 424 | magit-branch-adjust-remote-upstream-alist)))))) |
| 425 | (magit-call-git "branch" (concat "--set-upstream-to=" upstream) branch))) |
| 426 | |
| 427 | ;;;###autoload |
| 428 | (defun magit-branch-orphan (branch start-point) |
| 429 | "Create and checkout an orphan BRANCH with contents from revision START-POINT." |
| 430 | (interactive (magit-branch-read-args "Create and checkout orphan branch")) |
| 431 | (magit-run-git "checkout" "--orphan" branch start-point)) |
| 432 | |
| 433 | ;;;###autoload |
| 434 | (defun magit-checkout-remote-ref (remote ref) |
| 435 | "Checkout reference REF from REMOTE. |
| 436 | |
| 437 | This command queries the REMOTE for a list of its references. After |
| 438 | the user has selected on of them, it fetches just that, and finally |
| 439 | it checks out \"FETCH_HEAD\", which now refers to the same commit as |
| 440 | REF does on REMOTE. |
| 441 | |
| 442 | This is only useful if you usually only fetch a subset of the refs |
| 443 | from REMOTE. Otherwise it is better to use `magit-checkout', as |
| 444 | that avoids a round-trip." |
| 445 | (declare (interactive-only magit-call-git)) |
| 446 | (interactive |
| 447 | (let ((remote (magit-read-remote "Checkout ref from remote" nil t))) |
| 448 | (list remote |
| 449 | (magit-completing-read |
| 450 | "Fetch and checkout ref" |
| 451 | (prog2 (message "Fetching list of remote refs...") |
| 452 | (magit-remote-list-refs remote) |
| 453 | (message "Fetching list of remote refs...done")))))) |
| 454 | (magit-run-git-async "fetch" remote ref) |
| 455 | (set-process-sentinel |
| 456 | magit-this-process |
| 457 | (lambda (process _event) |
| 458 | (when (memq (process-status process) '(exit signal)) |
| 459 | (magit--checkout "FETCH_HEAD") |
| 460 | (magit-refresh))))) |
| 461 | |
| 462 | (defun magit-branch-read-args (prompt &optional default-start) |
| 463 | (cond-let |
| 464 | ((not magit-branch-read-upstream-first) |
| 465 | (let ((name (magit-branch--read-name (concat prompt " named")))) |
| 466 | (list name (magit-read-starting-point prompt name default-start)))) |
| 467 | [[start (magit-read-starting-point prompt nil default-start)]] |
| 468 | ((magit-rev-verify start) |
| 469 | (list (magit-branch--read-name |
| 470 | (if magit-completing-read--silent-default |
| 471 | (format "%s (starting at `%s')" prompt start) |
| 472 | "Name for new branch") |
| 473 | start) |
| 474 | start)) |
| 475 | [[name start]] |
| 476 | ((eq magit-branch-read-upstream-first 'fallback) |
| 477 | (list name (magit-read-starting-point prompt name default-start))) |
| 478 | ((user-error "Not a valid starting-point: %s" name)))) |
| 479 | |
| 480 | (defun magit-branch--read-name (prompt &optional start-point) |
| 481 | (let ((taken (magit-list-local-branch-names)) |
| 482 | (choices magit-branch-name-suggestions)) |
| 483 | (when (and start-point |
| 484 | (magit-remote-branch-p start-point) |
| 485 | (string-match "/" start-point)) |
| 486 | (cl-pushnew (substring start-point (match-end 0)) |
| 487 | choices :test #'equal)) |
| 488 | (magit-completing-read |
| 489 | prompt (seq-difference choices taken) nil |
| 490 | ;; Ivy does not handle the PREDICATE correctly, so give |
| 491 | ;; up on any validation for the time being when that is |
| 492 | ;; being used. See #5599, #5581, and the proposed fix |
| 493 | ;; https://github.com/abo-abo/swiper/pull/3083. |
| 494 | (and (not (bound-and-true-p ivy-mode)) |
| 495 | (lambda (&optional choice) |
| 496 | (cond |
| 497 | ((not choice) |
| 498 | ;; ( This is currently dead code, see above comment. Even |
| 499 | ;; with this, Ivy users would always have to confirm and |
| 500 | ;; the below check would remain unreachable. ) |
| 501 | ;; This function ought to be called with one argument (see |
| 502 | ;; `completing-read') but Ivy calls it with zero arguments. |
| 503 | ;; Since Ivy doesn't tell us what choice the user made, we |
| 504 | ;; also cannot validate it, and assume it is valid. |
| 505 | t) |
| 506 | ((member choice taken) |
| 507 | (run-with-timer |
| 508 | 0 nil (##minibuffer-message "conflicts with existing branch")) |
| 509 | nil) |
| 510 | ((not (magit-git-success "check-ref-format" "--branch" choice)) |
| 511 | (run-with-timer |
| 512 | 0 nil (##minibuffer-message "not a valid branch name")) |
| 513 | nil) |
| 514 | (t))))))) |
| 515 | |
| 516 | ;;;###autoload |
| 517 | (defun magit-branch-spinout (branch &optional from) |
| 518 | "Create new branch from the unpushed commits. |
| 519 | Like `magit-branch-spinoff' but remain on the current branch. |
| 520 | If there are any uncommitted changes, then behave exactly like |
| 521 | `magit-branch-spinoff'." |
| 522 | (interactive (list (magit-read-string-ns "Spin out branch") |
| 523 | (car (last (magit-region-values 'commit))))) |
| 524 | (magit--branch-spinoff branch from nil)) |
| 525 | |
| 526 | ;;;###autoload |
| 527 | (defun magit-branch-spinoff (branch &optional from) |
| 528 | "Create new branch from the unpushed commits. |
| 529 | |
| 530 | Create and checkout a new branch starting at and tracking the |
| 531 | current branch. That branch in turn is reset to the last commit |
| 532 | it shares with its upstream. If the current branch has no |
| 533 | upstream or no unpushed commits, then the new branch is created |
| 534 | anyway and the previously current branch is not touched. |
| 535 | |
| 536 | This is useful to create a feature branch after work has already |
| 537 | began on the old branch (likely but not necessarily \"master\"). |
| 538 | |
| 539 | If the current branch is a member of the value of option |
| 540 | `magit-branch-prefer-remote-upstream' (which see), then the |
| 541 | current branch will be used as the starting point as usual, but |
| 542 | the upstream of the starting-point may be used as the upstream |
| 543 | of the new branch, instead of the starting-point itself. |
| 544 | |
| 545 | If optional FROM is non-nil, then the source branch is reset |
| 546 | to `FROM~', instead of to the last commit it shares with its |
| 547 | upstream. Interactively, FROM is only ever non-nil, if the |
| 548 | region selects some commits, and among those commits, FROM is |
| 549 | the commit that is the fewest commits ahead of the source |
| 550 | branch. |
| 551 | |
| 552 | The commit at the other end of the selection actually does not |
| 553 | matter, all commits between FROM and `HEAD' are moved to the new |
| 554 | branch. If FROM is not reachable from `HEAD' or is reachable |
| 555 | from the source branch's upstream, then an error is raised." |
| 556 | (interactive (list (magit-read-string-ns "Spin off branch") |
| 557 | (car (last (magit-region-values 'commit))))) |
| 558 | (magit--branch-spinoff branch from t)) |
| 559 | |
| 560 | (defun magit--branch-spinoff (branch from checkout) |
| 561 | (when (magit-branch-p branch) |
| 562 | (user-error "Cannot spin off %s. It already exists" branch)) |
| 563 | (when (and (not checkout) |
| 564 | (magit-anything-modified-p)) |
| 565 | (message "Staying on HEAD due to uncommitted changes") |
| 566 | (setq checkout t)) |
| 567 | (cond-let |
| 568 | ([current (magit-get-current-branch)] |
| 569 | (let ((tracked (magit-get-upstream-branch current)) |
| 570 | base) |
| 571 | (when from |
| 572 | (unless (magit-rev-ancestor-p from current) |
| 573 | (user-error "Cannot spin off %s. %s is not reachable from %s" |
| 574 | branch from current)) |
| 575 | (when (and tracked |
| 576 | (magit-rev-ancestor-p from tracked)) |
| 577 | (user-error "Cannot spin off %s. %s is ancestor of upstream %s" |
| 578 | branch from tracked))) |
| 579 | (let ((magit-process-raise-error t)) |
| 580 | (if checkout |
| 581 | (magit-call-git "checkout" "-b" branch current) |
| 582 | (magit-call-git "branch" branch current))) |
| 583 | (when$ (magit-get-indirect-upstream-branch current) |
| 584 | (magit-call-git "branch" "--set-upstream-to" $ branch)) |
| 585 | (when (and tracked |
| 586 | (setq base |
| 587 | (if from |
| 588 | (concat from "^") |
| 589 | (magit-merge-base current tracked))) |
| 590 | (not (magit-rev-eq base current))) |
| 591 | (if checkout |
| 592 | (magit-call-git "update-ref" "-m" |
| 593 | (format "reset: moving to %s" base) |
| 594 | (concat "refs/heads/" current) base) |
| 595 | (magit-call-git "reset" "--hard" base))))) |
| 596 | (checkout |
| 597 | (magit-call-git "checkout" "-b" branch)) |
| 598 | ((magit-call-git "branch" branch))) |
| 599 | (magit-refresh)) |
| 600 | |
| 601 | ;;;###autoload |
| 602 | (defun magit-branch-reset (branch to &optional set-upstream) |
| 603 | "Reset a branch to the tip of another branch or any other commit. |
| 604 | |
| 605 | When the branch being reset is the current branch, then do a |
| 606 | hard reset. If there are any uncommitted changes, then the user |
| 607 | has to confirm the reset because those changes would be lost. |
| 608 | |
| 609 | This is useful when you have started work on a feature branch but |
| 610 | realize it's all crap and want to start over. |
| 611 | |
| 612 | When resetting to another branch and a prefix argument is used, |
| 613 | then also set the target branch as the upstream of the branch |
| 614 | that is being reset." |
| 615 | (interactive |
| 616 | (let ((branch (magit-read-local-branch "Reset branch" |
| 617 | (magit-local-branch-at-point)))) |
| 618 | (list branch |
| 619 | (magit-read-branch-or-commit (format "Reset %s to" branch) |
| 620 | (magit-get-upstream-branch branch) |
| 621 | branch) |
| 622 | current-prefix-arg))) |
| 623 | (let ((magit-inhibit-refresh t)) |
| 624 | (if (equal branch (magit-get-current-branch)) |
| 625 | (if (and (magit-anything-modified-p) |
| 626 | (not (yes-or-no-p |
| 627 | "Uncommitted changes will be lost. Proceed? "))) |
| 628 | (user-error "Abort") |
| 629 | (magit-reset-hard to)) |
| 630 | (magit-call-git "update-ref" |
| 631 | "-m" (format "reset: moving to %s" to) |
| 632 | (magit-ref-fullname branch) |
| 633 | to)) |
| 634 | (when (and set-upstream (magit-branch-p to)) |
| 635 | (magit-set-upstream-branch branch to) |
| 636 | (magit-branch-maybe-adjust-upstream branch to))) |
| 637 | (magit-refresh)) |
| 638 | |
| 639 | (defvar magit-branch-delete-never-verify nil |
| 640 | "Whether `magit-branch-delete' always pushes with \"--no-verify\".") |
| 641 | |
| 642 | ;;;###autoload |
| 643 | (defun magit-branch-delete (branches &optional force) |
| 644 | "Delete one or multiple branches. |
| 645 | |
| 646 | If the region marks multiple branches, then offer to delete |
| 647 | those, otherwise prompt for a single branch to be deleted, |
| 648 | defaulting to the branch at point. |
| 649 | |
| 650 | Require confirmation when deleting branches is dangerous in some |
| 651 | way. Option `magit-no-confirm' can be customized to not require |
| 652 | confirmation in certain cases. See its docstring to learn why |
| 653 | confirmation is required by default in certain cases or if a |
| 654 | prompt is confusing." |
| 655 | ;; One would expect this to be a command as simple as, for example, |
| 656 | ;; `magit-branch-rename'; but it turns out everyone wants to squeeze |
| 657 | ;; a bit of extra functionality into this one, including myself. |
| 658 | (interactive |
| 659 | (let ((branches (magit-region-values 'branch t)) |
| 660 | (force current-prefix-arg)) |
| 661 | (if (length> branches 1) |
| 662 | (magit-confirm t nil "Delete %d branches" nil branches) |
| 663 | (setq branches |
| 664 | (list (magit-read-branch-prefer-other |
| 665 | (if force "Force delete branch" "Delete branch"))))) |
| 666 | (cond-let |
| 667 | (force) |
| 668 | [[unmerged (seq-remove #'magit-branch-merged-p branches)]] |
| 669 | ((magit-confirm 'delete-unmerged-branch |
| 670 | "Delete unmerged branch %s" |
| 671 | "Delete %d unmerged branches" |
| 672 | 'noabort unmerged) |
| 673 | (setq force branches)) |
| 674 | ((setq branches (cl-set-difference branches unmerged :test #'equal))) |
| 675 | ((user-error "Abort"))) |
| 676 | (list branches force))) |
| 677 | (let ((refs (mapcar #'magit-ref-fullname branches))) |
| 678 | ;; If a member of refs is nil, that means that |
| 679 | ;; the respective branch name is ambiguous. |
| 680 | (when-let ((ambiguous (seq-filter #'null refs))) |
| 681 | (user-error |
| 682 | "%s ambiguous; please cleanup using git directly" |
| 683 | (let ((len (length ambiguous))) |
| 684 | (cond |
| 685 | ((= len 1) |
| 686 | (format "%s is" (seq-find #'magit-ref-ambiguous-p branches))) |
| 687 | ((= len (length refs)) |
| 688 | (format "These %s names are" len)) |
| 689 | ((format "%s of these names are" len)))))) |
| 690 | (cond |
| 691 | ((string-prefix-p "refs/remotes/" (car refs)) |
| 692 | (let* ((remote (car (magit-split-branch-name (substring (car refs) 13)))) |
| 693 | (offset (1+ (length remote)))) |
| 694 | (cond |
| 695 | ((magit-confirm 'delete-branch-on-remote |
| 696 | (list "Deleting local %s. Also delete on %s" |
| 697 | (magit-ref-fullname (car branches)) |
| 698 | remote) |
| 699 | (list "Deleting %d local refs. Also delete on %s" |
| 700 | (length refs) |
| 701 | remote) |
| 702 | 'noabort refs) |
| 703 | ;; The ref may actually point at another rev on the remote, |
| 704 | ;; but this is better than nothing. |
| 705 | (dolist (ref refs) |
| 706 | (message "Delete %s (was %s)" ref |
| 707 | (magit-rev-parse "--short" ref))) |
| 708 | ;; Assume the branches actually still exist on the remote. |
| 709 | (magit-run-git-async |
| 710 | "push" "--delete" |
| 711 | (and (or force magit-branch-delete-never-verify) "--no-verify") |
| 712 | remote |
| 713 | (mapcar (##concat "refs/heads/" (substring % offset)) branches)) |
| 714 | ;; If that is not the case, then this deletes the tracking branches. |
| 715 | (set-process-sentinel |
| 716 | magit-this-process |
| 717 | (apply-partially #'magit-delete-remote-branch-sentinel remote refs))) |
| 718 | (t |
| 719 | (dolist (ref refs) |
| 720 | (message "Delete %s (was %s)" ref |
| 721 | (magit-rev-parse "--short" ref)) |
| 722 | (magit-call-git "update-ref" "-d" ref)) |
| 723 | (magit-refresh))))) |
| 724 | ((length> branches 1) |
| 725 | (setq branches (delete (magit-get-current-branch) branches)) |
| 726 | (mapc #'magit-branch-maybe-delete-pr-remote branches) |
| 727 | (mapc #'magit-branch-unset-pushRemote branches) |
| 728 | (magit-run-git "branch" (if force "-D" "-d") branches)) |
| 729 | (t ; And now for something completely different. |
| 730 | (let* ((branch (car branches)) |
| 731 | (prompt (format "Branch %s is checked out. " branch)) |
| 732 | (target (magit-get-indirect-upstream-branch branch t))) |
| 733 | (when (equal branch (magit-get-current-branch)) |
| 734 | (when (or (equal branch target) |
| 735 | (not target)) |
| 736 | (setq target (magit-main-branch))) |
| 737 | (pcase (if (or (equal branch target) |
| 738 | (not target)) |
| 739 | (magit-read-char-case prompt nil |
| 740 | (?d "[d]etach HEAD & delete" 'detach) |
| 741 | (?a "[a]bort" 'abort)) |
| 742 | (magit-read-char-case prompt nil |
| 743 | (?d "[d]etach HEAD & delete" 'detach) |
| 744 | (?c (format "[c]heckout %s & delete" target) 'target) |
| 745 | (?a "[a]bort" 'abort))) |
| 746 | (`detach (unless (or (equal force '(4)) |
| 747 | (member branch force) |
| 748 | (magit-branch-merged-p branch t)) |
| 749 | (magit-confirm 'delete-unmerged-branch |
| 750 | "Delete unmerged branch %s" "" |
| 751 | nil (list branch))) |
| 752 | (magit-call-git "checkout" "--detach")) |
| 753 | (`target (unless (or (equal force '(4)) |
| 754 | (member branch force) |
| 755 | (magit-branch-merged-p branch target)) |
| 756 | (magit-confirm 'delete-unmerged-branch |
| 757 | "Delete unmerged branch %s" "" |
| 758 | nil (list branch))) |
| 759 | (magit-call-git "checkout" target)) |
| 760 | (`abort (user-error "Abort"))) |
| 761 | (setq force t)) |
| 762 | (magit-branch-maybe-delete-pr-remote branch) |
| 763 | (magit-branch-unset-pushRemote branch) |
| 764 | (magit-run-git "branch" (if force "-D" "-d") branch)))))) |
| 765 | |
| 766 | (put 'magit-branch-delete 'interactive-only t) |
| 767 | |
| 768 | (defun magit-branch-maybe-delete-pr-remote (branch) |
| 769 | (when-let ((remote (magit-get "branch" branch "pullRequestRemote"))) |
| 770 | (let* ((variable (format "remote.%s.fetch" remote)) |
| 771 | (refspecs (magit-get-all variable))) |
| 772 | (unless (member (format "+refs/heads/*:refs/remotes/%s/*" remote) |
| 773 | refspecs) |
| 774 | (let ((refspec |
| 775 | (if (equal (magit-get "branch" branch "pushRemote") remote) |
| 776 | (format "+refs/heads/%s:refs/remotes/%s/%s" |
| 777 | branch remote branch) |
| 778 | (let ((merge (magit-get "branch" branch "merge"))) |
| 779 | (and merge |
| 780 | (string-prefix-p "refs/heads/" merge) |
| 781 | (setq merge (substring merge 11)) |
| 782 | (format "+refs/heads/%s:refs/remotes/%s/%s" |
| 783 | merge remote merge)))))) |
| 784 | (when (member refspec refspecs) |
| 785 | (if (and (length= refspecs 1) |
| 786 | (magit-confirm 'delete-pr-remote |
| 787 | (list "Also delete remote %s (%s)" remote |
| 788 | "no pull-request branch remains") |
| 789 | nil t)) |
| 790 | (magit-call-git "remote" "rm" remote) |
| 791 | (magit-call-git "config" "--unset-all" variable |
| 792 | (format "^%s$" (regexp-quote refspec)))))))))) |
| 793 | |
| 794 | (defun magit-branch-unset-pushRemote (branch) |
| 795 | (magit-set nil "branch" branch "pushRemote")) |
| 796 | |
| 797 | (defun magit-delete-remote-branch-sentinel (remote refs process event) |
| 798 | (cond-let* |
| 799 | ((not (memq (process-status process) '(exit signal)))) |
| 800 | ([_(= (process-exit-status process) 1)] |
| 801 | [on-remote (mapcar (##concat "refs/remotes/" remote "/" %) |
| 802 | (magit-remote-list-branches remote))] |
| 803 | [rest (seq-filter (##and (not (member % on-remote)) |
| 804 | (magit-ref-exists-p %)) |
| 805 | refs)] |
| 806 | (process-put process 'inhibit-refresh t) |
| 807 | (magit-process-sentinel process event) |
| 808 | (setq magit-this-error nil) |
| 809 | (message "Some remote branches no longer exist. %s" |
| 810 | "Deleting just the local tracking refs instead...") |
| 811 | (dolist (ref rest) |
| 812 | (magit-call-git "update-ref" "-d" ref)) |
| 813 | (magit-refresh) |
| 814 | (message "Deleting local remote-tracking refs...done")) |
| 815 | ((magit-process-sentinel process event)))) |
| 816 | |
| 817 | ;;;###autoload |
| 818 | (defun magit-branch-rename (old new &optional force) |
| 819 | "Rename the branch named OLD to NEW. |
| 820 | |
| 821 | With a prefix argument FORCE, rename even if a branch named NEW |
| 822 | already exists. |
| 823 | |
| 824 | If `branch.OLD.pushRemote' is set, then unset it. Depending on |
| 825 | the value of `magit-branch-rename-push-target' (which see) maybe |
| 826 | set `branch.NEW.pushRemote' and maybe rename the push-target on |
| 827 | the remote." |
| 828 | (interactive |
| 829 | (let ((branch (magit-read-local-branch "Rename branch"))) |
| 830 | (list branch |
| 831 | (magit-read-string-ns (format "Rename branch '%s' to" branch) |
| 832 | nil 'magit-revision-history) |
| 833 | current-prefix-arg))) |
| 834 | (when (string-match "\\`heads/\\(.+\\)" old) |
| 835 | (setq old (match-str 1 old))) |
| 836 | (cond ((equal old new) |
| 837 | (user-error "Old and new branch names are the same")) |
| 838 | ((and (magit-local-branch-p new) (not force)) |
| 839 | (user-error "Branch `%s' already exists" new))) |
| 840 | (let* ((push-branch (magit-get-push-branch old)) |
| 841 | (push-remote (magit-get "branch" old "pushRemote")) |
| 842 | (push-default (magit-get "remote.pushDefault")) |
| 843 | (remote (or push-remote push-default))) |
| 844 | (magit-call-git "branch" (if force "-M" "-m") old new) |
| 845 | (when push-remote |
| 846 | (magit-set nil "branch" old "pushRemote") |
| 847 | (magit-set push-remote "branch" new "pushRemote")) |
| 848 | (when (and (magit-rev-verify (concat remote "/" old)) |
| 849 | (not (magit-rev-verify (concat remote "/" new))) |
| 850 | (or (eq magit-branch-rename-push-target t) |
| 851 | (and-let ((_(eq magit-branch-rename-push-target 'forge-only)) |
| 852 | (_(require (quote forge) nil t)) |
| 853 | (_(fboundp 'forge--split-forge-url)) |
| 854 | (url (magit-git-string "remote" "get-url" remote))) |
| 855 | (forge--split-forge-url url))) |
| 856 | (magit-y-or-n-p (format "Also rename %S to %S on \"%s\"?" |
| 857 | old new remote))) |
| 858 | (magit-call-git "push" "-v" remote |
| 859 | (format "%s:refs/heads/%s" push-branch new) |
| 860 | (format ":refs/heads/%s" old)))) |
| 861 | (magit-refresh)) |
| 862 | |
| 863 | ;;;###autoload |
| 864 | (defun magit-branch-shelve (branch) |
| 865 | "Shelve a BRANCH. |
| 866 | Rename \"refs/heads/BRANCH\" to \"refs/shelved/YYYY-MM-DD-BRANCH\", |
| 867 | and also rename the respective reflog file." |
| 868 | (interactive (list (magit-read-other-local-branch "Shelve branch"))) |
| 869 | (let ((old (concat "refs/heads/" branch)) |
| 870 | (new (format "refs/shelved/%s-%s" |
| 871 | (magit-rev-format "%cs" branch) |
| 872 | branch))) |
| 873 | (magit-git "update-ref" new old "") |
| 874 | (magit--rename-reflog-file old new) |
| 875 | (magit-branch-unset-pushRemote branch) |
| 876 | (magit-run-git "branch" "-D" branch))) |
| 877 | |
| 878 | ;;;###autoload |
| 879 | (defun magit-branch-unshelve (branch) |
| 880 | "Unshelve a BRANCH. |
| 881 | Rename \"refs/shelved/BRANCH\" to \"refs/heads/BRANCH\". If BRANCH |
| 882 | is prefixed with \"YYYY-MM-DD\", then drop that part of the name. |
| 883 | Also rename the respective reflog file." |
| 884 | (interactive (list (magit-read-shelved-branch "Unshelve branch"))) |
| 885 | (let ((old (concat "refs/shelved/" branch)) |
| 886 | (new (concat "refs/heads/" |
| 887 | (if (string-match-p |
| 888 | "\\`[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}-" branch) |
| 889 | (substring branch 11) |
| 890 | branch)))) |
| 891 | (magit-git "update-ref" new old "") |
| 892 | (magit--rename-reflog-file old new) |
| 893 | (magit-run-git "update-ref" "-d" old))) |
| 894 | |
| 895 | (defun magit--rename-reflog-file (old new) |
| 896 | (let* ((dir (magit-gitdir)) |
| 897 | (old (expand-file-name (concat "logs/" old) dir)) |
| 898 | (new (expand-file-name (concat "logs/" new) dir))) |
| 899 | (when (file-exists-p old) |
| 900 | (make-directory (file-name-directory new) t) |
| 901 | (rename-file old new t)))) |
| 902 | |
| 903 | ;;; Configure |
| 904 | |
| 905 | ;;;###autoload(autoload 'magit-branch-configure "magit-branch" nil t) |
| 906 | (transient-define-prefix magit-branch-configure (branch) |
| 907 | "Configure a branch." |
| 908 | :man-page "git-branch" |
| 909 | [:description (##concat |
| 910 | (propertize "Configure " 'face 'transient-heading) |
| 911 | (propertize (transient-scope) 'face 'magit-branch-local)) |
| 912 | ("d" magit-branch.<branch>.description) |
| 913 | ("u" magit-branch.<branch>.merge/remote) |
| 914 | ("r" magit-branch.<branch>.rebase) |
| 915 | ("p" magit-branch.<branch>.pushRemote)] |
| 916 | ["Configure repository defaults" |
| 917 | ("R" magit-pull.rebase) |
| 918 | ("P" magit-remote.pushDefault) |
| 919 | ("B" "Update default branch" magit-update-default-branch |
| 920 | :inapt-if-not magit-get-some-remote)] |
| 921 | ["Configure branch creation" |
| 922 | ("a m" magit-branch.autoSetupMerge) |
| 923 | ("a r" magit-branch.autoSetupRebase)] |
| 924 | (interactive |
| 925 | (list (or (and (not current-prefix-arg) |
| 926 | (not (and magit-branch-direct-configure |
| 927 | (eq transient-current-command 'magit-branch))) |
| 928 | (magit-get-current-branch)) |
| 929 | (magit--read-branch-scope)))) |
| 930 | (transient-setup 'magit-branch-configure nil nil :scope branch)) |
| 931 | |
| 932 | (defun magit--read-branch-scope (&optional obj) |
| 933 | (magit-read-local-branch |
| 934 | (if obj |
| 935 | (format "Set %s for branch" |
| 936 | (format (oref obj variable) "<name>")) |
| 937 | "Configure branch"))) |
| 938 | |
| 939 | (transient-define-suffix magit-branch.<branch>.description (branch) |
| 940 | "Edit the description of BRANCH." |
| 941 | :class 'magit--git-variable |
| 942 | :transient nil |
| 943 | :variable "branch.%s.description" |
| 944 | (interactive (list (oref transient-current-prefix scope))) |
| 945 | (magit-run-git-with-editor "branch" "--edit-description" branch)) |
| 946 | |
| 947 | (defclass magit--git-branch:upstream (magit--git-variable) |
| 948 | ((format :initform " %k %m %M\n %r %R") |
| 949 | (accessible-format :initform "%k %m is %M and %r is %R"))) |
| 950 | |
| 951 | (transient-define-infix magit-branch.<branch>.merge/remote () |
| 952 | :class 'magit--git-branch:upstream) |
| 953 | |
| 954 | (cl-defmethod transient-init-value ((obj magit--git-branch:upstream)) |
| 955 | (when-let* ((branch (transient-scope)) |
| 956 | (remote (magit-get "branch" branch "remote")) |
| 957 | (merge (magit-get "branch" branch "merge"))) |
| 958 | (oset obj value (list remote merge)))) |
| 959 | |
| 960 | (cl-defmethod transient-infix-read ((obj magit--git-branch:upstream)) |
| 961 | (if (oref obj value) |
| 962 | (oset obj value nil) |
| 963 | (magit-read-upstream-branch (transient-scope) "Upstream"))) |
| 964 | |
| 965 | (cl-defmethod transient-infix-set ((obj magit--git-branch:upstream) refname) |
| 966 | (magit-set-upstream-branch (transient-scope) refname) |
| 967 | (oset obj value |
| 968 | (and-let* ((branch (transient-scope)) |
| 969 | (r (magit-get "branch" branch "remote")) |
| 970 | (m (magit-get "branch" branch "merge"))) |
| 971 | (list r m))) |
| 972 | (magit-refresh)) |
| 973 | |
| 974 | (cl-defmethod transient-format ((obj magit--git-branch:upstream)) |
| 975 | (let ((branch (transient-scope))) |
| 976 | (format-spec |
| 977 | (transient--get-format obj) |
| 978 | `((?k . ,(transient-format-key obj)) |
| 979 | (?r . ,(format "branch.%s.remote" branch)) |
| 980 | (?m . ,(format "branch.%s.merge" branch)) |
| 981 | (?R . ,(transient-format-value obj #'car)) |
| 982 | (?M . ,(transient-format-value obj #'cadr)))))) |
| 983 | |
| 984 | (cl-defmethod transient-format-value ((obj magit--git-branch:upstream) key) |
| 985 | (if-let ((value (funcall key (oref obj value)))) |
| 986 | (propertize value 'face 'transient-argument) |
| 987 | (propertize "unset" 'face 'transient-inactive-argument))) |
| 988 | |
| 989 | (transient-define-infix magit-branch.<branch>.rebase () |
| 990 | :class 'magit--git-variable:choices |
| 991 | :scope #'magit--read-branch-scope |
| 992 | :variable "branch.%s.rebase" |
| 993 | :fallback "pull.rebase" |
| 994 | :choices '("true" "merges" "interactive" "false") |
| 995 | :default "false") |
| 996 | |
| 997 | (transient-define-infix magit-branch.<branch>.pushRemote () |
| 998 | :class 'magit--git-variable:choices |
| 999 | :scope #'magit--read-branch-scope |
| 1000 | :variable "branch.%s.pushRemote" |
| 1001 | :fallback "remote.pushDefault" |
| 1002 | :choices #'magit-list-remotes) |
| 1003 | |
| 1004 | (transient-define-infix magit-pull.rebase () |
| 1005 | :class 'magit--git-variable:choices |
| 1006 | :variable "pull.rebase" |
| 1007 | :choices '("true" "merges" "interactive" "false") |
| 1008 | :default "false") |
| 1009 | |
| 1010 | (transient-define-infix magit-remote.pushDefault () |
| 1011 | :class 'magit--git-variable:choices |
| 1012 | :variable "remote.pushDefault" |
| 1013 | :choices #'magit-list-remotes) |
| 1014 | |
| 1015 | (transient-define-infix magit-branch.autoSetupMerge () |
| 1016 | :class 'magit--git-variable:choices |
| 1017 | :variable "branch.autoSetupMerge" |
| 1018 | :choices '("always" "true" "false") |
| 1019 | :default "true") |
| 1020 | |
| 1021 | (transient-define-infix magit-branch.autoSetupRebase () |
| 1022 | :class 'magit--git-variable:choices |
| 1023 | :variable "branch.autoSetupRebase" |
| 1024 | :choices '("always" "local" "remote" "never") |
| 1025 | :default "never") |
| 1026 | |
| 1027 | ;;; _ |
| 1028 | (provide 'magit-branch) |
| 1029 | ;; Local Variables: |
| 1030 | ;; read-symbol-shorthands: ( |
| 1031 | ;; ("and$" . "cond-let--and$") |
| 1032 | ;; ("thread$" . "cond-let--thread$") |
| 1033 | ;; ("when$" . "cond-let--when$") |
| 1034 | ;; ("and-let*" . "cond-let--and-let*") |
| 1035 | ;; ("and-let" . "cond-let--and-let") |
| 1036 | ;; ("if-let*" . "cond-let--if-let*") |
| 1037 | ;; ("if-let" . "cond-let--if-let") |
| 1038 | ;; ("when-let*" . "cond-let--when-let*") |
| 1039 | ;; ("when-let" . "cond-let--when-let") |
| 1040 | ;; ("while-let*" . "cond-let--while-let*") |
| 1041 | ;; ("while-let" . "cond-let--while-let") |
| 1042 | ;; ("match-string" . "match-string") |
| 1043 | ;; ("match-str" . "match-string-no-properties")) |
| 1044 | ;; End: |
| 1045 | ;;; magit-branch.el ends here |