-
Notifications
You must be signed in to change notification settings - Fork 26
/
git-auto-commit-mode.el
271 lines (222 loc) · 9.39 KB
/
git-auto-commit-mode.el
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
;;; git-auto-commit-mode.el --- Emacs Minor mode to automatically commit and push -*- lexical-binding: t -*-
;; Copyright (C) 2012, 2013, 2014, 2015 Tom Willemse <[email protected]>
;; Author: Tom Willemse <[email protected]>
;; Created: Jan 9, 2012
;; Version: 4.7.0
;; Keywords: vc
;; URL: https://github.com/ryuslash/git-auto-commit-mode
;; This file 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
;; of the License, or (at your option) any later version.
;; This file 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 this file; If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; git-auto-commit-mode is an Emacs minor mode that tries to commit
;; changes to a file after every save.
;; When `gac-automatically-push-p' is non-nil, it also tries to push
;; to the current upstream.
;; When `gac-debounce-interval' is non-nil and set to a number
;; representing seconds, it will only perform Git actions at that
;; interval. That way, repeatedly saving a file will not hammer the
;; Git repository.
;;; Code:
(require 'subr-x)
(defgroup git-auto-commit-mode nil
"Customization options for `git-auto-commit-mode'."
:group 'external)
(defcustom gac-automatically-push-p nil
"Automatically push after each commit.
If non-nil a git push will be executed after each commit."
:tag "Automatically push"
:group 'git-auto-commit-mode
:type 'boolean
:risky t)
(make-variable-buffer-local 'gac-automatically-push-p)
(defcustom gac-automatically-add-new-files-p t
"Should new (untracked) files automatically be committed to the repo?"
:tag "Automatically add new files"
:group 'git-auto-commit-mode
:type 'boolean)
(defcustom gac-ask-for-summary-p nil
"Ask the user for a short summary each time a file is committed?"
:tag "Ask for a summary on each commit"
:group 'git-auto-commit-mode
:type 'boolean)
(defcustom gac-shell-and " && "
"How to join commands together in the shell. For fish shell,
you want to customise this to: \" ; and \" instead of the default."
:tag "Join shell commands"
:group 'git-auto-commit-mode
:type 'string)
(defcustom gac-add-additional-flag ""
"Flag to add to the git add command."
:tag "git add flag"
:group 'git-auto-commit-mode
:type 'string)
(defcustom gac-commit-additional-flag ""
"Flag to add to the git commit command."
:tag "git commit flag"
:group 'git-auto-commit-mode
:type 'string)
(defcustom gac-silent-message-p nil
"Should git output be output to the message area?"
:tag "Quiet message output"
:group 'git-auto-commit-mode
:type 'boolean)
(defcustom gac-debounce-interval nil
"Debounce automatic commits to avoid hammering Git.
If non-nil a commit will be scheduled to occur that many seconds
in the future. Note that this uses Emacs timer functionality, and
is subject to its limitations."
:tag "Debounce interval"
:group 'git-auto-commit-mode
:type '(choice (number :tag "Interval in seconds")
(const :tag "Off" nil)))
(make-variable-buffer-local 'gac-debounce-interval)
(defcustom gac-default-message nil
"Default message for automatic commits.
It can be:
- nil to use the default FILENAME
- a string which is used
- a function returning a string, called with FILENAME as
argument, in which case the result is used as commit message
"
:tag "Default commit message"
:group 'git-auto-commit-mode
:type '(choice (string :tag "Commit message")
(const :tag "Default: FILENAME" nil)
(function :tag "Function")))
(defun gac-relative-file-name (filename)
"Find the path to FILENAME relative to the git directory."
(let* ((git-dir
(string-trim-right
(shell-command-to-string "git rev-parse --show-toplevel"))))
(file-relative-name filename git-dir)))
(defun gac-password (proc string)
"Ask the user for a password when necessary.
PROC is the process running git. STRING is the line that was
output by PROC."
(let (ask)
(cond
((or
(string-match "^Enter passphrase for key '\\\(.*\\\)': $" string)
(string-match "^\\\(.*\\\)'s password:" string))
(setq ask (format "Password for '%s': " (match-string 1 string))))
((string-match "^[pP]assword:" string)
(setq ask "Password:")))
(when ask
(process-send-string proc (concat (read-passwd ask nil) "\n")))))
(defun gac-process-filter (proc string)
"Check if PROC is asking for a password and promps the user if so.
STRING is the output line from PROC."
(save-current-buffer
(set-buffer (process-buffer proc))
(let ((inhibit-read-only t))
(gac-password proc string))))
(defun gac-process-sentinel (proc status)
"Report PROC change to STATUS."
(message "git %s" (substring status 0 -1)))
(defun gac--commit-msg (filename)
"Get a commit message.
Default to FILENAME."
(let ((relative-filename (gac-relative-file-name filename)))
(if (not gac-ask-for-summary-p)
(if gac-default-message
(if (functionp gac-default-message)
(funcall gac-default-message filename)
gac-default-message)
relative-filename)
(or gac-default-message relative-filename)
(read-string "Summary: " nil nil relative-filename))))
(defun gac-commit (buffer)
"Commit the current buffer's file to git."
(let* ((buffer-file (buffer-file-name buffer))
(filename (convert-standard-filename
(file-name-nondirectory buffer-file)))
(commit-msg (gac--commit-msg buffer-file))
(default-directory (file-name-directory buffer-file)))
(funcall (if gac-silent-message-p
#'call-process-shell-command
#'shell-command)
(concat "git add " gac-add-additional-flag " " (shell-quote-argument filename)
gac-shell-and
"git commit -m " (shell-quote-argument commit-msg)
" " gac-commit-additional-flag))))
(defun gac-push (buffer)
"Push commits to the current upstream.
This doesn't check or ask for a remote, so the correct remote
should already have been set up."
;; gac-push is currently only called from gac--after-save, where it is wrapped
;; in with-current-buffer, which should already take care of
;; default-directory. The explicit binding here is defensive, in case gac-push
;; starts being used elsewhere.
(let ((default-directory (file-name-directory (buffer-file-name buffer))))
(let ((proc (start-process "git" "*git-auto-push*" "git" "push")))
(set-process-sentinel proc 'gac-process-sentinel)
(set-process-filter proc 'gac-process-filter))))
(defvar gac--debounce-timers (make-hash-table :test #'equal))
(defun gac--debounced-save ()
(let* ((actual-buffer (current-buffer))
(current-buffer-debounce-timer (gethash actual-buffer gac--debounce-timers)))
(unless current-buffer-debounce-timer
(puthash actual-buffer
(run-at-time gac-debounce-interval nil
#'gac--after-save
actual-buffer)
gac--debounce-timers))))
(defun gac--buffer-is-tracked (buffer)
"Check to see if BUFFER’s file is tracked in git."
(let ((file-name (convert-standard-filename
(file-name-nondirectory
(buffer-file-name buffer)))))
(not (string=
(shell-command-to-string (concat "git ls-files " file-name))
""))))
(defun gac--buffer-has-changes (buffer)
"Check to see if there is any change in BUFFER."
(let ((file-name (convert-standard-filename
(file-name-nondirectory
(buffer-file-name buffer)))))
(not (string=
(shell-command-to-string (concat "git diff " file-name))
""))))
(defun gac--after-save (buffer)
(unwind-protect
(when (and (buffer-live-p buffer)
(or (and gac-automatically-add-new-files-p
(not (gac--buffer-is-tracked buffer)))
(gac--buffer-has-changes buffer)))
(gac-commit buffer)
(with-current-buffer buffer
;; with-current-buffer required here because gac-automatically-push-p
;; is buffer-local
(when gac-automatically-push-p
(gac-push buffer))))
(remhash buffer gac--debounce-timers)))
(defun gac-kill-buffer-hook ()
(when (and gac-debounce-interval
gac--debounce-timers
(gethash (current-buffer) gac--debounce-timers))
(gac--after-save (current-buffer))))
(add-hook 'kill-buffer-hook #'gac-kill-buffer-hook)
(defun gac-after-save-func ()
"Commit the current file.
When `gac-automatically-push-p' is non-nil also push."
(if gac-debounce-interval
(gac--debounced-save)
(gac--after-save (current-buffer))))
;;;###autoload
(define-minor-mode git-auto-commit-mode
"Automatically commit any changes made when saving with this
mode turned on and optionally push them too."
:lighter " ga"
(if git-auto-commit-mode
(add-hook 'after-save-hook 'gac-after-save-func t t)
(remove-hook 'after-save-hook 'gac-after-save-func t)))
(provide 'git-auto-commit-mode)
;;; git-auto-commit-mode.el ends here