├── README.md └── github-browse-file.el /README.md: -------------------------------------------------------------------------------- 1 | github-browse-file 2 | ================== 3 | 4 | View the file you're editing in Emacs on GitHub. 5 | 6 | ### Installation: 7 | 8 | Available as a package in [Marmalade](https://marmalade-repo.org/) and [MELPA](http://melpa.org/). 9 | 10 | `M-x package-install github-browse-file` 11 | 12 | You can change some defaults using `M-x customize-group github-browse-file` 13 | 14 | ### Usage: 15 | Call `github-browse-file` (for the git blob) or `github-browse-file-blame` 16 | (for the git blame) to view current file on GitHub. With a prefix argument 17 | (`C-u`), you can force them to use the "master" branch. 18 | 19 | `github-browse-commit` can be used to link to the current commit. 20 | 21 | ### Contributors 22 | * [Charles Comstock](https://github.com/dgtized) 23 | * [Justin Talbott](https://github.com/waymondo) 24 | * [William Roe](https://github.com/wjlroe) 25 | * [Yukihiro Hara](https://github.com/yukihr) 26 | -------------------------------------------------------------------------------- /github-browse-file.el: -------------------------------------------------------------------------------- 1 | ;;; github-browse-file.el --- View the file you're editing on GitHub 2 | 3 | ;; Copyright (C) 2013 Ozan Sener & Contributors 4 | 5 | ;; Author: Ozan Sener 6 | ;; Homepage: https://github.com/osener/github-browse-file 7 | ;; Version: 0.5.0 8 | ;; Keywords: convenience vc git github 9 | ;; Package-Requires: ((cl-lib "0.5")) 10 | 11 | ;;; Installation: 12 | 13 | ;; Available as a package in Marmalade at http://marmalade-repo.org/ 14 | ;; M-x package-install github-browse-file 15 | 16 | ;;; Commentary: 17 | 18 | ;; Call `github-browse-file' (for the git blob) or `github-browse-file-blame' 19 | ;; (for the git blame) to view current file on GitHub. With a prefix argument 20 | ;; (C-u), you can force them to use the "master" branch. 21 | 22 | ;;; License: 23 | 24 | ;; This file is NOT part of GNU Emacs. 25 | 26 | ;; This program is free software; you can redistribute it and/or modify 27 | ;; it under the terms of the GNU General Public License as published by 28 | ;; the Free Software Foundation, either version 3 of the License, or 29 | ;; (at your option) any later version. 30 | 31 | ;; This program is distributed in the hope that it will be useful, 32 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 33 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 34 | ;; GNU General Public License for more details. 35 | 36 | ;; You should have received a copy of the GNU General Public License 37 | ;; along with this program. If not, see . 38 | 39 | ;;; Code: 40 | 41 | (require 'cl-lib) 42 | (require 'vc-git) 43 | 44 | (defgroup github-browse-file nil 45 | "View the current file on GitHub" 46 | :group 'tools) 47 | 48 | (defcustom github-browse-file-visit-url t 49 | "If non-nil, run `browse-url' after saving url to kill ring" 50 | :group 'github-browse-file 51 | :type 'boolean) 52 | 53 | (defcustom github-browse-file-show-line-at-point nil 54 | "If non-nil, link to the current line or active region" 55 | :group 'github-browse-file 56 | :type 'boolean) 57 | 58 | (defvar github-browse-file--view-blame nil 59 | "If non-nil, view \"blame\" instead of \"blob\". 60 | This should only ever be `let'-bound, not set outright.") 61 | 62 | (defvar github-browse-file--force-master nil 63 | "Whether to use \"master\" regardless of current branch 64 | This should only ever be `let'-bound, not set outright.") 65 | 66 | (defvar github-browse-file--magit-commit-link-modes 67 | '(magit-commit-mode magit-revision-mode magit-log-mode) 68 | "Non-file magit modes that should link to commits.") 69 | 70 | (defun github-browse-file--relative-url () 71 | "Return \"username/repo\" for current repository. 72 | 73 | Error out if this isn't a GitHub repo." 74 | (let ((url (vc-git--run-command-string nil "config" "remote.origin.url"))) 75 | (unless url (error "Not in a GitHub repo")) 76 | (when (and url (string-match "github.com:?/?\\(.*\\)" url)) 77 | (replace-regexp-in-string "\\.git$" "" (match-string 1 url))))) 78 | 79 | (defun github-browse-file--repo-relative-path () 80 | "Return the path to the current file relative to the repository root." 81 | (let* ((root (ignore-errors (vc-git-root buffer-file-name)))) 82 | (and root (file-relative-name buffer-file-name root)))) 83 | 84 | (defun github-browse-file--ahead-p () 85 | "Return non-nil if current git HEAD is ahead of origin/master" 86 | (let ((rev (vc-git--run-command-string 87 | nil "rev-list" "--left-right" "origin/master...HEAD"))) 88 | (and (> (length rev) 0) 89 | (string-equal (substring rev 0 1) ">")))) 90 | 91 | (defun github-browse-file--remote-branch () 92 | "Return the name of remote branch current branch is tracking. 93 | If there is none return 'master'." 94 | (let* ((ref (replace-regexp-in-string 95 | "\n" "" 96 | (vc-git--run-command-string nil "symbolic-ref" "-q" "HEAD"))) 97 | (origin-branch (replace-regexp-in-string 98 | "\n" "" 99 | (vc-git--run-command-string 100 | nil "for-each-ref" "--format=%(upstream:short)" ref))) 101 | (branch-name (mapconcat 'identity 102 | (cdr (split-string origin-branch "/")) 103 | "/"))) 104 | (if (eq branch-name "") "master" branch-name))) 105 | 106 | (defun github-browse-file--current-rev () 107 | "Return the SHA1 of HEAD if it is not ahead of origin/master. 108 | If github-browse-file--force-master is non-nil, return \"master\". 109 | Otherwise, return the name of the current branch." 110 | (cond 111 | (github-browse-file--force-master "master") 112 | ((member major-mode github-browse-file--magit-commit-link-modes) 113 | (magit-commit-at-point)) 114 | ((github-browse-file--ahead-p) (github-browse-file--remote-branch)) 115 | (t (let ((rev (vc-git--run-command-string nil "rev-parse" "HEAD"))) 116 | (and rev (replace-regexp-in-string "\n" "" rev)))))) 117 | 118 | (defun github-browse-file--browse-url (&optional anchor) 119 | "Load http://github.com/user/repo/file#ANCHOR in a web browser and add it to 120 | the kill ring." 121 | (let ((url (concat "https://github.com/" 122 | (github-browse-file--relative-url) "/" 123 | (cond ((eq major-mode 'magit-status-mode) "tree") 124 | ((member major-mode github-browse-file--magit-commit-link-modes) "commit") 125 | (github-browse-file--view-blame "blame") 126 | (t "blob")) "/" 127 | (github-browse-file--current-rev) "/" 128 | (github-browse-file--repo-relative-path) 129 | (when anchor (concat "#" anchor))))) 130 | (github-browse--save-and-view url))) 131 | 132 | (defun github-browse-file--anchor-lines () 133 | "Calculate anchor from lines in active region or current line 134 | 135 | If `github-browse-file-show-line-at-point' is non-nil, then 136 | default to current line." 137 | (cond 138 | ((and transient-mark-mode mark-active) 139 | (let ((start (line-number-at-pos (region-beginning))) 140 | (end (line-number-at-pos (region-end)))) 141 | (when (eq (char-before (region-end)) ?\n) (cl-decf end)) 142 | (if (>= start end) 143 | (format "L%d" start) 144 | (format "L%d-L%d" start end)))) 145 | (github-browse-file-show-line-at-point 146 | (format "L%d" (line-number-at-pos (point)))))) 147 | 148 | (defun github-browse-file--guess-commit () 149 | "Guess the current git commit. 150 | If you are in any magit mode, use `magit-commit-at-point'. 151 | Otherwise, if the region is active, use that. 152 | Otherwse, use `github-browse-file--current-rev'." 153 | (cond 154 | ((and (derived-mode-p 'magit-mode) (magit-commit-at-point)) 155 | (magit-commit-at-point)) 156 | ((region-active-p) 157 | (buffer-substring (region-beginning) (region-end))) 158 | (t (github-browse-file--current-rev)))) 159 | 160 | (defun github-browse--save-and-view (url) 161 | "Save url to kill ring and browse or show the url" 162 | (kill-new url) 163 | (if github-browse-file-visit-url 164 | (browse-url url) 165 | (message "GitHub: %s" url))) 166 | 167 | ;;;###autoload 168 | (defun github-browse-file (&optional force-master) 169 | "Show the GitHub webpage for the current file. The URL for the webpage is 170 | added to the kill ring. With a prefix argument, \"master\" is used 171 | regardless of the current branch. 172 | 173 | In Transient Mark mode, if the mark is active, highlight the contents of the 174 | region." 175 | (interactive "P") 176 | (let ((path (github-browse-file--repo-relative-path)) 177 | (github-browse-file--force-master force-master)) 178 | (github-browse-file--browse-url (github-browse-file--anchor-lines)))) 179 | 180 | ;;;###autoload 181 | (defun github-browse-file-blame (&optional force-master) 182 | "Show the GitHub blame page for the current file. The URL for the webpage is 183 | added to the kill ring. With a prefix argument, \"master\" is used 184 | regardless of the current branch. 185 | 186 | In Transient Mark mode, if the mark is active, highlight the contents of the 187 | region." 188 | (interactive "P") 189 | (let ((github-browse-file--view-blame t)) 190 | (github-browse-file force-master))) 191 | 192 | ;;;###autoload 193 | (defun github-browse-commit () 194 | "Show the GitHub page for the current commit." 195 | (interactive) 196 | (let* ((commit (github-browse-file--guess-commit)) 197 | (url (concat "https://github.com/" 198 | (github-browse-file--relative-url) 199 | "/commit/" 200 | commit))) 201 | (github-browse--save-and-view url))) 202 | 203 | (provide 'github-browse-file) 204 | ;;; github-browse-file.el ends here 205 | --------------------------------------------------------------------------------