system-manager-wakasu
1;;; config-vcs.el --- -*- lexical-binding: t; -*-
2;;; Commentary:
3;;; Version control configuration
4;;; Code:
5
6
7(defun vde/vc-browse-remote (&optional current-line)
8 "Open the repository's remote URL in the browser.
9If CURRENT-LINE is non-nil, point to the current branch, file, and line.
10Otherwise, open the repository's main page."
11 (interactive "P")
12 (let* ((remote-url (string-trim (vc-git--run-command-string nil "config" "--get" "remote.origin.url")))
13 (branch (string-trim (vc-git--run-command-string nil "rev-parse" "--abbrev-ref" "HEAD")))
14 (file (string-trim (file-relative-name (buffer-file-name) (vc-root-dir))))
15 (line (line-number-at-pos)))
16 (message "Opening remote on browser: %s" remote-url)
17 (if (and remote-url (string-match "\\(?:git@\\|https://\\)\\([^:/]+\\)[:/]\\(.+?\\)\\(?:\\.git\\)?$" remote-url))
18 (let ((host (match-string 1 remote-url))
19 (path (match-string 2 remote-url)))
20 ;; Convert SSH URLs to HTTPS (e.g., git@github.com:user/repo.git -> https://github.com/user/repo)
21 (when (string-prefix-p "git@" host)
22 (setq host (replace-regexp-in-string "^git@" "" host)))
23 ;; Construct the appropriate URL based on CURRENT-LINE
24 (browse-url
25 (if current-line
26 (format "https://%s/%s/blob/%s/%s#L%d" host path branch file line)
27 (format "https://%s/%s" host path))))
28 (message "Could not determine repository URL"))))
29
30(global-set-key (kbd "C-x v B") 'vde/vc-browse-remote)
31
32(use-package vc
33 :config
34 (setq-default vc-find-revision-no-save t
35 vc-follow-symlinks t)
36 :bind (("C-x v f" . vc-log-incoming) ; git fetch
37 ("C-x v F" . vc-update)
38 ("C-x v d" . vc-diff)))
39
40(use-package vc-dir
41 :config
42 (defun vde/vc-dir-project ()
43 "Unconditionally display `vc-diff' for the current project."
44 (interactive)
45 (vc-dir (vc-root-dir)))
46
47 (defun vde/vc-dir-jump ()
48 "Jump to present directory in a `vc-dir' buffer."
49 (interactive)
50 (vc-dir default-directory))
51 :bind (("C-x v p" . vde/vc-dir-project)
52 ("C-x v j" . vde/vc-dir-jump) ; similar to `dired-jump'
53 :map vc-dir-mode-map
54 ("f" . vc-log-incoming) ; replaces `vc-dir-find-file' (use RET)
55 ("F" . vc-update) ; symmetric with P: `vc-push'
56 ("d" . vc-diff) ; align with D: `vc-root-diff'
57 ("k" . vc-dir-clean-files)))
58
59(use-package vc-git
60 :config
61 (setq vc-git-diff-switches "--patch-with-stat")
62 (setq vc-git-print-log-follow t))
63
64(use-package vc-annotate
65 :config
66 (setq vc-annotate-display-mode 'scale)
67 :bind (("C-x v a" . vc-annotate)
68 :map vc-annotate-mode-map
69 ("t" . vc-annotate-toggle-annotation-visibility)))
70
71(use-package ediff
72 :commands (ediff ediff-files ediff-merge ediff3 ediff-files3 ediff-merge3)
73 :config
74 (setq ediff-window-setup-function 'ediff-setup-windows-plain)
75 (setq ediff-split-window-function 'split-window-horizontally)
76 (setq ediff-diff-options "-w")
77 (add-hook 'ediff-after-quit-hook-internal 'winner-undo))
78
79(use-package diff
80 :config
81 (setq diff-default-read-only nil)
82 (setq diff-advance-after-apply-hunk t)
83 (setq diff-update-on-the-fly t)
84 (setq diff-refine 'font-lock)
85 (setq diff-font-lock-prettify nil)
86 (setq diff-font-lock-syntax nil))
87
88(use-package magit-popup)
89
90(defun th/magit--with-difftastic (buffer command)
91 "Run COMMAND with GIT_EXTERNAL_DIFF=difft then show result in BUFFER."
92 (let ((process-environment
93 (cons (concat "GIT_EXTERNAL_DIFF=difft --width="
94 (number-to-string (frame-width)))
95 process-environment)))
96 ;; Clear the result buffer (we might regenerate a diff, e.g., for
97 ;; the current changes in our working directory).
98 (with-current-buffer buffer
99 (setq buffer-read-only nil)
100 (erase-buffer))
101 ;; Now spawn a process calling the git COMMAND.
102 (make-process
103 :name (buffer-name buffer)
104 :buffer buffer
105 :command command
106 ;; Don't query for running processes when emacs is quit.
107 :noquery t
108 ;; Show the result buffer once the process has finished.
109 :sentinel (lambda (proc event)
110 (when (eq (process-status proc) 'exit)
111 (with-current-buffer (process-buffer proc)
112 (goto-char (point-min))
113 (ansi-color-apply-on-region (point-min) (point-max))
114 (setq buffer-read-only t)
115 (view-mode)
116 (end-of-line)
117 ;; difftastic diffs are usually 2-column side-by-side,
118 ;; so ensure our window is wide enough.
119 (let ((width (current-column)))
120 (while (zerop (forward-line 1))
121 (end-of-line)
122 (setq width (max (current-column) width)))
123 ;; Add column size of fringes
124 (setq width (+ width
125 (fringe-columns 'left)
126 (fringe-columns 'right)))
127 (goto-char (point-min))
128 (pop-to-buffer
129 (current-buffer)
130 `(;; If the buffer is that wide that splitting the frame in
131 ;; two side-by-side windows would result in less than
132 ;; 80 columns left, ensure it's shown at the bottom.
133 ,(when (> 80 (- (frame-width) width))
134 #'display-buffer-at-bottom)
135 (window-width
136 . ,(min width (frame-width))))))))))))
137(defun th/magit-show-with-difftastic (rev)
138 "Show the result of \"git show REV\" with GIT_EXTERNAL_DIFF=difft."
139 (interactive
140 (list (or
141 ;; If REV is given, just use it.
142 (when (boundp 'rev) rev)
143 ;; If not invoked with prefix arg, try to guess the REV from
144 ;; point's position.
145 (and (not current-prefix-arg)
146 (or (magit-thing-at-point 'git-revision t)
147 (magit-branch-or-commit-at-point)))
148 ;; Otherwise, query the user.
149 (magit-read-branch-or-commit "Revision"))))
150 (if (not rev)
151 (error "No revision specified")
152 (th/magit--with-difftastic
153 (get-buffer-create (concat "*git show difftastic " rev "*"))
154 (list "git" "--no-pager" "show" "--ext-diff" rev))))
155(defun th/magit-diff-with-difftastic (arg)
156 "Show the result of \"git diff ARG\" with GIT_EXTERNAL_DIFF=difft."
157 (interactive
158 (list (or
159 ;; If RANGE is given, just use it.
160 (when (boundp 'range) range)
161 ;; If prefix arg is given, query the user.
162 (and current-prefix-arg
163 (magit-diff-read-range-or-commit "Range"))
164 ;; Otherwise, auto-guess based on position of point, e.g., based on
165 ;; if we are in the Staged or Unstaged section.
166 (pcase (magit-diff--dwim)
167 ('unmerged (error "unmerged is not yet implemented"))
168 ('unstaged nil)
169 ('staged "--cached")
170 (`(stash . ,value) (error "stash is not yet implemented"))
171 (`(commit . ,value) (format "%s^..%s" value value))
172 ((and range (pred stringp)) range)
173 (_ (magit-diff-read-range-or-commit "Range/Commit"))))))
174 (let ((name (concat "*git diff difftastic"
175 (if arg (concat " " arg) "")
176 "*")))
177 (th/magit--with-difftastic
178 (get-buffer-create name)
179 `("git" "--no-pager" "diff" "--ext-diff" ,@(when arg (list arg))))))
180
181(use-package magit
182 :unless noninteractive
183 :commands (magit-status magit-clone magit-pull magit-blame magit-log-buffer-file magit-log)
184 :bind (("C-c v c" . magit-commit)
185 ("C-c v C" . magit-checkout)
186 ("C-c v b" . magit-branch)
187 ("C-c v d" . magit-dispatch)
188 ("C-c v f" . magit-fetch)
189 ("C-c v g" . magit-blame)
190 ("C-c v l" . magit-log-buffer-file)
191 ("C-c v L" . magit-log)
192 ("C-c v p" . magit-pull)
193 ("C-c v P" . magit-push)
194 ("C-c v r" . magit-rebase)
195 ("C-c v s" . magit-stage)
196 ("C-c v v" . magit-status))
197 :config
198 (transient-define-prefix th/magit-aux-commands ()
199 "My personal auxiliary magit commands."
200 ["Auxiliary commands"
201 ("d" "Difftastic Diff (dwim)" th/magit-diff-with-difftastic)
202 ("s" "Difftastic Show" th/magit-show-with-difftastic)])
203 (transient-append-suffix 'magit-dispatch "!"
204 '("#" "My Magit Cmds" th/magit-aux-commands))
205 (setq-default magit-save-repository-buffers 'dontask
206 magit-refs-show-commit-count 'all
207 magit-branch-prefer-remote-upstream '("main")
208 magit-display-buffer-function #'magit-display-buffer-fullframe-status-v1
209 magit-bury-buffer-function #'magit-restore-window-configuration
210 magit-refresh-status-buffer nil)
211
212 (setq-default git-commit-summary-max-length 50
213 git-commit-style-convention-checks
214 '(non-empty-second-line
215 overlong-summary-line))
216
217;; TODO: complete with list of issues (async ?)
218;; (transient-append-suffix 'git-commit-insert-trailer "t"
219;; '("i" "Issue numero" hello))
220;;
221;; (defun hello (foo)
222;; (interactive (list (completing-read "Foo number"
223;; '("foo" "bar" "baz"))))
224;; (message foo)
225;; (git-commit--insert-trailer "Hello" foo))
226
227 ;; (magit-define-popup-option 'magit-rebase-popup
228 ;; ?S "Sign using gpg" "--gpg-sign=" #'magit-read-gpg-secret-key)
229 (magit-define-popup-switch 'magit-log-popup
230 ?m "Omit merge commits" "--no-merges")
231 ;; cargo-culted from https://github.com/magit/magit/issues/3717#issuecomment-734798341
232 ;; valid gitlab options are defined in https://docs.gitlab.com/ee/user/project/push_options.html
233 ;;
234 ;; the second argument to transient-append-suffix is where to append
235 ;; to, not sure what -u is, but this works
236 (transient-append-suffix 'magit-push "-u"
237 '(1 "=s" "Skip gitlab pipeline" "--push-option=ci.skip"))
238 (transient-append-suffix 'magit-push "=s"
239 '(1 "=m" "Create gitlab merge-request" "--push-option=merge_request.create"))
240 (transient-append-suffix 'magit-push "=m"
241 '(1 "=o" "Set push option" "--push-option=")) ;; Will prompt, can only set one extra
242
243 (defun vde/fetch-and-rebase-from-upstream ()
244 ""
245 (interactive)
246 (magit-fetch-all "--quiet")
247 (magit-git-rebase (concat "upstream/" (vc-git--symbolic-ref (buffer-file-name))) "-sS"))
248
249 ;; Hide "Recent Commits"
250 (magit-add-section-hook 'magit-status-sections-hook
251 'magit-insert-modules
252 'magit-insert-unpushed-to-upstream
253 'magit-insert-unpulled-from-upstream)
254 ;; No need for tag in the status header
255 (remove-hook 'magit-status-sections-hook 'magit-insert-tags-header)
256 (setq-default magit-module-sections-nested nil)
257
258 ;; Show refined hunks during diffs
259 (set-default 'magit-diff-refine-hunk t))
260
261(use-package gitconfig-mode
262 :commands (gitconfig-mode)
263 :mode (("/\\.gitconfig\\'" . gitconfig-mode)
264 ("/\\.git/config\\'" . gitconfig-mode)
265 ("/git/config\\'" . gitconfig-mode)
266 ("/\\.gitmodules\\'" . gitconfig-mode)))
267
268(use-package gitignore-mode
269 :commands (gitignore-mode)
270 :mode (("/\\.gitignore\\'" . gitignore-mode)
271 ("/\\.git/info/exclude\\'" . gitignore-mode)
272 ("/git/ignore\\'" . gitignore-mode)))
273
274(use-package gitattributes-mode
275 :commands (gitattributes-mode)
276 :mode (("/\\.gitattributes" . gitattributes-mode)))
277
278(use-package dired-git-info
279 :disabled
280 :bind (:map dired-mode-map
281 (")" . dired-git-info-mode))
282 :defer 2)
283
284(defun git-blame-line ()
285 "Runs `git blame` on the current line and
286 adds the commit id to the kill ring"
287 (interactive)
288 (let* ((line-number (save-excursion
289 (goto-char (point-at-bol))
290 (+ 1 (count-lines 1 (point)))))
291 (line-arg (format "%d,%d" line-number line-number))
292 (commit-buf (generate-new-buffer "*git-blame-line-commit*")))
293 (call-process "git" nil commit-buf nil
294 "blame" (buffer-file-name) "-L" line-arg)
295 (let* ((commit-id (with-current-buffer commit-buf
296 (buffer-substring 1 9)))
297 (log-buf (generate-new-buffer "*git-blame-line-log*")))
298 (kill-new commit-id)
299 (call-process "git" nil log-buf nil
300 "log" "-1" "--pretty=%h %an %s" commit-id)
301 (with-current-buffer log-buf
302 (message "Line %d: %s" line-number (buffer-string)))
303 (kill-buffer log-buf))
304 (kill-buffer commit-buf)))
305
306(use-package diff-hl
307 :hook (find-file . diff-hl-mode)
308 :hook (prog-mode . diff-hl-mode)
309 :hook (magit-post-refresh . diff-hl-magit-post-refresh)
310 :bind
311 (:map diff-hl-command-map
312 ("n" . diff-hl-next-hunk)
313 ("p" . diff-hl-previous-hunk)
314 ("[" . nil)
315 ("]" . nil)
316 ("DEL" . diff-hl-revert-hunk)
317 ("<delete>" . diff-hl-revert-hunk)
318 ("SPC" . diff-hl-mark-hunk)
319 :map vc-prefix-map
320 ("n" . diff-hl-next-hunk)
321 ("p" . diff-hl-previous-hunk)
322 ("s" . diff-hl-stage-dwim)
323 ("DEL" . diff-hl-revert-hunk)
324 ("<delete>" . diff-hl-revert-hunk)
325 ("SPC" . diff-hl-mark-hunk))
326 :config
327 (put 'diff-hl-inline-popup-hide
328 'repeat-map 'diff-hl-command-map))
329
330(use-package diff-hl-inline-popup
331 :after (diff-hl))
332(use-package diff-hl-show-hunk
333 :after (diff-hl))
334
335(use-package diff-hl-dired
336 :after (diff-hl)
337 :hook (dired-mode . diff-hl-dired-mode))
338
339(use-package consult-vc-modified-files
340 :after consult
341 :bind
342 ("C-x v /" . consult-vc-modified-files))
343
344;; FIXME bind pr-review-submit-review
345(use-package pr-review
346 :commands (pr-review pr-review-open pr-review-submit-review)
347;; :bind
348;; (("M-<SPC> p r" . pr-review-submit-review))
349 :custom
350 (pr-review-ghub-host "api.github.com")
351 (pr-review-notification-include-read nil)
352 (pr-review-notification-include-unsubscribed nil))
353
354(use-package pr-review-search
355 :commands (pr-review-search pr-review-search-open pr-review-current-repository pr-review-current-repository-search)
356;; :bind
357;; (("M-<SPC> p a" . pr-review-current-repository)
358;; ;; FIXME understand why this one doesn't work
359 ;; ("M-<SPC> p s" . pr-review-current-repository-search)))
360 )
361
362(use-package pr-review-notification
363 :commands (pr-review-notification)
364;; :bind
365 ;; (("M-<SPC> p n" . pr-review-notification)))
366 )
367
368(defun pr-review-current-repository-search (query)
369 "Run pr-review-search on the current repository."
370 (interactive "sSearch query: ")
371 (pr-review-search (format "is:pr archived:false is:open repo:%s %s" (vde/gh-get-current-repo) query)))
372
373(defun pr-review-current-repository ()
374 "Run pr-review-search on the current repository."
375 (interactive)
376 (pr-review-search (format "is:pr archived:false is:open repo:%s" (vde/gh-get-current-repo))))
377
378;; (pr-review-search "build is:pr archive:false is:open repo:tektoncd/pipeline")
379;; (pr-review-search "build")
380
381;; TODO this is relatively slow. Cache result or ?
382(defun vde/gh-get-current-repo ()
383 "Get the current repository name using the `gh' command line."
384 (unless (executable-find "gh")
385 (error "GitHub CLI (gh) command not found"))
386
387 (with-temp-buffer
388 (let ((exit-code (call-process "gh" nil t nil "repo" "view" "--json" "owner,name" "--template" "{{.owner.login}}/{{.name}}")))
389 (unless (= exit-code 0)
390 (error "Failed to get repository info: gh command exited with code %d" exit-code))
391 (string-trim (buffer-string)))))
392
393
394(provide 'config-vcs)
395;;; config-vcs.el ends here