lbranch 0.1.0__py3-none-any.whl
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.
- lbranch/__init__.py +8 -0
- lbranch/main.py +284 -0
- lbranch-0.1.0.dist-info/METADATA +76 -0
- lbranch-0.1.0.dist-info/RECORD +7 -0
- lbranch-0.1.0.dist-info/WHEEL +4 -0
- lbranch-0.1.0.dist-info/entry_points.txt +2 -0
- lbranch-0.1.0.dist-info/licenses/LICENSE +21 -0
lbranch/__init__.py
ADDED
lbranch/main.py
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
# Last Branch (lbranch) - Git branch history utility
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import os
|
7
|
+
import platform
|
8
|
+
import re
|
9
|
+
import subprocess
|
10
|
+
import sys
|
11
|
+
|
12
|
+
# Exit codes - following sysexits.h conventions
|
13
|
+
EXIT_SUCCESS = 0 # successful execution
|
14
|
+
EXIT_USAGE = 64 # command line usage error
|
15
|
+
EXIT_DATAERR = 65 # data format error
|
16
|
+
EXIT_NOINPUT = 66 # cannot open input
|
17
|
+
EXIT_NOUSER = 67 # addressee unknown
|
18
|
+
EXIT_NOHOST = 68 # host name unknown
|
19
|
+
EXIT_UNAVAILABLE = 69 # service unavailable
|
20
|
+
EXIT_SOFTWARE = 70 # internal software error
|
21
|
+
EXIT_OSERR = 71 # system error (e.g., can't fork)
|
22
|
+
EXIT_OSFILE = 72 # critical OS file missing
|
23
|
+
EXIT_CANTCREAT = 73 # can't create (user) output file
|
24
|
+
EXIT_IOERR = 74 # input/output error
|
25
|
+
EXIT_TEMPFAIL = 75 # temp failure; user is invited to retry
|
26
|
+
EXIT_PROTOCOL = 76 # remote error in protocol
|
27
|
+
EXIT_NOPERM = 77 # permission denied
|
28
|
+
EXIT_CONFIG = 78 # configuration error
|
29
|
+
|
30
|
+
# Custom mapping of our error conditions to sysexits.h values
|
31
|
+
EXIT_GIT_NOT_FOUND = EXIT_UNAVAILABLE # Git command not found (69)
|
32
|
+
EXIT_NOT_A_GIT_REPO = EXIT_USAGE # Not in a git repository (64)
|
33
|
+
EXIT_NO_COMMITS = EXIT_NOINPUT # No branch history/no commits (66)
|
34
|
+
EXIT_INVALID_SELECTION = EXIT_USAGE # Invalid branch selection (64)
|
35
|
+
EXIT_CHECKOUT_FAILED = EXIT_TEMPFAIL # Branch checkout failed, retry possible (75)
|
36
|
+
EXIT_INTERRUPTED = 130 # Operation interrupted (Ctrl+C) - shell standard
|
37
|
+
|
38
|
+
|
39
|
+
# Colors for output - with fallback detection
|
40
|
+
def supports_color():
|
41
|
+
"""
|
42
|
+
Returns True if the terminal supports color, False otherwise.
|
43
|
+
Falls back to no-color on non-TTY or Windows (unless FORCE_COLOR is set).
|
44
|
+
"""
|
45
|
+
# Return True if the FORCE_COLOR environment variable is set
|
46
|
+
if os.environ.get('FORCE_COLOR', '').lower() in ('1', 'true', 'yes', 'on'):
|
47
|
+
return True
|
48
|
+
|
49
|
+
# Return False if NO_COLOR environment variable is set (honor convention)
|
50
|
+
if os.environ.get('NO_COLOR', ''):
|
51
|
+
return False
|
52
|
+
|
53
|
+
# Return False if not connected to a terminal
|
54
|
+
if not sys.stdout.isatty():
|
55
|
+
return False
|
56
|
+
|
57
|
+
# On Windows, check if running in a terminal that supports ANSI
|
58
|
+
if platform.system() == 'Windows':
|
59
|
+
# Windows Terminal and modern PowerShell support colors
|
60
|
+
# Older Windows consoles may not
|
61
|
+
# Simple check for modern Windows terminals
|
62
|
+
return (
|
63
|
+
os.environ.get('WT_SESSION')
|
64
|
+
or os.environ.get('TERM')
|
65
|
+
or 'ANSICON' in os.environ
|
66
|
+
)
|
67
|
+
|
68
|
+
# Most Unix terminals support colors
|
69
|
+
return True
|
70
|
+
|
71
|
+
|
72
|
+
# Set up colors based on environment
|
73
|
+
if supports_color():
|
74
|
+
RED = '\033[0;31m'
|
75
|
+
GREEN = '\033[0;32m'
|
76
|
+
BLUE = '\033[0;34m'
|
77
|
+
NC = '\033[0m' # No Color
|
78
|
+
else:
|
79
|
+
# No colors if not supported
|
80
|
+
RED = ''
|
81
|
+
GREEN = ''
|
82
|
+
BLUE = ''
|
83
|
+
NC = ''
|
84
|
+
|
85
|
+
# Version - should match pyproject.toml
|
86
|
+
__version__ = '0.1.0'
|
87
|
+
|
88
|
+
|
89
|
+
def print_error(message, exit_code=EXIT_SOFTWARE):
|
90
|
+
"""Print error message and exit with specified code"""
|
91
|
+
print(f'{RED}Error: {message}{NC}', file=sys.stderr)
|
92
|
+
sys.exit(exit_code)
|
93
|
+
|
94
|
+
|
95
|
+
def run_command(cmd, check=True, capture_output=True):
|
96
|
+
"""Run a shell command and handle errors"""
|
97
|
+
try:
|
98
|
+
result = subprocess.run(
|
99
|
+
cmd,
|
100
|
+
check=check,
|
101
|
+
text=True,
|
102
|
+
shell=isinstance(cmd, str),
|
103
|
+
capture_output=capture_output,
|
104
|
+
)
|
105
|
+
return result
|
106
|
+
except subprocess.CalledProcessError as e:
|
107
|
+
if not check:
|
108
|
+
return e
|
109
|
+
print_error(f'Command failed: {e}')
|
110
|
+
sys.exit(EXIT_SOFTWARE)
|
111
|
+
|
112
|
+
|
113
|
+
def parse_arguments():
|
114
|
+
"""Parse command line arguments using argparse"""
|
115
|
+
parser = argparse.ArgumentParser(
|
116
|
+
description='Show recently checked out Git branches in chronological order',
|
117
|
+
epilog='Example: lbranch -n 10 -s (shows the last 10 branches with option to '
|
118
|
+
'select one)',
|
119
|
+
)
|
120
|
+
|
121
|
+
parser.add_argument(
|
122
|
+
'-n',
|
123
|
+
'--number',
|
124
|
+
type=int,
|
125
|
+
default=5,
|
126
|
+
help='Number of branches to display (default: 5)',
|
127
|
+
)
|
128
|
+
parser.add_argument(
|
129
|
+
'-s',
|
130
|
+
'--select',
|
131
|
+
action='store_true',
|
132
|
+
help='Enter interactive mode to select a branch for checkout',
|
133
|
+
)
|
134
|
+
parser.add_argument(
|
135
|
+
'-v',
|
136
|
+
'--version',
|
137
|
+
action='version',
|
138
|
+
version=f'%(prog)s {__version__}',
|
139
|
+
help='Show version information and exit',
|
140
|
+
)
|
141
|
+
parser.add_argument(
|
142
|
+
'-nc', '--no-color', action='store_true', help='Disable colored output'
|
143
|
+
)
|
144
|
+
parser.add_argument(
|
145
|
+
'-fc',
|
146
|
+
'--force-color',
|
147
|
+
action='store_true',
|
148
|
+
help='Force colored output even in non-TTY environments',
|
149
|
+
)
|
150
|
+
|
151
|
+
return parser.parse_args()
|
152
|
+
|
153
|
+
|
154
|
+
def main():
|
155
|
+
"""Main entry point for the lbranch command."""
|
156
|
+
args = parse_arguments()
|
157
|
+
|
158
|
+
# Handle manual color override options
|
159
|
+
global RED, GREEN, BLUE, NC
|
160
|
+
if args.no_color:
|
161
|
+
RED = GREEN = BLUE = NC = ''
|
162
|
+
elif args.force_color:
|
163
|
+
RED = '\033[0;31m'
|
164
|
+
GREEN = '\033[0;32m'
|
165
|
+
BLUE = '\033[0;34m'
|
166
|
+
NC = '\033[0m'
|
167
|
+
|
168
|
+
# Check if git is installed
|
169
|
+
try:
|
170
|
+
run_command(['git', '--version'], capture_output=True)
|
171
|
+
except FileNotFoundError:
|
172
|
+
print_error(
|
173
|
+
'git command not found. Please install git first.', EXIT_GIT_NOT_FOUND
|
174
|
+
)
|
175
|
+
|
176
|
+
# Check if we're in a git repository
|
177
|
+
if (
|
178
|
+
run_command(
|
179
|
+
['git', 'rev-parse', '--is-inside-work-tree'],
|
180
|
+
check=False,
|
181
|
+
capture_output=True,
|
182
|
+
).returncode
|
183
|
+
!= 0
|
184
|
+
):
|
185
|
+
print_error(
|
186
|
+
'Not a git repository. Please run this command from within a git '
|
187
|
+
'repository.',
|
188
|
+
EXIT_NOT_A_GIT_REPO,
|
189
|
+
)
|
190
|
+
|
191
|
+
# Check if the repository has any commits
|
192
|
+
if (
|
193
|
+
run_command(
|
194
|
+
['git', 'rev-parse', '--verify', 'HEAD'], check=False, capture_output=True
|
195
|
+
).returncode
|
196
|
+
!= 0
|
197
|
+
):
|
198
|
+
print(f'{BLUE}No branch history found - repository has no commits yet{NC}')
|
199
|
+
sys.exit(EXIT_NO_COMMITS)
|
200
|
+
|
201
|
+
# Get current branch name
|
202
|
+
try:
|
203
|
+
current_branch = run_command(
|
204
|
+
['git', 'symbolic-ref', '--short', 'HEAD'], capture_output=True
|
205
|
+
).stdout.strip()
|
206
|
+
except subprocess.CalledProcessError:
|
207
|
+
current_branch = run_command(
|
208
|
+
['git', 'rev-parse', '--short', 'HEAD'], capture_output=True
|
209
|
+
).stdout.strip()
|
210
|
+
|
211
|
+
# Get unique branch history
|
212
|
+
reflog_output = run_command(['git', 'reflog'], capture_output=True).stdout
|
213
|
+
|
214
|
+
branches = []
|
215
|
+
for line in reflog_output.splitlines():
|
216
|
+
# Look for checkout lines without using grep
|
217
|
+
if 'checkout: moving from' in line.lower():
|
218
|
+
# Parse the branch name after "from"
|
219
|
+
parts = line.split()
|
220
|
+
try:
|
221
|
+
from_index = parts.index('from')
|
222
|
+
if from_index + 1 < len(parts):
|
223
|
+
branch = parts[from_index + 1]
|
224
|
+
|
225
|
+
# Skip empty, current branch, or branches starting with '{'
|
226
|
+
if not branch or branch == current_branch or branch.startswith('{'):
|
227
|
+
continue
|
228
|
+
|
229
|
+
# Only add branch if it's not already in the list
|
230
|
+
if branch not in branches:
|
231
|
+
branches.append(branch)
|
232
|
+
except ValueError:
|
233
|
+
continue # "from" not found in this line
|
234
|
+
|
235
|
+
# Limit to requested number of branches
|
236
|
+
total_branches = len(branches)
|
237
|
+
if total_branches == 0:
|
238
|
+
print(f'{BLUE}Last {args.number} branches:{NC}')
|
239
|
+
sys.exit(EXIT_SUCCESS)
|
240
|
+
|
241
|
+
branch_limit = min(args.number, total_branches)
|
242
|
+
branches = branches[:branch_limit] # Limit to requested count
|
243
|
+
|
244
|
+
# Display branches
|
245
|
+
print(f'{BLUE}Last {args.number} branches:{NC}')
|
246
|
+
for i, branch in enumerate(branches, 1):
|
247
|
+
print(f'{i}) {branch}')
|
248
|
+
|
249
|
+
# Handle select mode
|
250
|
+
if args.select:
|
251
|
+
try:
|
252
|
+
print(f'\n{GREEN}Enter branch number to checkout (1-{branch_limit}):{NC}')
|
253
|
+
branch_num = input()
|
254
|
+
|
255
|
+
if (
|
256
|
+
not re.match(r'^\d+$', branch_num)
|
257
|
+
or int(branch_num) < 1
|
258
|
+
or int(branch_num) > branch_limit
|
259
|
+
):
|
260
|
+
print_error(f'Invalid selection: {branch_num}', EXIT_INVALID_SELECTION)
|
261
|
+
|
262
|
+
selected_branch = branches[int(branch_num) - 1]
|
263
|
+
print(f'\nChecking out: {selected_branch}')
|
264
|
+
|
265
|
+
# Attempt to checkout the branch
|
266
|
+
result = run_command(
|
267
|
+
['git', 'checkout', selected_branch], check=False, capture_output=True
|
268
|
+
)
|
269
|
+
if result.returncode != 0:
|
270
|
+
print_error(
|
271
|
+
f'Failed to checkout branch:\n{result.stderr}', EXIT_CHECKOUT_FAILED
|
272
|
+
)
|
273
|
+
|
274
|
+
print(f'{GREEN}Successfully checked out {selected_branch}{NC}')
|
275
|
+
except KeyboardInterrupt:
|
276
|
+
print('\nOperation cancelled.')
|
277
|
+
sys.exit(EXIT_INTERRUPTED)
|
278
|
+
|
279
|
+
# Success
|
280
|
+
return EXIT_SUCCESS
|
281
|
+
|
282
|
+
|
283
|
+
if __name__ == '__main__':
|
284
|
+
sys.exit(main())
|
@@ -0,0 +1,76 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: lbranch
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A Git utility that shows recently checked-out branches in chronological order and lets you quickly switch between them.
|
5
|
+
Author: Chuck Danielsson
|
6
|
+
License-Expression: MIT
|
7
|
+
License-File: LICENSE
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
9
|
+
Classifier: Environment :: Console
|
10
|
+
Classifier: Intended Audience :: Developers
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
13
|
+
Classifier: Programming Language :: Python :: 3.7
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
18
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
19
|
+
Requires-Python: >=3.7
|
20
|
+
Description-Content-Type: text/markdown
|
21
|
+
|
22
|
+
# lbranch
|
23
|
+
lbranch ("last branch") is a git utility that shows your recently checked out branches in chronological order, with an optional interactive checkout.
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
```bash
|
27
|
+
# Show last 5 branches (default)
|
28
|
+
lbranch
|
29
|
+
# Show last N branches
|
30
|
+
lbranch -n 3
|
31
|
+
lbranch --number 3
|
32
|
+
# Show branches and select one to checkout
|
33
|
+
lbranch -s
|
34
|
+
lbranch --select
|
35
|
+
# Show last N branches and select one
|
36
|
+
lbranch -n 3 -s
|
37
|
+
# Color control
|
38
|
+
lbranch --no-color # Disable colored output
|
39
|
+
lbranch --force-color # Force colored output even in non-TTY environments
|
40
|
+
```
|
41
|
+
|
42
|
+
## Example Output
|
43
|
+
```bash
|
44
|
+
Last 5 branches:
|
45
|
+
1) feature/new-ui
|
46
|
+
2) main
|
47
|
+
3) bugfix/login
|
48
|
+
4) feature/api
|
49
|
+
5) develop
|
50
|
+
```
|
51
|
+
|
52
|
+
## Color Support
|
53
|
+
lbranch automatically detects if your terminal supports colors:
|
54
|
+
- Colors are disabled when output is not to a terminal (when piped to a file or another command)
|
55
|
+
- Colors are disabled on Windows unless running in a modern terminal (Windows Terminal, VS Code, etc.)
|
56
|
+
- You can force colors on with `--force-color` or off with `--no-color`
|
57
|
+
- lbranch respects the `NO_COLOR` and `FORCE_COLOR` environment variables
|
58
|
+
|
59
|
+
## Exit Codes
|
60
|
+
lbranch follows the standard exit codes from sysexits.h for better integration with scripts and other tools:
|
61
|
+
|
62
|
+
- 0: Success
|
63
|
+
- 64: Command line usage error (not in a git repository, invalid selection)
|
64
|
+
- 66: Cannot open input (no branch history/no commits)
|
65
|
+
- 69: Service unavailable (git command not found)
|
66
|
+
- 75: Temporary failure (branch checkout failed, retry possible)
|
67
|
+
- 130: Operation interrupted (Ctrl+C)
|
68
|
+
|
69
|
+
These follow Unix conventions where exit codes 64-78 are standardized error codes, and 128+N indicates termination by signal N.
|
70
|
+
|
71
|
+
## Requirements
|
72
|
+
- Python 3.6+
|
73
|
+
- Git
|
74
|
+
|
75
|
+
## License
|
76
|
+
Distributed under the MIT License. See `LICENSE`
|
@@ -0,0 +1,7 @@
|
|
1
|
+
lbranch/__init__.py,sha256=_er8xHaxsjo0D5us09aEXjFOv5g4wCqbbZ74Cn5Sc48,163
|
2
|
+
lbranch/main.py,sha256=qlCuVBK8QYMaSuGsLXyHM-LRcE5gpWlbuEldKQQ8jH0,8998
|
3
|
+
lbranch-0.1.0.dist-info/METADATA,sha256=7_f_p6kqPdRRmkWX4rNd-kOjz7N-jqRhesIa-QmeBXw,2590
|
4
|
+
lbranch-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
5
|
+
lbranch-0.1.0.dist-info/entry_points.txt,sha256=Qt0Vg0xVAPAdyS9CTdTlrFJKuknrpuei08izhQb6bfY,46
|
6
|
+
lbranch-0.1.0.dist-info/licenses/LICENSE,sha256=oCT5BBwIA4HS-px42XMGO-QxY9dENLbh3_-GMpm1DFc,1066
|
7
|
+
lbranch-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 chuckd.dev
|
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.
|