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.
@@ -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)