tmuxpull 0.2.0__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tmuxpull
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Keep every git repo under a directory in step with its remote, concurrently, with a tmux session per repo that needs you
5
5
  Project-URL: Homepage, https://github.com/nguyengg/tmuxpull
6
6
  Project-URL: Repository, https://github.com/nguyengg/tmuxpull.git
@@ -121,6 +121,40 @@ tmuxpull [-d DEPTH] [-j JOBS] [--tmux {on,off}] [--rebase-pushed]
121
121
  - `--no-log` — Don't write a report file
122
122
  - `-v, --verbose` — Show incoming commit subjects (`-v` = top 3, `-vv` = all)
123
123
  - `--dry-run` — List repos that would be processed, then exit
124
+ - `-V, --version` — Print the version and the file it's running from, then exit
125
+
126
+ ### Am I running the latest?
127
+
128
+ ```bash
129
+ $ tmuxpull --version
130
+ tmuxpull 0.2.1 (/home/you/.local/share/uv/tools/tmuxpull/lib/python3.14/site-packages/tmuxpull/__init__.py)
131
+ ```
132
+
133
+ The path is there because one machine can easily have three copies on `PATH` —
134
+ a `pip install` into whichever Python was current, a `uv tool install`, and a
135
+ curl'd standalone script — and the version number alone won't tell you which one
136
+ just ran. The second field is always the bare version, so `tmuxpull --version |
137
+ awk '{print $2}'` is scriptable.
138
+
139
+ Compare against what's published:
140
+
141
+ ```bash
142
+ curl -s https://pypi.org/pypi/tmuxpull/json | grep -o '"version":"[^"]*"' | head -1
143
+ ```
144
+
145
+ Upgrading depends on how it was installed:
146
+
147
+ ```bash
148
+ uv tool upgrade tmuxpull # uv tool install
149
+ pip install -U tmuxpull # pip install
150
+ ```
151
+
152
+ A `pip install` into a version-managed Python (mise, pyenv, asdf) is worth
153
+ avoiding: the package lives under that exact interpreter, so the next Python
154
+ upgrade silently leaves it behind — or drops it off `PATH` entirely. `uv tool
155
+ install tmuxpull` keeps it in its own environment instead. The curl one-liners
156
+ need no upgrade at all: they read `main` directly, so they're current the moment
157
+ a fix lands.
124
158
 
125
159
  ### Per-repo git config
126
160
 
@@ -94,6 +94,40 @@ tmuxpull [-d DEPTH] [-j JOBS] [--tmux {on,off}] [--rebase-pushed]
94
94
  - `--no-log` — Don't write a report file
95
95
  - `-v, --verbose` — Show incoming commit subjects (`-v` = top 3, `-vv` = all)
96
96
  - `--dry-run` — List repos that would be processed, then exit
97
+ - `-V, --version` — Print the version and the file it's running from, then exit
98
+
99
+ ### Am I running the latest?
100
+
101
+ ```bash
102
+ $ tmuxpull --version
103
+ tmuxpull 0.2.1 (/home/you/.local/share/uv/tools/tmuxpull/lib/python3.14/site-packages/tmuxpull/__init__.py)
104
+ ```
105
+
106
+ The path is there because one machine can easily have three copies on `PATH` —
107
+ a `pip install` into whichever Python was current, a `uv tool install`, and a
108
+ curl'd standalone script — and the version number alone won't tell you which one
109
+ just ran. The second field is always the bare version, so `tmuxpull --version |
110
+ awk '{print $2}'` is scriptable.
111
+
112
+ Compare against what's published:
113
+
114
+ ```bash
115
+ curl -s https://pypi.org/pypi/tmuxpull/json | grep -o '"version":"[^"]*"' | head -1
116
+ ```
117
+
118
+ Upgrading depends on how it was installed:
119
+
120
+ ```bash
121
+ uv tool upgrade tmuxpull # uv tool install
122
+ pip install -U tmuxpull # pip install
123
+ ```
124
+
125
+ A `pip install` into a version-managed Python (mise, pyenv, asdf) is worth
126
+ avoiding: the package lives under that exact interpreter, so the next Python
127
+ upgrade silently leaves it behind — or drops it off `PATH` entirely. `uv tool
128
+ install tmuxpull` keeps it in its own environment instead. The curl one-liners
129
+ need no upgrade at all: they read `main` directly, so they're current the moment
130
+ a fix lands.
97
131
 
98
132
  ### Per-repo git config
99
133
 
@@ -35,6 +35,8 @@
35
35
  set -uo pipefail
36
36
 
37
37
  # --- defaults ------------------------------------------------------------
38
+ # Kept in step with src/tmuxpull/__init__.py's __version__ by a test.
39
+ version="0.2.1"
38
40
  max_depth=2
39
41
  jobs=8
40
42
  typeset -a excludes
@@ -73,6 +75,7 @@ Options:
73
75
  -v Verbose: -v shows top 3 incoming commit subjects per repo,
