org-git-link.el 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. ;;; org-git-link.el --- Provide org links to specific file version
  2. ;; Copyright (C) 2009-2014 Reimar Finken
  3. ;; Author: Reimar Finken <reimar.finken@gmx.de>
  4. ;; Keywords: files, calendar, hypermedia
  5. ;; This file is not part of GNU Emacs.
  6. ;; This program is free software; you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; This program is distaributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;; `org-git-link.el' defines two new link types. The `git' link
  18. ;; type is meant to be used in the typical scenario and mimics the
  19. ;; `file' link syntax as closely as possible. The `gitbare' link
  20. ;; type exists mostly for debugging reasons, but also allows e.g.
  21. ;; linking to files in a bare git repository for the experts.
  22. ;; * User friendy form
  23. ;; [[git:/path/to/file::searchstring]]
  24. ;; This form is the familiar from normal org file links
  25. ;; including search options. However, its use is
  26. ;; restricted to files in a working directory and does not
  27. ;; handle bare repositories on purpose (see the bare form for
  28. ;; that).
  29. ;; The search string references a commit (a tree-ish in Git
  30. ;; terminology). The two most useful types of search strings are
  31. ;; - A symbolic ref name, usually a branch or tag name (e.g.
  32. ;; master or nobelprize).
  33. ;; - A ref followed by the suffix @ with a date specification
  34. ;; enclosed in a brace pair (e.g. {yesterday}, {1 month 2
  35. ;; weeks 3 days 1 hour 1 second ago} or {1979-02-26 18:30:00})
  36. ;; to specify the value of the ref at a prior point in time
  37. ;;
  38. ;; * Bare git form
  39. ;; [[gitbare:$GIT_DIR::$OBJECT]]
  40. ;;
  41. ;; This is the more bare metal version, which gives the user most
  42. ;; control. It directly translates to the git command
  43. ;; git --no-pager --git-dir=$GIT_DIR show $OBJECT
  44. ;; Using this version one can also view files from a bare git
  45. ;; repository. For detailed information on how to specify an
  46. ;; object, see the man page of `git-rev-parse' (section
  47. ;; SPECIFYING REVISIONS). A specific blob (file) can be
  48. ;; specified by a suffix clolon (:) followed by a path.
  49. ;;; Code:
  50. (require 'org)
  51. (defcustom org-git-program "git"
  52. "Name of the git executable used to follow git links."
  53. :type '(string)
  54. :group 'org)
  55. ;; org link functions
  56. ;; bare git link
  57. (org-add-link-type "gitbare" 'org-gitbare-open)
  58. (defun org-gitbare-open (str)
  59. (let* ((strlist (org-git-split-string str))
  60. (gitdir (first strlist))
  61. (object (second strlist)))
  62. (org-git-open-file-internal gitdir object)))
  63. (defun org-git-open-file-internal (gitdir object)
  64. (let* ((sha (org-git-blob-sha gitdir object))
  65. (tmpdir (concat temporary-file-directory "org-git-" sha))
  66. (filename (org-git-link-filename object))
  67. (tmpfile (expand-file-name filename tmpdir)))
  68. (unless (file-readable-p tmpfile)
  69. (make-directory tmpdir)
  70. (with-temp-file tmpfile
  71. (org-git-show gitdir object (current-buffer))))
  72. (org-open-file tmpfile)
  73. (set-buffer (get-file-buffer tmpfile))
  74. (setq buffer-read-only t)))
  75. ;; user friendly link
  76. (org-add-link-type "git" 'org-git-open)
  77. (defun org-git-open (str)
  78. (let* ((strlist (org-git-split-string str))
  79. (filepath (first strlist))
  80. (commit (second strlist))
  81. (dirlist (org-git-find-gitdir (file-truename filepath)))
  82. (gitdir (first dirlist))
  83. (relpath (second dirlist)))
  84. (org-git-open-file-internal gitdir (concat commit ":" relpath))))
  85. ;; Utility functions (file names etc)
  86. (defun org-git-split-dirpath (dirpath)
  87. "Given a directory name, return '(dirname basname)"
  88. (let ((dirname (file-name-directory (directory-file-name dirpath)))
  89. (basename (file-name-nondirectory (directory-file-name dirpath))))
  90. (list dirname basename)))
  91. ;; finding the git directory
  92. (defun org-git-find-gitdir (path)
  93. "Given a file (not necessarily existing) file path, return the
  94. a pair (gitdir relpath), where gitdir is the path to the first
  95. .git subdirectory found updstream and relpath is the rest of
  96. the path. Example: (org-git-find-gitdir
  97. \"~/gitrepos/foo/bar.txt\") returns
  98. '(\"/home/user/gitrepos/.git\" \"foo/bar.txt\"). When not in a git repository, return nil."
  99. (let ((dir (file-name-directory path))
  100. (relpath (file-name-nondirectory path)))
  101. (catch 'toplevel
  102. (while (not (file-exists-p (expand-file-name ".git" dir)))
  103. (let ((dirlist (org-git-split-dirpath dir)))
  104. (when (string= (second dirlist) "") ; at top level
  105. (throw 'toplevel nil))
  106. (setq dir (first dirlist)
  107. relpath (concat (file-name-as-directory (second dirlist)) relpath))))
  108. (list (expand-file-name ".git" dir) relpath))))
  109. (eval-and-compile
  110. (if (featurep 'xemacs)
  111. (defalias 'org-git-gitrepos-p 'org-git-find-gitdir)
  112. (defalias 'org-git-gitrepos-p 'org-git-find-gitdir
  113. "Return non-nil if path is in git repository")))
  114. ;; splitting the link string
  115. ;; Both link open functions are called with a string of
  116. ;; consisting of two parts separated by a double colon (::).
  117. (defun org-git-split-string (str)
  118. "Given a string of the form \"str1::str2\", return a list of
  119. two substrings \'(\"str1\" \"str2\"). If the double colon is mising, take str2 to be the empty string."
  120. (let ((strlist (split-string str "::")))
  121. (cond ((= 1 (length strlist))
  122. (list (car strlist) ""))
  123. ((= 2 (length strlist))
  124. strlist)
  125. (t (error "org-git-split-string: only one :: allowed: %s" str)))))
  126. ;; finding the file name part of a commit
  127. (defun org-git-link-filename (str)
  128. "Given an object description (see the man page of
  129. git-rev-parse), return the nondirectory part of the referenced
  130. filename, if it can be extracted. Otherwise, return a valid
  131. filename."
  132. (let* ((match (and (string-match "[^:]+$" str)
  133. (match-string 0 str)))
  134. (filename (and match (file-name-nondirectory match)))) ;extract the final part without slash
  135. filename))
  136. ;; creating a link
  137. (defun org-git-create-searchstring (branch timestring)
  138. (concat branch "@{" timestring "}"))
  139. (defun org-git-create-git-link (file)
  140. "Create git link part to file at specific time"
  141. (interactive "FFile: ")
  142. (let* ((gitdir (first (org-git-find-gitdir (file-truename file))))
  143. (branchname (org-git-get-current-branch gitdir))
  144. (timestring (format-time-string "%Y-%m-%d" (current-time))))
  145. (concat "git:" file "::" (org-git-create-searchstring branchname timestring))))
  146. (defun org-git-store-link ()
  147. "Store git link to current file."
  148. (when (buffer-file-name)
  149. (let ((file (abbreviate-file-name (buffer-file-name))))
  150. (when (org-git-gitrepos-p file)
  151. (org-store-link-props
  152. :type "git"
  153. :link (org-git-create-git-link file))))))
  154. (add-hook 'org-store-link-functions 'org-git-store-link)
  155. (defun org-git-insert-link-interactively (file searchstring &optional description)
  156. (interactive "FFile: \nsSearch string: \nsDescription: ")
  157. (insert (org-make-link-string (concat "git:" file "::" searchstring) description)))
  158. ;; Calling git
  159. (defun org-git-show (gitdir object buffer)
  160. "Show the output of git --git-dir=gitdir show object in buffer."
  161. (unless
  162. (zerop (call-process org-git-program nil buffer nil
  163. "--no-pager" (concat "--git-dir=" gitdir) "show" object))
  164. (error "git error: %s " (with-current-buffer buffer (buffer-string)))))
  165. (defun org-git-blob-sha (gitdir object)
  166. "Return sha of the referenced object"
  167. (with-temp-buffer
  168. (if (zerop (call-process org-git-program nil t nil
  169. "--no-pager" (concat "--git-dir=" gitdir) "rev-parse" object))
  170. (buffer-substring (point-min) (1- (point-max))) ; to strip off final newline
  171. (error "git error: %s " (buffer-string)))))
  172. (defun org-git-get-current-branch (gitdir)
  173. "Return the name of the current branch."
  174. (with-temp-buffer
  175. (if (not (zerop (call-process org-git-program nil t nil
  176. "--no-pager" (concat "--git-dir=" gitdir) "symbolic-ref" "-q" "HEAD")))
  177. (error "git error: %s " (buffer-string))
  178. (goto-char (point-min))
  179. (if (looking-at "^refs/heads/") ; 11 characters
  180. (buffer-substring 12 (1- (point-max))))))) ; to strip off final newline
  181. (provide 'org-git-link)
  182. ;;; org-git-link.el ends here