はじめに
久しぶりにそれっぽいやつを作りたいなと思って作りました。
例のごとくまた、BitBucketでサンプルを置いておきます。
環境
フレームワーク
BottleというPythonの軽量Webフレームワークを使いました。
多分、自分が軽量Webフレームワークを使う中で1番使用回数が多いかなぁと。
ライブラリ
Python
- bottle==0.12.13
- GitPython==2.1.8
- python-dateutil==2.6.1
JavaScript
僕はJavaScriptなんて書けないし書く気もないので以下を使用させていただきました。
簡単な説明
リポジトリ自体がGitで管理されているのでその場に新たにGitリポジトリはcloneできないし mv
等の操作で追加してもゴミになるので git submodule
を使いました。
./submodules/
以下に新しく追加してBottleアプリを起動。 http://127.0.0.1:8080
にアクセスするとディレクトリのものだけがリストで表示されます。
Gitリポジトリではないものも表示されてしまうのでカレンダーを表示する画面でリポジトリかどうかの確認をしています。
Gitリポジトリであった場合は、カレンダーをD3.jsで表示します。
使い方
多分、使う人なんて滅多にいないと思いますが、
$ git clone --recursive https://bitbucket.org/nnsnodnb/git_calendar_graph.git # サンプルで入ってる django-ios-notifications も一緒に下ろしてきます $ cd git_calendar_graph $ pip install -r requirements.txt # 必要であればVirtualenv環境下で $ git submodule add https://github.com/hoge/repository.git submodules/repository # 新たにリポジトリをサブモジュールで追加します $ python app.py
これで起動しました!
サンプル表示
詳細について
app.py
からです。
/
へのアクセス
from bottle import route, template, HTTPResponse import constants import os import os.path @route('/') def repository(): files = os.listdir(constants.REPOSITORY_DIR) data = list( filter( lambda item: os.path.isdir(os.path.join(constants.REPOSITORY_DIR, item)), files ) ) return template('repository', data=data)
サブモジュールが入ってくるであろう、ディレクトリ内のファイル、ディレクトリを全部取得します。
そのあとに、ここでは、filterを使ってますが、 ディレクトリである ものだけをリストとして作成しました。
data = [item for item in files if os.path.isdir(os.path.join(constants.REPOSITORY, item))]
で普通に良かった。仕事柄、Pythonの内包表記を使う機会が全く無いので忘れていた。
あとは bottle.template
に流し込みます。今回は、Bottleが初めから持ってるテンプレートエンジンを使っています。これなんていうエンジンなんだ?
/<repository_name>
へのアクセス
from bottle import route, template, HTTPResponse from models import Calendar import os.path @route('/<repository_name>') def contribute(repository_name): if not os.path.exists(f'submodules/{repository_name}') or not os.path.exists(f'submodules/{repository_name}/.git'): body = '<html><body>Git repository not found.</body></html>' r = HTTPResponse(body=body, status=404) r.set_header('Content-Type', 'text/html') return r calendar = Calendar(repository=repository_name) data = calendar.create_days_list() commits = calendar.create_calendar_data(data) return template('contributes', repository_name=repository_name, commits=commits)
先述の通り、Gitリポジトリであるかどうかなど問答無用で入って来られた場合のこともあるので、ここでそのディレクトリがGitリポジトリであるかどうかの判断をします。
Gitリポジトリでなかったら404です。GitリポジトリであればそのままGitカレンダーを作成していきます。
ここでは基本的に Calendar
クラスを作って実装しています。
あとはデータをテンプレートに流し込んで終わりです。
静的ファイルの配信について
from bottle import route, static_files @route('/static/<file_path>') def static(file_path): return static_file(file_path, root='./static')
デフォルトで使える bottle.static_files
を使っています。 app.py
と同ディレクトリに static
フォルダを作ってそれにCSSやらJavaScriptやらをぶち込みました。
次、 models.py
についてです。
一応、 Calendar
クラスはモデル的なロジックを持っているのでモジュール名は models
にしてます。なんで複数形なのかは知りません。
from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta from git.repo import Repo import os.path import constants import multiprocessing class Calendar(object): def __init__(self, repository: str, now: datetime=datetime.now()): self.repository = repository self.now = self._convert_day_first(now) self.year_ago = self.now - relativedelta(years=1) # その日の 00:00:00 を取得 @staticmethod def _convert_day_first(now) -> datetime: return now - timedelta(hours=now.hour, minutes=now.minute, seconds=now.second) # 1年前から今日までの日付をstr型でlistで作成 def create_days_list(self) -> list: data = [] for day in range((self.now - self.year_ago).days + 1): data.append((self.year_ago + timedelta(day)).strftime('%Y-%m-%d')) return data # 指定日の総Contribute数を取得 def _get_data_dictionary(self, date: str) -> dict: repo = Repo(os.path.join(constants.REPOSITORY_DIR, self.repository)) commits = repo.iter_commits(rev='master', since=f'{date} 00:00+09:00', until=f'{date} 23:59:59+09:00') return { 'date': date, 'count': len(list(commits)) } # 1年間のデータ配列を作成 def create_calendar_data(self, data: list) -> list: pool = multiprocessing.Pool(multiprocessing.cpu_count()) commits = pool.map(self._get_data_dictionary, data) pool.close() return commits
インスタンス生成時に、リポジトリ名と今日の日付と1年前の日付データを取得しておきます。
create_days_list
メソッドでは、1年前から今日の日付までのデータをリストで生成です。 (例: 2018-02-10)
_get_data_dictionary
メソッドは一応、プロテクト的なあれで、外部から呼ばれたくないので _
を前置してます。
内容自体は、 GitPython
で日にちを指定して git log
を取得して repo.iter_commits
で取得した generator
をlistにして長さを取得してます。そうするとその日のコミット数を取得できました。
create_calendar_data
メソッドは、 app.py
から全部の日付データを受け取って multiprocessing
を使って _get_data_dictionary
に流しています。
ここのメソッドを呼ばないと中々に時間を吸収されます。今回は、コア数分の8つのプロセスで計算させています。それ以上しても D3.js
のレンダリング速度とかもあるので変わんなかったです。
8以下は試してないですが...
追記:
_get_data_dictionary
で作ってる repo
毎回作られてるしこれをイニシャライザで作ることによってもうちょっと計算速度上げられるかと思った。多分誤差
最後に、JavaScriptです。基本コピペなのでテンプレートエンジンのシンタックスの部分しか書いてないです。
var chartData = []; % for _commit in commits: chartData.push({ date: moment("{{ _commit['date'] }}").toDate(), count: {{ _commit['count'] }} }); % end var heatmap = calendarHeatmap() .data(chartData) .selector('#calendar') .tooltipEnabled(true) .colorRange(['#f4f7f7', '#af5916']) .onClick(function (data) { console.log('data', data); }); heatmap();
最後に
前々から、GitHubのカレンダーみたいなのを作ってみたいと思ってたので、やれてよかったです。
基本的にフロントエンドは書きたくないので、また何かしらの機会があれば、これを使った何かしらのそれを作っていきたいです。
あれ?作文?