mirror of
https://github.com/ejwa/gitinspector.git
synced 2025-03-26 10:11:10 +01:00
Merge 639fdea2b4
into b8375442a7
This commit is contained in:
commit
b275827aab
50 changed files with 2089 additions and 451 deletions
.coveragerc
.github/workflows
.gitignoreMakefilePipfilePipfile.lockREADME.mdgitinspector
basedir.pyblame.pychanges.pyclone.pycomment.pyconfig.pyextensions.pyfiltering.pyformat.pygitinspector.pygravatar.pyhelp.pyinterval.pylocalization.pymetrics.pyoptval.py
pyproject.tomlrequirements.txtoutput
blameoutput.pychangesoutput.pyextensionsoutput.pyfilteringoutput.pymetricsoutput.pyoutputable.pyresponsibilitiesoutput.pytimelineoutput.py
responsibilities.pyterminal.pytimeline.pyversion.pytests
2
.coveragerc
Normal file
2
.coveragerc
Normal file
|
@ -0,0 +1,2 @@
|
|||
[run]
|
||||
relative_files = True
|
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '27 19 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
37
.github/workflows/python-package.yml
vendored
Normal file
37
.github/workflows/python-package.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
|
||||
name: Python package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, develop, feature/** ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7, 3.8, 3.9, 3.10.2]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Test with pytest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: make test
|
36
.github/workflows/release.yml
vendored
Normal file
36
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel twine
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Test
|
||||
run: make dist
|
||||
|
||||
- name: Release
|
||||
id: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: dist/*
|
||||
fail_on_unmatched_files: true
|
||||
prerelease: ${{ endsWith(github.ref, 'dev') || endsWith(github.ref, 'pre') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -6,3 +6,5 @@ node_modules
|
|||
*.egg-info
|
||||
*.pyc
|
||||
*.tgz
|
||||
.DS_Store
|
||||
.coverage
|
84
Makefile
Normal file
84
Makefile
Normal file
|
@ -0,0 +1,84 @@
|
|||
.PHONY: clean clean-test clean-pyc clean-build docs help
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
define PRINT_HELP_PYSCRIPT
|
||||
import re, sys
|
||||
|
||||
for line in sys.stdin:
|
||||
match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
|
||||
if match:
|
||||
target, help = match.groups()
|
||||
print("%-20s %s" % (target, help))
|
||||
endef
|
||||
export PRINT_HELP_PYSCRIPT
|
||||
|
||||
help:
|
||||
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
|
||||
|
||||
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
|
||||
|
||||
clean-build: ## remove build artifacts
|
||||
rm -fr build/
|
||||
rm -fr dist/
|
||||
rm -fr .eggs/
|
||||
find . -name '*.egg-info' -exec rm -fr {} +
|
||||
find . -name '*.egg' -exec rm -f {} +
|
||||
|
||||
clean-pyc: ## remove Python file artifacts
|
||||
find . -name '*.pyc' -exec rm -f {} +
|
||||
find . -name '*.pyo' -exec rm -f {} +
|
||||
find . -name '*~' -exec rm -f {} +
|
||||
find . -name '__pycache__' -exec rm -fr {} +
|
||||
|
||||
clean-test: ## remove test and coverage artifacts
|
||||
rm -f .coverage
|
||||
rm -fr .pytest_cache
|
||||
|
||||
lint: ## check style with flake8 and pylint
|
||||
pylint --rcfile=.pylintrc gitinspector
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 gitinspector tests --count --select=E9,F63,F7,F82 --show-source --statistics --builtins="_"
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 gitinspector tests --count --ignore=E203,E722,W503,E401,C901 --exit-zero --max-complexity=10 --max-line-length=127 --statistics --builtins="_"
|
||||
|
||||
format: ## auto format all the code with black
|
||||
black ./gitinspector --line-length 127
|
||||
|
||||
test: ## run tests quickly with the default Python
|
||||
pytest
|
||||
|
||||
test-debug: ## run tests with debugging enabled
|
||||
LOGLEVEL=debug; py.test -s --pdb
|
||||
|
||||
test-coverage: ## check code coverage quickly with the default Python
|
||||
coverage run --source gitinspector -m pytest
|
||||
coverage report -m
|
||||
|
||||
test-coverage-report: test-coverage ## Report coverage to Coveralls
|
||||
coveralls
|
||||
|
||||
release: dist ## package and upload a release
|
||||
twine upload dist/*
|
||||
|
||||
tag-version:
|
||||
@export VERSION_TAG=`python3 -c "from gitinspector.version import __version__; print(__version__)"` \
|
||||
&& git tag v$$VERSION_TAG
|
||||
|
||||
untag-version:
|
||||
@export VERSION_TAG=`python3 -c "from gitinspector.version import __version__; print(__version__)"` \
|
||||
&& git tag -d v$$VERSION_TAG
|
||||
|
||||
push-tagged-version: tag-version
|
||||
@export VERSION_TAG=`python3 -c "from gitinspector.version import __version__; print(__version__)"` \
|
||||
&& git push origin v$$VERSION_TAG
|
||||
|
||||
dist: clean ## builds source and wheel package
|
||||
python3 setup.py sdist
|
||||
python3 setup.py bdist_wheel
|
||||
ls -l dist
|
||||
|
||||
install: clean ## install the package to the active Python's site-packages
|
||||
python3 setup.py install
|
||||
|
||||
requirements:
|
||||
pipenv lock -r --dev > requirements.txt
|
18
Pipfile
Normal file
18
Pipfile
Normal file
|
@ -0,0 +1,18 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
black-but-with-tabs-instead-of-spaces = "*"
|
||||
pytest = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
flake8 = "*"
|
||||
twine = "*"
|
||||
coverage = "*"
|
||||
coveralls = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
556
Pipfile.lock
generated
Normal file
556
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,556 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "ab3a082c5a80fa8d8b3d938bec3237bd3f5ba9e400869f61cef243ec1c2cfdfc"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
|
||||
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
|
||||
],
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
|
||||
"sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==21.4.0"
|
||||
},
|
||||
"black-but-with-tabs-instead-of-spaces": {
|
||||
"hashes": [
|
||||
"sha256:01b00ac677000874b86c6f22efc965ab2cc16645a27b86b01bac2fed68a5a12e",
|
||||
"sha256:bd5dd0842cef0a2c6714bd7381c8ead9106f68c64c64c706679a6a7fabb7ba48"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.11"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
|
||||
"sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==8.0.4"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
|
||||
"sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
|
||||
],
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
|
||||
"sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
|
||||
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.7"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
|
||||
"sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0.1"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:04611cc0f627fc4a50bc4a9a2e6178a974c6a6a4aa9c1cca921635d2c47b9c87",
|
||||
"sha256:0b5d6f9aed3153487252d00a18e53f19b7f52a1651bc1d0c4b5844bc286dfa52",
|
||||
"sha256:0d2f5c3f7057530afd7b739ed42eb04f1011203bc5e4663e1e1d01bb50f813e3",
|
||||
"sha256:11772be1eb1748e0e197a40ffb82fb8fd0d6914cd147d841d9703e2bef24d288",
|
||||
"sha256:1333b3ce73269f986b1fa4d5d395643810074dc2de5b9d262eb258daf37dc98f",
|
||||
"sha256:16f81025bb3556eccb0681d7946e2b35ff254f9f888cff7d2120e8826330315c",
|
||||
"sha256:1a171eaac36a08964d023eeff740b18a415f79aeb212169080c170ec42dd5184",
|
||||
"sha256:1d6301f5288e9bdca65fab3de6b7de17362c5016d6bf8ee4ba4cbe833b2eda0f",
|
||||
"sha256:1e031899cb2bc92c0cf4d45389eff5b078d1936860a1be3aa8c94fa25fb46ed8",
|
||||
"sha256:1f8c0ae0a0de4e19fddaaff036f508db175f6f03db318c80bbc239a1def62d02",
|
||||
"sha256:2245441445099411b528379dee83e56eadf449db924648e5feb9b747473f42e3",
|
||||
"sha256:22709d701e7037e64dae2a04855021b62efd64a66c3ceed99dfd684bfef09e38",
|
||||
"sha256:24c89346734a4e4d60ecf9b27cac4c1fee3431a413f7aa00be7c4d7bbacc2c4d",
|
||||
"sha256:25716aa70a0d153cd844fe861d4f3315a6ccafce22b39d8aadbf7fcadff2b633",
|
||||
"sha256:2dacb3dae6b8cc579637a7b72f008bff50a94cde5e36e432352f4ca57b9e54c4",
|
||||
"sha256:34316bf693b1d2d29c087ee7e4bb10cdfa39da5f9c50fa15b07489b4ab93a1b5",
|
||||
"sha256:36b2d700a27e168fa96272b42d28c7ac3ff72030c67b32f37c05616ebd22a202",
|
||||
"sha256:37978254d9d00cda01acc1997513f786b6b971e57b778fbe7c20e30ae81a97f3",
|
||||
"sha256:38289f1690a7e27aacd049e420769b996826f3728756859420eeee21cc857118",
|
||||
"sha256:385ccf6d011b97768a640e9d4de25412204fbe8d6b9ae39ff115d4ff03f6fe5d",
|
||||
"sha256:3c7ea86b9ca83e30fa4d4cd0eaf01db3ebcc7b2726a25990966627e39577d729",
|
||||
"sha256:49810f907dfe6de8da5da7d2b238d343e6add62f01a15d03e2195afc180059ed",
|
||||
"sha256:519c0b3a6fbb68afaa0febf0d28f6c4b0a1074aefc484802ecb9709faf181607",
|
||||
"sha256:51f02ca184518702975b56affde6c573ebad4e411599005ce4468b1014b4786c",
|
||||
"sha256:552a39987ac6655dad4bf6f17dd2b55c7b0c6e949d933b8846d2e312ee80005a",
|
||||
"sha256:596f5ae2eeddb79b595583c2e0285312b2783b0ec759930c272dbf02f851ff75",
|
||||
"sha256:6014038f52b4b2ac1fa41a58d439a8a00f015b5c0735a0cd4b09afe344c94899",
|
||||
"sha256:61ebbcd208d78658b09e19c78920f1ad38936a0aa0f9c459c46c197d11c580a0",
|
||||
"sha256:6213713ac743b190ecbf3f316d6e41d099e774812d470422b3a0f137ea635832",
|
||||
"sha256:637e27ea1ebe4a561db75a880ac659ff439dec7f55588212e71700bb1ddd5af9",
|
||||
"sha256:6aa427c55a0abec450bca10b64446331b5ca8f79b648531138f357569705bc4a",
|
||||
"sha256:6ca45359d7a21644793de0e29de497ef7f1ae7268e346c4faf87b421fea364e6",
|
||||
"sha256:6db1b52c6f2c04fafc8da17ea506608e6be7086715dab498570c3e55e4f8fbd1",
|
||||
"sha256:752e7ddfb743344d447367baa85bccd3629c2c3940f70506eb5f01abce98ee68",
|
||||
"sha256:760c54ad1b8a9b81951030a7e8e7c3ec0964c1cb9fee585a03ff53d9e531bb8e",
|
||||
"sha256:768632fd8172ae03852e3245f11c8a425d95f65ff444ce46b3e673ae5b057b74",
|
||||
"sha256:7a0b9f6a1a15d494b35f25ed07abda03209fa76c33564c09c9e81d34f4b919d7",
|
||||
"sha256:7e070d3aef50ac3856f2ef5ec7214798453da878bb5e5a16c16a61edf1817cc3",
|
||||
"sha256:7e12949e5071c20ec49ef00c75121ed2b076972132fc1913ddf5f76cae8d10b4",
|
||||
"sha256:7e26eac9e52e8ce86f915fd33380f1b6896a2b51994e40bb094841e5003429b4",
|
||||
"sha256:85ffd6b1cb0dfb037ede50ff3bef80d9bf7fa60515d192403af6745524524f3b",
|
||||
"sha256:8618d9213a863c468a865e9d2ec50221015f7abf52221bc927152ef26c484b4c",
|
||||
"sha256:8acef4d8a4353f6678fd1035422a937c2170de58a2b29f7da045d5249e934101",
|
||||
"sha256:8d2f355a951f60f0843f2368b39970e4667517e54e86b1508e76f92b44811a8a",
|
||||
"sha256:90b6840b6448203228a9d8464a7a0d99aa8fa9f027ef95fe230579abaf8a6ee1",
|
||||
"sha256:9187500d83fd0cef4669385cbb0961e227a41c0c9bc39219044e35810793edf7",
|
||||
"sha256:93c20777a72cae8620203ac11c4010365706062aa13aaedd1a21bb07adbb9d5d",
|
||||
"sha256:93cce7d422a0093cfb3606beae38a8e47a25232eea0f292c878af580a9dc7605",
|
||||
"sha256:94c623c331a48a5ccc7d25271399aff29729fa202c737ae3b4b28b89d2b0976d",
|
||||
"sha256:97f32dc03a8054a4c4a5ab5d761ed4861e828b2c200febd4e46857069a483916",
|
||||
"sha256:9a2bf98ac92f58777c0fafc772bf0493e67fcf677302e0c0a630ee517a43b949",
|
||||
"sha256:a602bdc8607c99eb5b391592d58c92618dcd1537fdd87df1813f03fed49957a6",
|
||||
"sha256:a9d24b03daf7415f78abc2d25a208f234e2c585e5e6f92f0204d2ab7b9ab48e3",
|
||||
"sha256:abfcb0ef78df0ee9df4ea81f03beea41849340ce33a4c4bd4dbb99e23ec781b6",
|
||||
"sha256:b013f759cd69cb0a62de954d6d2096d648bc210034b79b1881406b07ed0a83f9",
|
||||
"sha256:b02e3e72665cd02afafb933453b0c9f6c59ff6e3708bd28d0d8580450e7e88af",
|
||||
"sha256:b52cc45e71657bc4743a5606d9023459de929b2a198d545868e11898ba1c3f59",
|
||||
"sha256:ba37f11e1d020969e8a779c06b4af866ffb6b854d7229db63c5fdddfceaa917f",
|
||||
"sha256:bb804c7d0bfbd7e3f33924ff49757de9106c44e27979e2492819c16972ec0da2",
|
||||
"sha256:bf594cc7cc9d528338d66674c10a5b25e3cde7dd75c3e96784df8f371d77a298",
|
||||
"sha256:c38baee6bdb7fe1b110b6b3aaa555e6e872d322206b7245aa39572d3fc991ee4",
|
||||
"sha256:c73d2166e4b210b73d1429c4f1ca97cea9cc090e5302df2a7a0a96ce55373f1c",
|
||||
"sha256:c9099bf89078675c372339011ccfc9ec310310bf6c292b413c013eb90ffdcafc",
|
||||
"sha256:cf0db26a1f76aa6b3aa314a74b8facd586b7a5457d05b64f8082a62c9c49582a",
|
||||
"sha256:d19a34f8a3429bd536996ad53597b805c10352a8561d8382e05830df389d2b43",
|
||||
"sha256:da80047524eac2acf7c04c18ac7a7da05a9136241f642dd2ed94269ef0d0a45a",
|
||||
"sha256:de2923886b5d3214be951bc2ce3f6b8ac0d6dfd4a0d0e2a4d2e5523d8046fdfb",
|
||||
"sha256:defa0652696ff0ba48c8aff5a1fac1eef1ca6ac9c660b047fc8e7623c4eb5093",
|
||||
"sha256:e54a1eb9fd38f2779e973d2f8958fd575b532fe26013405d1afb9ee2374e7ab8",
|
||||
"sha256:e5c31d70a478b0ca22a9d2d76d520ae996214019d39ed7dd93af872c7f301e52",
|
||||
"sha256:ebaeb93f90c0903233b11ce913a7cb8f6ee069158406e056f884854c737d2442",
|
||||
"sha256:ecfe51abf7f045e0b9cdde71ca9e153d11238679ef7b5da6c82093874adf3338",
|
||||
"sha256:f99112aed4fb7cee00c7f77e8b964a9b10f69488cdff626ffd797d02e2e4484f",
|
||||
"sha256:fd914db437ec25bfa410f8aa0aa2f3ba87cdfc04d9919d608d02330947afaeab"
|
||||
],
|
||||
"version": "==2022.1.18"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e",
|
||||
"sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344",
|
||||
"sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266",
|
||||
"sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a",
|
||||
"sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd",
|
||||
"sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d",
|
||||
"sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837",
|
||||
"sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098",
|
||||
"sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e",
|
||||
"sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27",
|
||||
"sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b",
|
||||
"sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596",
|
||||
"sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76",
|
||||
"sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30",
|
||||
"sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4",
|
||||
"sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78",
|
||||
"sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca",
|
||||
"sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985",
|
||||
"sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb",
|
||||
"sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88",
|
||||
"sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7",
|
||||
"sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5",
|
||||
"sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e",
|
||||
"sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.5.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
|
||||
"sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.1.1"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
|
||||
"sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==21.4.0"
|
||||
},
|
||||
"bleach": {
|
||||
"hashes": [
|
||||
"sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da",
|
||||
"sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
|
||||
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
|
||||
],
|
||||
"version": "==2021.10.8"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
|
||||
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.12"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
||||
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.4.4"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
|
||||
"sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d",
|
||||
"sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf",
|
||||
"sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7",
|
||||
"sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6",
|
||||
"sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4",
|
||||
"sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059",
|
||||
"sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39",
|
||||
"sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536",
|
||||
"sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac",
|
||||
"sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c",
|
||||
"sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903",
|
||||
"sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d",
|
||||
"sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05",
|
||||
"sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684",
|
||||
"sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1",
|
||||
"sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f",
|
||||
"sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7",
|
||||
"sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca",
|
||||
"sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad",
|
||||
"sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca",
|
||||
"sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d",
|
||||
"sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92",
|
||||
"sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4",
|
||||
"sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf",
|
||||
"sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6",
|
||||
"sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1",
|
||||
"sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4",
|
||||
"sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359",
|
||||
"sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3",
|
||||
"sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620",
|
||||
"sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512",
|
||||
"sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69",
|
||||
"sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2",
|
||||
"sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518",
|
||||
"sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0",
|
||||
"sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa",
|
||||
"sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4",
|
||||
"sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e",
|
||||
"sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1",
|
||||
"sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.3.2"
|
||||
},
|
||||
"coveralls": {
|
||||
"hashes": [
|
||||
"sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea",
|
||||
"sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"docopt": {
|
||||
"hashes": [
|
||||
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
|
||||
],
|
||||
"version": "==0.6.2"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c",
|
||||
"sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==0.18.1"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d",
|
||||
"sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==3.3"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:b36ffa925fe3139b2f6ff11d6925ffd4fa7bc47870165e3ac260ac7b4f91e6ac",
|
||||
"sha256:d16e8c1deb60de41b8e8ed21c1a7b947b0bc62fab7e1d470bcdf331cea2e6735"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.11.2"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
|
||||
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
|
||||
],
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"keyring": {
|
||||
"hashes": [
|
||||
"sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9",
|
||||
"sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.5.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.3"
|
||||
},
|
||||
"pkginfo": {
|
||||
"hashes": [
|
||||
"sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff",
|
||||
"sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc"
|
||||
],
|
||||
"version": "==1.8.2"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
|
||||
"sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20",
|
||||
"sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c",
|
||||
"sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65",
|
||||
"sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
|
||||
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.7"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
|
||||
"sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0.1"
|
||||
},
|
||||
"readme-renderer": {
|
||||
"hashes": [
|
||||
"sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d",
|
||||
"sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==32.0"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
|
||||
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==2.27.1"
|
||||
},
|
||||
"requests-toolbelt": {
|
||||
"hashes": [
|
||||
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
|
||||
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
|
||||
],
|
||||
"version": "==0.9.1"
|
||||
},
|
||||
"rfc3986": {
|
||||
"hashes": [
|
||||
"sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd",
|
||||
"sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"tqdm": {
|
||||
"hashes": [
|
||||
"sha256:1d9835ede8e394bb8c9dcbffbca02d717217113adc679236873eeaac5bc0b3cd",
|
||||
"sha256:e643e071046f17139dea55b880dc9b33822ce21613b4a4f5ea57f202833dbc29"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==4.63.0"
|
||||
},
|
||||
"twine": {
|
||||
"hashes": [
|
||||
"sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19",
|
||||
"sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.8.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
|
||||
"sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||
"version": "==1.26.8"
|
||||
},
|
||||
"webencodings": {
|
||||
"hashes": [
|
||||
"sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
|
||||
"sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
|
||||
],
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
|
||||
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.7.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,4 +53,4 @@ The Debian packages offered with releases of gitinspector are unofficial and ver
|
|||
An [npm](https://npmjs.com) package is provided for convenience as well. To install it globally, execute `npm i -g gitinspector`.
|
||||
|
||||
### License
|
||||
gitinspector is licensed under the *GNU GPL v3*. The gitinspector logo is partly based on the git logo; based on the work of Jason Long. The logo is licensed under the *Creative Commons Attribution 3.0 Unported License*.
|
||||
gitinspector is licensed under the *GNU GPL v3*. The gitinspector logo is partly based on the git logo; based on the work of Jason Long. The logo is licensed under the *Creative Commons Attribution 3.0 Unported License*.
|
|
@ -21,43 +21,45 @@ import os
|
|||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def get_basedir():
|
||||
if hasattr(sys, "frozen"): # exists when running via py2exe
|
||||
if hasattr(sys, "frozen"): # exists when running via py2exe
|
||||
return sys.prefix
|
||||
else:
|
||||
return os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def get_basedir_git(path=None):
|
||||
previous_directory = None
|
||||
|
||||
if path != None:
|
||||
if path is not None:
|
||||
previous_directory = os.getcwd()
|
||||
os.chdir(path)
|
||||
|
||||
bare_command = subprocess.Popen(["git", "rev-parse", "--is-bare-repository"], bufsize=1,
|
||||
stdout=subprocess.PIPE, stderr=open(os.devnull, "w"))
|
||||
bare_command = subprocess.Popen(
|
||||
["git", "rev-parse", "--is-bare-repository"], stdout=subprocess.PIPE, stderr=open(os.devnull, "w")
|
||||
)
|
||||
|
||||
isbare = bare_command.stdout.readlines()
|
||||
bare_command.wait()
|
||||
|
||||
if bare_command.returncode != 0:
|
||||
sys.exit(_("Error processing git repository at \"%s\"." % os.getcwd()))
|
||||
sys.exit(_('Error processing git repository at "%s".' % os.getcwd()))
|
||||
|
||||
isbare = (isbare[0].decode("utf-8", "replace").strip() == "true")
|
||||
isbare = isbare[0].decode("utf-8", "replace").strip() == "true"
|
||||
absolute_path = None
|
||||
|
||||
if isbare:
|
||||
absolute_path = subprocess.Popen(["git", "rev-parse", "--git-dir"], bufsize=1, stdout=subprocess.PIPE).stdout
|
||||
absolute_path = subprocess.Popen(["git", "rev-parse", "--git-dir"], stdout=subprocess.PIPE).stdout
|
||||
else:
|
||||
absolute_path = subprocess.Popen(["git", "rev-parse", "--show-toplevel"], bufsize=1,
|
||||
stdout=subprocess.PIPE).stdout
|
||||
absolute_path = subprocess.Popen(["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE).stdout
|
||||
|
||||
absolute_path = absolute_path.readlines()
|
||||
|
||||
if len(absolute_path) == 0:
|
||||
sys.exit(_("Unable to determine absolute path of git repository."))
|
||||
|
||||
if path != None:
|
||||
if path is not None:
|
||||
os.chdir(previous_directory)
|
||||
|
||||
return absolute_path[0].decode("utf-8", "replace").strip()
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import multiprocessing
|
||||
import re
|
||||
|
@ -30,19 +29,22 @@ from . import comment, extensions, filtering, format, interval, terminal
|
|||
|
||||
NUM_THREADS = multiprocessing.cpu_count()
|
||||
|
||||
class BlameEntry(object):
|
||||
|
||||
class BlameEntry():
|
||||
rows = 0
|
||||
skew = 0 # Used when calculating average code age.
|
||||
skew = 0 # Used when calculating average code age.
|
||||
comments = 0
|
||||
|
||||
|
||||
__thread_lock__ = threading.BoundedSemaphore(NUM_THREADS)
|
||||
__blame_lock__ = threading.Lock()
|
||||
|
||||
AVG_DAYS_PER_MONTH = 30.4167
|
||||
|
||||
|
||||
class BlameThread(threading.Thread):
|
||||
def __init__(self, useweeks, changes, blame_command, extension, blames, filename):
|
||||
__thread_lock__.acquire() # Lock controlling the number of threads running
|
||||
__thread_lock__.acquire() # Lock controlling the number of threads running
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.useweeks = useweeks
|
||||
|
@ -72,32 +74,35 @@ class BlameThread(threading.Thread):
|
|||
except KeyError:
|
||||
return
|
||||
|
||||
if not filtering.set_filtered(author, "author") and not \
|
||||
filtering.set_filtered(self.blamechunk_email, "email") and not \
|
||||
filtering.set_filtered(self.blamechunk_revision, "revision"):
|
||||
if (
|
||||
not filtering.set_filtered(author, "author")
|
||||
and not filtering.set_filtered(self.blamechunk_email, "email")
|
||||
and not filtering.set_filtered(self.blamechunk_revision, "revision")
|
||||
):
|
||||
|
||||
__blame_lock__.acquire() # Global lock used to protect calls from here...
|
||||
__blame_lock__.acquire() # Global lock used to protect calls from here...
|
||||
|
||||
if self.blames.get((author, self.filename), None) == None:
|
||||
if self.blames.get((author, self.filename), None) is None:
|
||||
self.blames[(author, self.filename)] = BlameEntry()
|
||||
|
||||
self.blames[(author, self.filename)].comments += comments
|
||||
self.blames[(author, self.filename)].rows += 1
|
||||
|
||||
if (self.blamechunk_time - self.changes.first_commit_date).days > 0:
|
||||
self.blames[(author, self.filename)].skew += ((self.changes.last_commit_date - self.blamechunk_time).days /
|
||||
(7.0 if self.useweeks else AVG_DAYS_PER_MONTH))
|
||||
self.blames[(author, self.filename)].skew += (self.changes.last_commit_date - self.blamechunk_time).days / (
|
||||
7.0 if self.useweeks else AVG_DAYS_PER_MONTH
|
||||
)
|
||||
|
||||
__blame_lock__.release() # ...to here.
|
||||
__blame_lock__.release() # ...to here.
|
||||
|
||||
def run(self):
|
||||
git_blame_r = subprocess.Popen(self.blame_command, bufsize=1, stdout=subprocess.PIPE).stdout
|
||||
git_blame_r = subprocess.Popen(self.blame_command, stdout=subprocess.PIPE).stdout
|
||||
rows = git_blame_r.readlines()
|
||||
git_blame_r.close()
|
||||
|
||||
self.__clear_blamechunk_info__()
|
||||
|
||||
#pylint: disable=W0201
|
||||
# pylint: disable=W0201
|
||||
for j in range(0, len(rows)):
|
||||
row = rows[j].decode("utf-8", "replace").strip()
|
||||
keyval = row.split(" ", 2)
|
||||
|
@ -116,36 +121,45 @@ class BlameThread(threading.Thread):
|
|||
elif Blame.is_revision(keyval[0]):
|
||||
self.blamechunk_revision = keyval[0]
|
||||
|
||||
__thread_lock__.release() # Lock controlling the number of threads running
|
||||
__thread_lock__.release() # Lock controlling the number of threads running
|
||||
|
||||
|
||||
PROGRESS_TEXT = N_("Checking how many rows belong to each author (2 of 2): {0:.0f}%")
|
||||
|
||||
class Blame(object):
|
||||
|
||||
class Blame():
|
||||
def __init__(self, repo, hard, useweeks, changes):
|
||||
self.blames = {}
|
||||
ls_tree_p = subprocess.Popen(["git", "ls-tree", "--name-only", "-r", interval.get_ref()], bufsize=1,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
ls_tree_p = subprocess.Popen(
|
||||
["git", "ls-tree", "--name-only", "-r", interval.get_ref()], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
lines = ls_tree_p.communicate()[0].splitlines()
|
||||
ls_tree_p.stdout.close()
|
||||
|
||||
if ls_tree_p.returncode == 0:
|
||||
progress_text = _(PROGRESS_TEXT)
|
||||
|
||||
if repo != None:
|
||||
if repo is not None:
|
||||
progress_text = "[%s] " % repo.name + progress_text
|
||||
|
||||
for i, row in enumerate(lines):
|
||||
row = row.strip().decode("unicode_escape", "ignore")
|
||||
row = row.encode("latin-1", "replace")
|
||||
row = row.decode("utf-8", "replace").strip("\"").strip("'").strip()
|
||||
row = row.decode("utf-8", "replace").strip('"').strip("'").strip()
|
||||
|
||||
if FileDiff.get_extension(row) in extensions.get_located() and \
|
||||
FileDiff.is_valid_extension(row) and not filtering.set_filtered(FileDiff.get_filename(row)):
|
||||
blame_command = filter(None, ["git", "blame", "--line-porcelain", "-w"] + \
|
||||
(["-C", "-C", "-M"] if hard else []) +
|
||||
[interval.get_since(), interval.get_ref(), "--", row])
|
||||
thread = BlameThread(useweeks, changes, blame_command, FileDiff.get_extension(row),
|
||||
self.blames, row.strip())
|
||||
if (
|
||||
FileDiff.get_extension(row) in extensions.get_located()
|
||||
and FileDiff.is_valid_extension(row)
|
||||
and not filtering.set_filtered(FileDiff.get_filename(row))
|
||||
):
|
||||
blame_command = [
|
||||
_f
|
||||
for _f in ["git", "blame", "--line-porcelain", "-w"]
|
||||
+ (["-C", "-C", "-M"] if hard else [])
|
||||
+ [interval.get_since(), interval.get_ref(), "--", row]
|
||||
if _f
|
||||
]
|
||||
thread = BlameThread(useweeks, changes, blame_command, FileDiff.get_extension(row), self.blames, row.strip())
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
|
@ -163,15 +177,15 @@ class Blame(object):
|
|||
def __iadd__(self, other):
|
||||
try:
|
||||
self.blames.update(other.blames)
|
||||
return self;
|
||||
return self
|
||||
except AttributeError:
|
||||
return other;
|
||||
return other
|
||||
|
||||
@staticmethod
|
||||
def is_revision(string):
|
||||
revision = re.search("([0-9a-f]{40})", string)
|
||||
|
||||
if revision == None:
|
||||
if revision is None:
|
||||
return False
|
||||
|
||||
return revision.group(1).strip()
|
||||
|
@ -190,8 +204,8 @@ class Blame(object):
|
|||
|
||||
def get_summed_blames(self):
|
||||
summed_blames = {}
|
||||
for i in self.blames.items():
|
||||
if summed_blames.get(i[0][0], None) == None:
|
||||
for i in list(self.blames.items()):
|
||||
if summed_blames.get(i[0][0], None) is None:
|
||||
summed_blames[i[0][0]] = BlameEntry()
|
||||
|
||||
summed_blames[i[0][0]].rows += i[1].rows
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import bisect
|
||||
import datetime
|
||||
import multiprocessing
|
||||
|
@ -34,7 +33,8 @@ NUM_THREADS = multiprocessing.cpu_count()
|
|||
__thread_lock__ = threading.BoundedSemaphore(NUM_THREADS)
|
||||
__changes_lock__ = threading.Lock()
|
||||
|
||||
class FileDiff(object):
|
||||
|
||||
class FileDiff():
|
||||
def __init__(self, string):
|
||||
commit_line = string.split("|")
|
||||
|
||||
|
@ -46,27 +46,28 @@ class FileDiff(object):
|
|||
@staticmethod
|
||||
def is_filediff_line(string):
|
||||
string = string.split("|")
|
||||
return string.__len__() == 2 and string[1].find("Bin") == -1 and ('+' in string[1] or '-' in string[1])
|
||||
return string.__len__() == 2 and string[1].find("Bin") == -1 and ("+" in string[1] or "-" in string[1])
|
||||
|
||||
@staticmethod
|
||||
def get_extension(string):
|
||||
string = string.split("|")[0].strip().strip("{}").strip("\"").strip("'")
|
||||
string = string.split("|")[0].strip().strip("{}").strip('"').strip("'")
|
||||
return os.path.splitext(string)[1][1:]
|
||||
|
||||
@staticmethod
|
||||
def get_filename(string):
|
||||
return string.split("|")[0].strip().strip("{}").strip("\"").strip("'")
|
||||
return string.split("|")[0].strip().strip("{}").strip('"').strip("'")
|
||||
|
||||
@staticmethod
|
||||
def is_valid_extension(string):
|
||||
extension = FileDiff.get_extension(string)
|
||||
|
||||
for i in extensions.get():
|
||||
if (extension == "" and i == "*") or extension == i or i == '**':
|
||||
if (extension == "" and i == "*") or extension == i or i == "**":
|
||||
return True
|
||||
return False
|
||||
|
||||
class Commit(object):
|
||||
|
||||
class Commit():
|
||||
def __init__(self, string):
|
||||
self.filediffs = []
|
||||
commit_line = string.split("|")
|
||||
|
@ -79,7 +80,7 @@ class Commit(object):
|
|||
self.email = commit_line[4].strip()
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.timestamp.__lt__(other.timestamp) # only used for sorting; we just consider the timestamp.
|
||||
return self.timestamp.__lt__(other.timestamp) # only used for sorting; we just consider the timestamp.
|
||||
|
||||
def add_filediff(self, filediff):
|
||||
self.filediffs.append(filediff)
|
||||
|
@ -98,15 +99,17 @@ class Commit(object):
|
|||
def is_commit_line(string):
|
||||
return string.split("|").__len__() == 5
|
||||
|
||||
class AuthorInfo(object):
|
||||
|
||||
class AuthorInfo():
|
||||
email = None
|
||||
insertions = 0
|
||||
deletions = 0
|
||||
commits = 0
|
||||
|
||||
|
||||
class ChangesThread(threading.Thread):
|
||||
def __init__(self, hard, changes, first_hash, second_hash, offset):
|
||||
__thread_lock__.acquire() # Lock controlling the number of threads running
|
||||
__thread_lock__.acquire() # Lock controlling the number of threads running
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.hard = hard
|
||||
|
@ -122,10 +125,27 @@ class ChangesThread(threading.Thread):
|
|||
thread.start()
|
||||
|
||||
def run(self):
|
||||
git_log_r = subprocess.Popen(filter(None, ["git", "log", "--reverse", "--pretty=%ct|%cd|%H|%aN|%aE",
|
||||
"--stat=100000,8192", "--no-merges", "-w", interval.get_since(),
|
||||
interval.get_until(), "--date=short"] + (["-C", "-C", "-M"] if self.hard else []) +
|
||||
[self.first_hash + self.second_hash]), bufsize=1, stdout=subprocess.PIPE).stdout
|
||||
git_log_r = subprocess.Popen(
|
||||
[
|
||||
_f
|
||||
for _f in [
|
||||
"git",
|
||||
"log",
|
||||
"--reverse",
|
||||
"--pretty=%ct|%cd|%H|%aN|%aE",
|
||||
"--stat=100000,8192",
|
||||
"--no-merges",
|
||||
"-w",
|
||||
interval.get_since(),
|
||||
interval.get_until(),
|
||||
"--date=short",
|
||||
]
|
||||
+ (["-C", "-C", "-M"] if self.hard else [])
|
||||
+ [self.first_hash + self.second_hash]
|
||||
if _f
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout
|
||||
lines = git_log_r.readlines()
|
||||
git_log_r.close()
|
||||
|
||||
|
@ -134,7 +154,7 @@ class ChangesThread(threading.Thread):
|
|||
is_filtered = False
|
||||
commits = []
|
||||
|
||||
__changes_lock__.acquire() # Global lock used to protect calls from here...
|
||||
__changes_lock__.acquire() # Global lock used to protect calls from here...
|
||||
|
||||
for i in lines:
|
||||
j = i.strip().decode("unicode_escape", "ignore")
|
||||
|
@ -154,15 +174,15 @@ class ChangesThread(threading.Thread):
|
|||
is_filtered = False
|
||||
commit = Commit(j)
|
||||
|
||||
if Commit.is_commit_line(j) and \
|
||||
(filtering.set_filtered(commit.author, "author") or \
|
||||
filtering.set_filtered(commit.email, "email") or \
|
||||
filtering.set_filtered(commit.sha, "revision") or \
|
||||
filtering.set_filtered(commit.sha, "message")):
|
||||
if Commit.is_commit_line(j) and (
|
||||
filtering.set_filtered(commit.author, "author")
|
||||
or filtering.set_filtered(commit.email, "email")
|
||||
or filtering.set_filtered(commit.sha, "revision")
|
||||
or filtering.set_filtered(commit.sha, "message")
|
||||
):
|
||||
is_filtered = True
|
||||
|
||||
if FileDiff.is_filediff_line(j) and not \
|
||||
filtering.set_filtered(FileDiff.get_filename(j)) and not is_filtered:
|
||||
if FileDiff.is_filediff_line(j) and not filtering.set_filtered(FileDiff.get_filename(j)) and not is_filtered:
|
||||
extensions.add_located(FileDiff.get_extension(j))
|
||||
|
||||
if FileDiff.is_valid_extension(j):
|
||||
|
@ -171,12 +191,14 @@ class ChangesThread(threading.Thread):
|
|||
commit.add_filediff(filediff)
|
||||
|
||||
self.changes.commits[self.offset // CHANGES_PER_THREAD] = commits
|
||||
__changes_lock__.release() # ...to here.
|
||||
__thread_lock__.release() # Lock controlling the number of threads running
|
||||
__changes_lock__.release() # ...to here.
|
||||
__thread_lock__.release() # Lock controlling the number of threads running
|
||||
|
||||
|
||||
PROGRESS_TEXT = N_("Fetching and calculating primary statistics (1 of 2): {0:.0f}%")
|
||||
|
||||
class Changes(object):
|
||||
|
||||
class Changes():
|
||||
authors = {}
|
||||
authors_dateinfo = {}
|
||||
authors_by_email = {}
|
||||
|
@ -184,16 +206,18 @@ class Changes(object):
|
|||
|
||||
def __init__(self, repo, hard):
|
||||
self.commits = []
|
||||
interval.set_ref("HEAD");
|
||||
git_rev_list_p = subprocess.Popen(filter(None, ["git", "rev-list", "--reverse", "--no-merges",
|
||||
interval.get_since(), interval.get_until(), "HEAD"]), bufsize=1,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
interval.set_ref("HEAD")
|
||||
git_rev_list_p = subprocess.Popen(
|
||||
[_f for _f in ["git", "rev-list", "--reverse", "--no-merges", interval.get_since(), interval.get_until(), "HEAD"] if _f],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
lines = git_rev_list_p.communicate()[0].splitlines()
|
||||
git_rev_list_p.stdout.close()
|
||||
|
||||
if git_rev_list_p.returncode == 0 and len(lines) > 0:
|
||||
progress_text = _(PROGRESS_TEXT)
|
||||
if repo != None:
|
||||
if repo is not None:
|
||||
progress_text = "[%s] " % repo.name + progress_text
|
||||
|
||||
chunks = len(lines) // CHANGES_PER_THREAD
|
||||
|
@ -229,10 +253,12 @@ class Changes(object):
|
|||
if interval.has_interval():
|
||||
interval.set_ref(self.commits[-1].sha)
|
||||
|
||||
self.first_commit_date = datetime.date(int(self.commits[0].date[0:4]), int(self.commits[0].date[5:7]),
|
||||
int(self.commits[0].date[8:10]))
|
||||
self.last_commit_date = datetime.date(int(self.commits[-1].date[0:4]), int(self.commits[-1].date[5:7]),
|
||||
int(self.commits[-1].date[8:10]))
|
||||
self.first_commit_date = datetime.date(
|
||||
int(self.commits[0].date[0:4]), int(self.commits[0].date[5:7]), int(self.commits[0].date[8:10])
|
||||
)
|
||||
self.last_commit_date = datetime.date(
|
||||
int(self.commits[-1].date[0:4]), int(self.commits[-1].date[5:7]), int(self.commits[-1].date[8:10])
|
||||
)
|
||||
|
||||
def __iadd__(self, other):
|
||||
try:
|
||||
|
@ -255,7 +281,7 @@ class Changes(object):
|
|||
|
||||
@staticmethod
|
||||
def modify_authorinfo(authors, key, commit):
|
||||
if authors.get(key, None) == None:
|
||||
if authors.get(key, None) is None:
|
||||
authors[key] = AuthorInfo()
|
||||
|
||||
if commit.get_filediffs():
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
@ -27,22 +27,28 @@ import tempfile
|
|||
try:
|
||||
from urllib.parse import urlparse
|
||||
except:
|
||||
from urlparse import urlparse
|
||||
from urllib.parse import urlparse
|
||||
|
||||
__cloned_paths__ = []
|
||||
|
||||
|
||||
def create(url):
|
||||
class Repository(object):
|
||||
class Repository():
|
||||
def __init__(self, name, location):
|
||||
self.name = name
|
||||
self.location = location
|
||||
|
||||
parsed_url = urlparse(url)
|
||||
|
||||
if parsed_url.scheme == "file" or parsed_url.scheme == "git" or parsed_url.scheme == "http" or \
|
||||
parsed_url.scheme == "https" or parsed_url.scheme == "ssh":
|
||||
if (
|
||||
parsed_url.scheme == "file"
|
||||
or parsed_url.scheme == "git"
|
||||
or parsed_url.scheme == "http"
|
||||
or parsed_url.scheme == "https"
|
||||
or parsed_url.scheme == "ssh"
|
||||
):
|
||||
path = tempfile.mkdtemp(suffix=".gitinspector")
|
||||
git_clone = subprocess.Popen(["git", "clone", url, path], bufsize=1, stdout=sys.stderr)
|
||||
git_clone = subprocess.Popen(["git", "clone", url, path], stdout=sys.stderr)
|
||||
git_clone.wait()
|
||||
|
||||
if git_clone.returncode != 0:
|
||||
|
@ -53,6 +59,7 @@ def create(url):
|
|||
|
||||
return Repository(None, os.path.abspath(url))
|
||||
|
||||
|
||||
def delete():
|
||||
for path in __cloned_paths__:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
|
|
@ -17,51 +17,129 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__comment_begining__ = {"java": "/*", "c": "/*", "cc": "/*", "cpp": "/*", "cs": "/*", "h": "/*", "hh": "/*", "hpp": "/*",
|
||||
"hs": "{-", "html": "<!--", "php": "/*", "py": "\"\"\"", "glsl": "/*", "rb": "=begin", "js": "/*",
|
||||
"jspx": "<!--", "scala": "/*", "sql": "/*", "tex": "\\begin{comment}", "xhtml": "<!--",
|
||||
"xml": "<!--", "ml": "(*", "mli": "(*", "go": "/*", "ly": "%{", "ily": "%{"}
|
||||
__comment_begining__ = {
|
||||
"java": "/*",
|
||||
"c": "/*",
|
||||
"cc": "/*",
|
||||
"cpp": "/*",
|
||||
"cs": "/*",
|
||||
"h": "/*",
|
||||
"hh": "/*",
|
||||
"hpp": "/*",
|
||||
"hs": "{-",
|
||||
"html": "<!--",
|
||||
"php": "/*",
|
||||
"py": '"""',
|
||||
"glsl": "/*",
|
||||
"rb": "=begin",
|
||||
"js": "/*",
|
||||
"jspx": "<!--",
|
||||
"scala": "/*",
|
||||
"sql": "/*",
|
||||
"tex": "\\begin{comment}",
|
||||
"xhtml": "<!--",
|
||||
"xml": "<!--",
|
||||
"ml": "(*",
|
||||
"mli": "(*",
|
||||
"go": "/*",
|
||||
"ly": "%{",
|
||||
"ily": "%{",
|
||||
}
|
||||
|
||||
__comment_end__ = {"java": "*/", "c": "*/", "cc": "*/", "cpp": "*/", "cs": "*/", "h": "*/", "hh": "*/", "hpp": "*/", "hs": "-}",
|
||||
"html": "-->", "php": "*/", "py": "\"\"\"", "glsl": "*/", "rb": "=end", "js": "*/", "jspx": "-->",
|
||||
"scala": "*/", "sql": "*/", "tex": "\\end{comment}", "xhtml": "-->", "xml": "-->", "ml": "*)", "mli": "*)",
|
||||
"go": "*/", "ly": "%}", "ily": "%}"}
|
||||
__comment_end__ = {
|
||||
"java": "*/",
|
||||
"c": "*/",
|
||||
"cc": "*/",
|
||||
"cpp": "*/",
|
||||
"cs": "*/",
|
||||
"h": "*/",
|
||||
"hh": "*/",
|
||||
"hpp": "*/",
|
||||
"hs": "-}",
|
||||
"html": "-->",
|
||||
"php": "*/",
|
||||
"py": '"""',
|
||||
"glsl": "*/",
|
||||
"rb": "=end",
|
||||
"js": "*/",
|
||||
"jspx": "-->",
|
||||
"scala": "*/",
|
||||
"sql": "*/",
|
||||
"tex": "\\end{comment}",
|
||||
"xhtml": "-->",
|
||||
"xml": "-->",
|
||||
"ml": "*)",
|
||||
"mli": "*)",
|
||||
"go": "*/",
|
||||
"ly": "%}",
|
||||
"ily": "%}",
|
||||
}
|
||||
|
||||
__comment__ = {"java": "//", "c": "//", "cc": "//", "cpp": "//", "cs": "//", "h": "//", "hh": "//", "hpp": "//", "hs": "--",
|
||||
"pl": "#", "php": "//", "py": "#", "glsl": "//", "rb": "#", "robot": "#", "rs": "//", "rlib": "//", "js": "//",
|
||||
"scala": "//", "sql": "--", "tex": "%", "ada": "--", "ads": "--", "adb": "--", "pot": "#", "po": "#", "go": "//",
|
||||
"ly": "%", "ily": "%"}
|
||||
__comment__ = {
|
||||
"java": "//",
|
||||
"c": "//",
|
||||
"cc": "//",
|
||||
"cpp": "//",
|
||||
"cs": "//",
|
||||
"h": "//",
|
||||
"hh": "//",
|
||||
"hpp": "//",
|
||||
"hs": "--",
|
||||
"pl": "#",
|
||||
"php": "//",
|
||||
"py": "#",
|
||||
"glsl": "//",
|
||||
"rb": "#",
|
||||
"robot": "#",
|
||||
"rs": "//",
|
||||
"rlib": "//",
|
||||
"js": "//",
|
||||
"scala": "//",
|
||||
"sql": "--",
|
||||
"tex": "%",
|
||||
"ada": "--",
|
||||
"ads": "--",
|
||||
"adb": "--",
|
||||
"pot": "#",
|
||||
"po": "#",
|
||||
"go": "//",
|
||||
"ly": "%",
|
||||
"ily": "%",
|
||||
}
|
||||
|
||||
__comment_markers_must_be_at_begining__ = {"tex": True}
|
||||
|
||||
|
||||
def __has_comment_begining__(extension, string):
|
||||
if __comment_markers_must_be_at_begining__.get(extension, None) == True:
|
||||
if __comment_markers_must_be_at_begining__.get(extension, None):
|
||||
return string.find(__comment_begining__[extension]) == 0
|
||||
elif __comment_begining__.get(extension, None) != None and string.find(__comment_end__[extension], 2) == -1:
|
||||
elif __comment_begining__.get(extension, None) is not None and string.find(__comment_end__[extension], 2) == -1:
|
||||
return string.find(__comment_begining__[extension]) != -1
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def __has_comment_end__(extension, string):
|
||||
if __comment_markers_must_be_at_begining__.get(extension, None) == True:
|
||||
if __comment_markers_must_be_at_begining__.get(extension, None):
|
||||
return string.find(__comment_end__[extension]) == 0
|
||||
elif __comment_end__.get(extension, None) != None:
|
||||
elif __comment_end__.get(extension, None) is not None:
|
||||
return string.find(__comment_end__[extension]) != -1
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_comment(extension, string):
|
||||
if __comment_begining__.get(extension, None) != None and string.strip().startswith(__comment_begining__[extension]):
|
||||
if __comment_begining__.get(extension, None) is not None and string.strip().startswith(__comment_begining__[extension]):
|
||||
return True
|
||||
if __comment_end__.get(extension, None) != None and string.strip().endswith(__comment_end__[extension]):
|
||||
if __comment_end__.get(extension, None) is not None and string.strip().endswith(__comment_end__[extension]):
|
||||
return True
|
||||
if __comment__.get(extension, None) != None and string.strip().startswith(__comment__[extension]):
|
||||
if __comment__.get(extension, None) is not None and string.strip().startswith(__comment__[extension]):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def handle_comment_block(is_inside_comment, extension, content):
|
||||
comments = 0
|
||||
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from . import extensions, filtering, format, interval, optval
|
||||
|
||||
class GitConfig(object):
|
||||
|
||||
class GitConfig():
|
||||
def __init__(self, run, repo, global_only=False):
|
||||
self.run = run
|
||||
self.repo = repo
|
||||
|
@ -31,8 +32,10 @@ class GitConfig(object):
|
|||
def __read_git_config__(self, variable):
|
||||
previous_directory = os.getcwd()
|
||||
os.chdir(self.repo)
|
||||
setting = subprocess.Popen(filter(None, ["git", "config", "--global" if self.global_only else "",
|
||||
"inspector." + variable]), bufsize=1, stdout=subprocess.PIPE).stdout
|
||||
setting = subprocess.Popen(
|
||||
[_f for _f in ["git", "config", "--global" if self.global_only else "", "inspector." + variable] if _f],
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout
|
||||
os.chdir(previous_directory)
|
||||
|
||||
try:
|
||||
|
|
|
@ -17,25 +17,28 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
DEFAULT_EXTENSIONS = ["java", "c", "cc", "cpp", "h", "hh", "hpp", "py", "glsl", "rb", "js", "sql"]
|
||||
DEFAULT_EXTENSIONS = ["java", "c", "cc", "cpp", "h", "hh", "hpp", "py", "glsl", "rb", "js", "sql", "go"]
|
||||
|
||||
__extensions__ = DEFAULT_EXTENSIONS
|
||||
__located_extensions__ = set()
|
||||
|
||||
|
||||
def get():
|
||||
return __extensions__
|
||||
|
||||
|
||||
def define(string):
|
||||
global __extensions__
|
||||
__extensions__ = string.split(",")
|
||||
|
||||
|
||||
def add_located(string):
|
||||
if len(string) == 0:
|
||||
__located_extensions__.add("*")
|
||||
else:
|
||||
__located_extensions__.add(string)
|
||||
|
||||
|
||||
def get_located():
|
||||
return __located_extensions__
|
||||
|
|
|
@ -17,49 +17,63 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
__filters__ = {"file": [set(), set()], "author": [set(), set()], "email": [set(), set()], "revision": [set(), set()],
|
||||
"message" : [set(), None]}
|
||||
__filters__ = {
|
||||
"file": [set(), set()],
|
||||
"author": [set(), set()],
|
||||
"email": [set(), set()],
|
||||
"revision": [set(), set()],
|
||||
"message": [set(), None],
|
||||
}
|
||||
|
||||
|
||||
class InvalidRegExpError(ValueError):
|
||||
def __init__(self, msg):
|
||||
super(InvalidRegExpError, self).__init__(msg)
|
||||
self.msg = msg
|
||||
|
||||
|
||||
def get():
|
||||
return __filters__
|
||||
|
||||
|
||||
def __add_one__(string):
|
||||
for i in __filters__:
|
||||
if (i + ":").lower() == string[0:len(i) + 1].lower():
|
||||
__filters__[i][0].add(string[len(i) + 1:])
|
||||
if (i + ":").lower() == string[0 : len(i) + 1].lower():
|
||||
__filters__[i][0].add(string[len(i) + 1 :])
|
||||
return
|
||||
__filters__["file"][0].add(string)
|
||||
|
||||
|
||||
def add(string):
|
||||
rules = string.split(",")
|
||||
for rule in rules:
|
||||
__add_one__(rule)
|
||||
|
||||
|
||||
def clear():
|
||||
for i in __filters__:
|
||||
__filters__[i][0] = set()
|
||||
|
||||
|
||||
def get_filered(filter_type="file"):
|
||||
return __filters__[filter_type][1]
|
||||
|
||||
|
||||
def has_filtered():
|
||||
for i in __filters__:
|
||||
if __filters__[i][1]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def __find_commit_message__(sha):
|
||||
git_show_r = subprocess.Popen(filter(None, ["git", "show", "-s", "--pretty=%B", "-w", sha]), bufsize=1,
|
||||
stdout=subprocess.PIPE).stdout
|
||||
git_show_r = subprocess.Popen(
|
||||
[_f for _f in ["git", "show", "-s", "--pretty=%B", "-w", sha] if _f], stdout=subprocess.PIPE
|
||||
).stdout
|
||||
|
||||
commit_message = git_show_r.read()
|
||||
git_show_r.close()
|
||||
|
@ -68,6 +82,7 @@ def __find_commit_message__(sha):
|
|||
commit_message = commit_message.encode("latin-1", "replace")
|
||||
return commit_message.decode("utf-8", "replace")
|
||||
|
||||
|
||||
def set_filtered(string, filter_type="file"):
|
||||
string = string.strip()
|
||||
|
||||
|
@ -78,7 +93,7 @@ def set_filtered(string, filter_type="file"):
|
|||
if filter_type == "message":
|
||||
search_for = __find_commit_message__(string)
|
||||
try:
|
||||
if re.search(i, search_for) != None:
|
||||
if re.search(i, search_for) is not None:
|
||||
if filter_type == "message":
|
||||
__add_one__("revision:" + string)
|
||||
else:
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import os
|
||||
import textwrap
|
||||
|
@ -33,23 +32,28 @@ DEFAULT_FORMAT = __available_formats__[3]
|
|||
|
||||
__selected_format__ = DEFAULT_FORMAT
|
||||
|
||||
|
||||
class InvalidFormatError(Exception):
|
||||
def __init__(self, msg):
|
||||
super(InvalidFormatError, self).__init__(msg)
|
||||
self.msg = msg
|
||||
|
||||
|
||||
def select(format):
|
||||
global __selected_format__
|
||||
__selected_format__ = format
|
||||
|
||||
return format in __available_formats__
|
||||
|
||||
|
||||
def get_selected():
|
||||
return __selected_format__
|
||||
|
||||
|
||||
def is_interactive_format():
|
||||
return __selected_format__ == "text"
|
||||
|
||||
|
||||
def __output_html_template__(name):
|
||||
template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), name)
|
||||
file_r = open(template_path, "rb")
|
||||
|
@ -58,6 +62,7 @@ def __output_html_template__(name):
|
|||
file_r.close()
|
||||
return template
|
||||
|
||||
|
||||
def __get_zip_file_content__(name, file_name="/html/flot.zip"):
|
||||
zip_file = zipfile.ZipFile(basedir.get_basedir() + file_name, "r")
|
||||
content = zip_file.read(name)
|
||||
|
@ -65,17 +70,20 @@ def __get_zip_file_content__(name, file_name="/html/flot.zip"):
|
|||
zip_file.close()
|
||||
return content.decode("utf-8", "replace")
|
||||
|
||||
|
||||
INFO_ONE_REPOSITORY = N_("Statistical information for the repository '{0}' was gathered on {1}.")
|
||||
INFO_MANY_REPOSITORIES = N_("Statistical information for the repositories '{0}' was gathered on {1}.")
|
||||
|
||||
|
||||
def output_header(repos):
|
||||
repos_string = ", ".join([repo.name for repo in repos])
|
||||
|
||||
if __selected_format__ == "html" or __selected_format__ == "htmlembedded":
|
||||
base = basedir.get_basedir()
|
||||
html_header = __output_html_template__(base + "/html/html.header")
|
||||
tablesorter_js = __get_zip_file_content__("jquery.tablesorter.min.js",
|
||||
"/html/jquery.tablesorter.min.js.zip").encode("latin-1", "replace")
|
||||
tablesorter_js = __get_zip_file_content__("jquery.tablesorter.min.js", "/html/jquery.tablesorter.min.js.zip").encode(
|
||||
"latin-1", "replace"
|
||||
)
|
||||
tablesorter_js = tablesorter_js.decode("utf-8", "ignore")
|
||||
flot_js = __get_zip_file_content__("jquery.flot.js")
|
||||
pie_js = __get_zip_file_content__("jquery.flot.pie.js")
|
||||
|
@ -89,40 +97,44 @@ def output_header(repos):
|
|||
if __selected_format__ == "htmlembedded":
|
||||
jquery_js = ">" + __get_zip_file_content__("jquery.js")
|
||||
else:
|
||||
jquery_js = " src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\">"
|
||||
jquery_js = ' src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">'
|
||||
|
||||
print(html_header.format(title=_("Repository statistics for '{0}'").format(repos_string),
|
||||
jquery=jquery_js,
|
||||
jquery_tablesorter=tablesorter_js,
|
||||
jquery_flot=flot_js,
|
||||
jquery_flot_pie=pie_js,
|
||||
jquery_flot_resize=resize_js,
|
||||
logo=logo.decode("utf-8", "replace"),
|
||||
logo_text=_("The output has been generated by {0} {1}. The statistical analysis tool"
|
||||
" for git repositories.").format(
|
||||
"<a href=\"https://github.com/ejwa/gitinspector\">gitinspector</a>",
|
||||
version.__version__),
|
||||
repo_text=_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format(
|
||||
repos_string, localization.get_date()),
|
||||
show_minor_authors=_("Show minor authors"),
|
||||
hide_minor_authors=_("Hide minor authors"),
|
||||
show_minor_rows=_("Show rows with minor work"),
|
||||
hide_minor_rows=_("Hide rows with minor work")))
|
||||
print(
|
||||
html_header.format(
|
||||
title=_("Repository statistics for '{0}'").format(repos_string),
|
||||
jquery=jquery_js,
|
||||
jquery_tablesorter=tablesorter_js,
|
||||
jquery_flot=flot_js,
|
||||
jquery_flot_pie=pie_js,
|
||||
jquery_flot_resize=resize_js,
|
||||
logo=logo.decode("utf-8", "replace"),
|
||||
logo_text=_("The output has been generated by {0} {1}. The statistical analysis tool" " for git repositories.").format(
|
||||
'<a href="https://github.com/ejwa/gitinspector">gitinspector</a>', version.__version__
|
||||
),
|
||||
repo_text=_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format(
|
||||
repos_string, localization.get_date()
|
||||
),
|
||||
show_minor_authors=_("Show minor authors"),
|
||||
hide_minor_authors=_("Hide minor authors"),
|
||||
show_minor_rows=_("Show rows with minor work"),
|
||||
hide_minor_rows=_("Hide rows with minor work"),
|
||||
)
|
||||
)
|
||||
elif __selected_format__ == "json":
|
||||
print("{\n\t\"gitinspector\": {")
|
||||
print("\t\t\"version\": \"" + version.__version__ + "\",")
|
||||
print('{\n\t"gitinspector": {')
|
||||
print('\t\t"version": "' + version.__version__ + '",')
|
||||
|
||||
if len(repos) <= 1:
|
||||
print("\t\t\"repository\": \"" + repos_string + "\",")
|
||||
print('\t\t"repository": "' + repos_string + '",')
|
||||
else:
|
||||
repos_json = "\t\t\"repositories\": [ "
|
||||
repos_json = '\t\t"repositories": [ '
|
||||
|
||||
for repo in repos:
|
||||
repos_json += "\"" + repo.name + "\", "
|
||||
repos_json += '"' + repo.name + '", '
|
||||
|
||||
print(repos_json[:-2] + " ],")
|
||||
|
||||
print("\t\t\"report_date\": \"" + time.strftime("%Y/%m/%d") + "\",")
|
||||
print('\t\t"report_date": "' + time.strftime("%Y/%m/%d") + '",')
|
||||
|
||||
elif __selected_format__ == "xml":
|
||||
print("<gitinspector>")
|
||||
|
@ -140,8 +152,13 @@ def output_header(repos):
|
|||
|
||||
print("\t<report-date>" + time.strftime("%Y/%m/%d") + "</report-date>")
|
||||
else:
|
||||
print(textwrap.fill(_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format(
|
||||
repos_string, localization.get_date()), width=terminal.get_size()[0]))
|
||||
print(
|
||||
textwrap.fill(
|
||||
_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format(repos_string, localization.get_date()),
|
||||
width=terminal.get_size()[0],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def output_footer():
|
||||
if __selected_format__ == "html" or __selected_format__ == "htmlembedded":
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import atexit
|
||||
import getopt
|
||||
import os
|
||||
|
@ -27,8 +26,7 @@ from .blame import Blame
|
|||
from .changes import Changes
|
||||
from .config import GitConfig
|
||||
from .metrics import MetricsLogic
|
||||
from . import (basedir, clone, extensions, filtering, format, help, interval,
|
||||
localization, optval, terminal, version)
|
||||
from . import basedir, clone, extensions, filtering, format, help, interval, localization, optval, terminal, version
|
||||
from .output import outputable
|
||||
from .output.blameoutput import BlameOutput
|
||||
from .output.changesoutput import ChangesOutput
|
||||
|
@ -40,7 +38,8 @@ from .output.timelineoutput import TimelineOutput
|
|||
|
||||
localization.init()
|
||||
|
||||
class Runner(object):
|
||||
|
||||
class Runner():
|
||||
def __init__(self):
|
||||
self.hard = False
|
||||
self.include_metrics = False
|
||||
|
@ -102,10 +101,12 @@ class Runner(object):
|
|||
format.output_footer()
|
||||
os.chdir(previous_directory)
|
||||
|
||||
|
||||
def __check_python_version__():
|
||||
if sys.version_info < (2, 6):
|
||||
if sys.version_info < (3, 6):
|
||||
python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1])
|
||||
sys.exit(_("gitinspector requires at least Python 2.6 to run (version {0} was found).").format(python_version))
|
||||
sys.exit(_("gitinspector requires at least Python 3.6 to run (version {0} was found).").format(python_version))
|
||||
|
||||
|
||||
def __get_validated_git_repos__(repos_relative):
|
||||
if not repos_relative:
|
||||
|
@ -113,11 +114,11 @@ def __get_validated_git_repos__(repos_relative):
|
|||
|
||||
repos = []
|
||||
|
||||
#Try to clone the repos or return the same directory and bail out.
|
||||
# Try to clone the repos or return the same directory and bail out.
|
||||
for repo in repos_relative:
|
||||
cloned_repo = clone.create(repo)
|
||||
|
||||
if cloned_repo.name == None:
|
||||
if cloned_repo.name is None:
|
||||
cloned_repo.location = basedir.get_basedir_git(cloned_repo.location)
|
||||
cloned_repo.name = os.path.basename(cloned_repo.location)
|
||||
|
||||
|
@ -125,31 +126,49 @@ def __get_validated_git_repos__(repos_relative):
|
|||
|
||||
return repos
|
||||
|
||||
def main():
|
||||
|
||||
def main(argv=None):
|
||||
terminal.check_terminal_encoding()
|
||||
terminal.set_stdin_encoding()
|
||||
argv = terminal.convert_command_line_to_utf8()
|
||||
argv = terminal.convert_command_line_to_utf8() if argv is None else argv
|
||||
run = Runner()
|
||||
repos = []
|
||||
|
||||
try:
|
||||
opts, args = optval.gnu_getopt(argv[1:], "f:F:hHlLmrTwx:", ["exclude=", "file-types=", "format=",
|
||||
"hard:true", "help", "list-file-types:true", "localize-output:true",
|
||||
"metrics:true", "responsibilities:true", "since=", "grading:true",
|
||||
"timeline:true", "until=", "version", "weeks:true"])
|
||||
opts, args = optval.gnu_getopt(
|
||||
argv[1:],
|
||||
"f:F:hHlLmrTwx:",
|
||||
[
|
||||
"exclude=",
|
||||
"file-types=",
|
||||
"format=",
|
||||
"hard:true",
|
||||
"help",
|
||||
"list-file-types:true",
|
||||
"localize-output:true",
|
||||
"metrics:true",
|
||||
"responsibilities:true",
|
||||
"since=",
|
||||
"grading:true",
|
||||
"timeline:true",
|
||||
"until=",
|
||||
"version",
|
||||
"weeks:true",
|
||||
],
|
||||
)
|
||||
repos = __get_validated_git_repos__(set(args))
|
||||
|
||||
#We need the repos above to be set before we read the git config.
|
||||
# We need the repos above to be set before we read the git config.
|
||||
GitConfig(run, repos[-1].location).read()
|
||||
clear_x_on_next_pass = True
|
||||
|
||||
for o, a in opts:
|
||||
if o in("-h", "--help"):
|
||||
if o in ("-h", "--help"):
|
||||
help.output()
|
||||
sys.exit(0)
|
||||
elif o in("-f", "--file-types"):
|
||||
elif o in ("-f", "--file-types"):
|
||||
extensions.define(a)
|
||||
elif o in("-F", "--format"):
|
||||
elif o in ("-F", "--format"):
|
||||
if not format.select(a):
|
||||
raise format.InvalidFormatError(_("specified output format not supported."))
|
||||
elif o == "-H":
|
||||
|
@ -196,7 +215,7 @@ def main():
|
|||
run.useweeks = True
|
||||
elif o == "--weeks":
|
||||
run.useweeks = optval.get_boolean_argument(a)
|
||||
elif o in("-x", "--exclude"):
|
||||
elif o in ("-x", "--exclude"):
|
||||
if clear_x_on_next_pass:
|
||||
clear_x_on_next_pass = False
|
||||
filtering.clear()
|
||||
|
@ -210,9 +229,11 @@ def main():
|
|||
print(_("Try `{0} --help' for more information.").format(sys.argv[0]), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
@atexit.register
|
||||
def cleanup():
|
||||
clone.delete()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -17,16 +17,17 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
except:
|
||||
from urllib import urlencode
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from . import format
|
||||
|
||||
|
||||
def get_url(email, size=20):
|
||||
md5hash = hashlib.md5(email.encode("utf-8").lower().strip()).hexdigest()
|
||||
base_url = "https://www.gravatar.com/avatar/" + md5hash
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
from .extensions import DEFAULT_EXTENSIONS
|
||||
from .format import __available_formats__
|
||||
|
||||
|
||||
__doc__ = _("""Usage: {0} [OPTION]... [REPOSITORY]...
|
||||
__doc__ = _(
|
||||
"""Usage: {0} [OPTION]... [REPOSITORY]...
|
||||
List information about the repository in REPOSITORY. If no repository is
|
||||
specified, the current directory is used. If multiple repositories are
|
||||
given, information will be merged into a unified statistical report.
|
||||
|
@ -76,7 +76,9 @@ add or remove one of the specified extensions, see -f or --file-types for
|
|||
more information.
|
||||
|
||||
gitinspector requires that the git executable is available in your PATH.
|
||||
Report gitinspector bugs to gitinspector@ejwa.se.""")
|
||||
Report gitinspector bugs to gitinspector@ejwa.se."""
|
||||
)
|
||||
|
||||
|
||||
def output():
|
||||
print(__doc__.format(sys.argv[0], ",".join(DEFAULT_EXTENSIONS), ",".join(__available_formats__)))
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
try:
|
||||
from shlex import quote
|
||||
|
@ -30,26 +29,33 @@ __until__ = ""
|
|||
|
||||
__ref__ = "HEAD"
|
||||
|
||||
|
||||
def has_interval():
|
||||
return __since__ + __until__ != ""
|
||||
|
||||
|
||||
def get_since():
|
||||
return __since__
|
||||
|
||||
|
||||
def set_since(since):
|
||||
global __since__
|
||||
__since__ = "--since=" + quote(since)
|
||||
|
||||
|
||||
def get_until():
|
||||
return __until__
|
||||
|
||||
|
||||
def set_until(until):
|
||||
global __until__
|
||||
__until__ = "--until=" + quote(until)
|
||||
|
||||
|
||||
def get_ref():
|
||||
return __ref__
|
||||
|
||||
|
||||
def set_ref(ref):
|
||||
global __ref__
|
||||
__ref__ = ref
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import gettext
|
||||
import locale
|
||||
import os
|
||||
|
@ -31,10 +30,12 @@ __enabled__ = False
|
|||
__installed__ = False
|
||||
__translation__ = None
|
||||
|
||||
#Dummy function used to handle string constants
|
||||
|
||||
# Dummy function used to handle string constants
|
||||
def N_(message):
|
||||
return message
|
||||
|
||||
|
||||
def init():
|
||||
global __enabled__
|
||||
global __installed__
|
||||
|
@ -48,12 +49,12 @@ def init():
|
|||
else:
|
||||
lang = locale.getlocale()
|
||||
|
||||
#Fix for non-POSIX-compliant systems (Windows et al.).
|
||||
if os.getenv('LANG') is None:
|
||||
# Fix for non-POSIX-compliant systems (Windows et al.).
|
||||
if os.getenv("LANG") is None:
|
||||
lang = locale.getdefaultlocale()
|
||||
|
||||
if lang[0]:
|
||||
os.environ['LANG'] = lang[0]
|
||||
os.environ["LANG"] = lang[0]
|
||||
|
||||
if lang[0] is not None:
|
||||
filename = basedir.get_basedir() + "/translations/messages_%s.mo" % lang[0][0:2]
|
||||
|
@ -68,7 +69,8 @@ def init():
|
|||
|
||||
__enabled__ = True
|
||||
__installed__ = True
|
||||
__translation__.install(True)
|
||||
__translation__.install()
|
||||
|
||||
|
||||
def check_compatibility(version):
|
||||
if isinstance(__translation__, gettext.GNUTranslations):
|
||||
|
@ -76,21 +78,25 @@ def check_compatibility(version):
|
|||
header_entries = dict(header_pattern.findall(_("")))
|
||||
|
||||
if header_entries["Project-Id-Version"] != "gitinspector {0}".format(version):
|
||||
print("WARNING: The translation for your system locale is not up to date with the current gitinspector "
|
||||
"version. The current maintainer of this locale is {0}.".format(header_entries["Last-Translator"]),
|
||||
file=sys.stderr)
|
||||
print(
|
||||
"WARNING: The translation for your system locale is not up to date with the current gitinspector "
|
||||
"version. The current maintainer of this locale is {0}.".format(header_entries["Last-Translator"]),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def get_date():
|
||||
if __enabled__ and isinstance(__translation__, gettext.GNUTranslations):
|
||||
date = time.strftime("%x")
|
||||
|
||||
if hasattr(date, 'decode'):
|
||||
if hasattr(date, "decode"):
|
||||
date = date.decode("utf-8", "replace")
|
||||
|
||||
return date
|
||||
else:
|
||||
return time.strftime("%Y/%m/%d")
|
||||
|
||||
|
||||
def enable():
|
||||
if isinstance(__translation__, gettext.GNUTranslations):
|
||||
__translation__.install(True)
|
||||
|
@ -98,9 +104,10 @@ def enable():
|
|||
global __enabled__
|
||||
__enabled__ = True
|
||||
|
||||
|
||||
def disable():
|
||||
global __enabled__
|
||||
__enabled__ = False
|
||||
|
||||
if __installed__:
|
||||
gettext.NullTranslations().install(True)
|
||||
gettext.NullTranslations().install()
|
||||
|
|
|
@ -17,35 +17,68 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from .changes import FileDiff
|
||||
from . import comment, filtering, interval
|
||||
|
||||
__metric_eloc__ = {"java": 500, "c": 500, "cpp": 500, "cs": 500, "h": 300, "hpp": 300, "php": 500, "py": 500, "glsl": 1000,
|
||||
"rb": 500, "js": 500, "sql": 1000, "xml": 1000}
|
||||
__metric_eloc__ = {
|
||||
"java": 500,
|
||||
"c": 500,
|
||||
"cpp": 500,
|
||||
"cs": 500,
|
||||
"h": 300,
|
||||
"hpp": 300,
|
||||
"php": 500,
|
||||
"py": 500,
|
||||
"glsl": 1000,
|
||||
"rb": 500,
|
||||
"js": 500,
|
||||
"sql": 1000,
|
||||
"xml": 1000,
|
||||
}
|
||||
|
||||
__metric_cc_tokens__ = [[["java", "js", "c", "cc", "cpp"], ["else", r"for\s+\(.*\)", r"if\s+\(.*\)", r"case\s+\w+:",
|
||||
"default:", r"while\s+\(.*\)"],
|
||||
["assert", "break", "continue", "return"]],
|
||||
[["cs"], ["else", r"for\s+\(.*\)", r"foreach\s+\(.*\)", r"goto\s+\w+:", r"if\s+\(.*\)", r"case\s+\w+:",
|
||||
"default:", r"while\s+\(.*\)"],
|
||||
["assert", "break", "continue", "return"]],
|
||||
[["py"], [r"^\s+elif .*:$", r"^\s+else:$", r"^\s+for .*:", r"^\s+if .*:$", r"^\s+while .*:$"],
|
||||
[r"^\s+assert", "break", "continue", "return"]]]
|
||||
__metric_cc_tokens__ = [
|
||||
[
|
||||
["java", "js", "c", "cc", "cpp"],
|
||||
["else", r"for\s+\(.*\)", r"if\s+\(.*\)", r"case\s+\w+:", "default:", r"while\s+\(.*\)"],
|
||||
["assert", "break", "continue", "return"],
|
||||
],
|
||||
[
|
||||
["cs"],
|
||||
[
|
||||
"else",
|
||||
r"for\s+\(.*\)",
|
||||
r"foreach\s+\(.*\)",
|
||||
r"goto\s+\w+:",
|
||||
r"if\s+\(.*\)",
|
||||
r"case\s+\w+:",
|
||||
"default:",
|
||||
r"while\s+\(.*\)",
|
||||
],
|
||||
["assert", "break", "continue", "return"],
|
||||
],
|
||||
[
|
||||
["py"],
|
||||
[r"^\s+elif .*:$", r"^\s+else:$", r"^\s+for .*:", r"^\s+if .*:$", r"^\s+while .*:$"],
|
||||
[r"^\s+assert", "break", "continue", "return"],
|
||||
],
|
||||
]
|
||||
|
||||
METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD = 50
|
||||
METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD = 0.75
|
||||
|
||||
class MetricsLogic(object):
|
||||
|
||||
class MetricsLogic():
|
||||
def __init__(self):
|
||||
self.eloc = {}
|
||||
self.cyclomatic_complexity = {}
|
||||
self.cyclomatic_complexity_density = {}
|
||||
|
||||
ls_tree_p = subprocess.Popen(["git", "ls-tree", "--name-only", "-r", interval.get_ref()], bufsize=1,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
ls_tree_p = subprocess.Popen(
|
||||
["git", "ls-tree", "--name-only", "-r", interval.get_ref()], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
lines = ls_tree_p.communicate()[0].splitlines()
|
||||
ls_tree_p.stdout.close()
|
||||
|
||||
|
@ -53,17 +86,18 @@ class MetricsLogic(object):
|
|||
for i in lines:
|
||||
i = i.strip().decode("unicode_escape", "ignore")
|
||||
i = i.encode("latin-1", "replace")
|
||||
i = i.decode("utf-8", "replace").strip("\"").strip("'").strip()
|
||||
i = i.decode("utf-8", "replace").strip('"').strip("'").strip()
|
||||
|
||||
if FileDiff.is_valid_extension(i) and not filtering.set_filtered(FileDiff.get_filename(i)):
|
||||
file_r = subprocess.Popen(["git", "show", interval.get_ref() + ":{0}".format(i.strip())],
|
||||
bufsize=1, stdout=subprocess.PIPE).stdout.readlines()
|
||||
file_r = subprocess.Popen(
|
||||
["git", "show", interval.get_ref() + ":{0}".format(i.strip())], stdout=subprocess.PIPE
|
||||
).stdout.readlines()
|
||||
|
||||
extension = FileDiff.get_extension(i)
|
||||
lines = MetricsLogic.get_eloc(file_r, extension)
|
||||
cycc = MetricsLogic.get_cyclomatic_complexity(file_r, extension)
|
||||
|
||||
if __metric_eloc__.get(extension, None) != None and __metric_eloc__[extension] < lines:
|
||||
if __metric_eloc__.get(extension, None) is not None and __metric_eloc__[extension] < lines:
|
||||
self.eloc[i.strip()] = lines
|
||||
|
||||
if METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD < cycc:
|
||||
|
@ -79,7 +113,7 @@ class MetricsLogic(object):
|
|||
self.cyclomatic_complexity_density.update(other.cyclomatic_complexity_density)
|
||||
return self
|
||||
except AttributeError:
|
||||
return other;
|
||||
return other
|
||||
|
||||
@staticmethod
|
||||
def get_cyclomatic_complexity(file_r, extension):
|
||||
|
|
|
@ -17,14 +17,16 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import getopt
|
||||
|
||||
|
||||
class InvalidOptionArgument(Exception):
|
||||
def __init__(self, msg):
|
||||
super(InvalidOptionArgument, self).__init__(msg)
|
||||
self.msg = msg
|
||||
|
||||
|
||||
def __find_arg_in_options__(arg, options):
|
||||
for opt in options:
|
||||
if opt[0].find(arg) == 0:
|
||||
|
@ -32,6 +34,7 @@ def __find_arg_in_options__(arg, options):
|
|||
|
||||
return None
|
||||
|
||||
|
||||
def __find_options_to_extend__(long_options):
|
||||
options_to_extend = []
|
||||
|
||||
|
@ -43,8 +46,10 @@ def __find_options_to_extend__(long_options):
|
|||
|
||||
return options_to_extend
|
||||
|
||||
|
||||
# This is a duplicate of gnu_getopt, but with support for optional arguments in long options, in the form; "arg:default_value".
|
||||
|
||||
|
||||
def gnu_getopt(args, options, long_options):
|
||||
options_to_extend = __find_options_to_extend__(long_options)
|
||||
|
||||
|
@ -55,10 +60,11 @@ def gnu_getopt(args, options, long_options):
|
|||
|
||||
return getopt.gnu_getopt(args, options, long_options)
|
||||
|
||||
|
||||
def get_boolean_argument(arg):
|
||||
if isinstance(arg, bool):
|
||||
return arg
|
||||
elif arg == None or arg.lower() == "false" or arg.lower() == "f" or arg == "0":
|
||||
elif arg is None or arg.lower() == "false" or arg.lower() == "f" or arg == "0":
|
||||
return False
|
||||
elif arg.lower() == "true" or arg.lower() == "t" or arg == "1":
|
||||
return True
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import sys
|
||||
import textwrap
|
||||
|
@ -27,8 +26,10 @@ from .. import format, gravatar, terminal
|
|||
from ..blame import Blame
|
||||
from .outputable import Outputable
|
||||
|
||||
BLAME_INFO_TEXT = N_("Below are the number of rows from each author that have survived and are still "
|
||||
"intact in the current revision")
|
||||
BLAME_INFO_TEXT = N_(
|
||||
"Below are the number of rows from each author that have survived and are still " "intact in the current revision"
|
||||
)
|
||||
|
||||
|
||||
class BlameOutput(Outputable):
|
||||
def __init__(self, changes, blame):
|
||||
|
@ -40,10 +41,11 @@ class BlameOutput(Outputable):
|
|||
Outputable.__init__(self)
|
||||
|
||||
def output_html(self):
|
||||
blame_xml = "<div><div class=\"box\">"
|
||||
blame_xml += "<p>" + _(BLAME_INFO_TEXT) + ".</p><div><table id=\"blame\" class=\"git\">"
|
||||
blame_xml = '<div><div class="box">'
|
||||
blame_xml += "<p>" + _(BLAME_INFO_TEXT) + '.</p><div><table id="blame" class="git">'
|
||||
blame_xml += "<thead><tr> <th>{0}</th> <th>{1}</th> <th>{2}</th> <th>{3}</th> <th>{4}</th> </tr></thead>".format(
|
||||
_("Author"), _("Rows"), _("Stability"), _("Age"), _("% in comments"))
|
||||
_("Author"), _("Rows"), _("Stability"), _("Age"), _("% in comments")
|
||||
)
|
||||
blame_xml += "<tbody>"
|
||||
chart_data = ""
|
||||
blames = sorted(self.blame.get_summed_blames().items())
|
||||
|
@ -54,11 +56,11 @@ class BlameOutput(Outputable):
|
|||
|
||||
for i, entry in enumerate(blames):
|
||||
work_percentage = str("{0:.2f}".format(100.0 * entry[1].rows / total_blames))
|
||||
blame_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">")
|
||||
blame_xml += "<tr " + ('class="odd">' if i % 2 == 1 else ">")
|
||||
|
||||
if format.get_selected() == "html":
|
||||
author_email = self.changes.get_latest_email_by_author(entry[0])
|
||||
blame_xml += "<td><img src=\"{0}\"/>{1}</td>".format(gravatar.get_url(author_email), entry[0])
|
||||
blame_xml += '<td><img src="{0}"/>{1}</td>'.format(gravatar.get_url(author_email), entry[0])
|
||||
else:
|
||||
blame_xml += "<td>" + entry[0] + "</td>"
|
||||
|
||||
|
@ -66,24 +68,24 @@ class BlameOutput(Outputable):
|
|||
blame_xml += "<td>" + ("{0:.1f}".format(Blame.get_stability(entry[0], entry[1].rows, self.changes)) + "</td>")
|
||||
blame_xml += "<td>" + "{0:.1f}".format(float(entry[1].skew) / entry[1].rows) + "</td>"
|
||||
blame_xml += "<td>" + "{0:.2f}".format(100.0 * entry[1].comments / entry[1].rows) + "</td>"
|
||||
blame_xml += "<td style=\"display: none\">" + work_percentage + "</td>"
|
||||
blame_xml += '<td style="display: none">' + work_percentage + "</td>"
|
||||
blame_xml += "</tr>"
|
||||
chart_data += "{{label: {0}, data: {1}}}".format(json.dumps(entry[0]), work_percentage)
|
||||
|
||||
if blames[-1] != entry:
|
||||
chart_data += ", "
|
||||
|
||||
blame_xml += "<tfoot><tr> <td colspan=\"5\"> </td> </tr></tfoot></tbody></table>"
|
||||
blame_xml += "<div class=\"chart\" id=\"blame_chart\"></div></div>"
|
||||
blame_xml += "<script type=\"text/javascript\">"
|
||||
blame_xml += " blame_plot = $.plot($(\"#blame_chart\"), [{0}], {{".format(chart_data)
|
||||
blame_xml += '<tfoot><tr> <td colspan="5"> </td> </tr></tfoot></tbody></table>'
|
||||
blame_xml += '<div class="chart" id="blame_chart"></div></div>'
|
||||
blame_xml += '<script type="text/javascript">'
|
||||
blame_xml += ' blame_plot = $.plot($("#blame_chart"), [{0}], {{'.format(chart_data)
|
||||
blame_xml += " series: {"
|
||||
blame_xml += " pie: {"
|
||||
blame_xml += " innerRadius: 0.4,"
|
||||
blame_xml += " show: true,"
|
||||
blame_xml += " combine: {"
|
||||
blame_xml += " threshold: 0.01,"
|
||||
blame_xml += " label: \"" + _("Minor Authors") + "\""
|
||||
blame_xml += ' label: "' + _("Minor Authors") + '"'
|
||||
blame_xml += " }"
|
||||
blame_xml += " }"
|
||||
blame_xml += " }, grid: {"
|
||||
|
@ -95,38 +97,52 @@ class BlameOutput(Outputable):
|
|||
print(blame_xml)
|
||||
|
||||
def output_json(self):
|
||||
message_json = "\t\t\t\"message\": \"" + _(BLAME_INFO_TEXT) + "\",\n"
|
||||
message_json = '\t\t\t"message": "' + _(BLAME_INFO_TEXT) + '",\n'
|
||||
blame_json = ""
|
||||
|
||||
for i in sorted(self.blame.get_summed_blames().items()):
|
||||
author_email = self.changes.get_latest_email_by_author(i[0])
|
||||
|
||||
name_json = "\t\t\t\t\"name\": \"" + i[0] + "\",\n"
|
||||
email_json = "\t\t\t\t\"email\": \"" + author_email + "\",\n"
|
||||
gravatar_json = "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n"
|
||||
rows_json = "\t\t\t\t\"rows\": " + str(i[1].rows) + ",\n"
|
||||
stability_json = ("\t\t\t\t\"stability\": " + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows,
|
||||
self.changes)) + ",\n")
|
||||
age_json = ("\t\t\t\t\"age\": " + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + ",\n")
|
||||
percentage_in_comments_json = ("\t\t\t\t\"percentage_in_comments\": " +
|
||||
"{0:.2f}".format(100.0 * i[1].comments / i[1].rows) + "\n")
|
||||
blame_json += ("{\n" + name_json + email_json + gravatar_json + rows_json + stability_json + age_json +
|
||||
percentage_in_comments_json + "\t\t\t},")
|
||||
name_json = '\t\t\t\t"name": "' + i[0] + '",\n'
|
||||
email_json = '\t\t\t\t"email": "' + author_email + '",\n'
|
||||
gravatar_json = '\t\t\t\t"gravatar": "' + gravatar.get_url(author_email) + '",\n'
|
||||
rows_json = '\t\t\t\t"rows": ' + str(i[1].rows) + ",\n"
|
||||
stability_json = '\t\t\t\t"stability": ' + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, self.changes)) + ",\n"
|
||||
age_json = '\t\t\t\t"age": ' + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + ",\n"
|
||||
percentage_in_comments_json = (
|
||||
'\t\t\t\t"percentage_in_comments": ' + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) + "\n"
|
||||
)
|
||||
blame_json += (
|
||||
"{\n"
|
||||
+ name_json
|
||||
+ email_json
|
||||
+ gravatar_json
|
||||
+ rows_json
|
||||
+ stability_json
|
||||
+ age_json
|
||||
+ percentage_in_comments_json
|
||||
+ "\t\t\t},"
|
||||
)
|
||||
else:
|
||||
blame_json = blame_json[:-1]
|
||||
|
||||
print(",\n\t\t\"blame\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + blame_json + "]\n\t\t}", end="")
|
||||
print(',\n\t\t"blame": {\n' + message_json + '\t\t\t"authors": [\n\t\t\t' + blame_json + "]\n\t\t}", end="")
|
||||
|
||||
def output_text(self):
|
||||
if sys.stdout.isatty() and format.is_interactive_format():
|
||||
terminal.clear_row()
|
||||
|
||||
print(textwrap.fill(_(BLAME_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n")
|
||||
terminal.printb(terminal.ljust(_("Author"), 21) + terminal.rjust(_("Rows"), 10) + terminal.rjust(_("Stability"), 15) +
|
||||
terminal.rjust(_("Age"), 13) + terminal.rjust(_("% in comments"), 20))
|
||||
terminal.printb(
|
||||
terminal.ljust(_("Author"), 21)
|
||||
+ terminal.rjust(_("Rows"), 10)
|
||||
+ terminal.rjust(_("Stability"), 15)
|
||||
+ terminal.rjust(_("Age"), 13)
|
||||
+ terminal.rjust(_("% in comments"), 20)
|
||||
)
|
||||
|
||||
for i in sorted(self.blame.get_summed_blames().items()):
|
||||
print(terminal.ljust(i[0], 20)[0:20 - terminal.get_excess_column_count(i[0])], end=" ")
|
||||
print(terminal.ljust(i[0], 20)[0 : 20 - terminal.get_excess_column_count(i[0])], end=" ")
|
||||
print(str(i[1].rows).rjust(10), end=" ")
|
||||
print("{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, self.changes)).rjust(14), end=" ")
|
||||
print("{0:.1f}".format(float(i[1].skew) / i[1].rows).rjust(12), end=" ")
|
||||
|
@ -143,12 +159,23 @@ class BlameOutput(Outputable):
|
|||
email_xml = "\t\t\t\t<email>" + author_email + "</email>\n"
|
||||
gravatar_xml = "\t\t\t\t<gravatar>" + gravatar.get_url(author_email) + "</gravatar>\n"
|
||||
rows_xml = "\t\t\t\t<rows>" + str(i[1].rows) + "</rows>\n"
|
||||
stability_xml = ("\t\t\t\t<stability>" + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows,
|
||||
self.changes)) + "</stability>\n")
|
||||
age_xml = ("\t\t\t\t<age>" + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + "</age>\n")
|
||||
percentage_in_comments_xml = ("\t\t\t\t<percentage-in-comments>" + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) +
|
||||
"</percentage-in-comments>\n")
|
||||
blame_xml += ("\t\t\t<author>\n" + name_xml + email_xml + gravatar_xml + rows_xml + stability_xml +
|
||||
age_xml + percentage_in_comments_xml + "\t\t\t</author>\n")
|
||||
stability_xml = (
|
||||
"\t\t\t\t<stability>" + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, self.changes)) + "</stability>\n"
|
||||
)
|
||||
age_xml = "\t\t\t\t<age>" + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + "</age>\n"
|
||||
percentage_in_comments_xml = (
|
||||
"\t\t\t\t<percentage-in-comments>" + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) + "</percentage-in-comments>\n"
|
||||
)
|
||||
blame_xml += (
|
||||
"\t\t\t<author>\n"
|
||||
+ name_xml
|
||||
+ email_xml
|
||||
+ gravatar_xml
|
||||
+ rows_xml
|
||||
+ stability_xml
|
||||
+ age_xml
|
||||
+ percentage_in_comments_xml
|
||||
+ "\t\t\t</author>\n"
|
||||
)
|
||||
|
||||
print("\t<blame>\n" + message_xml + "\t\t<authors>\n" + blame_xml + "\t\t</authors>\n\t</blame>")
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import textwrap
|
||||
from ..localization import N_
|
||||
|
@ -28,6 +27,7 @@ from .outputable import Outputable
|
|||
HISTORICAL_INFO_TEXT = N_("The following historical commit information, by author, was found")
|
||||
NO_COMMITED_FILES_TEXT = N_("No commited files with the specified extensions were found")
|
||||
|
||||
|
||||
class ChangesOutput(Outputable):
|
||||
def __init__(self, changes):
|
||||
self.changes = changes
|
||||
|
@ -36,7 +36,7 @@ class ChangesOutput(Outputable):
|
|||
def output_html(self):
|
||||
authorinfo_list = self.changes.get_authorinfo_list()
|
||||
total_changes = 0.0
|
||||
changes_xml = "<div><div class=\"box\">"
|
||||
changes_xml = '<div><div class="box">'
|
||||
chart_data = ""
|
||||
|
||||
for i in authorinfo_list:
|
||||
|
@ -44,20 +44,22 @@ class ChangesOutput(Outputable):
|
|||
total_changes += authorinfo_list.get(i).deletions
|
||||
|
||||
if authorinfo_list:
|
||||
changes_xml += "<p>" + _(HISTORICAL_INFO_TEXT) + ".</p><div><table id=\"changes\" class=\"git\">"
|
||||
changes_xml += "<p>" + _(HISTORICAL_INFO_TEXT) + '.</p><div><table id="changes" class="git">'
|
||||
changes_xml += "<thead><tr> <th>{0}</th> <th>{1}</th> <th>{2}</th> <th>{3}</th> <th>{4}</th>".format(
|
||||
_("Author"), _("Commits"), _("Insertions"), _("Deletions"), _("% of changes"))
|
||||
_("Author"), _("Commits"), _("Insertions"), _("Deletions"), _("% of changes")
|
||||
)
|
||||
changes_xml += "</tr></thead><tbody>"
|
||||
|
||||
for i, entry in enumerate(sorted(authorinfo_list)):
|
||||
authorinfo = authorinfo_list.get(entry)
|
||||
percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
|
||||
|
||||
changes_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">")
|
||||
changes_xml += "<tr " + ('class="odd">' if i % 2 == 1 else ">")
|
||||
|
||||
if format.get_selected() == "html":
|
||||
changes_xml += "<td><img src=\"{0}\"/>{1}</td>".format(
|
||||
gravatar.get_url(self.changes.get_latest_email_by_author(entry)), entry)
|
||||
changes_xml += '<td><img src="{0}"/>{1}</td>'.format(
|
||||
gravatar.get_url(self.changes.get_latest_email_by_author(entry)), entry
|
||||
)
|
||||
else:
|
||||
changes_xml += "<td>" + entry + "</td>"
|
||||
|
||||
|
@ -71,17 +73,17 @@ class ChangesOutput(Outputable):
|
|||
if sorted(authorinfo_list)[-1] != entry:
|
||||
chart_data += ", "
|
||||
|
||||
changes_xml += ("<tfoot><tr> <td colspan=\"5\"> </td> </tr></tfoot></tbody></table>")
|
||||
changes_xml += "<div class=\"chart\" id=\"changes_chart\"></div></div>"
|
||||
changes_xml += "<script type=\"text/javascript\">"
|
||||
changes_xml += " changes_plot = $.plot($(\"#changes_chart\"), [{0}], {{".format(chart_data)
|
||||
changes_xml += '<tfoot><tr> <td colspan="5"> </td> </tr></tfoot></tbody></table>'
|
||||
changes_xml += '<div class="chart" id="changes_chart"></div></div>'
|
||||
changes_xml += '<script type="text/javascript">'
|
||||
changes_xml += ' changes_plot = $.plot($("#changes_chart"), [{0}], {{'.format(chart_data)
|
||||
changes_xml += " series: {"
|
||||
changes_xml += " pie: {"
|
||||
changes_xml += " innerRadius: 0.4,"
|
||||
changes_xml += " show: true,"
|
||||
changes_xml += " combine: {"
|
||||
changes_xml += " threshold: 0.01,"
|
||||
changes_xml += " label: \"" + _("Minor Authors") + "\""
|
||||
changes_xml += ' label: "' + _("Minor Authors") + '"'
|
||||
changes_xml += " }"
|
||||
changes_xml += " }"
|
||||
changes_xml += " }, grid: {"
|
||||
|
@ -104,7 +106,7 @@ class ChangesOutput(Outputable):
|
|||
total_changes += authorinfo_list.get(i).deletions
|
||||
|
||||
if authorinfo_list:
|
||||
message_json = "\t\t\t\"message\": \"" + _(HISTORICAL_INFO_TEXT) + "\",\n"
|
||||
message_json = '\t\t\t"message": "' + _(HISTORICAL_INFO_TEXT) + '",\n'
|
||||
changes_json = ""
|
||||
|
||||
for i in sorted(authorinfo_list):
|
||||
|
@ -112,23 +114,32 @@ class ChangesOutput(Outputable):
|
|||
authorinfo = authorinfo_list.get(i)
|
||||
|
||||
percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
|
||||
name_json = "\t\t\t\t\"name\": \"" + i + "\",\n"
|
||||
email_json = "\t\t\t\t\"email\": \"" + author_email + "\",\n"
|
||||
gravatar_json = "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n"
|
||||
commits_json = "\t\t\t\t\"commits\": " + str(authorinfo.commits) + ",\n"
|
||||
insertions_json = "\t\t\t\t\"insertions\": " + str(authorinfo.insertions) + ",\n"
|
||||
deletions_json = "\t\t\t\t\"deletions\": " + str(authorinfo.deletions) + ",\n"
|
||||
percentage_json = "\t\t\t\t\"percentage_of_changes\": " + "{0:.2f}".format(percentage) + "\n"
|
||||
name_json = '\t\t\t\t"name": "' + i + '",\n'
|
||||
email_json = '\t\t\t\t"email": "' + author_email + '",\n'
|
||||
gravatar_json = '\t\t\t\t"gravatar": "' + gravatar.get_url(author_email) + '",\n'
|
||||
commits_json = '\t\t\t\t"commits": ' + str(authorinfo.commits) + ",\n"
|
||||
insertions_json = '\t\t\t\t"insertions": ' + str(authorinfo.insertions) + ",\n"
|
||||
deletions_json = '\t\t\t\t"deletions": ' + str(authorinfo.deletions) + ",\n"
|
||||
percentage_json = '\t\t\t\t"percentage_of_changes": ' + "{0:.2f}".format(percentage) + "\n"
|
||||
|
||||
changes_json += ("{\n" + name_json + email_json + gravatar_json + commits_json +
|
||||
insertions_json + deletions_json + percentage_json + "\t\t\t}")
|
||||
changes_json += (
|
||||
"{\n"
|
||||
+ name_json
|
||||
+ email_json
|
||||
+ gravatar_json
|
||||
+ commits_json
|
||||
+ insertions_json
|
||||
+ deletions_json
|
||||
+ percentage_json
|
||||
+ "\t\t\t}"
|
||||
)
|
||||
changes_json += ","
|
||||
else:
|
||||
changes_json = changes_json[:-1]
|
||||
|
||||
print("\t\t\"changes\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + changes_json + "]\n\t\t}", end="")
|
||||
print('\t\t"changes": {\n' + message_json + '\t\t\t"authors": [\n\t\t\t' + changes_json + "]\n\t\t}", end="")
|
||||
else:
|
||||
print("\t\t\"exception\": \"" + _(NO_COMMITED_FILES_TEXT) + "\"")
|
||||
print('\t\t"exception": "' + _(NO_COMMITED_FILES_TEXT) + '"')
|
||||
|
||||
def output_text(self):
|
||||
authorinfo_list = self.changes.get_authorinfo_list()
|
||||
|
@ -140,15 +151,19 @@ class ChangesOutput(Outputable):
|
|||
|
||||
if authorinfo_list:
|
||||
print(textwrap.fill(_(HISTORICAL_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n")
|
||||
terminal.printb(terminal.ljust(_("Author"), 21) + terminal.rjust(_("Commits"), 13) +
|
||||
terminal.rjust(_("Insertions"), 14) + terminal.rjust(_("Deletions"), 15) +
|
||||
terminal.rjust(_("% of changes"), 16))
|
||||
terminal.printb(
|
||||
terminal.ljust(_("Author"), 21)
|
||||
+ terminal.rjust(_("Commits"), 13)
|
||||
+ terminal.rjust(_("Insertions"), 14)
|
||||
+ terminal.rjust(_("Deletions"), 15)
|
||||
+ terminal.rjust(_("% of changes"), 16)
|
||||
)
|
||||
|
||||
for i in sorted(authorinfo_list):
|
||||
authorinfo = authorinfo_list.get(i)
|
||||
percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
|
||||
|
||||
print(terminal.ljust(i, 20)[0:20 - terminal.get_excess_column_count(i)], end=" ")
|
||||
print(terminal.ljust(i, 20)[0 : 20 - terminal.get_excess_column_count(i)], end=" ")
|
||||
print(str(authorinfo.commits).rjust(13), end=" ")
|
||||
print(str(authorinfo.insertions).rjust(13), end=" ")
|
||||
print(str(authorinfo.deletions).rjust(14), end=" ")
|
||||
|
@ -181,8 +196,17 @@ class ChangesOutput(Outputable):
|
|||
deletions_xml = "\t\t\t\t<deletions>" + str(authorinfo.deletions) + "</deletions>\n"
|
||||
percentage_xml = "\t\t\t\t<percentage-of-changes>" + "{0:.2f}".format(percentage) + "</percentage-of-changes>\n"
|
||||
|
||||
changes_xml += ("\t\t\t<author>\n" + name_xml + email_xml + gravatar_xml + commits_xml +
|
||||
insertions_xml + deletions_xml + percentage_xml + "\t\t\t</author>\n")
|
||||
changes_xml += (
|
||||
"\t\t\t<author>\n"
|
||||
+ name_xml
|
||||
+ email_xml
|
||||
+ gravatar_xml
|
||||
+ commits_xml
|
||||
+ insertions_xml
|
||||
+ deletions_xml
|
||||
+ percentage_xml
|
||||
+ "\t\t\t</author>\n"
|
||||
)
|
||||
|
||||
print("\t<changes>\n" + message_xml + "\t\t<authors>\n" + changes_xml + "\t\t</authors>\n\t</changes>")
|
||||
else:
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import textwrap
|
||||
from ..localization import N_
|
||||
from .. import extensions, terminal
|
||||
|
@ -28,6 +27,7 @@ from .outputable import Outputable
|
|||
EXTENSIONS_INFO_TEXT = N_("The extensions below were found in the repository history")
|
||||
EXTENSIONS_MARKED_TEXT = N_("(extensions used during statistical analysis are marked)")
|
||||
|
||||
|
||||
class ExtensionsOutput(Outputable):
|
||||
@staticmethod
|
||||
def is_marked(extension):
|
||||
|
@ -38,7 +38,7 @@ class ExtensionsOutput(Outputable):
|
|||
|
||||
def output_html(self):
|
||||
if extensions.__located_extensions__:
|
||||
extensions_xml = "<div><div class=\"box\">"
|
||||
extensions_xml = '<div><div class="box">'
|
||||
extensions_xml += "<p>{0} {1}.</p><p>".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT))
|
||||
|
||||
for i in sorted(extensions.__located_extensions__):
|
||||
|
@ -53,32 +53,42 @@ class ExtensionsOutput(Outputable):
|
|||
|
||||
def output_json(self):
|
||||
if extensions.__located_extensions__:
|
||||
message_json = "\t\t\t\"message\": \"" + _(EXTENSIONS_INFO_TEXT) + "\",\n"
|
||||
message_json = '\t\t\t"message": "' + _(EXTENSIONS_INFO_TEXT) + '",\n'
|
||||
used_extensions_json = ""
|
||||
unused_extensions_json = ""
|
||||
|
||||
for i in sorted(extensions.__located_extensions__):
|
||||
if ExtensionsOutput.is_marked(i):
|
||||
used_extensions_json += "\"" + i + "\", "
|
||||
used_extensions_json += '"' + i + '", '
|
||||
else:
|
||||
unused_extensions_json += "\"" + i + "\", "
|
||||
unused_extensions_json += '"' + i + '", '
|
||||
|
||||
used_extensions_json = used_extensions_json[:-2]
|
||||
unused_extensions_json = unused_extensions_json[:-2]
|
||||
|
||||
print(",\n\t\t\"extensions\": {\n" + message_json + "\t\t\t\"used\": [ " + used_extensions_json +
|
||||
" ],\n\t\t\t\"unused\": [ " + unused_extensions_json + " ]\n" + "\t\t}", end="")
|
||||
print(
|
||||
',\n\t\t"extensions": {\n'
|
||||
+ message_json
|
||||
+ '\t\t\t"used": [ '
|
||||
+ used_extensions_json
|
||||
+ ' ],\n\t\t\t"unused": [ '
|
||||
+ unused_extensions_json
|
||||
+ " ]\n"
|
||||
+ "\t\t}",
|
||||
end="",
|
||||
)
|
||||
|
||||
def output_text(self):
|
||||
if extensions.__located_extensions__:
|
||||
print("\n" + textwrap.fill("{0} {1}:".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT)),
|
||||
width=terminal.get_size()[0]))
|
||||
print(
|
||||
"\n" + textwrap.fill("{0} {1}:".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT)), width=terminal.get_size()[0])
|
||||
)
|
||||
|
||||
for i in sorted(extensions.__located_extensions__):
|
||||
if ExtensionsOutput.is_marked(i):
|
||||
print("[" + terminal.__bold__ + i + terminal.__normal__ + "]", end=" ")
|
||||
else:
|
||||
print (i, end=" ")
|
||||
print(i, end=" ")
|
||||
print("")
|
||||
|
||||
def output_xml(self):
|
||||
|
@ -93,5 +103,14 @@ class ExtensionsOutput(Outputable):
|
|||
else:
|
||||
unused_extensions_xml += "\t\t\t<extension>" + i + "</extension>\n"
|
||||
|
||||
print("\t<extensions>\n" + message_xml + "\t\t<used>\n" + used_extensions_xml + "\t\t</used>\n" +
|
||||
"\t\t<unused>\n" + unused_extensions_xml + "\t\t</unused>\n" + "\t</extensions>")
|
||||
print(
|
||||
"\t<extensions>\n"
|
||||
+ message_xml
|
||||
+ "\t\t<used>\n"
|
||||
+ used_extensions_xml
|
||||
+ "\t\t</used>\n"
|
||||
+ "\t\t<unused>\n"
|
||||
+ unused_extensions_xml
|
||||
+ "\t\t</unused>\n"
|
||||
+ "\t</extensions>"
|
||||
)
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import textwrap
|
||||
from ..localization import N_
|
||||
from ..filtering import __filters__, has_filtered
|
||||
|
@ -26,11 +25,16 @@ from .. import terminal
|
|||
from .outputable import Outputable
|
||||
|
||||
FILTERING_INFO_TEXT = N_("The following files were excluded from the statistics due to the specified exclusion patterns")
|
||||
FILTERING_AUTHOR_INFO_TEXT = N_("The following authors were excluded from the statistics due to the specified exclusion patterns")
|
||||
FILTERING_EMAIL_INFO_TEXT = N_("The authors with the following emails were excluded from the statistics due to the specified " \
|
||||
"exclusion patterns")
|
||||
FILTERING_COMMIT_INFO_TEXT = N_("The following commit revisions were excluded from the statistics due to the specified " \
|
||||
"exclusion patterns")
|
||||
FILTERING_AUTHOR_INFO_TEXT = N_(
|
||||
"The following authors were excluded from the statistics due to the specified exclusion patterns"
|
||||
)
|
||||
FILTERING_EMAIL_INFO_TEXT = N_(
|
||||
"The authors with the following emails were excluded from the statistics due to the specified " "exclusion patterns"
|
||||
)
|
||||
FILTERING_COMMIT_INFO_TEXT = N_(
|
||||
"The following commit revisions were excluded from the statistics due to the specified " "exclusion patterns"
|
||||
)
|
||||
|
||||
|
||||
class FilteringOutput(Outputable):
|
||||
@staticmethod
|
||||
|
@ -38,7 +42,7 @@ class FilteringOutput(Outputable):
|
|||
filtering_xml = ""
|
||||
|
||||
if filtered:
|
||||
filtering_xml += "<p>" + info_string + "."+ "</p>"
|
||||
filtering_xml += "<p>" + info_string + "." + "</p>"
|
||||
|
||||
for i in filtered:
|
||||
filtering_xml += "<p>" + i + "</p>"
|
||||
|
@ -47,7 +51,7 @@ class FilteringOutput(Outputable):
|
|||
|
||||
def output_html(self):
|
||||
if has_filtered():
|
||||
filtering_xml = "<div><div class=\"box\">"
|
||||
filtering_xml = '<div><div class="box">'
|
||||
FilteringOutput.__output_html_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1])
|
||||
FilteringOutput.__output_html_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1])
|
||||
FilteringOutput.__output_html_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1])
|
||||
|
@ -59,22 +63,27 @@ class FilteringOutput(Outputable):
|
|||
@staticmethod
|
||||
def __output_json_section__(info_string, filtered, container_tagname):
|
||||
if filtered:
|
||||
message_json = "\t\t\t\t\"message\": \"" + info_string + "\",\n"
|
||||
message_json = '\t\t\t\t"message": "' + info_string + '",\n'
|
||||
filtering_json = ""
|
||||
|
||||
for i in filtered:
|
||||
filtering_json += "\t\t\t\t\t\"" + i + "\",\n"
|
||||
filtering_json += '\t\t\t\t\t"' + i + '",\n'
|
||||
else:
|
||||
filtering_json = filtering_json[:-3]
|
||||
|
||||
return "\n\t\t\t\"{0}\": {{\n".format(container_tagname) + message_json + \
|
||||
"\t\t\t\t\"entries\": [\n" + filtering_json + "\"\n\t\t\t\t]\n\t\t\t},"
|
||||
return (
|
||||
'\n\t\t\t"{0}": {{\n'.format(container_tagname)
|
||||
+ message_json
|
||||
+ '\t\t\t\t"entries": [\n'
|
||||
+ filtering_json
|
||||
+ '"\n\t\t\t\t]\n\t\t\t},'
|
||||
)
|
||||
|
||||
return ""
|
||||
|
||||
def output_json(self):
|
||||
if has_filtered():
|
||||
output = ",\n\t\t\"filtering\": {"
|
||||
output = ',\n\t\t"filtering": {'
|
||||
output += FilteringOutput.__output_json_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1], "files")
|
||||
output += FilteringOutput.__output_json_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1], "authors")
|
||||
output += FilteringOutput.__output_json_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1], "emails")
|
||||
|
@ -90,7 +99,7 @@ class FilteringOutput(Outputable):
|
|||
|
||||
for i in filtered:
|
||||
(width, _unused) = terminal.get_size()
|
||||
print("...%s" % i[-width+3:] if len(i) > width else i)
|
||||
print("...%s" % i[-width + 3 :] if len(i) > width else i)
|
||||
|
||||
def output_text(self):
|
||||
FilteringOutput.__output_text_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1])
|
||||
|
|
|
@ -17,26 +17,28 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ..changes import FileDiff
|
||||
from ..localization import N_
|
||||
from ..metrics import (__metric_eloc__, METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD)
|
||||
from ..metrics import __metric_eloc__, METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD
|
||||
from .outputable import Outputable
|
||||
|
||||
ELOC_INFO_TEXT = N_("The following files are suspiciously big (in order of severity)")
|
||||
CYCLOMATIC_COMPLEXITY_TEXT = N_("The following files have an elevated cyclomatic complexity (in order of severity)")
|
||||
CYCLOMATIC_COMPLEXITY_DENSITY_TEXT = N_("The following files have an elevated cyclomatic complexity density " \
|
||||
"(in order of severity)")
|
||||
CYCLOMATIC_COMPLEXITY_DENSITY_TEXT = N_(
|
||||
"The following files have an elevated cyclomatic complexity density " "(in order of severity)"
|
||||
)
|
||||
METRICS_MISSING_INFO_TEXT = N_("No metrics violations were found in the repository")
|
||||
|
||||
METRICS_VIOLATION_SCORES = [[1.0, "minimal"], [1.25, "minor"], [1.5, "medium"], [2.0, "bad"], [3.0, "severe"]]
|
||||
|
||||
|
||||
def __get_metrics_score__(ceiling, value):
|
||||
for i in reversed(METRICS_VIOLATION_SCORES):
|
||||
if value > ceiling * i[0]:
|
||||
return i[1]
|
||||
|
||||
|
||||
class MetricsOutput(Outputable):
|
||||
def __init__(self, metrics):
|
||||
self.metrics = metrics
|
||||
|
@ -48,47 +50,61 @@ class MetricsOutput(Outputable):
|
|||
|
||||
if self.metrics.eloc:
|
||||
print("\n" + _(ELOC_INFO_TEXT) + ":")
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.eloc.items()]), reverse=True):
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.eloc.items())]), reverse=True):
|
||||
print(_("{0} ({1} estimated lines of code)").format(i[1], str(i[0])))
|
||||
|
||||
if self.metrics.cyclomatic_complexity:
|
||||
print("\n" + _(CYCLOMATIC_COMPLEXITY_TEXT) + ":")
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity.items()]), reverse=True):
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity.items())]), reverse=True):
|
||||
print(_("{0} ({1} in cyclomatic complexity)").format(i[1], str(i[0])))
|
||||
|
||||
if self.metrics.cyclomatic_complexity_density:
|
||||
print("\n" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + ":")
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity_density.items()]), reverse=True):
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True):
|
||||
print(_("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0]))
|
||||
|
||||
def output_html(self):
|
||||
metrics_xml = "<div><div class=\"box\" id=\"metrics\">"
|
||||
metrics_xml = '<div><div class="box" id="metrics">'
|
||||
|
||||
if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density:
|
||||
metrics_xml += "<p>" + _(METRICS_MISSING_INFO_TEXT) + ".</p>"
|
||||
|
||||
if self.metrics.eloc:
|
||||
metrics_xml += "<div><h4>" + _(ELOC_INFO_TEXT) + ".</h4>"
|
||||
for num, i in enumerate(sorted(set([(j, i) for (i, j) in self.metrics.eloc.items()]), reverse=True)):
|
||||
metrics_xml += "<div class=\"" + __get_metrics_score__(__metric_eloc__[FileDiff.get_extension(i[1])], i[0]) + \
|
||||
(" odd\">" if num % 2 == 1 else "\">") + \
|
||||
_("{0} ({1} estimated lines of code)").format(i[1], str(i[0])) + "</div>"
|
||||
for num, i in enumerate(sorted(set([(j, i) for (i, j) in list(self.metrics.eloc.items())]), reverse=True)):
|
||||
metrics_xml += (
|
||||
'<div class="'
|
||||
+ __get_metrics_score__(__metric_eloc__[FileDiff.get_extension(i[1])], i[0])
|
||||
+ (' odd">' if num % 2 == 1 else '">')
|
||||
+ _("{0} ({1} estimated lines of code)").format(i[1], str(i[0]))
|
||||
+ "</div>"
|
||||
)
|
||||
metrics_xml += "</div>"
|
||||
|
||||
if self.metrics.cyclomatic_complexity:
|
||||
metrics_xml += "<div><h4>" + _(CYCLOMATIC_COMPLEXITY_TEXT) + "</h4>"
|
||||
for num, i in enumerate(sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity.items()]), reverse=True)):
|
||||
metrics_xml += "<div class=\"" + __get_metrics_score__(METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, i[0]) + \
|
||||
(" odd\">" if num % 2 == 1 else "\">") + \
|
||||
_("{0} ({1} in cyclomatic complexity)").format(i[1], str(i[0])) + "</div>"
|
||||
metrics_xml += "<div><h4>" + _(CYCLOMATIC_COMPLEXITY_TEXT) + "</h4>"
|
||||
for num, i in enumerate(sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity.items())]), reverse=True)):
|
||||
metrics_xml += (
|
||||
'<div class="'
|
||||
+ __get_metrics_score__(METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, i[0])
|
||||
+ (' odd">' if num % 2 == 1 else '">')
|
||||
+ _("{0} ({1} in cyclomatic complexity)").format(i[1], str(i[0]))
|
||||
+ "</div>"
|
||||
)
|
||||
metrics_xml += "</div>"
|
||||
|
||||
if self.metrics.cyclomatic_complexity_density:
|
||||
metrics_xml += "<div><h4>" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + "</h4>"
|
||||
for num, i in enumerate(sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity_density.items()]), reverse=True)):
|
||||
metrics_xml += "<div class=\"" + __get_metrics_score__(METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD, i[0]) + \
|
||||
(" odd\">" if num % 2 == 1 else "\">") + \
|
||||
_("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0]) + "</div>"
|
||||
metrics_xml += "<div><h4>" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + "</h4>"
|
||||
for num, i in enumerate(
|
||||
sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True)
|
||||
):
|
||||
metrics_xml += (
|
||||
'<div class="'
|
||||
+ __get_metrics_score__(METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD, i[0])
|
||||
+ (' odd">' if num % 2 == 1 else '">')
|
||||
+ _("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0])
|
||||
+ "</div>"
|
||||
)
|
||||
metrics_xml += "</div>"
|
||||
|
||||
metrics_xml += "</div></div>"
|
||||
|
@ -96,40 +112,41 @@ class MetricsOutput(Outputable):
|
|||
|
||||
def output_json(self):
|
||||
if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density:
|
||||
print(",\n\t\t\"metrics\": {\n\t\t\t\"message\": \"" + _(METRICS_MISSING_INFO_TEXT) + "\"\n\t\t}", end="")
|
||||
print(',\n\t\t"metrics": {\n\t\t\t"message": "' + _(METRICS_MISSING_INFO_TEXT) + '"\n\t\t}', end="")
|
||||
else:
|
||||
eloc_json = ""
|
||||
|
||||
if self.metrics.eloc:
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.eloc.items()]), reverse=True):
|
||||
eloc_json += "{\n\t\t\t\t\"type\": \"estimated-lines-of-code\",\n"
|
||||
eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n"
|
||||
eloc_json += "\t\t\t\t\"value\": " + str(i[0]) + "\n"
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.eloc.items())]), reverse=True):
|
||||
eloc_json += '{\n\t\t\t\t"type": "estimated-lines-of-code",\n'
|
||||
eloc_json += '\t\t\t\t"file_name": "' + i[1] + '",\n'
|
||||
eloc_json += '\t\t\t\t"value": ' + str(i[0]) + "\n"
|
||||
eloc_json += "\t\t\t},"
|
||||
else:
|
||||
if not self.metrics.cyclomatic_complexity:
|
||||
eloc_json = eloc_json[:-1]
|
||||
|
||||
if self.metrics.cyclomatic_complexity:
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity.items()]), reverse=True):
|
||||
eloc_json += "{\n\t\t\t\t\"type\": \"cyclomatic-complexity\",\n"
|
||||
eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n"
|
||||
eloc_json += "\t\t\t\t\"value\": " + str(i[0]) + "\n"
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity.items())]), reverse=True):
|
||||
eloc_json += '{\n\t\t\t\t"type": "cyclomatic-complexity",\n'
|
||||
eloc_json += '\t\t\t\t"file_name": "' + i[1] + '",\n'
|
||||
eloc_json += '\t\t\t\t"value": ' + str(i[0]) + "\n"
|
||||
eloc_json += "\t\t\t},"
|
||||
else:
|
||||
if not self.metrics.cyclomatic_complexity_density:
|
||||
eloc_json = eloc_json[:-1]
|
||||
|
||||
if self.metrics.cyclomatic_complexity_density:
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity_density.items()]), reverse=True):
|
||||
eloc_json += "{\n\t\t\t\t\"type\": \"cyclomatic-complexity-density\",\n"
|
||||
eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n"
|
||||
eloc_json += "\t\t\t\t\"value\": {0:.3f}\n".format(i[0])
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True):
|
||||
eloc_json += '{\n\t\t\t\t"type": "cyclomatic-complexity-density",\n'
|
||||
eloc_json += '\t\t\t\t"file_name": "' + i[1] + '",\n'
|
||||
eloc_json += '\t\t\t\t"value": {0:.3f}\n'.format(i[0])
|
||||
eloc_json += "\t\t\t},"
|
||||
else:
|
||||
eloc_json = eloc_json[:-1]
|
||||
|
||||
print(",\n\t\t\"metrics\": {\n\t\t\t\"violations\": [\n\t\t\t" + eloc_json + "]\n\t\t}", end="")
|
||||
print(',\n\t\t"metrics": {\n\t\t\t"violations": [\n\t\t\t' + eloc_json + "]\n\t\t}", end="")
|
||||
|
||||
def output_xml(self):
|
||||
if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density:
|
||||
print("\t<metrics>\n\t\t<message>" + _(METRICS_MISSING_INFO_TEXT) + "</message>\n\t</metrics>")
|
||||
|
@ -137,21 +154,21 @@ class MetricsOutput(Outputable):
|
|||
eloc_xml = ""
|
||||
|
||||
if self.metrics.eloc:
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.eloc.items()]), reverse=True):
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.eloc.items())]), reverse=True):
|
||||
eloc_xml += "\t\t\t<estimated-lines-of-code>\n"
|
||||
eloc_xml += "\t\t\t\t<file-name>" + i[1] + "</file-name>\n"
|
||||
eloc_xml += "\t\t\t\t<value>" + str(i[0]) + "</value>\n"
|
||||
eloc_xml += "\t\t\t</estimated-lines-of-code>\n"
|
||||
|
||||
if self.metrics.cyclomatic_complexity:
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity.items()]), reverse=True):
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity.items())]), reverse=True):
|
||||
eloc_xml += "\t\t\t<cyclomatic-complexity>\n"
|
||||
eloc_xml += "\t\t\t\t<file-name>" + i[1] + "</file-name>\n"
|
||||
eloc_xml += "\t\t\t\t<value>" + str(i[0]) + "</value>\n"
|
||||
eloc_xml += "\t\t\t</cyclomatic-complexity>\n"
|
||||
|
||||
if self.metrics.cyclomatic_complexity_density:
|
||||
for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity_density.items()]), reverse=True):
|
||||
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True):
|
||||
eloc_xml += "\t\t\t<cyclomatic-complexity-density>\n"
|
||||
eloc_xml += "\t\t\t\t<file-name>" + i[1] + "</file-name>\n"
|
||||
eloc_xml += "\t\t\t\t<value>{0:.3f}</value>\n".format(i[0])
|
||||
|
|
|
@ -17,22 +17,23 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .. import format
|
||||
|
||||
class Outputable(object):
|
||||
|
||||
class Outputable():
|
||||
def output_html(self):
|
||||
raise NotImplementedError(_("HTML output not yet supported in") + " \"" + self.__class__.__name__ + "\".")
|
||||
raise NotImplementedError(_("HTML output not yet supported in") + ' "' + self.__class__.__name__ + '".')
|
||||
|
||||
def output_json(self):
|
||||
raise NotImplementedError(_("JSON output not yet supported in") + " \"" + self.__class__.__name__ + "\".")
|
||||
raise NotImplementedError(_("JSON output not yet supported in") + ' "' + self.__class__.__name__ + '".')
|
||||
|
||||
def output_text(self):
|
||||
raise NotImplementedError(_("Text output not yet supported in") + " \"" + self.__class__.__name__ + "\".")
|
||||
raise NotImplementedError(_("Text output not yet supported in") + ' "' + self.__class__.__name__ + '".')
|
||||
|
||||
def output_xml(self):
|
||||
raise NotImplementedError(_("XML output not yet supported in") + " \"" + self.__class__.__name__ + "\".")
|
||||
raise NotImplementedError(_("XML output not yet supported in") + ' "' + self.__class__.__name__ + '".')
|
||||
|
||||
|
||||
def output(outputable):
|
||||
if format.get_selected() == "html" or format.get_selected() == "htmlembedded":
|
||||
|
|
|
@ -17,19 +17,21 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import textwrap
|
||||
from ..localization import N_
|
||||
from .. import format, gravatar, terminal
|
||||
from .. import responsibilities as resp
|
||||
from .outputable import Outputable
|
||||
|
||||
RESPONSIBILITIES_INFO_TEXT = N_("The following responsibilities, by author, were found in the current "
|
||||
"revision of the repository (comments are excluded from the line count, "
|
||||
"if possible)")
|
||||
RESPONSIBILITIES_INFO_TEXT = N_(
|
||||
"The following responsibilities, by author, were found in the current "
|
||||
"revision of the repository (comments are excluded from the line count, "
|
||||
"if possible)"
|
||||
)
|
||||
MOSTLY_RESPONSIBLE_FOR_TEXT = N_("is mostly responsible for")
|
||||
|
||||
|
||||
class ResponsibilitiesOutput(Outputable):
|
||||
def __init__(self, changes, blame):
|
||||
self.changes = changes
|
||||
|
@ -50,13 +52,13 @@ class ResponsibilitiesOutput(Outputable):
|
|||
width -= 7
|
||||
|
||||
print(str(entry[0]).rjust(6), end=" ")
|
||||
print("...%s" % entry[1][-width+3:] if len(entry[1]) > width else entry[1])
|
||||
print("...%s" % entry[1][-width + 3 :] if len(entry[1]) > width else entry[1])
|
||||
|
||||
if j >= 9:
|
||||
break
|
||||
|
||||
def output_html(self):
|
||||
resp_xml = "<div><div class=\"box\" id=\"responsibilities\">"
|
||||
resp_xml = '<div><div class="box" id="responsibilities">'
|
||||
resp_xml += "<p>" + _(RESPONSIBILITIES_INFO_TEXT) + ".</p>"
|
||||
|
||||
for i in sorted(set(i[0] for i in self.blame.blames)):
|
||||
|
@ -67,14 +69,14 @@ class ResponsibilitiesOutput(Outputable):
|
|||
|
||||
if format.get_selected() == "html":
|
||||
author_email = self.changes.get_latest_email_by_author(i)
|
||||
resp_xml += "<h3><img src=\"{0}\"/>{1} {2}</h3>".format(gravatar.get_url(author_email, size=32),
|
||||
i, _(MOSTLY_RESPONSIBLE_FOR_TEXT))
|
||||
resp_xml += '<h3><img src="{0}"/>{1} {2}</h3>'.format(
|
||||
gravatar.get_url(author_email, size=32), i, _(MOSTLY_RESPONSIBLE_FOR_TEXT)
|
||||
)
|
||||
else:
|
||||
resp_xml += "<h3>{0} {1}</h3>".format(i, _(MOSTLY_RESPONSIBLE_FOR_TEXT))
|
||||
|
||||
for j, entry in enumerate(responsibilities):
|
||||
resp_xml += "<div" + (" class=\"odd\">" if j % 2 == 1 else ">") + entry[1] + \
|
||||
" (" + str(entry[0]) + " eloc)</div>"
|
||||
resp_xml += "<div" + (' class="odd">' if j % 2 == 1 else ">") + entry[1] + " (" + str(entry[0]) + " eloc)</div>"
|
||||
if j >= 9:
|
||||
break
|
||||
|
||||
|
@ -83,7 +85,7 @@ class ResponsibilitiesOutput(Outputable):
|
|||
print(resp_xml)
|
||||
|
||||
def output_json(self):
|
||||
message_json = "\t\t\t\"message\": \"" + _(RESPONSIBILITIES_INFO_TEXT) + "\",\n"
|
||||
message_json = '\t\t\t"message": "' + _(RESPONSIBILITIES_INFO_TEXT) + '",\n'
|
||||
resp_json = ""
|
||||
|
||||
for i in sorted(set(i[0] for i in self.blame.blames)):
|
||||
|
@ -93,15 +95,15 @@ class ResponsibilitiesOutput(Outputable):
|
|||
author_email = self.changes.get_latest_email_by_author(i)
|
||||
|
||||
resp_json += "{\n"
|
||||
resp_json += "\t\t\t\t\"name\": \"" + i + "\",\n"
|
||||
resp_json += "\t\t\t\t\"email\": \"" + author_email + "\",\n"
|
||||
resp_json += "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n"
|
||||
resp_json += "\t\t\t\t\"files\": [\n\t\t\t\t"
|
||||
resp_json += '\t\t\t\t"name": "' + i + '",\n'
|
||||
resp_json += '\t\t\t\t"email": "' + author_email + '",\n'
|
||||
resp_json += '\t\t\t\t"gravatar": "' + gravatar.get_url(author_email) + '",\n'
|
||||
resp_json += '\t\t\t\t"files": [\n\t\t\t\t'
|
||||
|
||||
for j, entry in enumerate(responsibilities):
|
||||
resp_json += "{\n"
|
||||
resp_json += "\t\t\t\t\t\"name\": \"" + entry[1] + "\",\n"
|
||||
resp_json += "\t\t\t\t\t\"rows\": " + str(entry[0]) + "\n"
|
||||
resp_json += '\t\t\t\t\t"name": "' + entry[1] + '",\n'
|
||||
resp_json += '\t\t\t\t\t"rows": ' + str(entry[0]) + "\n"
|
||||
resp_json += "\t\t\t\t},"
|
||||
|
||||
if j >= 9:
|
||||
|
@ -111,7 +113,7 @@ class ResponsibilitiesOutput(Outputable):
|
|||
resp_json += "]\n\t\t\t},"
|
||||
|
||||
resp_json = resp_json[:-1]
|
||||
print(",\n\t\t\"responsibilities\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + resp_json + "]\n\t\t}", end="")
|
||||
print(',\n\t\t"responsibilities": {\n' + message_json + '\t\t\t"authors": [\n\t\t\t' + resp_json + "]\n\t\t}", end="")
|
||||
|
||||
def output_xml(self):
|
||||
message_xml = "\t\t<message>" + _(RESPONSIBILITIES_INFO_TEXT) + "</message>\n"
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import textwrap
|
||||
from ..localization import N_
|
||||
from .. import format, gravatar, terminal, timeline
|
||||
|
@ -27,6 +26,7 @@ from .outputable import Outputable
|
|||
TIMELINE_INFO_TEXT = N_("The following history timeline has been gathered from the repository")
|
||||
MODIFIED_ROWS_TEXT = N_("Modified Rows:")
|
||||
|
||||
|
||||
def __output_row__text__(timeline_data, periods, names):
|
||||
print("\n" + terminal.__bold__ + terminal.ljust(_("Author"), 20), end=" ")
|
||||
|
||||
|
@ -37,30 +37,32 @@ def __output_row__text__(timeline_data, periods, names):
|
|||
|
||||
for name in names:
|
||||
if timeline_data.is_author_in_periods(periods, name[0]):
|
||||
print(terminal.ljust(name[0], 20)[0:20 - terminal.get_excess_column_count(name[0])], end=" ")
|
||||
print(terminal.ljust(name[0], 20)[0 : 20 - terminal.get_excess_column_count(name[0])], end=" ")
|
||||
|
||||
for period in periods:
|
||||
multiplier = timeline_data.get_multiplier(period, 9)
|
||||
signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier)
|
||||
signs_str = (signs[1] * "-" + signs[0] * "+")
|
||||
print (("." if timeline_data.is_author_in_period(period, name[0]) and
|
||||
len(signs_str) == 0 else signs_str).rjust(10), end=" ")
|
||||
signs_str = signs[1] * "-" + signs[0] * "+"
|
||||
print(
|
||||
("." if timeline_data.is_author_in_period(period, name[0]) and len(signs_str) == 0 else signs_str).rjust(10), end=" ",
|
||||
)
|
||||
print("")
|
||||
|
||||
print(terminal.__bold__ + terminal.ljust(_(MODIFIED_ROWS_TEXT), 20) + terminal.__normal__, end=" ")
|
||||
print(terminal.__bold__ + terminal.ljust(_(MODIFIED_ROWS_TEXT), 20) + terminal.__normal__, end=" ")
|
||||
|
||||
for period in periods:
|
||||
total_changes = str(timeline_data.get_total_changes_in_period(period)[2])
|
||||
|
||||
if hasattr(total_changes, 'decode'):
|
||||
if hasattr(total_changes, "decode"):
|
||||
total_changes = total_changes.decode("utf-8", "replace")
|
||||
|
||||
print(terminal.rjust(total_changes, 10), end=" ")
|
||||
|
||||
print("")
|
||||
|
||||
|
||||
def __output_row__html__(timeline_data, periods, names):
|
||||
timeline_xml = "<table class=\"git full\"><thead><tr><th>" + _("Author") + "</th>"
|
||||
timeline_xml = '<table class="git full"><thead><tr><th>' + _("Author") + "</th>"
|
||||
|
||||
for period in periods:
|
||||
timeline_xml += "<th>" + str(period) + "</th>"
|
||||
|
@ -70,17 +72,17 @@ def __output_row__html__(timeline_data, periods, names):
|
|||
|
||||
for name in names:
|
||||
if timeline_data.is_author_in_periods(periods, name[0]):
|
||||
timeline_xml += "<tr" + (" class=\"odd\">" if i % 2 == 1 else ">")
|
||||
timeline_xml += "<tr" + (' class="odd">' if i % 2 == 1 else ">")
|
||||
|
||||
if format.get_selected() == "html":
|
||||
timeline_xml += "<td><img src=\"{0}\"/>{1}</td>".format(gravatar.get_url(name[1]), name[0])
|
||||
timeline_xml += '<td><img src="{0}"/>{1}</td>'.format(gravatar.get_url(name[1]), name[0])
|
||||
else:
|
||||
timeline_xml += "<td>" + name[0] + "</td>"
|
||||
|
||||
for period in periods:
|
||||
multiplier = timeline_data.get_multiplier(period, 18)
|
||||
signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier)
|
||||
signs_str = (signs[1] * "<div class=\"remove\"> </div>" + signs[0] * "<div class=\"insert\"> </div>")
|
||||
signs_str = signs[1] * '<div class="remove"> </div>' + signs[0] * '<div class="insert"> </div>'
|
||||
|
||||
timeline_xml += "<td>" + ("." if timeline_data.is_author_in_period(period, name[0]) and len(signs_str) == 0 else signs_str)
|
||||
timeline_xml += "</td>"
|
||||
|
@ -96,6 +98,7 @@ def __output_row__html__(timeline_data, periods, names):
|
|||
timeline_xml += "</tr></tfoot></tbody></table>"
|
||||
print(timeline_xml)
|
||||
|
||||
|
||||
class TimelineOutput(Outputable):
|
||||
def __init__(self, changes, useweeks):
|
||||
self.changes = changes
|
||||
|
@ -113,7 +116,7 @@ class TimelineOutput(Outputable):
|
|||
max_periods_per_row = int((width - 21) / 11)
|
||||
|
||||
for i in range(0, len(periods), max_periods_per_row):
|
||||
__output_row__text__(timeline_data, periods[i:i+max_periods_per_row], names)
|
||||
__output_row__text__(timeline_data, periods[i : i + max_periods_per_row], names)
|
||||
|
||||
def output_html(self):
|
||||
if self.changes.get_commits():
|
||||
|
@ -122,61 +125,60 @@ class TimelineOutput(Outputable):
|
|||
names = timeline_data.get_authors()
|
||||
max_periods_per_row = 8
|
||||
|
||||
timeline_xml = "<div><div id=\"timeline\" class=\"box\">"
|
||||
timeline_xml = '<div><div id="timeline" class="box">'
|
||||
timeline_xml += "<p>" + _(TIMELINE_INFO_TEXT) + ".</p>"
|
||||
print(timeline_xml)
|
||||
|
||||
for i in range(0, len(periods), max_periods_per_row):
|
||||
__output_row__html__(timeline_data, periods[i:i+max_periods_per_row], names)
|
||||
__output_row__html__(timeline_data, periods[i : i + max_periods_per_row], names)
|
||||
|
||||
timeline_xml = "</div></div>"
|
||||
print(timeline_xml)
|
||||
|
||||
def output_json(self):
|
||||
if self.changes.get_commits():
|
||||
message_json = "\t\t\t\"message\": \"" + _(TIMELINE_INFO_TEXT) + "\",\n"
|
||||
message_json = '\t\t\t"message": "' + _(TIMELINE_INFO_TEXT) + '",\n'
|
||||
timeline_json = ""
|
||||
periods_json = "\t\t\t\"period_length\": \"{0}\",\n".format("week" if self.useweeks else "month")
|
||||
periods_json += "\t\t\t\"periods\": [\n\t\t\t"
|
||||
periods_json = '\t\t\t"period_length": "{0}",\n'.format("week" if self.useweeks else "month")
|
||||
periods_json += '\t\t\t"periods": [\n\t\t\t'
|
||||
|
||||
timeline_data = timeline.TimelineData(self.changes, self.useweeks)
|
||||
periods = timeline_data.get_periods()
|
||||
names = timeline_data.get_authors()
|
||||
|
||||
for period in periods:
|
||||
name_json = "\t\t\t\t\"name\": \"" + str(period) + "\",\n"
|
||||
authors_json = "\t\t\t\t\"authors\": [\n\t\t\t\t"
|
||||
name_json = '\t\t\t\t"name": "' + str(period) + '",\n'
|
||||
authors_json = '\t\t\t\t"authors": [\n\t\t\t\t'
|
||||
|
||||
for name in names:
|
||||
if timeline_data.is_author_in_period(period, name[0]):
|
||||
multiplier = timeline_data.get_multiplier(period, 24)
|
||||
signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier)
|
||||
signs_str = (signs[1] * "-" + signs[0] * "+")
|
||||
signs_str = signs[1] * "-" + signs[0] * "+"
|
||||
|
||||
if len(signs_str) == 0:
|
||||
signs_str = "."
|
||||
|
||||
authors_json += "{\n\t\t\t\t\t\"name\": \"" + name[0] + "\",\n"
|
||||
authors_json += "\t\t\t\t\t\"email\": \"" + name[1] + "\",\n"
|
||||
authors_json += "\t\t\t\t\t\"gravatar\": \"" + gravatar.get_url(name[1]) + "\",\n"
|
||||
authors_json += "\t\t\t\t\t\"work\": \"" + signs_str + "\"\n\t\t\t\t},"
|
||||
authors_json += '{\n\t\t\t\t\t"name": "' + name[0] + '",\n'
|
||||
authors_json += '\t\t\t\t\t"email": "' + name[1] + '",\n'
|
||||
authors_json += '\t\t\t\t\t"gravatar": "' + gravatar.get_url(name[1]) + '",\n'
|
||||
authors_json += '\t\t\t\t\t"work": "' + signs_str + '"\n\t\t\t\t},'
|
||||
else:
|
||||
authors_json = authors_json[:-1]
|
||||
|
||||
authors_json += "],\n"
|
||||
modified_rows_json = "\t\t\t\t\"modified_rows\": " + \
|
||||
str(timeline_data.get_total_changes_in_period(period)[2]) + "\n"
|
||||
modified_rows_json = '\t\t\t\t"modified_rows": ' + str(timeline_data.get_total_changes_in_period(period)[2]) + "\n"
|
||||
timeline_json += "{\n" + name_json + authors_json + modified_rows_json + "\t\t\t},"
|
||||
else:
|
||||
timeline_json = timeline_json[:-1]
|
||||
|
||||
print(",\n\t\t\"timeline\": {\n" + message_json + periods_json + timeline_json + "]\n\t\t}", end="")
|
||||
print(',\n\t\t"timeline": {\n' + message_json + periods_json + timeline_json + "]\n\t\t}", end="")
|
||||
|
||||
def output_xml(self):
|
||||
if self.changes.get_commits():
|
||||
message_xml = "\t\t<message>" + _(TIMELINE_INFO_TEXT) + "</message>\n"
|
||||
timeline_xml = ""
|
||||
periods_xml = "\t\t<periods length=\"{0}\">\n".format("week" if self.useweeks else "month")
|
||||
periods_xml = '\t\t<periods length="{0}">\n'.format("week" if self.useweeks else "month")
|
||||
|
||||
timeline_data = timeline.TimelineData(self.changes, self.useweeks)
|
||||
periods = timeline_data.get_periods()
|
||||
|
@ -190,7 +192,7 @@ class TimelineOutput(Outputable):
|
|||
if timeline_data.is_author_in_period(period, name[0]):
|
||||
multiplier = timeline_data.get_multiplier(period, 24)
|
||||
signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier)
|
||||
signs_str = (signs[1] * "-" + signs[0] * "+")
|
||||
signs_str = signs[1] * "-" + signs[0] * "+"
|
||||
|
||||
if len(signs_str) == 0:
|
||||
signs_str = "."
|
||||
|
@ -201,8 +203,9 @@ class TimelineOutput(Outputable):
|
|||
authors_xml += "\t\t\t\t\t\t<work>" + signs_str + "</work>\n\t\t\t\t\t</author>\n"
|
||||
|
||||
authors_xml += "\t\t\t\t</authors>\n"
|
||||
modified_rows_xml = "\t\t\t\t<modified_rows>" + \
|
||||
str(timeline_data.get_total_changes_in_period(period)[2]) + "</modified_rows>\n"
|
||||
modified_rows_xml = (
|
||||
"\t\t\t\t<modified_rows>" + str(timeline_data.get_total_changes_in_period(period)[2]) + "</modified_rows>\n"
|
||||
)
|
||||
timeline_xml += "\t\t\t<period>\n" + name_xml + authors_xml + modified_rows_xml + "\t\t\t</period>\n"
|
||||
|
||||
print("\t<timeline>\n" + message_xml + periods_xml + timeline_xml + "\t\t</periods>\n\t</timeline>")
|
||||
|
|
|
@ -17,18 +17,17 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
class ResponsibiltyEntry(object):
|
||||
class ResponsibiltyEntry():
|
||||
blames = {}
|
||||
|
||||
class Responsibilities(object):
|
||||
|
||||
class Responsibilities():
|
||||
@staticmethod
|
||||
def get(blame, author_name):
|
||||
author_blames = {}
|
||||
|
||||
for i in blame.blames.items():
|
||||
for i in list(blame.blames.items()):
|
||||
if author_name == i[0][0]:
|
||||
total_rows = i[1].rows - i[1].comments
|
||||
if total_rows > 0:
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import platform
|
||||
|
@ -29,12 +29,13 @@ __normal__ = "\033[0;0m"
|
|||
|
||||
DEFAULT_TERMINAL_SIZE = (80, 25)
|
||||
|
||||
|
||||
def __get_size_windows__():
|
||||
res = None
|
||||
try:
|
||||
from ctypes import windll, create_string_buffer
|
||||
|
||||
handler = windll.kernel32.GetStdHandle(-12) # stderr
|
||||
handler = windll.kernel32.GetStdHandle(-12) # stderr
|
||||
csbi = create_string_buffer(22)
|
||||
res = windll.kernel32.GetConsoleScreenBufferInfo(handler, csbi)
|
||||
except:
|
||||
|
@ -42,6 +43,7 @@ def __get_size_windows__():
|
|||
|
||||
if res:
|
||||
import struct
|
||||
|
||||
(_, _, _, _, _, left, top, right, bottom, _, _) = struct.unpack("hhhhHhhhhhh", csbi.raw)
|
||||
sizex = right - left + 1
|
||||
sizey = bottom - top + 1
|
||||
|
@ -49,11 +51,13 @@ def __get_size_windows__():
|
|||
else:
|
||||
return DEFAULT_TERMINAL_SIZE
|
||||
|
||||
|
||||
def __get_size_linux__():
|
||||
def ioctl_get_window_size(file_descriptor):
|
||||
try:
|
||||
import fcntl, termios, struct
|
||||
size = struct.unpack('hh', fcntl.ioctl(file_descriptor, termios.TIOCGWINSZ, "1234"))
|
||||
|
||||
size = struct.unpack("hh", fcntl.ioctl(file_descriptor, termios.TIOCGWINSZ, "1234"))
|
||||
except:
|
||||
return DEFAULT_TERMINAL_SIZE
|
||||
|
||||
|
@ -76,9 +80,11 @@ def __get_size_linux__():
|
|||
|
||||
return int(size[1]), int(size[0])
|
||||
|
||||
|
||||
def clear_row():
|
||||
print("\r", end="")
|
||||
|
||||
|
||||
def skip_escapes(skip):
|
||||
if skip:
|
||||
global __bold__
|
||||
|
@ -86,9 +92,11 @@ def skip_escapes(skip):
|
|||
__bold__ = ""
|
||||
__normal__ = ""
|
||||
|
||||
|
||||
def printb(string):
|
||||
print(__bold__ + string + __normal__)
|
||||
|
||||
|
||||
def get_size():
|
||||
width = 0
|
||||
height = 0
|
||||
|
@ -98,7 +106,7 @@ def get_size():
|
|||
|
||||
if current_os == "Windows":
|
||||
(width, height) = __get_size_windows__()
|
||||
elif current_os == "Linux" or current_os == "Darwin" or current_os.startswith("CYGWIN"):
|
||||
elif current_os == "Linux" or current_os == "Darwin" or current_os.startswith("CYGWIN"):
|
||||
(width, height) = __get_size_linux__()
|
||||
|
||||
if width > 0:
|
||||
|
@ -106,14 +114,17 @@ def get_size():
|
|||
|
||||
return DEFAULT_TERMINAL_SIZE
|
||||
|
||||
|
||||
def set_stdout_encoding():
|
||||
if not sys.stdout.isatty() and sys.version_info < (3,):
|
||||
sys.stdout = codecs.getwriter("utf-8")(sys.stdout)
|
||||
|
||||
|
||||
def set_stdin_encoding():
|
||||
if not sys.stdin.isatty() and sys.version_info < (3,):
|
||||
sys.stdin = codecs.getreader("utf-8")(sys.stdin)
|
||||
|
||||
|
||||
def convert_command_line_to_utf8():
|
||||
try:
|
||||
argv = []
|
||||
|
@ -125,13 +136,20 @@ def convert_command_line_to_utf8():
|
|||
except AttributeError:
|
||||
return sys.argv
|
||||
|
||||
|
||||
def check_terminal_encoding():
|
||||
if sys.stdout.isatty() and (sys.stdout.encoding == None or sys.stdin.encoding == None):
|
||||
print(_("WARNING: The terminal encoding is not correctly configured. gitinspector might malfunction. "
|
||||
"The encoding can be configured with the environment variable 'PYTHONIOENCODING'."), file=sys.stderr)
|
||||
if sys.stdout.isatty() and (sys.stdout.encoding is None or sys.stdin.encoding is None):
|
||||
print(
|
||||
_(
|
||||
"WARNING: The terminal encoding is not correctly configured. gitinspector might malfunction. "
|
||||
"The encoding can be configured with the environment variable 'PYTHONIOENCODING'."
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
def get_excess_column_count(string):
|
||||
width_mapping = {'F': 2, 'H': 1, 'W': 2, 'Na': 1, 'N': 1, 'A': 1}
|
||||
width_mapping = {"F": 2, "H": 1, "W": 2, "Na": 1, "N": 1, "A": 1}
|
||||
result = 0
|
||||
|
||||
for i in string:
|
||||
|
@ -140,19 +158,22 @@ def get_excess_column_count(string):
|
|||
|
||||
return result - len(string)
|
||||
|
||||
|
||||
def ljust(string, pad):
|
||||
return string.ljust(pad - get_excess_column_count(string))
|
||||
|
||||
|
||||
def rjust(string, pad):
|
||||
return string.rjust(pad - get_excess_column_count(string))
|
||||
|
||||
|
||||
def output_progress(text, pos, length):
|
||||
if sys.stdout.isatty():
|
||||
(width, _unused) = get_size()
|
||||
progress_text = text.format(100 * pos / length)
|
||||
|
||||
if len(progress_text) > width:
|
||||
progress_text = "...%s" % progress_text[-width+3:]
|
||||
progress_text = "...%s" % progress_text[-width + 3 :]
|
||||
|
||||
print("\r{0}\r{1}".format(" " * width, progress_text), end="")
|
||||
sys.stdout.flush()
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
class TimelineData(object):
|
||||
|
||||
class TimelineData():
|
||||
def __init__(self, changes, useweeks):
|
||||
authordateinfo_list = sorted(changes.get_authordateinfo_list().items())
|
||||
self.changes = changes
|
||||
|
@ -37,7 +38,7 @@ class TimelineData(object):
|
|||
else:
|
||||
key = (i[0][1], i[0][0][0:7])
|
||||
|
||||
if self.entries.get(key, None) == None:
|
||||
if self.entries.get(key, None) is None:
|
||||
self.entries[key] = i[1]
|
||||
else:
|
||||
self.entries[key].insertions += i[1].insertions
|
||||
|
@ -49,12 +50,11 @@ class TimelineData(object):
|
|||
|
||||
for author in self.get_authors():
|
||||
entry = self.entries.get((author[0], period), None)
|
||||
if entry != None:
|
||||
if entry is not None:
|
||||
total_insertions += entry.insertions
|
||||
total_deletions += entry.deletions
|
||||
|
||||
self.total_changes_by_period[period] = (total_insertions, total_deletions,
|
||||
total_insertions + total_deletions)
|
||||
self.total_changes_by_period[period] = (total_insertions, total_deletions, total_insertions + total_deletions)
|
||||
|
||||
def get_periods(self):
|
||||
return sorted(set([i[1] for i in self.entries]))
|
||||
|
@ -63,7 +63,7 @@ class TimelineData(object):
|
|||
return self.total_changes_by_period[period]
|
||||
|
||||
def get_authors(self):
|
||||
return sorted(set([(i[0][0], self.changes.get_latest_email_by_author(i[0][0])) for i in self.entries.items()]))
|
||||
return sorted(set([(i[0][0], self.changes.get_latest_email_by_author(i[0][0])) for i in list(self.entries.items())]))
|
||||
|
||||
def get_author_signs_in_period(self, author, period, multiplier):
|
||||
authorinfo = self.entries.get((author, period), None)
|
||||
|
@ -91,7 +91,7 @@ class TimelineData(object):
|
|||
multiplier += 0.25
|
||||
|
||||
def is_author_in_period(self, period, author):
|
||||
return self.entries.get((author, period), None) != None
|
||||
return self.entries.get((author, period), None) is not None
|
||||
|
||||
def is_author_in_periods(self, periods, author):
|
||||
for period in periods:
|
||||
|
|
|
@ -17,18 +17,22 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from . import localization
|
||||
|
||||
localization.init()
|
||||
|
||||
__version__ = "0.5.0dev"
|
||||
|
||||
__doc__ = _("""Copyright © 2012-2015 Ejwa Software. All rights reserved.
|
||||
__doc__ = _(
|
||||
"""Copyright © 2012-2015 Ejwa Software. All rights reserved.
|
||||
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
|
||||
This is free software: you are free to change and redistribute it.
|
||||
There is NO WARRANTY, to the extent permitted by law.
|
||||
|
||||
Written by Adam Waldenberg.""")
|
||||
Written by Adam Waldenberg."""
|
||||
)
|
||||
|
||||
|
||||
def output():
|
||||
print("gitinspector {0}\n".format(__version__) + __doc__)
|
||||
|
|
2
pyproject.toml
Normal file
2
pyproject.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[tool.coverage.run]
|
||||
relative_files = true
|
49
requirements.txt
Normal file
49
requirements.txt
Normal file
|
@ -0,0 +1,49 @@
|
|||
-i https://pypi.org/simple/
|
||||
appdirs==1.4.4
|
||||
attrs==21.2.0
|
||||
black==20.8b1
|
||||
bleach==3.3.0
|
||||
certifi==2021.5.30
|
||||
cffi==1.14.5
|
||||
chardet==4.0.0
|
||||
click==8.0.1
|
||||
colorama==0.4.4
|
||||
coverage==5.5
|
||||
coveralls==3.0.1
|
||||
cryptography==3.4.7
|
||||
docopt==0.6.2
|
||||
docutils==0.17.1
|
||||
flake8==3.8.4
|
||||
idna==2.10
|
||||
importlib-metadata==4.4.0
|
||||
iniconfig==1.1.1
|
||||
jeepney==0.6.0 ; sys_platform == 'linux'
|
||||
keyring==23.0.1
|
||||
mccabe==0.6.1
|
||||
mypy-extensions==0.4.3
|
||||
packaging==20.9
|
||||
pathspec==0.8.1
|
||||
pkginfo==1.7.0
|
||||
pluggy==1.0.0.dev0
|
||||
py==1.10.0
|
||||
pycodestyle==2.6.0
|
||||
pycparser==2.20
|
||||
pyflakes==2.2.0
|
||||
pygments==2.9.0
|
||||
pyparsing==3.0.0b2
|
||||
pytest==7.0.1
|
||||
readme-renderer==29.0
|
||||
regex==2021.4.4
|
||||
requests-toolbelt==0.9.1
|
||||
requests==2.25.1
|
||||
rfc3986==1.5.0
|
||||
secretstorage==3.3.1 ; sys_platform == 'linux'
|
||||
six==1.16.0
|
||||
toml==0.10.2
|
||||
tqdm==4.61.0
|
||||
twine==3.3.0
|
||||
typed-ast==1.4.3
|
||||
typing-extensions==3.10.0.0
|
||||
urllib3==1.26.5
|
||||
webencodings==0.5.1
|
||||
zipp==3.4.1
|
32
tests/test_basedir.py
Normal file
32
tests/test_basedir.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from gitinspector import basedir
|
||||
|
||||
|
||||
class TestBasedirModule(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
self.TEST_BASEDIR = Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
self.PROJECT_BASEDIR = Path(self.TEST_BASEDIR).parent
|
||||
self.MODULE_BASEDIR = Path(self.PROJECT_BASEDIR, 'gitinspector')
|
||||
self.CWD = os.getcwd()
|
||||
|
||||
def test_get_basedir(self):
|
||||
expected = str(self.MODULE_BASEDIR)
|
||||
actual = basedir.get_basedir()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_get_basedir_git(self):
|
||||
expected = self.CWD
|
||||
actual = basedir.get_basedir_git()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_get_basedir_git_with_path(self):
|
||||
expected = str(self.PROJECT_BASEDIR)
|
||||
actual = basedir.get_basedir_git(self.TEST_BASEDIR)
|
||||
self.assertEqual(expected, actual)
|
24
tests/test_blame.py
Normal file
24
tests/test_blame.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from gitinspector import blame
|
||||
|
||||
|
||||
class TestBlameModule(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
self.TEST_BASEDIR = Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
self.PROJECT_BASEDIR = Path(self.TEST_BASEDIR).parent
|
||||
self.MODULE_BASEDIR = Path(self.PROJECT_BASEDIR, 'gitinspector')
|
||||
self.CWD = os.getcwd()
|
||||
|
||||
def test_BlameEntry_attrs(self):
|
||||
blame_entry = blame.BlameEntry()
|
||||
expected = 0
|
||||
self.assertEqual(expected, blame_entry.rows)
|
||||
self.assertEqual(expected, blame_entry.skew)
|
||||
self.assertEqual(expected, blame_entry.comments)
|
115
tests/test_changes.py
Normal file
115
tests/test_changes.py
Normal file
|
@ -0,0 +1,115 @@
|
|||
import unittest
|
||||
from gitinspector import changes
|
||||
|
||||
|
||||
FAKE_FILE_NAME = 'Arbitrary.ext'
|
||||
FAKE_COMMIT_STRING = "1614563270|2021-02-28|53d81bcd2612dbc47e73c71ee43baae83c1ec252|JP White|jpwhite3@gmail.com"
|
||||
|
||||
|
||||
class TestAuthorInfo(unittest.TestCase):
|
||||
|
||||
def test_AuthorInfo_attrs(self):
|
||||
author = changes.AuthorInfo()
|
||||
expected_email = None
|
||||
expected_insertions = 0
|
||||
expected_deletions = 0
|
||||
expected_commits = 0
|
||||
self.assertEqual(expected_email, author.email)
|
||||
self.assertEqual(expected_insertions, author.insertions)
|
||||
self.assertEqual(expected_deletions, author.deletions)
|
||||
self.assertEqual(expected_commits, author.commits)
|
||||
|
||||
|
||||
class TestFileDiff(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_FileDiff_init(self):
|
||||
test_string = 'ArbitraryName|-++-+'
|
||||
file_diff = changes.FileDiff(test_string)
|
||||
expected_name = 'ArbitraryName'
|
||||
self.assertEqual(expected_name, file_diff.name)
|
||||
expected_insertions = 3
|
||||
self.assertEqual(expected_insertions, file_diff.insertions)
|
||||
expected_deletions = 2
|
||||
self.assertEqual(expected_deletions, file_diff.deletions)
|
||||
|
||||
def test_is_not_filediff_line(self):
|
||||
actual = changes.FileDiff.is_filediff_line(FAKE_FILE_NAME)
|
||||
self.assertFalse(actual)
|
||||
|
||||
def test_is_filediff_line(self):
|
||||
test_file_diff_string = "arbitrary|--- a/file.txt"
|
||||
actual = changes.FileDiff.is_filediff_line(test_file_diff_string)
|
||||
self.assertTrue(actual)
|
||||
|
||||
def test_get_extension(self):
|
||||
expected = 'ext'
|
||||
actual = changes.FileDiff.get_extension(FAKE_FILE_NAME)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_get_extension_from_file_without_extension(self):
|
||||
test_file_name = 'Arbitrary'
|
||||
expected = ''
|
||||
actual = changes.FileDiff.get_extension(test_file_name)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_get_filename(self):
|
||||
expected = FAKE_FILE_NAME
|
||||
actual = changes.FileDiff.get_filename(expected)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_is_not_valid_extension(self):
|
||||
return_value = changes.FileDiff.is_valid_extension(FAKE_FILE_NAME)
|
||||
self.assertFalse(return_value)
|
||||
|
||||
def test_is_valid_extension(self):
|
||||
test_file_name = 'Arbitrary.cpp'
|
||||
return_value = changes.FileDiff.is_valid_extension(test_file_name)
|
||||
self.assertTrue(return_value)
|
||||
|
||||
|
||||
class TestCommitClass(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_Commit_init(self):
|
||||
commit = changes.Commit(FAKE_COMMIT_STRING)
|
||||
expected_timestamp = '1614563270'
|
||||
expected_date = '2021-02-28'
|
||||
expected_sha = '53d81bcd2612dbc47e73c71ee43baae83c1ec252'
|
||||
expected_author = 'JP White'
|
||||
expected_email = 'jpwhite3@gmail.com'
|
||||
self.assertEqual(expected_timestamp, commit.timestamp)
|
||||
self.assertEqual(expected_date, commit.date)
|
||||
self.assertEqual(expected_sha, commit.sha)
|
||||
self.assertEqual(expected_author, commit.author)
|
||||
self.assertEqual(expected_email, commit.email)
|
||||
|
||||
def test_get_author_and_email(self):
|
||||
expected_author = 'JP White'
|
||||
expected_email = 'jpwhite3@gmail.com'
|
||||
actual_author, actual_email = changes.Commit.get_author_and_email(FAKE_COMMIT_STRING)
|
||||
self.assertEqual(expected_author, actual_author)
|
||||
self.assertEqual(expected_email, actual_email)
|
||||
|
||||
def test_is_commit_line(self):
|
||||
return_value = changes.Commit.is_commit_line(FAKE_COMMIT_STRING)
|
||||
self.assertTrue(return_value)
|
||||
|
||||
def test_add_filediff(self):
|
||||
commit = changes.Commit(FAKE_COMMIT_STRING)
|
||||
commit.add_filediff(1)
|
||||
expected = [1]
|
||||
actual = commit.get_filediffs()
|
||||
self.assertEqual(expected, actual)
|
|
@ -19,32 +19,33 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import sys
|
||||
import unittest2
|
||||
import unittest
|
||||
import gitinspector.comment
|
||||
|
||||
|
||||
def __test_extension__(commented_file, extension):
|
||||
base = os.path.dirname(os.path.realpath(__file__))
|
||||
tex_file = open(base + commented_file, "r")
|
||||
tex = tex_file.readlines()
|
||||
tex_file.close()
|
||||
base = os.path.dirname(os.path.realpath(__file__))
|
||||
tex_file = open(base + commented_file, "r")
|
||||
tex = tex_file.readlines()
|
||||
tex_file.close()
|
||||
|
||||
is_inside_comment = False
|
||||
comment_counter = 0
|
||||
for i in tex:
|
||||
i = i.decode("utf-8", "replace")
|
||||
(_, is_inside_comment) = gitinspector.comment.handle_comment_block(is_inside_comment, extension, i)
|
||||
if is_inside_comment or gitinspector.comment.is_comment(extension, i):
|
||||
comment_counter += 1
|
||||
is_inside_comment = False
|
||||
comment_counter = 0
|
||||
for i in tex:
|
||||
(_, is_inside_comment) = gitinspector.comment.handle_comment_block(is_inside_comment, extension, i)
|
||||
if is_inside_comment or gitinspector.comment.is_comment(extension, i):
|
||||
comment_counter += 1
|
||||
|
||||
return comment_counter
|
||||
return comment_counter
|
||||
|
||||
class TexFileTest(unittest2.TestCase):
|
||||
|
||||
class TexFileTest(unittest.TestCase):
|
||||
def test(self):
|
||||
comment_counter = __test_extension__("/resources/commented_file.tex", "tex")
|
||||
self.assertEqual(comment_counter, 30)
|
||||
comment_counter = __test_extension__("/resources/commented_file.tex", "tex")
|
||||
self.assertEqual(comment_counter, 30)
|
||||
|
||||
class CppFileTest(unittest2.TestCase):
|
||||
|
||||
class CppFileTest(unittest.TestCase):
|
||||
def test(self):
|
||||
comment_counter = __test_extension__("/resources/commented_file.cpp", "cpp")
|
||||
self.assertEqual(comment_counter, 25)
|
||||
comment_counter = __test_extension__("/resources/commented_file.cpp", "cpp")
|
||||
self.assertEqual(comment_counter, 25)
|
||||
|
|
37
tests/test_config.py
Normal file
37
tests/test_config.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
import unittest
|
||||
from gitinspector import config
|
||||
|
||||
|
||||
class TestConfig(unittest.TestCase):
|
||||
|
||||
def test_GitConfig_init(self):
|
||||
expected_run = 'run'
|
||||
expected_repo = 'repo'
|
||||
expected_global_only = False
|
||||
test_config = config.GitConfig(expected_run, expected_repo)
|
||||
self.assertEqual(expected_run, test_config.run)
|
||||
self.assertEqual(expected_repo, test_config.repo)
|
||||
self.assertEqual(expected_global_only, test_config.global_only)
|
||||
|
||||
def test_read_git_config_unknown_variable(self):
|
||||
expected_return_value = ''
|
||||
test_config = config.GitConfig('arbitrary', '.')
|
||||
actual_return_value = test_config.__read_git_config__('unknown')
|
||||
self.assertEqual(expected_return_value, actual_return_value)
|
||||
|
||||
def test_read_git_config_string_unknown(self):
|
||||
expected_return_value = (False, None)
|
||||
test_config = config.GitConfig('arbitrary', '.')
|
||||
actual_return_value = test_config.__read_git_config_string__('unknown')
|
||||
self.assertEqual(expected_return_value, actual_return_value)
|
||||
|
||||
def test_read(self):
|
||||
class Dummy():
|
||||
pass
|
||||
test_config = config.GitConfig(Dummy(), '.')
|
||||
|
||||
with self.assertRaises(AttributeError):
|
||||
self.assertFalse(test_config.run.hard)
|
||||
|
||||
test_config.read()
|
||||
self.assertFalse(test_config.run.hard)
|
27
tests/test_extensions.py
Normal file
27
tests/test_extensions.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
import unittest
|
||||
from gitinspector import extensions
|
||||
|
||||
|
||||
class TestExtensions(unittest.TestCase):
|
||||
|
||||
def test_001_extensions_get(self):
|
||||
expected = extensions.DEFAULT_EXTENSIONS
|
||||
actual = extensions.get()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_002_extensions_define(self):
|
||||
expected = 'txt,md'
|
||||
extensions.define(expected)
|
||||
actual = extensions.get()
|
||||
self.assertEqual(expected.split(","), actual)
|
||||
|
||||
def test_003_add_located(self):
|
||||
expected = set('*')
|
||||
extensions.add_located('')
|
||||
actual = extensions.get_located()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
expected = set(['ext', '*'])
|
||||
extensions.add_located('ext')
|
||||
actual = extensions.get_located()
|
||||
self.assertEqual(expected, actual)
|
36
tests/test_filtering.py
Normal file
36
tests/test_filtering.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
import unittest
|
||||
from gitinspector import filtering
|
||||
|
||||
TEST_STRING = 'arbitrary'
|
||||
|
||||
|
||||
class TestFiltering(unittest.TestCase):
|
||||
|
||||
def test_InvalidRegExpError(self):
|
||||
with self.assertRaises(filtering.InvalidRegExpError):
|
||||
raise filtering.InvalidRegExpError(TEST_STRING)
|
||||
|
||||
def test_get(self):
|
||||
expected = filtering.__filters__
|
||||
actual = filtering.get()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_add(self):
|
||||
filtering.add(TEST_STRING)
|
||||
expected = [{TEST_STRING}, set()]
|
||||
actual = filtering.get()['file']
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_get_filered(self):
|
||||
filtering.add(TEST_STRING)
|
||||
expected = set()
|
||||
actual = filtering.get_filered()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_has_filtered(self):
|
||||
self.assertFalse(filtering.has_filtered())
|
||||
|
||||
def test_set_filtered(self):
|
||||
test_commit_sha = '53d81bcd2612dbc47e73c71ee43baae83c1ec252'
|
||||
return_value = filtering.set_filtered(test_commit_sha)
|
||||
self.assertFalse(return_value)
|
75
tests/test_format.py
Normal file
75
tests/test_format.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import unittest
|
||||
from hashlib import sha256
|
||||
from gitinspector import format
|
||||
from io import StringIO
|
||||
from contextlib import contextmanager
|
||||
|
||||
TEST_STRING = 'arbitrary'
|
||||
|
||||
|
||||
class DummyRepo:
|
||||
name = TEST_STRING
|
||||
|
||||
|
||||
@contextmanager
|
||||
def print_capture(*args, **kwds):
|
||||
temp_out = StringIO() # Create the in-memory "file"
|
||||
try:
|
||||
sys.stdout = temp_out # Replace default stdout (terminal) with our stream
|
||||
yield temp_out
|
||||
finally:
|
||||
sys.stdout = sys.__stdout__ # Restore default stdout
|
||||
|
||||
|
||||
class TestFormat(unittest.TestCase):
|
||||
|
||||
def test_InvalidFormatError(self):
|
||||
with self.assertRaises(format.InvalidFormatError):
|
||||
raise format.InvalidFormatError(TEST_STRING)
|
||||
|
||||
def test_select(self):
|
||||
test_format = 'json'
|
||||
return_value = format.select(test_format)
|
||||
self.assertTrue(return_value)
|
||||
|
||||
def test_get_selected(self):
|
||||
test_format = 'json'
|
||||
format.select(test_format)
|
||||
expected = test_format
|
||||
actual = format.get_selected()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_is_interactive_format(self):
|
||||
test_format = 'json'
|
||||
format.select(test_format)
|
||||
return_value = format.is_interactive_format()
|
||||
self.assertFalse(return_value)
|
||||
|
||||
def test__output_html_template__(self):
|
||||
test_template_path = os.path.join('html', 'html.header')
|
||||
return_value = format.__output_html_template__(test_template_path)
|
||||
return_value_hash = sha256(return_value.encode('utf-8')).hexdigest()
|
||||
expected_hash = '6b113dca32e7947e21ad9ad910c4995e62672ca4c0bc34577e33d2e328da7b3a'
|
||||
self.assertEqual(expected_hash, return_value_hash)
|
||||
|
||||
def test__get_zip_file_content__(self):
|
||||
return_value = format.__get_zip_file_content__('LICENSE.txt')
|
||||
return_value_hash = sha256(return_value.encode('utf-8')).hexdigest()
|
||||
expected_hash = '52cb566b16d84314b92b91361ed072eaaf166e8d3dfa3d0fd3577613925f205c'
|
||||
self.assertEqual(expected_hash, return_value_hash)
|
||||
|
||||
def test_json_output_header_and_footer(self):
|
||||
test_format = 'json'
|
||||
format.select(test_format)
|
||||
repos = [DummyRepo()]
|
||||
with print_capture() as output:
|
||||
format.output_header(repos)
|
||||
format.output_footer()
|
||||
output_text = output.getvalue()[:-2].replace('\n', '').replace('\t', '')[:-2] + "}}"
|
||||
output_json = json.loads(output_text)
|
||||
self.assertIn('report_date', output_json['gitinspector'])
|
||||
self.assertEqual(output_json['gitinspector']['repository'], 'arbitrary')
|
||||
self.assertEqual(output_json['gitinspector']['version'], '0.5.0dev')
|
35
tests/test_gitinspector.py
Normal file
35
tests/test_gitinspector.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
import unittest
|
||||
import json
|
||||
import pytest
|
||||
from gitinspector import gitinspector
|
||||
|
||||
TEST_STRING = 'arbitrary'
|
||||
|
||||
|
||||
class TestGitInspector(unittest.TestCase):
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def capsys(self, capsys):
|
||||
self.capsys = capsys
|
||||
|
||||
def test_Runner(self):
|
||||
test_runner = gitinspector.Runner()
|
||||
expected_attrs = {
|
||||
"hard": False,
|
||||
"include_metrics": False,
|
||||
"list_file_types": False,
|
||||
"localize_output": False,
|
||||
"responsibilities": False,
|
||||
"grading": False,
|
||||
"timeline": False,
|
||||
"useweeks": False
|
||||
}
|
||||
for key, val in expected_attrs.items():
|
||||
self.assertEqual(getattr(test_runner, key), val)
|
||||
|
||||
def test_main(self):
|
||||
self.maxDiff = None
|
||||
gitinspector.main()
|
||||
out, err = self.capsys.readouterr()
|
||||
json.loads(out)
|
||||
self.assertEqual(err, '')
|
13
tests/test_gravatar.py
Normal file
13
tests/test_gravatar.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
import unittest
|
||||
from gitinspector import gravatar
|
||||
|
||||
TEST_STRING = 'arbitrary'
|
||||
|
||||
|
||||
class TestGravatar(unittest.TestCase):
|
||||
|
||||
def test_get_url(self):
|
||||
expected_url = 'https://www.gravatar.com/avatar/c181b12d45d1fd849f885221f3ee3f39?default=identicon'
|
||||
arbitrary_email = TEST_STRING + '@example.com'
|
||||
actual_url = gravatar.get_url(arbitrary_email)
|
||||
self.assertEqual(expected_url, actual_url)
|
22
tests/test_interval.py
Normal file
22
tests/test_interval.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
import unittest
|
||||
from gitinspector import interval
|
||||
|
||||
TEST_STRING = 'arbitrary'
|
||||
|
||||
|
||||
class TestInterval(unittest.TestCase):
|
||||
|
||||
def test_has_interval(self):
|
||||
actual = interval.has_interval()
|
||||
self.assertFalse(actual)
|
||||
|
||||
def test_get_since(self):
|
||||
expected = ''
|
||||
actual = interval.get_since()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_set_since(self):
|
||||
expected = '--since=' + TEST_STRING
|
||||
interval.set_since(TEST_STRING)
|
||||
actual = interval.get_since()
|
||||
self.assertEqual(expected, actual)
|
Loading…
Add table
Reference in a new issue