loggit 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loggit-1.0.0/LICENSE +21 -0
- loggit-1.0.0/PKG-INFO +128 -0
- loggit-1.0.0/README.md +100 -0
- loggit-1.0.0/loggit/__init__.py +7 -0
- loggit-1.0.0/loggit/__main__.py +5 -0
- loggit-1.0.0/loggit/cli.py +160 -0
- loggit-1.0.0/loggit/gitcore.py +180 -0
- loggit-1.0.0/loggit/render.py +88 -0
- loggit-1.0.0/loggit.egg-info/PKG-INFO +128 -0
- loggit-1.0.0/loggit.egg-info/SOURCES.txt +14 -0
- loggit-1.0.0/loggit.egg-info/dependency_links.txt +1 -0
- loggit-1.0.0/loggit.egg-info/entry_points.txt +2 -0
- loggit-1.0.0/loggit.egg-info/top_level.txt +1 -0
- loggit-1.0.0/pyproject.toml +41 -0
- loggit-1.0.0/setup.cfg +4 -0
- loggit-1.0.0/tests/test_loggit.py +100 -0
loggit-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsk-dev-ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
loggit-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loggit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A friendly terminal git history explorer. Zero dependencies, works anywhere.
|
|
5
|
+
Author: dsk-dev-ai
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dsk-dev-ai/loggit
|
|
8
|
+
Project-URL: Repository, https://github.com/dsk-dev-ai/loggit
|
|
9
|
+
Project-URL: Funding, https://github.com/sponsors/dsk-dev-ai
|
|
10
|
+
Keywords: git,log,cli,terminal,git-history,developer-tools,productivity
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<img src="https://img.shields.io/badge/dependencies-ZERO-22c55e?style=flat-square" alt="zero deps">
|
|
31
|
+
<img src="https://img.shields.io/badge/python-3.8+-4B8BBE?style=flat-square&logo=python&logoColor=white" alt="python">
|
|
32
|
+
<img src="https://img.shields.io/pypi/v/loggit?style=flat-square" alt="pypi version">
|
|
33
|
+
<a href="https://github.com/dsk-dev-ai/loggit/actions"><img src="https://img.shields.io/github/actions/workflow/status/dsk-dev-ai/loggit/ci.yml?style=flat-square" alt="CI"></a>
|
|
34
|
+
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT">
|
|
35
|
+
<a href="https://github.com/users/dsk-dev-ai/packages/container/package/loggit"><img src="https://img.shields.io/badge/docker-ghcr-2496ED?style=flat-square&logo=docker&logoColor=white" alt="Docker GHCR"></a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
# loggit ๐ชต
|
|
39
|
+
|
|
40
|
+
**Your git history, actually readable.** A friendly terminal explorer that turns `git log` into color, graphs, and answers โ zero dependencies.
|
|
41
|
+
|
|
42
|
+
Stop squinting at `git log --oneline`. `loggit` gives you a clear, colored view of **what your repository is really doing** โ commits, authors, churn, and pace โ in one command. Pure Python stdlib, works on any laptop, installs instantly.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
โ main
|
|
46
|
+
17 commits
|
|
47
|
+
|
|
48
|
+
1000a50 Add Docker/GHCR badge + usage section to README
|
|
49
|
+
dsk-dev-ai ยท 6m ago
|
|
50
|
+
+14/-0 in 1 file(s)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## โจ Features
|
|
56
|
+
|
|
57
|
+
| | |
|
|
58
|
+
|---|---|
|
|
59
|
+
| ๐จ **Pretty output** | colored, aligned, compact log view with refs, ages, and authors |
|
|
60
|
+
| ๐ฅ **Author stats** | per-developer commit counts and line churn as a bar chart |
|
|
61
|
+
| ๐ **Pace metrics** | commits/day, authors, and added/removed lines over any window |
|
|
62
|
+
| ๐ **Powerful filters** | `--grep`, `--author`, `--since`, `--until`, `--branch` |
|
|
63
|
+
| ๐ฆ **Machine-readable** | `loggit json` for one-line-per-commit JSON |
|
|
64
|
+
| ๐ธ **100% free** | zero dependencies, no AI, no server, no API keys |
|
|
65
|
+
|
|
66
|
+
## ๐ง Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install loggit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
No dependencies. No compiled extensions. Works on Python 3.8+ everywhere.
|
|
73
|
+
|
|
74
|
+
## ๐ณ Docker
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
docker pull ghcr.io/dsk-dev-ai/loggit:latest
|
|
78
|
+
docker run --rm -v "$PWD:/repo" -w /repo ghcr.io/dsk-dev-ai/loggit:latest authors
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## ๐ Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
loggit # pretty log of recent commits (default: 40)
|
|
85
|
+
loggit -n 100 # show more
|
|
86
|
+
loggit --stat # add per-commit +added/-removed lines
|
|
87
|
+
loggit --grep "docker" # only commits mentioning "docker"
|
|
88
|
+
loggit --author "ada" # only commits by a specific developer
|
|
89
|
+
loggit --since "2 weeks ago" # recent activity only
|
|
90
|
+
loggit authors # per-developer commit & churn summary
|
|
91
|
+
loggit pace # overall stats + velocity
|
|
92
|
+
loggit json # JSON, one object per line (great for scripting)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See `loggit --help` for the full list of options.
|
|
96
|
+
|
|
97
|
+
<details>
|
|
98
|
+
<summary><b>JSON output (for piping into your tools)</b></summary>
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
loggit json -n 5 > commits.ndjson
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{"hash": "1000a50", "author": "dsk-dev-ai", "date": "2026-08-31T02:14:39+05:30", "subject": "Add Docker/GHCR badge", "refs": "HEAD -> main"}
|
|
106
|
+
```
|
|
107
|
+
</details>
|
|
108
|
+
|
|
109
|
+
## ๐ง How it works
|
|
110
|
+
|
|
111
|
+
1. **Runs `git log`** with a stable, machine-friendly pretty-format.
|
|
112
|
+
2. **Parses** commits, refs, dates, and message bodies (stdlib only).
|
|
113
|
+
3. **Renders** a clean, terminal-aware view โ colors auto-disable when piped.
|
|
114
|
+
4. **Summarizes** per-author and per-window metrics on demand.
|
|
115
|
+
|
|
116
|
+
## ๐ค Contributing
|
|
117
|
+
|
|
118
|
+
Have an idea or spotted a quirk? Open an issue or a pull request. Want to add a metric or a new view? It's a focused module in `loggit/`.
|
|
119
|
+
|
|
120
|
+
## ๐ License
|
|
121
|
+
|
|
122
|
+
[MIT](LICENSE) โ free to use, modify, and distribute.
|
|
123
|
+
|
|
124
|
+
## โญ Support
|
|
125
|
+
|
|
126
|
+
If `loggit` saved you a minute of `git log` squinting, **give it a star** โญ.
|
|
127
|
+
|
|
128
|
+
Want to help keep it free forever? Consider [becoming a sponsor](https://github.com/sponsors/dsk-dev-ai) on GitHub.
|
loggit-1.0.0/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/badge/dependencies-ZERO-22c55e?style=flat-square" alt="zero deps">
|
|
3
|
+
<img src="https://img.shields.io/badge/python-3.8+-4B8BBE?style=flat-square&logo=python&logoColor=white" alt="python">
|
|
4
|
+
<img src="https://img.shields.io/pypi/v/loggit?style=flat-square" alt="pypi version">
|
|
5
|
+
<a href="https://github.com/dsk-dev-ai/loggit/actions"><img src="https://img.shields.io/github/actions/workflow/status/dsk-dev-ai/loggit/ci.yml?style=flat-square" alt="CI"></a>
|
|
6
|
+
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT">
|
|
7
|
+
<a href="https://github.com/users/dsk-dev-ai/packages/container/package/loggit"><img src="https://img.shields.io/badge/docker-ghcr-2496ED?style=flat-square&logo=docker&logoColor=white" alt="Docker GHCR"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
# loggit ๐ชต
|
|
11
|
+
|
|
12
|
+
**Your git history, actually readable.** A friendly terminal explorer that turns `git log` into color, graphs, and answers โ zero dependencies.
|
|
13
|
+
|
|
14
|
+
Stop squinting at `git log --oneline`. `loggit` gives you a clear, colored view of **what your repository is really doing** โ commits, authors, churn, and pace โ in one command. Pure Python stdlib, works on any laptop, installs instantly.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
โ main
|
|
18
|
+
17 commits
|
|
19
|
+
|
|
20
|
+
1000a50 Add Docker/GHCR badge + usage section to README
|
|
21
|
+
dsk-dev-ai ยท 6m ago
|
|
22
|
+
+14/-0 in 1 file(s)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## โจ Features
|
|
28
|
+
|
|
29
|
+
| | |
|
|
30
|
+
|---|---|
|
|
31
|
+
| ๐จ **Pretty output** | colored, aligned, compact log view with refs, ages, and authors |
|
|
32
|
+
| ๐ฅ **Author stats** | per-developer commit counts and line churn as a bar chart |
|
|
33
|
+
| ๐ **Pace metrics** | commits/day, authors, and added/removed lines over any window |
|
|
34
|
+
| ๐ **Powerful filters** | `--grep`, `--author`, `--since`, `--until`, `--branch` |
|
|
35
|
+
| ๐ฆ **Machine-readable** | `loggit json` for one-line-per-commit JSON |
|
|
36
|
+
| ๐ธ **100% free** | zero dependencies, no AI, no server, no API keys |
|
|
37
|
+
|
|
38
|
+
## ๐ง Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install loggit
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
No dependencies. No compiled extensions. Works on Python 3.8+ everywhere.
|
|
45
|
+
|
|
46
|
+
## ๐ณ Docker
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
docker pull ghcr.io/dsk-dev-ai/loggit:latest
|
|
50
|
+
docker run --rm -v "$PWD:/repo" -w /repo ghcr.io/dsk-dev-ai/loggit:latest authors
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## ๐ Usage
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
loggit # pretty log of recent commits (default: 40)
|
|
57
|
+
loggit -n 100 # show more
|
|
58
|
+
loggit --stat # add per-commit +added/-removed lines
|
|
59
|
+
loggit --grep "docker" # only commits mentioning "docker"
|
|
60
|
+
loggit --author "ada" # only commits by a specific developer
|
|
61
|
+
loggit --since "2 weeks ago" # recent activity only
|
|
62
|
+
loggit authors # per-developer commit & churn summary
|
|
63
|
+
loggit pace # overall stats + velocity
|
|
64
|
+
loggit json # JSON, one object per line (great for scripting)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
See `loggit --help` for the full list of options.
|
|
68
|
+
|
|
69
|
+
<details>
|
|
70
|
+
<summary><b>JSON output (for piping into your tools)</b></summary>
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
loggit json -n 5 > commits.ndjson
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{"hash": "1000a50", "author": "dsk-dev-ai", "date": "2026-08-31T02:14:39+05:30", "subject": "Add Docker/GHCR badge", "refs": "HEAD -> main"}
|
|
78
|
+
```
|
|
79
|
+
</details>
|
|
80
|
+
|
|
81
|
+
## ๐ง How it works
|
|
82
|
+
|
|
83
|
+
1. **Runs `git log`** with a stable, machine-friendly pretty-format.
|
|
84
|
+
2. **Parses** commits, refs, dates, and message bodies (stdlib only).
|
|
85
|
+
3. **Renders** a clean, terminal-aware view โ colors auto-disable when piped.
|
|
86
|
+
4. **Summarizes** per-author and per-window metrics on demand.
|
|
87
|
+
|
|
88
|
+
## ๐ค Contributing
|
|
89
|
+
|
|
90
|
+
Have an idea or spotted a quirk? Open an issue or a pull request. Want to add a metric or a new view? It's a focused module in `loggit/`.
|
|
91
|
+
|
|
92
|
+
## ๐ License
|
|
93
|
+
|
|
94
|
+
[MIT](LICENSE) โ free to use, modify, and distribute.
|
|
95
|
+
|
|
96
|
+
## โญ Support
|
|
97
|
+
|
|
98
|
+
If `loggit` saved you a minute of `git log` squinting, **give it a star** โญ.
|
|
99
|
+
|
|
100
|
+
Want to help keep it free forever? Consider [becoming a sponsor](https://github.com/sponsors/dsk-dev-ai) on GitHub.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""loggit CLI - a friendly terminal git history explorer (zero dependencies)."""
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from loggit import __version__
|
|
7
|
+
from loggit.gitcore import (
|
|
8
|
+
GitError,
|
|
9
|
+
current_branch,
|
|
10
|
+
git_log,
|
|
11
|
+
is_repo,
|
|
12
|
+
)
|
|
13
|
+
from loggit.render import (
|
|
14
|
+
RESET,
|
|
15
|
+
BOLD,
|
|
16
|
+
DIM,
|
|
17
|
+
CYAN,
|
|
18
|
+
GREEN,
|
|
19
|
+
RED,
|
|
20
|
+
YELLOW,
|
|
21
|
+
authors_summary,
|
|
22
|
+
pace_summary,
|
|
23
|
+
render_authors,
|
|
24
|
+
render_commit,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_parser():
|
|
29
|
+
p = argparse.ArgumentParser(
|
|
30
|
+
prog="loggit",
|
|
31
|
+
description="Friendly terminal git history explorer. Zero dependencies.",
|
|
32
|
+
)
|
|
33
|
+
p.add_argument("-n", "--max-count", type=int, default=40, help="commits to show (default: 40)")
|
|
34
|
+
p.add_argument("--grep", help="only commits whose message matches")
|
|
35
|
+
p.add_argument("--author", help="only commits by this author (name or email)")
|
|
36
|
+
p.add_argument("--since", help="only commits since this date/ref")
|
|
37
|
+
p.add_argument("--until", help="only commits until this date/ref")
|
|
38
|
+
p.add_argument("--stat", action="store_true", help="show added/removed lines per commit")
|
|
39
|
+
p.add_argument("--no-color", action="store_true", help="disable ANSI colors")
|
|
40
|
+
p.add_argument("--branch", help="limit to a specific branch (default: all)")
|
|
41
|
+
p.add_argument("--version", action="version", version=f"loggit {__version__}")
|
|
42
|
+
sub = p.add_subparsers(dest="command")
|
|
43
|
+
|
|
44
|
+
authors_p = sub.add_parser("authors", help="per-author commit/churn summary")
|
|
45
|
+
authors_p.add_argument("-n", "--max-count", type=int, default=40, help="commits to consider")
|
|
46
|
+
authors_p.add_argument("--branch", help="limit to a specific branch")
|
|
47
|
+
authors_p.add_argument("--no-color", action="store_true", help="disable ANSI colors")
|
|
48
|
+
|
|
49
|
+
pace_p = sub.add_parser("pace", help="overall stats for the captured window")
|
|
50
|
+
pace_p.add_argument("-n", "--max-count", type=int, default=40, help="commits to consider")
|
|
51
|
+
pace_p.add_argument("--branch", help="limit to a specific branch")
|
|
52
|
+
pace_p.add_argument("--no-color", action="store_true", help="disable ANSI colors")
|
|
53
|
+
|
|
54
|
+
json_p = sub.add_parser("json", help="machine-readable log (objects, one per line)")
|
|
55
|
+
json_p.add_argument("-n", "--max-count", type=int, default=40, help="commits to emit")
|
|
56
|
+
json_p.add_argument("--branch", help="limit to a specific branch")
|
|
57
|
+
json_p.add_argument("--grep", help="only commits whose message matches")
|
|
58
|
+
json_p.add_argument("--author", help="only commits by this author")
|
|
59
|
+
json_p.add_argument("--since", help="only commits since this date/ref")
|
|
60
|
+
json_p.add_argument("--until", help="only commits until this date/ref")
|
|
61
|
+
json_p.add_argument("--no-color", action="store_true", help="disable ANSI colors")
|
|
62
|
+
|
|
63
|
+
return p
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _disable_color():
|
|
67
|
+
sys.stdout.isatty = lambda: False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_log(args):
|
|
71
|
+
commits = git_log(
|
|
72
|
+
cwd=".",
|
|
73
|
+
max_count=args.max_count,
|
|
74
|
+
grep=args.grep,
|
|
75
|
+
author=args.author,
|
|
76
|
+
since=args.since,
|
|
77
|
+
until=args.until,
|
|
78
|
+
stat=args.stat,
|
|
79
|
+
branches=args.branch,
|
|
80
|
+
)
|
|
81
|
+
branch = current_branch(".")
|
|
82
|
+
title = f" {branch or 'repository'} "
|
|
83
|
+
print(f"\n{BOLD}{CYAN}โ{title}{RESET}")
|
|
84
|
+
print(f"{DIM} {len(commits)} commits{RESET}")
|
|
85
|
+
for c in commits:
|
|
86
|
+
print()
|
|
87
|
+
print(render_commit(c))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cmd_authors(args):
|
|
91
|
+
commits = git_log(cwd=".", max_count=args.max_count, branches=args.branch, stat=True)
|
|
92
|
+
rows = authors_summary(commits)
|
|
93
|
+
print(f"\n{BOLD}{CYAN}โ Authors{RESET}")
|
|
94
|
+
print(render_authors(rows))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_pace(args):
|
|
98
|
+
commits = git_log(cwd=".", max_count=args.max_count, branches=args.branch, stat=True)
|
|
99
|
+
p = pace_summary(commits)
|
|
100
|
+
print(f"\n{BOLD}{CYAN}โ Pace / stats{RESET}")
|
|
101
|
+
print(f" commits {p['commits']}")
|
|
102
|
+
print(f" authors {p['authors']}")
|
|
103
|
+
print(f" window {p['span_days']:.1f} days")
|
|
104
|
+
print(f" avg/day {p['avg_per_day']:.2f}")
|
|
105
|
+
print(f" lines added {GREEN}+{p['added']}{RESET}")
|
|
106
|
+
print(f" lines removed {RED}-{p['removed']}{RESET}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cmd_json(args):
|
|
110
|
+
import json as _json
|
|
111
|
+
|
|
112
|
+
commits = git_log(
|
|
113
|
+
cwd=".",
|
|
114
|
+
max_count=args.max_count,
|
|
115
|
+
grep=args.grep,
|
|
116
|
+
author=args.author,
|
|
117
|
+
since=args.since,
|
|
118
|
+
until=args.until,
|
|
119
|
+
stat=False,
|
|
120
|
+
branches=args.branch,
|
|
121
|
+
)
|
|
122
|
+
for c in commits:
|
|
123
|
+
out = {
|
|
124
|
+
"hash": c["hash"],
|
|
125
|
+
"short": c["short"],
|
|
126
|
+
"author": c["author"],
|
|
127
|
+
"email": c["email"],
|
|
128
|
+
"date": c["age"],
|
|
129
|
+
"subject": c["subject"],
|
|
130
|
+
"refs": c["refs"].strip(),
|
|
131
|
+
}
|
|
132
|
+
print(_json.dumps(out))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv=None):
|
|
136
|
+
args = build_parser().parse_args(argv)
|
|
137
|
+
if args.no_color:
|
|
138
|
+
_disable_color()
|
|
139
|
+
|
|
140
|
+
if not is_repo("."):
|
|
141
|
+
print(f"{RED}โ not inside a git repository{RESET}", file=sys.stderr)
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
if args.command == "authors":
|
|
146
|
+
cmd_authors(args)
|
|
147
|
+
elif args.command == "pace":
|
|
148
|
+
cmd_pace(args)
|
|
149
|
+
elif args.command == "json":
|
|
150
|
+
cmd_json(args)
|
|
151
|
+
else:
|
|
152
|
+
cmd_log(args)
|
|
153
|
+
except GitError as exc:
|
|
154
|
+
print(f"{RED}โ {exc}{RESET}", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
sys.exit(main())
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Low-level git interaction and log parsing (stdlib only, no deps)."""
|
|
2
|
+
import datetime as _dt
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GitError(RuntimeError):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_git(args, cwd=None):
|
|
12
|
+
"""Run a git command and return its stdout, raising on failure."""
|
|
13
|
+
try:
|
|
14
|
+
proc = subprocess.run(
|
|
15
|
+
["git", *args],
|
|
16
|
+
cwd=cwd,
|
|
17
|
+
capture_output=True,
|
|
18
|
+
text=True,
|
|
19
|
+
check=False,
|
|
20
|
+
)
|
|
21
|
+
except FileNotFoundError as exc:
|
|
22
|
+
raise GitError("git is not installed or not on PATH") from exc
|
|
23
|
+
if proc.returncode != 0:
|
|
24
|
+
raise GitError((proc.stderr or "git command failed").strip())
|
|
25
|
+
return proc.stdout
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_repo(cwd=None):
|
|
29
|
+
try:
|
|
30
|
+
run_git(["-C", (cwd or "."), "rev-parse", "--is-inside-work-tree"], cwd=cwd)
|
|
31
|
+
return True
|
|
32
|
+
except GitError:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_ts(iso):
|
|
37
|
+
try:
|
|
38
|
+
return _dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
|
39
|
+
except ValueError:
|
|
40
|
+
return _dt.datetime.now()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def human_age(dt, now=None):
|
|
44
|
+
"""Return a compact relative age like '3h ago' from a datetime."""
|
|
45
|
+
now = now or _dt.datetime.now(dt.tzinfo) if dt.tzinfo else (_dt.datetime.now())
|
|
46
|
+
delta = now - dt
|
|
47
|
+
if dt.tzinfo is None:
|
|
48
|
+
# naive datetimes: compare against naive now
|
|
49
|
+
delta = _dt.datetime.now() - dt
|
|
50
|
+
secs = int(delta.total_seconds())
|
|
51
|
+
if secs < 0:
|
|
52
|
+
secs = 0
|
|
53
|
+
if secs < 90:
|
|
54
|
+
return f"{secs}s ago"
|
|
55
|
+
mins = secs // 60
|
|
56
|
+
if mins < 60:
|
|
57
|
+
return f"{mins}m ago"
|
|
58
|
+
hours = mins // 60
|
|
59
|
+
if hours < 48:
|
|
60
|
+
return f"{hours}h ago"
|
|
61
|
+
days = hours // 24
|
|
62
|
+
if days < 30:
|
|
63
|
+
return f"{days}d ago"
|
|
64
|
+
months = days // 30
|
|
65
|
+
if months < 12:
|
|
66
|
+
return f"{months}mo ago"
|
|
67
|
+
years = months // 12
|
|
68
|
+
return f"{years}y ago"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def git_log(cwd=None, max_count=40, grep=None, author=None, since=None, until=None, stat=False, branches=None):
|
|
72
|
+
"""Fetch a parseable git log stream with a stable, machine-friendly format.
|
|
73
|
+
|
|
74
|
+
Returns a list of dicts describing each commit.
|
|
75
|
+
"""
|
|
76
|
+
sep = "\x1e" # record separator unlikely to appear in messages
|
|
77
|
+
fieldsep = "\x1f"
|
|
78
|
+
args = ["-C", (cwd or "."), "log"]
|
|
79
|
+
if max_count:
|
|
80
|
+
args += ["--max-count", str(max_count)]
|
|
81
|
+
if grep:
|
|
82
|
+
args += ["--grep", grep, "--regexp-ignore-case"]
|
|
83
|
+
if author:
|
|
84
|
+
args += ["--author", author]
|
|
85
|
+
if since:
|
|
86
|
+
args += ["--since", since]
|
|
87
|
+
if until:
|
|
88
|
+
args += ["--until", until]
|
|
89
|
+
if branches:
|
|
90
|
+
args += [branches]
|
|
91
|
+
else:
|
|
92
|
+
args += ["--all"]
|
|
93
|
+
|
|
94
|
+
fmt = "%H%x1f%h%x1f%an%x1f%ae%x1f%aI%x1f%s%x1f%D%x1f%n%b"
|
|
95
|
+
# Use git's own %x1e hex escape so commits are separated by the real byte.
|
|
96
|
+
args += [
|
|
97
|
+
f"--pretty=format:{fmt}%x1e",
|
|
98
|
+
]
|
|
99
|
+
out = run_git(args, cwd=None)
|
|
100
|
+
commits = []
|
|
101
|
+
for record in out.split(sep):
|
|
102
|
+
record = record.strip("\n")
|
|
103
|
+
if not record:
|
|
104
|
+
continue
|
|
105
|
+
lines = record.split("\n")
|
|
106
|
+
header = lines[0]
|
|
107
|
+
parts = header.split(fieldsep)
|
|
108
|
+
if len(parts) < 7:
|
|
109
|
+
continue
|
|
110
|
+
full, short, name, email, iso, subject, refs = parts[:7]
|
|
111
|
+
body = "\n".join(lines[1:]).strip() if len(lines) > 1 else ""
|
|
112
|
+
commits.append(
|
|
113
|
+
{
|
|
114
|
+
"hash": full,
|
|
115
|
+
"short": short,
|
|
116
|
+
"author": name,
|
|
117
|
+
"email": email,
|
|
118
|
+
"date": _parse_ts(iso),
|
|
119
|
+
"age": iso,
|
|
120
|
+
"subject": subject,
|
|
121
|
+
"refs": refs,
|
|
122
|
+
"body": body,
|
|
123
|
+
"added": 0,
|
|
124
|
+
"removed": 0,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
commits[-1]["age_human"] = human_age(commits[-1]["date"])
|
|
128
|
+
|
|
129
|
+
if stat:
|
|
130
|
+
stats = git_stats(cwd=cwd, max_count=max_count, branches=branches)
|
|
131
|
+
for c in commits:
|
|
132
|
+
s = stats.get(c["hash"])
|
|
133
|
+
if s:
|
|
134
|
+
c.update(s)
|
|
135
|
+
return commits
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def git_stats(cwd=None, max_count=None, branches=None):
|
|
139
|
+
"""Fetch per-commit added/removed/file counts keyed by full hash.
|
|
140
|
+
|
|
141
|
+
Uses a separate --numstat stream (clean to parse, unlike --stat output).
|
|
142
|
+
"""
|
|
143
|
+
args = [
|
|
144
|
+
"-C",
|
|
145
|
+
(cwd or "."),
|
|
146
|
+
"log",
|
|
147
|
+
"--numstat",
|
|
148
|
+
"--no-renames",
|
|
149
|
+
"--pretty=format:%x1e%H",
|
|
150
|
+
]
|
|
151
|
+
if max_count:
|
|
152
|
+
args += ["--max-count", str(max_count)]
|
|
153
|
+
if branches:
|
|
154
|
+
args += [branches]
|
|
155
|
+
else:
|
|
156
|
+
args += ["--all"]
|
|
157
|
+
out = run_git(args, cwd=None)
|
|
158
|
+
stats = {}
|
|
159
|
+
for rec in out.split("\x1e"):
|
|
160
|
+
rec = rec.strip("\n")
|
|
161
|
+
if not rec:
|
|
162
|
+
continue
|
|
163
|
+
lines = rec.split("\n")
|
|
164
|
+
h = lines[0]
|
|
165
|
+
added = removed = files = 0
|
|
166
|
+
for ln in lines[1:]:
|
|
167
|
+
parts = ln.split("\t")
|
|
168
|
+
if len(parts) >= 3 and parts[0].isdigit() and parts[1].isdigit():
|
|
169
|
+
added += int(parts[0])
|
|
170
|
+
removed += int(parts[1])
|
|
171
|
+
files += 1
|
|
172
|
+
stats[h] = {"added": added, "removed": removed, "changed_files": files}
|
|
173
|
+
return stats
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def current_branch(cwd=None):
|
|
177
|
+
try:
|
|
178
|
+
return run_git(["-C", (cwd or "."), "branch", "--show-current"], cwd=None).strip()
|
|
179
|
+
except GitError:
|
|
180
|
+
return None
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Terminal rendering and summary statistics (stdlib only)."""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# ANSI helpers - no deps, degrade gracefully when not a TTY.
|
|
6
|
+
def _c(code):
|
|
7
|
+
if not sys.stdout.isatty():
|
|
8
|
+
return ""
|
|
9
|
+
return f"\x1b[{code}m"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
RESET = _c("0")
|
|
13
|
+
BOLD = _c("1")
|
|
14
|
+
DIM = _c("2")
|
|
15
|
+
RED = _c("31")
|
|
16
|
+
GREEN = _c("32")
|
|
17
|
+
YELLOW = _c("33")
|
|
18
|
+
BLUE = _c("34")
|
|
19
|
+
MAGENTA = _c("35")
|
|
20
|
+
CYAN = _c("36")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def render_commit(c):
|
|
24
|
+
"""Render a single commit as a compact colored block."""
|
|
25
|
+
lines = []
|
|
26
|
+
head = f"{BOLD}{YELLOW}{c['short']}{RESET}"
|
|
27
|
+
refs = f" {CYAN}{c['refs'].strip()}{RESET}" if c.get("refs", "").strip() else ""
|
|
28
|
+
author = f"{MAGENTA}{c['author']}{RESET}"
|
|
29
|
+
age = f"{DIM}{c['age_human']}{RESET}"
|
|
30
|
+
lines.append(f"{head}{refs} {c['subject']}")
|
|
31
|
+
lines.append(f" {author} ยท {age}")
|
|
32
|
+
if c.get("body"):
|
|
33
|
+
for line in c["body"].split("\n"):
|
|
34
|
+
lines.append(f" {DIM}{line}{RESET}")
|
|
35
|
+
if c.get("changed_files"):
|
|
36
|
+
lines.append(
|
|
37
|
+
f" {GREEN}+{c['added']}{RESET}/{RED}-{c['removed']}{RESET} in "
|
|
38
|
+
f"{c['changed_files']} file(s)"
|
|
39
|
+
)
|
|
40
|
+
return "\n".join(lines)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def authors_summary(commits):
|
|
44
|
+
"""Summarize commit counts per author: [(name, count, added, removed), ...]."""
|
|
45
|
+
agg = {}
|
|
46
|
+
for c in commits:
|
|
47
|
+
a = agg.setdefault(
|
|
48
|
+
c["author"],
|
|
49
|
+
{"name": c["author"], "count": 0, "added": 0, "removed": 0},
|
|
50
|
+
)
|
|
51
|
+
a["count"] += 1
|
|
52
|
+
a["added"] += c.get("added", 0)
|
|
53
|
+
a["removed"] += c.get("removed", 0)
|
|
54
|
+
return sorted(agg.values(), key=lambda a: (-a["count"], -a["added"]))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render_authors(author_rows, width=None):
|
|
58
|
+
width = width or 24
|
|
59
|
+
lines = []
|
|
60
|
+
for a in author_rows:
|
|
61
|
+
name = f"{BOLD}{a['name']}{RESET}"
|
|
62
|
+
bar = "โ" * min(a["count"], 40)
|
|
63
|
+
lines.append(
|
|
64
|
+
f" {name:<{width}} {a['count']:>4} commits "
|
|
65
|
+
f"{GREEN}+{a['added']}{RESET}/{RED}-{a['removed']}{RESET} {bar}"
|
|
66
|
+
)
|
|
67
|
+
return "\n".join(lines)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def pace_summary(commits):
|
|
71
|
+
"""Return a dict with overall stats for the captured window."""
|
|
72
|
+
if not commits:
|
|
73
|
+
return {"commits": 0}
|
|
74
|
+
dates = [c["date"] for c in commits]
|
|
75
|
+
try:
|
|
76
|
+
span_days = max((max(dates) - min(dates)).total_seconds() / 86400.0, 0.0001)
|
|
77
|
+
except Exception:
|
|
78
|
+
span_days = 0.0001
|
|
79
|
+
total_added = sum(c.get("added", 0) for c in commits)
|
|
80
|
+
total_removed = sum(c.get("removed", 0) for c in commits)
|
|
81
|
+
return {
|
|
82
|
+
"commits": len(commits),
|
|
83
|
+
"authors": len({c["author"] for c in commits}),
|
|
84
|
+
"span_days": span_days,
|
|
85
|
+
"avg_per_day": len(commits) / span_days,
|
|
86
|
+
"added": total_added,
|
|
87
|
+
"removed": total_removed,
|
|
88
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loggit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A friendly terminal git history explorer. Zero dependencies, works anywhere.
|
|
5
|
+
Author: dsk-dev-ai
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dsk-dev-ai/loggit
|
|
8
|
+
Project-URL: Repository, https://github.com/dsk-dev-ai/loggit
|
|
9
|
+
Project-URL: Funding, https://github.com/sponsors/dsk-dev-ai
|
|
10
|
+
Keywords: git,log,cli,terminal,git-history,developer-tools,productivity
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<img src="https://img.shields.io/badge/dependencies-ZERO-22c55e?style=flat-square" alt="zero deps">
|
|
31
|
+
<img src="https://img.shields.io/badge/python-3.8+-4B8BBE?style=flat-square&logo=python&logoColor=white" alt="python">
|
|
32
|
+
<img src="https://img.shields.io/pypi/v/loggit?style=flat-square" alt="pypi version">
|
|
33
|
+
<a href="https://github.com/dsk-dev-ai/loggit/actions"><img src="https://img.shields.io/github/actions/workflow/status/dsk-dev-ai/loggit/ci.yml?style=flat-square" alt="CI"></a>
|
|
34
|
+
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT">
|
|
35
|
+
<a href="https://github.com/users/dsk-dev-ai/packages/container/package/loggit"><img src="https://img.shields.io/badge/docker-ghcr-2496ED?style=flat-square&logo=docker&logoColor=white" alt="Docker GHCR"></a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
# loggit ๐ชต
|
|
39
|
+
|
|
40
|
+
**Your git history, actually readable.** A friendly terminal explorer that turns `git log` into color, graphs, and answers โ zero dependencies.
|
|
41
|
+
|
|
42
|
+
Stop squinting at `git log --oneline`. `loggit` gives you a clear, colored view of **what your repository is really doing** โ commits, authors, churn, and pace โ in one command. Pure Python stdlib, works on any laptop, installs instantly.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
โ main
|
|
46
|
+
17 commits
|
|
47
|
+
|
|
48
|
+
1000a50 Add Docker/GHCR badge + usage section to README
|
|
49
|
+
dsk-dev-ai ยท 6m ago
|
|
50
|
+
+14/-0 in 1 file(s)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## โจ Features
|
|
56
|
+
|
|
57
|
+
| | |
|
|
58
|
+
|---|---|
|
|
59
|
+
| ๐จ **Pretty output** | colored, aligned, compact log view with refs, ages, and authors |
|
|
60
|
+
| ๐ฅ **Author stats** | per-developer commit counts and line churn as a bar chart |
|
|
61
|
+
| ๐ **Pace metrics** | commits/day, authors, and added/removed lines over any window |
|
|
62
|
+
| ๐ **Powerful filters** | `--grep`, `--author`, `--since`, `--until`, `--branch` |
|
|
63
|
+
| ๐ฆ **Machine-readable** | `loggit json` for one-line-per-commit JSON |
|
|
64
|
+
| ๐ธ **100% free** | zero dependencies, no AI, no server, no API keys |
|
|
65
|
+
|
|
66
|
+
## ๐ง Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install loggit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
No dependencies. No compiled extensions. Works on Python 3.8+ everywhere.
|
|
73
|
+
|
|
74
|
+
## ๐ณ Docker
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
docker pull ghcr.io/dsk-dev-ai/loggit:latest
|
|
78
|
+
docker run --rm -v "$PWD:/repo" -w /repo ghcr.io/dsk-dev-ai/loggit:latest authors
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## ๐ Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
loggit # pretty log of recent commits (default: 40)
|
|
85
|
+
loggit -n 100 # show more
|
|
86
|
+
loggit --stat # add per-commit +added/-removed lines
|
|
87
|
+
loggit --grep "docker" # only commits mentioning "docker"
|
|
88
|
+
loggit --author "ada" # only commits by a specific developer
|
|
89
|
+
loggit --since "2 weeks ago" # recent activity only
|
|
90
|
+
loggit authors # per-developer commit & churn summary
|
|
91
|
+
loggit pace # overall stats + velocity
|
|
92
|
+
loggit json # JSON, one object per line (great for scripting)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See `loggit --help` for the full list of options.
|
|
96
|
+
|
|
97
|
+
<details>
|
|
98
|
+
<summary><b>JSON output (for piping into your tools)</b></summary>
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
loggit json -n 5 > commits.ndjson
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{"hash": "1000a50", "author": "dsk-dev-ai", "date": "2026-08-31T02:14:39+05:30", "subject": "Add Docker/GHCR badge", "refs": "HEAD -> main"}
|
|
106
|
+
```
|
|
107
|
+
</details>
|
|
108
|
+
|
|
109
|
+
## ๐ง How it works
|
|
110
|
+
|
|
111
|
+
1. **Runs `git log`** with a stable, machine-friendly pretty-format.
|
|
112
|
+
2. **Parses** commits, refs, dates, and message bodies (stdlib only).
|
|
113
|
+
3. **Renders** a clean, terminal-aware view โ colors auto-disable when piped.
|
|
114
|
+
4. **Summarizes** per-author and per-window metrics on demand.
|
|
115
|
+
|
|
116
|
+
## ๐ค Contributing
|
|
117
|
+
|
|
118
|
+
Have an idea or spotted a quirk? Open an issue or a pull request. Want to add a metric or a new view? It's a focused module in `loggit/`.
|
|
119
|
+
|
|
120
|
+
## ๐ License
|
|
121
|
+
|
|
122
|
+
[MIT](LICENSE) โ free to use, modify, and distribute.
|
|
123
|
+
|
|
124
|
+
## โญ Support
|
|
125
|
+
|
|
126
|
+
If `loggit` saved you a minute of `git log` squinting, **give it a star** โญ.
|
|
127
|
+
|
|
128
|
+
Want to help keep it free forever? Consider [becoming a sponsor](https://github.com/sponsors/dsk-dev-ai) on GitHub.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
loggit/__init__.py
|
|
5
|
+
loggit/__main__.py
|
|
6
|
+
loggit/cli.py
|
|
7
|
+
loggit/gitcore.py
|
|
8
|
+
loggit/render.py
|
|
9
|
+
loggit.egg-info/PKG-INFO
|
|
10
|
+
loggit.egg-info/SOURCES.txt
|
|
11
|
+
loggit.egg-info/dependency_links.txt
|
|
12
|
+
loggit.egg-info/entry_points.txt
|
|
13
|
+
loggit.egg-info/top_level.txt
|
|
14
|
+
tests/test_loggit.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
loggit
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "loggit"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A friendly terminal git history explorer. Zero dependencies, works anywhere."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "dsk-dev-ai" }]
|
|
13
|
+
keywords = ["git", "log", "cli", "terminal", "git-history", "developer-tools", "productivity"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
27
|
+
"Topic :: Utilities",
|
|
28
|
+
]
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/dsk-dev-ai/loggit"
|
|
31
|
+
Repository = "https://github.com/dsk-dev-ai/loggit"
|
|
32
|
+
Funding = "https://github.com/sponsors/dsk-dev-ai"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
loggit = "loggit.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
include = ["loggit*"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
loggit-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Tests for loggit (zero-dependency, uses a scratch git repo)."""
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
11
|
+
|
|
12
|
+
from loggit.gitcore import GitError, git_log, human_age, is_repo, run_git # noqa: E402
|
|
13
|
+
from loggit.render import authors_summary, pace_summary # noqa: E402
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _git(repo, *args):
|
|
17
|
+
subprocess.run(["git", "-C", repo, *args], check=True, capture_output=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def repo():
|
|
22
|
+
tmp = tempfile.mkdtemp()
|
|
23
|
+
_git(tmp, "init", "-q")
|
|
24
|
+
_git(tmp, "config", "user.email", "dev@example.com")
|
|
25
|
+
_git(tmp, "config", "user.name", "Dev Tester")
|
|
26
|
+
return tmp
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _commit(repo, msg, file="a.txt", content="x\n"):
|
|
30
|
+
path = os.path.join(repo, file)
|
|
31
|
+
with open(path, "a") as fh:
|
|
32
|
+
fh.write(content)
|
|
33
|
+
_git(repo, "add", ".")
|
|
34
|
+
_git(repo, "commit", "-q", "-m", msg)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_is_repo(repo):
|
|
38
|
+
assert is_repo(repo) is True
|
|
39
|
+
temp = tempfile.mkdtemp()
|
|
40
|
+
assert is_repo(temp) is False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_git_log_parses(repo):
|
|
44
|
+
_commit(repo, "first commit")
|
|
45
|
+
_commit(repo, "second commit")
|
|
46
|
+
commits = git_log(cwd=repo, max_count=10)
|
|
47
|
+
assert len(commits) == 2
|
|
48
|
+
assert commits[0]["subject"] == "second commit"
|
|
49
|
+
assert commits[0]["author"] == "Dev Tester"
|
|
50
|
+
assert commits[1]["subject"] == "first commit"
|
|
51
|
+
for c in commits:
|
|
52
|
+
assert c["hash"]
|
|
53
|
+
assert c["short"]
|
|
54
|
+
assert c["age_human"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_git_log_grep_filter(repo):
|
|
58
|
+
_commit(repo, "adds awesome feature")
|
|
59
|
+
_commit(repo, "fixes typo")
|
|
60
|
+
commits = git_log(cwd=repo, max_count=10, grep="awesome")
|
|
61
|
+
assert len(commits) == 1
|
|
62
|
+
assert commits[0]["subject"] == "adds awesome feature"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_git_log_author_filter(repo):
|
|
66
|
+
_commit(repo, "alpha")
|
|
67
|
+
commits = git_log(cwd=repo, max_count=10, author="Tester")
|
|
68
|
+
assert len(commits) == 1
|
|
69
|
+
commits = git_log(cwd=repo, max_count=10, author="Nobody")
|
|
70
|
+
assert len(commits) == 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_authors_summary(repo):
|
|
74
|
+
_commit(repo, "one")
|
|
75
|
+
_commit(repo, "two")
|
|
76
|
+
_commit(repo, "three")
|
|
77
|
+
rows = authors_summary(git_log(cwd=repo, max_count=10, stat=True))
|
|
78
|
+
assert len(rows) == 1
|
|
79
|
+
assert rows[0]["count"] == 3
|
|
80
|
+
assert rows[0]["added"] >= 3
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_pace_summary(repo):
|
|
84
|
+
_commit(repo, "one")
|
|
85
|
+
_commit(repo, "two")
|
|
86
|
+
p = pace_summary(git_log(cwd=repo, max_count=10, stat=True))
|
|
87
|
+
assert p["commits"] == 2
|
|
88
|
+
assert p["authors"] == 1
|
|
89
|
+
assert p["avg_per_day"] > 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_human_age():
|
|
93
|
+
import datetime
|
|
94
|
+
|
|
95
|
+
assert "ago" in human_age(datetime.datetime.now() - datetime.timedelta(hours=3))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_git_error_when_no_git():
|
|
99
|
+
with pytest.raises(GitError):
|
|
100
|
+
run_git(["--definitely-not-a-command"])
|