diff --git a/.travis.yml b/.travis.yml index 2310e76..80b0d21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,6 @@ before_install: # fix a crash with multiprocessing on Travis # - sudo rm -rf /dev/shm # - sudo ln -s /run/shm /dev/shm - # coverage submission packages - git fetch --tags install: - pip install tox diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..d8389aa --- /dev/null +++ b/LICENCE @@ -0,0 +1,10 @@ +* files: * + MPLv2.0 2015-2018 (c) Casper da Costa-Luis + [casperdcl](https://github.com/casperdcl). + + +Mozilla Public Licence (MPL) v. 2.0 - Exhibit A +----------------------------------------------- + +This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/MANIFEST.in b/MANIFEST.in index 0ee3ce4..1aa3c9a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,13 @@ # Misc include .coveragerc +include LICENCE include Makefile -include README.rst include tox.ini include git-fame_completion.bash # Test suite recursive-include gitfame/tests *.py + +# Examples/Documentation +include README.rst +include git-fame.1 diff --git a/Makefile b/Makefile index e8029f8..87a2179 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,9 @@ none run +help: + @python setup.py make + alltests: @+make testcoverage @+make flake8 @@ -76,10 +79,11 @@ coverclean: @+python -c "import shutil; shutil.rmtree('gitfame/__pycache__', True)" @+python -c "import shutil; shutil.rmtree('gitfame/tests/__pycache__', True)" clean: - @+python -c "import os; import glob; [os.remove(i) for i in glob.glob('*.py[co]')]" - @+python -c "import os; import glob; [os.remove(i) for i in glob.glob('gitfame/*.py[co]')]" - @+python -c "import os; import glob; [os.remove(i) for i in glob.glob('gitfame/*.c')]" - @+python -c "import os; import glob; [os.remove(i) for i in glob.glob('gitfame/tests/*.py[co]')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('*.py[co]')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('gitfame/*.py[co]')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('gitfame/*.c')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('gitfame/*.so')]" + @+python -c "import os, glob; [os.remove(i) for i in glob.glob('gitfame/tests/*.py[co]')]" toxclean: @+python -c "import shutil; shutil.rmtree('.tox', True)" @@ -113,3 +117,9 @@ none: run: python -Om gitfame + +git-fame.1: git-fame.1.md + python -m gitfame --help | tail -n+9 | head -n-2 | cat "$<" - |\ + sed -r 's/^ (--\S+) (\S+)\s*(.*)$$/\n\\\1=*\2*\n: \3/' |\ + sed -r 's/^ (-\S+, -\S+)\s*/\n\1\n: /' |\ + pandoc -o "$@" -s -t man diff --git a/README.rst b/README.rst index da432a2..25fb46a 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,14 @@ git-fame ======== +Pretty-print ``git`` repository collaborators sorted by contributions. + |PyPI-Status| |PyPI-Versions| |Build-Status| |Coverage-Status| |Branch-Coverage-Status| |Codacy-Grade| |LICENCE| |Donate| |OpenHub-Status| - -Pretty-print ``git`` repository collaborators sorted by contributions. - .. code:: sh ~$ git fame @@ -35,6 +34,7 @@ The ``distribution`` column is a percentage breakdown of the other columns :backlinks: top :local: + Installation ------------ @@ -111,9 +111,7 @@ It is also possible to run from within a python shell or script. .. code:: python >>> import gitfame - >>> import sys - >>> sys.argv = ['', '--sort=commits', '-wt', './path/to/my/repo'] - >>> gitfame.main() + >>> gitfame.main(['--sort=commits', '-wt', '/path/to/my/repo']) Documentation @@ -131,7 +129,7 @@ Documentation -h, --help Print this help and exit. -v, --version Print module version and exit. --branch= Branch or tag [default: HEAD]. - --sort= Options: [default: loc], files, commits. + --sort= [default: loc]|commits|files. --excl= Excluded files (default: None). In no-regex mode, may be a comma-separated list. Escape (\,) for a literal comma (may require \\, in shell). @@ -154,7 +152,7 @@ Licence Open Source (OSI approved): |LICENCE| -Copyright (c) 2016-7 Casper da Costa-Luis. +Copyright (c) 2016-8 Casper da Costa-Luis. This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. @@ -169,6 +167,8 @@ Authors - Casper da Costa-Luis (`@casperdcl `__) |Donate| +|git-fame-hits| + .. |Build-Status| image:: https://travis-ci.org/casperdcl/git-fame.svg?branch=master :target: https://travis-ci.org/casperdcl/git-fame .. |Coverage-Status| image:: https://coveralls.io/repos/casperdcl/git-fame/badge.svg?branch=master @@ -181,6 +181,7 @@ Authors :target: https://pypi.python.org/pypi/git-fame .. |PyPI-Versions| image:: https://img.shields.io/pypi/pyversions/git-fame.svg :target: https://pypi.python.org/pypi/git-fame +.. |git-fame-hits| image:: https://caspersci.uk.to/cgi-bin/hits.cgi?q=git-fame&a=hidden .. |OpenHub-Status| image:: https://www.openhub.net/p/git-fame/widgets/project_thin_badge?format=gif :target: https://www.openhub.net/p/git-fame?ref=Thin+badge .. |LICENCE| image:: https://img.shields.io/pypi/l/git-fame.svg diff --git a/git-fame.1 b/git-fame.1 new file mode 100644 index 0000000..09d9707 --- /dev/null +++ b/git-fame.1 @@ -0,0 +1,122 @@ +.\" Automatically generated by Pandoc 1.19.2.1 +.\" +.TH "GIT\-FAME" "1" "2017\-2018" "git\-fame User Manuals" "" +.hy +.SH NAME +.PP +git\-fame \- Pretty\-print \f[C]git\f[] repository collaborators sorted +by contributions. +.SH SYNOPSIS +.PP +gitfame [\-\-help | \f[I]options\f[]] [<\f[I]gitdir\f[]>] +.SH DESCRIPTION +.PP +See . +.PP +Probably not necessary on UNIX systems: +.IP +.nf +\f[C] +git\ config\ \-\-global\ alias.fame\ "!python\ \-m\ gitfame" +\f[] +.fi +.PP +For example, to print statistics regarding all source files in a +C++/CUDA repository (\f[C]*.c/h/t(pp),\ *.cu(h)\f[]), carefully handling +whitespace and line copies: +.IP +.nf +\f[C] +git\ fame\ \-\-incl\ \[aq]\\.[cht][puh]{0,2}$\[aq]\ \-twMC +\f[] +.fi +.SH OPTIONS +.TP +.B +[default: ./] +.RS +.RE +.TP +.B \-h, \-\-help +show this help message and exit +.RS +.RE +.TP +.B \-\-sort=\f[I]key\f[] +[default: loc]|commits|files. +.RS +.RE +.TP +.B \-t, \-\-bytype +Show stats per file extension [default: False]. +.RS +.RE +.TP +.B \-M, \-M +Detect intra\-file line moves and copies [default: False]. +.RS +.RE +.TP +.B \-w, \-\-ignore\-whitespace +.IP +.nf +\f[C] +\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ Ignore\ whitespace\ when\ comparing\ the\ parent\[aq]s\ version +\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ and\ the\ child\[aq]s\ to\ find\ where\ the\ lines\ came\ from +\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ [default:\ False]. +\f[] +.fi +.RS +.RE +.TP +.B \-\-incl=\f[I]f\f[] +Included files [default: .*]. +See \f[C]\-\-excl\f[] for format. +.RS +.RE +.TP +.B \-s, \-\-silent\-progress +.IP +.nf +\f[C] +\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ Suppress\ `tqdm`\ [default:\ False]. +\f[] +.fi +.RS +.RE +.TP +.B \-\-branch=\f[I]b\f[] +Branch or tag [default: HEAD]. +.RS +.RE +.TP +.B \-v, \-\-version +show program\[aq]s version number and exit +.RS +.RE +.TP +.B \-C, \-C +Detect inter\-file line moves and copies [default: False]. +.RS +.RE +.TP +.B \-\-excl=\f[I]f\f[] +Excluded files (default: None). +In no\-regex mode, may be a comma\-separated list. +Escape (,) for a literal comma (may require \\, in shell). +.RS +.RE +.TP +.B \-\-log=\f[I]lvl\f[] +FATAL|CRITICAL|ERROR|WARN(ING)|[default: INFO]|DEBUG|NOTSET. +.RS +.RE +.TP +.B \-n, \-\-no\-regex +Assume are comma\-separated exact matches rather than regular +expressions [default: False]. +NB: if regex is enabled \f[C],\f[] is equivalent to \f[C]|\f[]. +.RS +.RE +.SH AUTHORS +Casper da Costa\-Luis . diff --git a/git-fame.1.md b/git-fame.1.md new file mode 100644 index 0000000..aecd885 --- /dev/null +++ b/git-fame.1.md @@ -0,0 +1,34 @@ +% GIT-FAME(1) git-fame User Manuals +% Casper da Costa-Luis +% 2017-2018 + +# NAME + +git-fame - Pretty-print `git` repository collaborators sorted by contributions. + +# SYNOPSIS + +gitfame [--help | *options*] [<*gitdir*>] + +# DESCRIPTION + +See . + +Probably not necessary on UNIX systems: + +```sh +git config --global alias.fame "!python -m gitfame" +``` + +For example, to print statistics regarding all source files in a C++/CUDA +repository (``*.c/h/t(pp), *.cu(h)``), carefully handling whitespace and line +copies: + +```sh +git fame --incl '\.[cht][puh]{0,2}$' -twMC +``` + +# OPTIONS + +\ +: [default: ./] diff --git a/gitfame/_gitfame.py b/gitfame/_gitfame.py index a20e7b4..ad1ec94 100755 --- a/gitfame/_gitfame.py +++ b/gitfame/_gitfame.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -r""" -Usage: +r"""Usage: gitfame [--help | options] [] Arguments: @@ -10,7 +9,7 @@ -h, --help Print this help and exit. -v, --version Print module version and exit. --branch= Branch or tag [default: HEAD]. - --sort= Options: [default: loc], files, commits. + --sort= [default: loc]|commits|files. --excl= Excluded files (default: None). In no-regex mode, may be a comma-separated list. Escape (\,) for a literal comma (may require \\, in shell). @@ -40,7 +39,7 @@ tabber = None from ._utils import TERM_WIDTH, int_cast_or_len, Max, fext, _str, \ - check_output, tqdm, TqdmStream + check_output, tqdm, TqdmStream, print_unicode from ._version import __version__ # NOQA __author__ = "Casper da Costa-Luis " @@ -152,10 +151,6 @@ def run(args): log.debug("parsing args") - if args.gitdir is None: - args.gitdir = './' - # sys.argv[0][:sys.argv[0].replace('\\','/').rfind('/')] - if args.sort not in ["loc", "commits", "files"]: log.warn("--sort argument (" + args.sort + ") unrecognised\n" + __doc__) @@ -216,7 +211,7 @@ def run(args): try: blame_out = check_output(git_blame_cmd, stderr=subprocess.STDOUT) except Exception as e: - log.warn(str(e)) + log.warn(fname + ':' + str(e)) continue log.log(logging.NOTSET, blame_out) auths = RE_AUTHS.findall(blame_out) @@ -258,30 +253,25 @@ def run(args): for stats in it_val_as()) log.debug(stats_tot) - ''' - extns = set() - if args.bytype: - for stats in it_val_as(): - extns.update([fext(i) for i in stats["files"]]) - log.debug(extns) - ''' + # TODO: + # extns = set() + # if args.bytype: + # for stats in it_val_as(): + # extns.update([fext(i) for i in stats["files"]]) + # log.debug(extns) print('Total ' + '\nTotal '.join("{0:s}: {1:d}".format(k, v) for (k, v) in sorted(getattr( stats_tot, 'iteritems', stats_tot.items)()))) - for c in tabulate(auth_stats, stats_tot, args.sort): - try: - print(c, end='') - except UnicodeEncodeError: - print('?', end='') - print ('') + print_unicode(tabulate(auth_stats, stats_tot, args.sort, args.bytype)) -def main(): +def main(args=None): + """args : list [default: sys.argv[1:]]""" from argopt import argopt args = argopt(__doc__ + '\n' + __copyright__, - version=__version__).parse_args() + version=__version__).parse_args(args=args) logging.basicConfig( level=getattr(logging, args.log, logging.INFO), stream=TqdmStream) diff --git a/gitfame/_utils.py b/gitfame/_utils.py index 879ff0e..97e849a 100644 --- a/gitfame/_utils.py +++ b/gitfame/_utils.py @@ -1,3 +1,4 @@ +from __future__ import print_function import sys import subprocess import logging @@ -37,7 +38,7 @@ def write(cls, msg, end='\n'): __date__ = "2016" __licence__ = "[MPLv2.0](https://mozilla.org/MPL/2.0/)" __all__ = ["TERM_WIDTH", "int_cast_or_len", "Max", "fext", "_str", "tqdm", - "tighten", "check_output"] + "tighten", "check_output", "print_unicode"] __copyright__ = ' '.join(("Copyright (c)", __date__, __author__, __licence__)) __license__ = __licence__ # weird foreign language @@ -223,3 +224,13 @@ def Max(it, empty_default=0): if 'empty sequence' in str(e): return empty_default raise # pragma: no cover + + +def print_unicode(msg, end='\n', err='?'): + """print `msg`, replacing unicode characters with `err` upon failure""" + for c in msg: + try: + print(c, end='') + except UnicodeEncodeError: + print(err, end='') + print ('', end=end) diff --git a/gitfame/_version.py b/gitfame/_version.py index 7efa007..495ec81 100644 --- a/gitfame/_version.py +++ b/gitfame/_version.py @@ -12,7 +12,7 @@ __all__ = ["__version__"] # major, minor, patch, -extra -version_info = 1, 4, 1 +version_info = 1, 4, 2 # Nice string for the version __version__ = '.'.join(map(str, version_info)) diff --git a/gitfame/tests/tests_gitfame.py b/gitfame/tests/tests_gitfame.py index e8deaae..c1149fe 100644 --- a/gitfame/tests/tests_gitfame.py +++ b/gitfame/tests/tests_gitfame.py @@ -14,12 +14,12 @@ def test_table_line(): - """ Test table line drawing """ + """Test table line drawing""" assert (_gitfame.tr_hline([3, 4, 2], hl='/', x='#') == '#///#////#//#') def test_tabulate(): - """ Test tabulate """ + """Test tabulate""" auth_stats = { u'Not Committed Yet': {'files': set([ @@ -46,7 +46,7 @@ def test_tabulate(): # WARNING: this should be the last test as it messes with sys.argv def test_main(): - """ Test command line pipes """ + """Test command line pipes""" import subprocess from os.path import dirname as dn @@ -67,23 +67,22 @@ def test_main(): sys.stdout = StringIO() sys.stderr = sys.stdout - sys.argv = ['', '--silent-progress'] - import gitfame.__main__ # NOQA + # sys.argv = ['', '--silent-progress'] + # import gitfame.__main__ # NOQA + main(['--silent-progress']) - sys.argv = ['', '--bad', 'arg'] try: - main() - except: + main(['--bad', 'arg']) + except SystemExit: if """usage: gitfame [-h] [""" not in sys.stdout.getvalue(): raise else: raise ValueError("Expected --bad arg to fail") sys.stdout.seek(0) - sys.argv = ['', '-s', '--sort', 'badSortArg'] # import logging # logging.basicConfig(level=logging.INFO, stream=sys.stdout) - main() + main(['-s', '--sort', 'badSortArg']) # if "--sort argument (badSortArg) unrecognised" \ # not in sys.stdout.getvalue(): # raise ValueError("Expected --sort argument (badSortArg) unrecognised") @@ -91,13 +90,13 @@ def test_main(): for params in [ ['--sort', 'commits'], ['--no-regex'], - ['--no-regex', '--incl', '.*py'], + ['--no-regex', '--incl', 'setup.py,README.rst'], + ['--excl', r'.*\.py'], ['-w'], ['-M'], ['-C'], ['-t'] ]: - sys.argv = ['', '-s'] + params - main() + main(['-s'] + params) sys.argv, sys.stdout, sys.stderr = _SYS_AOE diff --git a/gitfame/tests/tests_utils.py b/gitfame/tests/tests_utils.py index 4c6a112..05a4c71 100644 --- a/gitfame/tests/tests_utils.py +++ b/gitfame/tests/tests_utils.py @@ -1,19 +1,9 @@ from __future__ import unicode_literals - -# import sys -# import re -# from nose import with_setup -# from nose.plugins.skip import SkipTest -# from io import IOBase # to support unicode strings -# try: -# from StringIO import StringIO -# except: -# from io import StringIO from gitfame import _utils def test_tighten(): - """ Test (grid) table compression """ + """Test (grid) table compression""" orig_tab = ''' +------------------------+-----+------+------+----------------------+ @@ -48,20 +38,25 @@ def test_tighten(): def test_fext(): - """ Test detection of file extensions """ + """Test detection of file extensions""" assert (_utils.fext('foo/bar.baz') == 'baz') assert (_utils.fext('foo/.baz') == 'baz') assert (_utils.fext('foo/bar') == '') def test_Max(): - """ Test max with defaults """ + """Test max with defaults""" assert (_utils.Max(range(10), -1) == 9) assert (_utils.Max(range(0), -1) == -1) def test_integer_stats(): - """ Test integer representations """ + """Test integer representations""" assert (_utils.int_cast_or_len(range(10)) == 10) assert (_utils.int_cast_or_len('90 foo') == 6) assert (_utils.int_cast_or_len('90') == 90) + + +def test_print(): + """Test printing of unicode""" + _utils.print_unicode("\x81") diff --git a/setup.py b/setup.py index df0ebe7..567478c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,9 @@ except ImportError: from distutils.core import setup import sys -import subprocess +from subprocess import check_call +from io import open as io_open + # For Makefile parsing import shlex try: # pragma: no cover @@ -17,7 +19,6 @@ # Python 3 compatibility import configparser as ConfigParser import io as StringIO -import io import re try: @@ -35,16 +36,14 @@ def cythonize(*args, **kwargs): __licence__ = None __version__ = None main_file = os.path.join(os.path.dirname(__file__), 'gitfame', '_gitfame.py') -for l in io.open(main_file, mode='r'): +for l in io_open(main_file, mode='r'): if any(l.startswith(i) for i in ('__author__', '__licence__')): exec(l) version_file = os.path.join(os.path.dirname(__file__), 'gitfame', '_version.py') -with io.open(version_file, mode='r') as fd: +with io_open(version_file, mode='r') as fd: exec(fd.read()) - -# # Makefile auxiliary functions # # - +# Makefile auxiliary functions # RE_MAKE_CMD = re.compile('^\t(@\+?)(make)?', flags=re.M) @@ -60,7 +59,7 @@ def parse_makefile_aliases(filepath): # -- Parsing the Makefile using ConfigParser # Adding a fake section to make the Makefile a valid Ini file ini_str = '[root]\n' - with io.open(filepath, mode='r') as fd: + with io_open(filepath, mode='r') as fd: ini_str = ini_str + RE_MAKE_CMD.sub('\t', fd.read()) ini_fp = StringIO.StringIO(ini_str) # Parse using ConfigParser @@ -136,16 +135,17 @@ def execute_makefile_commands(commands, alias, verbose=False): if verbose: print("Running command: " + cmd) # Launch the command and wait to finish (synchronized call) - subprocess.check_call(parsed_cmd) + check_call(parsed_cmd, + cwd=os.path.dirname(os.path.abspath(__file__))) -# # Main setup.py config # # +# Main setup.py config # # Executing makefile commands if specified if sys.argv[1].lower().strip() == 'make': # Filename of the makefile - fpath = 'Makefile' + fpath = os.path.join(os.path.dirname(__file__), 'Makefile') # Parse the makefile, substitute the aliases and extract the commands commands = parse_makefile_aliases(fpath) @@ -172,13 +172,12 @@ def execute_makefile_commands(commands, alias, verbose=False): sys.exit(0) -# # Python package config # # +# Python package config # - -README_rst = None -with io.open('README.rst', mode='r', encoding='utf-8') as fd: +README_rst = '' +fndoc = os.path.join(os.path.dirname(__file__), 'README.rst') +with io_open(fndoc, mode='r', encoding='utf-8') as fd: README_rst = fd.read() - setup( name='git-fame', version=__version__, @@ -193,8 +192,10 @@ def execute_makefile_commands(commands, alias, verbose=False): platforms=['any'], packages=['gitfame'], provides=['gitfame'], - install_requires=['argopt'], + install_requires=['argopt>=0.3.5'], entry_points={'console_scripts': ['git-fame=gitfame:main'], }, + data_files=[('man/man1', ['git-fame.1'])], + package_data={'': ['LICENCE']}, ext_modules=cythonize(["gitfame/_gitfame.py", "gitfame/_utils.py"], nthreads=2), classifiers=[ diff --git a/tox.ini b/tox.ini index 79b7c83..37ed79b 100644 --- a/tox.ini +++ b/tox.ini @@ -39,16 +39,14 @@ deps = {[extra]deps} tqdm tabulate -commands = - {[extra]commands} +commands = {[extra]commands} [testenv:py26] deps = {[coverage]deps} tqdm tabulate -commands = - {[coverage]commands} +commands = {[coverage]commands} [testenv:nodeps] deps = {[extra]deps}