Skip to content

Commit

Permalink
Merge branch 'release/0.2.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
Sumin Byeon committed Mar 1, 2017
2 parents f05159d + c3b72d2 commit 3a4b55f
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 91 deletions.
23 changes: 10 additions & 13 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,31 @@ indicates my own commits whereas the blue indicates your teammates'.
Installation
============

The latest code can be cloned from GitHub as follows:

.. code-block:: console
git clone https://github.com/suminb/gitstats
We are planning to register our project to PyPi in the near future, so please
stay tuned. Once the repository has been cloned, ``gitstats`` can be installed
as follows:

.. code-block:: console
pip install -e gitstats
pip install gitstats
Usage
=====

Discover all Git repositories in the home directory to generate statistics.
Here your email address is used to differentiate your commits from others.

.. code-block:: console
gitstats analyze ~
gitstats analyze --email ${your_email} ~
It may be a single Git repository.

.. code-block:: console
gitstats analyze ~/dev/projectx
gitstats analyze --email ${your_email} ~/work/project_x
If you would like to exclude certain repositories, put a ``.exclude`` file in
each directory you want to exclude from the statistics.

If you have multiple email addresses, you may pass them as follows:

.. code-block:: console
gitstats analyze --email ${your_email1} --email ${your_email2} ~
15 changes: 5 additions & 10 deletions gitstats/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import sys
import logging

from logbook import Logger, StreamHandler

__author__ = 'Sumin Byeon'
__email__ = 'suminb@gmail.com'
__version__ = '0.2.1'
__version__ = '0.2.2'


# TODO: Name the following variable as `log`
logger = logging.getLogger('gitstats')
# handler = logging.FileHandler('gitstats.log')
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(
logging.Formatter('%(asctime)s %(levelname)s %(message)s'))
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
StreamHandler(sys.stderr).push_application()
log = Logger(__name__)
66 changes: 31 additions & 35 deletions gitstats/__main__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import json
import os
# import StringIO

import click
from dateutil.parser import parse as parse_datetime

from gitstats import logger
from gitstats import log
from gitstats.utils import (
discover_repositories, generate_git_log, make_svg_report, process_log,
sort_by_year)
datetime_handler, discover_repositories, generate_git_log, make_svg_report,
get_annual_data, sort_by_year)


@click.group()
Expand All @@ -16,45 +17,40 @@ def cli():

@cli.command()
@click.argument('path', type=click.Path(exists=True))
@click.option('--year', type=str, help='Specify a year or \'all\'')
@click.option('--out')
def analyze(path, year, out):
def analyze(path):
"""Analyzes Git repositories to generate a report in JSON format."""

repositories = discover_repositories(os.path.expanduser(path))

logs = []
gitlogs = []
for repo in repositories:
try:
logs += generate_git_log(repo)
gitlogs += generate_git_log(repo)
except RuntimeError:
logger.warn('Not able to generate logs for {}', path)
log.warn('Not able to generate logs for {}', path)

log_by_year = sort_by_year(logs)
print(json.dumps(gitlogs, default=datetime_handler))

max_commits = []
for y in log_by_year:
data = process_log(log_by_year[y], y)
max_commits.append(data['max_commits'])

if not year:
try:
year = y
except NameError:
# When running `generate_git_log()` for an empty repository,
# `log_by_year` becomes an empty list and `y` won't have a chance
# to be assigned. We will refactor this function entirely so we
# will stick with the following temporary workaround.
logger.info('{} appears to be an empty repository', path)
return
else:
year = int(year)
global_max = max(max_commits)
processed_logs = process_log(log_by_year[year], year)
logger.info('Generating report for year {}'.format(year))
if out:
with open(out, 'w') as fout:
make_svg_report(processed_logs, global_max, fout)
else:
make_svg_report(processed_logs, global_max)
@cli.command()
@click.argument('json_input', type=click.File('r'))
@click.argument('year', type=int)
@click.option('--email', help='My email address', multiple=True, required=True)
def generate_graph(json_input, year, email):
"""Generates an annual commit graph in .svg format."""

gitlogs = json.loads(json_input.read())
gitlogs = [(x, y, parse_datetime(z)) for x, y, z in gitlogs]

gitlogs_by_year = sort_by_year(gitlogs)
annual_data = {}
for y, l in gitlogs_by_year.items():
annual_data[y] = get_annual_data(l, y, email)

global_max = max([x['max_commits'] for x in annual_data.values()])

log.info('Generating report for year {}', year)
make_svg_report(annual_data[year], global_max)


if __name__ == '__main__':
Expand Down
65 changes: 37 additions & 28 deletions gitstats/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
from datetime import datetime
import os
import subprocess
import sys

from dateutil.parser import parse as parse_datetime
from gitstats import __email__, logger
from gitstats import log


def datetime_handler(d):
if isinstance(d, datetime):
return d.isoformat()
raise TypeError('Unknown type')


