summaryrefslogtreecommitdiff
path: root/.cask/30.2/bootstrap/git-20140128.1041/git.el
blob: 5adefae12ebe5c9cb158d68d21eccf59d9e509a7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
;;; 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>
;; Package-Version: 20140128.1041
;; Package-Revision: 8b7f1477ef36
;; 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