;;; magit-base.el --- Early birds  -*- lexical-binding:t; coding:utf-8 -*-

;; Copyright (C) 2008-2026 The Magit Project Contributors

;; Author: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>
;; Maintainer: Jonas Bernoulli <emacs.magit@jonas.bernoulli.dev>

;; SPDX-License-Identifier: GPL-3.0-or-later

;; Magit is free software: you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; Magit is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
;; or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
;; License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with Magit.  If not, see <https://www.gnu.org/licenses/>.

;; This file contains code taken from GNU Emacs, which is
;; Copyright (C) 1976-2023 Free Software Foundation, Inc.

;;; Commentary:

;; This library defines utility functions, options and other things that
;; have to be available early on because they are used by several other
;; libraries, which cannot depend on one another, because that would lead
;; to circular dependencies.

;;; Code:

;; Also update EMACS_VERSION in "default.mk".
(defconst magit--minimal-emacs "28.1")
(defconst magit--minimal-git "2.25.0")

(require 'cl-lib)
(require 'compat)
(require 'cond-let)
(require 'eieio)
(require 'llama) ; For (##these ...) see M-x describe-function RET # # RET.
(require 'subr-x)

(require 'crm)

(require 'magit-section)

(eval-when-compile (require 'info))
(declare-function Info-get-token "info" (pos start all &optional errorstring))

(eval-when-compile (require 'vc-git))
(declare-function vc-git--run-command-string "vc-git" (file &rest args))

(eval-when-compile (require 'which-func))
(declare-function which-function "which-func" ())

;;; Options

(defcustom magit-completing-read-function #'magit-builtin-completing-read
  "Function to be called when requesting input from the user.

The default, `magit-builtin-completing-read', support third-party
completion frameworks, including `vertico-mode', `ivy-mode' and
`helm-mode'.

However, if you would like to use Ivy or Helm completion with Magit but
not enable the respective modes globally, then customize this option to
use `ivy-completing-read' or `helm--completing-read-default'.

If you still use `ido-mode', you'll likely need the `magit-ido' package."
  :group 'magit-essentials
  :type `(radio (function-item ,#'magit-builtin-completing-read)
                (function-item ivy-completing-read)
                (function-item helm--completing-read-default)
                (function :tag "Other function")))

(defcustom magit-dwim-selection
  ;; Do not function-quote to avoid circular dependencies.
  '((magit-stash-apply        nil t)
    (magit-ediff-resolve-all  nil t)
    (magit-ediff-resolve-rest nil t)
    (magit-stash-branch       nil t)
    (magit-stash-branch-here  nil t)
    (magit-stash-format-patch nil t)
    (magit-stash-drop         nil ask)
    (magit-stash-pop          nil ask))
  "When not to offer alternatives and ask for confirmation.

Many commands by default ask the user to select from a list of
possible candidates.  They do so even when there is a thing at
point that they can act on, which is then offered as the default.

This option can be used to tell certain commands to use the thing
at point instead of asking the user to select a candidate to act
on, with or without confirmation.

The value has the form ((COMMAND nil|PROMPT DEFAULT)...).

- COMMAND is the command that should not prompt for a choice.
  To have an effect, the command has to use the function
  `magit-completing-read' or a utility function which in turn uses
  that function.

- If the command uses `magit-completing-read' multiple times, then
  PROMPT can be used to only affect one of these uses.  PROMPT, if
  non-nil, is a regular expression that is used to match against
  the PROMPT argument passed to `magit-completing-read'.

- DEFAULT specifies how to use the default.  If it is t, then
  the DEFAULT argument passed to `magit-completing-read' is used
  without confirmation.  If it is `ask', then the user is given
  a chance to abort.  DEFAULT can also be nil, in which case the
  entry has no effect."
  :package-version '(magit . "2.12.0")
  :group 'magit-commands
  :type '(repeat
          (list (symbol :tag "Command") ; It might not be fboundp yet.
                (choice (const  :tag "For all prompts" nil)
                        (regexp :tag "For prompts matching regexp"))
                (choice (const  :tag "Offer other choices" nil)
                        (const  :tag "Require confirmation" ask)
                        (const  :tag "Use default without confirmation" t)))))

(defconst magit--confirm-actions
  '((const discard)
    (const reverse)
    (const stage-all-changes)
    (const unstage-all-changes)
    (const delete)
    (const trash)
    (const resurrect)
    (const untrack)
    (const rename)
    (const reset-bisect)
    (const abort-cherry-pick)
    (const abort-revert)
    (const abort-rebase)
    (const abort-merge)
    (const merge-dirty)
    (const delete-unmerged-branch)
    (const delete-branch-on-remote)
    (const delete-pr-remote)
    (const drop-stashes)
    (const set-and-push)
    (const amend-published)
    (const rebase-published)
    (const edit-published)
    (const remove-modules)
    (const remove-dirty-modules)
    (const trash-module-gitdirs)
    (const stash-apply-3way)
    (const kill-process)
    (const safe-with-wip)))

(defcustom magit-no-confirm '(set-and-push)
  "A list of symbols for actions Magit should not confirm, or t.

Many potentially dangerous commands by default ask the user for
confirmation.  Each of the below symbols stands for an action
which, when invoked unintentionally or without being fully aware
of the consequences, could lead to tears.  In many cases there
are several commands that perform variations of a certain action,
so we don't use the command names but more generic symbols.

Applying changes:

  `discard' Discarding one or more changes (i.e., hunks or the
  complete diff for a file) loses that change, obviously.

  `reverse' Reverting one or more changes can usually be undone
  by reverting the reversion.

  `stage-all-changes', `unstage-all-changes' When there are both
  staged and unstaged changes, then un-/staging everything would
  destroy that distinction.  Of course that also applies when
  un-/staging a single change, but then less is lost and one does
  that so often that having to confirm every time would be
  unacceptable.

Files:

  `delete' When a file that isn't yet tracked by Git is deleted
  then it is completely lost, not just the last changes.  Very
  dangerous.

  `trash' Instead of deleting a file it can also be move to the
  system trash.  Obviously much less dangerous than deleting it.

  Also see option `magit-delete-by-moving-to-trash'.

  `resurrect' A deleted file can easily be resurrected by
  \"deleting\" the deletion, which is done using the same command
  that was used to delete the same file in the first place.

  `untrack' Untracking a file can be undone by tracking it again.

  `rename' Renaming a file can easily be undone.

Sequences:

  `reset-bisect' Aborting (known to Git as \"resetting\") a
  bisect operation loses all information collected so far.

  `abort-cherry-pick' Aborting a cherry-pick throws away all
  conflict resolutions which has already been carried out by the
  user.

  `abort-revert' Aborting a revert throws away all conflict
  resolutions which has already been carried out by the user.

  `abort-rebase' Aborting a rebase throws away all already
  modified commits, but it's possible to restore those from the
  reflog.

  `abort-merge' Aborting a merge throws away all conflict
  resolutions which has already been carried out by the user.

  `merge-dirty' Merging with a dirty worktree can make it hard to
  go back to the state before the merge was initiated.

References:

  `delete-unmerged-branch' Once a branch has been deleted it can
  only be restored using low-level recovery tools provided by
  Git.  And even then the reflog is gone.  The user always has
  to confirm the deletion of a branch by accepting the default
  choice (or selecting another branch), but when a branch has
  not been merged yet, also make sure the user is aware of that.

  `delete-branch-on-remote' Deleting a \"remote branch\" may mean
  deleting the (local) \"remote-tracking\" branch only, or also
  removing it from the remote itself.  The latter often makes more
  sense because otherwise simply fetching from the remote would
  restore the remote-tracking branch, but doing that can be
  surprising and hard to recover from, so we ask.

  `delete-pr-remote' When deleting a branch that was created from
  a pull-request and if no other branches still exist on that
  remote, then `magit-branch-delete' offers to delete the remote
  as well.  This should be safe because it only happens if no
  other refs exist in the remotes namespace, and you can recreate
  the remote if necessary.

  `drop-stashes' Dropping a stash is dangerous because Git stores
  stashes in the reflog.  Once a stash is removed, there is no
  going back without using low-level recovery tools provided by
  Git.  When a single stash is dropped, then the user always has
  to confirm by accepting the default (or selecting another).
  This action only concerns the deletion of multiple stashes at
  once.

Publishing:

  `set-and-push' When pushing to the upstream or the push-remote
  and that isn't actually configured yet, then the user can first
  set the target.  If s/he confirms the default too quickly, then
  s/he might end up pushing to the wrong branch and if the remote
  repository is configured to disallow fixing such mistakes, then
  that can be quite embarrassing and annoying.

Edit published history:

  Without adding these symbols here, you will be warned before
  editing commits that have already been pushed to one of the
  branches listed in `magit-published-branches'.

  `amend-published' Affects most commands that amend to `HEAD'.

  `rebase-published' Affects commands that perform interactive
  rebases.  This includes commands from the commit popup that
  modify a commit other than `HEAD', namely the various fixup
  and squash variants.

  `edit-published' Affects the commands `magit-edit-line-commit'
  and `magit-diff-edit-hunk-commit'.  These two commands make
  it quite easy to accidentally edit a published commit, so you
  should think twice before configuring them not to ask for
  confirmation.

  To disable confirmation completely, add all three symbols here
  or set `magit-published-branches' to nil.

Removing modules:

  `remove-modules' When you remove the working directory of a
  module that does not contain uncommitted changes, then that is
  safer than doing so when there are uncommitted changes and/or
  when you also remove the gitdir.  Still, you don't want to do
  that by accident.

  `remove-dirty-modules' When you remove the working directory of
  a module that contains uncommitted changes, then those changes
  are gone for good.  It is better to go to the module, inspect
  these changes and only if appropriate discard them manually.

  `trash-module-gitdirs' When you remove the gitdir of a module,
  then all unpushed changes are gone for good.  It is very easy
  to forget that you have some unfinished work on an unpublished
  feature branch or even in a stash.

  Actually there are some safety precautions in place, that might
  help you out if you make an unwise choice here, but don't count
  on it.  In case of emergency, stay calm and check the stash and
  the `trash-directory' for traces of lost work.

Various:

  `stash-apply-3way' When a stash cannot be applied using \"git
  stash apply\", then Magit uses \"git apply\" instead, possibly
  using the \"--3way\" argument, which isn't always perfectly
  safe.  See also `magit-stash-apply'.

  `kill-process' There seldom is a reason to kill a process.

Global settings:

  Instead of adding all of the above symbols to the value of this
  option you can also set it to the atom `t', which has the same
  effect as adding all of the above symbols.  Doing that most
  certainly is a bad idea, especially because other symbols might
  be added in the future.  So even if you don't want to be asked
  for confirmation for any of these actions, you are still better
  of adding all of the respective symbols individually.

  When `magit-wip-mode' is enabled then these actions can fairly
  easily be undone: `discard', `reverse', `stage-all-changes', and
  `unstage-all-changes'.  If and only if this mode is enabled, then
  `safe-with-wip' has the same effect as adding all of these symbols
  individually."
  :package-version '(magit . "2.1.0")
  :group 'magit-essentials
  :group 'magit-commands
  :type `(choice (const :tag "Always require confirmation" nil)
                 (const :tag "Never require confirmation" t)
                 (set   :tag "Require confirmation except for"
                        ;; `remove-dirty-modules' and
                        ;; `trash-module-gitdirs' intentionally
                        ;; omitted.
                        ,@magit--confirm-actions)))

(defcustom magit-slow-confirm '(drop-stashes)
  "Whether to ask user \"y or n\" or \"yes or no\" questions.

When this is nil, then `y-or-n-p' is used when the user has to
confirm a potentially destructive action.  When this is t, then
`yes-or-no-p' is used instead.  If this is a list of symbols
identifying actions, then `yes-or-no-p' is used for those,
`y-or-no-p' for all others.  The list of actions is the same as
for `magit-no-confirm' (which see)."
  :package-version '(magit . "2.9.0")
  :group 'magit-miscellaneous
  :type `(choice (const :tag "Always ask \"yes or no\" questions" t)
                 (const :tag "Always ask \"y or n\" questions" nil)
                 (set   :tag "Ask \"yes or no\" questions only for"
                        ,@magit--confirm-actions)))

(defcustom magit-no-message nil
  "A list of messages Magit should not display.

Magit displays most echo area messages using `message', but a few
are displayed using `magit-message' instead, which takes the same
arguments as the former, FORMAT-STRING and ARGS.  `magit-message'
forgoes printing a message if any member of this list is a prefix
of the respective FORMAT-STRING.

If Magit prints a message which causes you grief, then please
first investigate whether there is another option which can be
used to suppress it.  If that is not the case, then ask the Magit
maintainers to start using `magit-message' instead of `message'
in that case.  We are not proactively replacing all uses of
`message' with `magit-message', just in case someone *might* find
some of these messages useless.

Messages which can currently be suppressed using this option are:
* \"Turning on magit-auto-revert-mode...\""
  :package-version '(magit . "2.8.0")
  :group 'magit-miscellaneous
  :type '(repeat string))

(defcustom magit-verbose-messages nil
  "Whether to make certain prompts and messages more verbose.

Occasionally a user suggests that a certain prompt or message
should be more verbose, but I would prefer to keep it as-is
because I don't think that the fact that that one user did not
understand the existing prompt/message means that a large number
of users would have the same difficulty, and that making it more
verbose would actually do a disservice to users who understand
the shorter prompt well enough.

Going forward I will start offering both messages when I feel the
suggested longer message is reasonable enough, and the value of
this option decides which will be used.  Note that changing the
value of this option affects all such messages and that I do not
intend to add an option per prompt."
  :package-version '(magit . "4.0.0")
  :group 'magit-miscellaneous
  :type 'boolean)

(defcustom magit-ellipsis
  '((margin (?… . ">"))
    (t      (?… . "...")))
  "Characters or strings used to abbreviate text in some buffers.

Each element has the form (WHERE (FANCY . UNIVERSAL)).

FANCY is a single character or nil whereas UNIVERSAL is a string
of any length.  The ellipsis produced by `magit--ellipsis' will
be FANCY if it's a non-nil character that can be displayed with
the available fonts, otherwise UNIVERSAL will be used.  FANCY is
meant to be a rich character like a horizontal ellipsis symbol or
an emoji whereas UNIVERSAL something simpler available in a less
rich environment like the CLI.  WHERE determines the use-case for
the ellipsis definition.  Currently the only acceptable values
for WHERE are `margin' or t (representing the default).

Whether collapsed sections are indicated using ellipsis is
controlled by option `magit-section-visibility-indicators'."
  :package-version '(magit . "4.0.0")
  :group 'magit-miscellaneous
  :type '(repeat (list (symbol :tag "Where")
                       (cons (choice :tag "Fancy" character (const nil))
                             (string :tag "Universal")))))

(defcustom magit-update-other-window-delay 0.2
  "Delay before automatically updating the other window.

When moving around in certain buffers, then certain other
buffers, which are being displayed in another window, may
optionally be updated to display information about the
section at point.

When holding down a key to move by more than just one section,
then that would update that buffer for each section on the way.
To prevent that, updating the revision buffer is delayed, and
this option controls for how long.  For optimal experience you
might have to adjust this delay and/or the keyboard repeat rate
and delay of your graphical environment or operating system."
  :package-version '(magit . "2.3.0")
  :group 'magit-miscellaneous
  :type 'number)

(defcustom magit-view-git-manual-method 'info
  "How links to Git documentation are followed from Magit's Info manuals.

`info'  Follow the link to the node in the `gitman' Info manual
        as usual.  Unfortunately that manual is not installed by
        default on some platforms, and when it is then the nodes
        look worse than the actual manpages.

`man'   View the respective man-page using the `man' package.

`woman' View the respective man-page using the `woman' package."
  :package-version '(magit . "2.9.0")
  :group 'magit-miscellaneous
  :type '(choice (const :tag "View info manual" info)
                 (const :tag "View manpage using `man'" man)
                 (const :tag "View manpage using `woman'" woman)))

;;; Section Classes

(defclass magit-commit-section (magit-section)
  ((keymap :initform 'magit-commit-section-map)))

(setf (alist-get 'commit magit--section-type-alist) 'magit-commit-section)

(defclass magit-diff-section (magit-section)
  ((keymap :initform 'magit-diff-section-map))
  :abstract t)

(defclass magit-file-section (magit-diff-section)
  ((keymap :initform 'magit-file-section-map)
   (source :initform nil :initarg :source)
   (header :initform nil :initarg :header)
   (binary :initform nil :initarg :binary)
   (heading-highlight-face :initform 'magit-diff-file-heading-highlight)
   (heading-selection-face :initform 'magit-diff-file-heading-selection)))

(defclass magit-module-section (magit-file-section)
  ((keymap :initform 'magit-module-section-map)
   (range  :initform nil :initarg :range)))

(defclass magit-hunk-section (magit-diff-section)
  ((keymap      :initform 'magit-hunk-section-map)
   (painted     :initform nil)
   (fontified   :initform nil)
   (refined     :initform nil)
   (combined    :initform nil :initarg :combined)
   (from-range  :initform nil :initarg :from-range)
   (from-ranges :initform nil)
   (to-range    :initform nil :initarg :to-range)
   (about       :initform nil :initarg :about)
   (heading-highlight-face :initform 'magit-diff-hunk-heading-highlight)
   (heading-selection-face :initform 'magit-diff-hunk-heading-selection)))

(defun magit--meta-hunk-p (section)
  (not (cdr (oref section value))))

(setf (alist-get 'file   magit--section-type-alist) 'magit-file-section)
(setf (alist-get 'module magit--section-type-alist) 'magit-module-section)
(setf (alist-get 'hunk   magit--section-type-alist) 'magit-hunk-section)

(defclass magit-log-section (magit-section)
  ((keymap :initform 'magit-log-section-map))
  :abstract t)
(defclass magit-unpulled-section (magit-log-section) ())
(defclass magit-unpushed-section (magit-log-section) ())
(defclass magit-unmerged-section (magit-log-section) ())

(setf (alist-get 'unpulled magit--section-type-alist) 'magit-unpulled-section)
(setf (alist-get 'unpushed magit--section-type-alist) 'magit-unpushed-section)
(setf (alist-get 'unmerged magit--section-type-alist) 'magit-unmerged-section)

;;; User Input

(defvar helm-completion-in-region-default-sort-fn)
(defvar helm-crm-default-separator)
(defvar ivy-sort-functions-alist)
(defvar ivy-sort-matches-functions-alist)
(defvar vertico-sort-function)

(defvar magit-completing-read--silent-default nil)

(defvar magit-completing-read-default-prompt-predicate
  (lambda ()
    (and (eq magit-completing-read-function
             'magit-builtin-completing-read)
         (not (or (bound-and-true-p helm-mode)
                  (bound-and-true-p ivy-mode)
                  (bound-and-true-p selectrum-mode)
                  (bound-and-true-p vertico-mode)))))
  "Function used to determine whether to add default to prompt.

This is used by `magit-completing-read' (which see).

The default function returns nil, when a completion frameworks is used
for which this is undesirable.  More precisely, it returns nil, when
`magit-completing-read-function' is not `magit-builtin-completing-read',
or one of `helm-mode', `ivy-mode', `selectrum-mode' or `vertico-mode'
is enabled.  When this function returns nil, then nil is passed to
`format-prompt' (which see), instead of the default (DEF or FALLBACK).")

(defun magit-completing-read ( prompt collection &optional
                               predicate require-match initial-input
                               hist def fallback)
  "Read a choice in the minibuffer, or use the default choice.

This is the function that Magit commands use when they need the
user to select a single thing to act on.  The arguments have the
same meaning as for `completing-read', except for FALLBACK, which
is unique to this function and is described below.

Instead of asking the user to choose from a list of possible
candidates, this function may instead just return the default
specified by DEF, with or without requiring user confirmation.
Whether that is the case depends on PROMPT, `this-command' and
`magit-dwim-selection'.  See the documentation of the latter for
more information.

If it does use the default without the user even having to
confirm that, then `magit-completing-read--silent-default' is set
to t, otherwise nil.

If it does read a value in the minibuffer, then this function
acts similarly to `completing-read', except for the following:

- COLLECTION must be a list of choices.  A function is not
  supported.

- If REQUIRE-MATCH is nil and the user exits without a choice,
  then nil is returned instead of an empty string.

- If REQUIRE-MATCH is `any', then do not require a match but
  do require non-empty input (or non-nil DEFAULT, since that
  is substituted for empty input).

- If REQUIRE-MATCH is non-nil and the user exits without a
  choice, `user-error' is raised.

- FALLBACK specifies a secondary default that is only used if
  the primary default DEF is nil.  The secondary default is not
  subject to `magit-dwim-selection' — if DEF is nil but FALLBACK
  is not, then this function always asks the user to choose a
  candidate, just as if both defaults were nil.

- `format-prompt' is called on PROMPT and DEF (or FALLBACK if
  DEF is nil).  This appends \": \" to the prompt and may also
  add the default to the prompt, using the format specified by
  `minibuffer-default-prompt-format' and depending on
  `magit-completing-read-default-prompt-predicate'."
  (setq magit-completing-read--silent-default nil)
  (if-let ((_ def)
           (dwim (seq-some (pcase-lambda (`(,cmd ,re ,dwim))
                             (and (eq cmd this-command)
                                  (or (not re)
                                      (string-match-p re prompt))
                                  dwim))
                           magit-dwim-selection)))
      (if (eq dwim 'ask)
          (if (y-or-n-p (format "%s %s? " prompt def))
              def
            (user-error "Abort"))
        (setq magit-completing-read--silent-default t)
        def)
    (unless def
      (setq def fallback))
    (when (and def
               (not (functionp collection))
               (not (member def collection)))
      (setq collection (cons def collection)))
    (let ((command this-command)
          (reply (funcall magit-completing-read-function
                          (magit--format-prompt prompt def)
                          collection predicate
                          (if (eq require-match 'any) nil require-match)
                          initial-input hist def)))
      (setq this-command command)
      ;; Note: Avoid `string=' to support `helm-comp-read-use-marked'.
      (if (equal reply "")
          (if (and require-match
                   (not (and (listp collection)
                             (member "" collection))))
              (user-error "Nothing selected")
            nil)
        reply))))

(defun magit--format-prompt (prompt default)
  (format-prompt (if (string-suffix-p ": " prompt)
                     (substring prompt 0 -2)
                   prompt)
                 (and (funcall magit-completing-read-default-prompt-predicate)
                      default)))

(defun magit--completion-table (collection)
  (lambda (string pred action)
    (if (eq action 'metadata)
        '(metadata (display-sort-function . identity))
      (complete-with-action action collection string pred))))

(defun magit-builtin-completing-read
    (prompt choices &optional predicate require-match initial-input hist def)
  "Magit wrapper for standard `completing-read' function."
  (unless (or (bound-and-true-p helm-mode)
              (bound-and-true-p ivy-mode))
    (setq choices (magit--completion-table choices)))
  (let ((ivy-sort-functions-alist nil))
    (completing-read prompt
                     (lambda (str pred action)
                       (if (eq action 'metadata)
                           '(metadata (display-sort-function . identity)
                                      (cycle-sort-function . identity))
                         (complete-with-action action choices str pred)))
                     predicate require-match
                     initial-input hist def)))

(define-obsolete-function-alias 'magit-completing-read-multiple*
  'magit-completing-read-multiple "Magit-Section 4.0.0")

(defun magit-completing-read-multiple
    ( prompt table &optional predicate require-match initial-input
      hist def inherit-input-method
      no-split)
  "Read multiple strings in the minibuffer, with completion.
Like `completing-read-multiple' but don't mess with order of
TABLE and take an additional argument NO-SPLIT, which causes
the user input to be returned as a single unmodified string.
Also work around various incompatible features of various
third-party completion frameworks."
  (cl-letf*
      (;; To implement NO-SPLIT we have to manipulate the respective
       ;; `split-string' invocation.  We cannot simply advice it to
       ;; return the input string because `SELECTRUM' would choke on
       ;; that string.  Use a variable to pass along the raw user
       ;; input string. aa5f098ab
       (input nil)
       (split-string (symbol-function #'split-string))
       ((symbol-function #'split-string)
        (lambda (string &optional separators omit-nulls trim)
          (when (and no-split
                     (equal separators crm-separator)
                     (equal omit-nulls t))
            (setq input string))
          (funcall split-string string separators omit-nulls trim)))
       ;; Add the default to the table if absent, which is necessary
       ;; because we don't add it to the prompt for some frameworks.
       (table (if (and def
                       (listp table)
                       (not (listp (car table)))
                       (not (member def table)))
                  (cons def table)
                table))
       ;; Prevent `BUILT-IN' completion from messing up our existing
       ;; order of the completion candidates. aa5f098ab
       (table (magit--completion-table table))
       ;; Prevent `IVY' from messing up our existing order. c7af78726
       (ivy-sort-matches-functions-alist nil)
       ;; Prevent `HELM' from messing up our existing order.  6fcf994bd
       (helm-completion-in-region-default-sort-fn nil)
       ;; Prevent `HELM' from automatically appending the separator,
       ;; which is counterproductive when NO-SPLIT is non-nil and/or
       ;; when reading commit ranges. 798aff564
       (helm-crm-default-separator
        (if no-split nil (bound-and-true-p helm-crm-default-separator)))
       ;; And now, the moment we have all been waiting for...
       (values (completing-read-multiple
                (magit--format-prompt prompt def)
                table predicate
                (if (eq require-match 'any) nil require-match)
                initial-input hist def inherit-input-method)))
    (when (and (eq require-match 'any)
               (not values))
      (user-error "Nothing selected"))
    (if no-split input values)))

(defvar-keymap magit-minibuffer-local-ns-map
  :parent minibuffer-local-map
  "SPC" #'magit-whitespace-disallowed
  "TAB" #'magit-whitespace-disallowed)

(defun magit-whitespace-disallowed ()
  "Beep to tell the user that whitespace is not allowed."
  (interactive)
  (ding)
  (message "Whitespace isn't allowed here")
  (setq defining-kbd-macro nil)
  (force-mode-line-update))

(defun magit-read-string ( prompt &optional initial-input history default-value
                           inherit-input-method no-whitespace)
  "Read a string from the minibuffer, prompting with string PROMPT.

This is similar to `read-string', but
* empty input is only allowed if DEFAULT-VALUE is non-nil in
  which case that is returned,
* whitespace is not allowed and leading and trailing whitespace is
  removed automatically if NO-WHITESPACE is non-nil,
* `format-prompt' is used internally.
* an invalid DEFAULT-VALUE is silently ignored."
  (when default-value
    (when (consp default-value)
      (setq default-value (car default-value)))
    (unless (stringp default-value)
      (setq default-value nil)))
  (let* ((minibuffer-completion-table nil)
         (val (read-from-minibuffer
               (format-prompt prompt default-value)
               initial-input (and no-whitespace magit-minibuffer-local-ns-map)
               nil history default-value inherit-input-method))
         (trim (lambda (regexp string)
                 (save-match-data
                   (if (string-match regexp string)
                       (replace-match "" t t string)
                     string)))))
    (when (and (string= val "") default-value)
      (setq val default-value))
    (when no-whitespace
      (setq val (funcall trim "\\`\\(?:[ \t\n\r]+\\)"
                         (funcall trim "\\(?:[ \t\n\r]+\\)\\'" val))))
    (cond ((string= val "")
           (user-error "Need non-empty input"))
          ((and no-whitespace (string-match-p "[\s\t\n]" val))
           (user-error "Input contains whitespace"))
          (val))))

(defun magit-read-string-ns ( prompt &optional initial-input history
                              default-value inherit-input-method)
  "Call `magit-read-string' with non-nil NO-WHITESPACE."
  (magit-read-string prompt initial-input history default-value
                     inherit-input-method t))

(defmacro magit-read-char-case (prompt verbose &rest clauses)
  (declare (indent 2)
           (debug (form form &rest (characterp form body))))
  `(prog1 (pcase (read-char-choice
                  (let ((parts (nconc (list ,@(mapcar #'cadr clauses))
                                      ,(and verbose '(list "[C-g] to abort")))))
                    (concat ,prompt
                            (string-join (butlast parts) ", ")
                            ", or "  (car (last parts)) " "))
                  ',(mapcar #'car clauses))
            ,@(mapcar (##`(,(car %) ,@(cddr %))) clauses))
     (message "")))

(defun magit-y-or-n-p (prompt &optional action)
  "Ask user a \"y or n\" or a \"yes or no\" question using PROMPT.
Which kind of question is used depends on whether
ACTION is a member of option `magit-slow-confirm'."
  (if (or (eq magit-slow-confirm t)
          (and action (member action magit-slow-confirm)))
      (yes-or-no-p prompt)
    (y-or-n-p prompt)))

(defvar magit--no-confirm-alist
  '((safe-with-wip magit-wip-mode
                   discard reverse stage-all-changes unstage-all-changes)))

(cl-defun magit-confirm ( action &optional prompt prompt-n noabort
                          (items nil sitems) prompt-suffix)
  (declare (indent defun))
  (when (and prompt (listp prompt))
    (setq prompt
          (apply #'format (car prompt)
                 (mapcar (##if (stringp %) (string-replace "%" "%%" %) %)
                         (cdr prompt)))))
  (when (and prompt-n (listp prompt-n))
    (setq prompt-n
          (apply #'format (car prompt-n)
                 (mapcar (##if (stringp %) (string-replace "%" "%%" %) %)
                         (cdr prompt-n)))))
  (setq prompt-n (format (concat (or prompt-n prompt) "? ") (length items)))
  (setq prompt   (format (concat (or prompt (magit-confirm-make-prompt action))
                                 "? ")
                         (car items)))
  (when prompt-suffix
    (setq prompt (concat prompt prompt-suffix)))
  (or (cond ((and (not (eq action t))
                  (or (eq magit-no-confirm t)
                      (memq action magit-no-confirm)
                      (any (pcase-lambda (`(,key ,var . ,sub))
                             (and (memq key magit-no-confirm)
                                  (memq action sub)
                                  (or (not var)
                                      (and (boundp var)
                                           (symbol-value var)))))
                           magit--no-confirm-alist)))
             (or (not sitems) items))
            ((not sitems)
             (magit-y-or-n-p prompt action))
            ((length= items 1)
             (and (magit-y-or-n-p prompt action) items))
            ((length> items 1)
             (and (magit-y-or-n-p (concat (string-join items "\n")
                                          "\n\n" prompt-n)
                                  action)
                  items)))
      (if noabort nil (user-error "Abort"))))

(defun magit-confirm-files (action files &optional prompt prompt-suffix noabort)
  (when files
    (unless prompt
      (setq prompt (magit-confirm-make-prompt action)))
    (magit-confirm action
      (concat prompt " \"%s\"")
      (concat prompt " %d files")
      noabort files prompt-suffix)))

(defun magit-confirm-make-prompt (action)
  (let ((prompt (symbol-name action)))
    (string-replace "-" " "
                    (concat (upcase (substring prompt 0 1))
                            (substring prompt 1)))))

(defun magit-read-number-string (prompt &optional default _history)
  "Like `read-number' but return value is a string.
DEFAULT may be a number or a numeric string."
  (number-to-string
   (read-number prompt (if (stringp default)
                           (string-to-number default)
                         default))))

;;; Debug Utilities

;;;###autoload
(defun magit-emacs-Q-command ()
  "Show a shell command that runs an uncustomized Emacs with only Magit loaded.
See info node `(magit)Debugging Tools' for more information."
  (interactive)
  (let ((cmd (mapconcat
              #'shell-quote-argument
              `(,(concat invocation-directory invocation-name)
                "-Q" "--eval" "(setq debug-on-error t)"
                ,@(mapcan
                   (##list "-L" %)
                   (delete-dups
                    (seq-keep
                     (lambda (lib)
                       (if-let ((path (locate-library lib)))
                           (file-name-directory path)
                         (error "Cannot find mandatory dependency %s" lib)))
                     '(;; Like `LOAD_PATH' in `default.mk'.
                       "compat"
                       "cond-let"
                       "llama"
                       "seq"
                       "transient"
                       "with-editor"
                       ;; Obviously `magit' itself is needed too.
                       "magit"
                       ;; While this is part of the Magit repository,
                       ;; it is distributed as a separate package.
                       "magit-section"))))
                ;; Avoid Emacs bug#16406 by using full path.
                "-l" ,(file-name-sans-extension (locate-library "magit")))
              " ")))
    (message "Uncustomized Magit command saved to kill-ring, %s"
             "please run it in a terminal.")
    (kill-new cmd)))

;;; Text Utilities

(defmacro magit-bind-match-strings (varlist string &rest body)
  "Bind variables to submatches according to VARLIST then evaluate BODY.
Bind the symbols in VARLIST to submatches of the current match data,
starting with 1 and incrementing by 1 for each symbol.  If the last
match was against a string, then that has to be provided as STRING."
  (declare (indent 2) (debug (listp form body)))
  (let ((s (gensym "string"))
        (i 0))
    `(let* ((,s ,string)
            ,@(save-match-data
                (seq-keep (lambda (sym)
                            (incf i)
                            (and (not (eq (aref (symbol-name sym) 0) ?_))
                                 `(,sym (match-str ,i ,s))))
                          varlist)))
       ,@body)))

(defun magit-delete-line ()
  "Delete the rest of the current line."
  (delete-region (point) (1+ (line-end-position))))

(defun magit-delete-match (&optional num)
  "Delete text matched by last search.
If optional NUM is specified, only delete that subexpression."
  (delete-region (match-beginning (or num 0))
                 (match-end (or num 0))))

(defun magit-file-line (file)
  "Return the first line of FILE as a string."
  (and (file-regular-p file)
       (with-temp-buffer
         (insert-file-contents file)
         (buffer-substring-no-properties (point-min)
                                         (line-end-position)))))

(defun magit-file-lines (file &optional keep-empty-lines)
  "Return a list of strings containing one element per line in FILE.
Unless optional argument KEEP-EMPTY-LINES is t, trim all empty lines."
  (and (file-regular-p file)
       (with-temp-buffer
         (insert-file-contents file)
         (split-string (buffer-string) "\n" (not keep-empty-lines)))))

(defun magit-set-header-line-format (string)
  "Set `header-line-format' in the current buffer based on STRING.
Pad the left side of STRING so that it aligns with the text area."
  (setq header-line-format
        (concat (propertize " " 'display '(space :align-to 0))
                string)))

(defun magit--format-spec (format specification)
  "Like `format-spec' but preserve text properties in SPECIFICATION."
  (with-temp-buffer
    (insert format)
    (goto-char (point-min))
    (while (search-forward "%" nil t)
      (cond
        ;; Quoted percent sign.
        ((eq (char-after) ?%)
         (delete-char 1))
        ;; Valid format spec.
        ((looking-at "\\([-0-9.]*\\)\\([a-zA-Z]\\)")
         (let* ((num (match-str 1))
                (spec (string-to-char (match-str 2)))
                (val (assq spec specification)))
           (unless val
             (error "Invalid format character: `%%%c'" spec))
           (setq val (cdr val))
           ;; Pad result to desired length.
           (let ((text (format (concat "%" num "s") val)))
             ;; Insert first, to preserve text properties.
             (if (next-property-change 0 (concat " " text))
                 ;; If the inserted text has properties, then preserve those.
                 (insert text)
               ;; Otherwise preserve FORMAT's properties, like `format-spec'.
               (insert-and-inherit text))
             ;; Delete the specifier body.
             (delete-region (+ (match-beginning 0) (length text))
                            (+ (match-end 0) (length text)))
             ;; Delete the percent sign.
             (delete-region (1- (match-beginning 0)) (match-beginning 0)))))
        ;; Signal an error on bogus format strings.
        ((error "Invalid format string"))))
    (buffer-string)))

;;; Missing from Emacs

(defun magit--separate (pred list)
  "Separate elements of LIST that do and don't satisfy PRED.
Return a list of two lists; the first containing the elements that
do satisfy PRED and the second containing the elements that don't."
  (let (y n)
    (dolist (elt list)
      (push elt (if (funcall pred elt) y n)))
    (list (nreverse y)
          (nreverse n))))

(defun magit--version> (v1 v2)
  "Return t if version V1 is higher (younger) than V2.
This function should be named `version>' and be part of Emacs."
  (version-list-< (version-to-list v2) (version-to-list v1)))

(defun magit--version>= (v1 v2)
  "Return t if version V1 is higher (younger) than or equal to V2.
This function should be named `version>=' and be part of Emacs."
  (version-list-<= (version-to-list v2) (version-to-list v1)))

(defun magit--delete-text-properties (string &optional props)
  "Delete text properties PROPS from STRING and return it.
If PROPS is nil, remove all properties.  To leave STRING unchanged
and return a new string, instead use `magit--remove-text-properties'."
  (set-text-properties 0 (length string) props string)
  string)

(defun magit--remove-text-properties (string &optional props)
  "Return a copy of STRING with text properties PROPS removed.
If PROPS is nil, remove all properties."
  (magit--delete-text-properties (copy-sequence string) props))

;;; Kludges for Emacs Bugs

(defun magit-which-function ()
  "Return current function name based on point, without caching.

This is a simple wrapper around `which-function', that resets
Imenu's potentially outdated and therefore unreliable cache by
setting `imenu--index-alist' to nil before calling that function."
  (setq imenu--index-alist nil)
  (which-function))

(static-when (version< emacs-version "31.1")
  (define-advice dabbrev-capf (:around (fn) git-commit)
    "Backport bugfix from debbug#80645 / a7d05207214 / 31.1.
See #5551, #5556 and #5558 (I wish I had not rushed this)."
    (pcase-let ((`(,beg ,end ,table . ,rest) (funcall fn)))
      `( ,beg ,end
         ,(lambda (&rest args)
            (condition-case err
                (apply table args)
              (user-error
               (unless (string-prefix-p "No dynamic expansion" (cadr err))
                 (signal (car err) (cdr err))))))
         ,@rest))))

;;; Kludges for Custom

(defun magit-custom-initialize-reset (symbol exp)
  "Initialize SYMBOL based on EXP.
Set the value of the variable SYMBOL, using `set-default'
\(unlike `custom-initialize-reset', which uses the `:set'
function if any).  The value is either the symbol's current
value (as obtained using the `:get' function), if any, or
the value in the symbol's `saved-value' property if any, or
\(last of all) the value of EXP."
  (set-default-toplevel-value
   symbol
   (condition-case nil
       (let ((def (default-toplevel-value symbol))
             (getter (get symbol 'custom-get)))
         (if getter (funcall getter symbol) def))
     (error
      (eval (let ((sv (get symbol 'saved-value)))
              (if sv (car sv) exp)))))))

(defun magit-hook-custom-get (symbol)
  (if (symbol-file symbol 'defvar)
      (default-toplevel-value symbol)
    ;;
    ;; Called by `custom-initialize-reset' on behalf of `symbol's
    ;; `defcustom', which is being evaluated for the first time to
    ;; set the initial value, but there's already a default value,
    ;; which most likely was established by one or more `add-hook'
    ;; calls.
    ;;
    ;; We combine the `standard-value' and the current value, while
    ;; preserving the order established by `:options', and return
    ;; the result of that to be used as the "initial" default value.
    ;;
    (let ((standard (eval (car (get symbol 'standard-value))))
          (current (default-toplevel-value symbol))
          (value nil))
      (dolist (fn (get symbol 'custom-options))
        (when (or (memq fn standard)
                  (memq fn current))
          (push fn value)))
      (dolist (fn current)
        (unless (memq fn value)
          (push fn value)))
      (nreverse value))))

;;; Kludges for Info Manuals

;;;###autoload
(define-advice Info-follow-nearest-node (:around (fn &optional fork) gitman)
  ;; Do not use `if-let*' (aka `cond-let--if-let*') because this is
  ;; copied to the autoload file, which does not require `cond-let'.
  (let ((node (Info-get-token
               (point) "\\*note[ \n\t]+"
               "\\*note[ \n\t]+\\([^:]*\\):\\(:\\|[ \n\t]*(\\)?")))
    (if (and node (string-match "^(gitman)\\(.+\\)" node))
        (pcase magit-view-git-manual-method
          ('info  (funcall fn fork))
          ('man   (require 'man)
                  (man (match-str 1 node)))
          ('woman (require 'woman)
                  (woman (match-str 1 node)))
          (_ (user-error "Invalid value for `magit-view-git-manual-method'")))
      (funcall fn fork))))

;; When making changes here, then also adjust the copy in docs/Makefile.
;;;###autoload
(define-advice org-man-export (:around (fn link description format) gitman)
  (if (and (eq format 'texinfo)
           (string-prefix-p "git" link))
      (string-replace "%s" link "
@ifinfo
@ref{%s,,,gitman,}.
@end ifinfo
@ifhtml
@html
the <a href=\"http://git-scm.com/docs/%s\">%s(1)</a> manpage.
@end html
@end ifhtml
@iftex
the %s(1) manpage.
@end iftex
")
    (funcall fn link description format)))

;;; Kludges for Package Managers

(defun magit--chase-links (filename)
  "Chase links in FILENAME until a name that is not a link.

This is the same as `file-chase-links', except that it also handles
fake symlinks that are created by some source based package managers
\(Elpaca and Straight) on Windows.

See <https://github.com/raxod502/straight.el/issues/520>."
  (when-let*
      ((manager (cond ((bound-and-true-p straight-symlink-mode) 'straight)
                      ((bound-and-true-p elpaca-no-symlink-mode) 'elpaca)))
       (build (pcase manager
                ('straight (bound-and-true-p straight-build-dir))
                ('elpaca (bound-and-true-p elpaca-builds-directory))))
       (_(string-prefix-p build filename))
       (repo (pcase manager
               ('straight
                (and (bound-and-true-p straight-base-dir)
                     (expand-file-name "repos/magit/lisp/" straight-base-dir)))
               ('elpaca
                (and (bound-and-true-p elpaca-repos-directory)
                     (expand-file-name "magit/lisp/" elpaca-repos-directory))))))
    (setq filename (expand-file-name (file-name-nondirectory filename) repo)))
  (file-chase-links filename))

;;; Miscellaneous

(defun magit-message (format-string &rest args)
  "Display a message at the bottom of the screen, or not.
Like `message', except that if the users configured option
`magit-no-message' to prevent the message corresponding to
FORMAT-STRING to be displayed, then don't."
  (unless (seq-find (##string-prefix-p % format-string) magit-no-message)
    (apply #'message format-string args)))

(defun magit-msg (format-string &rest args)
  "Display a message at the bottom of the screen, but don't log it.
Like `message', except that `message-log-max' is bound to nil."
  (let ((message-log-max nil))
    (apply #'message format-string args)))

(defmacro magit--with-temp-position (buf pos &rest body)
  (declare (indent 2))
  `(with-current-buffer ,buf
     (save-excursion
       (save-restriction
         (widen)
         (goto-char (or ,pos 1))
         ,@body))))

(defun magit--ellipsis (&optional where)
  "Build an ellipsis always as string, depending on WHERE."
  (cond-let
    ((stringp magit-ellipsis)
     magit-ellipsis)
    ([pair (car (or (alist-get (or where t) magit-ellipsis)
                    (alist-get t magit-ellipsis)))]
     (pcase-let* ((`(,fancy . ,universal) pair)
                  (ellipsis (if (and fancy (char-displayable-p fancy))
                                fancy
                              universal)))
       (if (characterp ellipsis)
           (char-to-string ellipsis)
         ellipsis)))
    ((user-error "Variable magit-ellipsis is invalid"))))

(defun magit--ext-regexp-quote (string)
  "Like `reqexp-quote', but for Extended Regular Expressions."
  (let ((special (string-to-list "[*.\\?+^$({"))
        (quoted nil))
    (dolist (char string)
      (when (memq char special)
        (push ?\\ quoted))
      (push char quoted))
    (concat (nreverse quoted))))

(defun magit--find-buffer (&rest plist)
  "Like `find-buffer' but take multiple VARIABLE-VALUE pairs."
  (seq-find (lambda (buf)
              (let ((plist plist))
                (while (and plist
                            (equal (buffer-local-value (car plist) buf)
                                   (cadr plist)))
                  (setq plist (cddr plist)))
                (not plist)))
            (buffer-list)))

;;; _
(provide 'magit-base)
;; Local Variables:
;; read-symbol-shorthands: (
;;   ("and$"         . "cond-let--and$")
;;   ("thread$"      . "cond-let--thread$")
;;   ("when$"        . "cond-let--when$")
;;   ("and-let*"     . "cond-let--and-let*")
;;   ("and-let"      . "cond-let--and-let")
;;   ("if-let*"      . "cond-let--if-let*")
;;   ("if-let"       . "cond-let--if-let")
;;   ("when-let*"    . "cond-let--when-let*")
;;   ("when-let"     . "cond-let--when-let")
;;   ("while-let*"   . "cond-let--while-let*")
;;   ("while-let"    . "cond-let--while-let")
;;   ("match-string" . "match-string")
;;   ("match-str"    . "match-string-no-properties"))
;; End:
;;; magit-base.el ends here
