; esvn-mode: pennyante SVN mode for emacs
; version 0.1.3

(defvar esvn-mode nil)
(make-variable-buffer-local 'esvn-mode)
(defvar esvn-mode-map nil "Bindings for esvn mode.")

(defgroup esvn-group nil "Group for esvn customization stuff."
  :version "21.4.20" :prefix 'esvn-)

(defcustom esvn-default-commit-message "" 
  "*Default commit message."
  :type '(string) :group 'esvn-group)
(defcustom esvn-default-add-message "" 
  "*Default commit message if adding a file.

If empty, try esvn-default-commit-message."
  :type '(string) :group 'esvn-group)
(defcustom esvn-default-autocommit-message ""
  "*Default commit message if autocommitting (committing without user intervention).

If empty, try esvn-default-commit-message."
  :type '(string) :group 'esvn-group)
(defcustom esvn-default-autocommit-add-message ""
  "*Default commit message if adding a file while autocommitting.

If empty, try esvn-default-add-message, esvn-default-autocommit-message, and
esvn-default-commit-message in that order."
  :type '(string) :group 'esvn-group)

(make-variable-buffer-local 'esvn-default-commit-message)
(make-variable-buffer-local 'esvn-default-autocommit-message)
(make-variable-buffer-local 'esvn-default-add-message)
(make-variable-buffer-local 'esvn-default-autocommit-add-message)
(defvar esvn-buffer-file-status nil
  "Status of file associated with a buffer.

This can be the symbol :new, which means \"not in SVN\"; the symbol
:committed, which means \"in SVN\"; or nil, which means \"unknown\".")
(make-variable-buffer-local 'esvn-buffer-file-status)

(setq esvn-error-types 
      '((esvn-stat-failed . "svn stat failed")
	(esvn-bad-commit-message . "Bad commit message")))

(if (fboundp 'define-error)
    (progn (mapcar (lambda (elem) (define-error (car elem) (cdr elem)))
		esvn-error-types)
	   (defun esvn-error (symbol string)
	     (error symbol string)))
  (progn (mapcar (lambda (elem) (set (car elem) (cdr elem)))
	      esvn-error-types)
	 (defun esvn-error (symbol string)
	   (error "%s: %s" (eval symbol) string))))

(defun esvn-new-file ()
  "Function to mark a new file as \"new\", for later adding."
  (setq esvn-buffer-file-status :new))

(add-hook 'find-file-not-found-hooks 'esvn-new-file)

(when (not esvn-mode-map)
  (setq esvn-mode-map (make-sparse-keymap))
  ;(define-key esvn-mode-map [?\C-c ?\C-d] 'esvn-diff)
  (define-key esvn-mode-map [?\C-c ?\C-c] 'esvn-commit))

(or (assq 'esvn-mode minor-mode-alist)
    (setq minor-mode-alist
          (cons '(esvn-mode " E") minor-mode-alist)))

(or (assq 'esvn-mode minor-mode-map-alist)
    (setq minor-mode-map-alist
	  (push (cons 'esvn-mode esvn-mode-map) minor-mode-map-alist)))

(defun esvn-mode (&optional arg)
  (interactive "P")
  (setq esvn-mode
	(if (null arg) (not esvn-mode)
	  (> (prefix-numeric-value arg) 0))))

(defun esvn-default-message (adding autocommit)
  "Get the default message to commit this file."
  (let* ((choices (cons esvn-default-commit-message '()))
	 (choices (if autocommit (cons esvn-default-autocommit-message choices)
		    choices))
	 (choices (if adding (cons esvn-default-add-message choices) choices))
	 (choices (if (and adding autocommit) 
		      (cons esvn-default-autocommit-add-message choices) 
		    choices)))
    (find "" choices :test-not 'string=)))

(defun esvn-get-commit-message (adding autocommit)
  "Get a commit message from the user.

If adding a file for the first time, use esvn-default-add-message
as the default. If not adding, or if that variable is empty,
use esvn-default-commit-message."
  (let* ((defmsg (esvn-default-message adding autocommit))
	 (prompt
	  (if (not (string= defmsg ""))
	      (format "Commit message (default %s):" defmsg)
	    "Commit message:"))
	 (response (read-from-minibuffer (concat prompt " "))))
    (if (string= response "")
	defmsg
      response)))

(defun esvn-execvp (&rest args)
  "Simulate C's execvp() function.

Quote each argument seperately, join with spaces and call shell-command-to-string to run in a shell."
  (let ((cmd (mapconcat 'shell-quote-argument args " ")))
    (shell-command-to-string cmd)))

(defun esvn-get-buffer-file-status ()
  "Find out whether the file corresponding to the buffer is in SVN.

On success, returns :new or :committed."
  (let* ((svnout (esvn-execvp "svn" "stat" buffer-file-name))
	 (fields (split-string svnout)))
    (when (< (length fields) 1) ; no stat output -- file not there?
      (esvn-error 'esvn-stat-failed "file not there"))

    (let ((status (car fields))
	  (file (car (cdr fields))))
      (when (not (string-match (concat file "$") buffer-file-name))
	(esvn-error 'esvn-stat-failed "file not the same as buffer" file (buffer-file-name)))

      (cond ((string= status "?") :new)
	    ((string= status "A") :committed) ; no need to add
	    ((string= status "M") :committed)))))

(defun last-line (string)
  (car (last (split-string string "\n" t))))

(defun esvn-commit (autocommit)
  "Commit the local buffer.

If autocommit is true, automatically commit using the default message (either 
esvn-default-add-message or esvn-default-commit-message)."
  (interactive "P")
  (save-buffer)
  (when (not esvn-buffer-file-status)
    (setq esvn-buffer-file-status (esvn-get-buffer-file-status)))
  (let ((adding (eq esvn-buffer-file-status :new)))
    (when adding
      (message (esvn-execvp "svn" "add" buffer-file-name)))
    (let ((response (if autocommit (esvn-default-message adding autocommit)
		      (esvn-get-commit-message adding autocommit))))
      (when (string= response "")
	(esvn-error 'esvn-bad-commit-message "No message given"))
      (message (last-line (esvn-execvp "svn" "commit" buffer-file-name "-m" response)))
      (setq esvn-buffer-file-status :committed))))

(defun esvn-save-or-autocommit (save)
  "Save or autocommit, based on prefix arg. Prefix arg means save; otherwise, autocommit."
  (interactive "P")
  (if save (save-buffer) (esvn-commit t)))

(provide 'esvn)
