diff --git a/docs/forge.org b/docs/forge.org index 31d85c6a..95b558db 100644 --- a/docs/forge.org +++ b/docs/forge.org @@ -869,7 +869,7 @@ argument, i.e., ~C-u RET~. - Key: o [on repository in repository list] (forge-browse-this-repository) :: These commands visit the topic, issue(s), pull-request(s), post, - branch, commit, remote or repository at point in a browser. + branch, commit, remote, repository or blob at point in a browser. - Command: forge-browse-commit :: - Command: forge-browse-branch :: @@ -884,6 +884,22 @@ argument, i.e., ~C-u RET~. These commands read a topic, issue(s), pull-request(s), branch, commit, remote or repository, and open it in a browser. +- Command: forge-browse-commit :: + + This command visit a blob in a browser. + + When invoked from a blob- or file-visiting buffer, visit that blob + without prompting. If the region is active, try to jump to the marked + line or lines, and highlight them in the browser. To what extend that + is possible depends on the forge. When the region is not active just + visit the blob, without trying to jump to the current line. When + jumping to a line, always use a commit hash as part of the URL. From + a file in the worktree with no active region, instead use the branch + name as part of the URL, unless a prefix argument is used. + + When invoked from any other buffer, prompt the user for a branch or + commit, and for a file. + * Creating Topics and Posts We call both issues and pull-requests "topics". The contributions to diff --git a/docs/forge.texi b/docs/forge.texi index 330cfd69..1829689b 100644 --- a/docs/forge.texi +++ b/docs/forge.texi @@ -996,7 +996,7 @@ separate buffer. @findex forge-browse-this-topic @findex forge-browse-this-repository These commands visit the topic, issue(s), pull-request(s), post, -branch, commit, remote or repository at point in a browser. +branch, commit, remote, repository or blob at point in a browser. @end table @deffn Command forge-browse-commit @@ -1028,6 +1028,22 @@ These commands read a topic, issue(s), pull-request(s), branch, commit, remote or repository, and open it in a browser. @end table +@deffn Command forge-browse-commit +This command visit a blob in a browser. + +When invoked from a blob- or file-visiting buffer, visit that blob +without prompting. If the region is active, try to jump to the marked +line or lines, and highlight them in the browser. To what extend that +is possible depends on the forge. When the region is not active just +visit the blob, without trying to jump to the current line. When +jumping to a line, always use a commit hash as part of the URL@. From +a file in the worktree with no active region, instead use the branch +name as part of the URL, unless a prefix argument is used. + +When invoked from any other buffer, prompt the user for a branch or +commit, and for a file. +@end deffn + @node Creating Topics and Posts @chapter Creating Topics and Posts diff --git a/lisp/forge-bitbucket.el b/lisp/forge-bitbucket.el index 2fa1c830..859d0579 100644 --- a/lisp/forge-bitbucket.el +++ b/lisp/forge-bitbucket.el @@ -38,6 +38,7 @@ (commit-url-format :initform "https://%h/%o/%n/commits/%r") (branch-url-format :initform "https://%h/%o/%n/branch/%r") (remote-url-format :initform "https://%h/%o/%n/src") + (blob-url-format :initform "https://%h/%o/%n/src/%r/%f") (create-issue-url-format :initform "https://%h/%o/%n/issues/new") (create-pullreq-url-format :initform "https://%h/%o/%n/pull-requests/new"))) diff --git a/lisp/forge-commands.el b/lisp/forge-commands.el index 124cc939..81e81a4a 100644 --- a/lisp/forge-commands.el +++ b/lisp/forge-commands.el @@ -293,6 +293,24 @@ argument also offer closed pull-requests." (interactive (list (forge-read-repository "Browse repository"))) (browse-url (forge-get-url repository))) +;;;###autoload +(defun forge-browse-blob (commit file &optional line end force-hash) + "Visit a blob using a browser. + +When invoked from a blob- or file-visiting buffer, visit that blob +without prompting. If the region is active, try to jump to the marked +line or lines, and highlight them in the browser. To what extend that +is possible depends on the forge. When the region is not active just +visit the blob, without trying to jump to the current line. When +jumping to a line, always use a commit hash as part of the URL. From +a file in the worktree with no active region, instead use the branch +name as part of the URL, unless a prefix argument is used. + +When invoked from any other buffer, prompt the user for a branch or +commit, and for a file." + (interactive (forge--browse-blob-args)) + (browse-url (forge-get-url :blob commit file line end force-hash))) + ;;;###autoload(autoload 'forge-browse-this-topic "forge-commands" nil t) (transient-define-suffix forge-browse-this-topic () "Visit the topic at point using a browser." @@ -308,7 +326,7 @@ argument also offer closed pull-requests." ;;;###autoload (defun forge-copy-url-at-point-as-kill () - "Copy the url of the thing at point." + "Copy the url of thing at point or the thing visited in the current buffer." (interactive) (if-let ((target (forge--browse-target))) (let ((url (if (stringp target) target (forge-get-url target)))) @@ -337,12 +355,32 @@ argument also offer closed pull-requests." (forge-get-url :branch branch)) (and-let* ((remote (magit-remote-at-point))) (forge-get-url :remote remote)) + (and-let* ((file (magit-file-at-point))) + (forge-get-url :blob nil file)) (forge-post-at-point) (forge-current-topic) + (and (or magit-buffer-file-name buffer-file-name) + (apply #'forge-get-url :blob (forge--browse-blob-args))) (and magit-buffer-revision (forge-get-url :commit magit-buffer-revision)) (forge-get-repository :stub?))) +(defun forge--browse-blob-args () + (cond + (magit-buffer-file-name + `(,(or magit-buffer-refname magit-buffer-revision) + ,(magit-file-relative-name magit-buffer-file-name) + ,@(magit-file-region-line-numbers) + ,current-prefix-arg)) + (buffer-file-name + `(nil + ,(magit-file-relative-name buffer-file-name) + ,@(magit-file-region-line-numbers) + ,current-prefix-arg)) + ((let ((commit (magit-read-local-branch-or-commit + "Browse file from commit"))) + (list commit (magit-read-file-from-rev commit "Browse file")))))) + ;;;; Urls (cl-defgeneric forge-get-url (obj) @@ -369,6 +407,23 @@ argument also offer closed pull-requests." (forge--format repo 'commit-url-format `((?r . ,(magit-commit-p commit)))))) +(cl-defmethod forge-get-url ((_(eql :blob)) commit file + &optional line end force-hash) + (let* ((commit (or (and (magit-branch-p commit) + (cdr (magit-split-branch-name commit))) + (and commit (magit-commit-p commit)) + (and (not (or line force-hash)) + (magit-get-current-branch)) + (magit-rev-parse "HEAD"))) + (repo (forge-get-repository :stub)) + (format (oref repo blob-url-format))) + (when (cl-typep repo 'forge-gitweb-repository) + (setq commit (concat (if (magit-branch-p commit) "hb=" "h=") commit))) + (concat + (forge--format repo format `((?r . ,commit) (?f . ,file))) + (and line (forge-format-blob-lines repo line + (and (not (equal line end)) end)))))) + (cl-defmethod forge-get-url ((_(eql :branch)) branch) (let (remote) (if (magit-remote-branch-p branch) @@ -393,6 +448,27 @@ argument also offer closed pull-requests." (cl-defmethod forge-get-url ((notify forge-notification)) (oref notify url)) +(cl-defmethod forge-format-blob-lines ((repo forge-repository) line end) + (cl-etypecase repo ;Third-party classes require separate methods. + ((or forge-github-repository + forge-gitlab-repository ;Also supports "#L%s-%s". + forge-forgejo-repository + forge-gitea-repository + forge-gogs-repository) + (format (if end "#L%s-L%s" "#L%s") line end)) + (forge-bitbucket-repository + (format (if end "#lines-%s:%s" "#lines-%s") line end)) + ((or forge-cgit-repository + forge-cgit*-repository + forge-cgit**-repository) + (format "#n%s" line)) + ((or forge-gitweb-repository + forge-repoorcz-repository + forge-stagit-repository) + (format "#l%s" line)) + (forge-srht-repository + (format "#L%s" line)))) + ;;; Visit ;;;###autoload diff --git a/lisp/forge-forgejo.el b/lisp/forge-forgejo.el index 00582307..2cd0a306 100644 --- a/lisp/forge-forgejo.el +++ b/lisp/forge-forgejo.el @@ -37,6 +37,7 @@ (commit-url-format :initform "https://%h/%o/%n/commit/%r") (branch-url-format :initform "https://%h/%o/%n/commits/branch/%r") (remote-url-format :initform "https://%h/%o/%n") + (blob-url-format :initform "https://%h/%o/%n/src/%r/%f") (create-issue-url-format :initform "https://%h/%o/%n/issues/new") (create-pullreq-url-format :initform "https://%h/%o/%n/pulls") ; sic (pullreq-refspec :initform "+refs/pull/*/head:refs/pullreqs/*"))) diff --git a/lisp/forge-gitea.el b/lisp/forge-gitea.el index 74cede69..d7a5c40d 100644 --- a/lisp/forge-gitea.el +++ b/lisp/forge-gitea.el @@ -38,6 +38,7 @@ (commit-url-format :initform "https://%h/%o/%n/commit/%r") (branch-url-format :initform "https://%h/%o/%n/commits/branch/%r") (remote-url-format :initform "https://%h/%o/%n") + (blob-url-format :initform "https://%h/%o/%n/src/%r/%f") (create-issue-url-format :initform "https://%h/%o/%n/issues/new") (create-pullreq-url-format :initform "https://%h/%o/%n/pulls") ; sic (pullreq-refspec :initform "+refs/pull/*/head:refs/pullreqs/*"))) diff --git a/lisp/forge-github.el b/lisp/forge-github.el index 13705a3f..9daf9587 100644 --- a/lisp/forge-github.el +++ b/lisp/forge-github.el @@ -40,6 +40,7 @@ (commit-url-format :initform "https://%h/%o/%n/commit/%r") (branch-url-format :initform "https://%h/%o/%n/commits/%r") (remote-url-format :initform "https://%h/%o/%n") + (blob-url-format :initform "https://%h/%o/%n/blob/%r/%f") (create-issue-url-format :initform "https://%h/%o/%n/issues/new") (create-pullreq-url-format :initform "https://%h/%o/%n/compare") (pullreq-refspec :initform "+refs/pull/*/head:refs/pullreqs/*"))) diff --git a/lisp/forge-gitlab.el b/lisp/forge-gitlab.el index 7391c36b..c87df872 100644 --- a/lisp/forge-gitlab.el +++ b/lisp/forge-gitlab.el @@ -40,6 +40,7 @@ (commit-url-format :initform "https://%h/%o/%n/commit/%r") (branch-url-format :initform "https://%h/%o/%n/commits/%r") (remote-url-format :initform "https://%h/%o/%n") + (blob-url-format :initform "https://%h/%o/%n/-/blob/%r/%f") (create-issue-url-format :initform "https://%h/%o/%n/issues/new") (create-pullreq-url-format :initform "https://%h/%o/%n/merge_requests/new") (pullreq-refspec :initform "+refs/merge-requests/*/head:refs/pullreqs/*"))) diff --git a/lisp/forge-gogs.el b/lisp/forge-gogs.el index 9fd5d62b..eb3a0cd7 100644 --- a/lisp/forge-gogs.el +++ b/lisp/forge-gogs.el @@ -37,6 +37,7 @@ (commit-url-format :initform "https://%h/%o/%n/commit/%r") (branch-url-format :initform "https://%h/%o/%n/commits/%r") (remote-url-format :initform "https://%h/%o/%n") + (blob-url-format :initform "https://%h/%o/%n/src/%r/%f") (create-issue-url-format :initform "https://%h/%o/%n/issues/new") (create-pullreq-url-format :initform "https://%h/%o/%n/pulls") ; sic (pullreq-refspec :initform "+refs/pull/*/head:refs/pullreqs/*"))) diff --git a/lisp/forge-repo.el b/lisp/forge-repo.el index f154b386..278b0c3b 100644 --- a/lisp/forge-repo.el +++ b/lisp/forge-repo.el @@ -41,6 +41,7 @@ (commit-url-format :initform nil :allocation :class) (branch-url-format :initform nil :allocation :class) (remote-url-format :initform nil :allocation :class) + (blob-url-format :initform nil :allocation :class) (create-issue-url-format :initform nil :allocation :class) (create-pullreq-url-format :initform nil :allocation :class) (pullreq-refspec :initform nil :allocation :class) diff --git a/lisp/forge-semi.el b/lisp/forge-semi.el index f07d1b06..f34a6181 100644 --- a/lisp/forge-semi.el +++ b/lisp/forge-semi.el @@ -29,13 +29,18 @@ (defclass forge-gitweb-repository (forge-noapi-repository) ((commit-url-format :initform "https://%h/gitweb/?p=%P.git;a=commitdiff;h=%r") (branch-url-format :initform "https://%h/gitweb/?p=%P.git;a=log;h=refs/heads/%r") - (remote-url-format :initform "https://%h/gitweb/?p=%P.git;a=summary")) + (remote-url-format :initform "https://%h/gitweb/?p=%P.git;a=summary") + ;; We must use "hb=BRANCH" because "h=refs/heads/BRANCH" does not work + ;; here. So "%r" stands for either "hb=BRANCH" or "h=HASH" and which + ;; it is, has to be handled as a special case in `forge-get-url(:blob)'. + (blob-url-format :initform "https://%h/gitweb/?p=%P.git;a=blob;f=%s;%r")) "Gitweb from https://git-scm.com/docs/gitweb.") (defclass forge-cgit-repository (forge-noapi-repository) ((commit-url-format :initform "https://%h/%p.git/commit/?id=%r") (branch-url-format :initform "https://%h/%p.git/log/?h=%r") - (remote-url-format :initform "https://%h/%p.git/about")) + (remote-url-format :initform "https://%h/%p.git/about") + (blob-url-format :initform "https://%h/%p.git/tree/%f?id=%r")) "Cgit from https://git.zx2c4.com/cgit/about. Different hosts use different url schemata, so we need multiple classes. See their definitions in \"forge-semi.el\".") @@ -43,7 +48,8 @@ classes. See their definitions in \"forge-semi.el\".") (defclass forge-cgit*-repository (forge-cgit-repository) ((commit-url-format :initform "https://%h/cgit/%p.git/commit/?id=%r") (branch-url-format :initform "https://%h/cgit/%p.git/log/?h=%r") - (remote-url-format :initform "https://%h/cgit/%p.git/about")) + (remote-url-format :initform "https://%h/cgit/%p.git/about") + (blob-url-format :initform "https://%h/cgit/%p.git/tree/%f?id=%r")) "Cgit from https://git.zx2c4.com/cgit/about. Different hosts use different url schemata, so we need multiple classes. See their definitions in \"forge-semi.el\".") @@ -51,7 +57,8 @@ classes. See their definitions in \"forge-semi.el\".") (defclass forge-cgit**-repository (forge-cgit-repository) ((commit-url-format :initform "https://%h/cgit/%n.git/commit/?id=%r") (branch-url-format :initform "https://%h/cgit/%n.git/log/?h=%r") - (remote-url-format :initform "https://%h/cgit/%n.git/about")) + (remote-url-format :initform "https://%h/cgit/%n.git/about") + (blob-url-format :initform "https://%h/cgit/%n.git/tree/%f?id=%r")) "Cgit from https://git.zx2c4.com/cgit/about. Different hosts use different url schemata, so we need multiple classes. See their definitions in \"forge-semi.el\".") @@ -59,7 +66,8 @@ classes. See their definitions in \"forge-semi.el\".") (defclass forge-repoorcz-repository (forge-cgit-repository) ((commit-url-format :initform "https://%h/%p.git/commit/%r") (branch-url-format :initform "https://%h/%p.git/log/%r") - (remote-url-format :initform "https://%h/%p.git")) + (remote-url-format :initform "https://%h/%p.git") + (blob-url-format :initform "https://%h/%p.git/blob/%r:/%f")) "Cgit fork used on https://repo.or.cz/cgit.git. Different hosts use different url schemata, so we need multiple classes. See their definitions in \"forge-semi.el\".") @@ -67,7 +75,9 @@ classes. See their definitions in \"forge-semi.el\".") (defclass forge-stagit-repository (forge-noapi-repository) ((commit-url-format :initform "https://%h/%n/commit/%r.html") (branch-url-format :initform "https://%h/%n/refs.html") - (remote-url-format :initform "https://%h/%n/file/README.html")) + (remote-url-format :initform "https://%h/%n/file/README.html") + ;; Can only link to the tip of the main branch. + (blob-url-format :initform "https://%h/%n/")) "Stagit from https://codemadness.org/git/stagit/file/README.html. Only the history of \"master\" can be shown, so this links to the list of refs instead of the log of the specified branch.") @@ -75,7 +85,8 @@ list of refs instead of the log of the specified branch.") (defclass forge-srht-repository (forge-noapi-repository) ((commit-url-format :initform "https://%h/~%o/%n/commit/%r") (branch-url-format :initform "https://%h/~%o/%n/log/%r") - (remote-url-format :initform "https://%h/~%o/%n")) + (remote-url-format :initform "https://%h/~%o/%n") + (blob-url-format :initform "https://%h/~%o/%n/tree/%r/item/%f")) "See https://meta.sr.ht.") ;;; _