diff options
author | blendoit <blendoit@gmail.com> | 2019-09-03 17:32:57 -0700 |
---|---|---|
committer | blendoit <blendoit@gmail.com> | 2019-09-03 17:32:57 -0700 |
commit | f1bd7efba96174cdaa197f3ecdccbeacf9235f88 (patch) | |
tree | 4c620ad3babfca3759011f38950ed156b6e13d6c /elpa/magit-20190902.1343/magit-log.el |
First commit
Diffstat (limited to 'elpa/magit-20190902.1343/magit-log.el')
-rw-r--r-- | elpa/magit-20190902.1343/magit-log.el | 1746 |
1 files changed, 1746 insertions, 0 deletions
diff --git a/elpa/magit-20190902.1343/magit-log.el b/elpa/magit-20190902.1343/magit-log.el new file mode 100644 index 0000000..0307c90 --- /dev/null +++ b/elpa/magit-20190902.1343/magit-log.el @@ -0,0 +1,1746 @@ +;;; magit-log.el --- inspect Git history -*- lexical-binding: t -*- + +;; Copyright (C) 2010-2019 The Magit Project Contributors +;; +;; You should have received a copy of the AUTHORS.md file which +;; lists all contributors. If not, see http://magit.vc/authors. + +;; Author: Jonas Bernoulli <jonas@bernoul.li> +;; Maintainer: Jonas Bernoulli <jonas@bernoul.li> + +;; 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, 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 http://www.gnu.org/licenses. + +;;; Commentary: + +;; This library implements support for looking at Git logs, including +;; special logs like cherry-logs, as well as for selecting a commit +;; from a log. + +;;; Code: + +(require 'magit-core) +(require 'magit-diff) + +(declare-function magit-blob-visit "magit-files" (blob-or-file line)) +(declare-function magit-insert-head-branch-header "magit-status" + (&optional branch)) +(declare-function magit-insert-upstream-branch-header "magit-status" + (&optional branch pull keyword)) +(declare-function magit-read-file-from-rev "magit-files" + (rev prompt &optional default)) +(declare-function magit-show-commit "magit-diff" + (arg1 &optional arg2 arg3 arg4)) +(declare-function magit-reflog-format-subject "magit-reflog" (subject)) +(defvar magit-refs-focus-column-width) +(defvar magit-refs-margin) +(defvar magit-refs-show-commit-count) +(defvar magit-buffer-margin) +(defvar magit-status-margin) +(defvar magit-status-sections-hook) + +(require 'ansi-color) +(require 'crm) +(require 'which-func) + +(eval-when-compile + (require 'subr-x)) + +;;; Options +;;;; Log Mode + +(defgroup magit-log nil + "Inspect and manipulate Git history." + :link '(info-link "(magit)Logging") + :group 'magit-modes) + +(defcustom magit-log-mode-hook nil + "Hook run after entering Magit-Log mode." + :group 'magit-log + :type 'hook) + +(defcustom magit-log-remove-graph-args '("--follow" "--grep" "-G" "-S" "-L") + "The log arguments that cause the `--graph' argument to be dropped." + :package-version '(magit . "2.3.0") + :group 'magit-log + :type '(repeat (string :tag "Argument")) + :options '("--follow" "--grep" "-G" "-S" "-L")) + +(defcustom magit-log-revision-headers-format "\ +%+b +Author: %aN <%aE> +Committer: %cN <%cE>" + "Additional format string used with the `++header' argument." + :package-version '(magit . "2.3.0") + :group 'magit-log + :type 'string) + +(defcustom magit-log-auto-more nil + "Insert more log entries automatically when moving past the last entry. +Only considered when moving past the last entry with +`magit-goto-*-section' commands." + :group 'magit-log + :type 'boolean) + +(defcustom magit-log-margin '(t age magit-log-margin-width t 18) + "Format of the margin in `magit-log-mode' buffers. + +The value has the form (INIT STYLE WIDTH AUTHOR AUTHOR-WIDTH). + +If INIT is non-nil, then the margin is shown initially. +STYLE controls how to format the committer date. It can be one + of `age' (to show the age of the commit), `age-abbreviated' (to + abbreviate the time unit to a character), or a string (suitable + for `format-time-string') to show the actual date. +WIDTH controls the width of the margin. This exists for forward + compatibility and currently the value should not be changed. +AUTHOR controls whether the name of the author is also shown by + default. +AUTHOR-WIDTH has to be an integer. When the name of the author + is shown, then this specifies how much space is used to do so." + :package-version '(magit . "2.9.0") + :group 'magit-log + :group 'magit-margin + :type magit-log-margin--custom-type + :initialize 'magit-custom-initialize-reset + :set (apply-partially #'magit-margin-set-variable 'magit-log-mode)) + +(defcustom magit-log-show-refname-after-summary nil + "Whether to show refnames after commit summaries. +This is useful if you use really long branch names." + :package-version '(magit . "2.2.0") + :group 'magit-log + :type 'boolean) + +(defcustom magit-log-highlight-keywords t + "Whether to highlight bracketed keywords in commit summaries." + :package-version '(magit . "2.12.0") + :group 'magit-log + :type 'boolean) + +(defcustom magit-log-header-line-function 'magit-log-header-line-sentence + "Function used to generate text shown in header line of log buffers." + :package-version '(magit . "2.12.0") + :group 'magit-log + :type '(choice (function-item magit-log-header-line-arguments) + (function-item magit-log-header-line-sentence) + function)) + +(defcustom magit-log-trace-definition-function 'magit-which-function + "Function used to determine the function at point. +This is used by the command `magit-log-trace-definition'. +You should prefer `magit-which-function' over `which-function' +because the latter may make use of Imenu's outdated cache." + :package-version '(magit . "2.91.0") + :group 'magit-log + :type '(choice (function-item magit-which-function) + (function-item which-function) + (function-item add-log-current-defun) + function)) + +(defface magit-log-graph + '((((class color) (background light)) :foreground "grey30") + (((class color) (background dark)) :foreground "grey80")) + "Face for the graph part of the log output." + :group 'magit-faces) + +(defface magit-log-author + '((((class color) (background light)) + :foreground "firebrick" + :slant normal + :weight normal) + (((class color) (background dark)) + :foreground "tomato" + :slant normal + :weight normal)) + "Face for the author part of the log output." + :group 'magit-faces) + +(defface magit-log-date + '((((class color) (background light)) + :foreground "grey30" + :slant normal + :weight normal) + (((class color) (background dark)) + :foreground "grey80" + :slant normal + :weight normal)) + "Face for the date part of the log output." + :group 'magit-faces) + +(defface magit-header-line-log-select + '((t :inherit bold)) + "Face for the `header-line' in `magit-log-select-mode'." + :group 'magit-faces) + +;;;; File Log + +(defcustom magit-log-buffer-file-locked t + "Whether `magit-log-buffer-file-quick' uses a dedicated buffer." + :package-version '(magit . "2.7.0") + :group 'magit-commands + :group 'magit-log + :type 'boolean) + +;;;; Select Mode + +(defcustom magit-log-select-show-usage 'both + "Whether to show usage information when selecting a commit from a log. +The message can be shown in the `echo-area' or the `header-line', or in +`both' places. If the value isn't one of these symbols, then it should +be nil, in which case no usage information is shown." + :package-version '(magit . "2.1.0") + :group 'magit-log + :type '(choice (const :tag "in echo-area" echo-area) + (const :tag "in header-line" header-line) + (const :tag "in both places" both) + (const :tag "nowhere"))) + +(defcustom magit-log-select-margin + (list (nth 0 magit-log-margin) + (nth 1 magit-log-margin) + 'magit-log-margin-width t + (nth 4 magit-log-margin)) + "Format of the margin in `magit-log-select-mode' buffers. + +The value has the form (INIT STYLE WIDTH AUTHOR AUTHOR-WIDTH). + +If INIT is non-nil, then the margin is shown initially. +STYLE controls how to format the committer date. It can be one + of `age' (to show the age of the commit), `age-abbreviated' (to + abbreviate the time unit to a character), or a string (suitable + for `format-time-string') to show the actual date. +WIDTH controls the width of the margin. This exists for forward + compatibility and currently the value should not be changed. +AUTHOR controls whether the name of the author is also shown by + default. +AUTHOR-WIDTH has to be an integer. When the name of the author + is shown, then this specifies how much space is used to do so." + :package-version '(magit . "2.9.0") + :group 'magit-log + :group 'magit-margin + :type magit-log-margin--custom-type + :initialize 'magit-custom-initialize-reset + :set-after '(magit-log-margin) + :set (apply-partially #'magit-margin-set-variable 'magit-log-select-mode)) + +;;;; Cherry Mode + +(defcustom magit-cherry-sections-hook + '(magit-insert-cherry-headers + magit-insert-cherry-commits) + "Hook run to insert sections into the cherry buffer." + :package-version '(magit . "2.1.0") + :group 'magit-log + :type 'hook) + +(defcustom magit-cherry-margin + (list (nth 0 magit-log-margin) + (nth 1 magit-log-margin) + 'magit-log-margin-width t + (nth 4 magit-log-margin)) + "Format of the margin in `magit-cherry-mode' buffers. + +The value has the form (INIT STYLE WIDTH AUTHOR AUTHOR-WIDTH). + +If INIT is non-nil, then the margin is shown initially. +STYLE controls how to format the committer date. It can be one + of `age' (to show the age of the commit), `age-abbreviated' (to + abbreviate the time unit to a character), or a string (suitable + for `format-time-string') to show the actual date. +WIDTH controls the width of the margin. This exists for forward + compatibility and currently the value should not be changed. +AUTHOR controls whether the name of the author is also shown by + default. +AUTHOR-WIDTH has to be an integer. When the name of the author + is shown, then this specifies how much space is used to do so." + :package-version '(magit . "2.9.0") + :group 'magit-log + :group 'magit-margin + :type magit-log-margin--custom-type + :initialize 'magit-custom-initialize-reset + :set-after '(magit-log-margin) + :set (apply-partially #'magit-margin-set-variable 'magit-cherry-mode)) + +;;;; Log Sections + +(defcustom magit-log-section-commit-count 10 + "How many recent commits to show in certain log sections. +How many recent commits `magit-insert-recent-commits' and +`magit-insert-unpulled-from-upstream-or-recent' (provided +the upstream isn't ahead of the current branch) show." + :package-version '(magit . "2.1.0") + :group 'magit-status + :type 'number) + +;;; Arguments +;;;; Prefix Classes + +(defclass magit-log-prefix (transient-prefix) + ((history-key :initform 'magit-log) + (major-mode :initform 'magit-log-mode))) + +(defclass magit-log-refresh-prefix (magit-log-prefix) + ((history-key :initform 'magit-log) + (major-mode :initform nil))) + +;;;; Prefix Methods + +(cl-defmethod transient-init-value ((obj magit-log-prefix)) + (pcase-let ((`(,args ,files) + (magit-log--get-value 'magit-log-mode + magit-prefix-use-buffer-arguments))) + (unless (eq current-transient-command 'magit-dispatch) + (when-let ((file (magit-file-relative-name))) + (setq files (list file)))) + (oset obj value (if files `(("--" ,@files) ,args) args)))) + +(cl-defmethod transient-init-value ((obj magit-log-refresh-prefix)) + (oset obj value (if magit-buffer-log-files + `(("--" ,@magit-buffer-log-files) + ,magit-buffer-log-args) + magit-buffer-log-args))) + +(cl-defmethod transient-set-value ((obj magit-log-prefix)) + (magit-log--set-value obj)) + +(cl-defmethod transient-save-value ((obj magit-log-prefix)) + (magit-log--set-value obj 'save)) + +;;;; Argument Access + +(defun magit-log-arguments (&optional mode) + "Return the current log arguments." + (if (memq current-transient-command '(magit-log magit-log-refresh)) + (pcase-let ((`(,args ,alist) + (-separate #'atom (transient-get-value)))) + (list args (cdr (assoc "--" alist)))) + (magit-log--get-value (or mode 'magit-log-mode)))) + +(defun magit-log--get-value (mode &optional use-buffer-args) + (unless use-buffer-args + (setq use-buffer-args magit-direct-use-buffer-arguments)) + (let (args files) + (cond + ((and (memq use-buffer-args '(always selected current)) + (eq major-mode mode)) + (setq args magit-buffer-log-args) + (setq files magit-buffer-log-files)) + ((and (memq use-buffer-args '(always selected)) + (when-let ((buffer (magit-get-mode-buffer + mode nil + (or (eq use-buffer-args 'selected) 'all)))) + (setq args (buffer-local-value 'magit-buffer-log-args buffer)) + (setq files (buffer-local-value 'magit-buffer-log-files buffer)) + t))) + ((plist-member (symbol-plist mode) 'magit-log-current-arguments) + (setq args (get mode 'magit-log-current-arguments))) + ((when-let ((elt (assq (intern (format "magit-log:%s" mode)) + transient-values))) + (setq args (cdr elt)) + t)) + (t + (setq args (get mode 'magit-log-default-arguments)))) + (list args files))) + +(defun magit-log--set-value (obj &optional save) + (pcase-let* ((obj (oref obj prototype)) + (mode (or (oref obj major-mode) major-mode)) + (key (intern (format "magit-log:%s" mode))) + (`(,args ,alist) + (-separate #'atom (transient-get-value))) + (files (cdr (assoc "--" alist)))) + (put mode 'magit-log-current-arguments args) + (when save + (setf (alist-get key transient-values) args) + (transient-save-values)) + (transient--history-push obj) + (setq magit-buffer-log-args args) + (unless (derived-mode-p 'magit-log-select-mode) + (setq magit-buffer-log-files files)) + (magit-refresh))) + +;;; Commands +;;;; Prefix Commands + +;;;###autoload (autoload 'magit-log "magit-log" nil t) +(define-transient-command magit-log () + "Show a commit or reference log." + :man-page "git-log" + :class 'magit-log-prefix + ;; The grouping in git-log(1) appears to be guided by implementation + ;; details, so our logical grouping only follows it to an extend. + ;; Arguments that are "misplaced" here: + ;; 1. From "Commit Formatting". + ;; 2. From "Common Diff Options". + ;; 3. From unnamed first group. + ;; 4. Implemented by Magit. + ["Commit limiting" + (magit-log:-n) + (magit:--author) + (7 "=s" "Limit to commits since" "--since=" transient-read-date) + (7 "=u" "Limit to commits until" "--until=" transient-read-date) + (magit-log:--grep) + (7 "-I" "Invert search pattern" "--invert-grep") + (magit-log:-G) ;2 + (magit-log:-S) ;2 + (magit-log:-L) ;2 + (7 "=m" "Omit merges" "--no-merges") + (7 "=p" "First parent" "--first-parent")] + ["History simplification" + ( "-D" "Simplify by decoration" "--simplify-by-decoration") + (magit:--) + ( "-f" "Follow renames when showing single-file log" "--follow") ;3 + (6 "/s" "Only commits changing given paths" "--sparse") + (7 "/d" "Only selected commits plus meaningful history" "--dense") + (7 "/a" "Only commits existing directly on ancestry path" "--ancestry-path") + (6 "/f" "Do not prune history" "--full-history") + (7 "/m" "Prune some history" "--simplify-merges")] + ["Commit ordering" + (magit-log:--*-order) + ("-r" "Reverse order" "--reverse")] + ["Formatting" + ("-g" "Show graph" "--graph") ;1 + ("-c" "Show graph in color" "--color") ;2 + ("-d" "Show refnames" "--decorate") ;3 + ("=S" "Show signatures" "--show-signature") ;1 + ("-h" "Show header" "++header") ;4 + ("-p" "Show diffs" ("-p" "--patch")) ;2 + ("-s" "Show diffstats" "--stat")] ;2 + [["Log" + ("l" "current" magit-log-current) + ("o" "other" magit-log-other) + ("h" "HEAD" magit-log-head)] + ["" + ("L" "local branches" magit-log-branches) + (7 "B" "matching branches" magit-log-matching-branches) + ("b" "all branches" magit-log-all-branches) + ("a" "all references" magit-log-all) + (7 "m" "merged" magit-log-merged)] + ["Reflog" + ("r" "current" magit-reflog-current) + ("O" "other" magit-reflog-other) + ("H" "HEAD" magit-reflog-head)] + [:if-non-nil magit-wip-mode + :description "Wiplog" + ("i" "index" magit-wip-log-index) + ("w" "worktree" magit-wip-log-worktree)]]) + +;;;###autoload (autoload 'magit-log-refresh "magit-log" nil t) +(define-transient-command magit-log-refresh () + "Change the arguments used for the log(s) in the current buffer." + :man-page "git-log" + :class 'magit-log-refresh-prefix + [:if-mode magit-log-mode + :class transient-subgroups + ["Commit limiting" + (magit-log:-n) + (magit:--author) + (magit-log:--grep) + (7 "-I" "Invert search pattern" "--invert-grep") + (magit-log:-G) + (magit-log:-S) + (magit-log:-L)] + ["History simplification" + ( "-D" "Simplify by decoration" "--simplify-by-decoration") + (magit:--) + ( "-f" "Follow renames when showing single-file log" "--follow") ;3 + (6 "/s" "Only commits changing given paths" "--sparse") + (7 "/d" "Only selected commits plus meaningful history" "--dense") + (7 "/a" "Only commits existing directly on ancestry path" "--ancestry-path") + (6 "/f" "Do not prune history" "--full-history") + (7 "/m" "Prune some history" "--simplify-merges")] + ["Commit ordering" + (magit-log:--*-order) + ("-r" "Reverse order" "--reverse")] + ["Formatting" + ("-g" "Show graph" "--graph") + ("-c" "Show graph in color" "--color") + ("-d" "Show refnames" "--decorate") + ("=S" "Show signatures" "--show-signature") + ("-h" "Show header" "++header") + ("-p" "Show diffs" ("-p" "--patch")) + ("-s" "Show diffstats" "--stat")]] + [:if-not-mode magit-log-mode + :description "Arguments" + (magit-log:-n) + (magit-log:--*-order) + ("-g" "Show graph" "--graph") + ("-c" "Show graph in color" "--color") + ("-d" "Show refnames" "--decorate")] + [["Refresh" + ("g" "buffer" magit-log-refresh) + ("s" "buffer and set defaults" transient-set :transient nil) + ("w" "buffer and save defaults" transient-save :transient nil)] + ["Margin" + ("L" "toggle visibility" magit-toggle-margin) + ("l" "cycle style" magit-cycle-margin-style) + ("d" "toggle details" magit-toggle-margin-details) + ("x" "toggle shortstat" magit-toggle-log-margin-style)] + [:if-mode magit-log-mode + :description "Toggle" + ("b" "buffer lock" magit-toggle-buffer-lock)]] + (interactive) + (cond + ((not (eq current-transient-command 'magit-log-refresh)) + (pcase major-mode + (`magit-reflog-mode + (user-error "Cannot change log arguments in reflog buffers")) + (`magit-cherry-mode + (user-error "Cannot change log arguments in cherry buffers"))) + (transient-setup 'magit-log-refresh)) + (t + (pcase-let ((`(,args ,files) (magit-log-arguments))) + (setq magit-buffer-log-args args) + (unless (derived-mode-p 'magit-log-select-mode) + (setq magit-buffer-log-files files))) + (magit-refresh)))) + +;;;; Infix Commands + +(define-infix-argument magit-log:-n () + :description "Limit number of commits" + :class 'transient-option + ;; For historic reasons (and because it easy to guess what "-n" + ;; stands for) this is the only argument where we do not use the + ;; long argument ("--max-count"). + :shortarg "-n" + :argument "-n" + :reader 'transient-read-number-N+) + +(define-infix-argument magit:--author () + :description "Limit to author" + :class 'transient-option + :key "-A" + :argument "--author=" + :reader 'magit-transient-read-person) + +(define-infix-argument magit-log:--*-order () + :description "Order commits by" + :class 'transient-switches + :key "-o" + :argument-format "--%s-order" + :argument-regexp "\\(--\\(topo\\|author-date\\|date\\)-order\\)" + :choices '("topo" "author-date" "date")) + +(define-infix-argument magit-log:--grep () + :description "Search messages" + :class 'transient-option + :key "-F" + :argument "--grep=") + +(define-infix-argument magit-log:-G () + :description "Search changes" + :class 'transient-option + :argument "-G") + +(define-infix-argument magit-log:-S () + :description "Search occurrences" + :class 'transient-option + :argument "-S") + +(define-infix-argument magit-log:-L () + :description "Trace line evolution" + :class 'transient-option + :argument "-L" + :reader 'magit-read-file-trace) + +(defun magit-read-file-trace (&rest _ignored) + (let ((file (magit-read-file-from-rev "HEAD" "File")) + (trace (magit-read-string "Trace"))) + (concat trace ":" file))) + +;;;; Setup Commands + +(defvar magit-log-read-revs-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map crm-local-completion-map) + (define-key map "\s" 'self-insert-command) + map)) + +(defun magit-log-read-revs (&optional use-current) + (or (and use-current (--when-let (magit-get-current-branch) (list it))) + (let ((collection (magit-list-refnames nil t))) + (split-string + (magit-completing-read-multiple "Log rev,s" collection + "\\(\\.\\.\\.?\\|[, ]\\)" + (or (magit-branch-or-commit-at-point) + (unless use-current + (magit-get-previous-branch))) + 'magit-revision-history + magit-log-read-revs-map) + "[, ]" t)))) + +(defun magit-log-read-pattern () + "Read a string from the user to use as --branches= pattern." + (magit-read-string "Type a pattern to pass to --branches")) + +;;;###autoload +(defun magit-log-current (revs &optional args files) + "Show log for the current branch. +When `HEAD' is detached or with a prefix argument show log for +one or more revs read from the minibuffer." + (interactive (cons (magit-log-read-revs t) + (magit-log-arguments))) + (magit-log-setup-buffer revs args files)) + +;;;###autoload +(defun magit-log-other (revs &optional args files) + "Show log for one or more revs read from the minibuffer. +The user can input any revision or revisions separated by a +space, or even ranges, but only branches and tags, and a +representation of the commit at point, are available as +completion candidates." + (interactive (cons (magit-log-read-revs) + (magit-log-arguments))) + (magit-log-setup-buffer revs args files)) + +;;;###autoload +(defun magit-log-head (&optional args files) + "Show log for `HEAD'." + (interactive (magit-log-arguments)) + (magit-log-setup-buffer (list "HEAD") args files)) + +;;;###autoload +(defun magit-log-branches (&optional args files) + "Show log for all local branches and `HEAD'." + (interactive (magit-log-arguments)) + (magit-log-setup-buffer (if (magit-get-current-branch) + (list "--branches") + (list "HEAD" "--branches")) + args files)) + +;;;###autoload +(defun magit-log-matching-branches (pattern &optional args files) + "Show log for all branches matching PATTERN and `HEAD'." + (interactive (cons (magit-log-read-pattern) (magit-log-arguments))) + (magit-log-setup-buffer + (list "HEAD" (format "--branches=%s" pattern)) + args files)) + +;;;###autoload +(defun magit-log-all-branches (&optional args files) + "Show log for all local and remote branches and `HEAD'." + (interactive (magit-log-arguments)) + (magit-log-setup-buffer (if (magit-get-current-branch) + (list "--branches" "--remotes") + (list "HEAD" "--branches" "--remotes")) + args files)) + +;;;###autoload +(defun magit-log-all (&optional args files) + "Show log for all references and `HEAD'." + (interactive (magit-log-arguments)) + (magit-log-setup-buffer (if (magit-get-current-branch) + (list "--all") + (list "HEAD" "--all")) + args files)) + +;;;###autoload +(defun magit-log-buffer-file (&optional follow beg end) + "Show log for the blob or file visited in the current buffer. +With a prefix argument or when `--follow' is an active log +argument, then follow renames. When the region is active, +restrict the log to the lines that the region touches." + (interactive + (cons current-prefix-arg + (and (region-active-p) + (magit-file-relative-name) + (save-restriction + (widen) + (list (line-number-at-pos (region-beginning)) + (line-number-at-pos + (let ((end (region-end))) + (if (char-after end) + end + ;; Ensure that we don't get the line number + ;; of a trailing newline. + (1- end))))))))) + (require 'magit) + (if-let ((file (magit-file-relative-name))) + (magit-log-setup-buffer + (list (or magit-buffer-refname + (magit-get-current-branch) + "HEAD")) + (let ((args (car (magit-log-arguments)))) + (when (and follow (not (member "--follow" args))) + (push "--follow" args)) + (when (and (file-regular-p + (expand-file-name file (magit-toplevel))) + beg end) + (setq args (cons (format "-L%s,%s:%s" beg end file) + (cl-delete "-L" args :test + 'string-prefix-p))) + (setq file nil)) + args) + (and file (list file)) + magit-log-buffer-file-locked) + (user-error "Buffer isn't visiting a file"))) + +;;;###autoload +(defun magit-log-trace-definition (file fn rev) + "Show log for the definition at point." + (interactive (list (or (magit-file-relative-name) + (user-error "Buffer isn't visiting a file")) + (funcall magit-log-trace-definition-function) + (or magit-buffer-refname + (magit-get-current-branch) + "HEAD"))) + (require 'magit) + (magit-log-setup-buffer + (list rev) + (cons (format "-L:%s%s:%s" + (regexp-quote fn) + (if (derived-mode-p 'lisp-mode 'emacs-lisp-mode) + ;; Git doesn't treat "-" the same way as + ;; "_", leading to false-positives such as + ;; "foo-suffix" being considered a match + ;; for "foo". Wing it. + "\\( \\|$\\)" + ;; We could use "\\b" here, but since Git + ;; already does something equivalent, that + ;; isn't necessary. + "") + file) + (cl-delete "-L" (car (magit-log-arguments)) + :test 'string-prefix-p)) + nil magit-log-buffer-file-locked)) + +(defun magit-diff-trace-definition () + "Show log for the definition at point in a diff." + (interactive) + (pcase-let ((`(,buf ,pos) (magit-diff-visit-file--noselect))) + (magit--with-temp-position buf pos + (call-interactively #'magit-log-trace-definition)))) + +;;;###autoload +(defun magit-log-merged (commit branch &optional args files) + "Show log for the merge of COMMIT into BRANCH. + +More precisely, find merge commit M that brought COMMIT into +BRANCH, and show the log of the range \"M^1..M\". If COMMIT is +directly on BRANCH, then show approximately twenty surrounding +commits instead. + +This command requires git-when-merged, which is available from +https://github.com/mhagger/git-when-merged." + (interactive + (append (let ((commit (magit-read-branch-or-commit "Commit"))) + (list commit + (magit-read-other-branch "Merged into" commit))) + (magit-log-arguments))) + (unless (executable-find "git-when-merged") + (user-error "This command requires git-when-merged (%s)" + "https://github.com/mhagger/git-when-merged")) + (let (exit m) + (with-temp-buffer + (save-excursion + (setq exit (magit-process-file + magit-git-executable nil t nil + "when-merged" "-c" + "--abbrev" (number-to-string (magit-abbrev-length)) + commit branch))) + (setq m (buffer-substring-no-properties (point) (line-end-position)))) + (if (zerop exit) + (magit-log-setup-buffer (list (format "%s^1..%s" m m)) + args files nil commit) + (setq m (string-trim-left (substring m (string-match " " m)))) + (if (equal m "Commit is directly on this branch.") + (let* ((from (concat commit "~10")) + (to (- (car (magit-rev-diff-count branch commit)) 10)) + (to (if (<= to 0) + branch + (format "%s~%s" branch to)))) + (unless (magit-rev-verify-commit from) + (setq from (magit-git-string "rev-list" "--max-parents=0" + commit))) + (magit-log-setup-buffer (list (concat from ".." to)) + (cons "--first-parent" args) + files nil commit)) + (user-error "Could not find when %s was merged into %s: %s" + commit branch m))))) + +;;;; Limit Commands + +(defun magit-log-toggle-commit-limit () + "Toggle the number of commits the current log buffer is limited to. +If the number of commits is currently limited, then remove that +limit. Otherwise set it to 256." + (interactive) + (magit-log-set-commit-limit (lambda (&rest _) nil))) + +(defun magit-log-double-commit-limit () + "Double the number of commits the current log buffer is limited to." + (interactive) + (magit-log-set-commit-limit '*)) + +(defun magit-log-half-commit-limit () + "Half the number of commits the current log buffer is limited to." + (interactive) + (magit-log-set-commit-limit '/)) + +(defun magit-log-set-commit-limit (fn) + (let* ((val magit-buffer-log-args) + (arg (--first (string-match "^-n\\([0-9]+\\)?$" it) val)) + (num (and arg (string-to-number (match-string 1 arg)))) + (num (if num (funcall fn num 2) 256))) + (setq val (delete arg val)) + (setq magit-buffer-log-args + (if (and num (> num 0)) + (cons (format "-n%i" num) val) + val))) + (magit-refresh)) + +(defun magit-log-get-commit-limit () + (--when-let (--first (string-match "^-n\\([0-9]+\\)?$" it) + magit-buffer-log-args) + (string-to-number (match-string 1 it)))) + +;;;; Mode Commands + +(defun magit-log-bury-buffer (&optional arg) + "Bury the current buffer or the revision buffer in the same frame. +Like `magit-mode-bury-buffer' (which see) but with a negative +prefix argument instead bury the revision buffer, provided it +is displayed in the current frame." + (interactive "p") + (if (< arg 0) + (let* ((buf (magit-get-mode-buffer 'magit-revision-mode)) + (win (and buf (get-buffer-window buf (selected-frame))))) + (if win + (with-selected-window win + (with-current-buffer buf + (magit-mode-bury-buffer (> (abs arg) 1)))) + (user-error "No revision buffer in this frame"))) + (magit-mode-bury-buffer (> arg 1)))) + +;;;###autoload +(defun magit-log-move-to-parent (&optional n) + "Move to the Nth parent of the current commit." + (interactive "p") + (when (derived-mode-p 'magit-log-mode) + (when (magit-section-match 'commit) + (let* ((section (magit-current-section)) + (parent-rev (format "%s^%s" (oref section value) (or n 1)))) + (if-let ((parent-hash (magit-rev-parse "--short" parent-rev))) + (if-let ((parent (--first (equal (oref it value) + parent-hash) + (magit-section-siblings section 'next)))) + (magit-section-goto parent) + (user-error + (substitute-command-keys + (concat "Parent " parent-hash " not found. Try typing " + "\\[magit-log-double-commit-limit] first")))) + (user-error "Parent %s does not exist" parent-rev)))))) + +;;; Log Mode + +(defvar magit-log-disable-graph-hack-args + '("-G" "--grep" "--author") + "Arguments which disable the graph speedup hack.") + +(defvar magit-log-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magit-mode-map) + (define-key map "\C-c\C-b" 'magit-go-backward) + (define-key map "\C-c\C-f" 'magit-go-forward) + (define-key map "\C-c\C-n" 'magit-log-move-to-parent) + (define-key map "=" 'magit-log-toggle-commit-limit) + (define-key map "+" 'magit-log-double-commit-limit) + (define-key map "-" 'magit-log-half-commit-limit) + (define-key map "q" 'magit-log-bury-buffer) + map) + "Keymap for `magit-log-mode'.") + +(define-derived-mode magit-log-mode magit-mode "Magit Log" + "Mode for looking at Git log. + +This mode is documented in info node `(magit)Log Buffer'. + +\\<magit-mode-map>\ +Type \\[magit-refresh] to refresh the current buffer. +Type \\[magit-visit-thing] or \\[magit-diff-show-or-scroll-up] \ +to visit the commit at point. + +Type \\[magit-branch] to see available branch commands. +Type \\[magit-merge] to merge the branch or commit at point. +Type \\[magit-cherry-pick] to apply the commit at point. +Type \\[magit-reset] to reset `HEAD' to the commit at point. + +\\{magit-log-mode-map}" + :group 'magit-log + (hack-dir-local-variables-non-file-buffer) + (setq imenu-prev-index-position-function + 'magit-imenu--log-prev-index-position-function) + (setq imenu-extract-index-name-function + 'magit-imenu--log-extract-index-name-function)) + +(put 'magit-log-mode 'magit-log-default-arguments + '("--graph" "-n256" "--decorate")) + +(defun magit-log-setup-buffer (revs args files &optional locked focus) + (require 'magit) + (with-current-buffer + (magit-setup-buffer #'magit-log-mode locked + (magit-buffer-revisions revs) + (magit-buffer-log-args args) + (magit-buffer-log-files files)) + (when (if focus + (magit-log-goto-commit-section focus) + (magit-log-goto-same-commit)) + (magit-section-update-highlight)) + (current-buffer))) + +(defun magit-log-refresh-buffer () + (let ((revs magit-buffer-revisions) + (args magit-buffer-log-args) + (files magit-buffer-log-files)) + (magit-set-header-line-format + (funcall magit-log-header-line-function revs args files)) + (if (= (length files) 1) + (unless (magit-file-tracked-p (car files)) + (setq args (cons "--full-history" args))) + (setq args (remove "--follow" args))) + (when (--any-p (string-match-p + (concat "^" (regexp-opt magit-log-remove-graph-args)) it) + args) + (setq args (remove "--graph" args))) + (unless (member "--graph" args) + (setq args (remove "--color" args))) + (when-let ((limit (magit-log-get-commit-limit)) + (limit (* 2 limit)) ; increase odds for complete graph + (count (and (= (length revs) 1) + (> limit 1024) ; otherwise it's fast enough + (setq revs (car revs)) + (not (string-match-p "\\.\\." revs)) + (not (member revs '("--all" "--branches"))) + (-none-p (lambda (arg) + (--any-p (string-prefix-p it arg) + magit-log-disable-graph-hack-args)) + args) + (magit-git-string "rev-list" "--count" + "--first-parent" args revs)))) + (setq revs (if (< (string-to-number count) limit) + revs + (format "%s~%s..%s" revs limit revs)))) + (magit-insert-section (logbuf) + (magit-insert-log revs args files)))) + +(cl-defmethod magit-buffer-value (&context (major-mode magit-log-mode)) + (append magit-buffer-revisions + (if (and magit-buffer-revisions magit-buffer-log-files) + (cons "--" magit-buffer-log-files) + magit-buffer-log-files))) + +(defun magit-log-header-line-arguments (revs args files) + "Return string describing some of the used arguments." + (mapconcat (lambda (arg) + (if (string-match-p " " arg) + (prin1 arg) + arg)) + `("git" "log" ,@args ,@revs "--" ,@files) + " ")) + +(defun magit-log-header-line-sentence (revs args files) + "Return string containing all arguments." + (concat "Commits in " + (mapconcat #'identity revs " ") + (and (member "--reverse" args) + " in reverse") + (and files (concat " touching " + (mapconcat 'identity files " "))) + (--some (and (string-prefix-p "-L" it) + (concat " " it)) + args))) + +(defun magit-insert-log (revs &optional args files) + "Insert a log section. +Do not add this to a hook variable." + (let ((magit-git-global-arguments + (remove "--literal-pathspecs" magit-git-global-arguments))) + (magit-git-wash (apply-partially #'magit-log-wash-log 'log) + "log" + (format "--format=%s%%h%%x00%s%%x00%s%%x00%%aN%%x00%%at%%x00%%s%s" + (if (and (member "--left-right" args) + (not (member "--graph" args))) + "%m " + "") + (if (member "--decorate" args) "%D" "") + (if (member "--show-signature" args) + (progn (setq args (remove "--show-signature" args)) "%G?") + "") + (if (member "++header" args) + (if (member "--graph" (setq args (remove "++header" args))) + (concat "\n" magit-log-revision-headers-format "\n") + (concat "\n" magit-log-revision-headers-format "\n")) + "")) + (progn + (--when-let (--first (string-match "^\\+\\+order=\\(.+\\)$" it) args) + (setq args (cons (format "--%s-order" (match-string 1 it)) + (remove it args)))) + (when (member "--decorate" args) + (setq args (cons "--decorate=full" (remove "--decorate" args)))) + (when (member "--reverse" args) + (setq args (remove "--graph" args))) + args) + "--use-mailmap" "--no-prefix" revs "--" files))) + +(defvar magit-commit-section-map + (let ((map (make-sparse-keymap))) + (define-key map [remap magit-visit-thing] 'magit-show-commit) + (define-key map "a" 'magit-cherry-apply) + map) + "Keymap for `commit' sections.") + +(defvar magit-module-commit-section-map + (let ((map (make-sparse-keymap))) + (define-key map [remap magit-visit-thing] 'magit-show-commit) + map) + "Keymap for `module-commit' sections.") + +(defconst magit-log-heading-re + (concat "^" + "\\(?4:[-_/|\\*o<>. ]*\\)" ; graph + "\\(?1:[0-9a-fA-F]+\\)?\0" ; sha1 + "\\(?3:[^\0\n]+\\)?\0" ; refs + "\\(?7:[BGUXYREN]\\)?\0" ; gpg + "\\(?5:[^\0\n]*\\)\0" ; author + ;; Note: Date is optional because, prior to Git v2.19.0, + ;; `git rebase -i --root` corrupts the root's author date. + "\\(?6:[^\0\n]*\\)\0" ; date + "\\(?2:.*\\)$")) ; msg + +(defconst magit-log-cherry-re + (concat "^" + "\\(?8:[-+]\\) " ; cherry + "\\(?1:[0-9a-fA-F]+\\) " ; sha1 + "\\(?2:.*\\)$")) ; msg + +(defconst magit-log-module-re + (concat "^" + "\\(?:\\(?11:[<>]\\) \\)?" ; side + "\\(?1:[0-9a-fA-F]+\\) " ; sha1 + "\\(?2:.*\\)$")) ; msg + +(defconst magit-log-bisect-vis-re + (concat "^" + "\\(?4:[-_/|\\*o<>. ]*\\)" ; graph + "\\(?1:[0-9a-fA-F]+\\)?\0" ; sha1 + "\\(?3:[^\0\n]+\\)?\0" ; refs + "\\(?2:.*\\)$")) ; msg + +(defconst magit-log-bisect-log-re + (concat "^# " + "\\(?3:bad:\\|skip:\\|good:\\) " ; "refs" + "\\[\\(?1:[^]\n]+\\)\\] " ; sha1 + "\\(?2:.*\\)$")) ; msg + +(defconst magit-log-reflog-re + (concat "^" + "\\(?1:[^\0\n]+\\)\0" ; sha1 + "\\(?5:[^\0\n]*\\)\0" ; author + "\\(?:\\(?:[^@\n]+@{\\(?6:[^}\n]+\\)}\0" ; date + "\\(?10:merge \\|autosave \\|restart \\|[^:\n]+: \\)?" ; refsub + "\\(?2:.*\\)?\\)\\|\0\\)$")) ; msg + +(defconst magit-reflog-subject-re + (concat "\\(?1:[^ ]+\\) ?" ; command + "\\(?2:\\(?: ?-[^ ]+\\)+\\)?" ; option + "\\(?: ?(\\(?3:[^)]+\\))\\)?")) ; type + +(defconst magit-log-stash-re + (concat "^" + "\\(?1:[^\0\n]+\\)\0" ; "sha1" + "\\(?5:[^\0\n]*\\)\0" ; author + "\\(?6:[^\0\n]+\\)\0" ; date + "\\(?2:.*\\)$")) ; msg + +(defvar magit-log-count nil) + +(defvar magit-log-format-message-function 'magit-log-propertize-keywords) + +(defun magit-log-wash-log (style args) + (setq args (-flatten args)) + (when (and (member "--graph" args) + (member "--color" args)) + (let ((ansi-color-apply-face-function + (lambda (beg end face) + (put-text-property beg end 'font-lock-face + (or face 'magit-log-graph))))) + (ansi-color-apply-on-region (point-min) (point-max)))) + (when (eq style 'cherry) + (reverse-region (point-min) (point-max))) + (let ((magit-log-count 0)) + (magit-wash-sequence (apply-partially 'magit-log-wash-rev style + (magit-abbrev-length))) + (if (derived-mode-p 'magit-log-mode 'magit-reflog-mode) + (when (eq magit-log-count (magit-log-get-commit-limit)) + (magit-insert-section (longer) + (insert-text-button + (substitute-command-keys + (format "Type \\<%s>\\[%s] to show more history" + 'magit-log-mode-map + 'magit-log-double-commit-limit)) + 'action (lambda (_button) + (magit-log-double-commit-limit)) + 'follow-link t + 'mouse-face 'magit-section-highlight))) + (insert ?\n)))) + +(cl-defun magit-log-wash-rev (style abbrev) + (when (derived-mode-p 'magit-log-mode 'magit-reflog-mode) + (cl-incf magit-log-count)) + (looking-at (pcase style + (`log magit-log-heading-re) + (`cherry magit-log-cherry-re) + (`module magit-log-module-re) + (`reflog magit-log-reflog-re) + (`stash magit-log-stash-re) + (`bisect-vis magit-log-bisect-vis-re) + (`bisect-log magit-log-bisect-log-re))) + (magit-bind-match-strings + (hash msg refs graph author date gpg cherry _ refsub side) nil + (setq msg (substring-no-properties msg)) + (when refs + (setq refs (substring-no-properties refs))) + (let ((align (or (eq style 'cherry) + (not (member "--stat" magit-buffer-log-args)))) + (non-graph-re (if (eq style 'bisect-vis) + magit-log-bisect-vis-re + magit-log-heading-re))) + (magit-delete-line) + ;; If the reflog entries have been pruned, the output of `git + ;; reflog show' includes a partial line that refers to the hash + ;; of the youngest expired reflog entry. + (when (and (eq style 'reflog) (not date)) + (cl-return-from magit-log-wash-rev t)) + (magit-insert-section section (commit hash) + (pcase style + (`stash (oset section type 'stash)) + (`module (oset section type 'module-commit)) + (`bisect-log (setq hash (magit-rev-parse "--short" hash)))) + (when cherry + (when (and (derived-mode-p 'magit-refs-mode) + magit-refs-show-commit-count) + (insert (make-string (1- magit-refs-focus-column-width) ?\s))) + (insert (propertize cherry 'font-lock-face + (if (string= cherry "-") + 'magit-cherry-equivalent + 'magit-cherry-unmatched))) + (insert ?\s)) + (when side + (insert (propertize side 'font-lock-face + (if (string= side "<") + 'magit-cherry-equivalent + 'magit-cherry-unmatched))) + (insert ?\s)) + (when align + (insert (propertize hash 'font-lock-face 'magit-hash) ?\s)) + (when graph + (insert graph)) + (unless align + (insert (propertize hash 'font-lock-face 'magit-hash) ?\s)) + (when (and refs (not magit-log-show-refname-after-summary)) + (insert (magit-format-ref-labels refs) ?\s)) + (when (eq style 'reflog) + (insert (format "%-2s " (1- magit-log-count))) + (when refsub + (insert (magit-reflog-format-subject + (substring refsub 0 (if (string-match-p ":" refsub) -2 -1)))))) + (when msg + (when gpg + (setq msg (propertize msg 'font-lock-face + (pcase (aref gpg 0) + (?G 'magit-signature-good) + (?B 'magit-signature-bad) + (?U 'magit-signature-untrusted) + (?X 'magit-signature-expired) + (?Y 'magit-signature-expired-key) + (?R 'magit-signature-revoked) + (?E 'magit-signature-error))))) + (insert (funcall magit-log-format-message-function hash msg))) + (when (and refs magit-log-show-refname-after-summary) + (insert ?\s) + (insert (magit-format-ref-labels refs))) + (insert ?\n) + (when (memq style '(log reflog stash)) + (goto-char (line-beginning-position)) + (when (and refsub + (string-match "\\`\\([^ ]\\) \\+\\(..\\)\\(..\\)" date)) + (setq date (+ (string-to-number (match-string 1 date)) + (* (string-to-number (match-string 2 date)) 60 60) + (* (string-to-number (match-string 3 date)) 60)))) + (save-excursion + (backward-char) + (magit-log-format-margin hash author date))) + (when (and (eq style 'cherry) + (magit-buffer-margin-p)) + (save-excursion + (backward-char) + (apply #'magit-log-format-margin hash + (split-string (magit-rev-format "%aN%x00%ct" hash) "\0")))) + (when (and graph + (not (eobp)) + (not (looking-at non-graph-re))) + (when (looking-at "") + (magit-insert-heading) + (delete-char 1) + (magit-insert-section (commit-header) + (forward-line) + (magit-insert-heading) + (re-search-forward "") + (backward-delete-char 1) + (forward-char) + (insert ?\n)) + (delete-char 1)) + (if (looking-at "^\\(---\\|\n\s\\|\ndiff\\)") + (let ((limit (save-excursion + (and (re-search-forward non-graph-re nil t) + (match-beginning 0))))) + (unless (oref magit-insert-section--current content) + (magit-insert-heading)) + (delete-char (if (looking-at "\n") 1 4)) + (magit-diff-wash-diffs (list "--stat") limit)) + (when align + (setq align (make-string (1+ abbrev) ? ))) + (when (and (not (eobp)) (not (looking-at non-graph-re))) + (when align + (setq align (make-string (1+ abbrev) ? ))) + (while (and (not (eobp)) (not (looking-at non-graph-re))) + (when align + (save-excursion (insert align))) + (magit-make-margin-overlay) + (forward-line)) + ;; When `--format' is used and its value isn't one of the + ;; predefined formats, then `git-log' does not insert a + ;; separator line. + (save-excursion + (forward-line -1) + (looking-at "[-_/|\\*o<>. ]*")) + (setq graph (match-string 0)) + (unless (string-match-p "[/\\]" graph) + (insert graph ?\n)))))))) + t) + +(defun magit-log-propertize-keywords (_rev msg) + (let ((start 0)) + (when (string-match "^\\(squash\\|fixup\\)! " msg start) + (setq start (match-end 0)) + (magit--put-face (match-beginning 0) (match-end 0) + 'magit-keyword-squash msg)) + (while (string-match "\\[[^[]*\\]" msg start) + (setq start (match-end 0)) + (when magit-log-highlight-keywords + (magit--put-face (match-beginning 0) (match-end 0) + 'magit-keyword msg)))) + msg) + +(defun magit-log-maybe-show-more-commits (section) + "When point is at the end of a log buffer, insert more commits. + +Log buffers end with a button \"Type + to show more history\". +When the use of a section movement command puts point on that +button, then automatically show more commits, without the user +having to press \"+\". + +This function is called by `magit-section-movement-hook' and +exists mostly for backward compatibility reasons." + (when (and (eq (oref section type) 'longer) + magit-log-auto-more) + (magit-log-double-commit-limit) + (forward-line -1) + (magit-section-forward))) + +(defvar magit--update-revision-buffer nil) + +(defun magit-log-maybe-update-revision-buffer (&optional _) + "When moving in a log or cherry buffer, update the revision buffer. +If there is no revision buffer in the same frame, then do nothing." + (when (derived-mode-p 'magit-log-mode 'magit-cherry-mode 'magit-reflog-mode) + (magit--maybe-update-revision-buffer))) + +(defun magit--maybe-update-revision-buffer () + (when-let ((commit (magit-section-value-if 'commit)) + (buffer (magit-get-mode-buffer 'magit-revision-mode nil t))) + (if magit--update-revision-buffer + (setq magit--update-revision-buffer (list commit buffer)) + (setq magit--update-revision-buffer (list commit buffer)) + (run-with-idle-timer + magit-update-other-window-delay nil + (let ((args (with-current-buffer buffer + (let ((magit-direct-use-buffer-arguments 'selected)) + (magit-show-commit--arguments))))) + (lambda () + (pcase-let ((`(,rev ,buf) magit--update-revision-buffer)) + (setq magit--update-revision-buffer nil) + (when (buffer-live-p buf) + (let ((magit-display-buffer-noselect t)) + (apply #'magit-show-commit rev args)))) + (setq magit--update-revision-buffer nil))))))) + +(defvar magit--update-blob-buffer nil) + +(defun magit-log-maybe-update-blob-buffer (&optional _) + "When moving in a log or cherry buffer, update the blob buffer. +If there is no blob buffer in the same frame, then do nothing." + (when (derived-mode-p 'magit-log-mode 'magit-cherry-mode 'magit-reflog-mode) + (magit--maybe-update-blob-buffer))) + +(defun magit--maybe-update-blob-buffer () + (when-let ((commit (magit-section-value-if 'commit)) + (buffer (--first (with-current-buffer it + (eq revert-buffer-function + 'magit-revert-rev-file-buffer)) + (mapcar #'window-buffer (window-list))))) + (if magit--update-blob-buffer + (setq magit--update-blob-buffer (list commit buffer)) + (setq magit--update-blob-buffer (list commit buffer)) + (run-with-idle-timer + magit-update-other-window-delay nil + (lambda () + (pcase-let ((`(,rev ,buf) magit--update-blob-buffer)) + (setq magit--update-blob-buffer nil) + (when (buffer-live-p buf) + (with-selected-window (get-buffer-window buf) + (with-current-buffer buf + (save-excursion + (magit-blob-visit (list (magit-rev-parse rev) + (magit-file-relative-name + magit-buffer-file-name)) + (line-number-at-pos)))))))))))) + +(defun magit-log-goto-commit-section (rev) + (let ((abbrev (magit-rev-format "%h" rev))) + (when-let ((section (--first (equal (oref it value) abbrev) + (oref magit-root-section children)))) + (goto-char (oref section start))))) + +(defun magit-log-goto-same-commit () + (when (and magit-previous-section + (magit-section-match '(commit branch) + magit-previous-section)) + (magit-log-goto-commit-section (oref magit-previous-section value)))) + +;;; Log Margin + +(defvar-local magit-log-margin-show-shortstat nil) + +(defun magit-toggle-log-margin-style () + "Toggle between the regular and the shortstat margin style. +The shortstat style is experimental and rather slow." + (interactive) + (setq magit-log-margin-show-shortstat + (not magit-log-margin-show-shortstat)) + (magit-set-buffer-margin nil t)) + +(defun magit-log-format-margin (rev author date) + (when (magit-margin-option) + (if magit-log-margin-show-shortstat + (magit-log-format-shortstat-margin rev) + (magit-log-format-author-margin author date)))) + +(defun magit-log-format-author-margin (author date &optional previous-line) + (pcase-let ((`(,_ ,style ,width ,details ,details-width) + (or magit-buffer-margin + (symbol-value (magit-margin-option))))) + (magit-make-margin-overlay + (concat (and details + (concat (magit--propertize-face + (truncate-string-to-width + (or author "") + details-width + nil ?\s (make-string 1 magit-ellipsis)) + 'magit-log-author) + " ")) + (magit--propertize-face + (if (stringp style) + (format-time-string + style + (seconds-to-time (string-to-number date))) + (pcase-let* ((abbr (eq style 'age-abbreviated)) + (`(,cnt ,unit) (magit--age date abbr))) + (format (format (if abbr "%%2i%%-%ic" "%%2i %%-%is") + (- width (if details (1+ details-width) 0))) + cnt unit))) + 'magit-log-date)) + previous-line))) + +(defun magit-log-format-shortstat-margin (rev) + (magit-make-margin-overlay + (if-let ((line (and rev (magit-git-string + "show" "--format=" "--shortstat" rev)))) + (if (string-match "\ +\\([0-9]+\\) files? changed, \ +\\(?:\\([0-9]+\\) insertions?(\\+)\\)?\ +\\(?:\\(?:, \\)?\\([0-9]+\\) deletions?(-)\\)?\\'" line) + (magit-bind-match-strings (files add del) line + (format + "%5s %5s%4s" + (if add + (magit--propertize-face (format "%s+" add) + 'magit-diffstat-added) + "") + (if del + (magit--propertize-face (format "%s-" del) + 'magit-diffstat-removed) + "") + files)) + "") + ""))) + +(defun magit-log-margin-width (style details details-width) + (if magit-log-margin-show-shortstat + 16 + (+ (if details (1+ details-width) 0) + (if (stringp style) + (length (format-time-string style)) + (+ 2 ; two digits + 1 ; trailing space + (if (eq style 'age-abbreviated) + 1 ; single character + (+ 1 ; gap after digits + (apply #'max (--map (max (length (nth 1 it)) + (length (nth 2 it))) + magit--age-spec))))))))) + +;;; Select Mode + +(defvar magit-log-select-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magit-log-mode-map) + (define-key map "\C-c\C-b" 'undefined) + (define-key map "\C-c\C-f" 'undefined) + (define-key map "." 'magit-log-select-pick) + (define-key map "e" 'magit-log-select-pick) + (define-key map "\C-c\C-c" 'magit-log-select-pick) + (define-key map "q" 'magit-log-select-quit) + (define-key map "\C-c\C-k" 'magit-log-select-quit) + map) + "Keymap for `magit-log-select-mode'.") + +(put 'magit-log-select-pick :advertised-binding [?\C-c ?\C-c]) +(put 'magit-log-select-quit :advertised-binding [?\C-c ?\C-k]) + +(define-derived-mode magit-log-select-mode magit-log-mode "Magit Select" + "Mode for selecting a commit from history. + +This mode is documented in info node `(magit)Select from Log'. + +\\<magit-mode-map>\ +Type \\[magit-refresh] to refresh the current buffer. +Type \\[magit-visit-thing] or \\[magit-diff-show-or-scroll-up] \ +to visit the commit at point. + +\\<magit-log-select-mode-map>\ +Type \\[magit-log-select-pick] to select the commit at point. +Type \\[magit-log-select-quit] to abort without selecting a commit." + :group 'magit-log + (hack-dir-local-variables-non-file-buffer)) + +(put 'magit-log-select-mode 'magit-log-default-arguments + '("--graph" "-n256" "--decorate")) + +(defun magit-log-select-setup-buffer (revs args) + (magit-setup-buffer #'magit-log-select-mode nil + (magit-buffer-revisions revs) + (magit-buffer-log-args args))) + +(defun magit-log-select-refresh-buffer () + (magit-insert-section (logbuf) + (magit-insert-log magit-buffer-revisions + magit-buffer-log-args))) + +(cl-defmethod magit-buffer-value (&context (major-mode magit-log-select-mode)) + magit-buffer-revisions) + +(defvar-local magit-log-select-pick-function nil) +(defvar-local magit-log-select-quit-function nil) + +(defun magit-log-select (pick &optional msg quit branch args initial) + (declare (indent defun)) + (unless initial + (setq initial (magit-commit-at-point))) + (magit-log-select-setup-buffer + (or branch (magit-get-current-branch) "HEAD") + (append args + (car (magit-log--get-value 'magit-log-select-mode + magit-direct-use-buffer-arguments)))) + (when initial + (magit-log-goto-commit-section initial)) + (setq magit-log-select-pick-function pick) + (setq magit-log-select-quit-function quit) + (when magit-log-select-show-usage + (let ((pick (propertize (substitute-command-keys + "\\[magit-log-select-pick]") + 'font-lock-face + 'magit-header-line-key)) + (quit (propertize (substitute-command-keys + "\\[magit-log-select-quit]") + 'font-lock-face + 'magit-header-line-key))) + (setq msg (format-spec + (if msg + (if (string-suffix-p "," msg) + (concat msg " or %q to abort") + msg) + "Type %p to select commit at point, or %q to abort") + `((?p . ,pick) + (?q . ,quit))))) + (magit--add-face-text-property + 0 (length msg) 'magit-header-line-log-select t msg) + (when (memq magit-log-select-show-usage '(both header-line)) + (magit-set-header-line-format msg)) + (when (memq magit-log-select-show-usage '(both echo-area)) + (message "%s" (substring-no-properties msg))))) + +(defun magit-log-select-pick () + "Select the commit at point and act on it. +Call `magit-log-select-pick-function' with the selected +commit as argument." + (interactive) + (let ((fun magit-log-select-pick-function) + (rev (magit-commit-at-point))) + (magit-mode-bury-buffer 'kill) + (funcall fun rev))) + +(defun magit-log-select-quit () + "Abort selecting a commit, don't act on any commit." + (interactive) + (magit-mode-bury-buffer 'kill) + (when magit-log-select-quit-function + (funcall magit-log-select-quit-function))) + +;;; Cherry Mode + +(defvar magit-cherry-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map magit-mode-map) + (define-key map "q" 'magit-log-bury-buffer) + (define-key map "L" 'magit-margin-settings) + map) + "Keymap for `magit-cherry-mode'.") + +(define-derived-mode magit-cherry-mode magit-mode "Magit Cherry" + "Mode for looking at commits not merged upstream. + +\\<magit-mode-map>\ +Type \\[magit-refresh] to refresh the current buffer. +Type \\[magit-visit-thing] or \\[magit-diff-show-or-scroll-up] \ +to visit the commit at point. + +Type \\[magit-cherry-pick] to apply the commit at point. + +\\{magit-cherry-mode-map}" + :group 'magit-log + (hack-dir-local-variables-non-file-buffer) + (setq imenu-create-index-function + 'magit-imenu--cherry-create-index-function)) + +(defun magit-cherry-setup-buffer (head upstream) + (magit-setup-buffer #'magit-cherry-mode nil + (magit-buffer-refname head) + (magit-buffer-upstream upstream) + (magit-buffer-range (concat upstream ".." head)))) + +(defun magit-cherry-refresh-buffer () + (magit-insert-section (cherry) + (magit-run-section-hook 'magit-cherry-sections-hook))) + +(cl-defmethod magit-buffer-value (&context (major-mode magit-cherry-mode)) + magit-buffer-range) + +;;;###autoload +(defun magit-cherry (head upstream) + "Show commits in a branch that are not merged in the upstream branch." + (interactive + (let ((head (magit-read-branch "Cherry head"))) + (list head (magit-read-other-branch "Cherry upstream" head + (magit-get-upstream-branch head))))) + (require 'magit) + (magit-cherry-setup-buffer head upstream)) + +(defun magit-insert-cherry-headers () + "Insert headers appropriate for `magit-cherry-mode' buffers." + (let ((branch (propertize magit-buffer-refname + 'font-lock-face 'magit-branch-local)) + (upstream (propertize magit-buffer-upstream 'font-lock-face + (if (magit-local-branch-p magit-buffer-upstream) + 'magit-branch-local + 'magit-branch-remote)))) + (magit-insert-head-branch-header branch) + (magit-insert-upstream-branch-header branch upstream "Upstream: ") + (insert ?\n))) + +(defun magit-insert-cherry-commits () + "Insert commit sections into a `magit-cherry-mode' buffer." + (magit-insert-section (cherries) + (magit-insert-heading "Cherry commits:") + (magit-git-wash (apply-partially 'magit-log-wash-log 'cherry) + "cherry" "-v" "--abbrev" + magit-buffer-upstream + magit-buffer-refname))) + +;;; Log Sections +;;;; Standard Log Sections + +(defvar magit-unpulled-section-map + (let ((map (make-sparse-keymap))) + (define-key map [remap magit-visit-thing] 'magit-diff-dwim) + map) + "Keymap for `unpulled' sections.") + +(magit-define-section-jumper magit-jump-to-unpulled-from-upstream + "Unpulled from @{upstream}" unpulled "..@{upstream}") + +(defun magit-insert-unpulled-from-upstream () + "Insert commits that haven't been pulled from the upstream yet." + (when-let ((upstream (magit-get-upstream-branch))) + (magit-insert-section (unpulled "..@{upstream}" t) + (magit-insert-heading + (format (propertize "Unpulled from %s." + 'font-lock-face 'magit-section-heading) + upstream)) + (magit-insert-log "..@{upstream}" magit-buffer-log-args) + (magit-log-insert-child-count)))) + +(magit-define-section-jumper magit-jump-to-unpulled-from-pushremote + "Unpulled from <push-remote>" unpulled + (concat ".." (magit-get-push-branch))) + +(defun magit-insert-unpulled-from-pushremote () + "Insert commits that haven't been pulled from the push-remote yet." + (--when-let (magit-get-push-branch) + (unless (and (equal (magit-rev-name it) + (magit-rev-name "@{upstream}")) + (or (memq 'magit-insert-unpulled-from-upstream + magit-status-sections-hook) + (memq 'magit-insert-unpulled-from-upstream-or-recent + magit-status-sections-hook))) + (magit-insert-section (unpulled (concat ".." it) t) + (magit-insert-heading + (format (propertize "Unpulled from %s." + 'font-lock-face 'magit-section-heading) + (propertize it 'font-lock-face 'magit-branch-remote))) + (magit-insert-log (concat ".." it) magit-buffer-log-args) + (magit-log-insert-child-count))))) + +(defvar magit-unpushed-section-map + (let ((map (make-sparse-keymap))) + (define-key map [remap magit-visit-thing] 'magit-diff-dwim) + map) + "Keymap for `unpushed' sections.") + +(magit-define-section-jumper magit-jump-to-unpushed-to-upstream + "Unpushed to @{upstream}" unpushed "@{upstream}..") + +(defun magit-insert-unpushed-to-upstream-or-recent () + "Insert section showing unpushed or other recent commits. +If an upstream is configured for the current branch and it is +behind of the current branch, then show the commits that have +not yet been pushed into the upstream branch. If no upstream is +configured or if the upstream is not behind of the current branch, +then show the last `magit-log-section-commit-count' commits." + (let ((upstream (magit-get-upstream-branch))) + (if (or (not upstream) + (magit-rev-ancestor-p "HEAD" upstream)) + (magit-insert-recent-commits 'unpushed "@{upstream}..") + (magit-insert-unpushed-to-upstream)))) + +(defun magit-insert-unpushed-to-upstream () + "Insert commits that haven't been pushed to the upstream yet." + (when (magit-git-success "rev-parse" "@{upstream}") + (magit-insert-section (unpushed "@{upstream}..") + (magit-insert-heading + (format (propertize "Unmerged into %s." + 'font-lock-face 'magit-section-heading) + (magit-get-upstream-branch))) + (magit-insert-log "@{upstream}.." magit-buffer-log-args) + (magit-log-insert-child-count)))) + +(defun magit-insert-recent-commits (&optional type value) + "Insert section showing recent commits. +Show the last `magit-log-section-commit-count' commits." + (let* ((start (format "HEAD~%s" magit-log-section-commit-count)) + (range (and (magit-rev-verify start) + (concat start "..HEAD")))) + (magit-insert-section ((eval (or type 'recent)) + (or value range) + t) + (magit-insert-heading "Recent commits") + (magit-insert-log range + (cons (format "-n%d" magit-log-section-commit-count) + (--remove (string-prefix-p "-n" it) + magit-buffer-log-args)))))) + +(magit-define-section-jumper magit-jump-to-unpushed-to-pushremote + "Unpushed to <push-remote>" unpushed + (concat (magit-get-push-branch) "..")) + +(defun magit-insert-unpushed-to-pushremote () + "Insert commits that haven't been pushed to the push-remote yet." + (--when-let (magit-get-push-branch) + (unless (and (equal (magit-rev-name it) + (magit-rev-name "@{upstream}")) + (or (memq 'magit-insert-unpushed-to-upstream + magit-status-sections-hook) + (memq 'magit-insert-unpushed-to-upstream-or-recent + magit-status-sections-hook))) + (magit-insert-section (unpushed (concat it "..") t) + (magit-insert-heading + (format (propertize "Unpushed to %s." + 'font-lock-face 'magit-section-heading) + (propertize it 'font-lock-face 'magit-branch-remote))) + (magit-insert-log (concat it "..") magit-buffer-log-args) + (magit-log-insert-child-count))))) + +(defun magit-log-insert-child-count () + (when magit-section-show-child-count + (let ((count (length (oref magit-insert-section--current children)))) + (when (> count 0) + (when (= count (magit-log-get-commit-limit)) + (setq count (format "%s+" count))) + (save-excursion + (goto-char (- (oref magit-insert-section--current content) 2)) + (insert (format " (%s)" count)) + (delete-char 1)))))) + +;;;; Auxiliary Log Sections + +(defun magit-insert-unpulled-cherries () + "Insert section showing unpulled commits. +Like `magit-insert-unpulled-from-upstream' but prefix each commit +which has not been applied yet (i.e. a commit with a patch-id +not shared with any local commit) with \"+\", and all others with +\"-\"." + (when (magit-git-success "rev-parse" "@{upstream}") + (magit-insert-section (unpulled "..@{upstream}") + (magit-insert-heading "Unpulled commits:") + (magit-git-wash (apply-partially 'magit-log-wash-log 'cherry) + "cherry" "-v" (magit-abbrev-arg) + (magit-get-current-branch) "@{upstream}")))) + +(defun magit-insert-unpushed-cherries () + "Insert section showing unpushed commits. +Like `magit-insert-unpushed-to-upstream' but prefix each commit +which has not been applied to upstream yet (i.e. a commit with +a patch-id not shared with any upstream commit) with \"+\", and +all others with \"-\"." + (when (magit-git-success "rev-parse" "@{upstream}") + (magit-insert-section (unpushed "@{upstream}..") + (magit-insert-heading "Unpushed commits:") + (magit-git-wash (apply-partially 'magit-log-wash-log 'cherry) + "cherry" "-v" (magit-abbrev-arg) "@{upstream}")))) + +;;; _ +(provide 'magit-log) +;;; magit-log.el ends here |