├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── etc ├── _git-link └── git-link.sh ├── git-link ├── gitlink ├── __init__.py ├── __main__.py ├── git.py ├── main.py ├── pyperclip.py ├── repobrowsers.py └── utils.py ├── man ├── git-link-man1.rst └── git-link.1 ├── setup.cfg ├── setup.py ├── tests ├── test_cgit.py ├── test_git.py ├── test_github.py ├── test_gitweb.py └── util.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # --- Python --- 2 | *.py[co] 3 | *.egg 4 | *.egg-info/ 5 | develop-eggs/ 6 | dist/ 7 | build/ 8 | dropin.cache 9 | pip-log.txt 10 | .installed.cfg 11 | *# 12 | .#* 13 | .coverage 14 | .ropeproject 15 | .tox 16 | tags 17 | tests/test-output 18 | tests/test-repos 19 | __pycache__/ 20 | doc/_build/ 21 | git-link.zip 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | install: python setup.py install 8 | script: py.test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Georgi Valkov. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | 3. Neither the name of author nor the names of its contributors may 16 | be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL GEORGI VALKOV BE LIABLE FOR ANY 23 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include man/*.rst 4 | include man/*.1 5 | include etc/git-link.sh 6 | include etc/_git-link 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = $(shell grep -Eo '[0-9][0-9\.]+' gitlink/__init__.py) 2 | EXCLUDE = \*__pycache__ \*.pyo \*.pyc \*__main__.py 3 | 4 | git-link: git-link.zip 5 | echo "#!/usr/bin/env python" | cat - $< > $@ 6 | chmod +x $@ 7 | 8 | git-link.zip: 9 | zip -0 -r $@ gitlink -x $(EXCLUDE) 10 | (cd gitlink && zip -0 ../$@ __main__.py) 11 | 12 | man/git-link.1: man/git-link-man1.rst update-man-version 13 | rst2man $< > $@ 14 | 15 | test: 16 | py.test tests 17 | 18 | clean: 19 | -rm git-link.zip git-link 20 | 21 | update-man-version: man/git-link-man1.rst 22 | sed -i -e "s,\(:Version: *\).*,\1$(VERSION)," $< 23 | 24 | .PHONY: update-man-version test clean 25 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | *git-link* 2 | ---------- 3 | 4 | *Git-link* is a git sub-command for getting a repo-browser link to a 5 | git object. The motivation behind *git-link* is that it is often 6 | faster to navigate to a git object or path on the command line than it 7 | is to click your way to it through a web interface. An example using 8 | *git-link*'s github sources: 9 | 10 | .. code-block:: bash 11 | 12 | $ git config --add link.url https://github.com/gvalkov/git-link 13 | $ git config --add link.browser github 14 | 15 | $ git link HEAD~10 16 | https://github.com/gvalkov/git-link/commit/d0bca29bd7 17 | 18 | $ git link v0.2.0 19 | https://github.com/gvalkov/git-link/tree/v0.2.0 20 | 21 | $ git link v0.2.0 -- setup.py 22 | https://github.com/gvalkov/git-link/tree/af4ad8c89b/setup.py 23 | 24 | *Git-link* can be used with : cgit_, gitweb_, github_, github-private_ 25 | 26 | Install 27 | ======= 28 | 29 | Install from PyPi: 30 | 31 | .. code-block:: bash 32 | 33 | $ pip install gitlink 34 | 35 | Or simply put git-link in your ``$PATH`` and make it executable:: 36 | 37 | https://raw.githubusercontent.com/gvalkov/git-link/master/git-link 38 | 39 | Usage 40 | ===== 41 | 42 | :: 43 | 44 | Usage: git link [options] 45 | 46 | Options: 47 | -h, --help show this help message and exit 48 | -v, --version show version and exit 49 | -c, --clipboard copy link to clipboard (overwrites link.clipboard) 50 | -u, --url repo browser url (overwrites link.url) 51 | -b, --browser repo browser type (overwrites link.browser) 52 | -s, --short truncate hashes to length (overwrites link.short) 53 | -r, --raw show raw blob if possible 54 | 55 | Repo browsers: 56 | github-private cgit gitweb github 57 | 58 | Configuration: 59 | git config --add link.url 60 | git config --add link.browser 61 | git config --add link.clipboard false|true 62 | 63 | Examples: 64 | git link HEAD~10 url of 10th commit before HEAD 65 | git link v0.1.0^{tree} url of tree object at tag v0.1.0 66 | git link master:file url of file in branch master 67 | git link path/file url of path/file in current branch 68 | git link devel -- path url of path in branch devel 69 | git link v0.1.0 url of tag v0.1.0 70 | 71 | Setup 72 | ===== 73 | 74 | *Git-link* needs to know the name and url of the repository browser 75 | for the repository it is being run in. This can be set through 76 | ``git-config`` or on the command line on each run: 77 | 78 | .. code-block:: bash 79 | 80 | $ git config --add link.url 81 | $ git config --add link.browser 82 | $ git config --add link.clipboard false|true # optional 83 | $ git config --add link.short 7 # optional 84 | $ git link --browser --name --clipboard ... 85 | 86 | Development 87 | =========== 88 | 89 | .. image:: https://travis-ci.org/gvalkov/git-link.svg?branch=master 90 | :target: https://travis-ci.org/gvalkov/git-link 91 | 92 | See repobrowsers.py_ and test_cgit.py_ if you are interested in adding 93 | a new repository browser. Release checklist: 94 | 95 | 1) Run ``py.test``. 96 | 97 | 2) Bump version in ``gitlink/__init__.py``. 98 | 99 | 3) Update man page - ``make man/git-link.1``. 100 | 101 | 4) Create standalone script - ``make git-link``. 102 | 103 | Please make do without bringing in any external dependencies. As nice 104 | as GitPython_ and libgit2_ are, anything that this tool needs from git 105 | can be queried using its command line interface. 106 | 107 | 108 | License 109 | ======= 110 | 111 | *Git-link* is released under the terms of the `Revised BSD License`_. 112 | 113 | Links 114 | ===== 115 | 116 | Development: 117 | https://github.com/gvalkov/git-link 118 | 119 | Package: 120 | http://pypi.python.org/pypi/gitlink 121 | 122 | .. _cgit: http://hjemli.net/git/cgit/ 123 | .. _gitweb: http://git.kernel.org/?p=git/git.git;a=tree;f=gitweb;hb=refs/heads/master 124 | .. _github: http://github.com/ 125 | .. _github-private: https://github.com/plans 126 | .. _`Revised BSD License`: https://raw.github.com/gvalkov/git-link/master/LICENSE 127 | .. _GitPython: https://pypi.python.org/pypi/GitPython/ 128 | .. _PyGit2: https://pypi.python.org/pypi/pygit2 129 | .. _repobrowsers.py: https://github.com/gvalkov/git-link/blob/master/gitlink/repobrowsers.py 130 | .. _test_cgit.py: https://github.com/gvalkov/git-link/blob/master/tests/test_cgit.py 131 | .. _libgit2: http://www.pygit2.org/ 132 | -------------------------------------------------------------------------------- /etc/_git-link: -------------------------------------------------------------------------------- 1 | #compdef git-link 2 | #description get a repo browser link to a git object 3 | 4 | # zsh completion for the git link command 5 | # http://github.com/gvalkov/git-link 6 | 7 | # For best results, please upgrade your _zsh completion to the latest version: 8 | # http://zsh.git.sourceforge.net/git/gitweb.cgi?p=zsh/zsh;a=blob_plain;f=Completion/Unix/Command/_git;hb=HEAD 9 | 10 | _git-link () { 11 | local curcontext=$curcontext state line ret=1 12 | typeset -A opt_args 13 | 14 | _arguments -w -C -S -s \ 15 | '(-h --help)'{-h,--help}'[show this help message and exit]' \ 16 | '(-v --version)'{-v,--version}'[show version and exit]' \ 17 | '(-c --clipboard)'{-c,--clipboard}'[copy link to clipboard (overwrites link.clipboard)]' \ 18 | '(-u --url)'{-u,--url}'[repo browser base url (overwrites link.url)]:arg' \ 19 | '(-b --browser)'{-b,--browser}'[repo browser type (overwrites link.browser)]:name:->browser' \ 20 | '(-r --raw)'{-r,--raw}'[show raw blob if possible ]' \ 21 | '*:: :->object' && ret=0 22 | 23 | case $state in 24 | (object) 25 | _alternative \ 26 | 'path::_files' \ 27 | 'commits::__git_commits' \ 28 | 'tags::__git_tags' \ 29 | 'trees::__git_trees' \ 30 | 'blobs::__git_blobs' && ret=0 31 | ;; 32 | 33 | (browser) 34 | compadd cgit gitweb github github.private repo.or.cz 35 | ;; 36 | esac 37 | 38 | } 39 | 40 | zstyle ':completion:*:*:git:*' user-commands link:'get a repo browser link to a git object' 41 | 42 | # vim: ft=zsh: 43 | -------------------------------------------------------------------------------- /etc/git-link.sh: -------------------------------------------------------------------------------- 1 | # bash completion for the the git link command 2 | # http://github.com/gvalkov/git-link 3 | 4 | _git_flow () 5 | { 6 | local cur prev opts 7 | 8 | cur="${COMP_WORDS[COMP_CWORD]}" 9 | prev="${COMP_WORDS[COMP_CWORD-1]}" 10 | 11 | opts="-h --help -v --version -c --clipboard -u --url -b --browser -r --raw" 12 | # tbd: leverage bash's git comlpetion 13 | } 14 | 15 | # vim: ft=sh: 16 | -------------------------------------------------------------------------------- /git-link: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gvalkov/git-link/26aa09b1134afd1844b4d0420fc1ba076e276b68/git-link -------------------------------------------------------------------------------- /gitlink/__init__.py: -------------------------------------------------------------------------------- 1 | version = '0.5.0' 2 | -------------------------------------------------------------------------------- /gitlink/__main__.py: -------------------------------------------------------------------------------- 1 | from gitlink.main import main 2 | if __name__ == '__main__': 3 | main() 4 | -------------------------------------------------------------------------------- /gitlink/git.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | from __future__ import absolute_import 5 | from os.path import relpath 6 | 7 | from . repobrowsers import LinkType as LT 8 | from . utils import run 9 | 10 | 11 | #----------------------------------------------------------------------------- 12 | def get_config(section, strip_section=True): 13 | '''Get a git config section as a dictionary. 14 | 15 | [link] 16 | clipboard = true 17 | browser = cgit 18 | url = false 19 | 20 | => {'clipboard' : True, 'browser' : 'cgit', 'url' : False} 21 | => {'link.clipboard': True ...} if not strip_section 22 | ''' 23 | 24 | ret, out = run('git config --get-regexp "%s\\..*"' % section) 25 | 26 | if ret: 27 | return {} 28 | 29 | def parse_helper(item): 30 | key, value = item 31 | if strip_section: 32 | key = key.replace(section + '.', '', 1) 33 | 34 | if value == 'true': value = True 35 | elif value == 'false': value = False 36 | return key, value 37 | 38 | out = out.splitlines() 39 | out = (i.split(' ', 1) for i in out) 40 | out = map(parse_helper, out) 41 | 42 | return dict(out) 43 | 44 | def revparse(ish): 45 | r, out = run('git rev-parse %s' % ish) 46 | return out.rstrip('\n') 47 | 48 | def cat_commit(commitish): 49 | '''commitish => { 50 | 'commit': sha of commit pointed by commit-ish, 51 | 'tree': ..., 52 | 'parent': ..., 53 | 'author': ..., 54 | 'comitter': ..., 55 | } 56 | ''' 57 | 58 | r, out = run('git cat-file commit %s' % commitish) 59 | out = out.splitlines()[:4] 60 | 61 | res = dict([i.split(' ', 1) for i in out if i]) 62 | res['sha'] = revparse(commitish) # :todo: why am I doing this!? 63 | 64 | return res 65 | 66 | def cat_tag(tag): 67 | '''tag name or sha => { 68 | 'object': sha of object pointed by tag, 69 | 'type': type of object pointed by tag, 70 | 'tag': name of tag (if tag was a sha), 71 | 'sha': revparsed sha of tag, 72 | 'tagger': ... 73 | } 74 | ''' 75 | 76 | ret, out = run('git cat-file tag %s' % tag) 77 | out = out.splitlines()[:4] 78 | res = dict([i.split(' ', 1) for i in out if i]) 79 | res['sha'] = revparse(tag) 80 | 81 | return res 82 | 83 | 84 | def commit(arg): 85 | '''HEAD~10 -> actual commit sha and tree sha.''' 86 | res = cat_commit(arg) 87 | return { 88 | 'type': LT.commit, 89 | 'sha': res['sha'], 90 | 'tree_sha': res['tree'] 91 | } 92 | 93 | def tag(arg): 94 | '''Tag name or sha -> cat_tag().''' 95 | res = cat_tag(arg) 96 | res['type'] = LT.tag 97 | return res 98 | 99 | def tree(arg): 100 | '''HEAD~~^{tree} -> actual tree sha.''' 101 | return { 102 | 'type': LT.tree, 103 | 'sha': revparse(arg) 104 | } 105 | 106 | def blob(arg): 107 | '''HEAD~2:main.py -> tree + blob + path relative to git topdir.''' 108 | 109 | res = { 110 | 'type': LT.blob, 111 | 'tree_sha': None, 112 | 'commit_sha': None, 113 | 'path': None, 114 | } 115 | 116 | if ':' in arg: 117 | # the commitish may also be a tag 118 | commitish, path = arg.split(':', 1) 119 | 120 | ret, t = run('git cat-file -t %s' % commitish) 121 | if t == 'tag': 122 | commitish = cat_tag(commitish)['object'] 123 | 124 | commitd = cat_commit(commitish) 125 | sha, t, tree_sha = _path(path.split('/'), commitd['tree']) 126 | 127 | ret, topdir = run('git rev-parse --show-toplevel') 128 | 129 | res['path'] = relpath(path, topdir) 130 | res['tree_sha'] = tree_sha 131 | res['sha'] = sha 132 | res['commit_sha'] = commitd['sha'] 133 | else: 134 | res['sha'] = arg 135 | 136 | return res 137 | 138 | 139 | def lstree(sha): 140 | ret, out = run('git ls-tree %s' % sha) 141 | 142 | for line in out.splitlines(): 143 | mode, type, sha = line.split(' ', 3) 144 | sha, path = sha.split('\t', 1) 145 | yield mode, type, sha, path 146 | 147 | 148 | def _path(arg, tree_sha='HEAD^{tree}'): 149 | ''':param arg: a path.split('/') relative to root of the wc 150 | :param tree_sha: tree-ish to search 151 | 152 | if path leads to a blob object return: 153 | blob sha, 'blob', tree sha 154 | if path leads to a tree object return: 155 | tree sha, 'tree', None 156 | if path does not exist, return None 157 | ''' 158 | 159 | if not arg: 160 | return tree_sha, 'tree', None 161 | 162 | for m, t, sha, p in lstree(tree_sha): 163 | if p == arg[0] and t == 'tree': 164 | return _path(arg[1:], sha) 165 | 166 | if p == arg[0] and t == 'blob': 167 | return sha, 'blob', tree_sha 168 | 169 | 170 | def path(arg, commitish='HEAD'): 171 | res = {} 172 | top_tree_sha = '%s^{tree}' % commitish 173 | 174 | r, topdir = run('git rev-parse --show-toplevel') 175 | path = relpath(arg, topdir) 176 | 177 | stt = _path(path.split('/'), top_tree_sha) 178 | 179 | if stt: 180 | sha, type, tree_sha = stt 181 | else: 182 | return {} 183 | 184 | if type == 'blob': 185 | res['type'] = LT.blob 186 | elif type == 'tree': 187 | res['type'] = LT.path 188 | 189 | ret, t = run('git cat-file -t %s' % commitish) 190 | if t == 'tag': 191 | commitish = cat_tag(commitish)['object'] 192 | 193 | res['commit_sha'] = revparse(commitish) 194 | res['path'] = path 195 | res['sha'] = sha # tree or blob sha 196 | res['tree_sha'] = revparse(tree_sha) # tree sha if blob, None otherwise 197 | res['top_tree_sha'] = revparse(top_tree_sha) 198 | return res # :bug: 199 | 200 | 201 | def branch(arg): 202 | '''Check if arg is a branch pointer.''' 203 | 204 | remotes = run('git remote')[1].splitlines() 205 | 206 | ret, sha = run('git show-ref "%s"' % arg) 207 | sha = sha.splitlines()[-1] 208 | sha, ref = sha.split(' ') 209 | 210 | shortref = None 211 | for i in remotes: 212 | if i in ref: 213 | shortref = ref.replace('refs/remotes/%s/' % i, '') 214 | break 215 | 216 | res = { 217 | 'type': LT.branch, 218 | 'sha': sha, 219 | 'ref': ref, 220 | 'shortref': shortref, 221 | } 222 | return res 223 | -------------------------------------------------------------------------------- /gitlink/main.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | ''' 4 | Git sub-command for getting a repo browser link to a git object. 5 | ''' 6 | 7 | from __future__ import print_function, absolute_import 8 | 9 | from optparse import OptionParser, make_option as mkopt 10 | from sys import argv, exit, stderr, stdout, version_info 11 | 12 | from . repobrowsers import repobrowsers, LinkType as LT 13 | from . import git, version, utils 14 | 15 | 16 | #----------------------------------------------------------------------------- 17 | # Option parsing 18 | usage = '''\ 19 | Usage: git link [options] 20 | 21 | Options: 22 | -h, --help show this help message and exit 23 | -v, --version show version and exit 24 | -c, --clipboard copy link to clipboard (overwrites link.clipboard) 25 | -u, --url repo browser url (overwrites link.url) 26 | -b, --browser repo browser type (overwrites link.browser) 27 | -s, --short truncate hashes to length (overwrites link.short) 28 | -r, --raw show raw blob if possible 29 | 30 | Repo browsers: 31 | %s 32 | 33 | Configuration: 34 | git config --add link.url 35 | git config --add link.browser 36 | git config --add link.clipboard false|true 37 | 38 | Examples: 39 | git link HEAD~10 url of 10th commit before HEAD 40 | git link v0.1.0^{tree} url of tree object at tag v0.1.0 41 | git link master:file url of file in branch master 42 | git link path/file url of path/file in current branch 43 | git link devel -- path url of path in branch devel 44 | git link v0.1.0 url of tag v0.1.0''' % ' '.join(repobrowsers) 45 | 46 | def parseopt(args=argv[1:]): 47 | opts = ( 48 | mkopt('-h', '--help', action='store_true'), 49 | mkopt('-v', '--version', action='store_true'), 50 | mkopt('-c', '--clipboard', action='store_true'), 51 | mkopt('-r', '--raw', action='store_true'), 52 | mkopt('-u', '--url', action='store'), 53 | mkopt('-b', '--browser', action='store', choices=list(repobrowsers)), 54 | mkopt('-s', '--short', action='store'), 55 | mkopt('-t', '--traceback', action='store_true'), 56 | ) 57 | 58 | parser = OptionParser(usage=usage, option_list=opts, add_help_option=False) 59 | opts, args = parser.parse_args(args) 60 | return parser, opts, args 61 | 62 | def readopts(cmdargs=argv[1:]): 63 | '''Read configuration from command line or git config.''' 64 | 65 | parser, opts, args = parseopt(cmdargs) 66 | 67 | if opts.version: 68 | vstr = 'git-link version %s' % version 69 | print(vstr, file=stderr) 70 | exit(0) 71 | 72 | if opts.help or not args: 73 | print(usage) 74 | exit(0) 75 | 76 | cfg = git.get_config('link') 77 | 78 | # command line options overrule git config options 79 | opts.url = opts.url or cfg.get('url') 80 | opts.browser = opts.browser or cfg.get('browser') 81 | opts.clipboard = opts.clipboard or cfg.get('clipboard') 82 | opts.short = opts.short or cfg.get('short') 83 | 84 | errors = [] 85 | if not opts.url: 86 | msg = "repo browser url not - use 'git config link.url' or '-u, --url'" 87 | errors.append(msg) 88 | if not opts.browser: 89 | msg = "repo browser type not set - use 'git config link.browser' or '-b, --browser'" 90 | errors.append(msg) 91 | 92 | if opts.short: 93 | try: 94 | opts.short = int(opts.short) 95 | except ValueError: 96 | msg = "invalid integer value for option 'git config link.short' or '-s, --short': %s" 97 | errors.append(msg % opts.short) 98 | 99 | if errors: 100 | print('\n'.join(errors), file=stderr) 101 | exit(1) 102 | 103 | return opts, args 104 | 105 | 106 | #----------------------------------------------------------------------------- 107 | def expand_args(ish, path): 108 | '''Determine *ish type and prepare response dict.''' 109 | 110 | if ish and path: 111 | res = git.path(path, ish) 112 | if not res: 113 | raise Exception('invalid reference: %s -- %s' % (ish, path)) 114 | return res 115 | 116 | ret, t = git.run('git cat-file -t %s' % ish) 117 | res = {} 118 | 119 | if t == 'commit' and ish != 'HEAD': 120 | try: 121 | res = git.branch(ish) 122 | except: 123 | res = git.commit(ish) 124 | 125 | elif t == 'commit': res = git.commit(ish) 126 | elif t == 'tree': res = git.tree(ish) 127 | elif t == 'blob': res = git.blob(ish) 128 | elif t == 'tag': res = git.tag(ish) 129 | else: 130 | res = git.path(ish, 'HEAD') 131 | 132 | if not res: 133 | res = {'type': LT.unknown} 134 | 135 | return res 136 | 137 | 138 | #----------------------------------------------------------------------------- 139 | def get_link(r, rb, ish, raw=False): 140 | t = r['type'] 141 | 142 | if t == LT.commit: 143 | link = rb.commit(r['sha']) 144 | 145 | elif t == LT.tree: 146 | link = rb.tree(r['sha']) 147 | 148 | elif t == LT.tag: 149 | link = rb.tag(r['tag'], r['sha'], r['object']) 150 | 151 | elif t == LT.branch: 152 | link = rb.branch(r['ref'], r['shortref']) 153 | 154 | elif t == LT.blob: 155 | link = rb.blob(r['sha'], r['path'], r['tree_sha'], r['commit_sha'], raw=raw) 156 | 157 | elif t == LT.path: 158 | link = rb.path(r['path'], r['sha'], r['commit_sha']) 159 | 160 | elif t == LT.unknown: 161 | raise Exception('unhandled object type') 162 | 163 | return link 164 | 165 | def main(cmdargs=argv[1:], out=stdout): 166 | opts, args = readopts(cmdargs) 167 | 168 | if len(args) == 2: 169 | ish, path = args 170 | else: 171 | ish, path = args[0], None 172 | 173 | try: 174 | # instantiate repository browser 175 | rb = repobrowsers[opts.browser](opts.url) 176 | except KeyError as e: 177 | msg = 'repository browser "%s" not supported' 178 | print(msg % opts.browser, file=stderr) 179 | exit(1) 180 | 181 | try: 182 | # determine *ish type and expand 183 | res = expand_args(ish, path) 184 | if opts.short: 185 | utils.shorten_hashes(res, opts.short) 186 | link = get_link(res, rb, ish, opts.raw) 187 | except Exception as e: 188 | if opts.traceback: 189 | raise 190 | print(str(e), stderr) 191 | exit(1) 192 | 193 | if link and opts.clipboard: 194 | try: 195 | utils.to_clipboard(link) 196 | except Exception as e: 197 | if opts.traceback: 198 | raise 199 | print(str(e), file=stderr) 200 | 201 | if link: 202 | print(link, file=out) 203 | -------------------------------------------------------------------------------- /gitlink/pyperclip.py: -------------------------------------------------------------------------------- 1 | # Pyperclip v1.3 2 | # A cross-platform clipboard module for Python. (only handles plain text for now) 3 | # By Al Sweigart al@coffeeghost.net 4 | 5 | # Usage: 6 | # import pyperclip 7 | # pyperclip.copy('The text to be copied to the clipboard.') 8 | # spam = pyperclip.paste() 9 | 10 | # On Mac, this module makes use of the pbcopy and pbpaste commands, which should come with the os. 11 | # On Linux, this module makes use of the xclip command, which should come with the os. Otherwise run "sudo apt-get install xclip" 12 | 13 | 14 | # Copyright (c) 2010, Albert Sweigart 15 | # All rights reserved. 16 | # 17 | # BSD-style license: 18 | # 19 | # Redistribution and use in source and binary forms, with or without 20 | # modification, are permitted provided that the following conditions are met: 21 | # * Redistributions of source code must retain the above copyright 22 | # notice, this list of conditions and the following disclaimer. 23 | # * Redistributions in binary form must reproduce the above copyright 24 | # notice, this list of conditions and the following disclaimer in the 25 | # documentation and/or other materials provided with the distribution. 26 | # * Neither the name of the pyperclip nor the 27 | # names of its contributors may be used to endorse or promote products 28 | # derived from this software without specific prior written permission. 29 | # 30 | # THIS SOFTWARE IS PROVIDED BY Albert Sweigart "AS IS" AND ANY 31 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 32 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 33 | # DISCLAIMED. IN NO EVENT SHALL Albert Sweigart BE LIABLE FOR ANY 34 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 35 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 36 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 37 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 39 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | 41 | # Change Log: 42 | # 1.2 Use the platform module to help determine OS. 43 | # 1.3 Changed ctypes.windll.user32.OpenClipboard(None) to ctypes.windll.user32.OpenClipboard(0), after some people ran into some TypeError 44 | 45 | import platform, os 46 | 47 | def winGetClipboard(): 48 | ctypes.windll.user32.OpenClipboard(0) 49 | pcontents = ctypes.windll.user32.GetClipboardData(1) # 1 is CF_TEXT 50 | data = ctypes.c_char_p(pcontents).value 51 | #ctypes.windll.kernel32.GlobalUnlock(pcontents) 52 | ctypes.windll.user32.CloseClipboard() 53 | return data 54 | 55 | def winSetClipboard(text): 56 | GMEM_DDESHARE = 0x2000 57 | ctypes.windll.user32.OpenClipboard(0) 58 | ctypes.windll.user32.EmptyClipboard() 59 | try: 60 | # works on Python 2 (bytes() only takes one argument) 61 | hCd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len(bytes(text))+1) 62 | except TypeError: 63 | # works on Python 3 (bytes() requires an encoding) 64 | hCd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len(bytes(text, 'ascii'))+1) 65 | pchData = ctypes.windll.kernel32.GlobalLock(hCd) 66 | try: 67 | # works on Python 2 (bytes() only takes one argument) 68 | ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), bytes(text)) 69 | except TypeError: 70 | # works on Python 3 (bytes() requires an encoding) 71 | ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), bytes(text, 'ascii')) 72 | ctypes.windll.kernel32.GlobalUnlock(hCd) 73 | ctypes.windll.user32.SetClipboardData(1,hCd) 74 | ctypes.windll.user32.CloseClipboard() 75 | 76 | def macSetClipboard(text): 77 | outf = os.popen('pbcopy', 'w') 78 | outf.write(text) 79 | outf.close() 80 | 81 | def macGetClipboard(): 82 | outf = os.popen('pbpaste', 'r') 83 | content = outf.read() 84 | outf.close() 85 | return content 86 | 87 | def gtkGetClipboard(): 88 | return gtk.Clipboard().wait_for_text() 89 | 90 | def gtkSetClipboard(text): 91 | cb = gtk.Clipboard() 92 | cb.set_text(text) 93 | cb.store() 94 | 95 | def qtGetClipboard(): 96 | return str(cb.text()) 97 | 98 | def qtSetClipboard(text): 99 | cb.setText(text) 100 | 101 | def xclipSetClipboard(text): 102 | outf = os.popen('xclip -selection c', 'w') 103 | outf.write(text) 104 | outf.close() 105 | 106 | def xclipGetClipboard(): 107 | outf = os.popen('xclip -selection c -o', 'r') 108 | content = outf.read() 109 | outf.close() 110 | return content 111 | 112 | def xselSetClipboard(text): 113 | outf = os.popen('xsel -i', 'w') 114 | outf.write(text) 115 | outf.close() 116 | 117 | def xselGetClipboard(): 118 | outf = os.popen('xsel -o', 'r') 119 | content = outf.read() 120 | outf.close() 121 | return content 122 | 123 | 124 | if os.name == 'nt' or platform.system() == 'Windows': 125 | import ctypes 126 | getcb = winGetClipboard 127 | setcb = winSetClipboard 128 | elif os.name == 'mac' or platform.system() == 'Darwin': 129 | getcb = macGetClipboard 130 | setcb = macSetClipboard 131 | elif os.name == 'posix' or platform.system() == 'Linux': 132 | xclipExists = os.system('which xclip 1>/dev/null 2>&1 ') == 0 133 | if xclipExists: 134 | getcb = xclipGetClipboard 135 | setcb = xclipSetClipboard 136 | else: 137 | xselExists = os.system('which xsel 1>/dev/null 2>&1') == 0 138 | if xselExists: 139 | getcb = xselGetClipboard 140 | setcb = xselSetClipboard 141 | try: 142 | import gtk 143 | getcb = gtkGetClipboard 144 | setcb = gtkSetClipboard 145 | except: 146 | try: 147 | import PyQt4.QtCore 148 | import PyQt4.QtGui 149 | app = QApplication([]) 150 | cb = PyQt4.QtGui.QApplication.clipboard() 151 | getcb = qtGetClipboard 152 | setcb = qtSetClipboard 153 | except: 154 | raise Exception('Pyperclip requires the gtk or PyQt4 module installed, or the xclip command.') 155 | copy = setcb 156 | paste = getcb 157 | -------------------------------------------------------------------------------- /gitlink/repobrowsers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | from __future__ import absolute_import 5 | from os.path import join as pjoin 6 | 7 | 8 | #----------------------------------------------------------------------------- 9 | class RepoBrowser(object): 10 | '''I represent a repository browser and my methods return links to 11 | the various git objects that I am capable of showing.''' 12 | 13 | def tag(self, name, tag_sha=None, obj_sha=None): 14 | '''Get url for tag name.''' 15 | raise NotImplementedError 16 | 17 | def commit(self, sha): 18 | '''Get url for commit object.''' 19 | raise NotImplementedError 20 | 21 | def branch(self, ref, shortref): 22 | '''Get url for branch name''' 23 | raise NotImplementedError 24 | 25 | def diff(self, diffspec): 26 | '''Get url for diff.''' 27 | raise NotImplementedError 28 | 29 | def tree(self, sha): 30 | '''Get url for tree object.''' 31 | raise NotImplementedError 32 | 33 | def path(self, path, tree, commit): 34 | '''Get url for a path relative to the root of repository.''' 35 | raise NotImplementedError 36 | 37 | def blob(self, sha, path, tree, commit, raw): 38 | '''Get url for a blob object.''' 39 | raise NotImplementedError 40 | 41 | def join(self, *args): 42 | l = [self.url] 43 | l.extend(args) 44 | return pjoin(*l) 45 | 46 | 47 | #----------------------------------------------------------------------------- 48 | class GitwebBrowser(RepoBrowser): 49 | '''Gitweb - git's default web interface.''' 50 | 51 | options = { 52 | 'head-view': { 53 | 'help': 'default head view', 54 | 'choices': ('shortlog', 'log', 'tree'), 55 | }, 56 | 'commit-view': { 57 | 'help': 'default commit view', 58 | 'choices': ('commit', 'commitdiff', 'tree'), 59 | }, 60 | 'tag-view': { 61 | 'help': 'default tag view', 62 | 'choices': ('commit', 'shortlog', 'log'), 63 | }, 64 | } 65 | 66 | def __init__(self, url, 67 | head_view='shortlog', 68 | commit_view='commitdiff', 69 | tag_view='commit'): 70 | 71 | self.url = url.rstrip('/') 72 | 73 | self.tag_view = tag_view 74 | self.head_view = head_view 75 | self.commit_view = commit_view 76 | 77 | def commit(self, sha): 78 | l = (self.url, 'a=%s' % self.commit_view, 'h=%s' % sha) 79 | return ';'.join(l) 80 | 81 | def tree(self, sha, path=None): 82 | l = [self.url, 'a=tree', 'h=%s' % sha] 83 | if path: l.append('f=%s' % path) 84 | return ';'.join(l) 85 | 86 | def branch(self, ref, shortref): 87 | l = (self.url, 'a=%s' % self.head_view, 'h=%s' % shortref) 88 | return ';'.join(l) 89 | 90 | def tag(self, name, tag_sha=None, obj_sha=None): 91 | l = (self.url, 'a=%s' % self.tag_view, 'h=%s' % name) 92 | return ';'.join(l) 93 | 94 | def blob(self, sha, path, tree=None, commit=None, raw=False): 95 | if tree and tree == 'HEAD^{tree}': 96 | tree = None 97 | 98 | l = [self.url, 'a=blob', 'h=%s' % sha] 99 | if path: 100 | l.append('f=%s' % path) 101 | 102 | url = ';'.join(l) 103 | 104 | if raw: 105 | url = url.replace('a=blob', 'a=blob_plain', 1) 106 | 107 | return url 108 | 109 | def path(self, path, tree=None, commit=None): 110 | l = (self.url, 'a=tree', 'f=%s' % path, 'h=%s' % tree) 111 | return ';'.join(l) 112 | 113 | 114 | #----------------------------------------------------------------------------- 115 | class GithubBrowser(RepoBrowser): 116 | '''Github public repositories.''' 117 | 118 | def __init__(self, url): 119 | self.url = url 120 | 121 | def commit(self, sha): 122 | return self.join('commit', sha) 123 | 124 | def tree(self, sha): 125 | ''' TBD ''' 126 | 127 | def branch(self, ref, shortref): 128 | return self.join('tree', shortref) 129 | 130 | def tag(self, name, tag_sha=None, obj_sha=None): 131 | return self.join('tree', name) 132 | 133 | def blob(self, sha, path, tree=None, commit=None, raw=False): 134 | # github does not seem to allow linking directly to a blob 135 | url = self.join('tree', commit, path) # @bug 136 | 137 | if raw: 138 | url = url.replace('github.com', 'raw.github.com', 1) 139 | url = url.replace('/tree/', '/', 1) 140 | 141 | return url 142 | 143 | def path(self, path, tree, commit): 144 | return self.join('tree', commit, path) 145 | 146 | 147 | #----------------------------------------------------------------------------- 148 | # I don't know how a private repo looks like, but I assume it's 149 | # similar to a public one. 150 | class GithubPrivateBrowser(GithubBrowser): 151 | '''Github private repositories.''' 152 | 153 | 154 | #----------------------------------------------------------------------------- 155 | class CgitBrowser(RepoBrowser): 156 | '''Cgit - web interface for git repositories, written in c.''' 157 | 158 | def __init__(self, url): 159 | self.url = url 160 | 161 | def commit(self, sha): 162 | return self.join('commit', '?id=%s' % sha) 163 | 164 | def tree(self, sha): 165 | return self.join('tree', '?tree=%s' % sha) 166 | 167 | def branch(self, ref, shortref): 168 | if not shortref: return None 169 | return self.join('log', '?h=%s' % shortref) 170 | 171 | def tag(self, name, tag_sha=None, obj_sha=None): 172 | return self.join('tag', '?id=%s' % name) 173 | 174 | def blob(self, sha, path, tree=None, commit=None, raw=False): 175 | if tree and tree == 'HEAD^{tree}': 176 | tree = None 177 | 178 | url = self.path(path, tree) 179 | if raw: 180 | url = url.replace('tree', 'plain', 1) 181 | 182 | return url 183 | 184 | def path(self, path, tree=None, commit=None): 185 | url = [self.url, 'tree', path] 186 | if tree: 187 | url.append('?tree=%s' % tree) 188 | 189 | return pjoin(*url) 190 | 191 | 192 | #----------------------------------------------------------------------------- 193 | class LinkType(object): 194 | unknown = 0x1 195 | commit = 0x2 196 | tree = 0x3 197 | tag = 0x4 198 | branch = 0x5 199 | diff = 0x6 200 | blob = 0x7 201 | path = 0x8 202 | 203 | 204 | #----------------------------------------------------------------------------- 205 | repobrowsers = { 206 | 'cgit': CgitBrowser, 207 | 'gitweb': GitwebBrowser, 208 | 'github': GithubBrowser, 209 | 'github-private': GithubPrivateBrowser, 210 | } 211 | -------------------------------------------------------------------------------- /gitlink/utils.py: -------------------------------------------------------------------------------- 1 | from sys import getdefaultencoding, version_info 2 | from subprocess import Popen, PIPE 3 | 4 | 5 | basestr = (str, unicode) if version_info[0] == 2 else (str,) 6 | default_encoding = getdefaultencoding() 7 | 8 | def run(*args, **kw): 9 | p = Popen(stdout=PIPE, stderr=PIPE, shell=True, *args, **kw) 10 | out, err = p.communicate() 11 | ret = p.poll() 12 | 13 | if ret: 14 | cmd = kw.get('args') 15 | if cmd is None: 16 | cmd = args[0] 17 | 18 | if out: 19 | out = out.decode(default_encoding) 20 | out = out.rstrip('\n') 21 | 22 | return ret, out 23 | 24 | def to_clipboard(s): 25 | '''Send string to clipboard.''' 26 | try: 27 | from . pyperclip import copy 28 | except: 29 | raise Exception('warning: xclip or xsel must be installed for copying to work') 30 | copy(s) 31 | 32 | def shorten_hashes(res, length=7): 33 | for key in 'sha', 'tree_sha', 'object', 'commit_sha', 'top_tree_sha': 34 | if key in res and isinstance(res[key], basestr): 35 | res[key] = res[key][:length] 36 | -------------------------------------------------------------------------------- /man/git-link-man1.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | git-link 3 | ======== 4 | 5 | ------------------------------------- 6 | get repo browser links to git objects 7 | ------------------------------------- 8 | 9 | :Author: Georgi Valkov - https://github.com/gvalkov/git-link 10 | :Copyright: Revised BSD License 11 | :Version: 0.5.0 12 | :Manual section: 1 13 | 14 | SYNOPSIS 15 | ======== 16 | 17 | :: 18 | 19 | git link [-hvcubsr] |||| 20 | git link [-r|--raw] 21 | git link [-b|--browser|-u|--url] ||||| 22 | 23 | 24 | DESCRIPTION 25 | =========== 26 | 27 | Git-link builds repository browser urls for objects and paths in the working 28 | copy. The motivation behind git-link is that it is often faster to navigate to 29 | a git object or path on the command line than it is to click your way to it 30 | through a web interface. 31 | 32 | 33 | SETUP 34 | ============= 35 | 36 | Git-link needs to know the name and url of the repository browser for every 37 | repository it is being run in. They can be specified either through 38 | git-config(1) or on the command line:: 39 | 40 | git config --add link.url 41 | git config --add link.browser 42 | git config --add link.clipboard false|true 43 | git config --add link.short 7 44 | git link --url --browser --clipboard ... 45 | 46 | Only ``link.url|--url`` and ``link.browser|--browser`` need to be set. 47 | 48 | 49 | EXAMPLES 50 | ======== 51 | 52 | **git link** HEAD~10 53 | 54 | url to 10th commit before HEAD 55 | 56 | **git link** v0.1.0^{tree} 57 | 58 | url to tree object at tag v0.1.0 59 | 60 | **git link** --raw master:file 61 | 62 | url to raw file in branch master 63 | 64 | **git link** path/file 65 | 66 | url to path/file in current branch 67 | 68 | **git link** devel -- path/file 69 | 70 | url to path/fiel in branch devel 71 | 72 | **git link** --clipboard v0.1.0 73 | 74 | url to tag v0.1.0, copied to clipboard 75 | 76 | **git link** --short 10 HEAD~10 77 | 78 | url to 10th commit before HEAD with hashes truncated to 10 79 | characters 80 | 81 | 82 | OPTIONS 83 | ======= 84 | 85 | -h,--help 86 | 87 | show help message and exit 88 | 89 | -v,--version 90 | 91 | show version and exit 92 | 93 | -c,--clipboard 94 | 95 | copy link to clipboard (overwrites git config link.clipboard) 96 | 97 | -u,--url 98 | 99 | repo browser base url (overwrites git config link.url) 100 | 101 | -b,--browser 102 | 103 | repo browser type (overwrites git config link.browser) 104 | 105 | -s,--short 106 | 107 | truncate hashes to characters 108 | 109 | -r,--raw 110 | 111 | show raw blob if possible 112 | 113 | 114 | NOTES 115 | ===== 116 | 117 | The base repo browser url for gitweb must include the project name: 118 | 119 | **git config** --add http://git.kernel.org/?p=git/git.git 120 | 121 | 122 | SEE ALSO 123 | ======== 124 | 125 | ``git-config(1)``, ``gitrevisions(7)`` 126 | -------------------------------------------------------------------------------- /man/git-link.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH GIT-LINK 1 "" "0.3.0" "" 4 | .SH NAME 5 | git-link \- get a repo browser link to a git object 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .INDENT 0.0 35 | .INDENT 3.5 36 | .sp 37 | .nf 38 | .ft C 39 | git link [\-hvcubr] |||| 40 | git link [\-r|\-\-raw] 41 | git link [\-b|\-\-browser|\-u|\-\-url] ||||| 42 | .ft P 43 | .fi 44 | .UNINDENT 45 | .UNINDENT 46 | .SH DESCRIPTION 47 | .sp 48 | Git\-link builds repository browser urls for git objects and paths in the 49 | working copy. The motivation behind git\-link is that it is often faster to 50 | navigate to a git object or path on the command line than it is to click your 51 | way to it through a web interface. 52 | .SH SETUP 53 | .sp 54 | Git\-link needs to know the name and url of the repository browser for every 55 | repository it is being run in. They can be specified either through 56 | git\-config(1) or on the command line: 57 | .INDENT 0.0 58 | .INDENT 3.5 59 | .sp 60 | .nf 61 | .ft C 62 | git config \-\-add link.url 63 | git config \-\-add link.browser 64 | git config \-\-add link.clipboard false|true 65 | git config \-\-add link.short 7 66 | git link \-\-url \-\-browser \-\-clipboard ... 67 | .ft P 68 | .fi 69 | .UNINDENT 70 | .UNINDENT 71 | .sp 72 | Only \fBlink.url|\-\-url\fP and \fBlink.browser|\-\-browser\fP need to be set. 73 | .SH EXAMPLES 74 | .sp 75 | \fBgit link\fP HEAD~10 76 | .INDENT 0.0 77 | .INDENT 3.5 78 | url to 10th commit before HEAD 79 | .UNINDENT 80 | .UNINDENT 81 | .sp 82 | \fBgit link\fP v0.1.0^{tree} 83 | .INDENT 0.0 84 | .INDENT 3.5 85 | url to tree object at tag v0.1.0 86 | .UNINDENT 87 | .UNINDENT 88 | .sp 89 | \fBgit link\fP \-\-raw master:file 90 | .INDENT 0.0 91 | .INDENT 3.5 92 | url to raw file in branch master 93 | .UNINDENT 94 | .UNINDENT 95 | .sp 96 | \fBgit link\fP path/file 97 | .INDENT 0.0 98 | .INDENT 3.5 99 | url to path/file in current branch 100 | .UNINDENT 101 | .UNINDENT 102 | .sp 103 | \fBgit link\fP devel \-\- path/file 104 | .INDENT 0.0 105 | .INDENT 3.5 106 | url to path/fiel in branch devel 107 | .UNINDENT 108 | .UNINDENT 109 | .sp 110 | \fBgit link\fP \-\-clipboard v0.1.0 111 | .INDENT 0.0 112 | .INDENT 3.5 113 | url to tag v0.1.0, copied to clipboard 114 | .UNINDENT 115 | .UNINDENT 116 | .sp 117 | \fBgit link\fP \-\-short 10 HEAD~10 118 | .INDENT 0.0 119 | .INDENT 3.5 120 | url to 10th commit before HEAD with hashes truncated to 10 121 | characters 122 | .UNINDENT 123 | .UNINDENT 124 | .SH OPTIONS 125 | .sp 126 | \-h,\-\-help 127 | .INDENT 0.0 128 | .INDENT 3.5 129 | show help message and exit 130 | .UNINDENT 131 | .UNINDENT 132 | .sp 133 | \-v,\-\-version 134 | .INDENT 0.0 135 | .INDENT 3.5 136 | show version and exit 137 | .UNINDENT 138 | .UNINDENT 139 | .sp 140 | \-c,\-\-clipboard 141 | .INDENT 0.0 142 | .INDENT 3.5 143 | copy link to clipboard (overwrites git config link.clipboard) 144 | .UNINDENT 145 | .UNINDENT 146 | .sp 147 | \-u,\-\-url 148 | .INDENT 0.0 149 | .INDENT 3.5 150 | repo browser base url (overwrites git config link.url) 151 | .UNINDENT 152 | .UNINDENT 153 | .sp 154 | \-b,\-\-browser 155 | .INDENT 0.0 156 | .INDENT 3.5 157 | repo browser type (overwrites git config link.browser) 158 | .UNINDENT 159 | .UNINDENT 160 | .sp 161 | \-s,\-\-short 162 | .INDENT 0.0 163 | .INDENT 3.5 164 | truncate hashes to characters 165 | .UNINDENT 166 | .UNINDENT 167 | .sp 168 | \-r,\-\-raw 169 | .INDENT 0.0 170 | .INDENT 3.5 171 | show raw blob if possible 172 | .UNINDENT 173 | .UNINDENT 174 | .SH NOTES 175 | .sp 176 | The base repo browser url for gitweb must include the project name: 177 | .INDENT 0.0 178 | .INDENT 3.5 179 | \fBgit config\fP \-\-add \fI\%http://git.kernel.org/?p=git/git.git\fP 180 | .UNINDENT 181 | .UNINDENT 182 | .SH SEE ALSO 183 | .sp 184 | \fBgit\-config(1)\fP, \fBgitrevisions(7)\fP 185 | .SH AUTHOR 186 | Georgi Valkov - https://github.com/gvalkov/git-link 187 | .SH COPYRIGHT 188 | Revised BSD License 189 | .\" Generated by docutils manpage writer. 190 | . 191 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = test-output test-repos .tox 3 | 4 | [wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | from __future__ import print_function 5 | 6 | from os import getuid 7 | from os.path import isdir 8 | from setuptools import setup 9 | 10 | from gitlink import version 11 | 12 | 13 | classifiers = [ 14 | 'Environment :: Console', 15 | 'Topic :: Utilities', 16 | 'Operating System :: OS Independent', 17 | 'Programming Language :: Python :: 2.7', 18 | 'Programming Language :: Python :: 3.3', 19 | 'Programming Language :: Python :: 3.4', 20 | 'License :: OSI Approved :: BSD License', 21 | 'Development Status :: 5 - Production/Stable', 22 | ] 23 | 24 | kw = { 25 | 'name': 'gitlink', 26 | 'version': version, 27 | 'description': 'Git sub-command for getting a repo browser link to a git object', 28 | 'long_description': open('README.rst').read(), 29 | 'url': 'https://github.com/gvalkov/git-link', 30 | 'author': 'Georgi Valkov', 31 | 'author_email': 'georgi.t.valkov@gmail.com', 32 | 'license': 'Revised BSD License', 33 | 'keywords': 'git gitweb github cgit subcommand', 34 | 'classifiers': classifiers, 35 | 'packages': ['gitlink'], 36 | 'entry_points': {'console_scripts': ['git-link = gitlink.main:main']}, 37 | 'data_files': [('share/man/man1', ['man/git-link.1'])], 38 | 'tests_require': ['pytest'], 39 | 'zip_safe': True, 40 | } 41 | 42 | # try to install bash and zsh completions (emphasis on the *try*) 43 | if getuid() == 0: 44 | if isdir('/etc/bash_completion.d'): 45 | t = ('/etc/bash_completion.d/', ['etc/git-link.sh']) 46 | kw['data_files'].append(t) 47 | 48 | # this is only valid for fedora, freebsd and most debians 49 | dirs = ['/usr/share/zsh/functions/Completion/Unix/', 50 | '/usr/local/share/zsh/site-functions', 51 | '/usr/share/zsh/site-functions'] 52 | 53 | for dir in dirs: 54 | if isdir(dir): 55 | t = (dir, ['etc/_git-link']) 56 | kw['data_files'].append(t) 57 | continue 58 | 59 | if __name__ == '__main__': 60 | setup(**kw) 61 | -------------------------------------------------------------------------------- /tests/test_cgit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | from util import * 5 | 6 | res = [ 7 | # branch 8 | ('origin/wip', 9 | 'http://hjemli.net/git/cgit/log/?h=wip'), 10 | 11 | # commit 12 | ('bebe89d7c11a92bf206bf6e528c51ffa8ecbc0d5', 13 | 'http://hjemli.net/git/cgit/commit/?id=bebe89d7c11a92bf206bf6e528c51ffa8ecbc0d5'), 14 | 15 | # tree 16 | ('33e28db20cbae2aa513ccec38c7d4706654eed46', 17 | 'http://hjemli.net/git/cgit/tree/?tree=33e28db20cbae2aa513ccec38c7d4706654eed46'), 18 | 19 | # tag by name 20 | ('v0.8.3', 21 | 'http://hjemli.net/git/cgit/tag/?id=v0.8.3'), 22 | 23 | # tag by sha 24 | ('9094ee117ccbf5e76ec216548e2473aba70f1c8f', 25 | 'http://hjemli.net/git/cgit/tag/?id=v0.8.3.2'), 26 | 27 | # file path (HEAD) 28 | ('tests/setup.sh', 29 | 'http://hjemli.net/git/cgit/tree/tests/setup.sh/?tree=138e4f6924ba176e05b95e9921bab419912b0e01'), 30 | 31 | # dir path (HEAD) 32 | ('tests/', 33 | 'http://hjemli.net/git/cgit/tree/tests/?tree=138e4f6924ba176e05b95e9921bab419912b0e01'), 34 | 35 | # blob with tag 36 | ('v0.8.3~10:tests/setup.sh', 37 | 'http://hjemli.net/git/cgit/tree/tests/setup.sh/?tree=97eff9d6f8333ab79b08a5af72b836736b10c280'), 38 | 39 | # blob with commit 40 | ('7640d90:COPYING', 41 | 'http://hjemli.net/git/cgit/tree/COPYING/?tree=a0ec3e5222dbb0cff965487def39f5781e5cb231'), 42 | 43 | # raw blob with commit 44 | ('-r 7640d90:COPYING', 45 | 'http://hjemli.net/git/cgit/plain/COPYING/?tree=a0ec3e5222dbb0cff965487def39f5781e5cb231'), 46 | 47 | # raw blob with commit (short) 48 | ('-s 7 -r 7640d90:COPYING', 49 | 'http://hjemli.net/git/cgit/plain/COPYING/?tree=a0ec3e5'), 50 | ] 51 | 52 | url = 'http://hjemli.net/git/cgit' 53 | headrev = 'f9b801a1746d6c4476b230659d2e1f3714986550' 54 | 55 | @pytest.fixture 56 | def gitlink(request): 57 | return mk_gitlink(url, 'cgit', 'cgit', url, headrev) 58 | 59 | @mark.parametrize(('cmdargs', 'expect'), res) 60 | def test_cgit_auto_lib(gitlink, cmdargs, expect): 61 | assert gitlink[0](cmdargs) == expect 62 | assert validate_url_404(expect) 63 | 64 | @skipif_no_gitlink 65 | @mark.parametrize(('cmdargs', 'expect'), res) 66 | def test_cgit_auto_exe(gitlink, cmdargs, expect): 67 | assert gitlink[1](cmdargs) == expect 68 | assert validate_url_404(expect) 69 | -------------------------------------------------------------------------------- /tests/test_git.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import pytest 5 | from util import * 6 | from gitlink.git import * 7 | 8 | @pytest.fixture 9 | def repo(request): 10 | repo = Repo('https://github.com/gvalkov/git-link.git', '%s/test-git' % test_repo_dir, 'HEAD') 11 | repo.clone() 12 | repo.chdir() 13 | return repo 14 | 15 | def test_get_config(repo): 16 | repo.config('link.browser', '123') 17 | repo.config('link.url', 'asdf') 18 | repo.config('link.clipboard', 'true') 19 | 20 | assert get_config('link') == { 21 | 'url': 'asdf', 22 | 'browser': '123', 23 | 'clipboard': True 24 | } 25 | 26 | assert get_config('link', False) == { 27 | 'link.url': 'asdf', 28 | 'link.browser': '123', 29 | 'link.clipboard': True, 30 | } 31 | 32 | def test_cat_commit(repo): 33 | assert cat_commit('v0.1.0') == { 34 | 'author': 'Georgi Valkov 1329252276 +0200', 35 | 'committer': 'Georgi Valkov 1329252276 +0200', 36 | 'sha': '29faca327f595c01f795f9a2e9c27dca8aabcaee', 37 | 'tree': '80f10ec249b6916adcf6c95f575a0125b8541c05', 38 | 'parent': 'f5d981a75b18533c270d4aa4ffffa9fcf67f9a8b', 39 | } 40 | 41 | def test_commit(repo): 42 | assert commit('v0.1.0^') == commit('v0.1.0~') 43 | assert commit('v0.1.0~5')['sha'] == 'eb17e35ec82ab6ac947d73d4dc782d1f680d191d' 44 | 45 | def test_tree(repo): 46 | assert tree('v0.1.0^^{tree}') == tree('v0.1.0~^{tree}') 47 | assert tree('v0.1.0~5^{tree}')['sha'] == 'cfa9b260c33b9535c945c14564dd50c8ffa3c89e' 48 | 49 | def test_blob(repo): 50 | assert blob('v0.1.0^:setup.py') == blob('v0.1.0~:setup.py') 51 | assert blob('v0.1.0~2:setup.py') == blob('v0.1.0~2:setup.py') 52 | assert blob('v0.1.0~2:setup.py') == blob('v0.1.0~2:setup.py') 53 | 54 | assert blob('v0.1.0~5:gitlink/main.py') == { 55 | 'path': 'gitlink/main.py', 56 | 'sha': '489de118c078bd472073d2f20267e441a931b9d0', 57 | 'type': LT.blob, 58 | 'commit_sha': 'eb17e35ec82ab6ac947d73d4dc782d1f680d191d', 59 | 'tree_sha': '42706139a2f62814251c5b027f8e9d38239fbcee' 60 | } 61 | 62 | def test_branch(repo): 63 | assert branch('master')['ref'] == 'refs/remotes/origin/master' 64 | assert branch('master')['shortref'] == 'master' 65 | assert branch('origin/master')['ref'] == 'refs/remotes/origin/master' 66 | assert branch('origin/master')['shortref'] == 'master' 67 | 68 | def test_tag(repo): 69 | assert cat_tag('v0.1.0') == { 70 | 'sha': '29faca327f595c01f795f9a2e9c27dca8aabcaee', 71 | 'tagger': 'Georgi Valkov 1329252311 +0200', 72 | 'object': 'f54a0b6ad8518babf440db870dc778acc84877a8', 73 | 'type': 'commit', 74 | 'tag': 'v0.1.0' 75 | } 76 | 77 | def test_path(repo): 78 | assert path('gitlink/main.py', 'v0.1.0') == { 79 | 'tree_sha': 'b8ff9fc80e42bec20cfb1638f4efa0215fe4987a', 80 | 'commit_sha': 'f54a0b6ad8518babf440db870dc778acc84877a8', 81 | 'top_tree_sha': '80f10ec249b6916adcf6c95f575a0125b8541c05', 82 | 'sha': 'd930815a23f0cf53a471e2993bc42401926793fa', 83 | 'path': 'gitlink/main.py', 84 | 'type': LT.blob, 85 | } 86 | 87 | assert path('tests', 'v0.1.0') == { 88 | 'tree_sha': 'None', 89 | 'commit_sha': 'f54a0b6ad8518babf440db870dc778acc84877a8', 90 | 'top_tree_sha': '80f10ec249b6916adcf6c95f575a0125b8541c05', 91 | 'sha': '1a5bf01fcd47ff9936aac0344c587b616f081dfd', 92 | 'path': 'tests', 93 | 'type': LT.path, 94 | } 95 | 96 | assert path('non-existant') == {} 97 | -------------------------------------------------------------------------------- /tests/test_github.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | from util import * 5 | 6 | res = [ 7 | # branch 8 | ('master', 9 | 'https://github.com/gvalkov/rsstail.py/tree/master'), 10 | 11 | # commit 12 | ('a45ee2c27d2c8106c92fee0d909c31b3b0f67cd8', 13 | 'https://github.com/gvalkov/rsstail.py/commit/a45ee2c27d2c8106c92fee0d909c31b3b0f67cd8'), 14 | 15 | # tag by name 16 | ('v0.1.1', 17 | 'https://github.com/gvalkov/rsstail.py/tree/v0.1.1'), 18 | 19 | # tag by sha 20 | ('42c7a1094a5bfe9adec228d65681940f9e673f7d', 21 | 'https://github.com/gvalkov/rsstail.py/tree/v0.1.1'), 22 | 23 | # file path (HEAD) 24 | ('.gitignore', 25 | 'https://github.com/gvalkov/rsstail.py/tree/790cc8cde0ef6c06443ee14c11ff7b1c6d3f22f4/.gitignore'), 26 | 27 | # dir path (HEAD) 28 | ('tests/', 29 | 'https://github.com/gvalkov/rsstail.py/tree/790cc8cde0ef6c06443ee14c11ff7b1c6d3f22f4/tests'), 30 | 31 | # blob with tag 32 | ('v0.1.1:setup.py', 33 | 'https://github.com/gvalkov/rsstail.py/tree/3f0a595895d9ed0932af7de7229cec00f4acda4e/setup.py'), 34 | 35 | # blob with commit 36 | ('9407973:.gitignore', 37 | 'https://github.com/gvalkov/rsstail.py/tree/9407973348bf2250981c00aa5258c23bb2b04cdf/.gitignore'), 38 | 39 | # raw blob with commit 40 | ('-r 9407973:.gitignore', 41 | 'https://raw.github.com/gvalkov/rsstail.py/9407973348bf2250981c00aa5258c23bb2b04cdf/.gitignore'), 42 | 43 | # branch/tag -- file 44 | ('v0.1.0 -- tests', 45 | 'https://github.com/gvalkov/rsstail.py/tree/8c30df6a69052e02b67201d77ba2513ec832b530/tests'), 46 | 47 | # branch/tag -- file (short) 48 | ('-s 7 v0.1.0 -- tests', 49 | 'https://github.com/gvalkov/rsstail.py/tree/8c30df6/tests'), 50 | ] 51 | 52 | url = 'https://github.com/gvalkov/rsstail.py.git' 53 | linkurl = 'https://github.com/gvalkov/rsstail.py' 54 | headrev = '790cc8cde0ef6c06443ee14c11ff7b1c6d3f22f4' 55 | 56 | @pytest.fixture 57 | def gitlink(request): 58 | return mk_gitlink(url, 'github', 'github', linkurl, headrev) 59 | 60 | @mark.parametrize(('cmdargs', 'expect'), res) 61 | def test_github_auto_lib(gitlink, cmdargs, expect): 62 | assert gitlink[0](cmdargs) == expect 63 | assert validate_url_404(expect) 64 | 65 | @skipif_no_gitlink 66 | @mark.parametrize(('cmdargs', 'expect'), res) 67 | def test_github_auto_exe(gitlink, cmdargs, expect): 68 | assert gitlink[1](cmdargs) == expect 69 | assert validate_url_404(expect) 70 | -------------------------------------------------------------------------------- /tests/test_gitweb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | from util import * 5 | 6 | res = [ 7 | # commit 8 | ('90f02cb510335a5bfdb57f0c78915d5ac236013c', 9 | 'http://git.naquadah.org/?p=oocairo.git;a=commitdiff;h=90f02cb510335a5bfdb57f0c78915d5ac236013c'), 10 | 11 | # tree 12 | ('90f02cb510335a5bfdb57f0c78915d5ac236013c^{tree}', 13 | 'http://git.naquadah.org/?p=oocairo.git;a=tree;h=7d0f2011b9aa9343cf3ae6675416ddcbfddab7e9'), 14 | 15 | # branch 16 | ('master', 17 | 'http://git.naquadah.org/?p=oocairo.git;a=shortlog;h=master'), 18 | 19 | # tag by name 20 | ('v1.4', 21 | 'http://git.naquadah.org/?p=oocairo.git;a=commit;h=v1.4'), 22 | 23 | # tab by sha 24 | ('f8e35c47ddb48dfeffb1f80cf523ba3207b31aa1', 25 | 'http://git.naquadah.org/?p=oocairo.git;a=commit;h=v1.3'), 26 | 27 | # file path (HEAD) 28 | ('test/context.lua', 29 | 'http://git.naquadah.org/?p=oocairo.git;a=blob;h=472061f27b61d2bcba7a7dc75743a0e8db1a4e4c;f=test/context.lua'), 30 | 31 | # dir path (HEAD) 32 | ('test/', 33 | 'http://git.naquadah.org/?p=oocairo.git;a=tree;f=test;h=8e941a88c606930750c98fc10927b17f0588cc8d'), 34 | 35 | # blob with tag 36 | ('v1.4:Changes', 37 | 'http://git.naquadah.org/?p=oocairo.git;a=blob;h=5a2f18eac98afb6a601369f5fa867cd0d386b266;f=Changes'), 38 | 39 | # blob with commit 40 | ('47bf539:COPYRIGHT', 41 | 'http://git.naquadah.org/?p=oocairo.git;a=blob;h=f90b1e3f8284f6a94f36919219acc575d9362e10;f=COPYRIGHT'), 42 | 43 | # raw blob with commit 44 | ('-r 47bf539:COPYRIGHT', 45 | 'http://git.naquadah.org/?p=oocairo.git;a=blob_plain;h=f90b1e3f8284f6a94f36919219acc575d9362e10;f=COPYRIGHT'), 46 | 47 | # raw blob with commit (short) 48 | ('-s 7 -r 47bf539:COPYRIGHT', 49 | 'http://git.naquadah.org/?p=oocairo.git;a=blob_plain;h=f90b1e3;f=COPYRIGHT'), 50 | ] 51 | 52 | url = 'http://git.naquadah.org/git/oocairo.git' 53 | linkurl = 'http://git.naquadah.org/?p=oocairo.git' 54 | headrev = '2b40c79192e3c86074d21af51774971e19cbd2ab' 55 | 56 | @pytest.fixture 57 | def gitlink(request): 58 | return mk_gitlink(url, 'gitweb', 'gitweb', linkurl, headrev) 59 | 60 | @mark.parametrize(('cmdargs', 'expect'), res) 61 | def test_gitweb_auto_lib(gitlink, cmdargs, expect): 62 | assert gitlink[0](cmdargs) == expect 63 | assert validate_url_404(expect) 64 | 65 | @skipif_no_gitlink 66 | @mark.parametrize(('cmdargs', 'expect'), res) 67 | def test_gitweb_auto_exe(gitlink, cmdargs, expect): 68 | assert gitlink[1](cmdargs) == expect 69 | assert validate_url_404(expect) 70 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import pytest 5 | import os, sys 6 | 7 | from shutil import rmtree 8 | from tempfile import mkdtemp 9 | from functools import partial 10 | from subprocess import call, check_call, check_output 11 | from os.path import dirname, abspath, join as pjoin, isdir, isfile 12 | 13 | try: 14 | from cStringIO import StringIO 15 | except ImportError: 16 | from io import StringIO 17 | 18 | from pytest import mark 19 | from gitlink.main import main 20 | 21 | 22 | run = lambda *x, **kw: check_call(x, **kw) 23 | here = dirname(abspath(__file__)) 24 | test_output_dir = pjoin(here, 'test-output') 25 | test_repo_dir = pjoin(here, 'test-repos') 26 | 27 | 28 | class Repo(object): 29 | def __init__(self, remote, local=None, headrev='HEAD'): 30 | self.remote = remote 31 | self.headrev = headrev 32 | self.local = local if local else mkdtemp() 33 | 34 | def clone(self, overwrite=False): 35 | if isdir(self.local): 36 | return 37 | os.makedirs(self.local) 38 | run('git', 'clone', self.remote, self.local) 39 | run('git', 'reset', '--hard', self.headrev, cwd=self.local) 40 | 41 | def init(self): 42 | run('git', 'init', self.local) 43 | 44 | def config(self, key, value): 45 | run('git', 'config', key, value, cwd=self.local) 46 | 47 | def rmtree(self): 48 | rmtree(self.local) 49 | 50 | def chdir(self): 51 | os.chdir(self.local) 52 | 53 | def validate_url_404(url): 54 | res = call('curl -sI "%s" | grep -iq "404 Not found"' % url, shell=True) 55 | return bool(res) 56 | 57 | def mk_gitlink(url, codir, browser, linkurl, headrev): 58 | codir = '%s/%s' % (test_repo_dir, codir) 59 | repo = Repo(url, codir, headrev) 60 | repo.clone() 61 | repo.config('link.browser', browser) 62 | repo.config('link.url', linkurl) 63 | 64 | def gitlink_lib(args): 65 | os.chdir(codir) 66 | out = StringIO() 67 | main(args.split(' '), out) 68 | return out.getvalue().rstrip() 69 | 70 | def gitlink_exe(args): 71 | os.chdir(codir) 72 | cmd = ['%s/../git-link' % here] 73 | cmd.extend(args.split()) 74 | return check_output(cmd).rstrip().decode('utf8') 75 | 76 | return gitlink_lib, gitlink_exe 77 | 78 | skipif_no_gitlink = mark.skipif(not isfile(pjoin(here, '../git-link')), reason='missing git-link.zip') 79 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py32, py33, py34 3 | 4 | [testenv] 5 | deps = pytest 6 | changedir = tests 7 | commands = py.test --basetemp={envtmpdir} [] 8 | --------------------------------------------------------------------------------