Я люблю аналитику и люблю git – поэтому я создал анализатор коммитов git 🙌.
Веб-приложение работает с вашим локальным git-репозиторием, а также интегрируется с GitHub.
Попробуйте его здесь или посмотрите репозиторий на GitHub.
Обзор
Git Commit Analyzer читает ваш журнал git, разбирает его на составляющие, а затем позволяет изучить его с помощью ряда удобных визуализаций.
Вы также можете выбрать импорт проекта непосредственно из GitHub.
Реализация
Если вам интересно, как я реализовал веб-приложение, надеюсь, этот раздел даст вам некоторое представление.
На целевой странице веб-приложения пользователь может либо загрузить git log
из локального репозитория, либо продолжить интеграцию с GitHub. В каждом случае данные будут получены, проверены, и пользователь сможет перейти на приборную панель. На приборной панели пользователь может создавать пользовательские визуализации для репозитория.
Технологии
Веб-приложение реализовано с использованием Vue.js и JavaScript. Я использую Bulma в качестве чистого CSS-фреймворка. Веб-приложение развернуто на бесплатном уровне Netlify. Я использовал Jest и Vue Test Utils для модульного тестирования реализации. Визуализация приборной панели реализована с помощью Vue Plotly.
Настройка рабочего процесса OAuth может быть несколько утомительной. К счастью, Pizzly предлагает удивительно простой способ позаботиться об этом. Pizzly предоставляет прокси-сервер для более чем 80 интеграций OAuth, и вы можете бесплатно развернуть свой собственный экземпляр на Heroku (как это сделал я).
Подведем итоги:
- JavaScript
- Vue.js как веб-фреймворк
- Vue Test Utils для тестирования
- Vue Plotly для визуализации
- Bulma как CSS фреймворк
- Pizzly для обработки GitHub OAuth
- Netlify для развертывания
Разбор журнала Git
Необработанный файл журнала git разбивается на отдельные коммиты с помощью регулярных выражений и преобразуется в объекты Commit
, которые выглядят примерно так:
class Commit:
hash: String
authorName: String
authorMail: String
timestamp: Date
message: String
nodes: Array[Node]
class Node:
path: String
additions: Integer
deletions: Integer
Если журнал git импортируется из GitHub, история JSON преобразуется в объекты Commit
аналогичным образом.
Интеграция с GitHub
Импорт истории коммитов из GitHub обычно довольно прост с помощью официального API GitHub. Однако настройка полного рабочего процесса OAuth для аутентификации в GitHub API может сделать этот процесс более трудоемким.
В процессе разработки я наткнулся на Pizzly, сервис с открытым исходным кодом, который действует как прокси-сервис для более чем 80 часто используемых API, среди которых GitHub. Вы можете бесплатно развернуть свой собственный экземпляр Pizzly на Heroku и использовать его для управления всеми вызовами API.
Рабочий процесс OAuth сводится к нескольким строкам кода:
import Pizzly from "pizzly-js";
// get environment variables
const HOST = process.env.VUE_APP_PIZZLY_HOST
const SECRET = process.env.VUE_APP_PIZZLY_SECRET_KEY
// create pizzly instance and integration instance
const pizzly = new Pizzly({host: HOST, publishableKey: SECRET})
const githubApi = pizzly.integration('github')
/**
* Perform the OAuth workflow using the GitHub API.
* @return authId
**/
const authenticate = function() {
return githubApi.connect()
}
Вызовите функцию authenticate
, и Pizzly позаботится о вашей аутентификации.
Для примера, чтобы получить названия репозиториев с определенной страницы API, вы можете вызвать следующую функцию. Вам также нужно передать authId
, возвращаемый во время процесса аутентификации.
/**
* Get the names of the repositories of a given page of the GitHub API.
* @param page (Number) page to get
* @param perPage (Number) entries per page
* @param authId (String) authentication ID from the auth workflow
* @return (Array) repository names
**/
const getRepoPage = function(page, perPage, authId) {
return githubApi
.auth(authId)
.get('/user/repos', {
headers: {"Content-Type": "application/vnd.github.v3+json"},
query: {"page": page, "per_page": perPage, "visibility": "all"}
})
.then(res => res.json())
.then(jsn => jsn.map(e => e.name))
}
Довольно аккуратно, вы не находите?
Работа с данными
При создании приборной панели я хотел предоставить пользователю как можно больше свободы в выборе показателей для визуализации. Применительно к простому двухмерному графику это означает, что пользователь должен иметь возможность выбирать, какая переменная находится на каждой оси, а также группировать данные по третьей переменной.
Реализовывать это было очень весело! Используя разобранный журнал git, содержащий объекты Commit
(как описано выше), я определил ряд функций, которые могут быть применены к массиву коммитов.
Эти функции делятся на две категории: ключевые и функции значения.
Ключевые функции берут объект Commit
и извлекают определенное ключевое значение (например, хэш, дату, автора и т.д.). Функции значения берут массив объектов Commit
и суммируют их по одному значению (например, количество фиксаций, добавлений, удалений).
При такой настройке мы можем взять массив объектов Commit
и суммировать его по определенной ключевой функции с помощью функции значения. Например, мы можем получить количество коммитов (значение) для каждого автора (ключ).
Рассмотрим следующий класс LogHandler
, который определяет aggregateBy
и groupBy
, а также функцию значения vfNumCommits
и функцию ключа kfAuthorName
.
class LogHandler {
constructor(gitlog) {
this.data = [...gitlog.log]
}
// Key function for name of author
static kfAuthorName(obj) {
return obj.authorName
}
// Value function for number of commits
static vfNumCommits(array) {
return array.length
}
/**
* Group by a key function.
* @param keyFunc: function to get the key per commit
* */
groupBy(keyFunc) {
return this.data.reduce((agg, next) => {
const curKeyValue = keyFunc(next)
curKeyValue in agg ? agg[curKeyValue].push(next) : agg[curKeyValue] = [next]
return agg
}, {})
}
/**
* Aggregator for top level keys of the Gitlog object.
* @param keyFunc: function to get the key per commit
* @param valueFunc: function to aggregate by
* */
aggregateBy(keyFunc, valueFunc) {
const grouped = this.groupBy(keyFunc)
Object.keys(grouped).forEach((k) => {
grouped[k] = {
value: valueFunc(grouped[k]),
}
})
return grouped
}
}
Если мы инстанцируем LogHandler
с нашим журналом git, мы можем вызвать aggregateBy(LogHandler.kfAuthorName, LogHandler.vfNumCommits)
, мы получим объект, содержащий количество коммитов для каждого автора, вот так:
{
"Alice" : {"value" : 42},
"Bob" : {"value" : 13}
}
Теперь, что если мы захотим сгруппировать эти результаты по годам, т.е. количество коммитов для каждого автора за каждый год.
Мы можем определить еще один метод в классе LogHandler
, который называется groupAggregateBy
и ключевую функцию для года kfYear
.
static kfYear(obj) {
return obj.timestamp.getFullYear()
}
groupAggregateBy(groupFunc, keyFunc, valueFunc) {
const grouped = this.data.reduce((agg, next) => {
const curKey = [keyFunc(next), groupFunc(next)]
curKey in agg ? agg[curKey].push(next) : agg[curKey] = [next]
return agg
}, {})
Object.keys(grouped).forEach((k) => {
grouped[k] = {
key: keyFunc(grouped[k][0]),
group: groupFunc(grouped[k][0]),
value: valueFunc(grouped[k])
}
})
return grouped
}
Функция groupAggregateBy
принимает дополнительный аргумент, groupFunc
, который может быть любой ключевой функцией. Каждый ключ, созданный путем применения функции key к массиву объектов Commit
, является одной группой.
Продолжая наш пример, вызовем groupAggregateBy(LogHandler.kfYear, LogHandler.kfAuthorName, LogHandler.vfNumCommits)
, что приведет к созданию следующего объекта:
{
"[2022,Alice]" : {"key": "Alice", "group": 2022, "value": 2}
"[2021,Alice]" : {"key": "Alice", "group": 2021, "value": 30}
"[2020,Alice]" : {"key": "Alice", "group": 2020, "value": 10}
"[2022,Bob]" : {"key": "Bob", "group": 2022, "value": 10}
"[2019,Bob]" : {"key": "Bob", "group": 2019, "value": 3}
}
Теперь нам просто нужно реализовать функцию ключа и значения для любого ключа и значения, к которым мы хотим предоставить доступ пользователю.
На приборной панели пользователь может выбрать любую из определенных функций, которые затем применяются к журналу git, в результате чего преобразованный набор данных используется в качестве входных данных для визуализации.
Заключение и улучшения
Я получил массу удовольствия от внедрения анализатора git-коммитов, и мне нравится то понимание, которое я получаю от него.
Есть ряд моментов, которые все еще можно улучшить:
- Разбор расширений файлов: это было бы отличным улучшением для добавления информации о языках, используемых в репозитории.
- Информация о ветке: сейчас информация о ветке игнорируется инструментом.
- Сохранение сессии: сейчас визуализации теряются при обновлении страницы.
- Общие улучшения UX: Я заметил, что пользователи, которые впервые заходят на приборную панель, интуитивно не понимают всех функций.
Тем не менее, я надеюсь, что инструмент будет интересен в использовании, и вы сможете найти новые сведения о своей истории коммитов!
Пожалуйста, не стесняйтесь обращаться ко мне с отзывами, комментариями или идеями по улучшению!
Скриншоты