nftable-migration
  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