diff --git a/README.org b/README.org index e4814ea..1da4334 100644 --- a/README.org +++ b/README.org @@ -13,7 +13,7 @@ Collect recent git commits across multiple repositories and format them as org-m - Weekday-aware lookback: on Monday (and weekends) looks back to Friday; otherwise looks back 1 day - Transient menu lets you override the since-date, the repository list and the author list before running - Filters by several authors at once, which helps when you commit under more than one identity -- Output is an org-mode buffer with clickable commit links (via [[https://github.com/magit/orgit][orgit]] or [[https://orgmode.org/worg/org-contrib/org-git-link.html][org-git-link]]) +- Output is an org-mode buffer with clickable commit links (via [[https://github.com/magit/orgit][orgit]] or [[https://orgmode.org/worg/org-contrib/org-git-link.html][org-git-link]]), or https links to the web page of each commit (via [[https://github.com/sshaw/git-link][git-link]]) - Directories that aren't git repos are searched recursively for nested repos * Requirements @@ -102,13 +102,20 @@ Press =s= to generate the standup notes in a =*magit-standup*= buffer. All options can be customized via =M-x customize-group RET magit-standup=. -| Variable | Default | Description | -|---------------------------------+---------+--------------------------------------------------------------------| -| =magit-standup-repos= | =nil= | List of directories to scan. When nil, uses current repo. | -| =magit-standup-repos-max-depth= | =1= | Max depth to search non-repo directories for nested repos. | -| =magit-standup-authors= | =nil= | List of author filters. When nil, uses =git config user.email=. | -| =magit-standup-since-days-ago= | =nil= | Fixed lookback override. When nil, weekday-aware logic. | -| =magit-standup-link-package= | =nil= | Link style: =orgit=, =org-git-link=, =none=, or nil (auto-detect). | +| Variable | Default | Description | +|---------------------------------+---------+--------------------------------------------------------------------------------| +| =magit-standup-repos= | =nil= | List of directories to scan. When nil, uses current repo. | +| =magit-standup-repos-max-depth= | =1= | Max depth to search non-repo directories for nested repos. | +| =magit-standup-authors= | =nil= | List of author filters. When nil, uses =git config user.email=. | +| =magit-standup-since-days-ago= | =nil= | Fixed lookback override. When nil, weekday-aware logic. | +| =magit-standup-link-package= | =nil= | Link style: =orgit=, =org-git-link=, =git-link=, =none=, or nil (auto-detect). | + +** Links + +With =git-link=, each commit links to its web page on the remote, for +example on GitHub. A local commit has no web page until you push it, +so its link opens a "page not found" error. If git-link does not +support the remote, the report shows the commit as plain text. ** Authors diff --git a/magit-standup.el b/magit-standup.el index 772588e..a134bc3 100644 --- a/magit-standup.el +++ b/magit-standup.el @@ -43,6 +43,10 @@ (require 'magit) (require 'transient) +(declare-function git-link-commit "ext:git-link") +(defvar git-link-add-to-kill-ring) +(defvar git-link-open-in-browser) + (defgroup magit-standup nil "Collect recent git commits for standup notes." :group 'magit @@ -78,13 +82,15 @@ identity when a repository has a `.mailmap' file." (defcustom magit-standup-link-package nil "Package to use for linking commit hashes in org output. -When nil, auto-detect by checking if `orgit' or `org-git-link' -is loaded. When `orgit', use orgit-rev links. When -`org-git-link', use git links. When `none', plain text -without links." +When nil, auto-detect by checking if `orgit', `org-git-link' or +`git-link' is loaded. When `orgit', use orgit-rev links. When +`org-git-link', use git links. When `git-link', use https links +to the web page of the commit. When `none', plain text without +links." :type '(choice (const :tag "Auto-detect" nil) (const :tag "orgit" orgit) (const :tag "org-git-link" org-git-link) + (const :tag "git-link" git-link) (const :tag "Plain text" none)) :group 'magit-standup) @@ -144,19 +150,53 @@ searched recursively up to `magit-standup-repos-max-depth'." (defun magit-standup--detect-link-package () "Detect which git-link package is available. Returns `orgit' if orgit is loaded, `org-git-link' if -org-git-link is loaded, or nil if neither is available." +org-git-link is loaded, `git-link' if git-link is loaded, or nil +if none is available." (cond ((featurep 'orgit) 'orgit) - ((featurep 'org-git-link) 'org-git-link))) + ((featurep 'org-git-link) 'org-git-link) + ((featurep 'git-link) 'git-link))) + +(defconst magit-standup--web-link-prefix "https" + "The link prefix that links a commit to its web page.") (defun magit-standup--link-prefix (package) "Return the org link type string for PACKAGE. -PACKAGE should be `orgit', `org-git-link', or nil." +PACKAGE should be `orgit', `org-git-link', `git-link', or nil." (pcase package ('orgit "orgit-rev") ('org-git-link "git") + ('git-link magit-standup--web-link-prefix) (_ nil))) +(defun magit-standup--git-link-url (repo-path hash) + "Return the web URL of commit HASH in REPO-PATH. +Return nil if git-link does not support the remote of REPO-PATH." + ;; `git-link-commit' is a command: it reads the hash at point and copies the URL. + ;; shortcut: git-link finds the remote again for each commit (about 15 ms). + (let ((default-directory (file-name-as-directory repo-path)) + (git-link-add-to-kill-ring nil) + (git-link-open-in-browser nil) + (current-prefix-arg nil) + (inhibit-message t) + (message-log-max nil)) + (with-temp-buffer + (insert hash) + (goto-char (point-min)) + (ignore-error user-error + (call-interactively #'git-link-commit))))) + +(defun magit-standup--format-hash (repo-path hash link-prefix) + "Format HASH of REPO-PATH as an org link of type LINK-PREFIX. +Return HASH as plain text if there is no link to make." + (cond + ((null link-prefix) hash) + ((equal link-prefix magit-standup--web-link-prefix) + (if-let* ((url (magit-standup--git-link-url repo-path hash))) + (format "[[%s][%s]]" url hash) + hash)) + (t (format "[[%s:%s::%s][%s]]" link-prefix repo-path hash hash)))) + (defun magit-standup--format-commit (repo-path line &optional link-prefix) "Format a commit LINE, optionally as an org link. REPO-PATH is the repository directory. LINE is expected to have @@ -166,9 +206,7 @@ the org link prefix string, or nil for plain text." (hash (car parts)) (rest (cadr parts))) (format "%s %s" - (if link-prefix - (format "[[%s:%s::%s][%s]]" link-prefix repo-path hash hash) - hash) + (magit-standup--format-hash repo-path hash link-prefix) rest))) (defun magit-standup--resolve-authors (repo-path &optional authors) diff --git a/test/magit-standup-test.el b/test/magit-standup-test.el index 2fbc60e..3c41ea9 100644 --- a/test/magit-standup-test.el +++ b/test/magit-standup-test.el @@ -34,6 +34,11 @@ (lambda (f) (eq f 'org-git-link)))) (expect (magit-standup--detect-link-package) :to-be 'org-git-link))) + (it "returns git-link when git-link is loaded" + (cl-letf (((symbol-function 'featurep) + (lambda (f) (eq f 'git-link)))) + (expect (magit-standup--detect-link-package) :to-be 'git-link))) + (it "returns nil when nothing is loaded" (cl-letf (((symbol-function 'featurep) (lambda (_) nil))) @@ -46,12 +51,34 @@ (it "returns git for org-git-link" (expect (magit-standup--link-prefix 'org-git-link) :to-equal "git")) + (it "returns https for git-link" + (expect (magit-standup--link-prefix 'git-link) :to-equal "https")) + (it "returns nil for none" (expect (magit-standup--link-prefix 'none) :to-be nil)) (it "returns nil for nil" (expect (magit-standup--link-prefix nil) :to-be nil))) +(describe "magit-standup--git-link-url" + (it "calls git-link-commit without its side effects" + (cl-letf (((symbol-function 'git-link-commit) + (lambda () + (interactive) + (list git-link-add-to-kill-ring + git-link-open-in-browser + current-prefix-arg + (thing-at-point 'word) + default-directory)))) + (let ((current-prefix-arg '(4))) + (expect (magit-standup--git-link-url "/home/user/repo" "abc1234") + :to-equal '(nil nil nil "abc1234" "/home/user/repo/"))))) + + (it "returns nil when git-link-commit signals a user-error" + (cl-letf (((symbol-function 'git-link-commit) + (lambda () (interactive) (user-error "Remote `origin' not found")))) + (expect (magit-standup--git-link-url "/home/user/repo" "abc1234") :to-be nil)))) + (describe "magit-standup--format-commit" (it "formats with orgit-rev link" (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice" "orgit-rev") @@ -61,6 +88,17 @@ (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice" "git") :to-equal "[[git:/home/user/repo::abc123][abc123]] Fix bug <2026-01-05> Alice")) + (it "formats with https link" + (spy-on 'magit-standup--git-link-url :and-return-value "https://github.com/user/repo/commit/abc123") + (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice" "https") + :to-equal "[[https://github.com/user/repo/commit/abc123][abc123]] Fix bug <2026-01-05> Alice") + (expect 'magit-standup--git-link-url :to-have-been-called-with "/home/user/repo" "abc123")) + + (it "formats as plain text when git-link has no URL" + (spy-on 'magit-standup--git-link-url :and-return-value nil) + (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice" "https") + :to-equal "abc123 Fix bug <2026-01-05> Alice")) + (it "formats as plain text when link-prefix is omitted" (expect (magit-standup--format-commit "/home/user/repo" "abc123\0Fix bug <2026-01-05> Alice") :to-equal "abc123 Fix bug <2026-01-05> Alice"))) @@ -303,7 +341,7 @@ (describe "magit-standup--display" (after-each - (when-let ((buf (get-buffer magit-standup--buffer-name))) + (when-let* ((buf (get-buffer magit-standup--buffer-name))) (kill-buffer buf))) (it "creates a buffer with org-mode content"