74
76
  -vv shows all
75
77
  --dry-run List repos and exit
78
+ -V, --version Show the version and which copy is running, then exit
76
79
  -h, --help Show this help
77
80
 
78
81
  Per-repo git config:
@@ -93,6 +96,7 @@ while [[ $# -gt 0 ]]; do
93
96
  -v) ((verbose++)); shift ;;
94
97
  -vv) verbose=2; shift ;;
95
98
  --dry-run) dry_run=true; shift ;;
99
+ -V|--version) printf 'tmuxpull %s (%s)\n' "$version" "${0:A}"; exit 0 ;;
96
100
  -h|--help) usage; exit 0 ;;
97
101
  --) shift; break ;;
98
102
  -*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
@@ -60,6 +60,12 @@ from typing import Iterable
60
60
  import libtmux
61
61
 
62
62
 
63
+ # Single source of truth for the version: pyproject.toml reads it from here
64
+ # (hatch dynamic version), so the PyPI package, the generated standalone script
65
+ # and `--version` can never disagree.
66
+ __version__ = "0.2.1"
67
+
68
+
63
69
  # Directories that are never a repo we want to descend into.
64
70
  _SKIP_DIRS: frozenset[str] = frozenset(
65
71
  {
@@ -923,6 +929,32 @@ async def _run(repos: list[Repo], jobs: int, verbose: int, opts: Options) -> lis
923
929
  return results
924
930
 
925
931
 
932
+ def version_line() -> str:
933
+ """Version plus the file it is running from.
934
+
935
+ The path matters as much as the number: the same machine can have a PyPI
936
+ install, a `uv tool` install and a curl'd standalone script on PATH, and
937
+ this says which one you just ran.
938
+ """
939
+ return f"tmuxpull {__version__} ({Path(__file__).resolve()})"
940
+
941
+
942
+ class _VersionAction(argparse.Action):
943
+ """Print version_line() verbatim.
944
+
945
+ argparse's built-in "version" action runs its text through the help
946
+ formatter, which re-wraps at terminal width and splits the path onto its
947
+ own line -- so `--version` output would depend on how wide the window is.
948
+ """
949
+
950
+ def __init__(self, option_strings: list[str], dest: str, **kw: object) -> None:
951
+ super().__init__(option_strings, dest, nargs=0, **kw) # type: ignore[arg-type]
952
+
953
+ def __call__(self, parser, namespace, values, option_string=None) -> None: # noqa: ANN001
954
+ print(version_line())
955
+ parser.exit()
956
+
957
+
926
958
  def main() -> None:
927
959
  ap = argparse.ArgumentParser(
928
960
  prog="tmuxpull",
@@ -941,6 +973,12 @@ def main() -> None:
941
973
  "main/master/trunk)."
942
974
  ),
943
975
  )
976
+ ap.add_argument(
977
+ "-V",
978
+ "--version",
979
+ action=_VersionAction,
980
+ help="Show the version and which copy is running, then exit.",
981
+ )
944
982
  ap.add_argument(
945
983
  "-d",
946
984
  "--max-depth",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tmuxpull"
7
- version = "0.2.0"
7
+ dynamic = ["version"]
8
8
  description = "Keep every git repo under a directory in step with its remote, concurrently, with a tmux session per repo that needs you"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -42,6 +42,10 @@ tmuxpull = "tmuxpull.__main__:main"
42
42
  [tool.hatch.build.targets.wheel]
43
43
  packages = ["src/tmuxpull"]
44
44
 
45
+ # One place to bump: src/tmuxpull/__init__.py's __version__.
46
+ [tool.hatch.version]
47
+ path = "src/tmuxpull/__init__.py"
48
+
45
49
  [tool.ruff]
46
50
  line-length = 120
47
51
  target-version = "py311"
@@ -50,6 +50,12 @@ from typing import Iterable
50
50
  import libtmux
51
51
 
52
52
 
53
+ # Single source of truth for the version: pyproject.toml reads it from here
54
+ # (hatch dynamic version), so the PyPI package, the generated standalone script
55
+ # and `--version` can never disagree.
56
+ __version__ = "0.2.1"
57
+
58
+
53
59
  # Directories that are never a repo we want to descend into.
54
60
  _SKIP_DIRS: frozenset[str] = frozenset(
55
61
  {
@@ -913,6 +919,32 @@ async def _run(repos: list[Repo], jobs: int, verbose: int, opts: Options) -> lis
913
919
  return results
914
920
 
915
921
 
922
+ def version_line() -> str:
923
+ """Version plus the file it is running from.
924
+
925
+ The path matters as much as the number: the same machine can have a PyPI
926
+ install, a `uv tool` install and a curl'd standalone script on PATH, and
927
+ this says which one you just ran.
928
+ """
929
+ return f"tmuxpull {__version__} ({Path(__file__).resolve()})"
930
+
931
+
932
+ class _VersionAction(argparse.Action):
933
+ """Print version_line() verbatim.
934
+
935
+ argparse's built-in "version" action runs its text through the help
936
+ formatter, which re-wraps at terminal width and splits the path onto its
937
+ own line -- so `--version` output would depend on how wide the window is.
938
+ """
939
+
940
+ def __init__(self, option_strings: list[str], dest: str, **kw: object) -> None:
941
+ super().__init__(option_strings, dest, nargs=0, **kw) # type: ignore[arg-type]
942
+
943
+ def __call__(self, parser, namespace, values, option_string=None) -> None: # noqa: ANN001
944
+ print(version_line())
945
+ parser.exit()
946
+
947
+
916
948
  def main() -> None:
917
949
  ap = argparse.ArgumentParser(
918
950
  prog="tmuxpull",
@@ -931,6 +963,12 @@ def main() -> None:
931
963
  "main/master/trunk)."
932
964
  ),
933
965
  )
966
+ ap.add_argument(
967
+ "-V",
968
+ "--version",
969
+ action=_VersionAction,
970
+ help="Show the version and which copy is running, then exit.",
971
+ )
934
972
  ap.add_argument(
935
973
  "-d",
936
974
  "--max-depth",
@@ -0,0 +1,90 @@
1
+ """The version must be one number, reported the same way by every copy.
2
+
3
+ There are four places it could drift: the module, the packaging metadata, the
4
+ generated standalone script, and the zsh port. `__version__` is the source of
5
+ truth (pyproject reads it via hatch's dynamic version) and these pin the rest
6
+ to it.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ import tmuxpull
19
+ from tmuxpull import __version__, version_line
20
+
21
+ ROOT = Path(__file__).resolve().parent.parent
22
+ ZSH_SCRIPT = ROOT / "bin" / "rebase-all"
23
+ STANDALONE = ROOT / "bin" / "rebase-all.py"
24
+
25
+
26
+ def _env() -> dict[str, str]:
27
+ import os
28
+
29
+ env = dict(os.environ)
30
+ env["PYTHONPATH"] = str(ROOT / "src")
31
+ return env
32
+
33
+
34
+ def test_version_is_a_release_number():
35
+ assert re.fullmatch(r"\d+\.\d+\.\d+([a-z0-9.]*)?", __version__), __version__
36
+
37
+
38
+ def test_packaging_metadata_matches_the_module():
39
+ """Proves pyproject's hatch dynamic version is wired to __version__."""
40
+ from importlib.metadata import PackageNotFoundError, version
41
+
42
+ try:
43
+ installed = version("tmuxpull")
44
+ except PackageNotFoundError:
45
+ pytest.skip("tmuxpull is not installed in this environment")
46
+ assert installed == __version__
47
+
48
+
49
+ def test_pyproject_does_not_hardcode_a_second_version():
50
+ body = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
51
+ assert 'dynamic = ["version"]' in body
52
+ assert '[tool.hatch.version]' in body
53
+ assert not re.search(r'(?m)^version = ', body), "a literal version would drift"
54
+
55
+
56
+ def test_version_line_names_the_running_file():
57
+ line = version_line()
58
+ assert line.startswith(f"tmuxpull {__version__} (")
59
+ assert str(Path(tmuxpull.__file__).resolve()) in line
60
+
61
+
62
+ def test_cli_version_flag_prints_and_exits_zero():
63
+ for flag in ("--version", "-V"):
64
+ proc = subprocess.run(
65
+ [sys.executable, "-m", "tmuxpull", flag],
66
+ capture_output=True,
67
+ text=True,
68
+ env=_env(),
69
+ )
70
+ assert proc.returncode == 0, proc.stderr
71
+ out = (proc.stdout + proc.stderr).strip()
72
+ assert out.startswith(f"tmuxpull {__version__} ")
73
+ # the second field is the bare version, so `--version | awk '{print $2}'` works
74
+ assert out.split()[1] == __version__
75
+
76
+
77
+ def test_standalone_script_carries_the_same_version():
78
+ body = STANDALONE.read_text(encoding="utf-8")
79
+ assert f'__version__ = "{__version__}"' in body
80
+
81
+
82
+ @pytest.mark.skipif(shutil.which("zsh") is None, reason="zsh not available")
83
+ def test_zsh_port_reports_the_same_version():
84
+ proc = subprocess.run(
85
+ [str(ZSH_SCRIPT), "--version"], capture_output=True, text=True, env=_env()
86
+ )
87
+ assert proc.returncode == 0, proc.stderr
88
+ out = proc.stdout.strip()
89
+ assert out.startswith(f"tmuxpull {__version__} (")
90
+ assert out.split()[1] == __version__
@@ -13,7 +13,6 @@ wheels = [
13
13
 
14
14
  [[package]]
15
15
  name = "tmuxpull"
16
- version = "0.2.0"
17
16
  source = { editable = "." }
18
17
  dependencies = [
19
18
  { name = "libtmux" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes