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