def discover_repositories(root_path):
Expand All @@ -14,7 +21,7 @@ def discover_repositories(root_path):
for root, dirs, files in os.walk(root_path):
if os.path.exists(os.path.join(root, '.git')) and \
not os.path.exists(os.path.join(root, '.exclude')):
logger.info('Git repository discovered: {}'.format(root))
log.info('Git repository discovered: {}'.format(root))
repositories.append(root)

return repositories
Expand All @@ -31,7 +38,7 @@ def generate_git_log(path, format='format:%an|%ae|%ad'):
"""
abs_path = os.path.abspath(path)

logger.info('Analyzing %s' % abs_path)
log.info('Analyzing %s' % abs_path)
command = ['git', 'log', '--pretty={}'.format(format)]
try:
log_rows = subprocess.check_output(
Expand All @@ -42,40 +49,42 @@ def generate_git_log(path, format='format:%an|%ae|%ad'):
return [parse_log_row(row) for row in log_rows.strip().split('\n')]


def process_log(logs, year):
"""Filters out logs by the given year.
def get_annual_data(gitlogs, year, my_emails):
"""Filters out git logs by the given year.
:param logs: A list of (name, email, datetime) tuples
:type logs: list
:param gitlogs: A list of (name, email, datetime) tuples
:type gitlogs: list
:type year: int
:param my_emails: A list of email addresses
:type my_emails: list
:return:
A dictionary containing information required to draw a commit graph.
:rtype: dict
"""
daily_commits_mine = {}
daily_commits_others = {}

for log in logs:
email = log[1]
timetuple = log[2].timetuple()
for gitlog in gitlogs:
email = gitlog[1]
timetuple = gitlog[2].timetuple()
if timetuple.tm_year == year:
key = timetuple.tm_yday
yday = timetuple.tm_yday

# TODO: Make it more general...
is_mine = email == __email__
is_mine = email in my_emails

if is_mine:
if key not in daily_commits_mine:
daily_commits_mine[key] = 1
if yday not in daily_commits_mine:
daily_commits_mine[yday] = 1
else:
daily_commits_mine[key] += 1
daily_commits_mine[yday] += 1
else:
if key not in daily_commits_others:
daily_commits_others[key] = 1
if yday not in daily_commits_others:
daily_commits_others[yday] = 1
else:
daily_commits_others[key] += 1
daily_commits_others[yday] += 1

# Calculate the maximum number of commits
max_commits = 0
Expand All @@ -97,13 +106,13 @@ def parse_log_row(row):
return columns[0], columns[1], parse_datetime(columns[2])


def sort_by_year(log):
def sort_by_year(gitlog):
"""
:param log: parsed log
:type log: list
:param gitlog: parsed gitlog
:type gitlog: list
"""
basket = {}
for r in log:
for r in gitlog:
name, email, timestamp = r

timetuple = timestamp.timetuple()
Expand Down Expand Up @@ -135,10 +144,10 @@ def make_colorcode(color):
return '%02x%02x%02x' % color


def make_svg_report(log, global_max, out=sys.stdout):
def make_svg_report(gitlog, global_max, out=sys.stdout):
"""
:param log: parsed log for a particular year
:type log: dict
:param gitlog: parsed gitlog for a particular year
:type gitlog: dict
:param global_max: global maximum of the number of commits at any given day
:type global_max: int
Expand All @@ -161,8 +170,8 @@ def make_svg_report(log, global_max, out=sys.stdout):

out.write(svg_epilogue)

daily_commits_mine = log['daily_commits_mine']
daily_commits_others = log['daily_commits_others']
daily_commits_mine = gitlog['daily_commits_mine']
daily_commits_others = gitlog['daily_commits_others']

# Gives clear distinction between no-commit day and a day with at least one
# commit
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
click
python-dateutil
logbook>=1.0.0
pytest
pytest-cov
coveralls
12 changes: 7 additions & 5 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime

from gitstats.utils import average_color, generate_git_log, make_colorcode, \
parse_log_row, process_log, sort_by_year
from gitstats.utils import average_color, generate_git_log, get_annual_data, \
make_colorcode, parse_log_row, sort_by_year


def validate_log_row(columns):
Expand Down Expand Up @@ -54,12 +54,14 @@ def test_sort_by_year():
validate_log_row(row)


def test_process_log():
"""Ensures process_log() works as intended."""
def test_get_annual_data():
"""Ensures get_annual_data() works as intended."""

from gitstats import __email__

# Extract logs for the current repository
logs = generate_git_log('.')
logs2013 = process_log(logs, 2013)
logs2013 = get_annual_data(logs, 2013, [__email__])
assert logs2013['year'] == 2013
assert logs2013['daily_commits_mine']
assert logs2013['daily_commits_others'] == {}
Expand Down

0 comments on commit 3a4b55f

Please sign in to comment.