;;
;; To start: `M-x stgit'
+(when (< emacs-major-version 22)
+ (error "Emacs older than 22 is not supported by stgit.el"))
+
(require 'git nil t)
(require 'cl)
(require 'ewoc)
(insert (propertize (if (eq status 'index) "Index" "Work tree")
'face face))
(insert (format "%-30s"
- (propertize (symbol-name name) 'face face))
+ (propertize (symbol-name name)
+ 'face face
+ 'syntax-table (string-to-syntax "w")))
" "
(if (stgit-patch-empty patch) "(empty) " "")
(propertize (or (stgit-patch-desc patch) "")
(interactive)
(let ((inhibit-read-only t)
(curline (line-number-at-pos))
- (curpatch (stgit-patch-name-at-point)))
+ (curpatch (stgit-patch-name-at-point))
+ (curfile (stgit-patched-file-at-point)))
(ewoc-filter stgit-ewoc #'(lambda (x) nil))
(ewoc-set-hf stgit-ewoc
(concat "Branch: "
'face 'stgit-description-face)))
(stgit-run-series stgit-ewoc)
(if curpatch
- (stgit-goto-patch curpatch)
+ (stgit-goto-patch curpatch (and curfile (stgit-file-file curfile)))
(goto-line curline)))
(stgit-refresh-git-status))
"StGit mode face used for unknown file status"
:group 'stgit)
+(defface stgit-ignored-file-face
+ '((((class color) (background light)) (:foreground "grey60"))
+ (((class color) (background dark)) (:foreground "grey40")))
+ "StGit mode face used for ignored files")
+
(defface stgit-file-permission-face
'((((class color) (background light)) (:foreground "green" :bold t))
(((class color) (background dark)) (:foreground "green" :bold t)))
:group 'stgit)
-(defcustom stgit-expand-find-copies-harder
+(defcustom stgit-find-copies-harder
nil
"Try harder to find copied files when listing patches.
(rename "Renamed" stgit-modified-file-face)
(mode-change "Mode change" stgit-modified-file-face)
(unmerged "Unmerged" stgit-unmerged-file-face)
- (unknown "Unknown" stgit-unknown-file-face)))
+ (unknown "Unknown" stgit-unknown-file-face)
+ (ignore "Ignored" stgit-ignored-file-face)))
"Alist of code symbols to description strings")
(defconst stgit-patch-status-face-alist
(let ((code (assoc str '(("A" . add)
("C" . copy)
("D" . delete)
+ ("I" . ignore)
("M" . modify)
("R" . rename)
("T" . mode-change)
(defun stgit-find-copies-harder-diff-arg ()
"Return the flag to use with `git-diff' depending on the
-`stgit-expand-find-copies-harder' flag."
- (if stgit-expand-find-copies-harder
- "--find-copies-harder"
- "-C"))
+`stgit-find-copies-harder' flag."
+ (if stgit-find-copies-harder "--find-copies-harder" "-C"))
+
+(defun stgit-insert-ls-files (args file-flag)
+ (let ((start (point)))
+ (apply 'stgit-run-git
+ (append '("ls-files" "--exclude-standard" "-z") args))
+ (goto-char start)
+ (while (looking-at "\\([^\0]*\\)\0")
+ (let ((name-len (- (match-end 0) (match-beginning 0))))
+ (insert ":0 0 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 " file-flag "\0")
+ (forward-char name-len)))))
(defun stgit-insert-patch-files (patch)
"Expand (show modification of) the patch PATCH after the line
(with-temp-buffer
(apply 'stgit-run-git
(cond ((eq patchsym :work)
- `("diff-files" ,@args))
+ `("diff-files" "-0" ,@args))
((eq patchsym :index)
`("diff-index" ,@args "--cached" "HEAD"))
(t
`("diff-tree" ,@args "-r" ,(stgit-id patchsym)))))
+
+ (when (and (eq patchsym :work))
+ (when stgit-show-ignored
+ (stgit-insert-ls-files '("--ignored" "--others") "I"))
+ (when stgit-show-unknown
+ (stgit-insert-ls-files '("--others") "X"))
+ (sort-regexp-fields nil ":[^\0]*\0\\([^\0]*\\)\0" "\\1"
+ (point-min) (point-max)))
+
(goto-char (point-min))
(unless (or (eobp) (memq patchsym '(:work :index)))
(forward-char 41))
"\n"))))
(goto-char end)))
-(defun stgit-select-file ()
- (let ((filename (expand-file-name
- (stgit-file-file (stgit-patched-file-at-point)))))
+(defun stgit-find-file (&optional other-window)
+ (let* ((file (or (stgit-patched-file-at-point)
+ (error "No file at point")))
+ (filename (expand-file-name (stgit-file-file file))))
(unless (file-exists-p filename)
(error "File does not exist"))
- (find-file filename)))
+ (funcall (if other-window 'find-file-other-window 'find-file)
+ filename)
+ (when (eq (stgit-file-status file) 'unmerged)
+ (smerge-mode 1))))
(defun stgit-select-patch ()
(let ((patchname (stgit-patch-name-at-point)))
('patch
(stgit-select-patch))
('file
- (stgit-select-file))
+ (stgit-find-file))
(t
(error "No patch or file on line"))))
(defun stgit-find-file-other-window ()
"Open file at point in other window"
(interactive)
- (let ((patched-file (stgit-patched-file-at-point)))
- (unless patched-file
- (error "No file on the current line"))
- (let ((filename (expand-file-name (stgit-file-file patched-file))))
- (unless (file-exists-p filename)
- (error "File does not exist"))
- (find-file-other-window filename))))
+ (stgit-find-file t))
+
+(defun stgit-find-file-merge ()
+ "Open file at point and merge it using `smerge-ediff'."
+ (interactive)
+ (stgit-find-file t)
+ (smerge-ediff))
(defun stgit-quit ()
"Hide the stgit buffer."
"Keymap for StGit major mode.")
(unless stgit-mode-map
- (let ((toggle-map (make-keymap)))
+ (let ((diff-map (make-keymap))
+ (toggle-map (make-keymap)))
+ (suppress-keymap diff-map)
+ (mapc (lambda (arg) (define-key diff-map (car arg) (cdr arg)))
+ '(("b" . stgit-diff-base)
+ ("c" . stgit-diff-combined)
+ ("m" . stgit-find-file-merge)
+ ("o" . stgit-diff-ours)
+ ("t" . stgit-diff-theirs)))
(suppress-keymap toggle-map)
(mapc (lambda (arg) (define-key toggle-map (car arg) (cdr arg)))
- '(("t" . stgit-toggle-worktree)))
+ '(("t" . stgit-toggle-worktree)
+ ("i" . stgit-toggle-ignored)
+ ("u" . stgit-toggle-unknown)))
(setq stgit-mode-map (make-keymap))
(suppress-keymap stgit-mode-map)
(mapc (lambda (arg) (define-key stgit-mode-map (car arg) (cdr arg)))
("<" . stgit-pop-next)
("P" . stgit-push-or-pop)
("G" . stgit-goto)
- ("=" . stgit-show)
+ ("=" . stgit-diff)
("D" . stgit-delete)
- ([(control ?/)] . stgit-undo)
+ ([?\C-/] . stgit-undo)
("\C-_" . stgit-undo)
+ ([?\C-c ?\C-/] . stgit-redo)
+ ("\C-c\C-_" . stgit-redo)
("B" . stgit-branch)
("t" . ,toggle-map)
+ ("d" . ,diff-map)
("q" . stgit-quit)))))
(defun stgit-mode ()
(set (make-local-variable 'stgit-show-worktree) stgit-default-show-worktree)
(set (make-local-variable 'stgit-index-node) nil)
(set (make-local-variable 'stgit-worktree-node) nil)
+ (set (make-local-variable 'parse-sexp-lookup-properties) t)
(set-variable 'truncate-lines 't)
(add-hook 'after-save-hook 'stgit-update-saved-file)
(run-hooks 'stgit-mode-hook))
(list patch)
'()))))
-(defun stgit-goto-patch (patchsym)
+(defun stgit-goto-patch (patchsym &optional file)
"Move point to the line containing patch PATCHSYM.
-If that patch cannot be found, do nothing."
+If that patch cannot be found, do nothing.
+
+If the patch was found and FILE is not nil, instead move to that
+file's line. If FILE cannot be found, stay on the line of
+PATCHSYM."
(let ((node (ewoc-nth stgit-ewoc 0)))
(while (and node (not (eq (stgit-patch-name (ewoc-data node))
patchsym)))
(setq node (ewoc-next stgit-ewoc node)))
+ (when (and node file)
+ (let* ((file-ewoc (stgit-patch-files-ewoc (ewoc-data node)))
+ (file-node (ewoc-nth file-ewoc 0)))
+ (while (and file-node (not (equal (stgit-file-file (ewoc-data file-node)) file)))
+ (setq file-node (ewoc-next file-ewoc file-node)))
+ (when file-node
+ (ewoc-goto-node file-ewoc file-node)
+ (move-to-column (stgit-goal-column))
+ (setq node nil))))
(when node
(ewoc-goto-node stgit-ewoc node)
(move-to-column goal-column))))
(defun stgit-commit (count)
"Run stg commit on COUNT commits.
-Interactively, the prefix argument is used as COUNT."
+Interactively, the prefix argument is used as COUNT.
+A negative COUNT will uncommit instead."
(interactive "p")
- (stgit-capture-output nil (stgit-run "commit" "-n" count))
- (stgit-reload))
+ (if (< count 0)
+ (stgit-uncommit (- count))
+ (stgit-capture-output nil (stgit-run "commit" "-n" count))
+ (stgit-reload)))
+
+(defun stgit-uncommit (count)
+ "Run stg uncommit on COUNT commits.
+Interactively, the prefix argument is used as COUNT.
+A negative COUNT will commit instead."
+ (interactive "p")
+ (if (< count 0)
+ (stgit-commit (- count))
+ (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
+ (stgit-reload)))
(defun stgit-revert-file ()
"Revert the file at point, which must be in the index or the
(unless (memq patch-name '(:work :index))
(error "No index or working tree file on this line"))
+ (when (eq file-status 'ignore)
+ (error "Cannot revert ignored files"))
+
+ (when (eq file-status 'unknown)
+ (error "Cannot revert unknown files"))
+
(let ((nfiles (+ (if rm-file 1 0) (if co-file 1 0))))
(when (yes-or-no-p (format "Revert %d file%s? "
nfiles
(stgit-reload)))
-(defun stgit-uncommit (count)
- "Run stg uncommit on COUNT commits.
-Interactively, the prefix argument is used as COUNT."
- (interactive "p")
- (stgit-capture-output nil (stgit-run "uncommit" "-n" count))
- (stgit-reload))
-
(defun stgit-push-next (npatches)
"Push the first unapplied patch.
With numeric prefix argument, push that many patches."
(error "Cannot find commit id for %s" patchsym))
(match-string 1 result))))
-(defun stgit-show ()
- "Show the patch on the current line."
- (interactive)
- (stgit-capture-output "*StGit patch*"
- (case (get-text-property (point) 'entry-type)
- ('file
- (let* ((patched-file (stgit-patched-file-at-point))
- (patch-name (stgit-patch-name-at-point))
- (patch-id (stgit-id patch-name))
- (args (append (and (stgit-file-cr-from patched-file)
- (list (stgit-find-copies-harder-diff-arg)))
- (cond ((eq patch-id :index)
- '("--cached"))
- ((eq patch-id :work)
- nil)
- (t
- (list (concat patch-id "^") patch-id)))
- '("--")
+(defun stgit-show-patch (unmerged-stage ignore-whitespace)
+ "Show the patch on the current line.
+
+UNMERGED-STAGE is the argument to `git-diff' that that selects
+which stage to diff against in the case of unmerged files."
+ (let ((space-arg (when (numberp ignore-whitespace)
+ (cond ((> ignore-whitespace 4)
+ "--ignore-all-space")
+ ((> ignore-whitespace 1)
+ "--ignore-space-change"))))
+ (patch-name (stgit-patch-name-at-point t)))
+ (stgit-capture-output "*StGit patch*"
+ (case (get-text-property (point) 'entry-type)
+ ('file
+ (let* ((patched-file (stgit-patched-file-at-point))
+ (patch-id (let ((id (stgit-id patch-name)))
+ (if (and (eq id :index)
+ (eq (stgit-file-status patched-file)
+ 'unmerged))
+ :work
+ id)))
+ (args (append (and space-arg (list space-arg))
+ (and (stgit-file-cr-from patched-file)
+ (list (stgit-find-copies-harder-diff-arg)))
+ (cond ((eq patch-id :index)
+ '("--cached"))
+ ((eq patch-id :work)
+ (list unmerged-stage))
+ (t
+ (list (concat patch-id "^") patch-id)))
+ '("--")
(if (stgit-file-copy-or-rename patched-file)
(list (stgit-file-cr-from patched-file)
(stgit-file-cr-to patched-file))
(list (stgit-file-file patched-file))))))
- (apply 'stgit-run-git "diff" args)))
- ('patch
- (let* ((patch-name (stgit-patch-name-at-point))
- (patch-id (stgit-id patch-name)))
- (if (or (eq patch-id :index) (eq patch-id :work))
- (apply 'stgit-run-git "diff"
- (stgit-find-copies-harder-diff-arg)
- (and (eq patch-id :index)
- '("--cached")))
- (stgit-run "show" "-O" "--patch-with-stat" "-O" "-M"
- (stgit-patch-name-at-point)))))
- (t
- (error "No patch or file at point")))
- (with-current-buffer standard-output
- (goto-char (point-min))
- (diff-mode))))
+ (apply 'stgit-run-git "diff" args)))
+ ('patch
+ (let* ((patch-id (stgit-id patch-name)))
+ (if (or (eq patch-id :index) (eq patch-id :work))
+ (apply 'stgit-run-git "diff"
+ (stgit-find-copies-harder-diff-arg)
+ (append (and space-arg (list space-arg))
+ (if (eq patch-id :index)
+ '("--cached")
+ (list unmerged-stage))))
+ (let ((args (append '("show" "-O" "--patch-with-stat" "-O" "-M")
+ (and space-arg (list "-O" space-arg))
+ (list (stgit-patch-name-at-point)))))
+ (apply 'stgit-run args)))))
+ (t
+ (error "No patch or file at point")))
+ (with-current-buffer standard-output
+ (goto-char (point-min))
+ (diff-mode)))))
+
+(defmacro stgit-define-diff (name diff-arg &optional unmerged-action)
+ `(defun ,name (&optional ignore-whitespace)
+ ,(format "Show the patch on the current line.
+
+%sWith a prefix argument, ignore whitespace. With a prefix argument
+greater than four (e.g., \\[universal-argument] \
+\\[universal-argument] \\[%s]), ignore all whitespace."
+ (if unmerged-action
+ (format "For unmerged files, %s.\n\n" unmerged-action)
+ "")
+ name)
+ (interactive "p")
+ (stgit-show-patch ,diff-arg ignore-whitespace)))
+
+(stgit-define-diff stgit-diff
+ "--ours" nil)
+(stgit-define-diff stgit-diff-ours
+ "--ours"
+ "diff against our branch")
+(stgit-define-diff stgit-diff-theirs
+ "--theirs"
+ "diff against their branch")
+(stgit-define-diff stgit-diff-base
+ "--base"
+ "diff against the merge base")
+(stgit-define-diff stgit-diff-combined
+ "--cc"
+ "show a combined diff")
(defun stgit-move-change-to-index (file)
"Copies the workspace state of FILE to index, using git add or git rm"
(stgit-run-git "reset" "-q" "--" file)))
(defun stgit-file-toggle-index ()
- "Move modified file in or out of the index."
+ "Move modified file in or out of the index.
+
+Leaves the point where it is, but moves the mark to where the
+file ended up. You can then jump to the file with \
+\\[exchange-point-and-mark]."
(interactive)
(let ((patched-file (stgit-patched-file-at-point)))
(unless patched-file
(error "No file on the current line"))
(when (eq (stgit-file-status patched-file) 'unmerged)
(error (substitute-command-keys "Use \\[stgit-resolve-file] to move an unmerged file to the index")))
- (let ((patch-name (stgit-patch-name-at-point)))
+ (when (eq (stgit-file-status patched-file) 'ignore)
+ (error "You cannot add ignored files to the index"))
+ (let* ((patch (stgit-patch-at-point))
+ (patch-name (stgit-patch-name patch))
+ (old-point (point))
+ next-file)
+
+ ;; find the next file in the patch, or the previous one if this
+ ;; was the last file
+ (and (zerop (forward-line 1))
+ (let ((f (stgit-patched-file-at-point)))
+ (and f (setq next-file (stgit-file-file f)))))
+ (goto-char old-point)
+ (unless next-file
+ (and (zerop (forward-line -1))
+ (let ((f (stgit-patched-file-at-point)))
+ (and f (setq next-file (stgit-file-file f)))))
+ (goto-char old-point))
+
(cond ((eq patch-name :work)
(stgit-move-change-to-index (stgit-file-file patched-file)))
((eq patch-name :index)
(stgit-remove-change-from-index (stgit-file-file patched-file)))
(t
- (error "Can only move files in the working tree to index")))))
- (stgit-refresh-worktree)
- (stgit-refresh-index))
+ (error "Can only move files in the working tree to index")))
+ (stgit-refresh-worktree)
+ (stgit-refresh-index)
+ (stgit-goto-patch (if (eq patch-name :index) :work :index)
+ (stgit-file-file patched-file))
+ (push-mark nil t t)
+ (stgit-goto-patch patch-name next-file))))
(defun stgit-edit ()
"Edit the patch on the current line."
(defun stgit-undo (&optional arg)
"Run stg undo.
-With prefix argument, run it with the --hard flag."
+With prefix argument, run it with the --hard flag.
+
+See also `stgit-redo'."
(interactive "P")
(stgit-capture-output nil
(if arg
(stgit-run "undo")))
(stgit-reload))
+(defun stgit-redo (&optional arg)
+ "Run stg redo.
+With prefix argument, run it with the --hard flag.
+
+See also `stgit-undo'."
+ (interactive "P")
+ (stgit-capture-output nil
+ (if arg
+ (stgit-run "redo" "--hard")
+ (stgit-run "redo")))
+ (stgit-reload))
+
(defun stgit-refresh (&optional arg)
"Run stg refresh.
If the index contains any changes, only refresh from index.
:group 'stgit)
(defcustom stgit-default-show-worktree
- nil
+ t
"Set to non-nil to by default show the working tree in a new stgit buffer.
This value is used as the default value for `stgit-show-worktree'."
See also `stgit-show-worktree-mode'.")
+(defvar stgit-show-ignored nil
+ "If nil, inhibit showing files ignored by git.")
+
+(defvar stgit-show-unknown nil
+ "If nil, inhibit showing files not registered with git.")
+
(defun stgit-toggle-worktree (&optional arg)
"Toggle the visibility of the work tree.
With arg, show the work tree if arg is positive.
(not stgit-show-worktree)))
(stgit-reload))
+(defun stgit-toggle-ignored (&optional arg)
+ "Toggle the visibility of files ignored by git in the work
+tree. With ARG, show these files if ARG is positive.
+
+Use \\[stgit-toggle-worktree] to show the work tree."
+ (interactive)
+ (setq stgit-show-ignored
+ (if (numberp arg)
+ (> arg 0)
+ (not stgit-show-ignored)))
+ (stgit-reload))
+
+(defun stgit-toggle-unknown (&optional arg)
+ "Toggle the visibility of files not registered with git in the
+work tree. With ARG, show these files if ARG is positive.
+
+Use \\[stgit-toggle-worktree] to show the work tree."
+ (interactive)
+ (setq stgit-show-unknown
+ (if (numberp arg)
+ (> arg 0)
+ (not stgit-show-unknown)))
+ (stgit-reload))
+
(provide 'stgit)