diff --git a/NEWS b/NEWS index 7408c538..d4d14951 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,10 @@ Changes in magit 0.6: +* Key bindings and command behavior for diffing, history listing, and + resetting have changed. Please see the manual. + +* All buffers created by Magit are now in magit-mode and share all key + bindings. + * Bug fix: avoid looking-at-p so that magit.el works on Emacs 22 as well. diff --git a/magit.el b/magit.el index 322cc348..0f3f84f1 100644 --- a/magit.el +++ b/magit.el @@ -34,6 +34,8 @@ ;;; TODO ;; - Tags +;; - Unified keymaps for status and history, etc +;; - Equivalent of interactive rebase ;; - 'Subsetting', only looking at a subset of all files. ;; - Detect and handle renames and copies. @@ -117,6 +119,71 @@ (put-text-property (line-beginning-position) (line-end-position) prop val)) +;;; Revisions and ranges + +(defun magit-list-interesting-revisions () + (magit-shell-lines "git branch -a | cut -c3-")) + +(defun magit-read-rev (prompt &optional def) + (let* ((prompt (if def + (format "%s (default %s): " prompt def) + (format "%s: " prompt))) + (rev (completing-read prompt (magit-list-interesting-revisions) + nil nil nil nil def))) + (if (string= rev "") + nil + rev))) + +(defun magit-read-rev-range (op &optional def-beg def-end) + (if current-prefix-arg + (read-string (format "%s range: " op)) + (let ((beg (magit-read-rev (format "%s start" op) + def-beg))) + (if (not beg) + nil + (let ((end (magit-read-rev (format "%s end" op) def-end))) + (cons beg end)))))) + +(defun magit-rev-to-git (rev) + (or rev + (error "No revision specified")) + (if (string= rev ".") + (or (magit-marked-object) + (error "Commit mark not set")) + rev)) + +(defun magit-rev-range-to-git (range) + (or range + (error "No revision range specified")) + (if (stringp range) + range + (if (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) + (or rev + (error "No revision specified")) + (if (string= rev ".") + "mark" + (magit-name-rev rev))) + +(defun magit-rev-range-describe (range things) + (or range + (error "No revision range specified")) + (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 at %s" things (magit-rev-describe (car range)))))) + +(defun magit-default-rev () + (magit-commit-at-point t)) + ;;; Sections (defun magit-insert-section (section title washer cmd &rest args) @@ -237,10 +304,14 @@ (define-key map (kbd "?") 'magit-describe-thing-at-point) (define-key map (kbd ".") 'magit-mark-thing-at-point) (define-key map (kbd "=") 'magit-diff-with-mark) - (define-key map (kbd "x") 'magit-reset-soft) - (define-key map (kbd "X") 'magit-reset-hard) + (define-key map (kbd "l") 'magit-log-head) + (define-key map (kbd "L") 'magit-log) + (define-key map (kbd "d") 'magit-diff-working-tree) + (define-key map (kbd "D") 'magit-diff) + (define-key map (kbd "x") 'magit-reset-head) + (define-key map (kbd "X") 'magit-reset-working-tree) (define-key map (kbd "RET") 'magit-visit-thing-at-point) - (define-key map (kbd "b") 'magit-switch-branch) + (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) @@ -249,19 +320,20 @@ (define-key map (kbd "P") 'magit-push) (define-key map (kbd "c") 'magit-log-edit) (define-key map (kbd "C") 'magit-add-log) - (define-key map (kbd "l") 'magit-browse-log) - (define-key map (kbd "L") 'magit-browse-branch-log) - (define-key map (kbd "d") 'magit-diff-with-branch) (define-key map (kbd "p") 'magit-display-process) map)) (defvar magit-mode-hook nil) (put 'magit-mode 'mode-class 'special) -(put 'magit-marked-object 'permanent-local t) (defvar magit-marked-object nil) (make-variable-buffer-local 'magit-marked-object) +(put 'magit-marked-object 'permanent-local t) + +(defvar magit-submode nil) +(make-variable-buffer-local 'magit-submode) +(put 'magit-submode 'permanent-local t) (defun magit-mode () ;;; XXX - the formatting is all screwed up because of the \\[...] @@ -325,13 +397,13 @@ that are being merged at the top. You can `soft reset' your repository by typing `\\[magit-reset-soft]'. The current head will be set to the -commit that you specify, but your working tree and the staging -area are not changed. Typing `\\[magit-reset-hard]' will do a -`hard reset': all of the current head, your working tree, and the -the index will be reverted to the commit that you specify. Doing -a hard reset without actually changing the current head will thus -throw away all your uncommitted changes. You can do this to -abort a merge, for example. +commit at point, but your working tree and the staging area are +not changed. Typing `\\[magit-reset-hard]' will do a `hard +reset': all of the current head, your working tree, and the the +index will be reverted to the commit at point. Doing a hard +reset without actually changing the current head will thus throw +away all your uncommitted changes. You can do this to abort a +merge, for example. When you have a remote repository configured for the current branch (such as when \"git clone\" has done this for you @@ -348,10 +420,16 @@ pushed. (setq buffer-read-only t) (setq major-mode 'magit-mode mode-name "Magit" - mode-line-process "") + mode-line-process "" + truncate-lines t) (use-local-map magit-mode-map) (run-mode-hooks 'magit-mode-hook)) +(defun magit-mode-init (dir submode) + (setq default-directory dir + magit-submode submode) + (magit-mode)) + ;;; Status (defun magit-wash-other-files (status) @@ -470,15 +548,19 @@ pushed. (magit-goto-section old-section)) (magit-refresh-marks-in-buffer buf))) -(defun magit-find-status-buffer (&optional dir) +(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 (equal default-directory topdir) - (eq major-mode 'magit-mode))) + (eq major-mode 'magit-mode) + (eq magit-submode submode))) (return buf))))) +(defun magit-find-status-buffer (&optional dir) + (magit-find-buffer 'status dir)) + (defun magit-status (dir) (interactive (list (magit-read-top-dir current-prefix-arg))) (save-some-buffers) @@ -487,8 +569,7 @@ pushed. (create-file-buffer (file-name-nondirectory (directory-file-name topdir)))))) (switch-to-buffer buf) - (setq default-directory topdir) - (magit-mode) + (magit-mode-init topdir 'status) (magit-update-status buf))) ;;; Staging @@ -584,39 +665,37 @@ pushed. ;;; Branches -(defun magit-list-branches () - (magit-shell-lines "git branch -a | cut -c3-")) - -(defun magit-read-rev (prompt) - (completing-read prompt (magit-list-branches))) - -(defun magit-switch-branch (branch) - (interactive (list (magit-read-rev "Switch to branch: "))) - (if (and branch (not (string= branch ""))) - (magit-run "git" "checkout" branch))) +(defun magit-checkout (rev) + (interactive (list (magit-read-rev "Switch to" (magit-default-rev)))) + (if rev + (magit-run "git" "checkout" (magit-rev-to-git rev)))) (defun magit-read-create-branch-args () - (let* ((branches (magit-list-branches)) - (cur-branch (magit-get-current-branch)) + (let* ((cur-branch (magit-get-current-branch)) (branch (read-string "Create branch: ")) - (parent (completing-read "Parent: " branches nil t cur-branch))) + (parent (magit-read-rev "Parent" cur-branch))) (list branch parent))) (defun magit-create-branch (branch parent) (interactive (magit-read-create-branch-args)) (if (and branch (not (string= branch "")) - parent (not (string= parent ""))) - (magit-run "git" "checkout" "-b" branch parent))) + parent) + (magit-run "git" "checkout" "-b" + branch + (magit-rev-to-git parent)))) ;;; Merging -(defun magit-manual-merge (branch) - (interactive (list (magit-read-rev "Manually merge from branch: "))) - (magit-run "git" "merge" "--no-ff" "--no-commit" branch)) +(defun magit-manual-merge (rev) + (interactive (list (magit-read-rev "Manually merge"))) + (if rev + (magit-run "git" "merge" "--no-ff" "--no-commit" + (magit-rev-to-git rev)))) -(defun magit-automatic-merge (branch) - (interactive (list (magit-read-rev "Merge from branch: "))) - (magit-run "git" "merge" branch)) +(defun magit-automatic-merge (rev) + (interactive (list (magit-read-rev "Merge"))) + (if rev + (magit-run "git" "merge" (magit-rev-to-git branch)))) ;;; Rebasing @@ -636,7 +715,9 @@ pushed. (interactive) (let ((info (magit-rebase-info))) (if (not info) - (magit-run "git" "rebase" (magit-read-rev "Rebase against: ")) + (let ((rev (magit-read-rev "Rebase to"))) + (if rev + (magit-run "git" "rebase" rev))) (let ((cursor-in-echo-area t) (message-log-max nil)) (message "Rebase in progress. Abort, Skip, or Continue? ") @@ -651,17 +732,15 @@ pushed. ;;; Resetting -(defun magit-reset-soft (target) - (interactive (list (read-string "Reset history to: " "HEAD^"))) - (magit-run "git" "reset" "--soft" target)) +(defun magit-reset-head (rev) + (interactive (list (magit-read-rev "Reset head to"))) + (if rev + (magit-run "git" "reset" "--soft" (magit-rev-to-git rev)))) -(defun magit-reset-hard (target) - (interactive (list (read-string "Reset working tree (and history) to: " - "HEAD"))) - (if (yes-or-no-p - (format "Hard reset to %s and throw away all uncommitted changes? " - target)) - (magit-run "git" "reset" "--hard" target))) +(defun magit-reset-working-tree () + (interactive) + (if (yes-or-no-p "Discard all uncommitted changes? ") + (magit-run "git" "reset" "--hard"))) ;;; Push and pull @@ -673,7 +752,7 @@ pushed. (interactive) (magit-run "git" "push" "-v")) -;;; Commit +;;; Committing (defvar magit-log-edit-map nil) @@ -748,76 +827,26 @@ pushed. (open-line 1) (insert (format "(%s): " fun))))))))) -;;; History browsing +;;; Commits -(defvar magit-log-mode-map - (let ((map (make-keymap))) - (suppress-keymap map) - (define-key map (kbd "RET") 'magit-show-commit) - (define-key map (kbd ".") 'magit-mark-thing-at-point) - (define-key map (kbd "=") 'magit-diff-with-mark) - (define-key map (kbd "R") 'magit-revert-commit) - (define-key map (kbd "P") 'magit-pick-commit) - (define-key map (kbd "C") 'magit-checkout-commit) - (define-key map (kbd "l") 'magit-log-commit) - (define-key map (kbd "L") 'magit-browse-branch-log) - (define-key map (kbd "q") 'magit-quit) - map)) - -(defvar magit-log-mode-hook nil) - -(put 'magit-log-mode 'mode-class 'special) - -(defun magit-log-mode () - "Review commit history. \\ - -The buffer shows a summary of the (usually non-linear) history of -changes starting form a given commit. You can see the details of -a the commit on the current line by typing -`\\[magit-show-commit]'. Typing `\\[magit-log-commit] will use -the commit on the current line as the new starting point for the -summary. Typing `\\[magit-browse-branch-log]' will ask you for a -branch and show its history. - -You can modify your working tree and staging area by using the -commit on the current line in a number of ways. Typing -`\\[magit-revert-commit]' will revert the change made by the -commit in your working tree (and staging area). Typing -`\\[magit-pick-commit]' will apply the commit. You can use this -to `cherry pick' changes from another branch. - -Typing `\\[magit-checkout-commit]' will checkout the commit on -the current line into your working tree. - -\\{magit-log-mode-map}" - (kill-all-local-variables) - (setq buffer-read-only t) - (toggle-truncate-lines t) - (setq major-mode 'magit-log-mode - mode-name "Magit Log") - (use-local-map magit-log-mode-map) - (run-mode-hooks 'magit-log-mode-hook)) - -(defun magit-commit-at-point () +(defun magit-commit-at-point (&optional nil-ok-p) (let* ((info (get-text-property (point) 'magit-info)) (commit (and info (eq (car info) 'commit) (cadr info)))) - (or commit - (error "No commit at point.")))) + (if nil-ok-p + commit + (or commit + (error "No commit at point."))))) (defun magit-revert-commit () (interactive) (magit-run "git" "revert" "--no-commit" (magit-commit-at-point))) -(defun magit-pick-commit () +(defun magit-apply-commit () (interactive) (magit-run "git" "cherry-pick" "--no-commit" (magit-commit-at-point))) -(defun magit-checkout-commit () - (interactive) - (magit-run "git" "checkout" (magit-commit-at-point))) - (defun magit-log-commit () (interactive) (magit-browse-log (magit-commit-at-point))) @@ -855,50 +884,55 @@ the current line into your working tree. (magit-put-line-property 'magit-info (list 'commit commit)))) (forward-line))) -(defun magit-browse-log (head) - (interactive (list (magit-get-current-branch))) - (let* ((topdir (magit-get-top-dir default-directory))) - (switch-to-buffer "*magit-log*") - (setq default-directory topdir) - (magit-log-mode) - (let ((inhibit-read-only t)) - (save-excursion - (erase-buffer) - (magit-insert-section 'history (format "History of %s" head) - 'magit-wash-log - "git" "log" "--graph" "--max-count=10000" - "--pretty=oneline" head))) - (magit-refresh-marks-in-buffer (current-buffer)))) +(defun magit-log (range) + (interactive (list (magit-read-rev-range "Log" (magit-get-current-branch)))) + (if range + (let* ((topdir (magit-get-top-dir default-directory)) + (args (magit-rev-range-to-git range))) + (switch-to-buffer "*magit-log*") + (magit-mode-init topdir 'log) + (let ((inhibit-read-only t)) + (save-excursion + (erase-buffer) + (magit-insert-section 'history + (magit-rev-range-describe range "Commits") + 'magit-wash-log + "git" "log" "--graph" "--max-count=10000" + "--pretty=oneline" args))) + (magit-refresh-marks-in-buffer (current-buffer))))) -(defun magit-browse-branch-log () +(defun magit-log-head () (interactive) - (magit-browse-log (magit-read-rev "Browse history of branch: "))) - -(defun magit-diff-with-mark () - (interactive) - (let ((commit (magit-commit-at-point)) - (marked (or (magit-marked-object) - (error "Nothing marked.")))) - (magit-show-diff commit marked))) + (magit-log "HEAD")) ;;; Diffing -(defun magit-show-diff (&rest args) - (let ((dir default-directory) - (buf (get-buffer-create "*magit-diff*"))) - (display-buffer buf) - (save-excursion - (set-buffer buf) - (setq buffer-read-only t) - (setq default-directory dir) - (let ((inhibit-read-only t)) - (erase-buffer) - (apply 'magit-insert-section 'diff nil 'magit-wash-diff - "git" "diff" args))))) +(defun magit-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) + (let ((inhibit-read-only t)) + (erase-buffer) + (magit-insert-section 'diff + (magit-rev-range-describe range "Changes") + 'magit-wash-diff + "git" "diff" args)))))) -(defun magit-diff-with-branch (branch) - (interactive (list (magit-read-rev "Diff against: "))) - (magit-show-diff branch)) +(defun magit-diff-working-tree (rev) + (interactive (list (magit-read-rev "Diff with"))) + (if rev + (magit-diff rev))) + +(defun magit-diff-with-mark () + (interactive) + (magit-diff (cons (magit-marked-object) + (magit-commit-at-point)))) ;;; Markers diff --git a/magit.texi b/magit.texi index a11cc2c1..0797e47a 100644 --- a/magit.texi +++ b/magit.texi @@ -57,12 +57,17 @@ put you in Magit's status buffer. You will be using it frequently, so it is probably a good idea to bind @code{magit-status} to a key of your choice. +In addition to the status buffer, Magit will also create buffers that +show lists of commits, buffers with diffs, and other kinds of buffers. +All these buffers are in @code{magit-mode} and have the same key +bindings. Not all commands make sense in all contexts, but a given +key will always do the same thing. + @node Status @chapter Status Running @kbd{M-x magit-status} displays the main interface of Magit, -the status buffer. Almost all operations are initiated with single -letter keystrokes from that buffer. +the status buffer. You can have multiple status buffers active at the same time, each associated with its own Git repository. Running @code{magit-status} @@ -124,9 +129,9 @@ Type @kbd{c} to pop up a buffer where you can write your change description. Once you are happy with the description, type @kbd{C-c C-c} in that buffer to commit the staged changes. -Typing @kbd{C} will also pop up the change description buffer, but it -will also try to insert a ChangeLog-style entry for the change that -point is in. +Typing @kbd{C} will also pop up the change description buffer, but in +addition, it will try to insert a ChangeLog-style entry for the change +that point is in. If the current branch is associated with a remote repository, the status buffer will show a fourth section, named @emph{Unpushed @@ -137,37 +142,46 @@ Pulling} for more information. @node History @chapter History -To browse the repository history, type @kbd{l} or @kbd{L} in the -status buffer. Typing @kbd{l} will show the history starting from the -current head, while @kbd{L} will ask for a starting point. - -A new buffer will be shown that displays the history in a terse form. +To show the repository history of your current head, type @kbd{l}. A +new buffer will be shown that displays the history in a terse form. The first paragraph of each commit message is displayed, next to a representation of the relationships between commits. +Typing @kbd{L} will ask for the starting and end point of the history. +This can be used to show the commits that are in one branch, but not +in another, for example. + You can move point to a commit and then cause various things to happen -with it. +with it. (The following commands work in any list of commit, such as +the one shown in the @emph{Unpushed commits} section.) Typing @kbd{RET} will pop up more information about the current -commit and typing @kbd{l} will use it as the new starting point of the -history buffer. +commit. -Typing @kbd{R} will revert the current commit in your working tree and -staging area. Thus, it will apply the changes made by that commit in -reverse. This is obviously useful to cleanly undo changes that turned -out to be wrong. +Typing @kbd{a} will apply the current commit to your working tree and +staging area. This is useful when you are browsing the history of +some other branch and you want to `cherry-pick' some changes from it +for your current branch. A typical situation is applying selected bug +fixes from the development version of a program to a release branch. -Typing @kbd{P} will apply the current commit in the normal way. This -is useful when you are browsing the history of some other branch and -you want to `cherry-pick' some changes from it for your current -branch. A typical situation is applying selected bug fixes from the -development version of a program to a release branch. +Typing @kbd{v} will revert the current commit. Thus, it will apply +the changes made by that commit in reverse. This is obviously useful +to cleanly undo changes that turned out to be wrong. -Typing @kbd{C} will switch your working tree to the current commit. +Typing @kbd{=} will show the differences from the current commit to +the @dfn{marked} commit. -You can also mark the current commit by typing @kbd{.}. Once you have -marked a commit, you can show the differences between it and the -current commit by typing @kbd{=}. +You can mark the current commit by typing @kbd{.}. Some commands, +such as @kbd{=}, will use the current commit and the marked commit as +implicit arguments. Other commands will offer the marked commit as a +default when prompting for their arguments. + +@node Diffing +@chapter Diffing + +To show the changes from you working tree to another revision, type +@kbd{d}. To show the changes between two arbitrary revisions, type +@kbd{D}. @node Resetting @chapter Resetting @@ -176,37 +190,43 @@ Once you have added a commit to your local repository, you can not change that commit anymore in any way. But you can reset your current head to an earlier commit and start over. -If you have published your history already, rewriting history in this -way can be confusing and should be avoided. However, rewriting your -local history is fine and it is often cleaner to fix mistakes this way -than by reverting commits (with @kbd{R} in the history buffer, for -example). +If you have published your history already, rewriting it in this way +can be confusing and should be avoided. However, rewriting your local +history is fine and it is often cleaner to fix mistakes this way than +by reverting commits (with @kbd{R}, for example). -Magit gives you two ways to reset your current head: soft and hard. -Type @kbd{x} to do a soft reset. This will change the current head to -the commit that you specify, but your current working tree and staging -area will not be touched. This is useful to redoing the last commit -to correct the commit message, for example. +Typing @kbd{x} will ask for a revision and reset your current head to +it. No changes will be made to your working tree and staging area. +Thus, the @emph{Staged changes} section in the status buffer will show +the changes that you have removed from your commit history. You can +commit the changes again as if you had just made them, thus rewriting +history. -Type @kbd{X} to do a hard reset. This will reset the current head to -the commit you specify and will check it out so that your working tree -and staging area will match it. In other words, a hard reset will -throw away the history completely, which can be useful to abort -experimental changes (like merging a branch just to see what happens). +Typing @kbd{x} while point is in a line that describes a commit will +offer this commit as the default revision to reset to. Thus, you can +move point to one of the commits in the @emph{Unpushed commits} +section and hit @kbd{x RET} to reset your current head to it. -In particular, doing a hard reset to HEAD will have no effect on the -current head, but it will reset your working tree and staging area -back to the last committed state. +Type @kbd{X} to reset your working tree and staging area to the most +recently committed state. This will discard your local modifications, +so be careful. @node Branching and Merging @chapter Branching and Merging The current branch is indicated in the header of the status buffer. -You can check out a different branch by typing @kbd{b}. To create a -new branch and check it out immediately, type @kbd{B}. +You can switch to a different branch by typing @kbd{b}. This will +immediately checkout the branch into your working copy, so you +shouldn't have any local modifications when switching branches. -You can also compare your working tree with some other branch. Type -@kbd{d} and then specify the branch to compare with. +Similar to @kbs{x}, typing @kbd{b} while point is at a commit +description will offer that commit as the default to switch to. +This will result in a detached head. + +To create a new branch and switch to it immediately, type @kbd{B}. + +@node Merging +@chapter Merging Magit offers two ways to merge branches: manually and automatic. A manual merge will apply all changes to your working tree and staging @@ -217,9 +237,9 @@ Type @kbd{m} to initiate a manual merge, and type @kbd{M} for a automatic merge. A manual merge is useful when carefully merging a new feature that you -want to review and test before committing it. A automatic merge is -appropriate when you are on a feature branch and want to catch up with -the master, say. +want to review and test before even committing it. A automatic merge +is appropriate when you are on a feature branch and want to catch up +with the master, say. After initiating a manual merge, the header of the status buffer will remind you that the next commit will be a merge commit (with more than