232 lines
14 KiB
Markdown
232 lines
14 KiB
Markdown
## gitfame
|
||
|
||
В этом задании нужно реализовать консольную утилиту для подсчёта статистик авторов git репозитория.
|
||
|
||
### Статистики
|
||
|
||
* Количество строк
|
||
* Количество коммитов
|
||
* Количество файлов
|
||
|
||
Все статистики считаются для состояния репозитория на момент конкретного коммита.
|
||
|
||
#### Расчёт
|
||
|
||
Каждой строке интересующего подмножества файлов репозитория сопоставляется последний коммит, модифицировавший эту строку.
|
||
Пустым файлам сопоставляются последние менявшие их коммиты.
|
||
|
||
После этого для каждого уникального автора, получившегося множества коммитов,
|
||
считается количество строк, уникальных коммитов и файлов, которые затрагивали коммиты автора.
|
||
|
||
Нужную информацию можно получить с помощью команды `git blame`:
|
||
|
||
```
|
||
✗ git blame utf8/README.md
|
||
f4640df4 (Fedor Korotkiy 2020-02-26 20:28:52 +0000 1) # utf8*
|
||
f4640df4 (Fedor Korotkiy 2020-02-26 20:28:52 +0000 2)
|
||
f4640df4 (Fedor Korotkiy 2020-02-26 20:28:52 +0000 3) Задача объединяет несколько задач на строки и unicode.
|
||
f4640df4 (Fedor Korotkiy 2020-02-26 20:28:52 +0000 4)
|
||
f4640df4 (Fedor Korotkiy 2020-02-26 20:28:52 +0000 5) Задача считается решённой, если все подзадачи решены.
|
||
```
|
||
|
||
`git blame` с флагом `--porcelain` (см. `git blame --help`) возвращает информацию в машиночитаемом формате.
|
||
Кроме того, этот формат схлопывает соседние строки относящиеся к одному коммиту,
|
||
что может сильно сократить размер результата. Поэтому использовать нужно его.
|
||
|
||
Стоит помнить, что не все файлы из директории git проекта обязательно принадлежат git репозиторию.
|
||
Получить список файлов git репозитория можно разными способами, например с помощью `git ls-tree`.
|
||
|
||
Для вызова команд git можно использовать [os/exec](https://golang.org/pkg/os/exec/).
|
||
|
||
В процессе работы скрипт не должен менять состояние репозитория ни в какой момент,
|
||
поскольку с репозиторием могут параллельно работать.
|
||
|
||
### Интерфейс
|
||
|
||
Утилита должна печатать результат в stdout.
|
||
При использовании невалидного значения флага или любой другой ошибке программа должна завершаться с ненулевым кодом возврата.
|
||
|
||
Расчёт может занимать довольно длительное время.
|
||
Хорошим качеством подобной утилиты является отображение прогресса (вынесенное за флаг).
|
||
Прогресс можно печатать в stderr в произвольном формате.
|
||
|
||
### Флаги
|
||
|
||
Утилита должна поддерживать следующий набор флагов:
|
||
|
||
**--repository** — путь до Git репозитория; по умолчанию текущая директория
|
||
|
||
**--revision** — указатель на коммит; HEAD по умолчанию
|
||
|
||
**--order-by** — ключ сортировки результатов, один из `lines` (дефолт), `commits`, `files`.
|
||
|
||
По умолчанию результаты сортируются по убыванию ключа `(lines, commits, files)`.
|
||
При равенстве ключей выше будет автор с лексикографически меньшим именем.
|
||
При использовании флага соответствующей поле в ключе перемещается на первое место.
|
||
|
||
**--use-committer** — булев флаг, заменяющий в расчётах автора (дефолт) на коммиттера
|
||
|
||
**--format** — формат вывода; один из `tabular` (дефолт), `csv`, `json`, `json-lines`;
|
||
|
||
`tabular`:
|
||
```
|
||
Name Lines Commits Files
|
||
Joe Tsai 64 3 2
|
||
Ross Light 2 1 1
|
||
ferhat elmas 1 1 1
|
||
```
|
||
Human-readable формат. Для паддинга используется пробел.
|
||
см. [text/tabwriter](https://golang.org/pkg/text/tabwriter/).
|
||
|
||
`csv`:
|
||
```
|
||
Name,Lines,Commits,Files
|
||
Joe Tsai,64,3,2
|
||
Ross Light,2,1,1
|
||
ferhat elmas,1,1,1
|
||
```
|
||
[encoding/csv](https://golang.org/pkg/encoding/csv/)
|
||
|
||
`json`:
|
||
```
|
||
[{"name":"Joe Tsai","lines":64,"commits":3,"files":2},{"name":"Ross Light","lines":2,"commits":1,"files":1},{"name":"ferhat elmas","lines":1,"commits":1,"files":1}]
|
||
```
|
||
[encoding/json](https://golang.org/pkg/encoding/json/)
|
||
|
||
`json-lines`:
|
||
```
|
||
{"name":"Joe Tsai","lines":64,"commits":3,"files":2}
|
||
{"name":"Ross Light","lines":2,"commits":1,"files":1}
|
||
{"name":"ferhat elmas","lines":1,"commits":1,"files":1}
|
||
```
|
||
|
||
**--extensions** — список расширений, сужающий список файлов в расчёте; множество ограничений разделяется запятыми, например, `'.go,.md'`
|
||
|
||
**--languages** — список языков (программирования, разметки и др.), сужающий список файлов в расчёте; множество ограничений разделяется запятыми, например `'go,markdown'`
|
||
|
||
Принадлежность файла к языку программирования определяется с помощью его расширения.
|
||
В [configs/language_extensions.json](configs/language_extensions.json) лежит маппинг.
|
||
Неизвестные языки никаких ограничений не накладывают. При их использовании можно написать warning в stderr.
|
||
|
||
**--exclude** — набор [Glob](https://en.wikipedia.org/wiki/Glob_(programming)) паттернов, исключающих файлы из расчёта, например `'foo/*,bar/*'`
|
||
|
||
Для работы с Glob'ом в стандартной библиотеке есть [path/filepath](https://golang.org/pkg/path/filepath/).
|
||
|
||
**--restrict-to** — набор Glob паттернов, исключающий все файлы, не удовлетворяющие ни одному из паттернов набора
|
||
|
||
### Тесты
|
||
|
||
Команда для запуска тестов
|
||
```
|
||
go test -v ./gitfame/test/integration/...
|
||
```
|
||
|
||
В [/tests/integration/testdata/bundles](test/integration/testdata/bundles) лежат запакованные git репозитории.
|
||
Каждый интеграционный тест ссылается на какой-нибудь бандл.
|
||
|
||
Как создать bundle? Находясь в git репозитории выполнить
|
||
```
|
||
git bundle create my.bundle --all
|
||
```
|
||
|
||
Как распаковать bundle? Находясь в пустой директории.
|
||
```
|
||
git clone /path/to/my.bundle .
|
||
```
|
||
|
||
### Сборка приложения
|
||
|
||
Как собрать приложение?
|
||
```
|
||
(cd gitfame/cmd/gitfame && go build .)
|
||
```
|
||
В `gitfame/cmd/gitfame` появится исполняемый файл с именем `gitfame`.
|
||
|
||
Как собрать приложение и установить его в `GOPATH/bin`?
|
||
```
|
||
go install ./gitfame/cmd/gitfame/...
|
||
```
|
||
|
||
Чтобы вызывать установленный бинарь без указания полного пути, нужно добавить `GOPATH/bin` в `PATH`.
|
||
```
|
||
export PATH=$GOPATH/bin:$PATH
|
||
```
|
||
|
||
После этого `gitfame` будет доступен всюду.
|
||
|
||
### Ненавязчивые предложения
|
||
|
||
#### Project layout
|
||
|
||
В go есть [набор рекомендаций](https://github.com/golang-standards/project-layout) по организации структуры проекта.
|
||
И подпроект этого задания уже частично ему следует. Например, `main.go`, который вам нужно реализовать, лежит в [cmd/gitfame](./cmd/gitfame),
|
||
интеграционные тесты в `/test`.
|
||
|
||
В небольших проектах нет ничего плохого в том, чтобы весь код лежал плоско в корне.
|
||
Здесь же, для ознакомления предлагаем изучить общепринятый подход.
|
||
|
||
#### Cli
|
||
|
||
Можно познакомиться с [spf13/cobra](https://github.com/spf13/cobra),
|
||
популярной библиотекой для написания [cli](https://en.wikipedia.org/wiki/Command-line_interface).
|
||
В этой задаче cobra поможет распарсить аргументы, написать подробный help message, сделать алиасы для флагов.
|
||
|
||
В cobra используется библиотека [pflag](https://pkg.go.dev/github.com/spf13/pflag) для работы с флагами.
|
||
Библиотеку можно использовать и отдельно от cobra.
|
||
`pflag` может побольше, чем стандартный [flag](https://golang.org/pkg/flag/),
|
||
в частости, в `pflag` есть полезные для решаемой задачи флаги для работы с аргументами-массивами.
|
||
|
||
Помимо библиотеки, в cobra есть ещё и бинарь (с именем cobra) для кодогенерации основы проекта.
|
||
|
||
Его можно установить в `GOPATH` командой
|
||
```
|
||
go get -u github.com/spf13/cobra
|
||
```
|
||
|
||
Генерируемый sample проект использует [viper](https://pkg.go.dev/github.com/spf13/viper) (библиотеку для работы с конфигами) и
|
||
[go-homedir](https://pkg.go.dev/github.com/mitchellh/go-homedir) (кроссплатформенную библиотеку для поиска домашней директории пользователя).
|
||
В этой задаче эти библиотеки не нужны, поэтому их нет в зависимостях проекта.
|
||
|
||
### Git ликбез
|
||
|
||
Вся информация взята из [книги](https://github.com/pluralsight/git-internals-pdf/releases/download/v2.0/peepcode-git.pdf).
|
||
|
||
Смело советуем прочитать её всем, даже если в контексте задания вы знаете про Git достаточно.
|
||
|
||
---------
|
||
|
||
Объекты Git хранятся в специальной базе данных `Git Object Database` в директории .git.
|
||
В базе в сжатом виде хранятся объекты разных типов.
|
||
У каждого объекта есть SHA-1 хэш, а также небольшой header.
|
||
|
||
Несколько основных типов объектов:
|
||
* **blob** — соответствует файлу; хранит его данные (только содержимое)
|
||
* **tree** — соответствует директории; хранит список блобов и деревьев, а также их описание (имена файлов, типы, права доступа)
|
||
* **commit** — соответствует истории изменения дерева; хранит указатель на дерево, автора изменений, субъекта, добавившего изменения (committer), сообщение с описанием изменений, ссылку на предыдущие (родительские) коммиты
|
||
|
||
**branch** (ветка) — это не объект `Git Object Database`, а всего лишь файл в директории `.git/refs/heads/` с хэшом последнего для этой ветки коммита.
|
||
То есть ветка — это указатель на коммит.
|
||
|
||
**head** — это ссылка на коммит. В каждом репозитории по умолчанию есть **head** именем **master**.
|
||
|
||
**HEAD** — один выделенный **head**. Файл `.git/HEAD`. Родитель следующего коммита.
|
||
|
||
**HEAD** может ссылаться на коммит напрямую (**detached HEAD**).
|
||
Следующий коммит в таком случае не будет принадлежать никакой ветке.
|
||
|
||
Гораздо чаще **HEAD** ссылается на ветку.
|
||
В таком случае следующий коммит "попадёт" в ту же ветку и продвинет **HEAD**.
|
||
**HEAD** определяет текущую активную ветку.
|
||
|
||
**revision** (ревизия) — способ сослаться на Git объект.
|
||
Например, SHA-1 коммита — это ревизия на коммит,
|
||
`HEAD@{5 minutes ago}` — это ревизия на последний коммит на момент 5 минут назад,
|
||
`HEAD:README` — это ревизия на блоб.
|
||
|
||
### Критерии сдачи
|
||
|
||
Решение должно проходить все тесты, так же как и в обычной задаче.
|
||
|
||
После прохождения тестов в табличке появится 0.
|
||
Нужно успеть отправить решение до дедлайна!
|
||
Проверяющие посмотрят на решение и заменят 0 на 1.
|