| 1 | ;;; magit-refs.el --- Listing references -*- 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 listing references in a buffer. |
| 26 | |
| 27 | ;;; Code: |
| 28 | |
| 29 | (require 'magit) |
| 30 | |
| 31 | ;;; Options |
| 32 | |
| 33 | (defgroup magit-refs nil |
| 34 | "Inspect and manipulate Git branches and tags." |
| 35 | :link '(info-link "(magit)References Buffer") |
| 36 | :group 'magit-modes) |
| 37 | |
| 38 | (defcustom magit-refs-mode-hook nil |
| 39 | "Hook run after entering Magit-Refs mode." |
| 40 | :package-version '(magit . "2.1.0") |
| 41 | :group 'magit-refs |
| 42 | :type 'hook) |
| 43 | |
| 44 | (defcustom magit-refs-sections-hook |
| 45 | (list #'magit-insert-error-header |
| 46 | #'magit-insert-branch-description |
| 47 | #'magit-insert-local-branches |
| 48 | #'magit-insert-remote-branches |
| 49 | #'magit-insert-tags) |
| 50 | "Hook run to insert sections into a references buffer." |
| 51 | :package-version '(magit . "2.1.0") |
| 52 | :group 'magit-refs |
| 53 | :type 'hook) |
| 54 | |
| 55 | (defcustom magit-refs-show-commit-count nil |
| 56 | "Whether to show commit counts in Magit-Refs mode buffers. |
| 57 | |
| 58 | all Show counts for branches and tags. |
| 59 | branch Show counts for branches only. |
| 60 | nil Never show counts. |
| 61 | |
| 62 | To change the value in an existing buffer use the command |
| 63 | `magit-refs-set-show-commit-count'." |
| 64 | :package-version '(magit . "2.1.0") |
| 65 | :group 'magit-refs |
| 66 | :local 'permanent-only |
| 67 | :safe (##memq % '(all branch nil)) |
| 68 | :type '(choice (const :tag "For branches and tags" all) |
| 69 | (const :tag "For branches only" branch) |
| 70 | (const :tag "Never" nil))) |
| 71 | (put 'magit-refs-show-commit-count 'permanent-local t) ;< 31.1 |
| 72 | |
| 73 | (defcustom magit-refs-pad-commit-counts nil |
| 74 | "Whether to pad all counts on all sides in `magit-refs-mode' buffers. |
| 75 | |
| 76 | If this is nil, then some commit counts are displayed right next |
| 77 | to one of the branches that appear next to the count, without any |
| 78 | space in between. This might look bad if the branch name faces |
| 79 | look too similar to `magit-dimmed'. |
| 80 | |
| 81 | If this is non-nil, then spaces are placed on both sides of all |
| 82 | commit counts." |
| 83 | :package-version '(magit . "2.12.0") |
| 84 | :group 'magit-refs |
| 85 | :type 'boolean) |
| 86 | |
| 87 | (defvar magit-refs-show-push-remote nil |
| 88 | "Whether to show the push-remotes of local branches. |
| 89 | Also show the commits that the local branch is ahead and behind |
| 90 | the push-target. Unfortunately there is a bug in Git that makes |
| 91 | this useless (the commits ahead and behind the upstream are |
| 92 | shown), so this isn't enabled yet.") |
| 93 | |
| 94 | (defcustom magit-refs-show-remote-prefix nil |
| 95 | "Whether to show the remote prefix in lists of remote branches. |
| 96 | |
| 97 | This is redundant because the name of the remote is already shown |
| 98 | in the heading preceding the list of its branches." |
| 99 | :package-version '(magit . "2.12.0") |
| 100 | :group 'magit-refs |
| 101 | :type 'boolean) |
| 102 | |
| 103 | (defcustom magit-refs-show-branch-descriptions nil |
| 104 | "Whether to show the description, if any, of local branches. |
| 105 | To distinguish branch descriptions from the commit summary of the tip, |
| 106 | which is shown when there is no description or this option is disabled, |
| 107 | descriptions use the bold face." |
| 108 | :package-version '(magit . "4.3.0") |
| 109 | :group 'magit-refs |
| 110 | :type 'boolean) |
| 111 | |
| 112 | (defcustom magit-refs-margin |
| 113 | (list nil |
| 114 | (nth 1 magit-log-margin) |
| 115 | 'magit-log-margin-width nil |
| 116 | (nth 4 magit-log-margin)) |
| 117 | "Format of the margin in `magit-refs-mode' buffers. |
| 118 | |
| 119 | The value has the form (INIT STYLE WIDTH AUTHOR AUTHOR-WIDTH). |
| 120 | |
| 121 | If INIT is non-nil, then the margin is shown initially. |
| 122 | STYLE controls how to format the author or committer date. |
| 123 | It can be one of `age' (to show the age of the commit), |
| 124 | `age-abbreviated' (to abbreviate the time unit to a character), |
| 125 | or a string (suitable for `format-time-string') to show the |
| 126 | actual date. Option `magit-log-margin-show-committer-date' |
| 127 | controls which date is being displayed. |
| 128 | WIDTH controls the width of the margin. This exists for forward |
| 129 | compatibility and currently the value should not be changed. |
| 130 | AUTHOR controls whether the name of the author is also shown by |
| 131 | default. |
| 132 | AUTHOR-WIDTH has to be an integer. When the name of the author |
| 133 | is shown, then this specifies how much space is used to do so." |
| 134 | :package-version '(magit . "2.9.0") |
| 135 | :group 'magit-refs |
| 136 | :group 'magit-margin |
| 137 | :safe (##memq % '(all branch nil)) |
| 138 | :type magit-log-margin--custom-type |
| 139 | :initialize #'magit-custom-initialize-reset |
| 140 | :set-after '(magit-log-margin) |
| 141 | :set (apply-partially #'magit-margin-set-variable 'magit-refs-mode)) |
| 142 | |
| 143 | (defcustom magit-refs-margin-for-tags nil |
| 144 | "Whether to show information about tags in the margin. |
| 145 | |
| 146 | This is disabled by default because it is slow if there are many |
| 147 | tags." |
| 148 | :package-version '(magit . "2.9.0") |
| 149 | :group 'magit-refs |
| 150 | :group 'magit-margin |
| 151 | :type 'boolean) |
| 152 | |
| 153 | (defcustom magit-refs-primary-column-width '(16 . 32) |
| 154 | "Width of the focus column in `magit-refs-mode' buffers. |
| 155 | |
| 156 | The primary column is the column that contains the name of the |
| 157 | branch that the current row is about. |
| 158 | |
| 159 | If this is an integer, then the column is that many columns wide. |
| 160 | Otherwise it has to be a cons-cell of two integers. The first |
| 161 | specifies the minimal width, the second the maximal width. In that |
| 162 | case the actual width is determined using the length of the names |
| 163 | of the shown local branches. (Remote branches and tags are not |
| 164 | taken into account when calculating to optimal width.)" |
| 165 | :package-version '(magit . "2.12.0") |
| 166 | :group 'magit-refs |
| 167 | :type '(choice (integer :tag "Constant wide") |
| 168 | (cons :tag "Wide constrains" |
| 169 | (integer :tag "Minimum") |
| 170 | (integer :tag "Maximum")))) |
| 171 | |
| 172 | (defcustom magit-refs-focus-column-width 5 |
| 173 | "Width of the focus column in `magit-refs-mode' buffers. |
| 174 | |
| 175 | The focus column is the first column, which marks one |
| 176 | branch (usually the current branch) as the focused branch using |
| 177 | \"*\" or \"@\". For each other reference, this column optionally |
| 178 | shows how many commits it is ahead of the focused branch and \"<\", or |
| 179 | if it isn't ahead then the commits it is behind and \">\", or if it |
| 180 | isn't behind either, then a \"=\". |
| 181 | |
| 182 | This column may also display only \"*\" or \"@\" for the focused |
| 183 | branch, in which case this option is ignored. Use \"L v\" to |
| 184 | change the verbosity of this column." |
| 185 | :package-version '(magit . "2.12.0") |
| 186 | :group 'magit-refs |
| 187 | :type 'integer) |
| 188 | |
| 189 | (defcustom magit-refs-filter-alist nil |
| 190 | "Alist controlling which refs are omitted from `magit-refs-mode' buffers. |
| 191 | |
| 192 | The purpose of this option is to forgo displaying certain refs |
| 193 | based on their name. If you want to not display any refs of a |
| 194 | certain type, then you should remove the appropriate function |
| 195 | from `magit-refs-sections-hook' instead. |
| 196 | |
| 197 | All keys are tried in order until one matches. Then its value |
| 198 | is used and subsequent elements are ignored. If the value is |
| 199 | non-nil, then the reference is displayed, otherwise it is not. |
| 200 | If no element matches, then the reference is displayed. |
| 201 | |
| 202 | A key can either be a regular expression that the refname has to |
| 203 | match, or a function that takes the refname as only argument and |
| 204 | returns a boolean. A remote branch such as \"origin/master\" is |
| 205 | displayed as just \"master\", however for this comparison the |
| 206 | former is used." |
| 207 | :package-version '(magit . "2.12.0") |
| 208 | :group 'magit-refs |
| 209 | :type '(alist :key-type (choice :tag "Key" regexp function) |
| 210 | :value-type (boolean :tag "Value" |
| 211 | :on "show (non-nil)" |
| 212 | :off "omit (nil)"))) |
| 213 | |
| 214 | (defcustom magit-visit-ref-behavior nil |
| 215 | "Control how `magit-visit-ref' behaves in `magit-refs-mode' buffers. |
| 216 | |
| 217 | By default `magit-visit-ref' behaves like `magit-show-commit', |
| 218 | in all buffers, including `magit-refs-mode' buffers. When the |
| 219 | type of the section at point is `commit' then \"RET\" is bound to |
| 220 | `magit-show-commit', and when the type is either `branch' or |
| 221 | `tag' then it is bound to `magit-visit-ref'. |
| 222 | |
| 223 | \"RET\" is one of Magit's most essential keys and at least by |
| 224 | default it should behave consistently across all of Magit, |
| 225 | especially because users quickly learn that it does something |
| 226 | very harmless; it shows more information about the thing at point |
| 227 | in another buffer. |
| 228 | |
| 229 | However \"RET\" used to behave differently in `magit-refs-mode' |
| 230 | buffers, doing surprising things, some of which cannot really be |
| 231 | described as \"visit this thing\". If you have grown accustomed |
| 232 | to such inconsistent, but to you useful, behavior, then you can |
| 233 | restore that by adding one or more of the below symbols to the |
| 234 | value of this option. But keep in mind that by doing so you |
| 235 | don't only introduce inconsistencies, you also lose some |
| 236 | functionality and might have to resort to `M-x magit-show-commit' |
| 237 | to get it back. |
| 238 | |
| 239 | `magit-visit-ref' looks for these symbols in the order in which |
| 240 | they are described here. If the presence of a symbol applies to |
| 241 | the current situation, then the symbols that follow do not affect |
| 242 | the outcome. |
| 243 | |
| 244 | `focus-on-ref' |
| 245 | |
| 246 | With a prefix argument update the buffer to show commit counts |
| 247 | and lists of cherry commits relative to the reference at point |
| 248 | instead of relative to the current buffer or `HEAD'. |
| 249 | |
| 250 | Instead of adding this symbol, consider pressing \\`C-u y o RET'. |
| 251 | |
| 252 | `create-branch' |
| 253 | |
| 254 | If point is on a remote branch, then create a new local branch |
| 255 | with the same name, use the remote branch as its upstream, and |
| 256 | then check out the local branch. |
| 257 | |
| 258 | Instead of adding this symbol, consider pressing \"b c RET RET\", |
| 259 | like you would do in other buffers. |
| 260 | |
| 261 | `checkout-any' |
| 262 | |
| 263 | Check out the reference at point. If that reference is a tag |
| 264 | or a remote branch, then this results in a detached `HEAD'. |
| 265 | |
| 266 | Instead of adding this symbol, consider pressing \"b b RET\", |
| 267 | like you would do in other buffers. |
| 268 | |
| 269 | `checkout-branch' |
| 270 | |
| 271 | Check out the local branch at point. |
| 272 | |
| 273 | Instead of adding this symbol, consider pressing \"b b RET\", |
| 274 | like you would do in other buffers." |
| 275 | :package-version '(magit . "2.9.0") |
| 276 | :group 'magit-refs |
| 277 | :group 'magit-commands |
| 278 | :options '(focus-on-ref create-branch checkout-any checkout-branch) |
| 279 | :type '(list :convert-widget custom-hook-convert-widget)) |
| 280 | |
| 281 | ;;; Mode |
| 282 | |
| 283 | (defvar-keymap magit-refs-mode-map |
| 284 | :doc "Keymap for `magit-refs-mode'." |
| 285 | :parent magit-mode-map |
| 286 | "C-y" #'magit-refs-set-show-commit-count |
| 287 | "L" #'magit-margin-settings) |
| 288 | |
| 289 | (define-derived-mode magit-refs-mode magit-mode "Magit Refs" |
| 290 | "Mode which lists and compares references. |
| 291 | |
| 292 | This mode is documented in info node `(magit)References Buffer'. |
| 293 | |
| 294 | \\<magit-mode-map>\ |
| 295 | Type \\[magit-refresh] to refresh the current buffer. |
| 296 | Type \\[magit-section-toggle] to expand or hide the section at point. |
| 297 | Type \\[magit-visit-thing] or \\[magit-diff-show-or-scroll-up] \ |
| 298 | to visit the commit or branch at point. |
| 299 | |
| 300 | Type \\[magit-branch] to see available branch commands. |
| 301 | Type \\[magit-merge] to merge the branch or commit at point. |
| 302 | Type \\[magit-cherry-pick] to apply the commit at point. |
| 303 | Type \\[magit-reset] to reset `HEAD' to the commit at point. |
| 304 | |
| 305 | \\{magit-refs-mode-map}" |
| 306 | :interactive nil |
| 307 | :group 'magit-refs |
| 308 | (magit-hack-dir-local-variables) |
| 309 | (setq magit--imenu-group-types '(local remote tags))) |
| 310 | |
| 311 | (defvar-local magit-buffer-refs-args nil) |
| 312 | (defvar-local magit-buffer-refs-upstream nil) |
| 313 | |
| 314 | (defun magit-refs-setup-buffer (ref args) |
| 315 | (magit-setup-buffer #'magit-refs-mode nil |
| 316 | (magit-buffer-refs-upstream ref) |
| 317 | (magit-buffer-refs-args args))) |
| 318 | |
| 319 | (defun magit-refs-refresh-buffer () |
| 320 | (setq magit--right-margin-delayed (not (magit--right-margin-active))) |
| 321 | (unless (magit-rev-verify magit-buffer-refs-upstream) |
| 322 | (setq magit-refs-show-commit-count nil)) |
| 323 | (magit-set-header-line-format |
| 324 | (format "%s %s" magit-buffer-refs-upstream |
| 325 | (string-join magit-buffer-refs-args " "))) |
| 326 | (magit-insert-section (branchbuf) |
| 327 | (magit-run-section-hook 'magit-refs-sections-hook)) |
| 328 | (add-hook 'kill-buffer-hook #'magit-preserve-section-visibility-cache)) |
| 329 | |
| 330 | (cl-defmethod magit-buffer-value (&context (major-mode magit-refs-mode)) |
| 331 | (cons magit-buffer-refs-upstream magit-buffer-refs-args)) |
| 332 | |
| 333 | ;;; Commands |
| 334 | |
| 335 | ;;;###autoload(autoload 'magit-show-refs "magit-refs" nil t) |
| 336 | (transient-define-prefix magit-show-refs (&optional transient) |
| 337 | "List and compare references in a dedicated buffer." |
| 338 | :man-page "git-branch" |
| 339 | :value (##magit-show-refs-arguments magit-prefix-use-buffer-arguments) |
| 340 | ["Arguments" |
| 341 | (magit-for-each-ref:--contains) |
| 342 | ("-M" "Merged" "--merged=" magit-transient-read-revision) |
| 343 | ("-m" "Merged to HEAD" "--merged") |
| 344 | ("-N" "Not merged" "--no-merged=" magit-transient-read-revision) |
| 345 | ("-n" "Not merged to HEAD" "--no-merged") |
| 346 | (magit-for-each-ref:--sort)] |
| 347 | ["Actions" |
| 348 | ("y" "Show refs, comparing them with HEAD" magit-show-refs-head) |
| 349 | ("c" "Show refs, comparing them with current branch" magit-show-refs-current) |
| 350 | ("o" "Show refs, comparing them with other branch" magit-show-refs-other) |
| 351 | ("r" "Show refs, changing commit count display" |
| 352 | magit-refs-set-show-commit-count)] |
| 353 | (interactive (list (or (derived-mode-p 'magit-refs-mode) |
| 354 | current-prefix-arg))) |
| 355 | (if transient |
| 356 | (transient-setup 'magit-show-refs) |
| 357 | (magit-refs-setup-buffer "HEAD" (magit-show-refs-arguments)))) |
| 358 | |
| 359 | (defun magit-show-refs-arguments (&optional use-buffer-args) |
| 360 | (unless use-buffer-args |
| 361 | (setq use-buffer-args magit-direct-use-buffer-arguments)) |
| 362 | (cond-let* |
| 363 | ((eq transient-current-command 'magit-show-refs) |
| 364 | (transient-args 'magit-show-refs)) |
| 365 | ((eq major-mode 'magit-refs-mode) |
| 366 | magit-buffer-refs-args) |
| 367 | ([_(memq use-buffer-args '(always selected))] |
| 368 | [buffer (magit-get-mode-buffer 'magit-refs-mode nil |
| 369 | (eq use-buffer-args 'selected))] |
| 370 | (buffer-local-value 'magit-buffer-refs-args buffer)) |
| 371 | ((alist-get 'magit-show-refs transient-values)))) |
| 372 | |
| 373 | (transient-define-argument magit-for-each-ref:--contains () |
| 374 | :description "Contains" |
| 375 | :class 'transient-option |
| 376 | :key "-c" |
| 377 | :argument "--contains=" |
| 378 | :reader #'magit-transient-read-revision) |
| 379 | |
| 380 | (transient-define-argument magit-for-each-ref:--sort () |
| 381 | :description "Sort" |
| 382 | :class 'transient-option |
| 383 | :key "-s" |
| 384 | :argument "--sort=" |
| 385 | :reader #'magit-read-ref-sort) |
| 386 | |
| 387 | (defun magit-read-ref-sort (prompt initial-input _history) |
| 388 | (magit-completing-read prompt |
| 389 | '("-committerdate" "-authordate" |
| 390 | "committerdate" "authordate") |
| 391 | nil nil initial-input)) |
| 392 | |
| 393 | ;;;###autoload |
| 394 | (defun magit-show-refs-head (&optional args) |
| 395 | "List and compare references in a dedicated buffer. |
| 396 | Compared with `HEAD'." |
| 397 | (interactive (list (magit-show-refs-arguments))) |
| 398 | (magit-refs-setup-buffer "HEAD" args)) |
| 399 | |
| 400 | ;;;###autoload |
| 401 | (defun magit-show-refs-current (&optional args) |
| 402 | "List and compare references in a dedicated buffer. |
| 403 | Compare with the current branch or `HEAD' if it is detached." |
| 404 | (interactive (list (magit-show-refs-arguments))) |
| 405 | (magit-refs-setup-buffer (magit-get-current-branch) args)) |
| 406 | |
| 407 | ;;;###autoload |
| 408 | (defun magit-show-refs-other (&optional ref args) |
| 409 | "List and compare references in a dedicated buffer. |
| 410 | Compared with a branch read from the user." |
| 411 | (interactive (list (magit-read-other-branch "Compare with") |
| 412 | (magit-show-refs-arguments))) |
| 413 | (magit-refs-setup-buffer ref args)) |
| 414 | |
| 415 | (transient-define-suffix magit-refs-set-show-commit-count () |
| 416 | "Change for which refs the commit count is shown." |
| 417 | :description "Change verbosity" |
| 418 | :key "v" |
| 419 | :transient nil |
| 420 | :if-derived 'magit-refs-mode |
| 421 | (interactive) |
| 422 | (setq-local magit-refs-show-commit-count |
| 423 | (magit-read-char-case "Show commit counts for " nil |
| 424 | (?a "[a]ll refs" 'all) |
| 425 | (?b "[b]ranches only" t) |
| 426 | (?n "[n]othing" nil))) |
| 427 | (magit-refresh)) |
| 428 | |
| 429 | (defun magit-visit-ref () |
| 430 | "Visit the reference or revision at point in another buffer. |
| 431 | If there is no revision at point or with a prefix argument prompt |
| 432 | for a revision. |
| 433 | |
| 434 | This command behaves just like `magit-show-commit', except if |
| 435 | point is on a reference in a `magit-refs-mode' buffer (a buffer |
| 436 | listing branches and tags), in which case the behavior may be |
| 437 | different, but only if you have customized the option |
| 438 | `magit-visit-ref-behavior' (which see). When invoked from a |
| 439 | menu this command always behaves like `magit-show-commit'." |
| 440 | (interactive) |
| 441 | (cond-let |
| 442 | ((not (and (derived-mode-p 'magit-refs-mode) |
| 443 | (magit-section-match '(branch tag)) |
| 444 | (not (magit-menu-position)))) |
| 445 | (call-interactively #'magit-show-commit)) |
| 446 | [[ref (oref (magit-current-section) value)]] |
| 447 | ((and current-prefix-arg |
| 448 | (memq 'focus-on-ref magit-visit-ref-behavior)) |
| 449 | (magit-refs-setup-buffer ref (magit-show-refs-arguments))) |
| 450 | ((and current-prefix-arg |
| 451 | magit-visit-ref-behavior |
| 452 | ;; Don't prompt for commit to visit. |
| 453 | (let ((current-prefix-arg nil)) |
| 454 | (call-interactively #'magit-show-commit)))) |
| 455 | ((and (memq 'create-branch magit-visit-ref-behavior) |
| 456 | (magit-section-match [branch remote])) |
| 457 | (let ((branch (cdr (magit-split-branch-name ref)))) |
| 458 | (if (magit-branch-p branch) |
| 459 | (if (magit-rev-eq branch ref) |
| 460 | (magit-call-git "checkout" branch) |
| 461 | (setq branch (propertize branch 'face 'magit-branch-local)) |
| 462 | (setq ref (propertize ref 'face 'magit-branch-remote)) |
| 463 | (pcase (prog1 (read-char-choice (format (propertize "\ |
| 464 | Branch %s already exists. |
| 465 | [c]heckout %s as-is |
| 466 | [r]reset %s to %s and checkout %s |
| 467 | [a]bort " 'face 'minibuffer-prompt) branch branch branch ref branch) |
| 468 | '(?c ?r ?a)) |
| 469 | (message "")) ; otherwise prompt sticks |
| 470 | (?c (magit-call-git "checkout" branch)) |
| 471 | (?r (magit-call-git "checkout" "-B" branch ref)) |
| 472 | (?a (user-error "Abort")))) |
| 473 | (magit-call-git "checkout" "-b" branch ref)) |
| 474 | (setq magit-buffer-refs-upstream branch) |
| 475 | (magit-refresh))) |
| 476 | ((or (memq 'checkout-any magit-visit-ref-behavior) |
| 477 | (and (memq 'checkout-branch magit-visit-ref-behavior) |
| 478 | (magit-section-match [branch local]))) |
| 479 | (magit-call-git "checkout" ref) |
| 480 | (setq magit-buffer-refs-upstream ref) |
| 481 | (magit-refresh)) |
| 482 | ((call-interactively #'magit-show-commit)))) |
| 483 | |
| 484 | ;;; Sections |
| 485 | |
| 486 | (defvar-keymap magit-remote-section-map |
| 487 | :doc "Keymap for `remote' sections." |
| 488 | "<remap> <magit-file-rename>" #'magit-remote-rename |
| 489 | "<remap> <magit-delete-thing>" #'magit-remote-remove |
| 490 | "<2>" (magit-menu-item "Rename %s" #'magit-remote-rename) |
| 491 | "<1>" (magit-menu-item "Remove %m" #'magit-remote-remove)) |
| 492 | |
| 493 | (defvar-keymap magit-branch-section-map |
| 494 | :doc "Keymap for `branch' sections." |
| 495 | "<remap> <magit-file-rename>" #'magit-branch-rename |
| 496 | "<remap> <magit-delete-thing>" #'magit-branch-delete |
| 497 | "<remap> <magit-visit-thing>" #'magit-visit-ref |
| 498 | "<3>" (magit-menu-item "Rename %s" #'magit-branch-rename) |
| 499 | "<2>" (magit-menu-item "Delete %m" #'magit-branch-delete) |
| 500 | "<1>" (magit-menu-item "Visit commit" #'magit-visit-ref)) |
| 501 | |
| 502 | (defvar-keymap magit-shelved-branch-section-map |
| 503 | :doc "Keymap for `shelved-branch' sections." |
| 504 | "<remap> <magit-delete-thing>" #'magit-delete-shelved-branch) |
| 505 | |
| 506 | (defvar-keymap magit-tag-section-map |
| 507 | :doc "Keymap for `tag' sections." |
| 508 | "<remap> <magit-delete-thing>" #'magit-tag-delete |
| 509 | "<remap> <magit-visit-thing>" #'magit-visit-ref |
| 510 | "<2>" (magit-menu-item "Delete %m" #'magit-tag-delete) |
| 511 | "<1>" (magit-menu-item "Visit %s" #'magit-visit-ref)) |
| 512 | |
| 513 | (defun magit--painted-branch-as-menu-section (section) |
| 514 | (and-let ((_(magit-section-match 'commit)) |
| 515 | (branch (magit--painted-branch-at-point))) |
| 516 | (let ((dummy (magit-section :type 'branch :value branch))) |
| 517 | (oset dummy keymap magit-branch-section-map) |
| 518 | (dolist (slot '(start content hidden parent children)) |
| 519 | (when (slot-boundp section slot) |
| 520 | (setf (eieio-oref dummy slot) |
| 521 | (eieio-oref section slot)))) |
| 522 | dummy))) |
| 523 | |
| 524 | (add-hook 'magit-menu-alternative-section-hook |
| 525 | #'magit--painted-branch-as-menu-section) |
| 526 | |
| 527 | (defun magit-insert-branch-description () |
| 528 | "Insert header containing the description of the current branch. |
| 529 | Insert a header line with the name and description of the |
| 530 | current branch. The description is taken from the Git variable |
| 531 | `branch.<NAME>.description'; if that is undefined then no header |
| 532 | line is inserted at all." |
| 533 | (when-let* ((branch (magit-get-current-branch)) |
| 534 | (desc (magit-get "branch" branch "description")) |
| 535 | (desc (split-string desc "\n"))) |
| 536 | (when (equal (car (last desc)) "") |
| 537 | (setq desc (butlast desc))) |
| 538 | (magit-insert-section (branchdesc branch t) |
| 539 | (magit-insert-heading branch ": " (car desc)) |
| 540 | (when (cdr desc) |
| 541 | (insert (string-join (cdr desc) "\n")) |
| 542 | (insert "\n\n"))))) |
| 543 | |
| 544 | (defun magit-insert-tags () |
| 545 | "Insert sections showing all tags." |
| 546 | (when-let ((tags (magit-git-lines "tag" "--list" "-n" magit-buffer-refs-args))) |
| 547 | (let ((_head (magit-rev-parse "HEAD"))) |
| 548 | (magit-insert-section (tags) |
| 549 | (magit-insert-heading (length tags) "Tags") |
| 550 | (dolist (tag tags) |
| 551 | (string-match "^\\([^ \t]+\\)[ \t]+\\([^ \t\n].*\\)?" tag) |
| 552 | (let ((tag (match-str 1 tag)) |
| 553 | (msg (match-str 2 tag))) |
| 554 | (when (magit-refs--insert-refname-p tag) |
| 555 | (magit-insert-section (tag tag t) |
| 556 | (magit-insert-heading |
| 557 | (magit-refs--format-focus-column tag 'tag) |
| 558 | (propertize tag 'font-lock-face 'magit-tag) |
| 559 | (make-string |
| 560 | (max 1 (- (if (consp magit-refs-primary-column-width) |
| 561 | (car magit-refs-primary-column-width) |
| 562 | magit-refs-primary-column-width) |
| 563 | (length tag))) |
| 564 | ?\s) |
| 565 | (and msg (magit-log--wash-summary msg))) |
| 566 | (when magit-refs-margin-for-tags |
| 567 | (magit-refs--maybe-format-margin tag)) |
| 568 | (magit-refs--insert-cherry-commits tag))))) |
| 569 | (insert ?\n) |
| 570 | (magit-make-margin-overlay))))) |
| 571 | |
| 572 | (defun magit-insert-remote-branches () |
| 573 | "Insert sections showing all remote-tracking branches." |
| 574 | (dolist (remote (magit-list-remotes)) |
| 575 | (magit-insert-section (remote remote) |
| 576 | (magit-insert-heading |
| 577 | (let ((pull (magit-get "remote" remote "url")) |
| 578 | (push (magit-get "remote" remote "pushurl"))) |
| 579 | (format (propertize "Remote %s (%s):" |
| 580 | 'font-lock-face 'magit-section-heading) |
| 581 | (propertize remote 'font-lock-face 'magit-branch-remote) |
| 582 | (concat pull (and pull push ", ") push)))) |
| 583 | (let (head) |
| 584 | (dolist (line (magit-git-lines "for-each-ref" "--format=\ |
| 585 | %(symref:short)%00%(refname:short)%00%(refname)%00%(subject)" |
| 586 | (concat "refs/remotes/" remote) |
| 587 | magit-buffer-refs-args)) |
| 588 | (pcase-let ((`(,head-branch ,branch ,ref ,msg) |
| 589 | (cl-substitute nil "" |
| 590 | (split-string line "\0") |
| 591 | :test #'equal))) |
| 592 | (cond |
| 593 | (head-branch |
| 594 | ;; Note: Use `ref' instead of `branch' for the check |
| 595 | ;; below because 'refname:short' shortens the remote |
| 596 | ;; HEAD to '<remote>' instead of '<remote>/HEAD' as of |
| 597 | ;; Git v2.40.0. |
| 598 | (cl-assert |
| 599 | (equal ref (concat "refs/remotes/" remote "/HEAD"))) |
| 600 | (setq head head-branch)) |
| 601 | ((not (equal ref (concat "refs/remotes/" remote "/HEAD"))) |
| 602 | ;; ^ Skip mis-configured remotes where HEAD is not a |
| 603 | ;; symref. See #5092. |
| 604 | (when (magit-refs--insert-refname-p branch) |
| 605 | (magit-insert-section (branch branch t) |
| 606 | (let ((headp (equal branch head)) |
| 607 | (abbrev (if magit-refs-show-remote-prefix |
| 608 | branch |
| 609 | (substring branch (1+ (length remote)))))) |
| 610 | (magit-insert-heading |
| 611 | (magit-refs--format-focus-column branch) |
| 612 | (magit-refs--propertize-branch |
| 613 | abbrev ref (and headp 'magit-branch-remote-head)) |
| 614 | (make-string |
| 615 | (max 1 (- (if (consp magit-refs-primary-column-width) |
| 616 | (car magit-refs-primary-column-width) |
| 617 | magit-refs-primary-column-width) |
| 618 | (length abbrev))) |
| 619 | ?\s) |
| 620 | (and msg (magit-log--wash-summary msg)))) |
| 621 | (magit-refs--maybe-format-margin branch) |
| 622 | (magit-refs--insert-cherry-commits branch)))))))) |
| 623 | (insert ?\n) |
| 624 | (magit-make-margin-overlay)))) |
| 625 | |
| 626 | (defun magit-insert-local-branches () |
| 627 | "Insert sections showing all local branches." |
| 628 | (magit-insert-section (local nil) |
| 629 | (magit-insert-heading t "Branches") |
| 630 | (dolist (line (magit-refs--format-local-branches)) |
| 631 | (pcase-let ((`(,branch . ,strings) line)) |
| 632 | (magit-insert-section |
| 633 | ((eval (if branch 'branch 'commit)) |
| 634 | (or branch (magit-rev-parse "HEAD")) |
| 635 | t) |
| 636 | (apply #'magit-insert-heading strings) |
| 637 | (magit-refs--maybe-format-margin branch) |
| 638 | (magit-refs--insert-cherry-commits branch)))) |
| 639 | (insert ?\n) |
| 640 | (magit-make-margin-overlay))) |
| 641 | |
| 642 | (defun magit-insert-shelved-branches () |
| 643 | "Insert sections showing all shelved branches." |
| 644 | (when-let ((refs (magit-list-refs "refs/shelved/"))) |
| 645 | (magit-insert-section (shelved nil t) |
| 646 | (magit-insert-heading t "Shelved branches") |
| 647 | (dolist (ref (nreverse refs)) |
| 648 | (magit-insert-section (shelved-branch ref t) |
| 649 | (magit-insert-heading |
| 650 | " " (magit--propertize-face (substring ref 13) 'magit-refname)) |
| 651 | (magit-refs--maybe-format-margin ref) |
| 652 | (magit-refs--insert-cherry-commits ref))) |
| 653 | (insert ?\n) |
| 654 | (magit-make-margin-overlay)))) |
| 655 | |
| 656 | (defun magit-refs--format-local-branches () |
| 657 | (let ((lines (seq-keep #'magit-refs--format-local-branch |
| 658 | (magit-git-lines |
| 659 | "for-each-ref" |
| 660 | (concat "--format=\ |
| 661 | %(HEAD)%00%(refname:short)%00%(refname)%00\ |
| 662 | %(upstream:short)%00%(upstream)%00%(upstream:track)%00" |
| 663 | (if magit-refs-show-push-remote "\ |
| 664 | %(push:remotename)%00%(push)%00%(push:track)%00%(subject)" |
| 665 | "%00%00%00%(subject)")) |
| 666 | "refs/heads" |
| 667 | magit-buffer-refs-args)))) |
| 668 | (unless (magit-get-current-branch) |
| 669 | (push (magit-refs--format-local-branch |
| 670 | (concat "*\0\0\0\0\0\0\0\0" (magit-rev-format "%s"))) |
| 671 | lines)) |
| 672 | (setq-local magit-refs-primary-column-width |
| 673 | (let ((def (default-value 'magit-refs-primary-column-width))) |
| 674 | (if (atom def) |
| 675 | def |
| 676 | (pcase-let ((`(,min . ,max) def)) |
| 677 | (min max (apply #'max min (mapcar #'car lines))))))) |
| 678 | (mapcar (pcase-lambda (`( ,_ ,branch ,focus |
| 679 | ,branch-desc ,u:ahead ,p:ahead |
| 680 | ,u:behind ,upstream ,p:behind ,msg)) |
| 681 | (list branch focus branch-desc u:ahead p:ahead |
| 682 | (make-string (max 1 (- magit-refs-primary-column-width |
| 683 | (length (concat branch-desc |
| 684 | u:ahead |
| 685 | p:ahead |
| 686 | u:behind)))) |
| 687 | ?\s) |
| 688 | u:behind upstream p:behind msg)) |
| 689 | lines))) |
| 690 | |
| 691 | (defun magit-refs--format-local-branch (line) |
| 692 | (pcase-let ((`(,head ,branch ,ref ,upstream ,u:ref ,u:track |
| 693 | ,push ,p:ref ,p:track ,msg) |
| 694 | (cl-substitute nil "" (split-string line "\0") :test #'equal))) |
| 695 | (when (or (not branch) |
| 696 | (magit-refs--insert-refname-p branch)) |
| 697 | (let* ((headp (equal head "*")) |
| 698 | (pushp (and push |
| 699 | magit-refs-show-push-remote |
| 700 | (magit-rev-verify p:ref) |
| 701 | (not (equal p:ref u:ref)))) |
| 702 | (branch-pretty |
| 703 | (if branch |
| 704 | (magit-refs--propertize-branch |
| 705 | branch ref (and headp 'magit-branch-current)) |
| 706 | (magit--propertize-face "(detached)" 'magit-branch-warning))) |
| 707 | (u:ahead (and u:track |
| 708 | (string-match "ahead \\([0-9]+\\)" u:track) |
| 709 | (magit--propertize-face |
| 710 | (concat (and magit-refs-pad-commit-counts " ") |
| 711 | (match-str 1 u:track) |
| 712 | ">") |
| 713 | 'magit-dimmed))) |
| 714 | (u:behind (and u:track |
| 715 | (string-match "behind \\([0-9]+\\)" u:track) |
| 716 | (magit--propertize-face |
| 717 | (concat "<" |
| 718 | (match-str 1 u:track) |
| 719 | (and magit-refs-pad-commit-counts " ")) |
| 720 | 'magit-dimmed))) |
| 721 | (p:ahead (and pushp p:track |
| 722 | (string-match "ahead \\([0-9]+\\)" p:track) |
| 723 | (magit--propertize-face |
| 724 | (concat (match-str 1 p:track) |
| 725 | ">" |
| 726 | (and magit-refs-pad-commit-counts " ")) |
| 727 | 'magit-branch-remote))) |
| 728 | (p:behind (and pushp p:track |
| 729 | (string-match "behind \\([0-9]+\\)" p:track) |
| 730 | (magit--propertize-face |
| 731 | (concat "<" |
| 732 | (match-str 1 p:track) |
| 733 | (and magit-refs-pad-commit-counts " ")) |
| 734 | 'magit-dimmed)))) |
| 735 | (list (1+ (length (concat branch-pretty u:ahead p:ahead u:behind))) |
| 736 | branch |
| 737 | (magit-refs--format-focus-column branch headp) |
| 738 | branch-pretty u:ahead p:ahead |
| 739 | u:behind |
| 740 | (and upstream |
| 741 | (concat (if (equal u:track "[gone]") |
| 742 | (magit--propertize-face upstream 'error) |
| 743 | (magit-refs--propertize-branch upstream u:ref)) |
| 744 | " ")) |
| 745 | (and pushp |
| 746 | (concat p:behind |
| 747 | (magit--propertize-face |
| 748 | push 'magit-branch-remote) |
| 749 | " ")) |
| 750 | (if-let ((_ magit-refs-show-branch-descriptions) |
| 751 | (desc (magit-get "branch" branch "description"))) |
| 752 | (magit--propertize-face desc 'bold) |
| 753 | (and msg (magit-log--wash-summary msg)))))))) |
| 754 | |
| 755 | (defun magit-refs--format-focus-column (ref &optional type) |
| 756 | (let ((focus magit-buffer-refs-upstream) |
| 757 | (width (if magit-refs-show-commit-count |
| 758 | magit-refs-focus-column-width |
| 759 | 1))) |
| 760 | (format |
| 761 | (format "%%%ss " width) |
| 762 | (cond ((or (equal ref focus) |
| 763 | (and (eq type t) |
| 764 | (equal focus "HEAD"))) |
| 765 | (magit--propertize-face (concat (if (equal focus "HEAD") "@" "*") |
| 766 | (make-string (1- width) ?\s)) |
| 767 | 'magit-section-heading)) |
| 768 | ((if (eq type 'tag) |
| 769 | (eq magit-refs-show-commit-count 'all) |
| 770 | magit-refs-show-commit-count) |
| 771 | (pcase-let ((`(,behind ,ahead) |
| 772 | (magit-rev-diff-count magit-buffer-refs-upstream ref))) |
| 773 | (magit--propertize-face |
| 774 | (cond ((> ahead 0) (concat "<" (number-to-string ahead))) |
| 775 | ((> behind 0) (concat (number-to-string behind) ">")) |
| 776 | ("=")) |
| 777 | 'magit-dimmed))) |
| 778 | (""))))) |
| 779 | |
| 780 | (defun magit-refs--propertize-branch (branch ref &optional head-face) |
| 781 | (let ((face (cdr (cl-find-if (pcase-lambda (`(,re . ,_)) |
| 782 | (string-match-p re ref)) |
| 783 | magit-ref-namespaces)))) |
| 784 | (magit--propertize-face |
| 785 | branch (if head-face (list face head-face) face)))) |
| 786 | |
| 787 | (defun magit-refs--insert-refname-p (refname) |
| 788 | (if-let ((entry (seq-find (pcase-lambda (`(,key . ,_)) |
| 789 | (if (functionp key) |
| 790 | (funcall key refname) |
| 791 | (string-match-p key refname))) |
| 792 | magit-refs-filter-alist))) |
| 793 | (cdr entry) |
| 794 | t)) |
| 795 | |
| 796 | (defun magit-refs--insert-cherry-commits (ref) |
| 797 | (magit-insert-section-body |
| 798 | (let ((start (point)) |
| 799 | (magit-insert-section--current nil)) |
| 800 | (magit-git-wash (apply-partially #'magit-log-wash-log 'cherry) |
| 801 | "cherry" "-v" (magit-abbrev-arg) magit-buffer-refs-upstream ref) |
| 802 | (if (= (point) start) |
| 803 | (message "No cherries for %s" ref) |
| 804 | (magit-make-margin-overlay))))) |
| 805 | |
| 806 | (defun magit-refs--maybe-format-margin (rev) |
| 807 | (when (magit--right-margin-active) |
| 808 | (if-let ((line (magit-rev-format "%cN%x00%ct" rev))) |
| 809 | (apply #'magit-log-format-margin rev (split-string line "\0")) |
| 810 | (magit-make-margin-overlay)))) |
| 811 | |
| 812 | ;;; _ |
| 813 | (provide 'magit-refs) |
| 814 | ;; Local Variables: |
| 815 | ;; read-symbol-shorthands: ( |
| 816 | ;; ("and$" . "cond-let--and$") |
| 817 | ;; ("thread$" . "cond-let--thread$") |
| 818 | ;; ("when$" . "cond-let--when$") |
| 819 | ;; ("and-let*" . "cond-let--and-let*") |
| 820 | ;; ("and-let" . "cond-let--and-let") |
| 821 | ;; ("if-let*" . "cond-let--if-let*") |
| 822 | ;; ("if-let" . "cond-let--if-let") |
| 823 | ;; ("when-let*" . "cond-let--when-let*") |
| 824 | ;; ("when-let" . "cond-let--when-let") |
| 825 | ;; ("while-let*" . "cond-let--while-let*") |
| 826 | ;; ("while-let" . "cond-let--while-let") |
| 827 | ;; ("match-string" . "match-string") |
| 828 | ;; ("match-str" . "match-string-no-properties")) |
| 829 | ;; End: |
| 830 | ;;; magit-refs.el ends here |