emacs/code/elpa/git-20140128.1041/git.el

281 lines
7.9 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; git.el --- An Elisp API for programmatically using Git
;; Copyright (C) 2013 Johan Andersson
;; Author: Johan Andersson <johan.rejeep@gmail.com>
;; Maintainer: Johan Andersson <johan.rejeep@gmail.com>
;; Version: 0.1.1
;; Package-Version: 20140128.1041
;; Package-Commit: a3396a7027a7d986598c6a2d6d5599bac918f3da
;; Keywords: git
;; URL: http://github.com/rejeep/git.el
;; Package-Requires: ((s "1.7.0") (dash "2.2.0") (f "0.10.0"))
;; This file is NOT part of GNU Emacs.
;;; License:
;; This program 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.
;; This program 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.
;; 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., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Code:
;; Todo: no-pager
(require 's)
(require 'dash)
(require 'f)
(defvar git-executable
(executable-find "git")
"Git executable.")
(defvar git-repo nil
"Path to current working repo.")
(defvar git-args nil
"List of args to include when running git command.")
(defconst git-stash-re "^\\(.+?\\): \\(?:WIP on\\|On\\) \\(.+\\): \\(.+\\)$"
"Regular expression matching a stash.")
(put 'git-error 'error-conditions '(error git-error))
(put 'git-error 'error-message "GIT Error")
(defun git-error (string &rest args)
"Signal a GIT error.
Signal an error with `git-error' type.
STRING is a `format' string, and ARGS are the formatted objects."
(signal 'git-error (list (apply #'format string args))))
(defun git-run (command &rest args)
"Run git COMMAND with ARGS."
(let ((default-directory (f-full git-repo)))
(with-temp-buffer
(let ((exit-code
(apply
'call-process
(append
(list git-executable nil (current-buffer) nil)
(git--args command args)))))
(if (zerop exit-code)
(buffer-string)
(git-error
"Error running command: %s %s\n%s"
git-executable
(s-join " " (git--args command args))
(buffer-string)))))))
(defun git-repo? (directory)
"Return true if there is a git repo in DIRECTORY, false otherwise."
(or
(f-dir? (f-expand ".git" directory))
(and
(f-dir? (f-expand "info" directory))
(f-dir? (f-expand "objects" directory))
(f-dir? (f-expand "refs" directory))
(f-file? (f-expand "HEAD" directory)))))
(defun git-branch? (branch)
"Return true if there's a branch called BRANCH."
(-contains? (git-branches) branch))
(defun git-tag? (tag)
"Return true if there's a tag called TAG."
(-contains? (git-tags) tag))
(defun git-on-branch ()
"Return currently active branch."
(condition-case err
(git--clean (git-run "rev-parse" "--abbrev-ref" "HEAD"))
(git-error
(git-error "Repository not initialized"))))
(defun git-on-branch? (branch)
"Return true if BRANCH is currently active."
(equal branch (git-on-branch)))
(defun git-add (&rest files)
"Add PATH or everything."
(git-run "add" (or files ".")))
(defun git-branch (branch)
"Create BRANCH."
(if (git-branch? branch)
(git-error "Branch already exists %s" branch)
(git-run "branch" branch)))
(defun git-branches ()
"List all available branches."
(-map
(lambda (line)
(if (s-starts-with? "*" line)
(substring line 2)
line))
(git--lines (git-run "branch"))))
(defun git-checkout (ref)
"Checkout REF."
(git-run "checkout" ref))
(defun git-clone (url &optional dir)
"Clone URL to DIR (if present)."
(git-run "clone" url dir))
(defun git-commit (message &rest files)
"Commit FILES (or added files) with MESSAGE."
(git-run "commit" (or files "-a") "--message" message files))
(defun git-fetch (&optional repo)
"Fetch REPO."
(git-run "fetch" repo))
(defun git-init (&optional dir bare)
"Create new Git repo at DIR (or `git-repo').
If BARE is true, create a bare repo."
(let ((git-repo (or dir git-repo)))
(git-run "init" (and bare "--bare"))))
;; Todo: The solution used here is not bulletproof. For example if the
;; message contains a pipe, the :message will only include everything
;; before that pipe. Figure out a good solution for this.
(defun git-log (&optional branch)
"Log history on BRANCH."
(let ((logs (git--lines (git-run "log" "--format=%h|%an|%ae|%cn|%ce|%ad|%s"))))
(-map
(lambda (log)
(let ((data (s-split "|" log)))
(list
:commit (nth 0 data)
:author-name (nth 1 data)
:author-email (nth 2 data)
:comitter-name (nth 3 data)
:comitter-email (nth 4 data)
:date (nth 5 data)
:message (nth 6 data))))
logs)))
(defun git-config (option &optional value)
"Set or get config OPTION. Set to VALUE if present."
(condition-case err
(git--clean (git-run "config" option value))
(git-error)))
(defun git-pull (&optional repo ref)
"Pull REF from REPO."
(git-run "pull" repo ref))
(defun git-push (&optional repo ref)
"Push REF to REPO."
(git-run "push" repo ref))
(defun git-remote? (name)
"Return true if remote with NAME exists, false otherwise."
(-contains? (git-remotes) name))
(defun git-remotes ()
"Return list of all remotes."
(git--lines (git-run "remote")))
(defun git-remote-add (name url)
"Add remote with NAME and URL."
(git-run "remote" "add" name url))
(defun git-remote-remove (name)
"Remove remote with NAME."
(if (git-remote? name)
(git-run "remote" "remove" name)
(git-error "No such remote %s" name)))
(defun git-reset (&optional commit mode)
"Reset to COMMIT with MODE."
(git-run "reset" (if mode (concat "--" (symbol-name mode))) commit))
(defun git-rm (path &optional recursive)
"Remove PATH.
To remove directory, use RECURSIVE argument."
(git-run "rm" path (and recursive "-r")))
(defun git-stash (&optional message)
"Stash changes in a dirty tree with MESSAGE.
If a stash was created, the name of the stash is returned,
otherwise nil is returned."
(let ((before-stashes (git-stashes)) after-stashes)
(git-run "stash" "save" message)
(setq after-stashes (git-stashes))
(if (> (length after-stashes) (length before-stashes))
(plist-get (car after-stashes) :name))))
(defun git-stashes ()
"Return list of stashes."
(let ((stashes (git--lines (git-run "stash" "list"))))
(-map
(lambda (stash)
(let ((matches (s-match git-stash-re stash)))
(list :name (nth 1 matches)
:branch (nth 2 matches)
:message (nth 3 matches))))
stashes)))
(defun git-stash-pop (&optional name)
"Apply and remove stash with NAME (or first stash)."
(git-run "stash" "pop" name))
(defun git-stash-apply (&optional name)
"Apply and keep stash with NAME (or first stash)."
(git-run "stash" "apply" name))
(defun git-tag (tag)
"Create TAG."
(if (git-tag? tag)
(git-error "Tag already exists %s" tag)
(git-run "tag" tag)))
(defun git-tags ()
"Return list of all tags."
(git--lines (git-run "tag")))
(defun git-untracked-files ()
"Return list of untracked files."
(git--lines
(git-run "ls-files" "--other" "--exclude-standard")))
(defun git-staged-files ()
"Return list of staged files."
(git--lines
(git-run "diff" "--cached" "--name-only")))
;;;; Helpers
(defun git--lines (string)
(-reject 's-blank? (-map 's-trim (s-lines string))))
(defun git--clean (string)
(s-presence (s-trim string)))
(defun git--args (command &rest args)
(-flatten (-reject 'null (append (list "--no-pager" command) args git-args))))
(provide 'git)
;;; git.el ends here