slackbuilds/mgit.el

380 lines
12 KiB
EmacsLisp
Raw Normal View History

2008-08-05 18:09:47 +02:00
;;; mgit -- control git from Emacs.
2008-07-31 22:11:46 +02:00
;; Copyright (C) 2008 Marius Vollmer
;;
2008-08-05 18:09:47 +02:00
;; Mgit is free software; you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
2008-07-31 22:11:46 +02:00
;;
2008-08-05 18:09:47 +02:00
;; Mgit is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
;; License for more details.
2008-07-31 22:11:46 +02:00
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Introduction
2008-08-05 18:09:47 +02:00
;; Invoking the mgit-status function will show a buffer with the
2008-07-31 22:11:46 +02:00
;; current status of the current git repository and its checkout.
;; That buffer offers key bindings for manipulating the status in
;; simple ways.
;;
;; The status buffer mainly shows the difference between the working
;; tree and the index, and the difference between the index and the
2008-08-04 01:05:02 +02:00
;; current HEAD. You can add individual hunks from the working tree
;; to the index, and you can commit the index.
2008-08-01 13:37:54 +02:00
2008-08-03 20:41:24 +02:00
;;; TODO
2008-08-04 14:52:53 +02:00
;; - Hide titles of empty sections
2008-08-04 14:35:25 +02:00
;; - Branch creation/switching
;; - Explicit diffing/merging of branches
2008-08-05 00:01:43 +02:00
;; - Detect and handle renames and copies.
2008-08-03 20:50:00 +02:00
;; - History browsing
2008-08-03 20:41:24 +02:00
2008-08-01 13:37:54 +02:00
;;; Utilities
2008-08-05 18:09:47 +02:00
(defun mgit-shell (cmd &rest args)
2008-08-01 13:37:54 +02:00
(let ((str (shell-command-to-string (apply 'format cmd args))))
(if (string= str "")
nil
(if (equal (elt str (- (length str) 1)) ?\n)
(substring str 0 (- (length str) 1))
str))))
2008-08-05 18:09:47 +02:00
(defun mgit-concat-with-delim (delim seqs)
2008-08-03 20:33:25 +02:00
(cond ((null seqs)
nil)
((null (cdr seqs))
(car seqs))
(t
2008-08-05 18:09:47 +02:00
(concat (car seqs) delim (mgit-concat-with-delim delim (cdr seqs))))))
2008-08-03 20:33:25 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-get (&rest keys)
(mgit-shell "git-config %s" (mgit-concat-with-delim "." keys)))
2008-08-03 20:33:25 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-set (val &rest keys)
2008-08-04 01:05:02 +02:00
(if val
2008-08-05 18:09:47 +02:00
(mgit-shell "git-config %s %s" (mgit-concat-with-delim "." keys) val)
(mgit-shell "git-config --unset %s" (mgit-concat-with-delim "." keys))))
2008-08-04 01:05:02 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-get-top-dir (cwd)
2008-08-01 13:37:54 +02:00
(let* ((cwd (expand-file-name cwd))
2008-08-05 18:09:47 +02:00
(mgit-dir (mgit-shell "cd '%s' && git-rev-parse --git-dir 2>/dev/null"
2008-08-01 13:37:54 +02:00
cwd)))
2008-08-05 18:09:47 +02:00
(if mgit-dir
(file-name-as-directory (or (file-name-directory mgit-dir) cwd))
2008-08-01 13:37:54 +02:00
nil)))
2008-08-05 18:09:47 +02:00
(defun mgit-get-ref (ref)
(mgit-shell "git-symbolic-ref -q %s" ref))
2008-08-03 20:33:25 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-get-current-branch ()
(let* ((head (mgit-get-ref "HEAD"))
2008-08-03 20:33:25 +02:00
(pos (and head (string-match "^refs/heads/" head))))
(if pos
(substring head 11)
nil)))
2008-08-05 18:09:47 +02:00
(defun mgit-read-top-dir (prefix)
(let ((dir (mgit-get-top-dir default-directory)))
2008-08-03 20:33:25 +02:00
(if prefix
2008-08-05 18:09:47 +02:00
(mgit-get-top-dir (read-directory-name "Git repository: " dir))
2008-08-03 20:33:25 +02:00
dir)))
2008-08-01 13:37:54 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-insert-output (title washer cmd &rest args)
2008-08-05 21:21:51 +02:00
(if title
(insert title "\n"))
2008-08-04 02:52:16 +02:00
(let* ((beg (point))
(status (apply 'call-process cmd nil t nil args))
(end (point)))
(if washer
(save-restriction
(narrow-to-region beg (point))
(funcall washer status)
(goto-char (point-max))
(insert "\n")))))
2008-08-03 21:50:39 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-put-line-property (prop val)
2008-08-04 01:05:02 +02:00
(put-text-property (line-beginning-position) (line-end-position)
prop val))
2008-08-01 15:04:52 +02:00
;;; Running asynchronous commands
2008-08-05 18:09:47 +02:00
(defvar mgit-process nil)
2008-08-01 15:04:52 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-run (cmd &rest args)
(or (not mgit-process)
2008-08-01 15:04:52 +02:00
(error "Git is already running."))
(let ((dir default-directory)
(buf (get-buffer-create "*git-process*")))
(save-excursion
(set-buffer buf)
(setq default-directory dir)
(erase-buffer)
2008-08-05 18:09:47 +02:00
(insert "$ " (mgit-concat-with-delim " " (cons cmd args)) "\n")
(setq mgit-process (apply 'start-process "git" buf cmd args))
(set-process-sentinel mgit-process 'mgit-process-sentinel))))
2008-08-01 15:04:52 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-process-sentinel (process event)
2008-08-01 15:04:52 +02:00
(cond ((string= event "finished\n")
(message "Git finished.")
2008-08-05 18:09:47 +02:00
(setq mgit-process nil))
2008-08-01 15:04:52 +02:00
((string= event "killed\n")
(message "Git was killed.")
2008-08-05 18:09:47 +02:00
(setq mgit-process nil))
2008-08-01 15:04:52 +02:00
((string-match "exited abnormally" event)
(message "Git failed.")
2008-08-05 18:09:47 +02:00
(setq mgit-process nil))
2008-08-01 15:04:52 +02:00
(t
(message "Git is weird.")))
2008-08-05 18:09:47 +02:00
(mgit-update-status))
2008-08-01 15:04:52 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-display-process ()
2008-08-03 21:29:08 +02:00
(interactive)
(display-buffer "*git-process*"))
2008-08-03 21:10:00 +02:00
;;; Keymap
2008-08-05 18:09:47 +02:00
(defvar mgit-keymap nil)
(when (not mgit-keymap)
(setq mgit-keymap (make-keymap))
(suppress-keymap mgit-keymap)
(define-key mgit-keymap (kbd "g") 'mgit-status)
(define-key mgit-keymap (kbd "S") 'mgit-stage-all)
(define-key mgit-keymap (kbd "a") 'mgit-stage-thing-at-point)
(define-key mgit-keymap (kbd "u") 'mgit-unstage-thing-at-point)
(define-key mgit-keymap (kbd "i") 'mgit-ignore-thing-at-point)
(define-key mgit-keymap (kbd "RET") 'mgit-visit-thing-at-point)
(define-key mgit-keymap (kbd "U") 'mgit-pull)
(define-key mgit-keymap (kbd "P") 'mgit-push)
(define-key mgit-keymap (kbd "c") 'mgit-log-edit)
(define-key mgit-keymap (kbd "p") 'mgit-display-process))
2008-08-03 21:10:00 +02:00
2008-08-01 15:04:52 +02:00
;;; Status
2008-08-05 18:09:47 +02:00
(defun mgit-wash-other-files (status)
2008-08-04 01:05:02 +02:00
(goto-char (point-min))
(while (not (eobp))
(let ((filename (buffer-substring (point) (line-end-position))))
(insert " ")
2008-08-05 18:09:47 +02:00
(mgit-put-line-property 'face '(:foreground "red"))
(mgit-put-line-property 'mgit-info (list 'other-file filename)))
2008-08-04 01:05:02 +02:00
(forward-line)
(beginning-of-line)))
2008-08-05 18:09:47 +02:00
(defun mgit-wash-diff-propertize-diff (head-beg head-end)
(let ((head-end (or head-end (point))))
(when head-beg
(put-text-property head-beg head-end
2008-08-05 18:09:47 +02:00
'mgit-info (list 'diff
head-beg (point))))))
2008-08-05 18:09:47 +02:00
(defun mgit-wash-diff-propertize-hunk (head-beg head-end hunk-beg)
(when hunk-beg
(put-text-property hunk-beg (point)
2008-08-05 18:09:47 +02:00
'mgit-info (list 'hunk
head-beg head-end
hunk-beg (point)))))
2008-08-04 14:54:08 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-wash-diff (status)
2008-08-04 02:26:34 +02:00
(goto-char (point-min))
(let ((n-files 1)
2008-08-04 02:52:16 +02:00
(head-beg nil)
(head-end nil)
(hunk-beg nil))
2008-08-04 02:26:34 +02:00
(while (not (eobp))
(let ((prefix (buffer-substring-no-properties
(point) (+ (point) n-files))))
2008-08-04 02:52:16 +02:00
(cond ((looking-at "^diff")
2008-08-05 18:09:47 +02:00
(mgit-wash-diff-propertize-diff head-beg head-end)
2008-08-04 02:52:16 +02:00
(setq head-beg (point))
(setq head-end nil))
((looking-at "^@+")
2008-08-04 02:26:34 +02:00
(setq n-files (- (length (match-string 0)) 1))
(if (null head-end)
(setq head-end (point)))
2008-08-05 18:09:47 +02:00
(mgit-wash-diff-propertize-hunk head-beg head-end hunk-beg)
2008-08-04 02:52:16 +02:00
(setq hunk-beg (point)))
2008-08-04 02:26:34 +02:00
((string-match "\\+" prefix)
2008-08-05 18:09:47 +02:00
(mgit-put-line-property 'face '(:foreground "blue1")))
2008-08-04 02:26:34 +02:00
((string-match "-" prefix)
2008-08-05 18:09:47 +02:00
(mgit-put-line-property 'face '(:foreground "red")))))
2008-08-04 02:26:34 +02:00
(forward-line)
(beginning-of-line))
2008-08-05 18:09:47 +02:00
(mgit-wash-diff-propertize-diff head-beg head-end)
(mgit-wash-diff-propertize-hunk head-beg head-end hunk-beg)))
2008-08-04 02:26:34 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-update-status ()
2008-08-01 15:04:52 +02:00
(let ((buf (get-buffer "*git-status*")))
(save-excursion
(set-buffer buf)
2008-08-03 21:10:00 +02:00
(setq buffer-read-only t)
2008-08-05 18:09:47 +02:00
(use-local-map mgit-keymap)
2008-08-03 21:10:00 +02:00
(let ((inhibit-read-only t))
(erase-buffer)
2008-08-05 18:09:47 +02:00
(let* ((branch (mgit-get-current-branch))
(remote (and branch (mgit-get "branch" branch "remote"))))
(if remote
(insert (format "Remote: %s %s\n"
remote (mgit-get "remote" remote "url"))))
2008-08-03 22:04:18 +02:00
(insert (format "Local: %s %s\n"
(or branch "(detached)")
2008-08-05 18:09:47 +02:00
(abbreviate-file-name default-directory)))
(insert "\n")
(mgit-insert-output "Untracked files:" 'mgit-wash-other-files
"git" "ls-files" "--others" "--exclude-standard")
(mgit-insert-output "Unstaged changes:" 'mgit-wash-diff
"git" "diff")
(mgit-insert-output "Staged changes:" 'mgit-wash-diff
"git" "diff" "--cached")
(if remote
(mgit-insert-output "Unpushed changes:" 'mgit-wash-diff
"git" "diff" "--stat"
(format "%s/%s..HEAD" remote branch))))))))
2008-08-05 18:09:47 +02:00
(defun mgit-status (dir)
(interactive (list (mgit-read-top-dir current-prefix-arg)))
2008-08-03 20:33:25 +02:00
(let ((buf (get-buffer-create "*git-status*")))
(switch-to-buffer buf)
(setq default-directory dir)
2008-08-05 18:09:47 +02:00
(mgit-update-status)))
2008-08-01 13:37:54 +02:00
2008-08-04 01:05:02 +02:00
;;; Staging
2008-08-01 15:04:52 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-write-diff-patch (info file)
(write-region (elt info 1) (elt info 2) file))
2008-08-05 18:09:47 +02:00
(defun mgit-write-hunk-patch (info file)
2008-08-04 03:10:48 +02:00
(write-region (elt info 1) (elt info 2) file)
(write-region (elt info 3) (elt info 4) file t))
2008-08-05 18:09:47 +02:00
(defun mgit-hunk-is-conflict-p (info)
(save-excursion
(goto-char (elt info 1))
(looking-at-p "^diff --cc")))
(defun mgit-diff-conflict-file (info)
(save-excursion
(goto-char (elt info 1))
(if (looking-at "^diff --cc +\\(.*\\)$")
(match-string 1)
nil)))
(defun mgit-stage-thing-at-point ()
2008-08-01 15:04:52 +02:00
(interactive)
2008-08-05 18:09:47 +02:00
(let ((info (get-char-property (point) 'mgit-info)))
2008-08-04 01:05:02 +02:00
(if info
(case (car info)
((other-file)
2008-08-05 18:09:47 +02:00
(mgit-run "git" "add" (cadr info)))
2008-08-04 03:10:48 +02:00
((hunk)
2008-08-05 18:09:47 +02:00
(if (mgit-hunk-is-conflict-p info)
(error
"Can't stage individual resolution hunks. Please stage the whole file."))
(mgit-write-hunk-patch info ".git/mgit-tmp")
(mgit-run "git" "apply" "--cached" ".git/mgit-tmp"))
((diff)
2008-08-05 18:09:47 +02:00
(let ((file (mgit-diff-conflict-file info)))
(if file
(mgit-run "git" "add" file)
(mgit-write-diff-patch info ".git/mgit-tmp")
(mgit-run "git" "apply" "--cached" ".git/mgit-tmp"))))))))
2008-08-01 15:04:52 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-unstage-thing-at-point ()
2008-08-04 03:13:23 +02:00
(interactive)
2008-08-05 18:09:47 +02:00
(let ((info (get-char-property (point) 'mgit-info)))
2008-08-04 03:13:23 +02:00
(if info
(case (car info)
((hunk)
2008-08-05 18:09:47 +02:00
(mgit-write-hunk-patch info ".git/mgit-tmp")
(mgit-run "git" "apply" "--cached" "--reverse" ".git/mgit-tmp"))
((diff)
2008-08-05 18:09:47 +02:00
(mgit-write-diff-patch info ".git/mgit-tmp")
(mgit-run "git" "apply" "--cached" "--reverse" ".git/mgit-tmp"))))))
2008-08-04 03:13:23 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-ignore-thing-at-point ()
2008-08-01 15:04:52 +02:00
(interactive)
2008-08-05 18:09:47 +02:00
(let ((info (get-char-property (point) 'mgit-info)))
2008-08-04 01:05:02 +02:00
(if info
(case (car info)
((other-file)
(append-to-file (concat (cadr info) "\n") nil ".gitignore")
2008-08-05 18:09:47 +02:00
(mgit-update-status))))))
2008-08-04 01:05:02 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-visit-thing-at-point ()
2008-08-04 02:26:34 +02:00
(interactive)
2008-08-05 18:09:47 +02:00
(let ((info (get-char-property (point) 'mgit-info)))
2008-08-04 02:26:34 +02:00
(message "visit %S" info)
(if info
(case (car info)
((other-file)
(find-file (cadr info)))))))
2008-08-04 01:05:02 +02:00
;;; Push and pull
2008-08-05 18:09:47 +02:00
(defun mgit-pull ()
(interactive)
2008-08-05 18:09:47 +02:00
(mgit-run "git" "pull" "-v"))
2008-08-04 01:05:02 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-push ()
(interactive)
2008-08-05 18:09:47 +02:00
(mgit-run "git" "push" "-v"))
2008-08-04 01:05:02 +02:00
;;; Commit
2008-08-01 15:04:52 +02:00
2008-08-05 18:09:47 +02:00
(defvar mgit-log-edit-map nil)
2008-08-04 14:35:25 +02:00
2008-08-05 18:09:47 +02:00
(when (not mgit-log-edit-map)
(setq mgit-log-edit-map (make-sparse-keymap))
(define-key mgit-log-edit-map (kbd "C-c C-c") 'mgit-log-edit-commit))
2008-08-04 14:35:25 +02:00
2008-08-05 18:09:47 +02:00
(defvar mgit-pre-log-edit-window-configuration nil)
2008-08-04 14:35:25 +02:00
(defun mgit-log-edit-cleanup ()
(save-excursion
(goto-char (point-min))
(flush-lines "^#")
(goto-char (point-min))
(replace-regexp "[ \t\n]*\\'" "\n")))
2008-08-05 18:09:47 +02:00
(defun mgit-log-edit-commit ()
2008-08-04 14:35:25 +02:00
(interactive)
(mgit-log-edit-cleanup)
(if (> (buffer-size) 0)
2008-08-05 18:09:47 +02:00
(write-region (point-min) (point-max) ".git/mgit-log")
(write-region "(Empty description)" nil ".git/mgit-log"))
(erase-buffer)
2008-08-05 18:09:47 +02:00
(mgit-run "git-commit" "-F" ".git/mgit-log")
2008-08-04 14:35:25 +02:00
(bury-buffer)
2008-08-05 18:09:47 +02:00
(when mgit-pre-log-edit-window-configuration
(set-window-configuration mgit-pre-log-edit-window-configuration)
(setq mgit-pre-log-edit-window-configuration nil)))
2008-08-04 14:35:25 +02:00
2008-08-05 18:09:47 +02:00
(defun mgit-log-edit ()
2008-08-01 15:04:52 +02:00
(interactive)
2008-08-04 14:35:25 +02:00
(let ((dir default-directory)
(buf (get-buffer-create "*git-log-edit*")))
2008-08-05 18:09:47 +02:00
(setq mgit-pre-log-edit-window-configuration (current-window-configuration))
2008-08-04 14:35:25 +02:00
(pop-to-buffer buf)
(setq default-directory dir)
2008-08-05 18:09:47 +02:00
(use-local-map mgit-log-edit-map)
(save-excursion
(mgit-log-edit-cleanup)
(if (and (= (buffer-size) 0)
(file-exists-p ".git/MERGE_MSG"))
(insert-file-contents ".git/MERGE_MSG")))
2008-08-04 14:35:25 +02:00
(message "Use C-c C-c when done.")))
2008-08-04 01:05:02 +02:00
;;; Misc
2008-08-05 18:09:47 +02:00
(defun mgit-stage-all ()
2008-08-04 01:05:02 +02:00
(interactive)
2008-08-05 18:09:47 +02:00
(mgit-run "git-add" "-u" "."))