diff options
Diffstat (limited to '')
-rw-r--r-- | emacs.d/lisp/magit/magit.el | 5963 |
1 files changed, 4214 insertions, 1749 deletions
diff --git a/emacs.d/lisp/magit/magit.el b/emacs.d/lisp/magit/magit.el index bfbfe9b..7fdddf0 100644 --- a/emacs.d/lisp/magit/magit.el +++ b/emacs.d/lisp/magit/magit.el @@ -1,11 +1,54 @@ -;;; Magit -- control Git from Emacs. - -;; Copyright (C) 2008, 2009 Marius Vollmer -;; Copyright (C) 2008 Linh Dang -;; Copyright (C) 2008 Alex Ott -;; Copyright (C) 2008 Marcin Bachry -;; Copyright (C) 2009 Alexey Voinov -;; Copyright (C) 2009 John Wiegley +;;; magit.el -- control Git from Emacs. + +;; Copyright (C) 2010 Aaron Culich. +;; Copyright (C) 2010 Alan Falloon. +;; Copyright (C) 2008, 2010 Alex Ott. +;; Copyright (C) 2008, 2009, 2010 Alexey Voinov. +;; Copyright (C) 2010 Ben Walton. +;; Copyright (C) 2010 Chris Bernard. +;; Copyright (C) 2010 Christian Kluge. +;; Copyright (C) 2008 Daniel Farina. +;; Copyright (C) 2010 David Abrahams. +;; Copyright (C) 2009 David Wallin. +;; Copyright (C) 2009, 2010 Hannu Koivisto. +;; Copyright (C) 2009 Ian Eure. +;; Copyright (C) 2009 Jesse Alama. +;; Copyright (C) 2009 John Wiegley. +;; Copyright (C) 2010 Leo. +;; Copyright (C) 2008, 2009 Marcin Bachry. +;; Copyright (C) 2008, 2009 Marius Vollmer. +;; Copyright (C) 2010 Mark Hepburn. +;; Copyright (C) 2010 Moritz Bunkus. +;; Copyright (C) 2010 Nathan Weizenbaum. +;; Copyright (C) 2010 Oscar Fuentes. +;; Copyright (C) 2009 Pavel Holejsovsky. +;; Copyright (C) 2011 Peter J Weisberg +;; Copyright (C) 2009, 2010 Phil Jackson. +;; Copyright (C) 2010 Philip Weaver. +;; Copyright (C) 2010 Ramkumar Ramachandra. +;; Copyright (C) 2010 Remco van 't Veer. +;; Copyright (C) 2009 René Stadler. +;; Copyright (C) 2010 Robin Green. +;; Copyright (C) 2010 Roger Crew. +;; Copyright (C) 2009, 2010, 2011 Rémi Vanicat. +;; Copyright (C) 2010 Sean Bryant. +;; Copyright (C) 2009, 2011 Steve Purcell. +;; Copyright (C) 2010 Timo Juhani Lindfors. +;; Copyright (C) 2010, 2011 Yann Hodique. +;; Copyright (C) 2010 Ævar Arnfjörð Bjarmason. +;; Copyright (C) 2010 Óscar Fuentes. + +;; Original Author: Marius Vollmer <marius.vollmer@nokia.com> +;; Former Maintainer: Phil Jackson <phil@shellarchive.co.uk> +;; Maintenance Group: https://github.com/organizations/magit/teams/53130 +;; Currently composed of: +;; - Phil Jackson +;; - Peter J Weisberg +;; - Yann Hodique +;; - Rémi Vanicat +;; Version: @GIT_DEV_VERSION@ +;; Keywords: tools + ;; ;; Magit is free software; you can redistribute it and/or modify it ;; under the terms of the GNU General Public License as published by @@ -20,7 +63,7 @@ ;; You should have received a copy of the GNU General Public License ;; along with Magit. If not, see <http://www.gnu.org/licenses/>. -;;; Commentary +;;; Commentary: ;; Invoking the magit-status function will show a buffer with the ;; status of the current git repository and its working tree. That @@ -34,33 +77,25 @@ ;; ;; See the Magit User Manual for more information. -;;; TODO +;;; Code: -;; For 0.8: -;; -;; - Fix display of unmerged files. -;; - Fix performance problems with large status buffers. -;; - Handle the case where remote and local branches have different names. -;; -;; Later: -;; -;; - Queuing of asynchronous commands. -;; - Good email integration. -;; - Showing tags. -;; - Visiting from staged hunks doesn't always work since the line -;; numbers don't refer to the working tree. Fix that somehow. -;; - Figure out how to discard staged changes for files that also have -;; unstaged changes. -;; - Get current defun from removed lines in a diff -;; - Amending commits other than HEAD. -;; - 'Subsetting', only looking at a subset of all files. - -(require 'cl) -(require 'parse-time) +(eval-when-compile (require 'cl)) (require 'log-edit) (require 'easymenu) (require 'diff-mode) +;; Silences byte-compiler warnings +(eval-when-compile (require 'view)) +(declare-function view-mode 'view) +(eval-when-compile (require 'iswitchb)) +(eval-when-compile (require 'ido)) +(eval-when-compile (require 'ediff)) + +;; Dummy to be used by the defcustoms when first loading the file. +(eval-when (load eval) + (defalias 'magit-set-variable-and-refresh 'set-default)) + +;;; Code: (defgroup magit nil "Controlling Git from Emacs." :prefix "magit-" @@ -71,6 +106,12 @@ :group 'magit :type 'string) +(defcustom magit-gitk-executable (concat (file-name-directory magit-git-executable) + "gitk") + "The name of the Gitk executable." + :group 'magit + :type 'string) + (defcustom magit-git-standard-options '("--no-pager") "Standard options when running Git." :group 'magit @@ -78,134 +119,565 @@ (defcustom magit-repo-dirs nil "Directories containing Git repositories. -Magit will look into these directories for Git repositories and offers them as choices for magit-status." +Magit will look into these directories for Git repositories and +offer them as choices for `magit-status'." :group 'magit :type '(repeat string)) (defcustom magit-repo-dirs-depth 3 - "When looking for Git repositors below the directories in magit-repo-dirs, Magit will only descend this many levels deep." + "The maximum depth to look for Git repos. +When looking for a Git repository below the directories in `magit-repo-dirs', +Magit will only descend this many levels deep." :group 'magit :type 'integer) +(defcustom magit-set-upstream-on-push nil + "Non-nil means that \\[magit-push] will use --set-upstream when pushing a branch. +This only applies if the branch does not have an upstream set yet. +Setting this to t will ask if --set-upstream should be used. +Setting it to 'dontask will always use --set-upstream. +Setting it to 'refuse will refuse to push unless a remote branch has already been set. + +--set-upstream is supported with git > 1.7.0" + :group 'magit + :type '(choice (const :tag "Never" nil) + (const :tag "Ask" t) + (const :tag "Refuse" refuse) + (const :tag "Always" dontask))) + (defcustom magit-save-some-buffers t "Non-nil means that \\[magit-status] will save modified buffers before running. Setting this to t will ask which buffers to save, setting it to 'dontask will save all modified buffers without asking." :group 'magit :type '(choice (const :tag "Never" nil) - (const :tag "Ask" t) - (const :tag "Save without asking" dontask))) + (const :tag "Ask" t) + (const :tag "Save without asking" dontask))) + +(defcustom magit-save-some-buffers-predicate + 'magit-save-buffers-predicate-tree-only + "Specifies a predicate function on \\[magit-save-some-buffers] to determine which + unsaved buffers should be prompted for saving." + + :group 'magit + :type '(radio (function-item magit-save-buffers-predicate-tree-only) + (function-item magit-save-buffers-predicate-all) + (function :tag "Other"))) + +(defcustom magit-default-tracking-name-function + 'magit-default-tracking-name-remote-plus-branch + "Specifies the function to use to generate default tracking branch names +when doing a \\[magit-checkout]. + +The default is magit-default-tracking-name-remote-plus-branch, +which generates a tracking name of the form 'REMOTE-BRANCHNAME'." + :group 'magit + :type '(radio (function-item magit-default-tracking-name-remote-plus-branch) + (function-item magit-default-tracking-name-branch-only) + (function :tag "Other"))) (defcustom magit-commit-all-when-nothing-staged 'ask "Determines what \\[magit-log-edit] does when nothing is staged. -Setting this to nil will make it do nothing, setting it to t will arrange things so that the actual commit command will use the \"--all\" option, setting it to 'ask will first ask for confirmation whether to do this, and setting it to 'ask-stage will cause all changes to be staged, after a confirmation." +Setting this to nil will make it do nothing, setting it to t will +arrange things so that the actual commit command will use the \"--all\" option, +setting it to 'ask will first ask for confirmation whether to do this, +and setting it to 'ask-stage will cause all changes to be staged, +after a confirmation." :group 'magit :type '(choice (const :tag "No" nil) - (const :tag "Always" t) - (const :tag "Ask" ask) - (const :tag "Ask to stage everything" ask-stage))) + (const :tag "Always" t) + (const :tag "Ask" ask) + (const :tag "Ask to stage everything" ask-stage))) (defcustom magit-commit-signoff nil - "When performing git commit adds --signoff" + "Add the \"Signed-off-by:\" line when committing." :group 'magit :type 'boolean) +(defcustom magit-sha1-abbrev-length 7 + "The number of digits to show when a sha1 is displayed in abbreviated form." + :group 'magit + :type 'integer) + (defcustom magit-log-cutoff-length 100 - "The maximum number of commits to show in the log and whazzup buffers" + "The maximum number of commits to show in the log and whazzup buffers." :group 'magit :type 'integer) +(defcustom magit-log-infinite-length 99999 + "Number of log used to show as maximum for `magit-log-cutoff-length'." + :group 'magit + :type 'integer) + +(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 + :type 'boolean) + (defcustom magit-process-popup-time -1 "Popup the process buffer if a command takes longer than this many seconds." :group 'magit :type '(choice (const :tag "Never" -1) - (const :tag "Immediately" 0) - (integer :tag "After this many seconds"))) + (const :tag "Immediately" 0) + (integer :tag "After this many seconds"))) + +(defcustom magit-revert-item-confirm t + "Require acknowledgment before reverting an item." + :group 'magit + :type 'boolean) (defcustom magit-log-edit-confirm-cancellation nil - "Require acknowledgement before cancelling the log edit buffer." + "Require acknowledgment before canceling the log edit buffer." :group 'magit :type 'boolean) +(defcustom magit-remote-ref-format 'branch-then-remote + "What format to use for autocompleting refs, in pariticular for remotes. + +Autocompletion is used by functions like `magit-checkout', +`magit-interactive-rebase' and others which offer branch name +completion. + +The value 'name-then-remote means remotes will be of the +form \"name (remote)\", while the value 'remote-slash-name +means that they'll be of the form \"remote/name\". I.e. something that's +listed as \"remotes/upstream/next\" by \"git branch -l -a\" +will be \"upstream/next\"." + :group 'magit + :type '(choice (const :tag "name (remote)" branch-then-remote) + (const :tag "remote/name" remote-slash-branch))) + +(defcustom magit-process-connection-type (not (eq system-type 'cygwin)) + "Connection type used for the git process. + +nil mean pipe, it is usually faster and more efficient, and work on cygwin. +t mean pty, it enable magit to prompt for passphrase when needed." + :group 'magit + :type 'boolean) + +(defcustom magit-completing-read-function 'magit-builtin-completing-read + "Function to be called when requesting input from the user." + :group 'magit + :type '(radio (function-item magit-iswitchb-completing-read) + (function-item magit-ido-completing-read) + (function-item magit-builtin-completing-read) + (function :tag "Other"))) + +(defcustom magit-create-branch-behaviour 'at-head + "Where magit will create a new branch if not supplied a branchname or ref. + +The value 'at-head means a new branch will be created at the tip +of your current branch, while the value 'at-point means magit +will try to find a valid reference at point..." + :group 'magit + :type '(choice (const :tag "at HEAD" at-head) + (const :tag "at point" at-point))) + +(defcustom magit-status-buffer-switch-function 'pop-to-buffer + "Function for `magit-status' to use for switching to the status buffer. + +The function is given one argument, the status buffer." + :group 'magit + :type '(radio (function-item switch-to-buffer) + (function-item pop-to-buffer) + (function :tag "Other"))) + +(defcustom magit-rewrite-inclusive t + "Whether magit includes the selected base commit in a rewrite operation. + +t means both the selected commit as well as any subsequent +commits will be rewritten. This is magit's default behaviour, +equivalent to 'git rebase -i ${REV}~1' + + A'---B'---C'---D' + ^ + +nil means the selected commit will be literally used as 'base', +so only subsequent commits will be rewritten. This is consistent +with git-rebase, equivalent to 'git rebase -i ${REV}', yet more +cumbersome to use from the status buffer. + + A---B'---C'---D' + ^ +" + :group 'magit + :type '(choice (const :tag "Always" t) + (const :tag "Never" nil) + (const :tag "Ask" ask))) + +(defcustom magit-highlight-whitespace t + "Specifies where to highlight whitespace errors. See +`magit-highlight-trailing-whitespace', +`magit-highlight-indentation'. `t' means in all diffs, the +symbol `status' means only in the status buffer, and `nil' means +nowhere." + :group 'magit + :type '(choice (const :tag "Always" t) + (const :tag "Never" nil) + (const :tag "In status buffer" status)) + :set 'magit-set-variable-and-refresh) + +(defcustom magit-highlight-trailing-whitespace t + "If `magit-highlight-whitespace' is enabled, highlight +whitespace at the end of a line in diffs." + :group 'magit + :type 'boolean + :set 'magit-set-variable-and-refresh) + +(defcustom magit-highlight-indentation nil + "If `magit-highlight-whitespace' is enabled, highlight the +\"wrong\" indentation style. + +The value is a list of cons cells. The car is a regular +expression, and the cdr is the value that applies to repositories +whose directory matches the regular expression. If more than one +item matches, then the *last* item in the list applies. So, the +default value should come first in the list. + +If the value is `tabs', highlight indentation with tabs. If the +value is an integer, highlight indentation with at least that +many spaces. Otherwise, highlight neither." + :group 'magit + :type `(repeat (cons (string :tag "Directory regexp") + (choice (const :tag "Tabs" tabs) + (integer :tag "Spaces" :value ,tab-width) + (const :tag "Neither" nil)))) + :set 'magit-set-variable-and-refresh) + +(defcustom magit-diff-refine-hunk nil + "Show fine (word-granularity) differences within diff hunks. + +There are three possible settings: + + nil means to never show fine differences + + t means to only show fine differences for the currently + selected diff hunk + + `all' means to always show fine differences for all displayed diff hunks" + :group 'magit + :type '(choice (const :tag "Never" nil) + (const :tag "Selected only" t) + (const :tag "All" all)) + :set 'magit-set-variable-and-refresh) + +(defvar magit-current-indentation nil + "Indentation highlight used in the current buffer, as specified +in `magit-highlight-indentation'.") +(make-variable-buffer-local 'magit-current-indentation) + +(defgroup magit-faces nil + "Customize the appearance of Magit." + :prefix "magit-" + :group 'faces + :group 'magit) + (defface magit-header - '((t)) + '((t :inherit header-line)) "Face for generic header lines. Many Magit faces inherit from this one by default." - :group 'magit) + :group 'magit-faces) (defface magit-section-title - '((t :weight bold :inherit magit-header)) + '((t :inherit magit-header)) "Face for section titles." - :group 'magit) + :group 'magit-faces) (defface magit-branch - '((t :weight bold :inherit magit-header)) + '((t :inherit magit-header)) "Face for the current branch." - :group 'magit) + :group 'magit-faces) (defface magit-diff-file-header - '((t :inherit magit-header)) + '((t :inherit diff-file-header)) "Face for diff file header lines." - :group 'magit) + :group 'magit-faces) (defface magit-diff-hunk-header - '((t :slant italic :inherit magit-header)) + '((t :inherit diff-hunk-header)) "Face for diff hunk header lines." - :group 'magit) + :group 'magit-faces) (defface magit-diff-add - '((((class color) (background light)) - :foreground "blue1") - (((class color) (background dark)) - :foreground "white")) + '((t :inherit diff-added)) "Face for lines in a diff that have been added." - :group 'magit) + :group 'magit-faces) (defface magit-diff-none - '((t)) + '((t :inherit diff-context)) "Face for lines in a diff that are unchanged." - :group 'magit) + :group 'magit-faces) (defface magit-diff-del + '((t :inherit diff-removed)) + "Face for lines in a diff that have been deleted." + :group 'magit-faces) + +(defface magit-log-graph '((((class color) (background light)) - :foreground "red") + :foreground "grey11") (((class color) (background dark)) - :foreground "OrangeRed")) - "Face for lines in a diff that have been deleted." - :group 'magit) + :foreground "grey80")) + "Face for the graph element of the log output." + :group 'magit-faces) -(defface magit-item-highlight +(defface magit-log-sha1 '((((class color) (background light)) - :background "gray95") + :foreground "firebrick") (((class color) (background dark)) - :background "dim gray")) + :foreground "tomato")) + "Face for the sha1 element of the log output." + :group 'magit-faces) + +(defface magit-log-message + '((t)) + "Face for the message element of the log output." + :group 'magit-faces) + +(defface magit-item-highlight + '((t :inherit highlight)) "Face for highlighting the current item." - :group 'magit) + :group 'magit-faces) (defface magit-item-mark + '((t :inherit secondary-selection)) + "Face for highlighting marked item." + :group 'magit-faces) + +(defface magit-log-head-label-bisect-good '((((class color) (background light)) - :foreground "red") + :box t + :background "light green" + :foreground "dark olive green") (((class color) (background dark)) - :foreground "orange")) - "Face for highlighting marked item." - :group 'magit) + :box t + :background "light green" + :foreground "dark olive green")) + "Face for good bisect refs." + :group 'magit-faces) -(defface magit-log-tag-label +(defface magit-log-head-label-bisect-bad '((((class color) (background light)) - :background "LightGoldenRod") + :box t + :background "IndianRed1" + :foreground "IndianRed4") (((class color) (background dark)) - :background "DarkGoldenRod")) - "Face for git tag labels shown in log buffer." - :group 'magit) + :box t + :background "IndianRed1" + :foreground "IndianRed4")) + "Face for bad bisect refs." + :group 'magit-faces) -(defface magit-log-head-label +(defface magit-log-head-label-remote '((((class color) (background light)) - :background "spring green") + :box t + :background "Grey85" + :foreground "OliveDrab4") (((class color) (background dark)) - :background "DarkGreen")) - "Face for branch head labels shown in log buffer." - :group 'magit) + :box t + :background "Grey11" + :foreground "DarkSeaGreen2")) + "Face for remote branch head labels shown in log buffer." + :group 'magit-faces) + +(defface magit-log-head-label-tags + '((((class color) (background light)) + :box t + :background "LemonChiffon1" + :foreground "goldenrod4") + (((class color) (background dark)) + :box t + :background "LemonChiffon1" + :foreground "goldenrod4")) + "Face for tag labels shown in log buffer." + :group 'magit-faces) + +(defface magit-log-head-label-patches + '((((class color) (background light)) + :box t + :background "IndianRed1" + :foreground "IndianRed4") + (((class color) (background dark)) + :box t + :background "IndianRed1" + :foreground "IndianRed4")) + "Face for Stacked Git patches." + :group 'magit-faces) + +(defface magit-whitespace-warning-face + '((t :inherit trailing-whitespace)) + "Face for highlighting whitespace errors in Magit diffs." + :group 'magit-faces) + +(defvar magit-custom-options '() + "List of custom options to pass to Git. +Do not customize this (used in the `magit-key-mode' implementation).") + +(defvar magit-read-rev-history nil + "The history of inputs to `magit-read-rev'.") + +(defvar magit-back-navigation-history nil + "History items that will be visited by successively going \"back\".") +(make-variable-buffer-local 'magit-back-navigation-history) +(put 'magit-back-navigation-history 'permanent-local t) + +(defvar magit-forward-navigation-history nil + "History items that will be visited by successively going \"forward\".") +(make-variable-buffer-local 'magit-forward-navigation-history) +(put 'magit-forward-navigation-history 'permanent-local t) + +(defvar magit-omit-untracked-dir-contents nil + "When non-nil magit will only list an untracked directory, not its contents.") + +(defvar magit-tmp-buffer-name " *magit-tmp*") + +(defface magit-log-head-label-local + '((((class color) (background light)) + :box t + :background "Grey85" + :foreground "LightSkyBlue4") + (((class color) (background dark)) + :box t + :background "Grey13" + :foreground "LightSkyBlue1")) + "Face for local branch head labels shown in log buffer." + :group 'magit-faces) + +(defface magit-log-head-label-default + '((((class color) (background light)) + :box t + :background "Grey50") + (((class color) (background dark)) + :box t + :background "Grey50")) + "Face for unknown ref labels shown in log buffer." + :group 'magit-faces) + +(defvar magit-mode-map + (let ((map (make-keymap))) + (suppress-keymap map t) + (define-key map (kbd "n") 'magit-goto-next-section) + (define-key map (kbd "p") 'magit-goto-previous-section) + (define-key map (kbd "^") 'magit-goto-parent-section) + (define-key map (kbd "M-n") 'magit-goto-next-sibling-section) + (define-key map (kbd "M-p") 'magit-goto-previous-sibling-section) + (define-key map (kbd "TAB") 'magit-toggle-section) + (define-key map (kbd "<backtab>") 'magit-expand-collapse-section) + (define-key map (kbd "1") 'magit-show-level-1) + (define-key map (kbd "2") 'magit-show-level-2) + (define-key map (kbd "3") 'magit-show-level-3) + (define-key map (kbd "4") 'magit-show-level-4) + (define-key map (kbd "M-1") 'magit-show-level-1-all) + (define-key map (kbd "M-2") 'magit-show-level-2-all) + (define-key map (kbd "M-3") 'magit-show-level-3-all) + (define-key map (kbd "M-4") 'magit-show-level-4-all) + (define-key map (kbd "M-h") 'magit-show-only-files) + (define-key map (kbd "M-H") 'magit-show-only-files-all) + (define-key map (kbd "M-s") 'magit-show-level-4) + (define-key map (kbd "M-S") 'magit-show-level-4-all) + (define-key map (kbd "g") 'magit-refresh) + (define-key map (kbd "G") 'magit-refresh-all) + (define-key map (kbd "?") 'magit-describe-item) + (define-key map (kbd "!") 'magit-key-mode-popup-running) + (define-key map (kbd ":") 'magit-git-command) + (define-key map (kbd "C-x 4 a") 'magit-add-change-log-entry-other-window) + (define-key map (kbd "RET") 'magit-visit-item) + (define-key map (kbd "SPC") 'magit-show-item-or-scroll-up) + (define-key map (kbd "DEL") 'magit-show-item-or-scroll-down) + (define-key map (kbd "C-w") 'magit-copy-item-as-kill) + (define-key map (kbd "R") 'magit-rebase-step) + (define-key map (kbd "t") 'magit-key-mode-popup-tagging) + (define-key map (kbd "r") 'magit-key-mode-popup-rewriting) + (define-key map (kbd "P") 'magit-key-mode-popup-pushing) + (define-key map (kbd "f") 'magit-key-mode-popup-fetching) + (define-key map (kbd "b") 'magit-key-mode-popup-branching) + (define-key map (kbd "B") 'magit-key-mode-popup-bisecting) + (define-key map (kbd "F") 'magit-key-mode-popup-pulling) + (define-key map (kbd "l") 'magit-key-mode-popup-logging) + (define-key map (kbd "$") 'magit-display-process) + (define-key map (kbd "c") 'magit-log-edit) + (define-key map (kbd "E") 'magit-interactive-rebase) + (define-key map (kbd "e") 'magit-ediff) + (define-key map (kbd "w") 'magit-wazzup) + (define-key map (kbd "q") 'magit-quit-window) + (define-key map (kbd "m") 'magit-key-mode-popup-merging) + (define-key map (kbd "x") 'magit-reset-head) + (define-key map (kbd "v") 'magit-revert-item) + (define-key map (kbd "a") 'magit-apply-item) + (define-key map (kbd "A") 'magit-cherry-pick-item) + (define-key map (kbd "d") 'magit-diff-working-tree) + (define-key map (kbd "D") 'magit-diff) + (define-key map (kbd "-") 'magit-diff-smaller-hunks) + (define-key map (kbd "+") 'magit-diff-larger-hunks) + (define-key map (kbd "0") 'magit-diff-default-hunks) + (define-key map (kbd "h") 'magit-toggle-diff-refine-hunk) + map)) + +(defvar magit-commit-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-b") 'magit-show-commit-backward) + (define-key map (kbd "C-c C-f") 'magit-show-commit-forward) + map)) + +(defvar magit-status-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "s") 'magit-stage-item) + (define-key map (kbd "S") 'magit-stage-all) + (define-key map (kbd "u") 'magit-unstage-item) + (define-key map (kbd "U") 'magit-unstage-all) + (define-key map (kbd "i") 'magit-ignore-item) + (define-key map (kbd "I") 'magit-ignore-item-locally) + (define-key map (kbd ".") 'magit-mark-item) + (define-key map (kbd "=") 'magit-diff-with-mark) + (define-key map (kbd "k") 'magit-discard-item) + (define-key map (kbd "C") 'magit-add-log) + (define-key map (kbd "X") 'magit-reset-working-tree) + (define-key map (kbd "z") 'magit-key-mode-popup-stashing) + map)) + +(defvar magit-log-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd ".") 'magit-mark-item) + (define-key map (kbd "=") 'magit-diff-with-mark) + (define-key map (kbd "e") 'magit-log-show-more-entries) + map)) + +(defvar magit-wazzup-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd ".") 'magit-mark-item) + (define-key map (kbd "=") 'magit-diff-with-mark) + (define-key map (kbd "i") 'magit-ignore-item) + map)) + +(defvar magit-branch-manager-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "k") 'magit-remove-branch) + (define-key map (kbd "K") 'magit-remove-branch-in-remote-repo) + (define-key map (kbd "T") 'magit-change-what-branch-tracks) + map)) + +(defvar magit-bug-report-url + "http://github.com/magit/magit/issues") + +(defconst magit-version "@GIT_DEV_VERSION@" + "The version of Magit that you're using.") + +(defun magit-bug-report (str) + (message (concat + "Unknown error: %s\n" + "Please, with as much information as possible, file a bug at\n" + "%s\n" + "You are using Magit version %s.") + str magit-bug-report-url magit-version)) + +(defun magit-buffer-switch (buf) + (if (string-match "magit" (buffer-name)) + (switch-to-buffer buf) + (pop-to-buffer buf))) ;;; Macros @@ -213,15 +685,144 @@ Many Magit faces inherit from this one by default." (declare (indent 0)) `(magit-refresh-wrapper (lambda () ,@body))) +;;; Git features + +(defvar magit-have-graph 'unset) +(defvar magit-have-decorate 'unset) +(defvar magit-have-abbrev 'unset) +(make-variable-buffer-local 'magit-have-graph) +(put 'magit-have-graph 'permanent-local t) +(make-variable-buffer-local 'magit-have-decorate) +(put 'magit-have-decorate 'permanent-local t) +(make-variable-buffer-local 'magit-have-abbrev) +(put 'magit-have-abbrev 'permanent-local t) + +(defun magit-configure-have-graph () + (if (eq magit-have-graph 'unset) + (let ((res (magit-git-exit-code "log" "--graph" "--max-count=0"))) + (setq magit-have-graph (eq res 0))))) + +(defun magit-configure-have-decorate () + (if (eq magit-have-decorate 'unset) + (let ((res (magit-git-exit-code "log" "--decorate=full" "--max-count=0"))) + (setq magit-have-decorate (eq res 0))))) + +(defun magit-configure-have-abbrev () + (if (eq magit-have-abbrev 'unset) + (let ((res (magit-git-exit-code "log" "--no-abbrev-commit" "--max-count=0"))) + (setq magit-have-abbrev (eq res 0))))) + +;;; Compatibilities + +(eval-and-compile + (defun magit-max-args-internal (function) + "Returns the maximum number of arguments accepted by FUNCTION." + (if (symbolp function) + (setq function (symbol-function function))) + (if (subrp function) + (let ((max (cdr (subr-arity function)))) + (if (eq 'many max) + most-positive-fixnum + max)) + (if (eq 'macro (car-safe function)) + (setq function (cdr function))) + (let ((arglist (if (byte-code-function-p function) + (aref function 0) + (second function)))) + (if (memq '&rest arglist) + most-positive-fixnum + (length (remq '&optional arglist)))))) + + (if (functionp 'start-file-process) + (defalias 'magit-start-process 'start-file-process) + (defalias 'magit-start-process 'start-process)) + + (unless (fboundp 'string-match-p) + (defun string-match-p (regexp string &optional start) + "Same as `string-match' except this function does not +change the match data." + (let ((inhibit-changing-match-data t)) + (string-match regexp string start)))) + + (if (fboundp 'with-silent-modifications) + (defalias 'magit-with-silent-modifications 'with-silent-modifications) + (defmacro magit-with-silent-modifications (&rest body) + "Execute body without changing `buffer-modified-p'. Also, do not +record undo information." + `(set-buffer-modified-p + (prog1 (buffer-modified-p) + (let ((buffer-undo-list t) + before-change-functions + after-change-functions) + ,@body))))) + + (if (>= (magit-max-args-internal 'delete-directory) 2) + (defalias 'magit-delete-directory 'delete-directory) + (defun magit-delete-directory (directory &optional recursive) + "Deletes a directory named DIRECTORY. If RECURSIVE is non-nil, +recursively delete all of DIRECTORY's contents as well. + +Does not follow symlinks." + (if (or (file-symlink-p directory) + (not (file-directory-p directory))) + (delete-file directory) + (if recursive + ;; `directory-files-no-dot-files-regex' borrowed from Emacs 23 + (dolist (file (directory-files directory 'full "\\([^.]\\|\\.\\([^.]\\|\\..\\)\\).*")) + (magit-delete-directory file recursive))) + (delete-directory directory))))) + ;;; Utilities +(defun magit-set-variable-and-refresh (symbol value) + "Set SYMBOL to VALUE and call `magit-refresh-all'" + (set-default symbol value) + (magit-refresh-all)) + +(defun magit-iswitchb-completing-read (prompt choices &optional predicate require-match + initial-input hist def) + "iswitchb-based completing-read almost-replacement." + (require 'iswitchb) + (let ((iswitchb-make-buflist-hook + (lambda () + (setq iswitchb-temp-buflist (if (consp (first choices)) + (mapcar #'car choices) + choices))))) + (iswitchb-read-buffer prompt (or initial-input def) require-match))) + +(defun magit-ido-completing-read (prompt choices &optional predicate require-match initial-input hist def) + "ido-based completing-read almost-replacement." + (require 'ido) + (let ((selected (ido-completing-read prompt (if (consp (first choices)) + (mapcar #'car choices) + choices) + predicate require-match initial-input hist def))) + (if (consp (first choices)) + (or (cdr (assoc selected choices)) + selected) + selected))) + +(defun magit-builtin-completing-read (prompt choices &optional predicate require-match + initial-input hist def) + "Magit wrapper for standard `completing-read' function." + (completing-read (if (and def (> (length prompt) 2) + (string-equal ": " (substring prompt -2))) + (format "%s (default %s): " (substring prompt 0 -2) def) + prompt) + choices predicate require-match initial-input hist def)) + +(defun magit-completing-read (prompt choices &optional predicate require-match + initial-input hist def) + (funcall magit-completing-read-function prompt choices predicate require-match + initial-input hist def)) + (defun magit-use-region-p () (if (fboundp 'use-region-p) (use-region-p) (and transient-mark-mode mark-active))) (defun magit-goto-line (line) - ;; Like goto-line but doesn't set the mark. + "Like `goto-line' but doesn't set the mark." (save-restriction (widen) (goto-char 1) @@ -231,7 +832,7 @@ Many Magit faces inherit from this one by default." (if (string= str "") nil (if (equal (elt str (- (length str) 1)) ?\n) - (substring str 0 (- (length str) 1)) + (substring str 0 (- (length str) 1)) str))) (defun magit-split-lines (str) @@ -239,39 +840,45 @@ Many Magit faces inherit from this one by default." nil (let ((lines (nreverse (split-string str "\n")))) (if (string= (car lines) "") - (setq lines (cdr lines))) + (setq lines (cdr lines))) (nreverse lines)))) (defun magit-git-insert (args) - (apply #'process-file - magit-git-executable - nil (list t nil) nil - (append magit-git-standard-options args))) + (insert (magit-git-output args))) (defun magit-git-output (args) - (with-output-to-string - (with-current-buffer - standard-output - (magit-git-insert args)))) + (magit-cmd-output magit-git-executable (append magit-git-standard-options args))) + +(defun magit-cmd-insert (cmd args) + (insert (magit-cmd-output cmd args))) + +(defun magit-cmd-output (cmd args) + (let ((cmd-output (with-output-to-string + (with-current-buffer standard-output + (apply #'process-file + cmd + nil (list t nil) nil + args))))) + (replace-regexp-in-string "\e\\[.*?m" "" cmd-output))) (defun magit-git-string (&rest args) (magit-trim-line (magit-git-output args))) -(defun magit-git-lines (&rest args) +(defun magit-git-lines (&rest args) (magit-split-lines (magit-git-output args))) (defun magit-git-exit-code (&rest args) (apply #'process-file magit-git-executable nil nil nil - (append magit-git-standard-options args))) + (append magit-git-standard-options args))) (defun magit-file-lines (file) (when (file-exists-p file) (with-temp-buffer (insert-file-contents file) (let ((rev (nreverse (split-string (buffer-string) "\n")))) - (nreverse (if (equal (car rev) "") - (cdr rev) - rev)))))) + (nreverse (if (equal (car rev) "") + (cdr rev) + rev)))))) (defun magit-write-file-lines (file lines) (with-temp-buffer @@ -279,121 +886,190 @@ Many Magit faces inherit from this one by default." (insert l "\n")) (write-file file))) -(defun magit-concat-with-delim (delim seqs) - (cond ((null seqs) - nil) - ((null (cdr seqs)) - (car seqs)) - (t - (concat (car seqs) delim (magit-concat-with-delim delim (cdr seqs)))))) - (defun magit-get (&rest keys) - (magit-git-string "config" (magit-concat-with-delim "." keys))) + "Return the value of Git config entry specified by KEYS." + (magit-git-string "config" (mapconcat 'identity keys "."))) + +(defun magit-get-all (&rest keys) + "Return all values of the Git config entry specified by KEYS." + (magit-git-lines "config" "--get-all" (mapconcat 'identity keys "."))) (defun magit-set (val &rest keys) + "Set Git config settings specified by KEYS to VAL." (if val - (magit-git-string "config" (magit-concat-with-delim "." keys) val) - (magit-git-string "config" "--unset" (magit-concat-with-delim "." keys)))) + (magit-git-string "config" (mapconcat 'identity keys ".") val) + (magit-git-string "config" "--unset" (mapconcat 'identity keys ".")))) (defun magit-remove-conflicts (alist) (let ((dict (make-hash-table :test 'equal)) - (result nil)) + (result nil)) (dolist (a alist) (puthash (car a) (cons (cdr a) (gethash (car a) dict)) - dict)) + dict)) (maphash (lambda (key value) - (if (= (length value) 1) - (push (cons key (car value)) result) - (let ((sub (magit-remove-conflicts - (mapcar (lambda (entry) - (let ((dir (directory-file-name - (subseq entry 0 (- (length key)))))) - (cons (concat (file-name-nondirectory dir) "/" key) - entry))) - value)))) - (setq result (append result sub))))) - dict) + (if (= (length value) 1) + (push (cons key (car value)) result) + (let ((sub (magit-remove-conflicts + (mapcar (lambda (entry) + (let ((dir (directory-file-name + (substring entry 0 (- (length key)))))) + (cons (concat (file-name-nondirectory dir) "/" key) + entry))) + value)))) + (setq result (append result sub))))) + dict) result)) (defun magit-git-repo-p (dir) (file-exists-p (expand-file-name ".git" dir))) +(defun magit-no-commit-p () + "Return non-nil if there is no commit in the current git repository." + (not (magit-git-string + "rev-list" "HEAD" "--max-count=1"))) + (defun magit-list-repos* (dir level) (if (magit-git-repo-p dir) (list dir) (apply #'append - (mapcar (lambda (entry) - (unless (or (string= (substring entry -3) "/..") - (string= (substring entry -2) "/.")) - (magit-list-repos* entry (+ level 1)))) - (and (file-directory-p dir) - (< level magit-repo-dirs-depth) - (directory-files dir t nil t)))))) + (mapcar (lambda (entry) + (unless (or (string= (substring entry -3) "/..") + (string= (substring entry -2) "/.")) + (magit-list-repos* entry (+ level 1)))) + (and (file-directory-p dir) + (< level magit-repo-dirs-depth) + (directory-files dir t nil t)))))) (defun magit-list-repos (dirs) (magit-remove-conflicts (apply #'append - (mapcar (lambda (dir) - (mapcar #'(lambda (repo) - (cons (file-name-nondirectory repo) - repo)) - (magit-list-repos* dir 0))) - dirs)))) + (mapcar (lambda (dir) + (mapcar #'(lambda (repo) + (cons (file-name-nondirectory repo) + repo)) + (magit-list-repos* dir 0))) + dirs)))) (defun magit-get-top-dir (cwd) - (let ((cwd (expand-file-name cwd))) - (and (file-directory-p cwd) - (let* ((default-directory cwd) - (magit-dir - (magit-git-string "rev-parse" "--git-dir"))) - (and magit-dir - (file-name-as-directory - (or (file-name-directory magit-dir) cwd))))))) + (let ((cwd (expand-file-name (file-truename cwd)))) + (when (file-directory-p cwd) + (let* ((default-directory (file-name-as-directory cwd)) + (cdup (magit-git-string "rev-parse" "--show-cdup"))) + (when cdup + (file-name-as-directory (expand-file-name cdup cwd))))))) (defun magit-get-ref (ref) (magit-git-string "symbolic-ref" "-q" ref)) (defun magit-get-current-branch () (let* ((head (magit-get-ref "HEAD")) - (pos (and head (string-match "^refs/heads/" head)))) + (pos (and head (string-match "^refs/heads/" head)))) (if pos - (substring head 11) + (substring head 11) nil))) +(defun magit-get-remote (branch) + "Return the name of the remote for BRANCH. +If branch is nil or it has no remote, but a remote named +\"origin\" exists, return that. Otherwise, return nil." + (let ((remote (or (and branch (magit-get "branch" branch "remote")) + (and (magit-get "remote" "origin" "url") "origin")))) + (if (string= remote "") nil remote))) + +(defun magit-get-current-remote () + "Return the name of the remote for the current branch. +If there is no current branch, or no remote for that branch, +but a remote named \"origin\" is configured, return that. +Otherwise, return nil." + (magit-get-remote (magit-get-current-branch))) + (defun magit-ref-exists-p (ref) (= (magit-git-exit-code "show-ref" "--verify" ref) 0)) -(defun magit-read-top-dir (rawp) - (if (and (not rawp) magit-repo-dirs) +(defun magit-read-top-dir (dir) + "Ask the user for a Git repository. The choices offered by +auto-completion will be the repositories under `magit-repo-dirs'. +If `magit-repo-dirs' is nil or DIR is non-nill, then +autocompletion will offer directory names." + (if (and (not dir) magit-repo-dirs) (let* ((repos (magit-list-repos magit-repo-dirs)) - (reply (completing-read "Git repository: " - (magit-list-repos magit-repo-dirs)))) - (file-name-as-directory - (cdr (assoc reply repos)))) + (reply (magit-completing-read "Git repository: " repos))) + (file-name-as-directory + (or (cdr (assoc reply repos)) + (if (file-directory-p reply) + (expand-file-name reply) + (error "Not a repository or a directory: %s" reply))))) (file-name-as-directory (read-directory-name "Git repository: " - (or (magit-get-top-dir default-directory) - default-directory))))) + (or (magit-get-top-dir default-directory) + default-directory))))) + +(defun magit-rev-parse (ref) + "Return the SHA hash for REF." + (magit-git-string "rev-parse" ref)) + +(defun magit-ref-ambiguous-p (ref) + "Return whether or not REF is ambiguous." + ;; If REF is ambiguous, rev-parse just prints errors, + ;; so magit-git-string returns nil. + (not (magit-git-string "rev-parse" "--abbrev-ref" ref))) (defun magit-name-rev (rev) - (and rev - (let ((name (magit-git-string "name-rev" "--name-only" rev))) - (if (or (not name) (string= name "undefined")) - rev - name)))) + "Return a human-readable name for REV. +Unlike git name-rev, this will remove tags/ and remotes/ prefixes +if that can be done unambiguously. In addition, it will filter +out revs involving HEAD." + (when rev + (let ((name (magit-git-string "name-rev" "--no-undefined" "--name-only" rev))) + ;; There doesn't seem to be a way of filtering HEAD out from name-rev, + ;; so we have to do it manually. + ;; HEAD-based names are too transient to allow. + (when (and (stringp name) + (string-match "^\\(.*\\<HEAD\\)\\([~^].*\\|$\\)" name)) + (let ((head-ref (match-string 1 name)) + (modifier (match-string 2 name))) + ;; Sometimes when name-rev gives a HEAD-based name, + ;; rev-parse will give an actual branch or remote name. + (setq name (concat (magit-git-string "rev-parse" "--abbrev-ref" head-ref) + modifier)) + ;; If rev-parse doesn't give us what we want, just use the SHA. + (when (or (null name) (string-match-p "\\<HEAD\\>" name)) + (setq name (magit-rev-parse rev))))) + (setq rev (or name rev)) + (when (string-match "^\\(?:tags\\|remotes\\)/\\(.*\\)" rev) + (let ((plain-name (match-string 1 rev))) + (unless (magit-ref-ambiguous-p plain-name) + (setq rev plain-name)))) + rev))) + +(defun magit-highlight-line-whitespace () + (when (and magit-highlight-whitespace + (or (derived-mode-p 'magit-status-mode) + (not (eq magit-highlight-whitespace 'status)))) + (if (and magit-highlight-trailing-whitespace + (looking-at "^[-+].*?\\([ \t]+\\)$")) + (overlay-put (make-overlay (match-beginning 1) (match-end 1)) + 'face 'magit-whitespace-warning-face)) + (if (or (and (eq magit-current-indentation 'tabs) + (looking-at "^[-+]\\( *\t[ \t]*\\)")) + (and (integerp magit-current-indentation) + (looking-at (format "^[-+]\\([ \t]* \\{%s,\\}[ \t]*\\)" + magit-current-indentation)))) + (overlay-put (make-overlay (match-beginning 1) (match-end 1)) + 'face 'magit-whitespace-warning-face)))) (defun magit-put-line-property (prop val) (put-text-property (line-beginning-position) (line-beginning-position 2) - prop val)) + prop val)) (defun magit-format-commit (commit format) (magit-git-string "log" "--max-count=1" - (concat "--pretty=format:" format) - commit)) + (concat "--pretty=format:" format) + commit)) (defun magit-current-line () (buffer-substring-no-properties (line-beginning-position) - (line-end-position))) + (line-end-position))) (defun magit-insert-region (beg end buf) (let ((text (buffer-substring-no-properties beg end))) @@ -402,7 +1078,7 @@ Many Magit faces inherit from this one by default." (defun magit-insert-current-line (buf) (let ((text (buffer-substring-no-properties - (line-beginning-position) (line-beginning-position 2)))) + (line-beginning-position) (line-beginning-position 2)))) (with-current-buffer buf (insert text)))) @@ -424,49 +1100,105 @@ Many Magit faces inherit from this one by default." (defun magit-choose-parent-id (commit op) (let* ((parents (magit-commit-parents commit))) (if (> (length parents) 1) - (error "Can't %s merge commits." op) + (error "Can't %s merge commits" op) nil))) ;;; Revisions and ranges -(defun magit-list-interesting-refs () +(defvar magit-current-range nil + "The range described by the current buffer. +This is only non-nil in diff and log buffers. + +This has three possible (non-nil) forms. If it's a string REF or +a singleton list (REF), then the range is from REF to the current +working directory state (or HEAD in a log buffer). If it's a +pair (START . END), then the range is START..END.") +(make-variable-buffer-local 'magit-current-range) + +(defun magit-list-interesting-refs (&optional uninteresting) + "Return interesting references as given by `git show-ref'. +Removes references matching UNINTERESTING from the +results. UNINTERESTING can be either a function taking a single +argument or a list of strings used as regexps." (let ((refs ())) (dolist (line (magit-git-lines "show-ref")) (if (string-match "[^ ]+ +\\(.*\\)" line) - (let ((ref (match-string 1 line))) - (cond ((string-match "refs/heads/\\(.*\\)" ref) - (let ((branch (match-string 1 ref))) - (push (cons branch branch) refs))) - ((string-match "refs/tags/\\(.*\\)" ref) - (push (cons (format "%s (tag)" (match-string 1 ref)) ref) - refs)) - ((string-match "refs/remotes/\\([^/]+\\)/\\(.+\\)" ref) - (push (cons (format "%s (%s)" - (match-string 2 ref) - (match-string 1 ref)) - ref) - refs)))))) - refs)) - -(defun magit-read-rev (prompt &optional def) - (let* ((prompt (if def - (format "%s (default %s): " prompt def) - (format "%s: " prompt))) - (interesting-refs (magit-list-interesting-refs)) - (reply (completing-read prompt interesting-refs - nil nil nil nil def)) - (rev (or (cdr (assoc reply interesting-refs)) reply))) + (let ((ref (match-string 1 line))) + (cond ((and (functionp uninteresting) + (funcall uninteresting ref))) + ((and (not (functionp uninteresting)) + (loop for i in uninteresting thereis (string-match i ref)))) + ((string-match "refs/heads/\\(.*\\)" ref) + (let ((branch (match-string 1 ref))) + (push (cons branch branch) refs))) + ((string-match "refs/tags/\\(.*\\)" ref) + (push (cons (format + (if (eq magit-remote-ref-format 'branch-then-remote) + "%s (tag)" "%s") + (match-string 1 ref)) + ref) + refs)) + ((string-match "refs/remotes/\\([^/]+\\)/\\(.+\\)" ref) + (push (cons (if (eq magit-remote-ref-format 'branch-then-remote) + (format "%s (%s)" + (match-string 2 ref) + (match-string 1 ref)) + (format "%s/%s" + (match-string 1 ref) + (match-string 2 ref))) + ref) + refs)))))) + (nreverse refs))) + +(defun magit-tree-contents (treeish) + "Returns a list of all files under TREEISH. TREEISH can be a tree, +a commit, or any reference to one of those." + (let ((return-value nil)) + (with-temp-buffer + (magit-git-insert (list "ls-tree" "-r" treeish)) + (if (eql 0 (buffer-size)) + (error "%s is not a commit or tree." treeish)) + (goto-char (point-min)) + (while (search-forward-regexp "\t\\(.*\\)" nil 'noerror) + (push (match-string 1) return-value))) + return-value)) + +(defvar magit-uninteresting-refs '("refs/remotes/\\([^/]+\\)/HEAD$")) + +(defun magit-read-file-from-rev (revision) + (magit-completing-read (format "Retrieve file from %s: " revision) + (magit-tree-contents revision) + nil + 'require-match + nil + 'magit-read-file-hist + (if buffer-file-name + (let ((topdir-length (length (magit-get-top-dir default-directory)))) + (substring (buffer-file-name) topdir-length))))) + +;; TODO: fix this so that def can (must?) be git rev instead of, say, "master (origin)" +;; which involves a particular display strategy and shouldn't be visible to callers +;; of magit-read-rev +(defun magit-read-rev (prompt &optional def uninteresting) + (let* ((interesting-refs (magit-list-interesting-refs + (or uninteresting magit-uninteresting-refs))) + (reply (magit-completing-read (concat prompt ": ") interesting-refs + nil nil nil 'magit-read-rev-history def)) + (rev (or (cdr (assoc reply interesting-refs)) reply))) (if (string= rev "") - nil + nil rev))) (defun magit-read-rev-range (op &optional def-beg def-end) (let ((beg (magit-read-rev (format "%s start" op) - def-beg))) + def-beg))) (if (not beg) - nil - (let ((end (magit-read-rev (format "%s end" op) def-end))) - (cons beg end))))) + nil + (save-match-data + (if (string-match "^\\(.+\\)\\.\\.\\(.+\\)$" beg) + (cons (match-string 1 beg) (match-string 2 beg)) + (let ((end (magit-read-rev (format "%s end" op) def-end))) + (cons beg end))))))) (defun magit-rev-to-git (rev) (or rev @@ -481,9 +1213,9 @@ Many Magit faces inherit from this one by default." (if (stringp range) range (if (cdr range) - (format "%s..%s" - (magit-rev-to-git (car range)) - (magit-rev-to-git (cdr range))) + (format "%s..%s" + (magit-rev-to-git (car range)) + (magit-rev-to-git (cdr range))) (format "%s" (magit-rev-to-git (car range)))))) (defun magit-rev-describe (rev) @@ -499,13 +1231,43 @@ Many Magit faces inherit from this one by default." (if (stringp range) (format "%s in %s" things range) (if (cdr range) - (format "%s from %s to %s" things - (magit-rev-describe (car range)) - (magit-rev-describe (cdr range))) + (format "%s from %s to %s" things + (magit-rev-describe (car range)) + (magit-rev-describe (cdr range))) (format "%s at %s" things (magit-rev-describe (car range)))))) (defun magit-default-rev () - (magit-name-rev (magit-commit-at-point t))) + (or (magit-name-rev (magit-commit-at-point t)) + (let ((branch (magit-guess-branch))) + (if branch + (if (string-match "^refs/\\(.*\\)" branch) + (match-string 1 branch) + branch))))) + +(defun magit-read-remote (&optional prompt def) + "Read the name of a remote. +PROMPT is used as the prompt, and defaults to \"Remote\". +DEF is the default value, and defaults to the value of `magit-get-current-branch'." + (let* ((prompt (or prompt "Remote: ")) + (def (or def (magit-get-current-remote))) + (remotes (magit-git-lines "remote")) + (reply (magit-completing-read prompt remotes + nil nil nil nil def))) + (if (string= reply "") nil reply))) + +(defun magit-read-remote-branch (remote &optional prompt default) + (let* ((prompt (or prompt (format "Remote branch (in %s): " remote))) + (branches (delete nil + (mapcar + (lambda (b) + (and (not (string-match " -> " b)) + (string-match (format "^ *%s/\\(.*\\)$" + remote) b) + (match-string 1 b))) + (magit-git-lines "branch" "-r")))) + (reply (magit-completing-read prompt branches + nil nil nil nil default))) + (if (string= reply "") nil reply))) ;;; Sections @@ -522,7 +1284,8 @@ Many Magit faces inherit from this one by default." parent title beginning end children hidden type info needs-refresh-on-show) -(defvar magit-top-section nil) +(defvar magit-top-section nil + "The top section of the current buffer.") (make-variable-buffer-local 'magit-top-section) (put 'magit-top-section 'permanent-local t) @@ -531,51 +1294,72 @@ Many Magit faces inherit from this one by default." (defvar magit-section-hidden-default nil) (defun magit-new-section (title type) + "Create a new section with title TITLE and type TYPE in current buffer. + +If not `magit-top-section' exist, the new section will be the new top-section +otherwise, the new-section will be a child of the current top-section. + +If TYPE is nil, the section won't be highlighted." (let* ((s (make-magit-section :parent magit-top-section - :title title - :type type - :hidden magit-section-hidden-default)) - (old (and magit-old-top-section - (magit-find-section (magit-section-path s) - magit-old-top-section)))) + :title title + :type type + :hidden magit-section-hidden-default)) + (old (and magit-old-top-section + (magit-find-section (magit-section-path s) + magit-old-top-section)))) (if magit-top-section - (setf (magit-section-children magit-top-section) - (cons s (magit-section-children magit-top-section))) + (push s (magit-section-children magit-top-section)) (setq magit-top-section s)) (if old - (setf (magit-section-hidden s) (magit-section-hidden old))) + (setf (magit-section-hidden s) (magit-section-hidden old))) s)) (defun magit-cancel-section (section) + "Delete the section SECTION." (delete-region (magit-section-beginning section) - (magit-section-end section)) + (magit-section-end section)) (let ((parent (magit-section-parent section))) (if parent - (setf (magit-section-children parent) - (delq section (magit-section-children parent))) + (setf (magit-section-children parent) + (delq section (magit-section-children parent))) (setq magit-top-section nil)))) (defmacro magit-with-section (title type &rest body) + "Create a new section of title TITLE and type TYPE and evaluate BODY there. + +Sections created inside BODY will become children of the new +section. BODY must leave point at the end of the created section. + +If TYPE is nil, the section won't be highlighted." (declare (indent 2)) - (let ((s (gensym))) + (let ((s (make-symbol "*section*"))) `(let* ((,s (magit-new-section ,title ,type)) - (magit-top-section ,s)) + (magit-top-section ,s)) (setf (magit-section-beginning ,s) (point)) ,@body (setf (magit-section-end ,s) (point)) (setf (magit-section-children ,s) - (nreverse (magit-section-children ,s))) + (nreverse (magit-section-children ,s))) ,s))) +(defun magit-set-section (title type start end) + "Create a new section of title TITLE and type TYPE with specified start and +end positions." + (let ((section (magit-new-section title type))) + (setf (magit-section-beginning section) start) + (setf (magit-section-end section) end) + section)) + (defun magit-set-section-info (info &optional section) (setf (magit-section-info (or section magit-top-section)) info)) (defun magit-set-section-needs-refresh-on-show (flag &optional section) (setf (magit-section-needs-refresh-on-show - (or section magit-top-section)) - flag)) + (or section magit-top-section)) + flag)) (defmacro magit-create-buffer-sections (&rest body) + "Empty current buffer of text and Magit's sections, and then evaluate BODY." (declare (indent 0)) `(let ((inhibit-read-only t)) (erase-buffer) @@ -583,396 +1367,608 @@ Many Magit faces inherit from this one by default." (setq magit-top-section nil) ,@body (when (null magit-top-section) - (magit-with-section 'top nil - (insert "(empty)\n"))) + (magit-with-section 'top nil + (insert "(empty)\n"))) (magit-propertize-section magit-top-section) (magit-section-set-hidden magit-top-section - (magit-section-hidden magit-top-section))))) + (magit-section-hidden magit-top-section))))) (defun magit-propertize-section (section) + "Add text-property needed for SECTION." (put-text-property (magit-section-beginning section) - (magit-section-end section) - 'magit-section section) + (magit-section-end section) + 'magit-section section) (dolist (s (magit-section-children section)) (magit-propertize-section s))) (defun magit-find-section (path top) + "Find the section at the path PATH in subsection of section TOP." (if (null path) top - (let ((sec (find-if (lambda (s) (equal (car path) - (magit-section-title s))) - (magit-section-children top)))) - (if sec - (magit-find-section (cdr path) sec) - nil)))) + (let ((secs (magit-section-children top))) + (while (and secs (not (equal (car path) + (magit-section-title (car secs))))) + (setq secs (cdr secs))) + (and (car secs) + (magit-find-section (cdr path) (car secs)))))) (defun magit-section-path (section) + "Return the path of SECTION." (if (not (magit-section-parent section)) '() (append (magit-section-path (magit-section-parent section)) - (list (magit-section-title section))))) + (list (magit-section-title section))))) -(defun magit-find-section-at (pos secs) - (while (and secs - (not (and (<= (magit-section-beginning (car secs)) pos) - (< pos (magit-section-end (car secs)))))) - (setq secs (cdr secs))) - (if secs - (or (magit-find-section-at pos (magit-section-children (car secs))) - (car secs)) - nil)) +(defun magit-find-section-after (pos) + "Find the first section that begins after POS." + (magit-find-section-after* pos (list magit-top-section))) -(defun magit-find-section-after (pos secs) +(defun magit-find-section-after* (pos secs) + "Find the first section that begins after POS in the list SECS +\(including children of sections in SECS)." (while (and secs - (not (> (magit-section-beginning (car secs)) pos))) - (setq secs (cdr secs))) + (<= (magit-section-beginning (car secs)) pos)) + (setq secs (if (magit-section-hidden (car secs)) + (cdr secs) + (append (magit-section-children (car secs)) + (cdr secs))))) (car secs)) -(defun magit-find-section-before (pos secs) +(defun magit-find-section-before (pos) + "Return the last section that begins before POS." + (let ((section (magit-find-section-at pos))) + (do* ((current (or (magit-section-parent section) + section) + next) + (next (if (not (magit-section-hidden current)) + (magit-find-section-before* pos (magit-section-children current))) + (if (not (magit-section-hidden current)) + (magit-find-section-before* pos (magit-section-children current))))) + ((null next) current)))) + +(defun magit-find-section-before* (pos secs) + "Find the last section that begins before POS in the list SECS." (let ((prev nil)) (while (and secs - (not (> (magit-section-beginning (car secs)) pos))) + (< (magit-section-beginning (car secs)) pos)) (setq prev (car secs)) (setq secs (cdr secs))) prev)) (defun magit-current-section () - (or (get-text-property (point) 'magit-section) + "Return the Magit section at point." + (magit-find-section-at (point))) + +(defun magit-find-section-at (pos) + "Return the Magit section at POS." + (or (get-text-property pos 'magit-section) magit-top-section)) (defun magit-insert-section (section-title-and-type - buffer-title washer cmd &rest args) + buffer-title washer cmd &rest args) + "Run CMD and put its result in a new section. + +SECTION-TITLE-AND-TYPE is either a string that is the title of the section +or (TITLE . TYPE) where TITLE is the title of the section and TYPE is its type. + +If there is no type, or if type is nil, the section won't be highlighted. + +BUFFER-TITLE is the inserted title of the section + +WASHER is a function that will be run after CMD. +The buffer will be narrowed to the inserted text. +It should add sectioning as needed for Magit interaction. + +CMD is an external command that will be run with ARGS as arguments." (let* ((body-beg nil) - (section-title (if (consp section-title-and-type) - (car section-title-and-type) - section-title-and-type)) - (section-type (if (consp section-title-and-type) - (cdr section-title-and-type) - nil)) - (section - (magit-with-section section-title section-type - (if buffer-title - (insert (propertize buffer-title 'face 'magit-section-title) - "\n")) - (setq body-beg (point)) - (apply 'process-file cmd nil t nil (append magit-git-standard-options args)) - (if (not (eq (char-before) ?\n)) - (insert "\n")) - (if washer - (save-restriction - (narrow-to-region body-beg (point)) - (goto-char (point-min)) - (funcall washer) - (goto-char (point-max))))))) + (section-title (if (consp section-title-and-type) + (car section-title-and-type) + section-title-and-type)) + (section-type (if (consp section-title-and-type) + (cdr section-title-and-type) + nil)) + (section + (magit-with-section section-title section-type + (if buffer-title + (insert (propertize buffer-title 'face 'magit-section-title) + "\n")) + (setq body-beg (point)) + (magit-cmd-insert cmd args) + (if (not (eq (char-before) ?\n)) + (insert "\n")) + (if washer + (save-restriction + (narrow-to-region body-beg (point)) + (goto-char (point-min)) + (funcall washer) + (goto-char (point-max))))))) (if (= body-beg (point)) - (magit-cancel-section section) + (magit-cancel-section section) (insert "\n")) section)) (defun magit-git-section (section-title-and-type - buffer-title washer &rest args) - (apply #'magit-insert-section - section-title-and-type - buffer-title - washer - magit-git-executable - (append magit-git-standard-options args))) + buffer-title washer &rest args) + "Run git and put its result in a new section. -(defun magit-next-section (section) - (let ((parent (magit-section-parent section))) - (if parent - (let ((next (cadr (memq section - (magit-section-children parent))))) - (or next - (magit-next-section parent)))))) +see `magit-insert-section' for meaning of the arguments" + (apply #'magit-insert-section + section-title-and-type + buffer-title + washer + magit-git-executable + (append magit-git-standard-options args))) (defun magit-goto-next-section () + "Go to the next section." (interactive) - (let* ((section (magit-current-section)) - (next (or (and (not (magit-section-hidden section)) - (magit-section-children section) - (magit-find-section-after (point) - (magit-section-children - section))) - (magit-next-section section)))) - + (let ((next (magit-find-section-after (point)))) (if next - (progn - (goto-char (magit-section-beginning next)) - (if (memq magit-submode '(log reflog)) - (magit-show-commit next)) - (if (not (magit-section-hidden next)) - (let ((offset (- (line-number-at-pos - (magit-section-beginning next)) - (line-number-at-pos - (magit-section-end next))))) - (if (< offset (window-height)) - (recenter offset))))) + (magit-goto-section next) (message "No next section")))) -(defun magit-prev-section (section) - (let ((parent (magit-section-parent section))) - (if parent - (let ((prev (cadr (memq section - (reverse (magit-section-children parent)))))) - (cond (prev - (while (and (not (magit-section-hidden prev)) - (magit-section-children prev)) - (setq prev (car (reverse (magit-section-children prev))))) - prev) - (t - parent)))))) - (defun magit-goto-previous-section () + "Go to the previous section." (interactive) - (let ((section (magit-current-section))) - (cond ((= (point) (magit-section-beginning section)) - (let ((prev (magit-prev-section (magit-current-section)))) - (if prev - (progn - (if (memq magit-submode '(log reflog)) - (magit-show-commit (or prev section))) - (goto-char (magit-section-beginning prev))) - (message "No previous section")))) - (t - (let ((prev (magit-find-section-before (point) - (magit-section-children - section)))) - (if (memq magit-submode '(log reflog)) - (magit-show-commit (or prev section))) - (goto-char (magit-section-beginning (or prev section)))))))) - -(defun magit-goto-section (path) + (if (eq (point) 1) + (message "No previous section") + (magit-goto-section (magit-find-section-before (point))))) + +(defun magit-goto-parent-section () + "Go to the parent section." + (interactive) + (let ((parent (magit-section-parent (magit-current-section)))) + (when parent + (goto-char (magit-section-beginning parent))))) + +(defun magit-goto-next-sibling-section () + "Go to the next sibling section." + (interactive) + (let* ((initial (point)) + (section (magit-current-section)) + (end (- (magit-section-end section) 1)) + (parent (magit-section-parent section)) + (siblings (magit-section-children parent)) + (next-sibling (magit-find-section-after* end siblings))) + (if next-sibling + (magit-goto-section next-sibling) + (magit-goto-next-section)))) + +(defun magit-goto-previous-sibling-section () + "Go to the previous sibling section." + (interactive) + (let* ((section (magit-current-section)) + (beginning (magit-section-beginning section)) + (parent (magit-section-parent section)) + (siblings (magit-section-children parent)) + (previous-sibling (magit-find-section-before* beginning siblings))) + (if previous-sibling + (magit-goto-section previous-sibling) + (magit-goto-parent-section)))) + +(defun magit-goto-section (section) + (goto-char (magit-section-beginning section)) + (cond + ((and magit-log-auto-more + (eq (magit-section-type section) 'longer)) + (magit-log-show-more-entries) + (forward-line -1) + (magit-goto-next-section)) + ((and (eq (magit-section-type section) 'commit) + (derived-mode-p 'magit-log-mode)) + (magit-show-commit section)))) + +(defun magit-goto-section-at-path (path) + "Go to the section described by PATH." (let ((sec (magit-find-section path magit-top-section))) (if sec - (goto-char (magit-section-beginning sec)) + (goto-char (magit-section-beginning sec)) (message "No such section")))) (defun magit-for-all-sections (func &optional top) + "Run FUNC on TOP and recursively on all its children. + +Default value for TOP is `magit-top-section'" (let ((section (or top magit-top-section))) (when section (funcall func section) (dolist (c (magit-section-children section)) - (magit-for-all-sections func c))))) + (magit-for-all-sections func c))))) (defun magit-section-set-hidden (section hidden) + "Hide SECTION if HIDDEN is not nil, show it otherwise." (setf (magit-section-hidden section) hidden) (if (and (not hidden) - (magit-section-needs-refresh-on-show section)) + (magit-section-needs-refresh-on-show section)) (magit-refresh) (let ((inhibit-read-only t) - (beg (save-excursion - (goto-char (magit-section-beginning section)) - (forward-line) - (point))) - (end (magit-section-end section))) - (put-text-property beg end 'invisible hidden)) + (beg (save-excursion + (goto-char (magit-section-beginning section)) + (forward-line) + (point))) + (end (magit-section-end section))) + (if (< beg end) + (put-text-property beg end 'invisible hidden))) (if (not hidden) - (dolist (c (magit-section-children section)) - (magit-section-set-hidden c (magit-section-hidden c)))))) + (dolist (c (magit-section-children section)) + (magit-section-set-hidden c (magit-section-hidden c)))))) (defun magit-section-any-hidden (section) + "Return true if SECTION or any of its children is hidden." (or (magit-section-hidden section) - (some #'magit-section-any-hidden (magit-section-children section)))) + (let ((kids (magit-section-children section))) + (while (and kids (not (magit-section-any-hidden (car kids)))) + (setq kids (cdr kids))) + kids))) (defun magit-section-collapse (section) + "Show SECTION and hide all its children." (dolist (c (magit-section-children section)) (setf (magit-section-hidden c) t)) (magit-section-set-hidden section nil)) (defun magit-section-expand (section) + "Show SECTION and all its children." (dolist (c (magit-section-children section)) (setf (magit-section-hidden c) nil)) (magit-section-set-hidden section nil)) (defun magit-section-expand-all-aux (section) + "Show recursively all SECTION's children." (dolist (c (magit-section-children section)) (setf (magit-section-hidden c) nil) (magit-section-expand-all-aux c))) (defun magit-section-expand-all (section) + "Show SECTION and all its children." (magit-section-expand-all-aux section) (magit-section-set-hidden section nil)) (defun magit-section-hideshow (flag-or-func) + "Show or hide current section depending on FLAG-OR-FUNC. + +If FLAG-OR-FUNC is a function, it will be ran on current section +IF FLAG-OR-FUNC is a Boolean value, the section will be hidden if its true, shown otherwise" (let ((section (magit-current-section))) - (cond ((magit-section-parent section) - (goto-char (magit-section-beginning section)) - (if (functionp flag-or-func) - (funcall flag-or-func section) - (magit-section-set-hidden section flag-or-func)))))) + (when (magit-section-parent section) + (goto-char (magit-section-beginning section)) + (if (functionp flag-or-func) + (funcall flag-or-func section) + (magit-section-set-hidden section flag-or-func))))) (defun magit-show-section () + "Show current section." (interactive) (magit-section-hideshow nil)) (defun magit-hide-section () + "Hide current section." (interactive) (magit-section-hideshow t)) (defun magit-collapse-section () + "Hide all subsection of current section." (interactive) (magit-section-hideshow #'magit-section-collapse)) (defun magit-expand-section () + "Show all subsection of current section." (interactive) (magit-section-hideshow #'magit-section-expand)) +(defun magit-toggle-file-section () + "Like `magit-toggle-section' but toggle at file granularity." + (interactive) + (when (eq 'hunk (first (magit-section-context-type (magit-current-section)))) + (magit-goto-parent-section)) + (magit-toggle-section)) + (defun magit-toggle-section () + "Toggle hidden status of current section." (interactive) (magit-section-hideshow (lambda (s) (magit-section-set-hidden s (not (magit-section-hidden s)))))) (defun magit-expand-collapse-section () + "Toggle hidden status of subsections of current section." (interactive) (magit-section-hideshow (lambda (s) (cond ((magit-section-any-hidden s) - (magit-section-expand-all s)) - (t - (magit-section-collapse s)))))) + (magit-section-expand-all s)) + (t + (magit-section-collapse s)))))) (defun magit-cycle-section () + "Cycle between expanded, hidden and collapsed state for current section. + +Hidden: only the first line of the section is shown +Collapsed: only the first line of the subsection is shown +Expanded: everything is shown." (interactive) (magit-section-hideshow (lambda (s) (cond ((magit-section-hidden s) - (magit-section-collapse s)) - ((notany #'magit-section-hidden (magit-section-children s)) - (magit-section-set-hidden s t)) - (t - (magit-section-expand s)))))) + (magit-section-collapse s)) + ((notany #'magit-section-hidden (magit-section-children s)) + (magit-section-set-hidden s t)) + (t + (magit-section-expand s)))))) (defun magit-section-lineage (s) - (and s (cons s (magit-section-lineage (magit-section-parent s))))) + "Return list of parent, grand-parents... for section S." + (when s + (cons s (magit-section-lineage (magit-section-parent s))))) (defun magit-section-show-level (section level threshold path) (magit-section-set-hidden section (>= level threshold)) - (when (< level threshold) + (when (and (< level threshold) + (not (magit-no-commit-p))) (if path - (magit-section-show-level (car path) (1+ level) threshold (cdr path)) + (magit-section-show-level (car path) (1+ level) threshold (cdr path)) (dolist (c (magit-section-children section)) - (magit-section-show-level c (1+ level) threshold nil))))) + (magit-section-show-level c (1+ level) threshold nil))))) (defun magit-show-level (level all) + "Show section whose level is less than LEVEL, hide the others. +If ALL is non nil, do this in all sections, +otherwise do it only on ancestors and descendants of current section." (magit-with-refresh (if all - (magit-section-show-level magit-top-section 0 level nil) + (magit-section-show-level magit-top-section 0 level nil) (let ((path (reverse (magit-section-lineage (magit-current-section))))) - (magit-section-show-level (car path) 0 level (cdr path)))))) + (magit-section-show-level (car path) 0 level (cdr path)))))) (defun magit-show-only-files () + "Show section that are files, but not there subsection. + +Do this in on ancestors and descendants of current section." (interactive) - (if (eq magit-submode 'status) + (if (derived-mode-p 'magit-status-mode) (call-interactively 'magit-show-level-2) (call-interactively 'magit-show-level-1))) (defun magit-show-only-files-all () + "Show section that are files, but not there subsection. + +Do this for all sections" (interactive) - (if (eq magit-submode 'status) + (if (derived-mode-p 'magit-status-mode) (call-interactively 'magit-show-level-2-all) (call-interactively 'magit-show-level-1-all))) (defmacro magit-define-level-shower-1 (level all) + "Define an interactive function to show function of level LEVEL. + +If ALL is non nil, this function will affect all section, +otherwise it will affect only ancestors and descendants of current section." (let ((fun (intern (format "magit-show-level-%s%s" - level (if all "-all" "")))) - (doc (format "Show sections on level %s." level))) + level (if all "-all" "")))) + (doc (format "Show sections on level %s." level))) `(defun ,fun () ,doc (interactive) (magit-show-level ,level ,all)))) (defmacro magit-define-level-shower (level) + "Define two interactive function to show function of level LEVEL. +one for all, one for current lineage." `(progn (magit-define-level-shower-1 ,level nil) (magit-define-level-shower-1 ,level t))) (defmacro magit-define-section-jumper (sym title) + "Define an interactive function to go to section SYM. + +TITLE is the displayed title of the section." (let ((fun (intern (format "magit-jump-to-%s" sym))) - (doc (format "Jump to section `%s'." title))) + (doc (format "Jump to section `%s'." title))) `(defun ,fun () ,doc (interactive) - (magit-goto-section '(,sym))))) - -(defvar magit-highlight-overlay nil) + (magit-goto-section-at-path '(,sym))))) + +(defmacro magit-define-inserter (sym arglist &rest body) + (declare (indent defun)) + (let ((fun (intern (format "magit-insert-%s" sym))) + (before (intern (format "magit-before-insert-%s-hook" sym))) + (after (intern (format "magit-after-insert-%s-hook" sym))) + (doc (format "Insert items for `%s'." sym))) + `(defun ,fun ,arglist + ,doc + (run-hooks ',before) + ,@body + (run-hooks ',after)))) (defvar magit-highlighted-section nil) +(defun magit-refine-section (section) + "Apply temporary refinements to the display of SECTION. +Refinements can be undone with `magit-unrefine-section'." + (let ((type (and section (magit-section-type section)))) + (cond ((and (eq type 'hunk) + magit-diff-refine-hunk + (not (eq magit-diff-refine-hunk 'all))) + ;; Refine the current hunk to show fine details, using + ;; diff-mode machinery. + (save-excursion + (goto-char (magit-section-beginning magit-highlighted-section)) + (diff-refine-hunk)))))) + +(defun magit-unrefine-section (section) + "Remove refinements to the display of SECTION done by `magit-refine-section'." + (let ((type (and section (magit-section-type section)))) + (cond ((and (eq type 'hunk) + magit-diff-refine-hunk + (not (eq magit-diff-refine-hunk 'all))) + ;; XXX this should be in some diff-mode function, like + ;; `diff-unrefine-hunk' + (remove-overlays (magit-section-beginning section) + (magit-section-end section) + 'diff-mode 'fine))))) + +(defvar magit-highlight-overlay nil) + (defun magit-highlight-section () + "Highlight current section if it has a type." (let ((section (magit-current-section))) (when (not (eq section magit-highlighted-section)) + (when magit-highlighted-section + ;; remove any refinement from previous hunk + (magit-unrefine-section magit-highlighted-section)) (setq magit-highlighted-section section) (if (not magit-highlight-overlay) - (let ((ov (make-overlay 1 1))) - (overlay-put ov 'face 'magit-item-highlight) - (setq magit-highlight-overlay ov))) + (let ((ov (make-overlay 1 1))) + (overlay-put ov 'face 'magit-item-highlight) + (setq magit-highlight-overlay ov))) (if (and section (magit-section-type section)) - (move-overlay magit-highlight-overlay - (magit-section-beginning section) - (magit-section-end section) - (current-buffer)) - (delete-overlay magit-highlight-overlay))))) + (progn + (magit-refine-section section) + (move-overlay magit-highlight-overlay + (magit-section-beginning section) + (magit-section-end section) + (current-buffer))) + (delete-overlay magit-highlight-overlay))))) (defun magit-section-context-type (section) (if (null section) '() (let ((c (or (magit-section-type section) - (if (symbolp (magit-section-title section)) - (magit-section-title section))))) + (if (symbolp (magit-section-title section)) + (magit-section-title section))))) (if c - (cons c (magit-section-context-type - (magit-section-parent section))) - '())))) + (cons c (magit-section-context-type + (magit-section-parent section))) + '())))) (defun magit-prefix-p (prefix list) + "Returns non-nil if PREFIX is a prefix of LIST. PREFIX and LIST should both be +lists. + +If the car of PREFIX is the symbol '*, then return non-nil if the cdr of PREFIX +is a sublist of LIST (as if '* matched zero or more arbitrary elements of LIST)" ;;; Very schemish... (or (null prefix) (if (eq (car prefix) '*) - (or (magit-prefix-p (cdr prefix) list) - (and (not (null list)) - (magit-prefix-p prefix (cdr list)))) - (and (not (null list)) - (equal (car prefix) (car list)) - (magit-prefix-p (cdr prefix) (cdr list)))))) + (or (magit-prefix-p (cdr prefix) list) + (and (not (null list)) + (magit-prefix-p prefix (cdr list)))) + (and (not (null list)) + (equal (car prefix) (car list)) + (magit-prefix-p (cdr prefix) (cdr list)))))) (defmacro magit-section-case (head &rest clauses) + "Make different action depending of current section. + +HEAD is (SECTION INFO &optional OPNAME), + SECTION will be bind to the current section, + INFO will be bind to the info's of the current section, + OPNAME is a string that will be used to describe current action, + +CLAUSES is a list of CLAUSE, each clause is (SECTION-TYPE &BODY) +where SECTION-TYPE describe section where BODY will be run. + +This returns non-nil if some section matches. If the +corresponding body return a non-nil value, it is returned, +otherwise it roturn t. + +If no section matches, this returns nil if no OPNAME was given +and throws an error otherwise." (declare (indent 1)) (let ((section (car head)) - (info (cadr head)) - (type (gensym)) - (context (gensym)) - (opname (caddr head))) + (info (cadr head)) + (type (make-symbol "*type*")) + (context (make-symbol "*context*")) + (opname (caddr head))) `(let* ((,section (magit-current-section)) - (,info (magit-section-info ,section)) - (,type (magit-section-type ,section)) - (,context (magit-section-context-type ,section))) + (,info (magit-section-info ,section)) + (,type (magit-section-type ,section)) + (,context (magit-section-context-type ,section))) (cond ,@(mapcar (lambda (clause) - (if (eq (car clause) t) - clause - (let ((prefix (reverse (car clause))) - (body (cdr clause))) - `((magit-prefix-p ',prefix ,context) - ,@body)))) - clauses) - ,@(if opname - `(((not ,type) - (error "Nothing to %s here." ,opname)) - (t - (error "Can't %s a %s." - ,opname - (or (get ,type 'magit-description) - ,type))))))))) + (if (eq (car clause) t) + `(t (or (progn ,@(cdr clause)) + t)) + (let ((prefix (reverse (car clause))) + (body (cdr clause))) + `((magit-prefix-p ',prefix ,context) + (or (progn ,@body) + t))))) + clauses) + ,@(when opname + `(((run-hook-with-args-until-success + ',(intern (format "magit-%s-action-hook" opname)))) + ((not ,type) + (error "Nothing to %s here" ,opname)) + (t + (error "Can't %s a %s" + ,opname + (or (get ,type 'magit-description) + ,type))))))))) (defmacro magit-section-action (head &rest clauses) (declare (indent 1)) `(magit-with-refresh (magit-section-case ,head ,@clauses))) +(defmacro magit-add-action (head &rest clauses) + "Add additional actions to a pre-existing operator. +The syntax is identical to `magit-section-case', except that +OPNAME is mandatory and specifies the operation to which to add +the actions." + (declare (indent 1)) + (let ((section (car head)) + (info (cadr head)) + (type (caddr head))) + `(add-hook ',(intern (format "magit-%s-action-hook" type)) + (lambda () + ,(macroexpand + ;; Don't pass in the opname so we don't recursively + ;; run the hook again, and so we don't throw an + ;; error if no action matches. + `(magit-section-case (,section ,info) + ,@clauses)))))) + (defun magit-wash-sequence (func) + "Run FUNC until end of buffer is reached. + +FUNC should leave point at the end of the modified region" (while (and (not (eobp)) - (funcall func)))) + (funcall func)))) + +(defmacro magit-define-command (sym arglist &rest body) + "Macro to define a magit command. +It will define the magit-SYM function having ARGLIST as argument. +It will also define the magit-SYM-command-hook variable. + +The defined function will call the function in the hook in +order until one return non nil. If they all return nil then body will be called. + +It is used to define hookable magit command: command defined by this +function can be enriched by magit extension like magit-topgit and magit-svn" + (declare (indent defun) + (debug (&define name lambda-list + [&optional stringp] ; Match the doc string, if present. + [&optional ("interactive" interactive)] + def-body))) + (let ((fun (intern (format "magit-%s" sym))) + (hook (intern (format "magit-%s-command-hook" sym))) + (doc (format "Command for `%s'." sym)) + (inter nil) + (instr body)) + (when (stringp (car body)) + (setq doc (car body) + instr (cdr body))) + (let ((form (car instr))) + (when (eq (car form) 'interactive) + (setq inter form + instr (cdr instr)))) + `(defun ,fun ,arglist + ,doc + ,inter + (or (run-hook-with-args-until-success + ',hook ,@(remq '&optional (remq '&rest arglist))) + ,@instr)))) ;;; Running commands @@ -980,114 +1976,164 @@ Many Magit faces inherit from this one by default." (let ((pr (if str (concat " " str) ""))) (save-excursion (magit-for-all-buffers (lambda () - (setq mode-line-process pr)))))) + (setq mode-line-process pr)))))) (defun magit-process-indicator-from-command (comps) (if (magit-prefix-p (cons magit-git-executable magit-git-standard-options) - comps) + comps) (setq comps (nthcdr (+ (length magit-git-standard-options) 1) comps))) (cond ((or (null (cdr comps)) - (not (member (car comps) '("remote")))) - (car comps)) - (t - (concat (car comps) " " (cadr comps))))) + (not (member (car comps) '("remote")))) + (car comps)) + (t + (concat (car comps) " " (cadr comps))))) (defvar magit-process nil) (defvar magit-process-client-buffer nil) +(defvar magit-process-buffer-name "*magit-process*" + "Buffer name for running git commands.") (defun magit-run* (cmd-and-args - &optional logline noerase noerror nowait input) + &optional logline noerase noerror nowait input) (if (and magit-process - (get-buffer "*magit-process*")) - (error "Git is already running.")) + (get-buffer magit-process-buffer-name)) + (error "Git is already running")) (let ((cmd (car cmd-and-args)) - (args (cdr cmd-and-args)) - (dir default-directory) - (buf (get-buffer-create "*magit-process*")) - (successp nil)) + (args (cdr cmd-and-args)) + (dir default-directory) + (buf (get-buffer-create magit-process-buffer-name)) + (successp nil)) (magit-set-mode-line-process (magit-process-indicator-from-command cmd-and-args)) (setq magit-process-client-buffer (current-buffer)) - (save-excursion - (set-buffer buf) + (with-current-buffer buf + (view-mode 1) + (set (make-local-variable 'view-no-disable-on-exit) t) + (setq view-exit-action + (lambda (buffer) + (with-current-buffer buffer + (bury-buffer)))) (setq buffer-read-only t) (let ((inhibit-read-only t)) - (setq default-directory dir) - (if noerase - (goto-char (point-max)) - (erase-buffer)) - (insert "$ " (or logline - (magit-concat-with-delim " " cmd-and-args)) - "\n") - (cond (nowait - (setq magit-process - (apply 'start-file-process cmd buf cmd args)) - (set-process-sentinel magit-process 'magit-process-sentinel) - (set-process-filter magit-process 'magit-process-filter) - (when input - (with-current-buffer input - (process-send-region magit-process - (point-min) (point-max))) - (process-send-eof magit-process) - (sit-for 0.1 t)) - (cond ((= magit-process-popup-time 0) - (pop-to-buffer (process-buffer magit-process))) - ((> magit-process-popup-time 0) - (run-with-timer - magit-process-popup-time nil - (function - (lambda (buf) - (with-current-buffer buf - (when magit-process - (display-buffer (process-buffer magit-process)) - (goto-char (point-max)))))) - (current-buffer)))) - (setq successp t)) - (input - (with-current-buffer input - (setq default-directory dir) - (setq successp - (equal (apply 'call-process-region - (point-min) (point-max) - cmd nil buf nil args) 0))) - (magit-set-mode-line-process nil) - (magit-need-refresh magit-process-client-buffer)) - (t - (setq successp - (equal (apply 'process-file cmd nil buf nil args) 0)) - (magit-set-mode-line-process nil) - (magit-need-refresh magit-process-client-buffer)))) + (setq default-directory dir) + (if noerase + (goto-char (point-max)) + (erase-buffer)) + (insert "$ " (or logline + (mapconcat 'identity cmd-and-args " ")) + "\n") + (cond (nowait + (setq magit-process + (let ((process-connection-type magit-process-connection-type)) + (apply 'magit-start-process cmd buf cmd args))) + (set-process-sentinel magit-process 'magit-process-sentinel) + (set-process-filter magit-process 'magit-process-filter) + (when input + (with-current-buffer input + (process-send-region magit-process + (point-min) (point-max))) + (process-send-eof magit-process) + (sit-for 0.1 t)) + (cond ((= magit-process-popup-time 0) + (pop-to-buffer (process-buffer magit-process))) + ((> magit-process-popup-time 0) + (run-with-timer + magit-process-popup-time nil + (function + (lambda (buf) + (with-current-buffer buf + (when magit-process + (display-buffer (process-buffer magit-process)) + (goto-char (point-max)))))) + (current-buffer)))) + (setq successp t)) + (input + (with-current-buffer input + (setq default-directory dir) + (setq magit-process + ;; Don't use a pty, because it would set icrnl + ;; which would modify the input (issue #20). + (let ((process-connection-type nil)) + (apply 'magit-start-process cmd buf cmd args))) + (set-process-filter magit-process 'magit-process-filter) + (process-send-region magit-process + (point-min) (point-max)) + (process-send-eof magit-process) + (while (equal (process-status magit-process) 'run) + (sit-for 0.1 t)) + (setq successp + (equal (process-exit-status magit-process) 0)) + (setq magit-process nil)) + (magit-set-mode-line-process nil) + (magit-need-refresh magit-process-client-buffer)) + (t + (setq successp + (equal (apply 'process-file cmd nil buf nil args) 0)) + (magit-set-mode-line-process nil) + (magit-need-refresh magit-process-client-buffer)))) (or successp - noerror - (error "Git failed.")) + noerror + (error + "%s ... [Hit %s or see buffer %s for details]" + (or (with-current-buffer (get-buffer magit-process-buffer-name) + (when (re-search-backward + (concat "^error: \\(.*\\)" paragraph-separate) nil t) + (match-string 1))) + "Git failed") + (with-current-buffer magit-process-client-buffer + (key-description (car (where-is-internal + 'magit-display-process)))) + magit-process-buffer-name)) successp))) +(autoload 'dired-uncache "dired") (defun magit-process-sentinel (process event) - (let ((msg (format "Git %s." (substring event 0 -1))) - (successp (string-match "^finished" event))) + (let ((msg (format "%s %s." (process-name process) (substring event 0 -1))) + (successp (string-match "^finished" event)) + (key (with-current-buffer magit-process-client-buffer + (key-description (car (where-is-internal + 'magit-display-process)))))) (with-current-buffer (process-buffer process) (let ((inhibit-read-only t)) - (goto-char (point-max)) - (insert msg "\n") - (message msg))) + (goto-char (point-max)) + (insert msg "\n") + (message (if successp msg + (format "%s Hit %s or see buffer %s for details." + msg key (current-buffer))))) + (unless (memq (process-status process) '(run open)) + (dired-uncache default-directory))) (setq magit-process nil) (magit-set-mode-line-process nil) (magit-refresh-buffer magit-process-client-buffer))) +(defun magit-password (proc string) + "Checks if git/ssh asks for a password and ask the user for it." + (let (ask) + (cond ((or (string-match "^Enter passphrase for key '\\\(.*\\\)': $" string) + (string-match "^\\\(.*\\\)'s password:" string)) + (setq ask (format "Password for '%s': " (match-string 1 string)))) + ((string-match "^[pP]assword:" string) + (setq ask "Password:"))) + (when ask + (process-send-string proc (concat (read-passwd ask nil) "\n"))))) + (defun magit-process-filter (proc string) (save-current-buffer (set-buffer (process-buffer proc)) (let ((inhibit-read-only t)) + (magit-password proc string) (goto-char (process-mark proc)) ;; Find last ^M in string. If one was found, ignore everything ;; before it and delete the current line. - (let ((ret-pos (position ?\r string :from-end t))) - (cond (ret-pos - (goto-char (line-beginning-position)) - (delete-region (point) (line-end-position)) - (insert (substring string (+ ret-pos 1)))) - (t - (insert string)))) + (let ((ret-pos (length string))) + (while (and (>= (setq ret-pos (1- ret-pos)) 0) + (/= ?\r (aref string ret-pos)))) + (cond ((>= ret-pos 0) + (goto-char (line-beginning-position)) + (delete-region (point) (line-end-position)) + (insert (substring string (+ ret-pos 1)))) + (t + (insert string)))) (set-marker (process-mark proc) (point))))) (defun magit-run (cmd &rest args) @@ -1097,32 +2143,32 @@ Many Magit faces inherit from this one by default." (defun magit-run-git (&rest args) (magit-with-refresh (magit-run* (append (cons magit-git-executable - magit-git-standard-options) - args)))) - -(defun magit-run-with-input (input cmd &rest args) - (magit-with-refresh - (magit-run* (cons cmd args) nil nil nil nil input))) + magit-git-standard-options) + args)))) (defun magit-run-git-with-input (input &rest args) (magit-with-refresh (magit-run* (append (cons magit-git-executable - magit-git-standard-options) - args) - nil nil nil nil input))) + magit-git-standard-options) + args) + nil nil nil nil input))) (defun magit-run-git-async (&rest args) + (message "Running %s %s" magit-git-executable (mapconcat 'identity args " ")) (magit-run* (append (cons magit-git-executable - magit-git-standard-options) - args) - nil nil nil t)) + magit-git-standard-options) + args) + nil nil nil t)) (defun magit-run-async-with-input (input cmd &rest args) (magit-run* (cons cmd args) nil nil nil t input)) (defun magit-display-process () + "Display output from most recent git command." (interactive) - (display-buffer "*magit-process*")) + (unless (get-buffer magit-process-buffer-name) + (error "No Git commands have run")) + (display-buffer magit-process-buffer-name)) ;;; Mode @@ -1133,93 +2179,12 @@ Many Magit faces inherit from this one by default." (magit-define-section-jumper unstaged "Unstaged changes") (magit-define-section-jumper staged "Staged changes") (magit-define-section-jumper unpushed "Unpushed commits") -(magit-define-section-jumper svn-unpushed "Unpushed commits (SVN)") (magit-define-level-shower 1) (magit-define-level-shower 2) (magit-define-level-shower 3) (magit-define-level-shower 4) -(defvar magit-mode-map - (let ((map (make-keymap))) - (suppress-keymap map t) - (define-key map (kbd "n") 'magit-goto-next-section) - (define-key map (kbd "p") 'magit-goto-previous-section) - (define-key map (kbd "TAB") 'magit-toggle-section) - (define-key map (kbd "<backtab>") 'magit-expand-collapse-section) - (define-key map (kbd "1") 'magit-show-level-1) - (define-key map (kbd "2") 'magit-show-level-2) - (define-key map (kbd "3") 'magit-show-level-3) - (define-key map (kbd "4") 'magit-show-level-4) - (define-key map (kbd "M-1") 'magit-show-level-1-all) - (define-key map (kbd "M-2") 'magit-show-level-2-all) - (define-key map (kbd "M-3") 'magit-show-level-3-all) - (define-key map (kbd "M-4") 'magit-show-level-4-all) - (define-key map (kbd "M-h") 'magit-show-only-files) - (define-key map (kbd "M-H") 'magit-show-only-files-all) - (define-key map (kbd "M-s") 'magit-show-level-4) - (define-key map (kbd "M-S") 'magit-show-level-4-all) - (define-key map (kbd "g") 'magit-refresh) - (define-key map (kbd "G") 'magit-refresh-all) - (define-key map (kbd "s") 'magit-stage-item) - (define-key map (kbd "S") 'magit-stage-all) - (define-key map (kbd "u") 'magit-unstage-item) - (define-key map (kbd "U") 'magit-unstage-all) - (define-key map (kbd "i") 'magit-ignore-item) - (define-key map (kbd "I") 'magit-ignore-item-locally) - (define-key map (kbd "?") 'magit-describe-item) - (define-key map (kbd ".") 'magit-mark-item) - (define-key map (kbd "=") 'magit-diff-with-mark) - (define-key map (kbd "-") 'magit-diff-smaller-hunks) - (define-key map (kbd "+") 'magit-diff-larger-hunks) - (define-key map (kbd "0") 'magit-diff-default-hunks) - (define-key map (kbd "l") 'magit-log) - (define-key map (kbd "L") 'magit-log-long) - (define-key map (kbd "h") 'magit-reflog-head) - (define-key map (kbd "H") 'magit-reflog) - (define-key map (kbd "d") 'magit-diff-working-tree) - (define-key map (kbd "D") 'magit-diff) - (define-key map (kbd "a") 'magit-apply-item) - (define-key map (kbd "A") 'magit-cherry-pick-item) - (define-key map (kbd "v") 'magit-revert-item) - (define-key map (kbd "x") 'magit-reset-head) - (define-key map (kbd "X") 'magit-reset-working-tree) - (define-key map (kbd "k") 'magit-discard-item) - (define-key map (kbd "!") 'magit-shell-command) - (define-key map (kbd "RET") 'magit-visit-item) - (define-key map (kbd "SPC") 'magit-show-item-or-scroll-up) - (define-key map (kbd "DEL") 'magit-show-item-or-scroll-down) - (define-key map (kbd "C-w") 'magit-copy-item-as-kill) - (define-key map (kbd "b") 'magit-checkout) - (define-key map (kbd "B") 'magit-create-branch) - (define-key map (kbd "m") 'magit-manual-merge) - (define-key map (kbd "M") 'magit-automatic-merge) - (define-key map (kbd "e") 'magit-interactive-resolve-item) - (define-key map (kbd "N r") 'magit-svn-rebase) - (define-key map (kbd "N c") 'magit-svn-dcommit) - (define-key map (kbd "R") 'magit-rebase-step) - (define-key map (kbd "r s") 'magit-rewrite-start) - (define-key map (kbd "r t") 'magit-rewrite-stop) - (define-key map (kbd "r a") 'magit-rewrite-abort) - (define-key map (kbd "r f") 'magit-rewrite-finish) - (define-key map (kbd "r *") 'magit-rewrite-set-unused) - (define-key map (kbd "r .") 'magit-rewrite-set-used) - (define-key map (kbd "P") 'magit-push) - (define-key map (kbd "f") 'magit-remote-update) - (define-key map (kbd "F") 'magit-pull) - (define-key map (kbd "c") 'magit-log-edit) - (define-key map (kbd "C") 'magit-add-log) - (define-key map (kbd "t") 'magit-tag) - (define-key map (kbd "T") 'magit-annotated-tag) - (define-key map (kbd "z") 'magit-stash) - (define-key map (kbd "Z") 'magit-stash-snapshot) - (define-key map (kbd "w") 'magit-wazzup) - (define-key map (kbd "$") 'magit-display-process) - (define-key map (kbd "E") 'magit-interactive-rebase) - (define-key map (kbd "V") 'magit-show-branches) - (define-key map (kbd "q") 'quit-window) - map)) - (easy-menu-define magit-mode-menu magit-mode-map "Magit menu" '("Magit" @@ -1237,10 +2202,11 @@ Many Magit faces inherit from this one by default." "---" ["Diff working tree" magit-diff-working-tree t] ["Diff" magit-diff t] - ["Log" magit-log t] - ["Long Log" magit-log-long t] - ["Reflog head" magit-reflog-head t] - ["Reflog" magit-reflog t] + ("Log" + ["Short Log" magit-log t] + ["Long Log" magit-log-long t] + ["Reflog" magit-reflog t] + ["Extended..." magit-key-mode-popup-logging t]) "---" ["Cherry pick" magit-cherry-pick-item t] ["Apply" magit-apply-item t] @@ -1254,16 +2220,10 @@ Many Magit faces inherit from this one by default." ["Stash" magit-stash t] ["Snapshot" magit-stash-snapshot t] "---" - ["Switch branch" magit-checkout t] - ["Create branch" magit-create-branch t] + ["Branch..." magit-checkout t] ["Merge" magit-automatic-merge t] - ["Merge (no commit)" magit-manual-merge t] ["Interactive resolve" magit-interactive-resolve-item t] ["Rebase" magit-rebase-step t] - ("Git SVN" - ["Rebase" magit-svn-rebase (magit-svn-enabled)] - ["Commit" magit-svn-dcommit (magit-svn-enabled)] - ) ("Rewrite" ["Start" magit-rewrite-start t] ["Stop" magit-rewrite-stop t] @@ -1275,18 +2235,21 @@ Many Magit faces inherit from this one by default." ["Push" magit-push t] ["Pull" magit-pull t] ["Remote update" magit-remote-update t] + ("Submodule" + ["Submodule update" magit-submodule-update t] + ["Submodule update and init" magit-submodule-update-init t] + ["Submodule init" magit-submodule-init t] + ["Submodule sync" magit-submodule-sync t]) + "---" + ("Extensions") "---" ["Display Git output" magit-display-process t] - ["Quit Magit" quit-window t])) + ["Quit Magit" magit-quit-window t])) -(defvar magit-mode-hook nil) +(defvar magit-mode-hook nil "Hook run by `magit-mode'.") (put 'magit-mode 'mode-class 'special) -(defvar magit-submode nil) -(make-variable-buffer-local 'magit-submode) -(put 'magit-submode 'permanent-local t) - (defvar magit-refresh-function nil) (make-variable-buffer-local 'magit-refresh-function) (put 'magit-refresh-function 'permanent-local t) @@ -1311,20 +2274,21 @@ Many Magit faces inherit from this one by default." pos) (defun magit-correct-point-after-command () - ;; Emacs often leaves point in invisible regions, it seems. To fix - ;; this, we move point ourselves and never let Emacs do its own - ;; adjustements. - ;; - ;; When point has to be moved out of an invisible region, it can be - ;; moved to its end or its beginning. We usually move it to its - ;; end, except when that would move point back to where it was - ;; before the last command. - ;; + "Move point outside of invisible regions. + +Emacs often leaves point in invisible regions, it seems. To fix +this, we move point ourselves and never let Emacs do its own +adjustments. + +When point has to be moved out of an invisible region, it can be +moved to its end or its beginning. We usually move it to its +end, except when that would move point back to where it was +before the last command." (if (invisible-p (point)) (let ((end (magit-invisible-region-end (point)))) - (goto-char (if (= end last-point) - (magit-invisible-region-start (point)) - end)))) + (goto-char (if (= end last-point) + (magit-invisible-region-start (point)) + end)))) (setq disable-point-adjustment t)) (defun magit-post-command-hook () @@ -1339,86 +2303,114 @@ Please see the manual for a complete description of Magit. \\{magit-mode-map}" (kill-all-local-variables) (buffer-disable-undo) - (setq buffer-read-only t) - (make-local-variable 'line-move-visual) - (setq major-mode 'magit-mode - mode-name "Magit" - mode-line-process "" - truncate-lines t - line-move-visual nil) + (setq buffer-read-only t + truncate-lines t + major-mode 'magit-mode + mode-name "Magit" + mode-line-process "") (add-hook 'pre-command-hook #'magit-remember-point nil t) (add-hook 'post-command-hook #'magit-post-command-hook t t) (use-local-map magit-mode-map) + (setq magit-current-indentation (magit-indentation-for default-directory)) + ;; Emacs' normal method of showing trailing whitespace gives weird + ;; results when `magit-whitespace-warning-face' is different from + ;; `trailing-whitespace'. + (if (and magit-highlight-whitespace magit-highlight-trailing-whitespace) + (setq show-trailing-whitespace nil)) (run-mode-hooks 'magit-mode-hook)) (defun magit-mode-init (dir submode refresh-func &rest refresh-args) (setq default-directory dir - magit-submode submode - magit-refresh-function refresh-func - magit-refresh-args refresh-args) - (magit-mode) + magit-refresh-function refresh-func + magit-refresh-args refresh-args) + (funcall submode) (magit-refresh-buffer)) +(defun magit-indentation-for (dir) + (let (result) + (dolist (pair magit-highlight-indentation) + (if (string-match-p (car pair) dir) + (setq result (cdr pair)))) + result)) + (defun magit-find-buffer (submode &optional dir) (let ((topdir (magit-get-top-dir (or dir default-directory)))) (dolist (buf (buffer-list)) - (if (save-excursion - (set-buffer buf) - (and default-directory - (equal (expand-file-name default-directory) topdir) - (eq major-mode 'magit-mode) - (eq magit-submode submode))) - (return buf))))) + (if (with-current-buffer buf + (and (eq major-mode submode) + default-directory + (equal (expand-file-name default-directory) topdir))) + (return buf))))) (defun magit-find-status-buffer (&optional dir) - (magit-find-buffer 'status dir)) + (magit-find-buffer 'magit-status-mode dir)) (defun magit-for-all-buffers (func &optional dir) (dolist (buf (buffer-list)) - (save-excursion - (set-buffer buf) - (if (and (eq major-mode 'magit-mode) - (or (null dir) - (equal default-directory dir))) - (funcall func))))) + (with-current-buffer buf + (if (and (derived-mode-p 'magit-mode) + (or (null dir) + (equal default-directory dir))) + (funcall func))))) (defun magit-refresh-buffer (&optional buffer) (with-current-buffer (or buffer (current-buffer)) (let* ((old-line (line-number-at-pos)) - (old-section (magit-current-section)) - (old-path (and old-section - (magit-section-path (magit-current-section)))) - (section-line (and old-section - (count-lines - (magit-section-beginning old-section) - (point))))) - (if magit-refresh-function - (apply magit-refresh-function - magit-refresh-args)) - (magit-refresh-marked-commits-in-buffer) - (let ((s (and old-path (magit-find-section old-path magit-top-section)))) - (cond (s - (goto-char (magit-section-beginning s)) - (forward-line section-line)) - (t - (magit-goto-line old-line))) - (dolist (w (get-buffer-window-list (current-buffer))) - (set-window-point w (point))) - (magit-highlight-section))))) + (old-point (point)) + (old-section (magit-current-section)) + (old-path (and old-section + (magit-section-path (magit-current-section))))) + (beginning-of-line) + (let ((section-line (and old-section + (count-lines + (magit-section-beginning old-section) + (point)))) + (line-char (- old-point (point)))) + (if magit-refresh-function + (apply magit-refresh-function + magit-refresh-args)) + (magit-refresh-marked-commits-in-buffer) + (let ((s (and old-path (magit-find-section old-path magit-top-section)))) + (cond (s + (goto-char (magit-section-beginning s)) + (forward-line section-line) + (forward-char line-char)) + (t + (magit-goto-line old-line))) + (dolist (w (get-buffer-window-list (current-buffer))) + (set-window-point w (point))) + (magit-highlight-section)))))) (defun magit-string-has-prefix-p (string prefix) (eq (compare-strings string nil (length prefix) prefix nil nil) t)) -(defun magit-revert-buffers (dir) +(defun magit-revert-buffers (dir &optional ignore-modtime) (dolist (buffer (buffer-list)) (when (and buffer - (buffer-file-name buffer) - (magit-string-has-prefix-p (buffer-file-name buffer) dir) - (not (verify-visited-file-modtime buffer)) - (not (buffer-modified-p buffer))) + (buffer-file-name buffer) + ;; don't revert indirect buffers, as the parent will be reverted + (not (buffer-base-buffer buffer)) + (magit-string-has-prefix-p (buffer-file-name buffer) dir) + (file-readable-p (buffer-file-name buffer)) + (or ignore-modtime (not (verify-visited-file-modtime buffer))) + (not (buffer-modified-p buffer))) (with-current-buffer buffer - (ignore-errors - (revert-buffer t t nil)))))) + (condition-case var + (revert-buffer t t nil) + (error (let ((signal-data (cadr var))) + (cond (t (magit-bug-report signal-data)))))))))) + +(defun magit-update-vc-modeline (dir) + "Update the modeline for buffers representable by magit." + (dolist (buffer (buffer-list)) + (when (and buffer + (buffer-file-name buffer) + (magit-string-has-prefix-p (buffer-file-name buffer) dir)) + (with-current-buffer buffer + (condition-case var + (vc-find-file-hook) + (error (let ((signal-data (cadr var))) + (cond (t (magit-bug-report signal-data)))))))))) (defvar magit-refresh-needing-buffers nil) (defvar magit-refresh-pending nil) @@ -1427,29 +2419,34 @@ Please see the manual for a complete description of Magit. (if magit-refresh-pending (funcall func) (let* ((dir default-directory) - (status-buffer (magit-find-buffer 'status dir)) - (magit-refresh-needing-buffers nil) - (magit-refresh-pending t)) + (status-buffer (magit-find-status-buffer dir)) + (magit-refresh-needing-buffers nil) + (magit-refresh-pending t)) (unwind-protect - (funcall func) - (when magit-refresh-needing-buffers - (magit-revert-buffers dir) - (dolist (b (adjoin status-buffer - magit-refresh-needing-buffers)) - (magit-refresh-buffer b))))))) + (funcall func) + (when magit-refresh-needing-buffers + (magit-revert-buffers dir) + (dolist (b (adjoin status-buffer + magit-refresh-needing-buffers)) + (magit-refresh-buffer b))))))) (defun magit-need-refresh (&optional buffer) - (let ((buffer (or buffer (current-buffer)))) - (when (not (memq buffer magit-refresh-needing-buffers)) - (setq magit-refresh-needing-buffers - (cons buffer magit-refresh-needing-buffers))))) + "Mark BUFFER as needing to be refreshed. If BUFFER is nil, use the +current buffer." + (pushnew (or buffer (current-buffer)) magit-refresh-needing-buffers :test 'eq)) (defun magit-refresh () + "Refresh current buffer to match repository state. +Also revert every unmodified buffer visiting files +in the corresponding directory." (interactive) (magit-with-refresh (magit-need-refresh))) (defun magit-refresh-all () + "Refresh all magit buffers to match respective repository states. +Also revert every unmodified buffer visiting files +in the corresponding directories." (interactive) (magit-for-all-buffers #'magit-refresh-buffer default-directory)) @@ -1458,11 +2455,11 @@ Please see the manual for a complete description of Magit. (defun magit-wash-untracked-file () (if (looking-at "^? \\(.*\\)$") (let ((file (match-string-no-properties 1))) - (delete-region (point) (+ (line-end-position) 1)) - (magit-with-section file 'file - (magit-set-section-info file) - (insert "\t" file "\n")) - t) + (delete-region (point) (+ (line-end-position) 1)) + (magit-with-section file 'file + (magit-set-section-info file) + (insert "\t" file "\n")) + t) nil)) (defun magit-wash-untracked-files () @@ -1476,9 +2473,14 @@ Please see the manual for a complete description of Magit. (magit-wash-sequence #'magit-wash-untracked-file))) (defun magit-insert-untracked-files () - (magit-git-section 'untracked "Untracked files:" - 'magit-wash-untracked-files - "ls-files" "-t" "--others" "--exclude-standard")) + (unless (string= (magit-get "status" "showUntrackedFiles") "no") + (apply 'magit-git-section + `(untracked + "Untracked files:" + magit-wash-untracked-files + "ls-files" "--others" "-t" "--exclude-standard" + ,@(when magit-omit-untracked-dir-contents + '("--directory")))))) ;;; Diffs and Hunks @@ -1488,27 +2490,67 @@ Please see the manual for a complete description of Magit. (format "-U%d" magit-diff-context-lines)) (defun magit-diff-smaller-hunks (&optional count) + "Decrease the context for diff hunks by COUNT." (interactive "p") (setq magit-diff-context-lines (max 0 (- magit-diff-context-lines count))) (magit-refresh)) (defun magit-diff-larger-hunks (&optional count) + "Increase the context for diff hunks by COUNT." (interactive "p") (setq magit-diff-context-lines (+ magit-diff-context-lines count)) (magit-refresh)) (defun magit-diff-default-hunks () + "Reset context for diff hunks to the default size." (interactive "") (setq magit-diff-context-lines 3) (magit-refresh)) +(defun magit-toggle-diff-refine-hunk (&optional other) + (interactive "P") + "Turn diff-hunk refining on or off. + +If hunk refining is currently on, then hunk refining is turned off. +If hunk refining is off, then hunk refining is turned on, in +`selected' mode (only the currently selected hunk is refined). + +With a prefix argument, the \"third choice\" is used instead: +If hunk refining is currently on, then refining is kept on, but +the refining mode (`selected' or `all') is switched. +If hunk refining is off, then hunk refining is turned on, in +`all' mode (all hunks refined). + +Customize `magit-diff-refine-hunk' to change the default mode." + (let* ((old magit-diff-refine-hunk) + (new + (if other + (if (eq old 'all) t 'all) + (not old)))) + + ;; remove any old refining in currently highlighted section + (when (and magit-highlighted-section old (not (eq old 'all))) + (magit-unrefine-section magit-highlighted-section)) + + ;; set variable to new value locally + (set (make-local-variable 'magit-diff-refine-hunk) new) + + ;; if now highlighting in "selected only" mode, turn refining back + ;; on in the current section + (when (and magit-highlighted-section new (not (eq new 'all))) + (magit-refine-section magit-highlighted-section)) + + ;; `all' mode being turned on or off needs a complete refresh + (when (or (eq old 'all) (eq new 'all)) + (magit-refresh)))) + (defun magit-diff-line-file () (cond ((looking-at "^diff --git ./\\(.*\\) ./\\(.*\\)$") - (match-string-no-properties 2)) - ((looking-at "^diff --cc +\\(.*\\)$") - (match-string-no-properties 1)) - (t - nil))) + (match-string-no-properties 2)) + ((looking-at "^diff --cc +\\(.*\\)$") + (match-string-no-properties 1)) + (t + nil))) (defun magit-wash-diffs () (magit-wash-sequence #'magit-wash-diff-or-other-file)) @@ -1520,74 +2562,111 @@ Please see the manual for a complete description of Magit. (defun magit-wash-other-file () (if (looking-at "^? \\(.*\\)$") (let ((file (match-string-no-properties 1))) - (delete-region (point) (+ (line-end-position) 1)) - (magit-with-section file 'file - (magit-set-section-info file) - (insert "\tNew " file "\n")) - t) + (delete-region (point) (+ (line-end-position) 1)) + (magit-with-section file 'file + (magit-set-section-info file) + (insert "\tNew " file "\n")) + t) nil)) (defvar magit-hide-diffs nil) +(defvar magit-indentation-level 1) + (defun magit-insert-diff-title (status file file2) (let ((status-text (case status - ((unmerged) - (format "Unmerged %s" file)) - ((new) - (format "New %s" file)) - ((deleted) - (format "Deleted %s" file)) - ((renamed) - (format "Renamed %s (from %s)" - file file2)) - ((modified) - (format "Modified %s" file)) - (t - (format "? %s" file))))) - (insert "\t" status-text "\n"))) + ((unmerged) + (format "Unmerged %s" file)) + ((new) + (format "New %s" file)) + ((deleted) + (format "Deleted %s" file)) + ((renamed) + (format "Renamed %s (from %s)" + file file2)) + ((modified) + (format "Modified %s" file)) + ((typechange) + (format "Typechange %s" file)) + (t + (format "? %s" file))))) + (insert (make-string magit-indentation-level ?\t) status-text "\n"))) + +(defvar magit-current-diff-range nil + "Used internally when setting up magit diff sections.") + +(defun magit-wash-typechange-section (file) + (magit-set-section-info (list 'typechange file)) + (let ((first-start (point-marker)) + (second-start (progn (forward-line 1) + (search-forward-regexp "^diff") + (beginning-of-line) + (point-marker)))) + (let ((magit-indentation-level (+ magit-indentation-level 1))) + (save-restriction + (narrow-to-region first-start second-start) + (goto-char (point-min)) + (magit-with-section file 'diff + (magit-wash-diff-section))) + (save-restriction + (narrow-to-region second-start (point-max)) + (goto-char (point-min)) + (magit-with-section file 'diff + (magit-wash-diff-section)))))) (defun magit-wash-diff-section () (cond ((looking-at "^\\* Unmerged path \\(.*\\)") - (let ((file (match-string-no-properties 1))) - (delete-region (point) (line-end-position)) - (insert "\tUnmerged " file "\n") - (magit-set-section-info (list 'unmerged file nil)) - t)) - ((looking-at "^diff") - (let ((file (magit-diff-line-file)) - (end (save-excursion - (forward-line) ;; skip over "diff" line - (if (search-forward-regexp "^diff\\|^@@" nil t) - (goto-char (match-beginning 0)) - (goto-char (point-max))) - (point-marker)))) - (let* ((status (cond - ((looking-at "^diff --cc") - 'unmerged) - ((save-excursion - (search-forward-regexp "^new file" end t)) - 'new) - ((save-excursion - (search-forward-regexp "^deleted" end t)) - 'deleted) - ((save-excursion - (search-forward-regexp "^rename" end t)) - 'renamed) - (t - 'modified))) - (file2 (cond - ((save-excursion - (search-forward-regexp "^rename from \\(.*\\)" - end t)) - (match-string-no-properties 1))))) - (magit-set-section-info (list status file file2)) - (magit-insert-diff-title status file file2) - (goto-char end) - (let ((magit-section-hidden-default nil)) - (magit-wash-sequence #'magit-wash-hunk)))) - t) - (t - nil))) + (let ((file (match-string-no-properties 1))) + (delete-region (point) (line-end-position)) + (insert "\tUnmerged " file "\n") + (magit-set-section-info (list 'unmerged file nil)) + t)) + ((looking-at "^diff") + (let ((file (magit-diff-line-file)) + (end (save-excursion + (forward-line) ;; skip over "diff" line + (if (search-forward-regexp "^diff\\|^@@" nil t) + (goto-char (match-beginning 0)) + (goto-char (point-max))) + (point-marker)))) + (let* ((status (cond + ((looking-at "^diff --cc") + 'unmerged) + ((save-excursion + (search-forward-regexp "^new file" end t)) + 'new) + ((save-excursion + (search-forward-regexp "^deleted" end t)) + 'deleted) + ((save-excursion + (search-forward-regexp "^rename" end t)) + 'renamed) + (t + 'modified))) + (file2 (cond + ((save-excursion + (search-forward-regexp "^rename from \\(.*\\)" + end t)) + (match-string-no-properties 1))))) + (magit-set-section-info (list status + file + (or file2 file) + magit-current-diff-range)) + (magit-insert-diff-title status file file2) + (when (search-forward-regexp "\\(--- \\(.*\\)\n\\+\\+\\+ \\(.*\\)\n\\)" () t) + (when (match-string 1) + (add-text-properties (match-beginning 1) (match-end 1) + '(face magit-diff-hunk-header)) + (add-text-properties (match-beginning 2) (match-end 2) + '(face magit-diff-file-header)) + (add-text-properties (match-beginning 3) (match-end 3) + '(face magit-diff-file-header)))) + (goto-char end) + (let ((magit-section-hidden-default nil)) + (magit-wash-sequence #'magit-wash-hunk)))) + t) + (t + nil))) (defun magit-wash-diff () (let ((magit-section-hidden-default magit-hide-diffs)) @@ -1603,45 +2682,61 @@ Please see the manual for a complete description of Magit. (defun magit-diff-item-file2 (diff) (caddr (magit-section-info diff))) +(defun magit-diff-item-range (diff) + (nth 3 (magit-section-info diff))) + (defun magit-wash-hunk () - (cond ((looking-at "\\(^@+\\)[^@]*@+") - (let ((n-columns (1- (length (match-string 1)))) - (head (match-string 0))) - (magit-with-section head 'hunk - (magit-put-line-property 'face 'magit-diff-hunk-header) - (forward-line) - (while (not (or (eobp) - (looking-at "^diff\\|^@@"))) - (let ((prefix (buffer-substring-no-properties - (point) (min (+ (point) n-columns) (point-max))))) - (cond ((string-match "\\+" prefix) - (magit-put-line-property 'face 'magit-diff-add)) - ((string-match "-" prefix) - (magit-put-line-property 'face 'magit-diff-del)) - (t - (magit-put-line-property 'face 'magit-diff-none)))) - (forward-line)))) - t) - (t - nil))) + (cond ((looking-at "\\(^@+\\)[^@]*@+.*") + (let ((n-columns (1- (length (match-string 1)))) + (head (match-string 0)) + (hunk-start-pos (point))) + (magit-with-section head 'hunk + (add-text-properties (match-beginning 0) (match-end 0) + '(face magit-diff-hunk-header)) + (forward-line) + (while (not (or (eobp) + (looking-at "^diff\\|^@@"))) + (magit-highlight-line-whitespace) + (let ((prefix (buffer-substring-no-properties + (point) (min (+ (point) n-columns) (point-max))))) + (cond ((string-match "\\+" prefix) + (magit-put-line-property 'face 'magit-diff-add)) + ((string-match "-" prefix) + (magit-put-line-property 'face 'magit-diff-del)) + (t + (magit-put-line-property 'face 'magit-diff-none)))) + (forward-line))) + + (when (eq magit-diff-refine-hunk 'all) + (save-excursion + (goto-char hunk-start-pos) + (diff-refine-hunk)))) + t) + (t + nil))) (defvar magit-diff-options nil) -(defun magit-insert-diff (file) +(defun magit-insert-diff (file status) (let ((cmd magit-git-executable) - (args (append (list "diff") - (list (magit-diff-U-arg)) - magit-diff-options - (list "--" file)))) + (args (append (list "diff") + (list (magit-diff-U-arg)) + magit-diff-options + (list "--" file)))) (let ((p (point))) (magit-git-insert args) (if (not (eq (char-before) ?\n)) - (insert "\n")) + (insert "\n")) (save-restriction - (narrow-to-region p (point)) - (goto-char p) - (magit-wash-diff-section) - (goto-char (point-max)))))) + (narrow-to-region p (point)) + (goto-char p) + (cond + ((eq status 'typechange) + (magit-insert-diff-title status file file) + (magit-wash-typechange-section file)) + (t + (magit-wash-diff-section))) + (goto-char (point-max)))))) (defvar magit-last-raw-diff nil) (defvar magit-ignore-unmerged-raw-diffs nil) @@ -1654,67 +2749,67 @@ Please see the manual for a complete description of Magit. (if (looking-at ":\\([0-7]+\\) \\([0-7]+\\) [0-9a-f]+ [0-9a-f]+ \\(.\\)[0-9]*\t\\([^\t\n]+\\)$") (let ((old-perm (match-string-no-properties 1)) - (new-perm (match-string-no-properties 2)) - (status (case (string-to-char (match-string-no-properties 3)) - (?A 'new) - (?D 'deleted) - (?M 'modified) - (?U 'unmerged) - (?T 'new-type) - (t nil))) - (file (match-string-no-properties 4))) - ;; If this is for the same file as the last diff, ignore it. - ;; Unmerged files seem to get two entries. - ;; We also ignore unmerged files when told so. - (if (or (equal file magit-last-raw-diff) - (and magit-ignore-unmerged-raw-diffs (eq status 'unmerged))) - (delete-region (point) (+ (line-end-position) 1)) - (setq magit-last-raw-diff file) - ;; The 'diff' section that is created here will not work with - ;; magit-insert-diff-item-patch etc when we leave it empty. - ;; Luckily, raw diffs are only produced for staged and - ;; unstaged changes, and we never call - ;; magit-insert-diff-item-patch on them. This is a bit - ;; brittle, of course. - (let ((magit-section-hidden-default magit-hide-diffs)) - (magit-with-section file 'diff - (delete-region (point) (+ (line-end-position) 1)) - (if (not (magit-section-hidden magit-top-section)) - (magit-insert-diff file) - (magit-set-section-info (list status file nil)) - (magit-set-section-needs-refresh-on-show t) - (magit-insert-diff-title status file nil))))) - t) + (new-perm (match-string-no-properties 2)) + (status (case (string-to-char (match-string-no-properties 3)) + (?A 'new) + (?D 'deleted) + (?M 'modified) + (?U 'unmerged) + (?T 'typechange) + (t nil))) + (file (match-string-no-properties 4))) + ;; If this is for the same file as the last diff, ignore it. + ;; Unmerged files seem to get two entries. + ;; We also ignore unmerged files when told so. + (if (or (equal file magit-last-raw-diff) + (and magit-ignore-unmerged-raw-diffs (eq status 'unmerged))) + (delete-region (point) (+ (line-end-position) 1)) + (setq magit-last-raw-diff file) + ;; The 'diff' section that is created here will not work with + ;; magit-insert-diff-item-patch etc when we leave it empty. + ;; Luckily, raw diffs are only produced for staged and + ;; unstaged changes, and we never call + ;; magit-insert-diff-item-patch on them. This is a bit + ;; brittle, of course. + (let ((magit-section-hidden-default magit-hide-diffs)) + (magit-with-section file 'diff + (delete-region (point) (+ (line-end-position) 1)) + (if (not (magit-section-hidden magit-top-section)) + (magit-insert-diff file status) + (magit-set-section-info (list status file nil)) + (magit-set-section-needs-refresh-on-show t) + (magit-insert-diff-title status file nil))))) + t) nil)) (defun magit-hunk-item-diff (hunk) (let ((diff (magit-section-parent hunk))) (or (eq (magit-section-type diff) 'diff) - (error "Huh? Parent of hunk not a diff.")) + (error "Huh? Parent of hunk not a diff")) diff)) (defun magit-diff-item-insert-header (diff buf) (let ((beg (save-excursion - (goto-char (magit-section-beginning diff)) - (forward-line) - (point))) - (end (if (magit-section-children diff) - (magit-section-beginning (car (magit-section-children diff))) - (magit-section-end diff)))) + (goto-char (magit-section-beginning diff)) + (forward-line) + (point))) + (end (if (magit-section-children diff) + (magit-section-beginning (car (magit-section-children diff))) + (magit-section-end diff)))) (magit-insert-region beg end buf))) (defun magit-insert-diff-item-patch (diff buf) (let ((beg (save-excursion - (goto-char (magit-section-beginning diff)) - (forward-line) - (point))) - (end (magit-section-end diff))) + (goto-char (magit-section-beginning diff)) + (forward-line) + (point))) + (end (magit-section-end diff))) (magit-insert-region beg end buf))) (defun magit-insert-hunk-item-patch (hunk buf) (magit-diff-item-insert-header (magit-hunk-item-diff hunk) buf) (magit-insert-region (magit-section-beginning hunk) (magit-section-end hunk) - buf)) + buf)) (defun magit-insert-hunk-item-region-patch (hunk reverse beg end buf) (magit-diff-item-insert-header (magit-hunk-item-diff hunk) buf) @@ -1724,174 +2819,540 @@ Please see the manual for a complete description of Magit. (forward-line) (let ((copy-op (if reverse "+" "-"))) (while (< (point) (magit-section-end hunk)) - (if (and (<= beg (point)) (< (point) end)) - (magit-insert-current-line buf) - (cond ((looking-at " ") - (magit-insert-current-line buf)) - ((looking-at copy-op) - (let ((text (buffer-substring-no-properties - (+ (point) 1) (line-beginning-position 2)))) - (with-current-buffer buf - (insert " " text)))))) - (forward-line)))) + (if (and (<= beg (point)) (< (point) end)) + (magit-insert-current-line buf) + (cond ((looking-at " ") + (magit-insert-current-line buf)) + ((looking-at copy-op) + (let ((text (buffer-substring-no-properties + (+ (point) 1) (line-beginning-position 2)))) + (with-current-buffer buf + (insert " " text)))))) + (forward-line)))) (with-current-buffer buf (diff-fixup-modifs (point-min) (point-max)))) (defun magit-hunk-item-is-conflict-p (hunk) ;;; XXX - Using the title is a bit too clever... (string-match "^diff --cc" - (magit-section-title (magit-hunk-item-diff hunk)))) + (magit-section-title (magit-hunk-item-diff hunk)))) (defun magit-hunk-item-target-line (hunk) (save-excursion (beginning-of-line) (let ((line (line-number-at-pos))) - (if (looking-at "-") - (error "Can't visit removed lines.")) (goto-char (magit-section-beginning hunk)) - (if (not (looking-at "@@+ .* \\+\\([0-9]+\\),[0-9]+ @@+")) - (error "Hunk header not found.")) - (let ((target (parse-integer (match-string 1)))) - (forward-line) - (while (< (line-number-at-pos) line) - ;; XXX - deal with combined diffs - (if (not (looking-at "-")) - (setq target (+ target 1))) - (forward-line)) - target)))) + (if (not (looking-at "@@+ .* \\+\\([0-9]+\\)\\(,[0-9]+\\)? @@+")) + (error "Hunk header not found")) + (let ((target (string-to-number (match-string 1)))) + (forward-line) + (while (< (line-number-at-pos) line) + ;; XXX - deal with combined diffs + (if (not (looking-at "-")) + (setq target (+ target 1))) + (forward-line)) + target)))) + +(defun magit-show (commit filename &optional select prefix) + "Returns a buffer containing the contents of the file FILENAME, as stored in +COMMIT. COMMIT may be one of the following: + +- A string with the name of a commit, such as \"head\" or \"dae86e\". See 'git + help revisions' for syntax. +- The symbol 'index, indicating that you want the version in Git's index or + staging area. +- The symbol 'working, indicating that you want the version in the working + directory. In this case you'll get a buffer visiting the file. If there's + already a buffer visiting that file, you'll get that one. + +When called interactively or when SELECT is non-nil, make the buffer active, +either in another window or (with a prefix argument) in the current window." + (interactive (let* ((revision (magit-read-rev "Retrieve file from revision")) + (filename (magit-read-file-from-rev revision))) + (list revision filename t current-prefix-arg))) + (if (eq commit 'working) + (find-file-noselect filename) + (let ((buffer (create-file-buffer (format "%s.%s" filename (replace-regexp-in-string ".*/" "" (prin1-to-string commit t)))))) + (cond + ((eq commit 'index) + (let ((checkout-string (magit-git-string "checkout-index" + "--temp" + filename))) + (string-match "^\\(.*\\)\t" checkout-string) + (with-current-buffer buffer + (let ((tmpname (match-string 1 checkout-string))) + (magit-with-silent-modifications + (insert-file-contents tmpname nil nil nil t)) + (delete-file tmpname))))) + (t + (with-current-buffer buffer + (magit-with-silent-modifications + (magit-git-insert (list "cat-file" "-p" + (concat commit ":" filename))))))) + (with-current-buffer buffer + (let ((buffer-file-name filename)) + (normal-mode)) + (goto-char (point-min))) + (if select + (if prefix + (switch-to-buffer buffer) + (switch-to-buffer-other-window buffer)) + buffer)))) + +(defmacro with-magit-tmp-buffer (var &rest body) + (declare (indent 1) + (debug (symbolp &rest form))) + `(let ((,var (generate-new-buffer magit-tmp-buffer-name))) + (unwind-protect + (progn ,@body) + (kill-buffer ,var)))) (defun magit-apply-diff-item (diff &rest args) (when (zerop magit-diff-context-lines) (setq args (cons "--unidiff-zero" args))) - (let ((tmp (get-buffer-create "*magit-tmp*"))) - (with-current-buffer tmp - (erase-buffer)) - (magit-insert-diff-item-patch diff "*magit-tmp*") + (with-magit-tmp-buffer tmp + (magit-insert-diff-item-patch diff tmp) (apply #'magit-run-git-with-input tmp - "apply" (append args (list "-"))))) + "apply" (append args (list "-"))))) (defun magit-apply-hunk-item* (hunk reverse &rest args) - (when (zerop magit-diff-context-lines) - (setq args (cons "--unidiff-zero" args))) - (let ((tmp (get-buffer-create "*magit-tmp*"))) - (with-current-buffer tmp - (erase-buffer)) - (if (magit-use-region-p) - (magit-insert-hunk-item-region-patch - hunk reverse (region-beginning) (region-end) tmp) - (magit-insert-hunk-item-patch hunk tmp)) - (apply #'magit-run-git-with-input tmp - "apply" (append args (list "-"))))) + "Apply single hunk or part of a hunk to the index or working file. + +This function is the core of magit's stage, unstage, apply, and +revert operations. HUNK (or the portion of it selected by the +region) will be applied to either the index, if \"--cached\" is a +member of ARGS, or to the working file otherwise." + (let ((zero-context (zerop magit-diff-context-lines)) + (use-region (magit-use-region-p))) + (when zero-context + (setq args (cons "--unidiff-zero" args))) + (when reverse + (setq args (cons "--reverse" args))) + (when (and use-region zero-context) + (error (concat "Not enough context to partially apply hunk. " + "Use `+' to increase context."))) + (with-magit-tmp-buffer tmp + (if use-region + (magit-insert-hunk-item-region-patch + hunk reverse (region-beginning) (region-end) tmp) + (magit-insert-hunk-item-patch hunk tmp)) + (apply #'magit-run-git-with-input tmp + "apply" (append args (list "-")))))) (defun magit-apply-hunk-item (hunk &rest args) (apply #'magit-apply-hunk-item* hunk nil args)) (defun magit-apply-hunk-item-reverse (hunk &rest args) - (apply #'magit-apply-hunk-item* hunk t (cons "--reverse" args))) - -(defun magit-insert-unstaged-changes (title) - (let ((magit-hide-diffs t)) - (let ((magit-diff-options '())) - (magit-git-section 'unstaged title 'magit-wash-raw-diffs - "diff-files")))) + (apply #'magit-apply-hunk-item* hunk t args)) -(defun magit-insert-staged-changes (no-commit) +(magit-define-inserter unstaged-changes (title) (let ((magit-hide-diffs t) - (base (if no-commit - (magit-git-string "mktree") - "HEAD"))) - (let ((magit-diff-options '("--cached")) - (magit-ignore-unmerged-raw-diffs t)) - (magit-git-section 'staged "Staged changes:" 'magit-wash-raw-diffs - "diff-index" "--cached" - base)))) + (magit-current-diff-range (cons 'index 'working))) + (let ((magit-diff-options (append '() magit-diff-options))) + (magit-git-section 'unstaged title 'magit-wash-raw-diffs + "diff-files")))) + +(magit-define-inserter staged-changes (staged no-commit) + (let ((magit-current-diff-range (cons "HEAD" 'index))) + (when staged + (let ((magit-hide-diffs t) + (base (if no-commit + (magit-git-string "mktree") + "HEAD"))) + (let ((magit-diff-options (append '("--cached") magit-diff-options)) + (magit-ignore-unmerged-raw-diffs t)) + (magit-git-section 'staged "Staged changes:" 'magit-wash-raw-diffs + "diff-index" "--cached" + base)))))) ;;; Logs and Commits -(defun magit-parse-log-ref (refname) - "Return shortened and propertized version of full REFNAME, like -\"refs/remotes/origin/master\"." - (let ((face 'magit-log-head-label)) - (cond ((string-match "^\\(tag: +\\)?refs/tags/\\(.+\\)" refname) - (setq refname (match-string 2 refname) - face 'magit-log-tag-label)) - ((string-match "^refs/remotes/\\(.+\\)" refname) - (setq refname (match-string 1 refname))) - ((string-match "[^/]+$" refname) - (setq refname (match-string 0 refname)))) - (propertize refname 'face face))) - -(defun magit-parse-log-refs (refstring) - "Parse REFSTRING annotation from `git log --decorate' -output (for example: \"refs/remotes/origin/master, -refs/heads/master\") and return prettified string for displaying -in log buffer." - (mapconcat 'identity - (mapcar 'magit-parse-log-ref - (remove-if (lambda (refname) - (string-match "/HEAD$" refname)) - (reverse (split-string refstring ", *" t)))) - " - ")) - -(defun magit-wash-log-line () - (if (and (search-forward-regexp "[0-9a-fA-F]\\{40\\}" (line-end-position) t) - (goto-char (match-beginning 0)) - (not (looking-back "commit "))) - (let ((commit (match-string-no-properties 0))) - (delete-region (match-beginning 0) (match-end 0)) - (fixup-whitespace) - (goto-char (line-beginning-position)) - (when (search-forward-regexp "^[|*\\/ ]+\\((\\(tag:.+?\\|refs/.+?\\))\\)" - (line-end-position) t) - (let ((refstring (match-string-no-properties 2))) - (delete-region (match-beginning 1) (match-end 1)) - (insert (magit-parse-log-refs refstring))) - (goto-char (line-beginning-position))) - (magit-with-section commit 'commit - (magit-set-section-info commit) - (forward-line))) - (forward-line)) - t) + ; Note: making this a plain defcustom would probably let users break + ; the parser too easily +(defvar magit-git-log-options + (list + "--pretty=format:* %h %s" + (format "--abbrev=%s" magit-sha1-abbrev-length))) + ; --decorate=full otherwise some ref prefixes are stripped + ; '("--pretty=format:* %H%d %s" "--decorate=full")) -(defun magit-wash-log () +;; +;; Regexps for parsing ref names +;; +;; see the `git-check-ref-format' manpage for details + +(defconst magit-ref-nonchars "\000-\037\177 ~^:?*[\\" + "Characters specifically disallowed from appearing in Git symbolic refs. + +Evaluate (man \"git-check-ref-format\") for details") + +(defconst magit-ref-nonslash-re + (concat "\\(?:" + ;; "no slash-separated component can begin with a dot ." (rule 1) + "[^" magit-ref-nonchars "./]" + ;; "cannot have two consecutive dots .. anywhere." (rule 3) + "\\.?" + "\\)*") + "Regexp that matches the non-slash parts of a ref name. + +Evaluate (man \"git-check-ref-format\") for details") + +(defconst magit-refname-re + (concat + "\\(?:HEAD\\|" + + "\\(?:tag: \\)?" + + ;; optional non-slash sequence at the beginning + magit-ref-nonslash-re + + ;; any number of slash-prefixed sequences + "\\(?:" + "/" + magit-ref-nonslash-re + "\\)*" + + "/" ;; "must contain at least one /." (rule 2) + magit-ref-nonslash-re + + ;; "cannot end with a slash / nor a dot .." (rule 5) + "[^" magit-ref-nonchars "./]" + + "\\)" + ) + "Regexp that matches a git symbolic reference name. + +Evaluate (man \"git-check-ref-format\") for details") + +(defconst magit-log-oneline-re + (concat + "^\\([_\\*|/ -.]+\\)?" ; graph (1) + "\\(?:" + "\\([0-9a-fA-F]+\\)" ; sha1 (2) + "\\(?:" ; refs (3) + " " + "\\(" + "(" + magit-refname-re "\\(?:, " magit-refname-re "\\)*" + ")" + "\\)" + "\\)?" + "\\)?" + " ?\\(.*\\)$" ; msg (4) + )) + +(defconst magit-log-longline-re + (concat + ;; use \0 delimiter (from -z option) to identify commits. this prevents + ;; commit messages containing lines like "commit 00000" from polluting the + ;; display + "\\(?:\\(?:\\`\\|\0\\)" + "\\([_\\*|/ -.]+\\)?" ; graph (1) + "commit " + "\\([0-9a-fA-F]+\\)" ; sha1 (2) + "\\(?:" ; refs (3) + " " + "\\(" + "(" + magit-refname-re "\\(?:, " magit-refname-re "\\)*" + ")" + "\\)" + "\\)?" + "$\\)?" + " ?\\(.*\\)$" ; msg (4) + )) + +(defvar magit-present-log-line-function 'magit-present-log-line + "The function to use when generating a log line. +It takes four args: CHART, SHA1, REFS and MESSAGE. The function +must return a string which will represent the log line.") + +(defun magit-log-get-bisect-state-color (suffix) + (if (string= suffix "bad") + (list suffix 'magit-log-head-label-bisect-bad) + (list suffix 'magit-log-head-label-bisect-good))) + +(defun magit-log-get-patches-color (suffix) + (list (and (string-match ".+/\\(.+\\)" suffix) + (match-string 1 suffix)) + 'magit-log-head-label-patches)) + +(defvar magit-log-remotes-color-hook nil) + +(defun magit-log-get-remotes-color (suffix) + (or + (run-hook-with-args-until-success + 'magit-log-remotes-color-hook suffix) + (list suffix 'magit-log-head-label-remote))) + +(defvar magit-refs-namespaces + '(("tags" . magit-log-head-label-tags) + ("remotes" magit-log-get-remotes-color) + ("heads" . magit-log-head-label-local) + ("patches" magit-log-get-patches-color) + ("bisect" magit-log-get-bisect-state-color))) + +(defun magit-ref-get-label-color (r) + (let* ((ref-re "\\(?:tag: \\)?refs/\\(?:\\([^/]+\\)/\\)?\\(.+\\)") + (label (and (string-match ref-re r) + (match-string 2 r))) + (res (let ((colorizer + (cdr (assoc (match-string 1 r) + magit-refs-namespaces)))) + (cond ((null colorizer) + (list r 'magit-log-head-label-default)) + ((symbolp colorizer) + (list label colorizer)) + ((listp colorizer) + (funcall (car colorizer) + (match-string 2 r))) + (t + (list r 'magit-log-head-label-default)))))) + res)) + +(defun magit-present-log-line (graph sha1 refs message) + "The default log line generator." + (let ((string-refs + (when refs + (let ((colored-labels + (delete nil + (mapcar (lambda (r) + (destructuring-bind (label face) + (magit-ref-get-label-color r) + (and label + (propertize label 'face face)))) + refs)))) + (concat + (mapconcat 'identity colored-labels " ") + " "))))) + + (concat + (if sha1 + (propertize sha1 'face 'magit-log-sha1) + (insert-char ? magit-sha1-abbrev-length)) + " " + (when graph + (propertize graph 'face 'magit-log-graph)) + string-refs + (when message + (propertize message 'face 'magit-log-message))))) + +(defvar magit-log-count () + "Internal var used to count the number of logs actually added in a buffer.") + +(defmacro magit-create-log-buffer-sections (&rest body) + "Empty current buffer of text and magit's section, and then evaluate BODY. + +if the number of logs inserted in the buffer is `magit-log-cutoff-length' +insert a line to tell how to insert more of them" + (declare (indent 0)) + `(let ((magit-log-count 0) (inhibit-read-only t)) + (magit-create-buffer-sections + (magit-with-section 'log nil + ,@body + (if (= magit-log-count magit-log-cutoff-length) + (magit-with-section "longer" 'longer + (insert "type \"e\" to show more logs\n"))))))) + +(defun magit-wash-log-line (style) + (beginning-of-line) + (let ((line-re (cond ((eq style 'long) magit-log-longline-re) + (t magit-log-oneline-re)))) + (cond + ((looking-at line-re) + (let ((chart (match-string 1)) + (sha1 (match-string 2)) + (msg (match-string 4)) + (refs (when (match-string 3) + (delq nil + (mapcar + (lambda (s) + (and (not + (or (string= s "tag:") + (string= s "HEAD"))) ; as of 1.6.6 + s)) + (split-string (match-string 3) "[(), ]" t)))))) + (delete-region (point-at-bol) (point-at-eol)) + (insert (funcall magit-present-log-line-function chart sha1 refs msg)) + (goto-char (point-at-bol)) + (if sha1 + (magit-with-section sha1 'commit + (when magit-log-count (setq magit-log-count (1+ magit-log-count))) + (magit-set-section-info sha1) + (forward-line)) + (forward-line)))) + (t + (forward-line))) + t)) + +(defun magit-wash-log (&optional style) (let ((magit-old-top-section nil)) - (magit-wash-sequence #'magit-wash-log-line))) + (magit-wash-sequence (apply-partially 'magit-wash-log-line style)))) (defvar magit-currently-shown-commit nil) (defun magit-wash-commit () - (cond ((search-forward-regexp "^diff" nil t) - (goto-char (match-beginning 0)) - (magit-wash-diffs)))) + (let ((magit-current-diff-range)) + (when (looking-at "^commit \\([0-9a-fA-F]\\{40\\}\\)") + (setq magit-current-diff-range (match-string 1)) + (add-text-properties (match-beginning 1) (match-end 1) + '(face magit-log-sha1))) + (cond + ((search-forward-regexp "^Merge: \\([0-9a-fA-F]+\\) \\([0-9a-fA-F]+\\)$" nil t) + (setq magit-current-diff-range (cons (cons (match-string 1) + (match-string 2)) + magit-current-diff-range)) + (let ((first (magit-set-section nil 'commit (match-beginning 1) (match-end 1))) + (second (magit-set-section nil 'commit (match-beginning 2) (match-end 2)))) + (magit-set-section-info (match-string 1) first) + (magit-set-section-info (match-string 2) second)) + (make-commit-button (match-beginning 1) (match-end 1)) + (make-commit-button (match-beginning 2) (match-end 2))) + (t + (setq magit-current-diff-range (cons (concat magit-current-diff-range "^") + magit-current-diff-range)))) + (search-forward-regexp "^$") + (while (and + (search-forward-regexp "\\(\\b[0-9a-fA-F]\\{4,40\\}\\b\\)\\|\\(^diff\\)" nil 'noerror) + (not (match-string 2))) + (let ((sha1 (match-string 1)) + (start (match-beginning 1)) + (end (match-end 1))) + (when (string-equal "commit" (magit-git-string "cat-file" "-t" sha1)) + (make-commit-button start end) + (let ((section (magit-set-section sha1 'commit start end))) + (magit-set-section-info sha1 section))))) + (beginning-of-line) + (when (looking-at "^diff") + (magit-wash-diffs)) + (goto-char (point-max)) + (insert "\n") + (if magit-back-navigation-history + (magit-with-section "[back]" 'button + (insert-text-button "[back]" + 'help-echo "Previous commit" + 'action 'magit-show-commit-backward + 'follow-link t + 'mouse-face 'magit-item-highlight))) + (insert " ") + (if magit-forward-navigation-history + (magit-with-section "[forward]" 'button + (insert-text-button "[forward]" + 'help-echo "Next commit" + 'action 'magit-show-commit-forward + 'follow-link t + 'mouse-face 'magit-item-highlight))))) + +(defun make-commit-button (start end) + (make-text-button start end + 'help-echo "Visit commit" + 'action (lambda (button) + (save-excursion + (goto-char button) + (magit-visit-item))) + 'follow-link t + 'mouse-face 'magit-item-highlight + 'face 'magit-log-sha1)) (defun magit-refresh-commit-buffer (commit) + (magit-configure-have-abbrev) + (magit-configure-have-decorate) (magit-create-buffer-sections - (magit-git-section nil nil - 'magit-wash-commit - "log" "--max-count=1" - "--pretty=medium" - "--cc" "-p" commit))) + (apply #'magit-git-section nil nil + 'magit-wash-commit + "log" + "--max-count=1" + "--pretty=medium" + `(,@(if magit-have-abbrev (list "--no-abbrev-commit")) + ,@(if magit-have-decorate (list "--decorate=full")) + "--cc" + "-p" ,commit)))) + +(define-derived-mode magit-commit-mode magit-mode "Magit" + "Mode to view a git commit. + +\\{magit-commit-mode-map}" + :group 'magit) + +(defvar magit-commit-buffer-name "*magit-commit*" + "Buffer name for displaying commit log messages.") + +(defun magit-show-commit (commit &optional scroll inhibit-history select) + "Show information about a commit in the buffer named by +`magit-commit-buffer-name'. COMMIT can be any valid name for a commit +in the current Git repository. + +When called interactively or when SELECT is non-nil, switch to +the commit buffer using `pop-to-buffer'. -(defun magit-show-commit (commit &optional scroll) +Unless INHIBIT-HISTORY is non-nil, the commit currently shown +will be pushed onto `magit-back-navigation-history' and +`magit-forward-navigation-history' will be cleared. + +Noninteractively, if the commit is already displayed and SCROLL +is provided, call SCROLL's function definition in the commit +window. (`scroll-up' and `scroll-down' are typically passed in +for this argument.)" + (interactive (list (magit-read-rev "Show commit (hash or ref)") + nil nil t)) (when (magit-section-p commit) (setq commit (magit-section-info commit))) + (unless (eql 0 (magit-git-exit-code "cat-file" "commit" commit)) + (error "%s is not a commit" commit)) (let ((dir default-directory) - (buf (get-buffer-create "*magit-commit*"))) - (cond ((equal magit-currently-shown-commit commit) - (let ((win (get-buffer-window buf))) - (cond ((not win) - (display-buffer buf)) - (scroll - (with-selected-window win - (funcall scroll)))))) - (t - (setq magit-currently-shown-commit commit) - (display-buffer buf) - (with-current-buffer buf - (set-buffer buf) - (goto-char (point-min)) - (magit-mode-init dir 'commit - #'magit-refresh-commit-buffer commit)))))) + (buf (get-buffer-create magit-commit-buffer-name))) + (cond + ((and (equal magit-currently-shown-commit commit) + ;; if it's empty then the buffer was killed + (with-current-buffer buf + (> (length (buffer-string)) 1))) + (let ((win (get-buffer-window buf))) + (cond ((not win) + (display-buffer buf)) + (scroll + (with-selected-window win + (funcall scroll)))))) + (commit + (display-buffer buf) + (with-current-buffer buf + (unless inhibit-history + (push (cons default-directory magit-currently-shown-commit) + magit-back-navigation-history) + (setq magit-forward-navigation-history nil)) + (setq magit-currently-shown-commit commit) + (goto-char (point-min)) + (magit-mode-init dir 'magit-commit-mode + #'magit-refresh-commit-buffer commit)))) + (if select + (pop-to-buffer buf)))) + +(defun magit-show-commit-backward (&optional ignored) + ;; Ignore argument passed by push-button + "Show the commit at the head of `magit-back-navigation-history in +`magit-commit-buffer-name`." + (interactive) + (with-current-buffer magit-commit-buffer-name + (unless magit-back-navigation-history + (error "No previous commit.")) + (let ((histitem (pop magit-back-navigation-history))) + (push (cons default-directory magit-currently-shown-commit) + magit-forward-navigation-history) + (setq default-directory (car histitem)) + (magit-show-commit (cdr histitem) nil 'inhibit-history)))) + +(defun magit-show-commit-forward (&optional ignored) + ;; Ignore argument passed by push-button + "Show the commit at the head of `magit-forward-navigation-history in +`magit-commit-buffer-name`." + (interactive) + (with-current-buffer magit-commit-buffer-name + (unless magit-forward-navigation-history + (error "No next commit.")) + (let ((histitem (pop magit-forward-navigation-history))) + (push (cons default-directory magit-currently-shown-commit) + magit-back-navigation-history) + (setq default-directory (car histitem)) + (magit-show-commit (cdr histitem) nil 'inhibit-history)))) (defvar magit-marked-commit nil) @@ -1905,18 +3366,18 @@ in log buffer." (defun magit-refresh-marked-commits-in-buffer () (if (not magit-mark-overlay) (let ((ov (make-overlay 1 1))) - (overlay-put ov 'face 'magit-item-mark) - (setq magit-mark-overlay ov))) + (overlay-put ov 'face 'magit-item-mark) + (setq magit-mark-overlay ov))) (delete-overlay magit-mark-overlay) (magit-for-all-sections (lambda (section) (when (and (eq (magit-section-type section) 'commit) - (equal (magit-section-info section) - magit-marked-commit)) + (equal (magit-section-info section) + magit-marked-commit)) (move-overlay magit-mark-overlay - (magit-section-beginning section) - (magit-section-end section) - (current-buffer)))))) + (magit-section-beginning section) + (magit-section-end section) + (current-buffer)))))) (defun magit-set-marked-commit (commit) (setq magit-marked-commit commit) @@ -1926,145 +3387,243 @@ in log buffer." (or magit-marked-commit (error "No commit marked"))) -(defun magit-insert-unpulled-commits (remote branch) - (magit-git-section 'unpulled - "Unpulled commits:" 'magit-wash-log - "log" "--pretty=format:* %H %s" - (format "HEAD..%s/%s" remote branch))) - -(defun magit-insert-unpushed-commits (remote branch) - (magit-git-section 'unpushed - "Unpushed commits:" 'magit-wash-log - "log" "--pretty=format:* %H %s" - (format "%s/%s..HEAD" remote branch))) - -(defun magit-insert-unpulled-svn-commits () - (magit-git-section 'svn-unpulled - "Unpulled commits (SVN):" 'magit-wash-log - "log" "--pretty=format:* %H %s" - (format "HEAD..%s" (magit-get-svn-ref)))) - -(defun magit-insert-unpushed-svn-commits () - (magit-git-section 'svn-unpushed - "Unpushed commits (SVN):" 'magit-wash-log - "log" "--pretty=format:* %H %s" - (format "%s..HEAD" (magit-get-svn-ref)))) +(defun magit-remote-branch-name (remote branch) + "Get the name of the branch BRANCH on remote REMOTE" + (if (string= remote ".") + branch + (concat remote "/" branch))) + +(magit-define-inserter unpulled-commits (remote branch) + (when remote + (apply #'magit-git-section + 'unpulled "Unpulled commits:" 'magit-wash-log "log" + (append magit-git-log-options + (list + (format "HEAD..%s" (magit-remote-branch-name remote branch))))))) + +(magit-define-inserter unpushed-commits (remote branch) + (when remote + (apply #'magit-git-section + 'unpushed "Unpushed commits:" 'magit-wash-log "log" + (append magit-git-log-options + (list + (format "%s..HEAD" (magit-remote-branch-name remote branch))))))) + +(defun magit-remote-branch-for (local-branch &optional prepend-remote-name) + "Guess the remote branch name that LOCAL-BRANCH is tracking. +Prepend \"remotes/\", the remote's name and \"/\" if +PREPEND-REMOTE-NAME is non-nil." + (let ((merge (magit-get "branch" local-branch "merge"))) + (save-match-data + (if (and merge (string-match "^refs/heads/\\(.+\\)" merge)) + (concat (if prepend-remote-name + (concat "remotes/" + (magit-get "branch" local-branch "remote") + "/")) + (match-string 1 merge)))))) ;;; Status +(defvar magit-remote-string-hook nil) + +(defun magit-remote-string (remote remote-branch) + (cond + ((string= "." remote) + (format "branch %s" + (propertize remote-branch 'face 'magit-branch))) + (remote + (concat + (propertize remote-branch 'face 'magit-branch) + " @ " + remote + " (" + (magit-get "remote" remote "url") + ")")) + (t + (run-hook-with-args-until-success 'magit-remote-string-hook)))) + +(declare-function magit--bisect-info-for-status "magit-bisect" (branch)) + (defun magit-refresh-status () (magit-create-buffer-sections (magit-with-section 'status nil (let* ((branch (magit-get-current-branch)) - (remote (and branch (magit-get "branch" branch "remote"))) - (svn-enabled (magit-svn-enabled)) - (head (magit-git-string - "log" "--max-count=1" "--abbrev-commit" "--pretty=oneline")) - (no-commit (not head))) - (if remote - (insert (format "Remote: %s %s\n" - remote (magit-get "remote" remote "url")))) - (insert (format "Local: %s %s\n" - (propertize (or branch "(detached)") - 'face 'magit-branch) - (abbreviate-file-name default-directory))) - (insert (format "Head: %s\n" + (remote (and branch (magit-get "branch" branch "remote"))) + (remote-branch (or (and branch (magit-remote-branch-for branch)) branch)) + (remote-string (magit-remote-string remote remote-branch)) + (head (magit-git-string + "log" + "--max-count=1" + "--abbrev-commit" + (format "--abbrev=%s" magit-sha1-abbrev-length) + "--pretty=oneline")) + (no-commit (not head))) + (when remote-string + (insert "Remote: " remote-string "\n")) + (insert (format "Local: %s %s\n" + (propertize (magit--bisect-info-for-status branch) + 'face 'magit-branch) + (abbreviate-file-name default-directory))) + (insert (format "Head: %s\n" (if no-commit "nothing commited (yet)" head))) - (let ((merge-heads (magit-file-lines ".git/MERGE_HEAD"))) - (if merge-heads - (insert (format "Merging: %s\n" - (magit-concat-with-delim - ", " - (mapcar 'magit-name-rev merge-heads)))))) - (let ((rebase (magit-rebase-info))) - (if rebase - (insert (apply 'format "Rebasing: %s (%s of %s)\n" rebase)))) - (insert "\n") + (let ((merge-heads (magit-file-lines ".git/MERGE_HEAD"))) + (if merge-heads + (insert (format "Merging: %s\n" + (mapconcat 'identity + (mapcar 'magit-name-rev merge-heads) + ", "))))) + (let ((rebase (magit-rebase-info))) + (if rebase + (insert (apply 'format "Rebasing: onto %s (%s of %s); Press \"R\" to Abort, Skip, or Continue\n" rebase)))) + (insert "\n") (magit-git-exit-code "update-index" "--refresh") - (magit-insert-untracked-files) - (magit-insert-stashes) - (magit-insert-topics) - (magit-insert-pending-changes) - (magit-insert-pending-commits) - (when remote - (magit-insert-unpulled-commits remote branch)) - (when svn-enabled - (magit-insert-unpulled-svn-commits)) - (let ((staged (or no-commit (magit-anything-staged-p)))) - (magit-insert-unstaged-changes - (if staged "Unstaged changes:" "Changes:")) - (if staged - (magit-insert-staged-changes no-commit))) - (when remote - (magit-insert-unpushed-commits remote branch)) - (when svn-enabled - (magit-insert-unpushed-svn-commits)))))) + (magit-insert-stashes) + (magit-insert-untracked-files) + (magit-insert-pending-changes) + (magit-insert-pending-commits) + (magit-insert-unpulled-commits remote remote-branch) + (let ((staged (or no-commit (magit-anything-staged-p)))) + (magit-insert-unstaged-changes + (if staged "Unstaged changes:" "Changes:")) + (magit-insert-staged-changes staged no-commit)) + (magit-insert-unpushed-commits remote remote-branch) + (run-hooks 'magit-refresh-status-hook))))) (defun magit-init (dir) - "Initialize git repository in specified directory" + "Initialize git repository in the DIR directory." (interactive (list (read-directory-name "Directory for Git repository: "))) (let ((topdir (magit-get-top-dir dir))) (when (or (not topdir) - (yes-or-no-p - (format - (if (string-equal topdir (expand-file-name dir)) - "There is already a Git repository in %s. Reinitialize? " - "There is a Git repository in %s. Create another in %s? ") - topdir dir))) + (yes-or-no-p + (format + (if (string-equal topdir (expand-file-name dir)) + "There is already a Git repository in %s. Reinitialize? " + "There is a Git repository in %s. Create another in %s? ") + topdir dir))) (unless (file-directory-p dir) - (and (y-or-n-p (format "Directory %s does not exists. Create it? " dir)) - (make-directory dir))) - (let ((default-directory dir)) - (magit-run* (list "git" "init")))))) + (and (y-or-n-p (format "Directory %s does not exists. Create it? " dir)) + (make-directory dir))) + (magit-run* (list magit-git-executable "init" dir))))) + +(define-derived-mode magit-status-mode magit-mode "Magit" + "Mode for looking at git status. + +\\{magit-status-mode-map}" + :group 'magit) + +(defvar magit-default-directory nil) + +(defun magit-save-some-buffers (&optional msg pred) + "Save some buffers if variable `magit-save-some-buffers' is non-nil. +If variable `magit-save-some-buffers' is set to 'dontask then +don't ask the user before saving the buffers, just go ahead and +do it. + +Optional argument MSG is displayed in the minibuffer if variable +`magit-save-some-buffers' is nil. + +Optional second argument PRED determines which buffers are considered: +If PRED is nil, all the file-visiting buffers are considered. +If PRED is t, then certain non-file buffers will also be considered. +If PRED is a zero-argument function, it indicates for each buffer whether +to consider it or not when called with that buffer current." + (interactive) + (let ((predicate-function (or pred magit-save-some-buffers-predicate)) + (magit-default-directory default-directory)) + (if magit-save-some-buffers + (save-some-buffers + (eq magit-save-some-buffers 'dontask) + predicate-function) + (when msg + (message msg))))) + +(defun magit-save-buffers-predicate-all () + "Prompt to save all buffers with unsaved changes" + t) + +(defun magit-save-buffers-predicate-tree-only () + "Only prompt to save buffers which are within the current git project (as + determined by the dir passed to `magit-status'." + (and buffer-file-name + (string= (magit-get-top-dir magit-default-directory) + (magit-get-top-dir (file-name-directory buffer-file-name))))) ;;;###autoload (defun magit-status (dir) - (interactive (list (or (and (not current-prefix-arg) - (magit-get-top-dir default-directory)) - (magit-read-top-dir (and (consp current-prefix-arg) - (> (car current-prefix-arg) 4)))))) - (if magit-save-some-buffers - (save-some-buffers (eq magit-save-some-buffers 'dontask))) + "Open a Magit status buffer for the Git repository containing +DIR. If DIR is not within a Git repository, offer to create a +Git repository in DIR. + +Interactively, a prefix argument means to ask the user which Git +repository to use even if `default-directory' is under Git control. +Two prefix arguments means to ignore `magit-repo-dirs' when asking for +user input." + (interactive (list (if current-prefix-arg + (magit-read-top-dir + (> (prefix-numeric-value current-prefix-arg) + 4)) + (or (magit-get-top-dir default-directory) + (magit-read-top-dir nil))))) + (magit-save-some-buffers) (let ((topdir (magit-get-top-dir dir))) (unless topdir - (when (y-or-n-p (format "There is no Git repository in %S. Create one? " - dir)) - (magit-init dir) - (setq topdir (magit-get-top-dir dir)))) + (when (y-or-n-p (format "There is no Git repository in %S. Create one? " + dir)) + (magit-init dir) + (setq topdir (magit-get-top-dir dir)))) (when topdir - (let ((buf (or (magit-find-buffer 'status topdir) - (switch-to-buffer - (get-buffer-create - (concat "*magit: " - (file-name-nondirectory - (directory-file-name topdir)) "*")))))) - (switch-to-buffer buf) - (magit-mode-init topdir 'status #'magit-refresh-status))))) + (let ((buf (or (magit-find-status-buffer topdir) + (generate-new-buffer + (concat "*magit: " + (file-name-nondirectory + (directory-file-name topdir)) "*"))))) + (funcall magit-status-buffer-switch-function buf) + (magit-mode-init topdir 'magit-status-mode #'magit-refresh-status))))) + +(magit-define-command automatic-merge (revision) + "Merge REVISION into the current 'HEAD'; commit unless merge fails. +\('git merge REVISION')." + (interactive (list (magit-read-rev "Merge" (magit-guess-branch)))) + (if revision + (magit-run-git "merge" (magit-rev-to-git revision)))) ;;; Staging and Unstaging -(defun magit-stage-item () - "Add the item at point to the staging area." - (interactive) - (magit-section-action (item info "stage") - ((untracked file) - (magit-run-git "add" info)) - ((untracked) - (apply #'magit-run-git "add" "--" - (magit-git-lines "ls-files" "--other" "--exclude-standard"))) - ((unstaged diff hunk) - (if (magit-hunk-item-is-conflict-p item) - (error (concat "Can't stage individual resolution hunks. " - "Please stage the whole file."))) - (magit-apply-hunk-item item "--cached")) - ((unstaged diff) - (magit-run-git "add" "-u" (magit-diff-item-file item))) - ((staged *) - (error "Already staged")) - ((hunk) - (error "Can't stage this hunk")) - ((diff) - (error "Can't stage this diff")))) +(defun magit-stage-item (&optional ask) + "Add the item at point to the staging area. +If ASK is set, ask for the file name rather than picking the one +at point." + (interactive "P") + (if ask + (magit-run-git "add" (read-file-name "File to stage: ")) + (magit-section-action (item info "stage") + ((untracked file) + (magit-run-git "add" info)) + ((untracked) + (apply #'magit-run-git "add" "--" + (magit-git-lines "ls-files" "--other" "--exclude-standard"))) + ((unstaged diff hunk) + (if (magit-hunk-item-is-conflict-p item) + (error (concat "Can't stage individual resolution hunks. " + "Please stage the whole file."))) + (magit-apply-hunk-item item "--cached")) + ((unstaged diff) + (magit-run-git "add" "-u" (magit-diff-item-file item))) + ((staged *) + (error "Already staged")) + ((diff diff) + (save-excursion + (magit-goto-parent-section) + (magit-stage-item))) + ((diff diff hunk) + (save-excursion + (magit-goto-parent-section) + (magit-goto-parent-section) + (magit-stage-item))) + ((hunk) + (error "Can't stage this hunk")) + ((diff) + (error "Can't stage this diff"))))) (defun magit-unstage-item () "Remove the item at point from the staging area." @@ -2074,153 +3633,294 @@ in log buffer." (magit-apply-hunk-item-reverse item "--cached")) ((staged diff) (if (eq (car info) 'unmerged) - (error "Can't unstage a unmerged file. Resolve it first.")) - (magit-run-git "reset" "-q" "HEAD" "--" (magit-diff-item-file item))) + (error "Can't unstage an unmerged file. Resolve it first")) + (if (magit-no-commit-p) + (magit-run-git "rm" "--cached" "--" (magit-diff-item-file item)) + (magit-run-git "reset" "-q" "HEAD" "--" (magit-diff-item-file item)))) ((unstaged *) (error "Already unstaged")) + ((diff diff) + (save-excursion + (magit-goto-parent-section) + (magit-unstage-item))) + ((diff diff hunk) + (save-excursion + (magit-goto-parent-section) + (magit-goto-parent-section) + (magit-unstage-item))) ((hunk) (error "Can't unstage this hunk")) ((diff) (error "Can't unstage this diff")))) (defun magit-stage-all (&optional also-untracked-p) + "Add all remaining changes in tracked files to staging area. +With prefix argument, add remaining untracked files as well. +\('git add -u .' or 'git add .', respectively)." (interactive "P") (if also-untracked-p (magit-run-git "add" ".") (magit-run-git "add" "-u" "."))) (defun magit-unstage-all () + "Remove all changes from staging area. +\('git reset --mixed HEAD')." (interactive) (magit-run-git "reset" "HEAD")) ;;; Branches +(defun escape-branch-name (branch) + "Escapes branch names to remove problematic characters." + (replace-regexp-in-string "[/]" "-" branch)) + +(defun magit-default-tracking-name-remote-plus-branch + (remote branch) + "Use the remote name plus a hyphen plus the escaped branch name for tracking branches." + (concat remote "-" (escape-branch-name branch))) + +(defun magit-default-tracking-name-branch-only + (remote branch) + "Use just the escaped branch name for tracking branches." + (escape-branch-name branch)) + +(defun magit-get-tracking-name (remote branch) + "Given a REMOTE and a BRANCH name, ask the user for a local +tracking brach name suggesting a sensible default." + (when (yes-or-no-p + (format "Create local tracking branch for %s? " branch)) + (let* ((default-name + (funcall magit-default-tracking-name-function remote branch)) + (chosen-name (read-string (format "Call local branch (%s): " default-name) + nil + nil + default-name))) + (when (magit-ref-exists-p (concat "refs/heads/" chosen-name)) + (error "'%s' already exists." chosen-name)) + chosen-name))) + (defun magit-maybe-create-local-tracking-branch (rev) - (if (string-match "^refs/remotes/\\([^/]+\\)/\\(.+\\)" rev) - (let ((remote (match-string 1 rev)) - (branch (match-string 2 rev))) - (when (and (not (magit-ref-exists-p (concat "refs/heads/" branch))) - (yes-or-no-p - (format "Create local tracking branch for %s? " branch))) - (magit-run-git "checkout" "-b" branch rev) - t)) + "Depending on the users wishes, create a tracking branch for +rev... maybe." + (if (string-match "^\\(?:refs/\\)?remotes/\\([^/]+\\)/\\(.+\\)" rev) + (let* ((remote (match-string 1 rev)) + (branch (match-string 2 rev)) + (tracker-name (magit-get-tracking-name remote branch))) + (when tracker-name + (magit-run-git "checkout" "-b" tracker-name rev) + t)) nil)) -(defun magit-checkout (rev) - (interactive (list (magit-read-rev "Switch to" (magit-default-rev)))) - (if rev - (if (not (magit-maybe-create-local-tracking-branch rev)) - (magit-run-git "checkout" (magit-rev-to-git rev))))) +(magit-define-command checkout (revision) + "Switch 'HEAD' to REVISION and update working tree. +Fails if working tree or staging area contain uncommitted changes. +If REVISION is a remote branch, offer to create a local tracking branch. +\('git checkout [-b] REVISION')." + (interactive + (list (let ((current-branch (magit-get-current-branch)) + (default (magit-default-rev))) + (magit-read-rev "Switch to" + (unless (string= current-branch default) + default) + (if current-branch + (cons (concat "refs/heads/" current-branch "$") + magit-uninteresting-refs) + magit-uninteresting-refs))))) + (if revision + (when (not (magit-maybe-create-local-tracking-branch revision)) + (magit-save-some-buffers) + (magit-run-git "checkout" (magit-rev-to-git revision)) + (magit-update-vc-modeline default-directory)))) (defun magit-read-create-branch-args () (let* ((cur-branch (magit-get-current-branch)) - (branch (read-string "Create branch: ")) - (parent (magit-read-rev "Parent" cur-branch))) + (cur-point (magit-default-rev)) + (branch (read-string "Create branch: ")) + (parent (magit-read-rev "Parent" + (cond + ((eq magit-create-branch-behaviour 'at-point) cur-point) + ((eq magit-create-branch-behaviour 'at-head) cur-branch) + (t cur-branch))))) (list branch parent))) -(defun magit-create-branch (branch parent) +(magit-define-command create-branch (branch parent) + "Switch 'HEAD' to new BRANCH at revision PARENT and update working tree. +Fails if working tree or staging area contain uncommitted changes. +\('git checkout -b BRANCH REVISION')." (interactive (magit-read-create-branch-args)) - (if (and branch (not (string= branch "")) - parent) - (magit-run-git "checkout" "-b" - branch - (magit-rev-to-git parent)))) - -;;; Merging + (when (and branch (not (string= branch "")) + parent) + (magit-save-some-buffers) + (magit-run-git "checkout" "-b" + branch + (append + magit-custom-options + (magit-rev-to-git parent))) + (magit-update-vc-modeline default-directory))) + +(defun magit-delete-branch (branch) + "Asks for a branch and deletes it. +If the branch is the current one, offers to switch to `master' first. +\('git branch -d BRANCH')." + (interactive (list (magit-read-rev "Branch to delete" (magit-default-rev)))) + (when (and branch (string= branch (magit-get-current-branch))) + (if (y-or-n-p "Cannot delete current branch. Switch to master first? ") + (magit-checkout "master") + (setq branch nil))) + (when branch + (magit-run-git "branch" "-d" (append magit-custom-options + (magit-rev-to-git branch))))) + +(defun magit-delete-branch-forced (branch) + "Asks for a branch and deletes it, irrespective of its merged status. +If the branch is the current one, offers to switch to `master' first. +\('git branch -D BRANCH')." + (interactive (list (magit-read-rev "Branch to force delete" (magit-default-rev)))) + (when (and branch (string= branch (magit-get-current-branch))) + (if (y-or-n-p "Cannot delete current branch. Switch to master first? ") + (magit-checkout "master") + (setq branch nil))) + (when branch + (magit-run-git "branch" "-D" (append magit-custom-options + (magit-rev-to-git branch))))) + +(defun magit-move-branch (old new) + "Renames or moves a branch. +\('git branch -m OLD NEW')." + (interactive (list (magit-read-rev "Old name" (magit-default-rev)) + (read-string "New name: "))) + (magit-run-git "branch" "-m" (magit-rev-to-git old) new)) (defun magit-guess-branch () - (let ((sec (magit-current-section))) - (if (and sec (eq (magit-section-type sec) 'wazzup)) - (magit-section-info sec)))) + (magit-section-case (item info) + ((wazzup commit) + (magit-section-info (magit-section-parent item))) + ((commit) (magit-name-rev (substring info 0 magit-sha1-abbrev-length))) + ((wazzup) info))) -(defun magit-manual-merge (rev) - (interactive (list (magit-read-rev "Manually merge" (magit-guess-branch)))) - (if rev - (magit-run-git "merge" "--no-ff" "--no-commit" - (magit-rev-to-git rev)))) +;;; Merging -(defun magit-automatic-merge (rev) - (interactive (list (magit-read-rev "Merge" (magit-guess-branch)))) - (if rev - (magit-run-git "merge" (magit-rev-to-git rev)))) +(defun magit-merge (revision) + "Merge REVISION into the current 'HEAD'; leave changes uncommitted. +With a prefix-arg, the merge will be squashed. +\('git merge --no-commit [--squash|--no-ff] REVISION')." + (interactive + (list (magit-read-rev "Merge" (magit-default-rev)))) + (if revision + (apply 'magit-run-git + "merge" + (magit-rev-to-git revision) + magit-custom-options))) ;;; Rebasing (defun magit-rebase-info () - (cond ((file-exists-p ".git/rebase-apply") - (list (magit-name-rev - (car (magit-file-lines ".git/rebase-apply/onto"))) - (car (magit-file-lines ".git/rebase-apply/next")) - (car (magit-file-lines ".git/rebase-apply/last")))) - ((file-exists-p ".dotest") - (list (magit-name-rev (car (magit-file-lines ".dotest/onto"))) - (car (magit-file-lines ".dotest/next")) - (car (magit-file-lines ".dotest/last")))) - ((file-exists-p ".git/.dotest-merge") - (list (car (magit-file-lines ".git/.dotest-merge/onto_name")) - (car (magit-file-lines ".git/.dotest-merge/msgnum")) - (car (magit-file-lines ".git/.dotest-merge/end")))) - (t - nil))) + "Returns a list indicating the state of an in-progress rebase, +if any." + (cond ((file-exists-p ".git/rebase-merge") + (list + ;; The commit we're rebasing onto, i.e. git rebase -i <onto> + (magit-name-rev (car (magit-file-lines ".git/rebase-merge/onto"))) + + ;; How many commits we've gone through + (length (magit-file-lines ".git/rebase-merge/done")) + + ;; How many commits we have in total, without the comments + ;; at the end of git-rebase-todo.backup + (let ((todo-lines-with-comments (magit-file-lines ".git/rebase-merge/git-rebase-todo.backup"))) + (loop for i in todo-lines-with-comments + until (string= "" i) + count i)))) + ((and (file-exists-p ".git/rebase-apply") + (file-exists-p ".git/rebase-apply/onto")) + ;; we might be here because a non-interactive rebase failed: the + ;; patches didn't apply cleanly + (list + ;; The commit we're rebasing onto, i.e. git rebase -i <onto> + (magit-name-rev (car (magit-file-lines ".git/rebase-apply/onto"))) + + ;; How many commits we've gone through + (- (string-to-number (car (magit-file-lines ".git/rebase-apply/next"))) 1) + + ;; How many commits we have in total + (string-to-number (car (magit-file-lines ".git/rebase-apply/last"))) + )) + (t nil))) (defun magit-rebase-step () (interactive) (let ((info (magit-rebase-info))) (if (not info) - (let ((rev (magit-read-rev "Rebase to"))) - (if rev - (magit-run-git "rebase" (magit-rev-to-git rev)))) + (let* ((current-branch (magit-get-current-branch)) + (remote (when current-branch + (magit-get "branch" current-branch "remote"))) + (remote-branch (when remote + (magit-get "branch" current-branch "merge"))) + (rev (magit-read-rev "Rebase to" + (when (and remote-branch + (string-match "refs/heads/\\(.*\\)" remote-branch)) + (concat (match-string 1 remote-branch) + " (" remote ")")) + (if current-branch + (cons (concat "refs/heads/" current-branch) + magit-uninteresting-refs) + magit-uninteresting-refs)))) + (if rev + (magit-run-git "rebase" (magit-rev-to-git rev)))) (let ((cursor-in-echo-area t) - (message-log-max nil)) - (message "Rebase in progress. Abort, Skip, or Continue? ") - (let ((reply (read-event))) - (case reply - ((?A ?a) - (magit-run-git "rebase" "--abort")) - ((?S ?s) - (magit-run-git "rebase" "--skip")) - ((?C ?c) - (magit-run-git "rebase" "--continue")))))))) - -;; git svn commands - -(defun magit-svn-rebase () - (interactive) - (magit-run-git-async "svn" "rebase")) - -(defun magit-svn-dcommit () - (interactive) - (magit-run-git-async "svn" "dcommit")) - -(defun magit-svn-enabled () - (not (null (magit-get-svn-ref)))) - -(defun magit-get-svn-ref () - (cond ((magit-ref-exists-p "refs/remotes/git-svn") - "refs/remotes/git-svn") - ((magit-ref-exists-p "refs/remotes/trunk") - "refs/remotes/trunk") - (t - nil))) + (message-log-max nil)) + (message "Rebase in progress. [A]bort, [S]kip, or [C]ontinue? ") + (let ((reply (read-event))) + (case reply + ((?A ?a) + (magit-run-git-async "rebase" "--abort")) + ((?S ?s) + (magit-run-git-async "rebase" "--skip")) + ((?C ?c) + (magit-run-git-async "rebase" "--continue")))))))) ;;; Resetting -(defun magit-reset-head (rev &optional hard) +(magit-define-command reset-head (revision &optional hard) + "Switch 'HEAD' to REVISION, keeping prior working tree and staging area. +Any differences from REVISION become new changes to be committed. +With prefix argument, all uncommitted changes in working tree +and staging area are lost. +\('git reset [--soft|--hard] REVISION')." (interactive (list (magit-read-rev (format "%s head to" - (if current-prefix-arg - "Hard reset" - "Reset")) - (or (magit-default-rev) - "HEAD^")) - current-prefix-arg)) - (if rev - (magit-run-git "reset" (if hard "--hard" "--soft") - (magit-rev-to-git rev)))) - -(defun magit-reset-working-tree () - (interactive) - (if (yes-or-no-p "Discard all uncommitted changes? ") - (magit-run-git "reset" "--hard"))) + (if current-prefix-arg + "Hard reset" + "Reset")) + (or (magit-default-rev) + "HEAD^")) + current-prefix-arg)) + (when revision + (magit-run-git "reset" (if hard "--hard" "--soft") + (magit-rev-to-git revision)) + (magit-update-vc-modeline default-directory))) + +(magit-define-command reset-head-hard (revision) + "Switch 'HEAD' to REVISION, losing all changes. +Uncomitted changes in both working tree and staging area are lost. +\('git reset --hard REVISION')." + (interactive (list (magit-read-rev (format "Hard reset head to") + (or (magit-default-rev) + "HEAD")))) + (magit-reset-head revision t)) + +(magit-define-command reset-working-tree (&optional include-untracked) + "Revert working tree and clear changes from staging area. +\('git reset --hard HEAD'). + +With a prefix arg, also remove untracked files." + (interactive "P") + (when (yes-or-no-p (format "Discard all uncommitted changes%s? " + (if include-untracked + " and untracked files" + ""))) + (magit-reset-head-hard "HEAD") + (if include-untracked + (magit-run-git "clean" "-fd")))) ;;; Rewriting @@ -2236,32 +3936,32 @@ in log buffer." (prin1 info (current-buffer)) (princ "\n" (current-buffer)))) -(defun magit-insert-pending-commits () +(magit-define-inserter pending-commits () (let* ((info (magit-read-rewrite-info)) - (pending (cdr (assq 'pending info)))) + (pending (cdr (assq 'pending info)))) (when pending (magit-with-section 'pending nil - (insert (propertize "Pending commits:\n" - 'face 'magit-section-title)) - (dolist (p pending) - (let* ((commit (car p)) - (properties (cdr p)) - (used (plist-get properties 'used))) - (magit-with-section commit 'commit - (magit-set-section-info commit) - (insert (magit-git-string - "log" "--max-count=1" - (if used - "--pretty=format:. %s" - "--pretty=format:* %s") - commit "--") - "\n"))))) + (insert (propertize "Pending commits:\n" + 'face 'magit-section-title)) + (dolist (p pending) + (let* ((commit (car p)) + (properties (cdr p)) + (used (plist-get properties 'used))) + (magit-with-section commit 'commit + (magit-set-section-info commit) + (insert (magit-git-string + "log" "--max-count=1" + (if used + "--pretty=format:. %s" + "--pretty=format:* %s") + commit "--") + "\n"))))) (insert "\n")))) (defun magit-rewrite-set-commit-property (commit prop value) (let* ((info (magit-read-rewrite-info)) - (pending (cdr (assq 'pending info))) - (p (assoc commit pending))) + (pending (cdr (assq 'pending info))) + (p (assoc commit pending))) (when p (setf (cdr p) (plist-put (cdr p) prop value)) (magit-write-rewrite-info info) @@ -2279,48 +3979,57 @@ in log buffer." ((pending commit) (magit-rewrite-set-commit-property info 'used nil)))) -(defun magit-insert-pending-changes () +(magit-define-inserter pending-changes () (let* ((info (magit-read-rewrite-info)) - (orig (cadr (assq 'orig info)))) + (orig (cadr (assq 'orig info)))) (when orig (let ((magit-hide-diffs t)) - (magit-git-section 'pending-changes - "Pending changes" - 'magit-wash-diffs - "diff" (magit-diff-U-arg) "-R" orig))))) + (magit-git-section 'pending-changes + "Pending changes" + 'magit-wash-diffs + "diff" (magit-diff-U-arg) "-R" orig))))) (defun magit-rewrite-start (from &optional onto) (interactive (list (magit-read-rev "Rewrite from" (magit-default-rev)))) (or (magit-everything-clean-p) - (error "You have uncommitted changes.")) + (error "You have uncommitted changes")) (or (not (magit-read-rewrite-info)) - (error "Rewrite in progress.")) - (let* ((orig (magit-git-string "rev-parse" "HEAD")) - (base (or (car (magit-commit-parents from)) - (error "Can't rewrite a commit without a parent, sorry."))) - (pending (magit-git-lines "rev-list" (concat base "..")))) + (error "Rewrite in progress")) + (let* ((orig (magit-rev-parse "HEAD")) + (base + (if + (or + (eq magit-rewrite-inclusive t) + (and + (eq magit-rewrite-inclusive 'ask) + (y-or-n-p "Include selected revision in rewrite? "))) + (or + (car (magit-commit-parents from)) + (error "Can't rewrite a parentless commit.")) + from)) + (pending (magit-git-lines "rev-list" (concat base "..")))) (magit-write-rewrite-info `((orig ,orig) - (pending ,@(mapcar #'list pending)))) + (pending ,@(mapcar #'list pending)))) (magit-run-git "reset" "--hard" base))) (defun magit-rewrite-stop (&optional noconfirm) (interactive) (let* ((info (magit-read-rewrite-info))) (or info - (error "No rewrite in progress.")) + (error "No rewrite in progress")) (when (or noconfirm - (yes-or-no-p "Stop rewrite? ")) + (yes-or-no-p "Stop rewrite? ")) (magit-write-rewrite-info nil) - (magit-need-refresh)))) + (magit-refresh)))) (defun magit-rewrite-abort () (interactive) (let* ((info (magit-read-rewrite-info)) - (orig (cadr (assq 'orig info)))) + (orig (cadr (assq 'orig info)))) (or info - (error "No rewrite in progress.")) + (error "No rewrite in progress")) (or (magit-everything-clean-p) - (error "You have uncommitted changes.")) + (error "You have uncommitted changes")) (when (yes-or-no-p "Abort rewrite? ") (magit-write-rewrite-info nil) (magit-run-git "reset" "--hard" orig)))) @@ -2333,227 +4042,341 @@ in log buffer." (defun magit-rewrite-finish-step (first-p) (let ((info (magit-read-rewrite-info))) (or info - (error "No rewrite in progress.")) + (error "No rewrite in progress")) (let* ((pending (cdr (assq 'pending info))) - (first-unused (find-if (lambda (p) - (not (plist-get (cdr p) 'used))) - pending - :from-end t)) - (commit (car first-unused))) + (first-unused + (let ((rpend (reverse pending))) + (while (and rpend (plist-get (cdr (car rpend)) 'used)) + (setq rpend (cdr rpend))) + (car rpend))) + (commit (car first-unused))) (cond ((not first-unused) - (magit-rewrite-stop t)) - ((magit-apply-commit commit t (not first-p)) - (magit-rewrite-set-commit-property commit 'used t) - (magit-rewrite-finish-step nil)))))) + (magit-rewrite-stop t)) + ((magit-apply-commit commit t (not first-p)) + (magit-rewrite-set-commit-property commit 'used t) + (magit-rewrite-finish-step nil)))))) ;;; Updating, pull, and push -(defun magit-remote-update () +(magit-define-command fetch (remote) + "Fetch from REMOTE." + (interactive (list (magit-read-remote))) + (apply 'magit-run-git-async "fetch" remote magit-custom-options)) + +(magit-define-command fetch-current () + "Run fetch for default remote. + +If there is no default remote, ask for one." (interactive) - (if (magit-svn-enabled) - (magit-run-git-async "svn" "fetch") - (magit-run-git-async "remote" "update"))) + (magit-fetch (or (magit-get-current-remote) + (magit-read-remote)))) -(defun magit-pull () +(magit-define-command remote-update () + "Update all remotes." + (interactive) + (apply 'magit-run-git-async "remote" "update" magit-custom-options)) + +(magit-define-command pull () + "Run git pull against the current remote." (interactive) (let* ((branch (magit-get-current-branch)) - (config-branch (and branch (magit-get "branch" branch "merge"))) - (merge-branch (or config-branch - (magit-read-rev (format "Pull from"))))) + (config-branch (and branch (magit-get "branch" branch "merge"))) + (merge-branch (or (and config-branch (not current-prefix-arg)) + (magit-read-rev (format "Pull from"))))) (if (and branch (not config-branch)) - (magit-set merge-branch "branch" branch "merge")) - (magit-run-git-async "pull" "-v"))) + (magit-set merge-branch "branch" branch "merge")) + (apply 'magit-run-git-async "pull" "-v" magit-custom-options))) + +(eval-when-compile (require 'eshell)) + +(defun magit-parse-arguments (command) + (require 'eshell) + (with-temp-buffer + (insert command) + (mapcar 'eval (eshell-parse-arguments (point-min) (point-max))))) (defun magit-shell-command (command) + "Perform arbitrary shell COMMAND." (interactive "sCommand: ") - (require 'pcomplete) - (let ((args (car (with-temp-buffer - (insert command) - (pcomplete-parse-buffer-arguments)))) - (magit-process-popup-time 0)) + (let ((args (magit-parse-arguments command)) + (magit-process-popup-time 0)) (magit-run* args nil nil nil t))) -(defun magit-read-remote (prompt def) - (completing-read (if def - (format "%s (default %s): " prompt def) - (format "%s: " prompt)) - (magit-git-lines "remote") - nil nil nil nil def)) +(defvar magit-git-command-history nil) + +(defun magit-git-command (command) + "Perform arbitrary Git COMMAND. + +Similar to `magit-shell-command', but involves slightly less +typing and automatically refreshes the status buffer." + (interactive + (list (read-string "Run git like this: " nil 'magit-git-command-history))) + (require 'pcomplete) + (let ((args (magit-parse-arguments command)) + (magit-process-popup-time 0)) + (magit-with-refresh + (magit-run* (append (cons magit-git-executable + magit-git-standard-options) + args) + nil nil nil t)))) + +(magit-define-command push-tags () + "Push tags." + (interactive) + (magit-run-git-async "push" "--tags")) -(defun magit-push () +(magit-define-command push () (interactive) (let* ((branch (or (magit-get-current-branch) - (error "Don't push a detached head. That's gross."))) - (branch-remote (magit-get "branch" branch "remote")) - (push-remote (if (or current-prefix-arg - (not branch-remote)) - (magit-read-remote (format "Push %s to" branch) - branch-remote) - branch-remote))) - (if (and (not branch-remote) - (not current-prefix-arg)) - (magit-set push-remote "branch" branch "remote")) - (magit-run-git-async "push" "-v" push-remote branch))) + (error "Don't push a detached head. That's gross"))) + (branch-remote (magit-get-remote branch)) + (push-remote (if (or current-prefix-arg + (not branch-remote)) + (magit-read-remote (format "Push %s to remote: " + branch) + branch-remote) + branch-remote)) + (ref-branch (or (and (>= (prefix-numeric-value current-prefix-arg) 16) + (magit-read-remote-branch + push-remote (format "Push %s as branch: " branch))) + (magit-get "branch" branch "merge")))) + (if (and (not ref-branch) + (eq magit-set-upstream-on-push 'refuse)) + (error "Not pushing since no upstream has been set.") + (let ((set-upstream-on-push (and (not ref-branch) + (or (eq magit-set-upstream-on-push 'dontask) + (and (eq magit-set-upstream-on-push t) + (yes-or-no-p "Set upstream while pushing? ")))))) + (if (and (not branch-remote) + (not current-prefix-arg)) + (magit-set push-remote "branch" branch "remote")) + (apply 'magit-run-git-async "push" "-v" push-remote + (if ref-branch + (format "%s:%s" branch ref-branch) + branch) + (if set-upstream-on-push + (cons "--set-upstream" magit-custom-options) + magit-custom-options)) + ;; Although git will automatically set up the remote, + ;; it doesn't set up the branch to merge (at least as of Git 1.6.6.1), + ;; so we have to do that manually. + (unless ref-branch + (magit-set (concat "refs/heads/" branch) "branch" branch "merge")))))) ;;; Log edit mode -(defvar magit-log-edit-map +(defvar magit-log-edit-buffer-name "*magit-edit-log*" + "Buffer name for composing commit messages.") + +(defvar magit-log-edit-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c C-c") 'magit-log-edit-commit) + (define-key map (kbd "C-x #") 'magit-log-edit-commit) (define-key map (kbd "C-c C-a") 'magit-log-edit-toggle-amending) (define-key map (kbd "C-c C-s") 'magit-log-edit-toggle-signoff) + (define-key map (kbd "C-c C-t") 'magit-log-edit-toggle-author) + (define-key map (kbd "C-c C-e") 'magit-log-edit-toggle-allow-empty) (define-key map (kbd "M-p") 'log-edit-previous-comment) (define-key map (kbd "M-n") 'log-edit-next-comment) (define-key map (kbd "C-c C-k") 'magit-log-edit-cancel-log-message) + (define-key map (kbd "C-c C-]") 'magit-log-edit-cancel-log-message) + (define-key map (kbd "C-x C-s") (lambda () + (interactive) + (message "Not saved. Use C-c C-c to finalize this commit message."))) map)) (defvar magit-pre-log-edit-window-configuration nil) -(defun magit-log-fill-paragraph (&optional justify) - "Fill the paragraph, but preserve open parentheses at beginning of lines. -Prefix arg means justify as well." - (interactive "P") - ;; Add lines starting with a left paren or an asterisk. - (let ((paragraph-start (concat paragraph-start "\\|*\\|("))) - (let ((end (progn (forward-paragraph) (point))) - (beg (progn (backward-paragraph) (point))) - (adaptive-fill-mode nil)) - (fill-region beg end justify) - t))) - (define-derived-mode magit-log-edit-mode text-mode "Magit Log Edit" - (set (make-local-variable 'fill-paragraph-function) - 'magit-log-fill-paragraph) - (use-local-map magit-log-edit-map)) + ;; Recognize changelog-style paragraphs + (set (make-local-variable 'paragraph-start) + (concat paragraph-start "\\|*\\|("))) (defun magit-log-edit-cleanup () (save-excursion (goto-char (point-min)) - (flush-lines "^#") (goto-char (point-min)) (if (re-search-forward "[ \t\n]*\\'" nil t) - (replace-match "\n" nil nil)))) + (replace-match "\n" nil nil)))) (defun magit-log-edit-append (str) - (save-excursion - (set-buffer (get-buffer-create "*magit-log-edit*")) + (with-current-buffer (get-buffer-create magit-log-edit-buffer-name) (goto-char (point-max)) (insert str "\n"))) (defconst magit-log-header-end "-- End of Magit header --\n") (defun magit-log-edit-get-fields () - (let ((buf (get-buffer "*magit-log-edit*")) - (result nil)) + (let ((buf (get-buffer magit-log-edit-buffer-name)) + (result nil)) (if buf - (save-excursion - (set-buffer buf) - (goto-char (point-min)) - (while (looking-at "^\\([A-Za-z0-9-_]+\\): *\\(.*\\)$") - (setq result (acons (intern (downcase (match-string 1))) - (match-string 2) - result)) - (forward-line)) - (if (not (looking-at (regexp-quote magit-log-header-end))) - (setq result nil)))) + (with-current-buffer buf + (goto-char (point-min)) + (while (looking-at "^\\([A-Za-z0-9-_]+\\): *\\(.+\\)?$") + (setq result (acons (intern (downcase (match-string 1))) + (read (or (match-string 2) "nil")) + result)) + (forward-line)) + (if (not (looking-at (regexp-quote magit-log-header-end))) + (setq result nil)))) (nreverse result))) (defun magit-log-edit-set-fields (fields) - (let ((buf (get-buffer-create "*magit-log-edit*"))) - (save-excursion - (set-buffer buf) + (let ((buf (get-buffer-create magit-log-edit-buffer-name))) + (with-current-buffer buf (goto-char (point-min)) - (if (search-forward-regexp (format "^\\([A-Za-z0-9-_]+:.*\n\\)+%s" - (regexp-quote magit-log-header-end)) - nil t) - (delete-region (match-beginning 0) (match-end 0))) + (if (search-forward-regexp (format "^\\([A-Za-z0-9-_]+:.*\n\\)*%s" + (regexp-quote magit-log-header-end)) + nil t) + (delete-region (match-beginning 0) (match-end 0))) (goto-char (point-min)) (when fields - (while fields - (insert (capitalize (symbol-name (caar fields))) ": " - (cdar fields) "\n") - (setq fields (cdr fields))) - (insert magit-log-header-end))))) + (while fields + (insert (capitalize (symbol-name (caar fields))) ": " + (prin1-to-string (cdar fields)) "\n") + (setq fields (cdr fields))) + (insert magit-log-header-end))))) (defun magit-log-edit-set-field (name value) (let* ((fields (magit-log-edit-get-fields)) - (cell (assq name fields))) + (cell (assq name fields))) (cond (cell - (if value - (rplacd cell value) - (setq fields (delq cell fields)))) - (t - (if value - (setq fields (append fields (list (cons name value))))))) + (if value + (rplacd cell value) + (setq fields (delq cell fields)))) + (t + (if value + (setq fields (append fields (list (cons name value))))))) (magit-log-edit-set-fields fields))) (defun magit-log-edit-get-field (name) (cdr (assq name (magit-log-edit-get-fields)))) +(defun magit-log-edit-toggle-field (name default) + "Toggle the log-edit field named NAME. +If it's currently unset, set it to DEFAULT (t or nil). + +Return nil if the field is toggled off, and non-nil if it's +toggled on. When it's toggled on for the first time, return +'first." + (let* ((fields (magit-log-edit-get-fields)) + (cell (assq name fields)) yesp) + (if cell + (progn + (setq yesp (equal (cdr cell) "yes")) + (rplacd cell (if yesp "no" "yes"))) + (setq fields (acons name (if default "yes" "no") fields)) + (setq yesp (if default 'first))) + (magit-log-edit-set-fields fields) + yesp)) + +(defun magit-log-edit-toggle-input (name default) + "Toggle the log-edit input named NAME. +If it's currently unset, set it to DEFAULT (a string). If it is +set remove it. + +Return nil if the input is toggled off, and its valud if it's +toggled on." + (let* ((fields (magit-log-edit-get-fields)) + (cell (assq name fields)) + result) + (if cell + (progn + (setq fields (assq-delete-all name fields) + result (cdr cell))) + (setq fields (acons name default fields))) + (magit-log-edit-set-fields fields) + result)) + (defun magit-log-edit-setup-author-env (author) - (cond (author - ;; XXX - this is a bit strict, probably. - (or (string-match "\\(.*\\) <\\(.*\\)>, \\(.*\\)" author) - (error "Can't parse author string.")) - ;; Shucks, setenv destroys the match data. - (let ((name (match-string 1 author)) - (email (match-string 2 author)) - (date (match-string 3 author))) - (setenv "GIT_AUTHOR_NAME" name) - (setenv "GIT_AUTHOR_EMAIL" email) - (setenv "GIT_AUTHOR_DATE" date))) - (t - (setenv "GIT_AUTHOR_NAME") - (setenv "GIT_AUTHOR_EMAIL") - (setenv "GIT_AUTHOR_DATE")))) + "Set GIT_AUTHOR_* variables from AUTHOR spec. +If AUTHOR is nil, honor default values from +environment (potentially empty)." + (when author + ;; XXX - this is a bit strict, probably. + (or (string-match "\\(.*\\) <\\(.*\\)>\\(?:,\\s-*\\(.+\\)\\)?" author) + (error "Can't parse author string")) + ;; Shucks, setenv destroys the match data. + (let ((name (match-string 1 author)) + (email (match-string 2 author)) + (date (match-string 3 author))) + (setenv "GIT_AUTHOR_NAME" name) + (setenv "GIT_AUTHOR_EMAIL" email) + (if date + (setenv "GIT_AUTHOR_DATE" date))))) (defun magit-log-edit-push-to-comment-ring (comment) (when (or (ring-empty-p log-edit-comment-ring) - (not (equal comment (ring-ref log-edit-comment-ring 0)))) + (not (equal comment (ring-ref log-edit-comment-ring 0)))) (ring-insert log-edit-comment-ring comment))) (defun magit-log-edit-commit () + "Finish edits and create new commit object. +\('git commit ...')" (interactive) (let* ((fields (magit-log-edit-get-fields)) - (amend (equal (cdr (assq 'amend fields)) "yes")) - (commit-all (equal (cdr (assq 'commit-all fields)) "yes")) - (sign-off-field (assq 'sign-off fields)) - (sign-off (if sign-off-field - (equal (cdr sign-off-field) "yes") - magit-commit-signoff)) - (tag (cdr (assq 'tag fields))) - (author (cdr (assq 'author fields)))) + (amend (equal (cdr (assq 'amend fields)) "yes")) + (allow-empty (equal (cdr (assq 'allow-empty fields)) "yes")) + (commit-all (equal (cdr (assq 'commit-all fields)) "yes")) + (sign-off-field (assq 'sign-off fields)) + (sign-off (if sign-off-field + (equal (cdr sign-off-field) "yes") + magit-commit-signoff)) + (tag-rev (cdr (assq 'tag-rev fields))) + (tag-name (cdr (assq 'tag-name fields))) + (author (cdr (assq 'author fields))) + (tag-options (cdr (assq 'tag-options fields)))) + + (unless (or (magit-anything-staged-p) + allow-empty + amend + tag-name + (file-exists-p ".git/MERGE_HEAD") + (and commit-all + (not (magit-everything-clean-p)))) + (error "Refusing to create empty commit. Maybe you want to amend (%s) or allow-empty (%s)?" + (key-description (car (where-is-internal + 'magit-log-edit-toggle-amending))) + (key-description (car (where-is-internal + 'magit-log-edit-toggle-allow-empty))))) + (magit-log-edit-push-to-comment-ring (buffer-string)) (magit-log-edit-setup-author-env author) (magit-log-edit-set-fields nil) (magit-log-edit-cleanup) (if (= (buffer-size) 0) - (insert "(Empty description)\n")) + (insert "(Empty description)\n")) (let ((commit-buf (current-buffer))) - (with-current-buffer (magit-find-buffer 'status default-directory) - (cond (tag - (magit-run-git-with-input commit-buf "tag" tag "-a" "-F" "-")) - (t - (apply #'magit-run-async-with-input commit-buf - magit-git-executable - (append magit-git-standard-options - (list "commit" "-F" "-") - (if commit-all '("--all") '()) - (if amend '("--amend") '()) - (if sign-off '("--signoff") '()))))))) + (with-current-buffer (magit-find-status-buffer default-directory) + (cond (tag-name + (apply #'magit-run-git-with-input commit-buf + "tag" (append tag-options (list tag-name "-a" "-F" "-" tag-rev)))) + (t + (apply #'magit-run-async-with-input commit-buf + magit-git-executable + (append magit-git-standard-options + '("commit") + magit-custom-options + '("-F" "-") + (if (and commit-all (not allow-empty)) '("--all") '()) + (if amend '("--amend") '()) + (if allow-empty '("--allow-empty")) + (if sign-off '("--signoff") '()))))))) (erase-buffer) (bury-buffer) (when (file-exists-p ".git/MERGE_MSG") (delete-file ".git/MERGE_MSG")) + (magit-update-vc-modeline default-directory) (when magit-pre-log-edit-window-configuration (set-window-configuration magit-pre-log-edit-window-configuration) (setq magit-pre-log-edit-window-configuration nil)))) (defun magit-log-edit-cancel-log-message () + "Abort edits and erase commit message being composed." (interactive) (when (or (not magit-log-edit-confirm-cancellation) - (yes-or-no-p - "Really cancel editing the log (any changes will be lost)?")) + (yes-or-no-p + "Really cancel editing the log (any changes will be lost)?")) (erase-buffer) (bury-buffer) (when magit-pre-log-edit-window-configuration @@ -2561,31 +4384,39 @@ Prefix arg means justify as well." (setq magit-pre-log-edit-window-configuration nil)))) (defun magit-log-edit-toggle-amending () + "Toggle whether this will be an amendment to the previous commit. +\(i.e., whether eventual commit does 'git commit --amend')" (interactive) - (let* ((fields (magit-log-edit-get-fields)) - (cell (assq 'amend fields))) - (if cell - (rplacd cell (if (equal (cdr cell) "yes") "no" "yes")) - (setq fields (acons 'amend "yes" fields)) - (magit-log-edit-append - (magit-format-commit "HEAD" "%s%n%n%b"))) - (magit-log-edit-set-fields fields))) + (when (eq (magit-log-edit-toggle-field 'amend t) 'first) + (magit-log-edit-append + (magit-trim-line (magit-format-commit "HEAD" "%s%n%n%b"))))) (defun magit-log-edit-toggle-signoff () + "Toggle whether this commit will include a signoff. +\(i.e., whether eventual commit does 'git commit --signoff')" (interactive) - (let* ((fields (magit-log-edit-get-fields)) - (cell (assq 'sign-off fields))) - (if cell - (rplacd cell (if (equal (cdr cell) "yes") "no" "yes")) - (setq fields (acons 'sign-off (if magit-commit-signoff "no" "yes") - fields))) - (magit-log-edit-set-fields fields))) + (magit-log-edit-toggle-field 'sign-off (not magit-commit-signoff))) + +(defun magit-log-edit-toggle-author () + "Toggle whether this commit will include an author. +\(i.e., whether eventual commit is run with GIT_AUTHOR_NAME and +GIT_AUTHOR_EMAIL set)" + (interactive) + (magit-log-edit-toggle-input 'author (format "%s <%s>" + (or (magit-get "user" "name") "Author Name") + (or (magit-get "user" "email") "author@email")))) + +(defun magit-log-edit-toggle-allow-empty () + "Toggle whether this commit is allowed to be empty. +This means that the eventual commit does 'git commit --allow-empty'." + (interactive) + (magit-log-edit-toggle-field 'allow-empty t)) (defun magit-pop-to-log-edit (operation) (let ((dir default-directory) - (buf (get-buffer-create "*magit-log-edit*"))) + (buf (get-buffer-create magit-log-edit-buffer-name))) (setq magit-pre-log-edit-window-configuration - (current-window-configuration)) + (current-window-configuration)) (pop-to-buffer buf) (when (file-exists-p ".git/MERGE_MSG") (insert-file-contents ".git/MERGE_MSG")) @@ -2593,106 +4424,142 @@ Prefix arg means justify as well." (magit-log-edit-mode) (message "Type C-c C-c to %s (C-c C-k to cancel)." operation))) -(defun magit-log-edit () - (interactive) - (cond ((magit-rebase-info) - (if (y-or-n-p "Rebase in progress. Continue it? ") - (magit-run-git "rebase" "--continue"))) - (t - (magit-log-edit-set-field 'tag nil) - (when (and magit-commit-all-when-nothing-staged - (not (magit-anything-staged-p))) - (cond ((eq magit-commit-all-when-nothing-staged 'ask-stage) - (if (and (not (magit-everything-clean-p)) - (y-or-n-p "Nothing staged. Stage everything now? ")) - (magit-stage-all))) - ((not (magit-log-edit-get-field 'commit-all)) - (magit-log-edit-set-field - 'commit-all - (if (or (eq magit-commit-all-when-nothing-staged t) - (y-or-n-p - "Nothing staged. Commit all unstaged changes? ")) - "yes" "no"))))) - (magit-pop-to-log-edit "commit")))) +(defun magit-log-edit (&optional arg) + "Brings up a buffer to allow editing of commit messages. + +Giving a simple prefix arg will amend a previous commit, while +a double prefix arg will allow creating an empty one. + +If there is a rebase in progress, offer the user the option to +continue it. + +\\{magit-log-edit-mode-map}" + (interactive "P") + ;; If repository is dirty there is no point in trying to + ;; suggest to continue the rebase. Git will rebuke you and exit with + ;; error code, so suggest it only if theres absolutely nothing else + ;; to do and rebase is ongoing. + (if (and (magit-everything-clean-p) + (magit-rebase-info) + (y-or-n-p "Rebase in progress. Continue it? ")) + (magit-run-git-async "rebase" "--continue") + + ;; If there's nothing staged, set commit flag to `nil', thus + ;; avoiding unnescessary popping up of the log edit buffer in case + ;; when user chose to forgo commiting all unstaged changes + (let ((amend-p (= (prefix-numeric-value arg) 4)) + (empty-p (= (prefix-numeric-value arg) 16))) + (when (and magit-commit-all-when-nothing-staged + (not (magit-everything-clean-p)) + (not (magit-anything-staged-p))) + (cond ((eq magit-commit-all-when-nothing-staged 'ask-stage) + (when (y-or-n-p "Nothing staged. Stage everything now? ") + (magit-stage-all))) + ((not (magit-log-edit-get-field 'commit-all)) + (when (or (eq magit-commit-all-when-nothing-staged t) + (y-or-n-p + "Nothing staged. Commit all unstaged changes? ")) + (magit-log-edit-set-field 'commit-all "yes"))))) + (when amend-p + (magit-log-edit-toggle-amending)) + (when empty-p + (magit-log-edit-toggle-allow-empty)) + (magit-pop-to-log-edit "commit")))) (defun magit-add-log () (interactive) (cond ((magit-rebase-info) - (if (y-or-n-p "Rebase in progress. Continue it? ") - (magit-run-git "rebase" "--continue"))) - (t - (let ((section (magit-current-section))) - (let ((fun (if (eq (magit-section-type section) 'hunk) - (save-window-excursion - (save-excursion - (magit-visit-item) - (add-log-current-defun))) - nil)) - (file (magit-diff-item-file - (cond ((eq (magit-section-type section) 'hunk) - (magit-hunk-item-diff section)) - ((eq (magit-section-type section) 'diff) - section) - (t - (error "No change at point")))))) - (magit-log-edit) - (goto-char (point-min)) - (cond ((not (search-forward-regexp - (format "^\\* %s" (regexp-quote file)) nil t)) - ;; No entry for file, create it. - (goto-char (point-max)) - (insert (format "\n* %s" file)) - (if fun - (insert (format " (%s)" fun))) - (insert ": ")) - (fun - ;; found entry for file, look for fun - (let ((limit (or (save-excursion - (and (search-forward-regexp "^\\* " - nil t) - (match-beginning 0))) - (point-max)))) - (cond ((search-forward-regexp (format "(.*\\<%s\\>.*):" - (regexp-quote fun)) - limit t) - ;; found it, goto end of current entry - (if (search-forward-regexp "^(" limit t) - (backward-char 2) - (goto-char limit))) - (t - ;; not found, insert new entry - (goto-char limit) - (if (bolp) - (open-line 1) - (newline)) - (insert (format "(%s): " fun)))))))))))) + (if (y-or-n-p "Rebase in progress. Continue it? ") + (magit-run-git-async "rebase" "--continue"))) + (t + (let ((section (magit-current-section))) + (let ((fun (if (eq (magit-section-type section) 'hunk) + (save-window-excursion + (save-excursion + (magit-visit-item) + (add-log-current-defun))) + nil)) + (file (magit-diff-item-file + (cond ((eq (magit-section-type section) 'hunk) + (magit-hunk-item-diff section)) + ((eq (magit-section-type section) 'diff) + section) + (t + (error "No change at point")))))) + (magit-log-edit nil) + (goto-char (point-min)) + (cond ((not (search-forward-regexp + (format "^\\* %s" (regexp-quote file)) nil t)) + ;; No entry for file, create it. + (goto-char (point-max)) + (insert (format "\n* %s" file)) + (if fun + (insert (format " (%s)" fun))) + (insert ": ")) + (fun + ;; found entry for file, look for fun + (let ((limit (or (save-excursion + (and (search-forward-regexp "^\\* " + nil t) + (match-beginning 0))) + (point-max)))) + (cond ((search-forward-regexp (format "(.*\\<%s\\>.*):" + (regexp-quote fun)) + limit t) + ;; found it, goto end of current entry + (if (search-forward-regexp "^(" limit t) + (backward-char 2) + (goto-char limit))) + (t + ;; not found, insert new entry + (goto-char limit) + (if (bolp) + (open-line 1) + (newline)) + (insert (format "(%s): " fun)))))) + (t + ;; found entry for file, look for beginning it + (when (looking-at ":") + (forward-char 2))))))))) ;;; Tags -(defun magit-tag (name) - (interactive "sNew tag name: ") - (magit-run-git "tag" name)) - -(defun magit-annotated-tag (name) - (interactive "sNew tag name: ") - (magit-log-edit-set-field 'tag name) +(magit-define-command tag (name rev) + "Create a new lightweight tag with the given NAME at REV. +\('git tag NAME')." + (interactive + (list + (read-string "Tag name: ") + (magit-read-rev "Place tag on: " (or (magit-default-rev) "HEAD")))) + (apply #'magit-run-git "tag" (append magit-custom-options (list name rev)))) + +(magit-define-command annotated-tag (name rev) + "Start composing an annotated tag with the given NAME. +Tag will point to the current 'HEAD'." + (interactive + (list + (read-string "Tag name: ") + (magit-read-rev "Place tag on: " (or (magit-default-rev) "HEAD")))) + (magit-log-edit-set-field 'tag-name name) + (magit-log-edit-set-field 'tag-rev rev) + (magit-log-edit-set-field 'tag-options magit-custom-options) (magit-pop-to-log-edit "tag")) ;;; Stashing (defun magit-wash-stash () - (if (search-forward-regexp "stash@{\\(.*\\)}" (line-end-position) t) + (if (search-forward-regexp "stash@{\\(.*?\\)}" (line-end-position) t) (let ((stash (match-string-no-properties 0)) - (name (match-string-no-properties 1))) - (delete-region (match-beginning 0) (match-end 0)) - (goto-char (match-beginning 0)) - (fixup-whitespace) - (goto-char (line-beginning-position)) - (insert name) - (goto-char (line-beginning-position)) - (magit-with-section stash 'stash - (magit-set-section-info stash) - (forward-line))) + (name (match-string-no-properties 1))) + (delete-region (match-beginning 0) (match-end 0)) + (goto-char (match-beginning 0)) + (fixup-whitespace) + (goto-char (line-beginning-position)) + (insert name) + (goto-char (line-beginning-position)) + (magit-with-section stash 'stash + (magit-set-section-info stash) + (forward-line))) (forward-line)) t) @@ -2700,105 +4567,99 @@ Prefix arg means justify as well." (let ((magit-old-top-section nil)) (magit-wash-sequence #'magit-wash-stash))) -(defun magit-insert-stashes () +(magit-define-inserter stashes () (magit-git-section 'stashes - "Stashes:" 'magit-wash-stashes - "stash" "list")) - -(defun magit-stash (description) + "Stashes:" 'magit-wash-stashes + "stash" "list")) + +(magit-define-command stash (description) + "Create new stash of working tree and staging area named DESCRIPTION. +Working tree and staging area revert to the current 'HEAD'. +With prefix argument, changes in staging area are kept. +\('git stash save [--keep-index] DESCRIPTION')" (interactive "sStash description: ") - (magit-run-git "stash" "save" description)) + (apply 'magit-run-git `("stash" "save" ,@magit-custom-options "--" ,description))) -(defun magit-stash-snapshot () +(magit-define-command stash-snapshot () + "Create new stash of working tree and staging area; keep changes in place. +\('git stash save \"Snapshot...\"; git stash apply stash@{0}')" (interactive) (magit-with-refresh - (magit-run-git "stash" "save" - (format-time-string "Snapshot taken at %Y-%m-%d %H:%M:%S" - (current-time))) + (apply 'magit-run-git `("stash" "save" ,@magit-custom-options + ,(format-time-string "Snapshot taken at %Y-%m-%d %H:%M:%S" + (current-time)))) (magit-run-git "stash" "apply" "stash@{0}"))) (defvar magit-currently-shown-stash nil) +(define-derived-mode magit-stash-mode magit-mode "Magit Stash" + "Mode for looking at a git stash. + +\\{magit-stash-mode-map}" + :group 'magit) + +(defvar magit-stash-buffer-name "*magit-stash*" + "Buffer name for displaying a stash.") + (defun magit-show-stash (stash &optional scroll) (when (magit-section-p stash) (setq stash (magit-section-info stash))) (let ((dir default-directory) - (buf (get-buffer-create "*magit-stash*"))) - (cond ((equal magit-currently-shown-stash stash) - (let ((win (get-buffer-window buf))) - (cond ((not win) - (display-buffer buf)) - (scroll - (with-selected-window win - (funcall scroll)))))) - (t - (setq magit-currently-shown-stash stash) - (display-buffer buf) - (with-current-buffer buf - (set-buffer buf) - (goto-char (point-min)) - (let* ((range (cons (concat stash "^2^") stash)) - (args (magit-rev-range-to-git range))) - (magit-mode-init dir 'diff #'magit-refresh-diff-buffer - range args))))))) - -;;; Topic branches (using topgit) - -(defun magit-wash-topic () - (if (search-forward-regexp "^..\\(t/\\S-+\\)\\s-+\\(\\S-+\\)\\s-+\\(\\S-+\\)" - (line-end-position) t) - (let ((topic (match-string 1))) - (delete-region (match-beginning 2) (match-end 2)) - (goto-char (line-beginning-position)) - (delete-char 4) - (insert "\t") - (goto-char (line-beginning-position)) - (magit-with-section topic 'topic - (magit-set-section-info topic) - (forward-line))) - (delete-region (line-beginning-position) (1+ (line-end-position)))) - t) - -(defun magit-wash-topics () - (let ((magit-old-top-section nil)) - (magit-wash-sequence #'magit-wash-topic))) - -(defun magit-insert-topics () - (magit-git-section 'topics - "Topics:" 'magit-wash-topics - "branch" "-v")) - + (buf (get-buffer-create magit-stash-buffer-name)) + (stash-id (magit-git-string "rev-list" "-1" stash))) + (cond ((and (equal magit-currently-shown-stash stash-id) + (with-current-buffer buf + (> (length (buffer-string)) 1))) + (let ((win (get-buffer-window buf))) + (cond ((not win) + (display-buffer buf)) + (scroll + (with-selected-window win + (funcall scroll)))))) + (t + (setq magit-currently-shown-stash stash-id) + (display-buffer buf) + (with-current-buffer buf + (set-buffer buf) + (goto-char (point-min)) + (let* ((range (cons (concat stash "^2^") stash)) + (magit-current-diff-range range) + (args (magit-rev-range-to-git range))) + (magit-mode-init dir 'magit-diff-mode #'magit-refresh-diff-buffer + range args))))))) ;;; Commits (defun magit-commit-at-point (&optional nil-ok-p) (let* ((section (magit-current-section)) - (commit (and (eq (magit-section-type section) 'commit) - (magit-section-info section)))) + (commit (or (and (not section) ; Places without a magit-section + (get-text-property (point) 'revision)) ; but with a text property 'revision + (and (eq (magit-section-type section) 'commit) + (magit-section-info section))))) (if nil-ok-p - commit + commit (or commit - (error "No commit at point."))))) + (error "No commit at point"))))) (defun magit-apply-commit (commit &optional docommit noerase revert) (let* ((parent-id (magit-choose-parent-id commit "cherry-pick")) - (success (magit-run* `(,magit-git-executable - ,@magit-git-standard-options - ,(if revert "revert" "cherry-pick") - ,@(if parent-id - (list "-m" (number-to-string parent-id))) - ,@(if (not docommit) (list "--no-commit")) - ,commit) - nil noerase))) - (when (or (not docommit) success) + (success (magit-run* `(,magit-git-executable + ,@magit-git-standard-options + ,(if revert "revert" "cherry-pick") + ,@(if parent-id + (list "-m" (number-to-string parent-id))) + ,@(if (not docommit) (list "--no-commit")) + ,commit) + nil noerase))) + (when (and (not docommit) success) (cond (revert - (magit-log-edit-append - (magit-format-commit commit "Reverting \"%s\""))) - (t - (magit-log-edit-append - (magit-format-commit commit "%s%n%n%b")) - (magit-log-edit-set-field - 'author - (magit-format-commit commit "%an <%ae>, %ai"))))) + (magit-log-edit-append + (magit-format-commit commit "Reverting \"%s\""))) + (t + (magit-log-edit-append + (magit-format-commit commit "%s%n%n%b")) + (magit-log-edit-set-field + 'author + (magit-format-commit commit "%an <%ae>, %ai"))))) success)) (defun magit-apply-item () @@ -2831,196 +4692,358 @@ Prefix arg means justify as well." ((stash) (magit-run-git "stash" "pop" info)))) +(defmacro magit-with-revert-confirmation (&rest body) + `(when (or (not magit-revert-item-confirm) + (yes-or-no-p "Really revert this item? ")) + ,@body)) + (defun magit-revert-item () (interactive) (magit-section-action (item info "revert") ((pending commit) - (magit-apply-commit info nil nil t) - (magit-rewrite-set-commit-property info 'used nil)) + (magit-with-revert-confirmation + (magit-apply-commit info nil nil t) + (magit-rewrite-set-commit-property info 'used nil))) ((commit) - (magit-apply-commit info nil nil t)) + (magit-with-revert-confirmation + (magit-apply-commit info nil nil t))) + ;; Reverting unstaged changes cannot be undone + ((unstaged *) + (magit-discard-item)) ((hunk) - (magit-apply-hunk-item-reverse item)) + (magit-with-revert-confirmation + (magit-apply-hunk-item-reverse item))) ((diff) - (magit-apply-diff-item item "--reverse")))) + (magit-with-revert-confirmation + (magit-apply-diff-item item "--reverse"))))) -(defvar magit-have-graph 'unset) -(defvar magit-have-decorate 'unset) -(make-variable-buffer-local 'magit-have-graph) -(put 'magit-have-graph 'permanent-local t) -(make-variable-buffer-local 'magit-have-decorate) -(put 'magit-have-decorate 'permanent-local t) +(defun magit-log-show-more-entries (&optional arg) + "Grow the number of log entries shown. -(defun magit-configure-have-graph () - (if (eq magit-have-graph 'unset) - (let ((res (magit-git-exit-code "log" "--graph" "--max-count=0"))) - (setq magit-have-graph (eq res 0))))) - -(defun magit-configure-have-decorate () - (if (eq magit-have-decorate 'unset) - (let ((res (magit-git-exit-code "log" "--decorate=full" "--max-count=0"))) - (setq magit-have-decorate (eq res 0))))) +With no prefix optional ARG, show twice as many log entries. +With a numerical prefix ARG, add this number to the number of shown log entries. +With a non numeric prefix ARG, show all entries" + (interactive "P") + (make-local-variable 'magit-log-cutoff-length) + (cond + ((numberp arg) + (setq magit-log-cutoff-length (+ magit-log-cutoff-length arg))) + (arg + (setq magit-log-cutoff-length magit-log-infinite-length)) + (t (setq magit-log-cutoff-length (* magit-log-cutoff-length 2)))) + (let ((old-point (point))) + (magit-refresh) + (goto-char old-point))) (defun magit-refresh-log-buffer (range style args) (magit-configure-have-graph) (magit-configure-have-decorate) - (magit-create-buffer-sections + (magit-configure-have-abbrev) + (setq magit-current-range range) + (magit-create-log-buffer-sections (apply #'magit-git-section nil - (magit-rev-range-describe range "Commits") - 'magit-wash-log - `("log" - ,(format "--max-count=%s" magit-log-cutoff-length) - ,style - ,@(if magit-have-decorate (list "--decorate=full")) - ,@(if magit-have-graph (list "--graph")) - ,args "--")))) - -(defun magit-log (&optional arg) - (interactive "P") - (let* ((range (if arg - (magit-read-rev-range "Log" "HEAD") - "HEAD")) - (topdir (magit-get-top-dir default-directory)) - (args (magit-rev-range-to-git range))) - (switch-to-buffer "*magit-log*") - (magit-mode-init topdir 'log #'magit-refresh-log-buffer range - "--pretty=oneline" args))) - -(defun magit-log-long (&optional arg) - (interactive "P") - (let* ((range (if arg - (magit-read-rev-range "Long log" "HEAD") - "HEAD")) - (topdir (magit-get-top-dir default-directory)) - (args (magit-rev-range-to-git range))) - (switch-to-buffer "*magit-log*") - (magit-mode-init topdir 'log #'magit-refresh-log-buffer range - "--stat" args))) + (magit-rev-range-describe range "Commits") + (apply-partially 'magit-wash-log style) + `("log" + ,(format "--max-count=%s" magit-log-cutoff-length) + ,"--abbrev-commit" + ,(format "--abbrev=%s" magit-sha1-abbrev-length) + ,@(cond ((eq style 'long) (list "--stat" "-z")) + ((eq style 'oneline) (list "--pretty=oneline")) + (t nil)) + ,@(if magit-have-decorate (list "--decorate=full")) + ,@(if magit-have-graph (list "--graph")) + ,@args + "--")))) + +(define-derived-mode magit-log-mode magit-mode "Magit Log" + "Mode for looking at git log. + +\\{magit-log-mode-map}" + :group 'magit) + +(defvar magit-log-buffer-name "*magit-log*" + "Buffer name for display of log entries.") + +(magit-define-command log-ranged () + (interactive) + (magit-log t)) +(define-obsolete-function-alias 'magit-display-log-ranged 'magit-log-ranged) + +(magit-define-command log (&optional ask-for-range &rest extra-args) + (interactive) + (let* ((log-range (if ask-for-range + (magit-read-rev-range "Log" "HEAD") + "HEAD")) + (topdir (magit-get-top-dir default-directory)) + (args (nconc (list (magit-rev-range-to-git log-range)) + magit-custom-options + extra-args))) + (magit-buffer-switch magit-log-buffer-name) + (magit-mode-init topdir 'magit-log-mode #'magit-refresh-log-buffer log-range + 'oneline args))) + +(define-obsolete-function-alias 'magit-display-log 'magit-log) + +(magit-define-command log-long-ranged () + (interactive) + (magit-log-long t)) + +(magit-define-command log-long (&optional ranged) + (interactive) + (let* ((range (if ranged + (magit-read-rev-range "Long log" "HEAD") + "HEAD")) + (topdir (magit-get-top-dir default-directory)) + (args (append (list (magit-rev-range-to-git range)) + magit-custom-options))) + (magit-buffer-switch magit-log-buffer-name) + (magit-mode-init topdir 'magit-log-mode #'magit-refresh-log-buffer range + 'long args))) ;;; Reflog +(defvar magit-reflog-head nil + "The HEAD of the reflog in the current buffer. +This is only non-nil in reflog buffers.") +(make-variable-buffer-local 'magit-reflog-head) + (defun magit-refresh-reflog-buffer (head args) - (magit-create-buffer-sections - (magit-git-section 'reflog - (format "Local history of head %s" head) - 'magit-wash-log - "log" "--walk-reflogs" - (format "--max-count=%s" magit-log-cutoff-length) - "--pretty=oneline" - args))) - -(defun magit-reflog (head) - (interactive (list (magit-read-rev "Reflog of" "HEAD"))) - (if head - (let* ((topdir (magit-get-top-dir default-directory)) - (args (magit-rev-to-git head))) - (switch-to-buffer "*magit-reflog*") - (magit-mode-init topdir 'reflog - #'magit-refresh-reflog-buffer head args)))) - -(defun magit-reflog-head () - (interactive) - (magit-reflog "HEAD")) + (setq magit-reflog-head head) + (magit-create-log-buffer-sections + (apply #'magit-git-section + 'reflog (format "Local history of head %s" head) 'magit-wash-log "log" + (append magit-git-log-options + (list + "--walk-reflogs" + (format "--max-count=%s" magit-log-cutoff-length) + args))))) + +(define-derived-mode magit-reflog-mode magit-log-mode "Magit Reflog" + "Mode for looking at git reflog. + +\\{magit-reflog-mode-map}" + :group 'magit) + +(magit-define-command reflog (&optional ask-for-range) + (interactive) + (let ((at (or (if ask-for-range + (magit-read-rev "Reflog of" (or (magit-guess-branch) "HEAD"))) + "HEAD"))) + (let* ((topdir (magit-get-top-dir default-directory)) + (args (magit-rev-to-git at))) + (magit-buffer-switch "*magit-reflog*") + (magit-mode-init topdir 'magit-reflog-mode + #'magit-refresh-reflog-buffer at args)))) + +(magit-define-command reflog-ranged () + (interactive) + (magit-reflog t)) ;;; Diffing +(defvar magit-ediff-buffers nil + "List of buffers that may be killed by `magit-ediff-restore'.") + +(defvar magit-ediff-windows nil + "The window configuration that will be restored when Ediff is finished.") + +(defun magit-ediff() + "View the current DIFF section in ediff." + (interactive) + (let ((diff (magit-current-section))) + (when (magit-section-hidden diff) + ;; Range is not set until the first time the diff is visible. + ;; This somewhat hackish code makes sure it's been visible at least once. + (magit-toggle-section) + (magit-toggle-section) + (setq diff (magit-current-section))) + (if (eq 'hunk (magit-section-type diff)) + (setq diff (magit-section-parent diff))) + (unless (eq 'diff (magit-section-type diff)) + (error "No diff at this location")) + (let* ((type (magit-diff-item-kind diff)) + (file1 (magit-diff-item-file diff)) + (file2 (magit-diff-item-file2 diff)) + (range (magit-diff-item-range diff))) + (cond + ((memq type '(new deleted typechange)) + (message "Why ediff a %s file?" type)) + ((and (eq type 'unmerged) + (eq (cdr range) 'working)) + (magit-interactive-resolve file1)) + ((consp (car range)) + (magit-ediff* (magit-show (caar range) file2) + (magit-show (cdar range) file2) + (magit-show (cdr range) file1))) + (t + (magit-ediff* (magit-show (car range) file2) + (magit-show (cdr range) file1))))))) + +(defun magit-ediff* (a b &optional c) + (setq magit-ediff-buffers (list a b c)) + (setq magit-ediff-windows (current-window-configuration)) + (add-hook 'ediff-quit-hook 'magit-ediff-restore 'append) + (if c + (ediff-buffers3 a b c) + (ediff-buffers a b))) + +(defun magit-ediff-restore() + "Kill any buffers in `magit-ediff-buffers' that are not visiting files and +restore the window state that was saved before ediff was called." + (dolist (buffer magit-ediff-buffers) + (if (and (null (buffer-file-name buffer)) + (buffer-live-p buffer)) + (kill-buffer buffer))) + (setq magit-ediff-buffers nil) + (set-window-configuration magit-ediff-windows) + (remove-hook 'ediff-quit-hook 'magit-ediff-restore)) + (defun magit-refresh-diff-buffer (range args) - (magit-create-buffer-sections - (magit-git-section 'diffbuf - (magit-rev-range-describe range "Changes") - 'magit-wash-diffs - "diff" (magit-diff-U-arg) args))) + (let ((magit-current-diff-range (cond + ((stringp range) + (cons range 'working)) + ((null (cdr range)) + (cons (car range) 'working)) + (t + range)))) + (setq magit-current-range range) + (magit-create-buffer-sections + (magit-git-section 'diffbuf + (magit-rev-range-describe range "Changes") + 'magit-wash-diffs + "diff" (magit-diff-U-arg) args)))) + +(define-derived-mode magit-diff-mode magit-mode "Magit Diff" + "Mode for looking at a git diff. + +\\{magit-diff-mode-map}" + :group 'magit) -(defun magit-diff (range) +(magit-define-command diff (range) (interactive (list (magit-read-rev-range "Diff"))) (if range (let* ((dir default-directory) - (args (magit-rev-range-to-git range)) - (buf (get-buffer-create "*magit-diff*"))) - (display-buffer buf) - (save-excursion - (set-buffer buf) - (magit-mode-init dir 'diff #'magit-refresh-diff-buffer range args))))) - -(defun magit-diff-working-tree (rev) - (interactive (list (magit-read-rev "Diff with (default HEAD)"))) + (args (magit-rev-range-to-git range)) + (buf (get-buffer-create "*magit-diff*"))) + (display-buffer buf) + (with-current-buffer buf + (magit-mode-init dir 'magit-diff-mode #'magit-refresh-diff-buffer range args))))) + +(magit-define-command diff-working-tree (rev) + (interactive (list (magit-read-rev "Diff with" (magit-default-rev)))) (magit-diff (or rev "HEAD"))) (defun magit-diff-with-mark () (interactive) (magit-diff (cons (magit-marked-commit) - (magit-commit-at-point)))) + (magit-commit-at-point)))) ;;; Wazzup +(defvar magit-wazzup-head nil + "The integration head for the current wazzup buffer. +This is only non-nil in wazzup buffers.") +(make-variable-buffer-local 'magit-wazzup-head) + +(defvar magit-wazzup-all-p nil + "Non-nil if the current wazzup buffer displays excluded branches. +This is only meaningful in wazzup buffers.") +(make-variable-buffer-local 'magit-wazzup-all-p) + (defun magit-wazzup-toggle-ignore (branch edit) (let ((ignore-file ".git/info/wazzup-exclude")) (if edit - (setq branch (read-string "Branch to ignore for wazzup: " branch))) + (setq branch (read-string "Branch to ignore for wazzup: " branch))) (let ((ignored (magit-file-lines ignore-file))) (cond ((member branch ignored) - (when (or (not edit) - (y-or-n-p "Branch %s is already ignored. Unignore?")) - (setq ignored (delete branch ignored)))) - (t - (setq ignored (append ignored (list branch))))) + (when (or (not edit) + (y-or-n-p "Branch %s is already ignored. Unignore? ")) + (setq ignored (delete branch ignored)))) + (t + (setq ignored (append ignored (list branch))))) (magit-write-file-lines ignore-file ignored) (magit-need-refresh)))) (defun magit-refresh-wazzup-buffer (head all) - (magit-create-buffer-sections - (magit-with-section 'wazzupbuf nil - (insert (format "Wazzup, %s\n\n" head)) - (let* ((excluded (magit-file-lines ".git/info/wazzup-exclude")) - (all-branches (magit-list-interesting-refs)) - (branches (if all all-branches - (remove-if (lambda (b) (member (cdr b) excluded)) - all-branches))) - (reported (make-hash-table :test #'equal))) - (dolist (branch branches) - (let* ((name (car branch)) - (ref (cdr branch)) - (hash (magit-git-string "rev-parse" ref)) - (reported-branch (gethash hash reported))) - (unless (or (and reported-branch - (string= (file-name-nondirectory ref) - reported-branch)) - (not (magit-git-string "merge-base" head ref))) - (puthash hash (file-name-nondirectory ref) reported) - (let* ((n (length (magit-git-lines "log" "--pretty=oneline" - (concat head ".." ref)))) - (section - (let ((magit-section-hidden-default t)) - (magit-git-section - (cons ref 'wazzup) - (format "%s unmerged commits in %s%s" - n name - (if (member ref excluded) - " (normally ignored)" - "")) - 'magit-wash-log - "log" - (format "--max-count=%s" magit-log-cutoff-length) - "--pretty=oneline" - (format "%s..%s" head ref) - "--")))) - (magit-set-section-info ref section))))))))) + (setq magit-wazzup-head head) + (setq magit-wazzup-all-p all) + (let ((branch-desc (or head "(detached) HEAD"))) + (unless head (setq head "HEAD")) + (magit-create-buffer-sections + (magit-with-section 'wazzupbuf nil + (insert (format "Wazzup, %s\n\n" branch-desc)) + (let* ((excluded (magit-file-lines ".git/info/wazzup-exclude")) + (all-branches (magit-list-interesting-refs)) + (branches (if all all-branches + (delq nil (mapcar + (lambda (b) + (and (not + (member (cdr b) excluded)) + b)) + all-branches)))) + (reported (make-hash-table :test #'equal))) + (dolist (branch branches) + (let* ((name (car branch)) + (ref (cdr branch)) + (hash (magit-rev-parse ref)) + (reported-branch (gethash hash reported))) + (unless (or (and reported-branch + (string= (file-name-nondirectory ref) + reported-branch)) + (not (magit-git-string "merge-base" head ref))) + (puthash hash (file-name-nondirectory ref) reported) + (let* ((n (length (magit-git-lines "log" "--pretty=oneline" + (concat head ".." ref)))) + (section + (let ((magit-section-hidden-default t)) + (magit-git-section + (cons ref 'wazzup) + (format "%s unmerged commits in %s%s" + n name + (if (member ref excluded) + " (normally ignored)" + "")) + 'magit-wash-log + "log" + (format "--max-count=%s" magit-log-cutoff-length) + "--abbrev-commit" + (format "--abbrev=%s" magit-sha1-abbrev-length) + "--graph" + "--pretty=oneline" + (format "%s..%s" head ref) + "--")))) + (magit-set-section-info ref section)))))))))) + +(define-derived-mode magit-wazzup-mode magit-mode "Magit Wazzup" + "Mode for looking at commits that could be merged from other branches. + +\\{magit-wazzup-mode-map}" + :group 'magit) (defun magit-wazzup (&optional all) (interactive "P") - (let* ((topdir (magit-get-top-dir default-directory))) - (switch-to-buffer "*magit-wazzup*") - (magit-mode-init topdir 'wazzup - #'magit-refresh-wazzup-buffer - (magit-get-current-branch) all))) + (let ((topdir (magit-get-top-dir default-directory)) + (current-branch (magit-get-current-branch))) + (magit-buffer-switch "*magit-wazzup*") + (magit-mode-init topdir 'magit-wazzup-mode + #'magit-refresh-wazzup-buffer + current-branch all))) ;;; Miscellaneous (defun magit-ignore-file (file edit local) (let ((ignore-file (if local ".git/info/exclude" ".gitignore"))) (if edit - (setq file (read-string "File to ignore: " file))) - (append-to-file (concat "/" file "\n") nil ignore-file) + (setq file (read-string "File to ignore: " file))) + (with-temp-buffer + (when (file-exists-p ignore-file) + (insert-file-contents ignore-file)) + (goto-char (point-max)) + (unless (bolp) + (insert "\n")) + (insert "/" file "\n") + (write-region nil nil ignore-file)) (magit-need-refresh))) (defun magit-ignore-item () @@ -3039,48 +5062,61 @@ Prefix arg means justify as well." (defun magit-discard-diff (diff stagedp) (let ((kind (magit-diff-item-kind diff)) - (file (magit-diff-item-file diff))) + (file (magit-diff-item-file diff))) (cond ((eq kind 'deleted) - (when (yes-or-no-p (format "Resurrect %s? " file)) - (magit-run-git "reset" "-q" "--" file) - (magit-run-git "checkout" "--" file))) - ((eq kind 'new) - (if (yes-or-no-p (format "Delete %s? " file)) - (magit-run-git "rm" "-f" "--" file))) - (t - (if (yes-or-no-p (format "Discard changes to %s? " file)) - (if stagedp - (magit-run-git "checkout" "HEAD" "--" file) - (magit-run-git "checkout" "--" file))))))) + (when (yes-or-no-p (format "Resurrect %s? " file)) + (magit-run-git "reset" "-q" "--" file) + (magit-run-git "checkout" "--" file))) + ((eq kind 'new) + (if (yes-or-no-p (format "Delete %s? " file)) + (magit-run-git "rm" "-f" "--" file))) + (t + (if (yes-or-no-p (format "Discard changes to %s? " file)) + (if stagedp + (magit-run-git "checkout" "HEAD" "--" file) + (magit-run-git "checkout" "--" file))))))) (defun magit-discard-item () (interactive) (magit-section-action (item info "discard") ((untracked file) - (if (yes-or-no-p (format "Delete %s? " info)) - (magit-run "rm" info))) + (when (yes-or-no-p (format "Delete %s? " info)) + (if (and (file-directory-p info) + (not (file-symlink-p info))) + (magit-delete-directory info 'recursive) + (delete-file info)) + (magit-refresh-buffer))) ((untracked) (if (yes-or-no-p "Delete all untracked files and directories? ") - (magit-run "git" "clean" "-df"))) + (magit-run-git "clean" "-df"))) ((unstaged diff hunk) (when (yes-or-no-p (if (magit-use-region-p) - "Discard changes in region? " - "Discard hunk? ")) + "Discard changes in region? " + "Discard hunk? ")) (magit-apply-hunk-item-reverse item))) ((staged diff hunk) (if (magit-file-uptodate-p (magit-diff-item-file - (magit-hunk-item-diff item))) - (when (yes-or-no-p (if (magit-use-region-p) - "Discard changes in region? " - "Discard hunk? ")) - (magit-apply-hunk-item-reverse item "--index")) - (error "Can't discard this hunk. Please unstage it first."))) + (magit-hunk-item-diff item))) + (when (yes-or-no-p (if (magit-use-region-p) + "Discard changes in region? " + "Discard hunk? ")) + (magit-apply-hunk-item-reverse item "--index")) + (error "Can't discard this hunk. Please unstage it first"))) ((unstaged diff) (magit-discard-diff item nil)) ((staged diff) (if (magit-file-uptodate-p (magit-diff-item-file item)) - (magit-discard-diff item t) - (error "Can't discard staged changes to this file. Please unstage it first."))) + (magit-discard-diff item t) + (error "Can't discard staged changes to this file. Please unstage it first"))) + ((diff diff) + (save-excursion + (magit-goto-parent-section) + (magit-discard-item))) + ((diff diff hunk) + (save-excursion + (magit-goto-parent-section) + (magit-goto-parent-section) + (magit-discard-item))) ((hunk) (error "Can't discard this hunk")) ((diff) @@ -3089,26 +5125,70 @@ Prefix arg means justify as well." (when (yes-or-no-p "Discard stash? ") (magit-run-git "stash" "drop" info))))) -(defun magit-visit-item () - (interactive) +(defun magit-add-change-log-entry (&optional whoami file-name other-window + new-entry put-new-entry-on-new-line) + "Add a change log entry for current change." + (interactive (list current-prefix-arg + (prompt-for-change-log-name))) + (let ((marker + (save-window-excursion + (magit-visit-item) + (set-marker (make-marker) (point))))) + (save-excursion + (with-current-buffer (marker-buffer marker) + (goto-char marker) + (if (>= (magit-max-args-internal 'add-change-log-entry) 5) + (add-change-log-entry whoami file-name other-window + new-entry put-new-entry-on-new-line) + (add-change-log-entry whoami file-name other-window new-entry) + (if put-new-entry-on-new-line + (display-warning 'magit (format "Emacs %s does not support `put-new-entry-on-new-line' option to `add-change-log-entry'" emacs-version)))))))) + +(defun magit-add-change-log-entry-other-window (&optional whoami file-name) + "Add a change log entry for current change in other window." + (interactive (if current-prefix-arg + (list current-prefix-arg + (prompt-for-change-log-name)))) + (magit-add-change-log-entry whoami file-name t)) + +(defun magit-visit-item (&optional other-window) + "Visit current item. +With a prefix argument, visit in other window." + (interactive (list current-prefix-arg)) (magit-section-action (item info "visit") ((untracked file) - (find-file info)) + (funcall + (if other-window 'find-file-other-window 'find-file) + info)) ((diff) - (find-file (magit-diff-item-file item))) + (let ((file (magit-diff-item-file item))) + (cond ((not (file-exists-p file)) + (error "Can't visit deleted file: %s" file)) + ((file-directory-p file) + (magit-status file)) + (t + (funcall + (if other-window 'find-file-other-window 'find-file) + file))))) ((hunk) (let ((file (magit-diff-item-file (magit-hunk-item-diff item))) - (line (magit-hunk-item-target-line item))) - (find-file file) - (goto-line line))) + (line (magit-hunk-item-target-line item))) + (if (not (file-exists-p file)) + (error "Can't visit deleted file: %s" file)) + (funcall + (if other-window 'find-file-other-window 'find-file) + file) + (goto-char (point-min)) + (forward-line (1- line)))) ((commit) - (magit-show-commit info) - (pop-to-buffer "*magit-commit*")) + (magit-show-commit info nil nil 'select)) ((stash) (magit-show-stash info) - (pop-to-buffer "*magit-diff*")) - ((topic) - (magit-checkout info)))) + (pop-to-buffer magit-stash-buffer-name)) + ((branch) + (magit-checkout (assoc-default 'branch info))) + ((longer) + (magit-log-show-more-entries ())))) (defun magit-show-item-or-scroll-up () (interactive) @@ -3137,19 +5217,19 @@ Prefix arg means justify as well." (magit-section-action (item info "mark") ((commit) (magit-set-marked-commit (if (eq magit-marked-commit info) - nil - info)))))) + nil + info)))))) (defun magit-describe-item () (interactive) (let ((section (magit-current-section))) (message "Section: %s %s-%s %S %S %S" - (magit-section-type section) - (magit-section-beginning section) - (magit-section-end section) - (magit-section-title section) - (magit-section-info section) - (magit-section-context-type section)))) + (magit-section-type section) + (magit-section-beginning section) + (magit-section-end section) + (magit-section-title section) + (magit-section-info section) + (magit-section-context-type section)))) (defun magit-copy-item-as-kill () "Copy sha1 of commit at point into kill ring." @@ -3159,54 +5239,364 @@ Prefix arg means justify as well." (kill-new info) (message "%s" info)))) +(eval-when-compile (require 'server)) + +(defun magit-server-running-p () + "Test whether server is running (works with < 23 as well). + +Return values: + nil the server is definitely not running. + t the server seems to be running. + something else we cannot determine whether it's running without using + commands which may have to wait for a long time." + (require 'server) + (if (functionp 'server-running-p) + (server-running-p) + (condition-case nil + (if server-use-tcp + (with-temp-buffer + (insert-file-contents-literally (expand-file-name server-name server-auth-dir)) + (or (and (looking-at "127\\.0\\.0\\.1:[0-9]+ \\([0-9]+\\)") + (assq 'comm + (process-attributes + (string-to-number (match-string 1)))) + t) + :other)) + (delete-process + (make-network-process + :name "server-client-test" :family 'local :server nil :noquery t + :service (expand-file-name server-name server-socket-dir))) + t) + (file-error nil)))) + (defun magit-interactive-rebase () "Start a git rebase -i session, old school-style." (interactive) - (server-start) + (unless (magit-server-running-p) + (server-start)) (let* ((section (get-text-property (point) 'magit-section)) - (commit (and (member 'commit (magit-section-context-type section)) - (magit-section-info section))) - (old-editor (getenv "GIT_EDITOR"))) - (setenv "GIT_EDITOR" (expand-file-name "emacsclient" exec-directory)) + (commit (and (member 'commit (magit-section-context-type section)) + (magit-section-info section))) + (old-editor (getenv "GIT_EDITOR"))) + (setenv "GIT_EDITOR" (locate-file "emacsclient" exec-path)) (unwind-protect - (magit-run-git-async "rebase" "-i" - (or (and commit (concat commit "^")) - (read-string "Interactively rebase to: "))) + (magit-run-git-async "rebase" "-i" + (or (and commit (concat commit "^")) + (magit-read-rev "Interactively rebase to" (magit-guess-branch)))) (if old-editor - (setenv "GIT_EDITOR" old-editor))))) + (setenv "GIT_EDITOR" old-editor))))) -(defun magit-show-branches () - "Show all of the current branches in other-window." +(define-derived-mode magit-branch-manager-mode magit-mode "Magit Branch" + "Magit Branches") + +(defun magit-quit-window (&optional kill-buffer) + "Bury the buffer and delete its window. With a prefix argument, kill the +buffer instead." + (interactive "P") + (quit-window kill-buffer (selected-window))) + +(defun magit--branch-name-from-section (branch) + "Extract the branch name from the specified magit-section of type 'branch" + (assoc-default 'branch (magit-section-info branch))) + +(defun magit--branch-name-at-point () + "Get the branch name in the line at point." + (let ((branch (magit--branch-name-from-section (magit-current-section)))) + (or branch (error "No branch at point")))) + +(defun magit-remove-remote (ref) + "Return REF with any remote part removed." + (if (string-match "^remotes/" ref) + (substring ref 8) + ref)) + +(defun magit-remove-branch (&optional force) + "Remove the branch in the line at point. +With prefix force the removal even it it hasn't been merged." + (interactive "P") + (let* ((branch-section (magit-current-section)) + (args (list "branch" + (if force "-D" "-d") + (when (magit--is-branch-section-remote branch-section) "-r") + (magit-remove-remote (magit--branch-name-from-section branch-section))))) + (if (and (magit--is-branch-section-remote branch-section) + (yes-or-no-p "Remove branch in remote repository as well? ")) + (magit-remove-branch-in-remote-repo (magit--branch-name-from-section branch-section)) + (apply 'magit-run-git (remq nil args))))) + +(defun magit--remotes () + "Return a list of names for known remotes." + (magit-git-lines "remote")) + +(defun magit--branches-for-remote-repo (remote) + "Return a list of remote branch names for REMOTE. +These are the branch names with the remote name stripped." + (remq nil + (mapcar (lambda (line) + (save-match-data + (if (and (not (string-match-p " -> " line)) + (string-match (concat "^ +" remote "/\\([^ $]+\\)") + line)) + (match-string 1 line)))) + (magit-git-lines "branch" "-r")))) + +(defun magit-remove-branch-in-remote-repo (&optional branch-name-at-local) + "Remove a branch in a remote repository by pushing nothing into it. +If BRANCH-NAME-AT-LOCAL is not given then ask the user for the +name of the remote and branch name. The remote must be known to git." (interactive) - (save-selected-window - (switch-to-buffer-other-window "*magit-branches*") - (erase-buffer) - (insert (magit-git-string "branch" "-va")) - (insert "\n"))) + (let ((all-remotes (magit--remotes)) + remote branch) + (unless all-remotes + (error "No remote has been configured")) + (if branch-name-at-local + (save-match-data + (if (string-match "^remotes/\\([^/]+\\)/\\(.+\\)" branch-name-at-local) + (setq remote (match-string 1 branch-name-at-local) + branch (match-string 2 branch-name-at-local)) + (error "Cannot parse remote and branch name from `%s'" branch-name-at-local))) + (setq remote (magit-completing-read "Name of remote repository: " all-remotes nil t) + branch (magit-completing-read "Name of branch in remote repository: " + (magit--branches-for-remote-repo remote)))) + (unless (magit-get "remote" remote "url") + (error "Unknown remote")) + (magit-run-git "push" + remote + (concat ":refs/heads/" branch)))) + +(defvar magit-branches-buffer-name "*magit-branches*") + +(defun magit--is-branch-at-point-remote () + "Return t if the branch at point is a remote tracking branch" + (magit--is-branch-section-remote (magit-current-section))) + +(defun magit--is-branch-section-remote (branch) + (assoc-default 'remote (magit-section-info branch))) + +(defun magit-wash-branch-line (&optional remote-name) + (looking-at (concat + "^\\([ *] \\)" ; 1: current branch marker + "\\(.+?\\) +" ; 2: branch name + + "\\(?:" + + "\\([0-9a-fA-F]+\\)" ; 3: sha1 + " " + "\\(?:\\[" + "\\([^:]+?\\)" ; 4: tracking (non-greedy + to avoid matching \n) + "\\(?:: \\)?" + "\\(?:ahead \\([0-9]+\\)\\)?" ; 5: ahead + "\\(?:, \\)?" + "\\(?:behind \\([0-9]+\\)\\)?" ; 6: behind + "\\] \\)?" + "\\(?:.*\\)" ; message + + "\\|" ; or + + "-> " ; the pointer to + "\\(.+\\)" ; 7: a ref + + "\\)\n")) + + (let* ((current-string (match-string 1)) + (branch (match-string 2)) + (sha1 (match-string 3)) + (tracking (match-string 4)) + (ahead (match-string 5)) + (behind (match-string 6)) + (other-ref (match-string 7)) + (current (string-match-p "^\\*" current-string)) + (remote (string-match-p "^remotes\\/" branch))) + + ; the current line is deleted before being reconstructed + (delete-region (point) + (line-beginning-position 2)) + + (magit-with-section branch 'branch + (magit-set-section-info (list (cons 'branch branch) + (cons 'remote remote))) + (insert-before-markers + ; sha1 + (propertize (or sha1 + (make-string magit-sha1-abbrev-length ? )) + 'face 'magit-log-sha1) + " " + ; current marker + (if current + "# " + " ") + ; branch name + (apply 'propertize (if remote + ; getting rid of substring "remotes/<remote-name>/" + ; this represents 9 characters plus the lenght of <remote-name> + (substring branch (+ 9 (length remote-name))) + branch) + (if current + '(face magit-branch))) + ; other ref that this branch is pointing to + (if other-ref + (concat " -> " (substring other-ref (+ 1 (length remote-name)))) + "") + ; tracking information + (if tracking + (concat " [" + ; getting rid of the tracking branch name if it is the same as the branch name + (let* ((tracking-remote (magit-get "branch" branch "remote")) + (tracking-branch (substring tracking (+ 1 (length tracking-remote))))) + (propertize (if (string= branch tracking-branch) + (concat "@ " tracking-remote) + (concat tracking-branch " @ " tracking-remote)) + 'face 'magit-log-head-label-remote)) + ; ahead/behind information + (if (or ahead + behind) + ": " + "") + (if ahead + (concat "ahead " + (propertize ahead + 'face (if current + 'magit-branch)) + (if behind + ", " + "")) + "") + (if behind + (concat "behind " + (propertize behind + 'face 'magit-log-head-label-remote)) + "") + "]") + "") + "\n")))) + +(defun magit-insert-branch-sub-group (sub-group) + (let* ((remote (first sub-group)) + (remote-name (if remote (first remote))) + (remote-url (if remote (second remote))) + (title (if remote (concat remote-name " (" remote-url ")"))) + (end-marker (second sub-group))) + (if title + (let ((magit-section-hidden-default t)) + (magit-with-section title nil + (insert-before-markers (concat " " + title + "\n")) + (save-restriction + (narrow-to-region (point) end-marker) + (magit-wash-sequence (apply-partially 'magit-wash-branch-line remote-name))))) + (save-restriction + (narrow-to-region (point) end-marker) + (magit-wash-sequence #'magit-wash-branch-line))))) + +(defun magit-insert-branch-group (group) + (let ((title (car group)) + (buffer-title (cadr group)) + (sub-groups (caddr group))) + (magit-with-section title nil + (insert-before-markers (propertize buffer-title 'face 'magit-section-title) "\n") + (mapc 'magit-insert-branch-sub-group sub-groups))) + (insert-before-markers "\n")) + +(defun magit-wash-branches () + ; get the names of the remotes + (let* ((remotes (magit-git-lines "remote")) + (remotes-urls (mapcar (lambda (remote) + (magit-get "remote" remote "url")) remotes)) + ; get the location of remotes in the buffer + (markers + (append (mapcar (lambda (remote) + (save-excursion + (search-forward-regexp (concat "^ remotes\\/" remote)) + (beginning-of-line) + (point-marker))) + remotes) + (list (save-excursion + (goto-char (point-max)) + (point-marker))))) + ; list of elements to display in the buffer + (groups `(("local" "Local branches:" ((nil ,(car markers)))) + ("remote" "Remote branches:" ,(loop for remote in remotes + for remote-url in remotes-urls + for end-marker in (cdr markers) + collect (list (list remote remote-url) end-marker)))))) + + ; actual displaying of information + (mapc 'magit-insert-branch-group groups) + ; make sure markers point to nil so that they can be garbage collected + (mapc (lambda (marker) + (set-marker marker nil)) + markers))) + +(defun magit-refresh-branch-manager () + (magit-create-buffer-sections + (magit-git-section "branches" nil 'magit-wash-branches + "branch" + "-vva" + (format "--abbrev=%s" magit-sha1-abbrev-length)))) + +(magit-define-command branch-manager () + (interactive) + (let ((topdir (magit-get-top-dir default-directory))) + (magit-buffer-switch magit-branches-buffer-name) + (magit-mode-init topdir 'magit-branch-manager-mode #'magit-refresh-branch-manager))) + +(defun magit-change-what-branch-tracks () + "Change which remote branch the current branch tracks." + (interactive) + (if (magit--is-branch-at-point-remote) + (error "Cannot modify a remote branch")) + (let* ((local-branch (magit--branch-name-at-point)) + (new-tracked (magit-read-rev "Change tracked branch to" + nil + (lambda (ref) + (not (string-match-p "refs/remotes/" + ref))))) + new-remote new-branch) + (unless (string= (or new-tracked "") "") + (cond (;; Match refs that are unknown in the local repository if + ;; `magit-remote-ref-format' is set to + ;; `name-then-remote'. Can be useful if you want to + ;; create a new branch in a remote repository. + (string-match "^\\([^ ]+\\) +(\\(.+\\))$" ; 1: branch name; 2: remote name + new-tracked) + (setq new-remote (match-string 2 new-tracked) + new-branch (concat "refs/heads/" (match-string 1 new-tracked)))) + ((string-match "^\\(?:refs/remotes/\\)?\\([^/]+\\)/\\(.+\\)" ; 1: remote name; 2: branch name + new-tracked) + (setq new-remote (match-string 1 new-tracked) + new-branch (concat "refs/heads/" (match-string 2 new-tracked)))) + (t (error "Cannot parse the remote and branch name")))) + (magit-set new-remote "branch" local-branch "remote") + (magit-set new-branch "branch" local-branch "merge") + (magit-branch-manager) + (if (string= (magit-get-current-branch) local-branch) + (magit-refresh-buffer (magit-find-status-buffer default-directory))))) (defvar magit-ediff-file) -(defvar magit-ediff-windows) (defun magit-interactive-resolve (file) + (require 'ediff) (let ((merge-status (magit-git-string "ls-files" "-u" "--" file)) - (base-buffer (generate-new-buffer (concat file ".base"))) - (our-buffer (generate-new-buffer (concat file ".current"))) - (their-buffer (generate-new-buffer (concat file ".merged"))) - (windows (current-window-configuration))) + (base-buffer (generate-new-buffer (concat file ".base"))) + (our-buffer (generate-new-buffer (concat file ".current"))) + (their-buffer (generate-new-buffer (concat file ".merged"))) + (windows (current-window-configuration))) (if (null merge-status) - (error "Cannot resolve %s" file)) + (error "Cannot resolve %s" file)) (with-current-buffer base-buffer (if (string-match "^[0-9]+ [0-9a-f]+ 1" merge-status) - (insert (magit-git-string "cat-file" "blob" - (concat ":1:" file))))) + (insert (magit-git-output `("cat-file" "blob" ,(concat ":1:" file)))))) (with-current-buffer our-buffer (if (string-match "^[0-9]+ [0-9a-f]+ 2" merge-status) - (insert (magit-git-string "cat-file" "blob" - (concat ":2:" file))))) + (insert (magit-git-output `("cat-file" "blob" ,(concat ":2:" file))))) + (let ((buffer-file-name file)) + (normal-mode))) (with-current-buffer their-buffer (if (string-match "^[0-9]+ [0-9a-f]+ 3" merge-status) - (insert (magit-git-string "cat-file" "blob" - (concat ":3:" file))))) + (insert (magit-git-output `("cat-file" "blob" ,(concat ":3:" file))))) + (let ((buffer-file-name file)) + (normal-mode))) ;; We have now created the 3 buffer with ours, theirs and the ancestor files (with-current-buffer (ediff-merge-buffers-with-ancestor our-buffer their-buffer base-buffer) (make-local-variable 'magit-ediff-file) @@ -3215,24 +5605,27 @@ Prefix arg means justify as well." (setq magit-ediff-windows windows) (make-local-variable 'ediff-quit-hook) (add-hook 'ediff-quit-hook - (lambda () - (let ((buffer-A ediff-buffer-A) - (buffer-B ediff-buffer-B) - (buffer-C ediff-buffer-C) - (buffer-Ancestor ediff-ancestor-buffer) - (file magit-ediff-file) - (windows magit-ediff-windows)) - (ediff-cleanup-mess) - (find-file file) - (erase-buffer) - (insert-buffer-substring buffer-C) - (kill-buffer buffer-A) - (kill-buffer buffer-B) - (kill-buffer buffer-C) - (when (bufferp buffer-Ancestor) (kill-buffer buffer-Ancestor)) - (set-window-configuration windows) - (message "Conflict resolution finished; you may save the buffer"))))))) - + (lambda () + (let ((buffer-A ediff-buffer-A) + (buffer-B ediff-buffer-B) + (buffer-C ediff-buffer-C) + (buffer-Ancestor ediff-ancestor-buffer) + (file magit-ediff-file) + (file-buffer) + (windows magit-ediff-windows)) + (ediff-cleanup-mess) + (find-file file) + (setq file-buffer (current-buffer)) + (erase-buffer) + (insert-buffer-substring buffer-C) + (kill-buffer buffer-A) + (kill-buffer buffer-B) + (kill-buffer buffer-C) + (when (bufferp buffer-Ancestor) (kill-buffer buffer-Ancestor)) + (set-window-configuration windows) + (magit-save-some-buffers + "Conflict resolution finished; you may save the buffer" + (lambda () (eq (current-buffer) file-buffer))))))))) (defun magit-interactive-resolve-item () (interactive) @@ -3240,5 +5633,77 @@ Prefix arg means justify as well." ((diff) (magit-interactive-resolve (cadr info))))) +(defun magit-submodule-update (&optional init) + "Update the submodule of the current git repository + +With a prefix arg, do a submodule update --init" + (interactive "P") + (let ((default-directory (magit-get-top-dir default-directory))) + (apply #'magit-run-git-async "submodule" "update" (if init '("--init") ())))) + +(defun magit-submodule-update-init () + "Update and init the submodule of the current git repository." + (interactive) + (magit-submodule-update t)) + +(defun magit-submodule-init () + "Initialize the submodules" + (interactive) + (let ((default-directory (magit-get-top-dir default-directory))) + (magit-run-git-async "submodule" "init"))) + +(defun magit-submodule-sync () + "Synchronizes submodules' remote URL configuration" + (interactive) + (let ((default-directory (magit-get-top-dir default-directory))) + (magit-run-git-async "submodule" "sync"))) + +(defun magit-run-git-gui () + "Run `git gui' for the current git repository" + (interactive) + (let* ((default-directory (magit-get-top-dir default-directory))) + (magit-start-process "Git Gui" nil magit-git-executable "gui"))) + +(defun magit-run-gitk () + "Run `gitk --all' for the current git repository" + (interactive) + (let ((default-directory (magit-get-top-dir default-directory))) + (cond + ((eq system-type 'windows-nt) + ;; Gitk is a shell script, and Windows doesn't know how to + ;; "execute" it. The Windows version of Git comes with an + ;; implementation of "sh" and everything else it needs, but + ;; Windows users might not have added the directory where it's + ;; installed to their path + (let ((git-bin-dir (file-name-directory magit-gitk-executable)) + (exec-path exec-path) + (process-environment process-environment)) + (when git-bin-dir + ;; Adding it onto the end so that anything the user + ;; specified will get tried first. Emacs looks in + ;; exec-path; PATH is the environment variable inherited by + ;; the process. I need to change both. + (setq exec-path (append exec-path (list git-bin-dir))) + (push (format "PATH=%s;%s" + (getenv "PATH") + (replace-regexp-in-string "/" "\\\\" git-bin-dir)) + process-environment)) + (magit-start-process "Gitk" nil "sh" magit-gitk-executable "--all"))) + (t + (magit-start-process "Gitk" nil magit-gitk-executable "--all"))))) + +(defun magit-load-config-extensions () + "Try to load magit extensions that are defined at git config +layer. This can be added to `magit-mode-hook' for example" + (dolist (ext (magit-get-all "magit.extension")) + (let ((sym (intern (format "magit-%s-mode" ext)))) + (when (fboundp sym) + (funcall sym 1))))) (provide 'magit) + +;; rest of magit core +(require 'magit-key-mode) +(require 'magit-bisect) + +;;; magit.el ends here |