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 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,7 @@
1
+ """loggit - a friendly, zero-dependency terminal git history explorer.
2
+
3
+ Turn `git log` into a readable, colored, interactive report of what your
4
+ repository is really doing: commits, authors, branches, churn, and pace.
5
+ """
6
+
7
+ __version__ = "1.0.0"
@@ -0,0 +1,5 @@
1
+ """Run `python -m loggit`."""
2
+ from loggit.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ loggit = loggit.cli:main
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"])