git-tunnel 0.1.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.
- git_tunnel-0.1.0/LICENSE +21 -0
- git_tunnel-0.1.0/PKG-INFO +124 -0
- git_tunnel-0.1.0/README.md +108 -0
- git_tunnel-0.1.0/pyproject.toml +32 -0
- git_tunnel-0.1.0/src/git_tunnel/__init__.py +1 -0
- git_tunnel-0.1.0/src/git_tunnel/cli.py +75 -0
- git_tunnel-0.1.0/src/git_tunnel/hooks/prepare-commit-msg +28 -0
- git_tunnel-0.1.0/src/git_tunnel/tunnel.py +159 -0
git_tunnel-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Younes Hamishebahar
|
|
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.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-tunnel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Visualize git history as per-machine tunnels in your terminal
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: developer-tools,git,multi-machine,terminal,visualization
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# git-tunnel
|
|
18
|
+
|
|
19
|
+
Visualize your git history as **per-machine tunnels** in the terminal.
|
|
20
|
+
|
|
21
|
+
If you work across multiple machines — a MacBook, a Workstation, a Server — git
|
|
22
|
+
log treats them all the same. `git-tunnel` splits each machine into its own
|
|
23
|
+
colored column so you can see at a glance *where* every commit was made.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
TIME │ pre git-tunnel │ MacBook │ Workstation │ HASH
|
|
28
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
2 hours ago │ · · · · · · · · · · · · · · │ WIP: invoice route │ · · · · · · · · · · · · · · │ 2c4930f
|
|
30
|
+
3 hours ago │ · · · · · · · · · · · · · · │ · · · · · · · · · · · · · · │ finished validation │ 6f46c9f
|
|
31
|
+
1 day ago │ added model │ · · · · · · · · · · · · · · │ · · · · · · · · · · · · · · │ fd7bcad
|
|
32
|
+
1 day ago │ initial setup │ · · · · · · · · · · · · · · │ · · · · · · · · · · · · · · │ f3bc95b
|
|
33
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
■ pre git-tunnel ■ MacBook ■ Workstation
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Commits made before `git-tunnel` was installed appear in a plain white
|
|
39
|
+
`pre git-tunnel` column — no history rewriting, no warnings.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install git-tunnel
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or with `uv`:
|
|
50
|
+
```bash
|
|
51
|
+
uv tool install git-tunnel
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Setup (once per machine)
|
|
57
|
+
|
|
58
|
+
Run the interactive setup:
|
|
59
|
+
```bash
|
|
60
|
+
git-tunnel install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This will:
|
|
64
|
+
- Ask for a device name (e.g. `MacBook`, `Workstation`, `Server`)
|
|
65
|
+
- Set `git config --global user.device`
|
|
66
|
+
- Install the `prepare-commit-msg` hook globally
|
|
67
|
+
- Point `git config --global core.hooksPath` at the hook
|
|
68
|
+
|
|
69
|
+
From this point on, every commit you make will automatically get a
|
|
70
|
+
`[device:YourDevice]` tag appended to the message — silently, without
|
|
71
|
+
you doing anything.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
Inside any git repo:
|
|
78
|
+
```bash
|
|
79
|
+
git-tunnel
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## How it works
|
|
85
|
+
|
|
86
|
+
`git-tunnel` uses a global `prepare-commit-msg` hook that appends a
|
|
87
|
+
`[device:X]` tag to every commit message, where `X` comes from
|
|
88
|
+
`git config --global user.device`.
|
|
89
|
+
|
|
90
|
+
Your `user.name` is never touched — it stays clean for collaboration.
|
|
91
|
+
The device tag lives in the commit message body and is stripped from
|
|
92
|
+
the display in the tunnel view.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Multiple machines
|
|
97
|
+
|
|
98
|
+
Set a different device name on each machine:
|
|
99
|
+
```bash
|
|
100
|
+
# MacBook
|
|
101
|
+
git-tunnel install # enter "MacBook" when prompted
|
|
102
|
+
|
|
103
|
+
# Workstation
|
|
104
|
+
git-tunnel install # enter "Workstation" when prompted
|
|
105
|
+
|
|
106
|
+
# Server
|
|
107
|
+
git-tunnel install # enter "Server" when prompted
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Since the hook is global, it applies to every repo on that machine
|
|
111
|
+
automatically.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Zero dependencies
|
|
116
|
+
|
|
117
|
+
`git-tunnel` uses only the Python standard library and git itself.
|
|
118
|
+
No third-party packages required.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# git-tunnel
|
|
2
|
+
|
|
3
|
+
Visualize your git history as **per-machine tunnels** in the terminal.
|
|
4
|
+
|
|
5
|
+
If you work across multiple machines — a MacBook, a Workstation, a Server — git
|
|
6
|
+
log treats them all the same. `git-tunnel` splits each machine into its own
|
|
7
|
+
colored column so you can see at a glance *where* every commit was made.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
TIME │ pre git-tunnel │ MacBook │ Workstation │ HASH
|
|
12
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
2 hours ago │ · · · · · · · · · · · · · · │ WIP: invoice route │ · · · · · · · · · · · · · · │ 2c4930f
|
|
14
|
+
3 hours ago │ · · · · · · · · · · · · · · │ · · · · · · · · · · · · · · │ finished validation │ 6f46c9f
|
|
15
|
+
1 day ago │ added model │ · · · · · · · · · · · · · · │ · · · · · · · · · · · · · · │ fd7bcad
|
|
16
|
+
1 day ago │ initial setup │ · · · · · · · · · · · · · · │ · · · · · · · · · · · · · · │ f3bc95b
|
|
17
|
+
────────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
■ pre git-tunnel ■ MacBook ■ Workstation
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Commits made before `git-tunnel` was installed appear in a plain white
|
|
23
|
+
`pre git-tunnel` column — no history rewriting, no warnings.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install git-tunnel
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or with `uv`:
|
|
34
|
+
```bash
|
|
35
|
+
uv tool install git-tunnel
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Setup (once per machine)
|
|
41
|
+
|
|
42
|
+
Run the interactive setup:
|
|
43
|
+
```bash
|
|
44
|
+
git-tunnel install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This will:
|
|
48
|
+
- Ask for a device name (e.g. `MacBook`, `Workstation`, `Server`)
|
|
49
|
+
- Set `git config --global user.device`
|
|
50
|
+
- Install the `prepare-commit-msg` hook globally
|
|
51
|
+
- Point `git config --global core.hooksPath` at the hook
|
|
52
|
+
|
|
53
|
+
From this point on, every commit you make will automatically get a
|
|
54
|
+
`[device:YourDevice]` tag appended to the message — silently, without
|
|
55
|
+
you doing anything.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
Inside any git repo:
|
|
62
|
+
```bash
|
|
63
|
+
git-tunnel
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
`git-tunnel` uses a global `prepare-commit-msg` hook that appends a
|
|
71
|
+
`[device:X]` tag to every commit message, where `X` comes from
|
|
72
|
+
`git config --global user.device`.
|
|
73
|
+
|
|
74
|
+
Your `user.name` is never touched — it stays clean for collaboration.
|
|
75
|
+
The device tag lives in the commit message body and is stripped from
|
|
76
|
+
the display in the tunnel view.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Multiple machines
|
|
81
|
+
|
|
82
|
+
Set a different device name on each machine:
|
|
83
|
+
```bash
|
|
84
|
+
# MacBook
|
|
85
|
+
git-tunnel install # enter "MacBook" when prompted
|
|
86
|
+
|
|
87
|
+
# Workstation
|
|
88
|
+
git-tunnel install # enter "Workstation" when prompted
|
|
89
|
+
|
|
90
|
+
# Server
|
|
91
|
+
git-tunnel install # enter "Server" when prompted
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Since the hook is global, it applies to every repo on that machine
|
|
95
|
+
automatically.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Zero dependencies
|
|
100
|
+
|
|
101
|
+
`git-tunnel` uses only the Python standard library and git itself.
|
|
102
|
+
No third-party packages required.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "git-tunnel"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Visualize git history as per-machine tunnels in your terminal"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["git", "developer-tools", "terminal", "visualization", "multi-machine"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
git-tunnel = "git_tunnel.cli:main"
|
|
24
|
+
git-tunnel-run = "git_tunnel.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/git_tunnel"]
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build]
|
|
30
|
+
include = [
|
|
31
|
+
"src/git_tunnel/**",
|
|
32
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import subprocess
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def install():
|
|
8
|
+
"""
|
|
9
|
+
Interactive setup:
|
|
10
|
+
- Sets git config --global user.device
|
|
11
|
+
- Installs the prepare-commit-msg hook
|
|
12
|
+
- Sets git config --global core.hooksPath
|
|
13
|
+
- Adds the shell function hint
|
|
14
|
+
"""
|
|
15
|
+
RESET = '\033[0m'
|
|
16
|
+
BOLD = '\033[1m'
|
|
17
|
+
CYAN = '\033[96m'
|
|
18
|
+
GREEN = '\033[92m'
|
|
19
|
+
YELLOW = '\033[93m'
|
|
20
|
+
|
|
21
|
+
def ok(t): print(f" \033[92m✓\033[0m {t}")
|
|
22
|
+
def info(t): print(f" \033[96m→\033[0m {t}")
|
|
23
|
+
def warn(t): print(f" \033[93m⚠\033[0m {t}")
|
|
24
|
+
|
|
25
|
+
print(f"\n{BOLD}git-tunnel setup{RESET}\n")
|
|
26
|
+
|
|
27
|
+
# ── Device name ───────────────────────────────────────────────────────────
|
|
28
|
+
current = subprocess.run(
|
|
29
|
+
['git', 'config', '--global', 'user.device'],
|
|
30
|
+
capture_output=True, text=True
|
|
31
|
+
).stdout.strip()
|
|
32
|
+
|
|
33
|
+
if current:
|
|
34
|
+
info(f"user.device already set to '{CYAN}{current}{RESET}'")
|
|
35
|
+
answer = input(" Change it? [y/N] ").strip().lower()
|
|
36
|
+
if answer != 'y':
|
|
37
|
+
device = current
|
|
38
|
+
else:
|
|
39
|
+
device = input(" Enter device name (e.g. MacBook, Workstation, Server): ").strip()
|
|
40
|
+
else:
|
|
41
|
+
device = input(" Enter device name for this machine (e.g. MacBook, Workstation, Server): ").strip()
|
|
42
|
+
|
|
43
|
+
if not device:
|
|
44
|
+
warn("No device name entered — skipping.")
|
|
45
|
+
else:
|
|
46
|
+
subprocess.run(['git', 'config', '--global', 'user.device', device])
|
|
47
|
+
ok(f"user.device set to '{device}'")
|
|
48
|
+
|
|
49
|
+
# ── Hook installation ─────────────────────────────────────────────────────
|
|
50
|
+
hooks_dir = Path.home() / '.git-tunnel-hooks'
|
|
51
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
52
|
+
|
|
53
|
+
# Find the hook bundled with the package
|
|
54
|
+
pkg_hook = Path(__file__).parent / 'hooks' / 'prepare-commit-msg'
|
|
55
|
+
dest = hooks_dir / 'prepare-commit-msg'
|
|
56
|
+
shutil.copy(pkg_hook, dest)
|
|
57
|
+
dest.chmod(0o755)
|
|
58
|
+
ok(f"Hook installed → {dest}")
|
|
59
|
+
|
|
60
|
+
# Point git at the hooks dir
|
|
61
|
+
subprocess.run(['git', 'config', '--global', 'core.hooksPath', str(hooks_dir)])
|
|
62
|
+
ok(f"core.hooksPath set to {hooks_dir}")
|
|
63
|
+
|
|
64
|
+
# ── Shell function hint ───────────────────────────────────────────────────
|
|
65
|
+
print(f"\n{BOLD}Almost done!{RESET} Add this to your shell config (~/.zshrc or ~/.bashrc):\n")
|
|
66
|
+
print(f" {CYAN}function git-tunnel() {{ git-tunnel-run; }}{RESET}\n")
|
|
67
|
+
print(f"Or just run: {BOLD}git-tunnel-run{RESET} directly.\n")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'install':
|
|
72
|
+
install()
|
|
73
|
+
else:
|
|
74
|
+
from git_tunnel.tunnel import main as run
|
|
75
|
+
run()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# prepare-commit-msg hook — part of git-tunnel
|
|
3
|
+
# Appends [device:X] tag to every commit using git config user.device
|
|
4
|
+
|
|
5
|
+
COMMIT_MSG_FILE="$1"
|
|
6
|
+
COMMIT_SOURCE="$2"
|
|
7
|
+
|
|
8
|
+
# Skip merge, squash, amend
|
|
9
|
+
if [ "$COMMIT_SOURCE" = "merge" ] || \
|
|
10
|
+
[ "$COMMIT_SOURCE" = "squash" ] || \
|
|
11
|
+
[ "$COMMIT_SOURCE" = "commit" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
DEVICE=$(git config --global user.device)
|
|
16
|
+
|
|
17
|
+
if [ -z "$DEVICE" ]; then
|
|
18
|
+
echo "⚠ git-tunnel: user.device not set. Run: git-tunnel install"
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Don't double-tag
|
|
23
|
+
if grep -qE "\[device:" "$COMMIT_MSG_FILE"; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
echo "" >> "$COMMIT_MSG_FILE"
|
|
28
|
+
echo "[device:$DEVICE]" >> "$COMMIT_MSG_FILE"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# ── ANSI styling ─────────────────────────────────────────────────────────────
|
|
6
|
+
RESET = '\033[0m'
|
|
7
|
+
BOLD = '\033[1m'
|
|
8
|
+
DIM = '\033[2m'
|
|
9
|
+
|
|
10
|
+
DEVICE_COLORS = [
|
|
11
|
+
'\033[96m', # Cyan
|
|
12
|
+
'\033[93m', # Yellow
|
|
13
|
+
'\033[92m', # Green
|
|
14
|
+
'\033[95m', # Magenta
|
|
15
|
+
'\033[91m', # Red
|
|
16
|
+
'\033[94m', # Blue
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
HASH_COLOR = '\033[90m'
|
|
20
|
+
TIME_COLOR = '\033[37m'
|
|
21
|
+
BORDER_COLOR = '\033[90m'
|
|
22
|
+
HEADER_COLOR = '\033[1;37m'
|
|
23
|
+
LEGACY_COLOR = '\033[37m'
|
|
24
|
+
|
|
25
|
+
# ── Layout ────────────────────────────────────────────────────────────────────
|
|
26
|
+
TIME_WIDTH = 17
|
|
27
|
+
MSG_WIDTH = 36
|
|
28
|
+
HASH_WIDTH = 9
|
|
29
|
+
|
|
30
|
+
DEVICE_TAG = re.compile(r'\[device:(.+?)\]', re.IGNORECASE)
|
|
31
|
+
LEGACY_LABEL = 'pre git-tunnel'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_git_log():
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
['git', 'log', '--pretty=format:%h|%an|%ar|%B|||END|||',
|
|
37
|
+
'--exclude=refs/original/*', '--all'],
|
|
38
|
+
capture_output=True, text=True
|
|
39
|
+
)
|
|
40
|
+
if result.returncode != 0:
|
|
41
|
+
print(f"\n ✗ Not a git repository (or git not found).\n")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
return result.stdout
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_commits(raw):
|
|
47
|
+
entries = raw.split('|||END|||')
|
|
48
|
+
commits = []
|
|
49
|
+
for entry in entries:
|
|
50
|
+
entry = entry.strip()
|
|
51
|
+
if not entry:
|
|
52
|
+
continue
|
|
53
|
+
lines = entry.split('\n')
|
|
54
|
+
header = lines[0]
|
|
55
|
+
parts = header.split('|', 3)
|
|
56
|
+
if len(parts) < 3:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
hash_ = parts[0].strip()
|
|
60
|
+
author = parts[1].strip()
|
|
61
|
+
time_ = parts[2].strip()
|
|
62
|
+
first_line = parts[3].strip() if len(parts) > 3 else ''
|
|
63
|
+
rest = '\n'.join(lines[1:]).strip()
|
|
64
|
+
body = (first_line + '\n' + rest).strip()
|
|
65
|
+
|
|
66
|
+
match = DEVICE_TAG.search(body)
|
|
67
|
+
device = match.group(1).strip() if match else None
|
|
68
|
+
|
|
69
|
+
clean_msg = DEVICE_TAG.sub('', body).strip().splitlines()
|
|
70
|
+
clean_msg = ' '.join(l.strip() for l in clean_msg if l.strip())
|
|
71
|
+
|
|
72
|
+
commits.append({
|
|
73
|
+
'hash': hash_,
|
|
74
|
+
'author': author,
|
|
75
|
+
'device': device,
|
|
76
|
+
'time': time_,
|
|
77
|
+
'message': clean_msg,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
seen, unique = set(), []
|
|
81
|
+
for c in commits:
|
|
82
|
+
if c['hash'] not in seen:
|
|
83
|
+
seen.add(c['hash'])
|
|
84
|
+
unique.append(c)
|
|
85
|
+
return unique
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def truncate(text, width):
|
|
89
|
+
return text if len(text) <= width else text[:width - 1] + '…'
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def border(ch):
|
|
93
|
+
return f"{BORDER_COLOR}{ch}{RESET}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def render(commits):
|
|
97
|
+
has_legacy = any(c['device'] is None for c in commits)
|
|
98
|
+
devices = [LEGACY_LABEL] if has_legacy else []
|
|
99
|
+
|
|
100
|
+
for c in reversed(commits):
|
|
101
|
+
if c['device'] and c['device'] not in devices:
|
|
102
|
+
devices.append(c['device'])
|
|
103
|
+
|
|
104
|
+
def get_color(d):
|
|
105
|
+
if d == LEGACY_LABEL:
|
|
106
|
+
return LEGACY_COLOR
|
|
107
|
+
device_only = [x for x in devices if x != LEGACY_LABEL]
|
|
108
|
+
idx = device_only.index(d) if d in device_only else 0
|
|
109
|
+
return DEVICE_COLORS[idx % len(DEVICE_COLORS)]
|
|
110
|
+
|
|
111
|
+
col_w = MSG_WIDTH + 2
|
|
112
|
+
row_total = TIME_WIDTH + (col_w + 3) * len(devices) + HASH_WIDTH + 4
|
|
113
|
+
bar = BORDER_COLOR + '─' * row_total + RESET
|
|
114
|
+
|
|
115
|
+
print(f"\n{bar}")
|
|
116
|
+
header = f" {HEADER_COLOR}{'TIME':<{TIME_WIDTH}}{RESET}"
|
|
117
|
+
header += border('│')
|
|
118
|
+
for d in devices:
|
|
119
|
+
color = get_color(d)
|
|
120
|
+
label = truncate(d, col_w)
|
|
121
|
+
header += f" {color}{BOLD}{label:<{col_w}}{RESET} {border('│')}"
|
|
122
|
+
header += f" {HEADER_COLOR}{'HASH':<{HASH_WIDTH}}{RESET}"
|
|
123
|
+
print(header)
|
|
124
|
+
print(bar)
|
|
125
|
+
|
|
126
|
+
for c in commits:
|
|
127
|
+
device = c['device'] if c['device'] else LEGACY_LABEL
|
|
128
|
+
time_str = truncate(c['time'], TIME_WIDTH)
|
|
129
|
+
color = get_color(device)
|
|
130
|
+
|
|
131
|
+
row = f" {TIME_COLOR}{time_str:<{TIME_WIDTH}}{RESET}"
|
|
132
|
+
row += border('│')
|
|
133
|
+
|
|
134
|
+
for d in devices:
|
|
135
|
+
if device == d:
|
|
136
|
+
msg = truncate(c['message'], col_w)
|
|
137
|
+
row += f" {color}{msg:<{col_w}}{RESET} {border('│')}"
|
|
138
|
+
else:
|
|
139
|
+
dots = DIM + '· ' * ((col_w + 1) // 2)
|
|
140
|
+
row += f" {dots:<{col_w + len(DIM)}}{RESET} {border('│')}"
|
|
141
|
+
|
|
142
|
+
row += f" {HASH_COLOR}{c['hash']}{RESET}"
|
|
143
|
+
print(row)
|
|
144
|
+
|
|
145
|
+
print(bar)
|
|
146
|
+
print()
|
|
147
|
+
legend = " "
|
|
148
|
+
for d in devices:
|
|
149
|
+
legend += f"{get_color(d)}{BOLD}■ {d}{RESET} "
|
|
150
|
+
print(legend + "\n")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def main():
|
|
154
|
+
raw = run_git_log()
|
|
155
|
+
commits = parse_commits(raw)
|
|
156
|
+
if not commits:
|
|
157
|
+
print("\n No commits found.\n")
|
|
158
|
+
return
|
|
159
|
+
render(commits)
|