gitterApp 0.1.10__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.
- gitterapp-0.1.10/BusinessLogic/GitManager.py +263 -0
- gitterapp-0.1.10/BusinessLogic/toml_helper.py +23 -0
- gitterapp-0.1.10/LICENSE.md +23 -0
- gitterapp-0.1.10/PKG-INFO +95 -0
- gitterapp-0.1.10/README.md +83 -0
- gitterapp-0.1.10/TUI/GitFlow/finish_feature.py +54 -0
- gitterapp-0.1.10/TUI/GitFlow/finish_release.py +52 -0
- gitterapp-0.1.10/TUI/GitFlow/start_feature.py +48 -0
- gitterapp-0.1.10/TUI/GitFlow/start_release.py +81 -0
- gitterapp-0.1.10/TUI/Help/about_gitter.py +47 -0
- gitterapp-0.1.10/TUI/Help/help_menu.py +45 -0
- gitterapp-0.1.10/TUI/Help/markdown_viewer.py +34 -0
- gitterapp-0.1.10/TUI/Menu/FileMenu.py +34 -0
- gitterapp-0.1.10/TUI/Menu/GitMenu.py +46 -0
- gitterapp-0.1.10/TUI/Menu/MenuBar.py +14 -0
- gitterapp-0.1.10/TUI/Menu/ViewMenu.py +29 -0
- gitterapp-0.1.10/TUI/MenuApp.py +424 -0
- gitterapp-0.1.10/TUI/commit/git_commit.py +122 -0
- gitterapp-0.1.10/TUI/commit/git_staging.py +117 -0
- gitterapp-0.1.10/TUI/debug/rich_log.py +105 -0
- gitterapp-0.1.10/TUI/project/ProjectView.py +199 -0
- gitterapp-0.1.10/TUI/project/ReleaseNotes.py +32 -0
- gitterapp-0.1.10/TUI/project/add_or_edit_project.py +154 -0
- gitterapp-0.1.10/gitterApp.egg-info/PKG-INFO +95 -0
- gitterapp-0.1.10/gitterApp.egg-info/SOURCES.txt +37 -0
- gitterapp-0.1.10/gitterApp.egg-info/dependency_links.txt +1 -0
- gitterapp-0.1.10/gitterApp.egg-info/entry_points.txt +2 -0
- gitterapp-0.1.10/gitterApp.egg-info/requires.txt +3 -0
- gitterapp-0.1.10/gitterApp.egg-info/top_level.txt +3 -0
- gitterapp-0.1.10/model/GitLog.py +112 -0
- gitterapp-0.1.10/model/GitStatus.py +141 -0
- gitterapp-0.1.10/model/GitStatusFile.py +11 -0
- gitterapp-0.1.10/model/Issue.py +10 -0
- gitterapp-0.1.10/model/MainFile.py +59 -0
- gitterapp-0.1.10/model/MainFileManager.py +54 -0
- gitterapp-0.1.10/model/Project.py +197 -0
- gitterapp-0.1.10/model/Release.py +34 -0
- gitterapp-0.1.10/pyproject.toml +22 -0
- gitterapp-0.1.10/setup.cfg +4 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from model.GitLog import GitLog
|
|
5
|
+
from model.GitStatus import GitStatus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GitManager:
|
|
9
|
+
"""Runs git CLI commands via subprocess for a given repository path."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, repo_path: str):
|
|
12
|
+
"""
|
|
13
|
+
:param repo_path: Absolute path to the git repository.
|
|
14
|
+
"""
|
|
15
|
+
self.repo = repo_path
|
|
16
|
+
|
|
17
|
+
def get_status(self):
|
|
18
|
+
"""
|
|
19
|
+
Returns the current working tree status as a GitStatus object.
|
|
20
|
+
|
|
21
|
+
:returns: GitStatus with staged, unstaged, and untracked file lists,
|
|
22
|
+
or a string error message if no repository is set.
|
|
23
|
+
"""
|
|
24
|
+
if self.repo is None:
|
|
25
|
+
return "No repository initialized"
|
|
26
|
+
|
|
27
|
+
theStatus = subprocess.run(["git", "-C", self.repo, "status"], capture_output=True, text=True)
|
|
28
|
+
|
|
29
|
+
result = GitStatus()
|
|
30
|
+
result.process_status_response(theStatus.stdout)
|
|
31
|
+
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
def commit(self, message: str, add_unstaged: bool = False) -> tuple:
|
|
35
|
+
"""
|
|
36
|
+
Creates a commit with the given message.
|
|
37
|
+
|
|
38
|
+
:param message: The commit message.
|
|
39
|
+
:param add_unstaged: If True, passes -a to also stage tracked modified files.
|
|
40
|
+
:returns: (success, output) tuple where success is True if returncode == 0.
|
|
41
|
+
"""
|
|
42
|
+
args = ["git", "-C", self.repo, "commit"]
|
|
43
|
+
if add_unstaged:
|
|
44
|
+
args.append("-a")
|
|
45
|
+
args += ["-m", message]
|
|
46
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
47
|
+
success = result.returncode == 0
|
|
48
|
+
output = result.stdout.strip() or result.stderr.strip()
|
|
49
|
+
return success, output
|
|
50
|
+
|
|
51
|
+
def stage(self, filename: str) -> tuple:
|
|
52
|
+
"""
|
|
53
|
+
Stages a single file.
|
|
54
|
+
|
|
55
|
+
:param filename: Path to the file to stage, relative to the repository root.
|
|
56
|
+
:returns: (success, output) tuple.
|
|
57
|
+
"""
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
["git", "-C", self.repo, "add", filename],
|
|
60
|
+
capture_output=True, text=True,
|
|
61
|
+
)
|
|
62
|
+
return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
|
|
63
|
+
|
|
64
|
+
def stage_all(self) -> tuple:
|
|
65
|
+
"""
|
|
66
|
+
Stages all changes in the repository (git add -A).
|
|
67
|
+
|
|
68
|
+
:returns: (success, output) tuple.
|
|
69
|
+
"""
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
["git", "-C", self.repo, "add", "-A"],
|
|
72
|
+
capture_output=True, text=True,
|
|
73
|
+
)
|
|
74
|
+
return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
|
|
75
|
+
|
|
76
|
+
def unstage(self, filename: str) -> tuple:
|
|
77
|
+
"""
|
|
78
|
+
Unstages a single file, keeping working tree changes intact.
|
|
79
|
+
|
|
80
|
+
:param filename: Path to the file to unstage, relative to the repository root.
|
|
81
|
+
:returns: (success, output) tuple.
|
|
82
|
+
"""
|
|
83
|
+
result = subprocess.run(
|
|
84
|
+
["git", "-C", self.repo, "restore", "--staged", filename],
|
|
85
|
+
capture_output=True, text=True,
|
|
86
|
+
)
|
|
87
|
+
return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
|
|
88
|
+
|
|
89
|
+
def unstage_all(self) -> tuple:
|
|
90
|
+
"""
|
|
91
|
+
Unstages all staged changes, keeping working tree changes intact.
|
|
92
|
+
|
|
93
|
+
:returns: (success, output) tuple.
|
|
94
|
+
"""
|
|
95
|
+
result = subprocess.run(
|
|
96
|
+
["git", "-C", self.repo, "restore", "--staged", "."],
|
|
97
|
+
capture_output=True, text=True,
|
|
98
|
+
)
|
|
99
|
+
return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
|
|
100
|
+
|
|
101
|
+
def push(self, remote: str = "origin", branch: str = "") -> tuple:
|
|
102
|
+
"""
|
|
103
|
+
Pushes commits to a remote repository.
|
|
104
|
+
|
|
105
|
+
:param remote: The remote name to push to (default: "origin").
|
|
106
|
+
:param branch: The branch to push. If empty, uses the current tracking branch.
|
|
107
|
+
:returns: (success, output) tuple.
|
|
108
|
+
"""
|
|
109
|
+
args = ["git", "-C", self.repo, "push", remote]
|
|
110
|
+
if branch:
|
|
111
|
+
args.append(branch)
|
|
112
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
113
|
+
return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
|
|
114
|
+
|
|
115
|
+
def push_main_and_develop(self, remote: str = "origin") -> tuple:
|
|
116
|
+
"""
|
|
117
|
+
Pushes both the main/master branch and develop to the remote.
|
|
118
|
+
|
|
119
|
+
:param remote: The remote name to push to (default: "origin").
|
|
120
|
+
:returns: (success, output) tuple.
|
|
121
|
+
"""
|
|
122
|
+
main_branch = self._detect_main_branch()
|
|
123
|
+
return self._run_sequence(
|
|
124
|
+
["push", remote, main_branch],
|
|
125
|
+
["push", remote, "develop"],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def pull(self, remote: str = "origin", branch: str = "") -> tuple:
|
|
129
|
+
"""
|
|
130
|
+
Pulls and merges changes from a remote repository.
|
|
131
|
+
|
|
132
|
+
:param remote: The remote name to pull from (default: "origin").
|
|
133
|
+
:param branch: The branch to pull. If empty, uses the current tracking branch.
|
|
134
|
+
:returns: (success, output) tuple.
|
|
135
|
+
"""
|
|
136
|
+
args = ["git", "-C", self.repo, "pull", remote]
|
|
137
|
+
if branch:
|
|
138
|
+
args.append(branch)
|
|
139
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
140
|
+
return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
|
|
141
|
+
|
|
142
|
+
def fetch(self, remote: str = "origin", prune: bool = False) -> tuple:
|
|
143
|
+
"""
|
|
144
|
+
Fetches changes from a remote without merging.
|
|
145
|
+
|
|
146
|
+
:param remote: The remote name to fetch from (default: "origin").
|
|
147
|
+
:param prune: If True, removes remote-tracking branches that no longer exist on the remote.
|
|
148
|
+
:returns: (success, output) tuple.
|
|
149
|
+
"""
|
|
150
|
+
args = ["git", "-C", self.repo, "fetch", remote]
|
|
151
|
+
if prune:
|
|
152
|
+
args.append("--prune")
|
|
153
|
+
result = subprocess.run(args, capture_output=True, text=True)
|
|
154
|
+
return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
|
|
155
|
+
|
|
156
|
+
def _run(self, *args) -> tuple:
|
|
157
|
+
"""Run a single git command, returning (success, output)."""
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
["git", "-C", self.repo] + list(args),
|
|
160
|
+
capture_output=True, text=True,
|
|
161
|
+
)
|
|
162
|
+
return result.returncode == 0, result.stdout.strip() or result.stderr.strip()
|
|
163
|
+
|
|
164
|
+
def _run_sequence(self, *commands) -> tuple:
|
|
165
|
+
"""Run a sequence of git command arg-lists, stopping on first failure."""
|
|
166
|
+
output_lines = []
|
|
167
|
+
for args in commands:
|
|
168
|
+
success, output = self._run(*args)
|
|
169
|
+
if output:
|
|
170
|
+
output_lines.append(output)
|
|
171
|
+
if not success:
|
|
172
|
+
return False, "\n".join(output_lines)
|
|
173
|
+
return True, "\n".join(output_lines)
|
|
174
|
+
|
|
175
|
+
def flow_feature_start(self, name: str) -> tuple:
|
|
176
|
+
"""
|
|
177
|
+
Starts a feature branch from develop: checkout develop, create feature/<name>.
|
|
178
|
+
|
|
179
|
+
:param name: Feature branch name (without the feature/ prefix).
|
|
180
|
+
:returns: (success, output) tuple.
|
|
181
|
+
"""
|
|
182
|
+
safe_name = name.replace(" ", "_")
|
|
183
|
+
return self._run_sequence(
|
|
184
|
+
["checkout", "develop"],
|
|
185
|
+
["checkout", "-b", f"feature/{safe_name}"],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def flow_feature_finish(self, name: str) -> tuple:
|
|
189
|
+
"""
|
|
190
|
+
Finishes a feature branch: merge --no-ff into develop, delete the branch.
|
|
191
|
+
|
|
192
|
+
:param name: Feature branch name (without the feature/ prefix).
|
|
193
|
+
:returns: (success, output) tuple.
|
|
194
|
+
"""
|
|
195
|
+
branch = f"feature/{name}"
|
|
196
|
+
return self._run_sequence(
|
|
197
|
+
["checkout", "develop"],
|
|
198
|
+
["merge", "--no-ff", branch, "-m", f"Merge feature '{name}' into develop"],
|
|
199
|
+
["branch", "-d", branch],
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def flow_release_start(self, version: str) -> tuple:
|
|
203
|
+
"""
|
|
204
|
+
Starts a release branch from develop: checkout develop, create release/<version>.
|
|
205
|
+
|
|
206
|
+
:param version: Release version string (e.g. "1.2.0").
|
|
207
|
+
:returns: (success, output) tuple.
|
|
208
|
+
"""
|
|
209
|
+
return self._run_sequence(
|
|
210
|
+
["checkout", "develop"],
|
|
211
|
+
["checkout", "-b", f"release/{version}"],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def _detect_main_branch(self) -> str:
|
|
215
|
+
"""Returns 'main' if it exists as a local branch, otherwise 'master'."""
|
|
216
|
+
result = subprocess.run(
|
|
217
|
+
["git", "-C", self.repo, "show-ref", "--verify", "--quiet", "refs/heads/main"],
|
|
218
|
+
capture_output=True, text=True,
|
|
219
|
+
)
|
|
220
|
+
return "main" if result.returncode == 0 else "master"
|
|
221
|
+
|
|
222
|
+
def flow_release_finish(self, version: str) -> tuple:
|
|
223
|
+
"""
|
|
224
|
+
Finishes a release branch: merge into main/master, tag, merge into develop, delete branch, merge main/master back into develop.
|
|
225
|
+
|
|
226
|
+
:param version: Release version string (e.g. "1.2.0").
|
|
227
|
+
:returns: (success, output) tuple.
|
|
228
|
+
"""
|
|
229
|
+
main_branch = self._detect_main_branch()
|
|
230
|
+
branch = f"release/{version}"
|
|
231
|
+
return self._run_sequence(
|
|
232
|
+
["checkout", main_branch],
|
|
233
|
+
["merge", "--no-ff", branch, "-m", f"Release {version}"],
|
|
234
|
+
["tag", "-a", version, "-m", f"Release {version}"],
|
|
235
|
+
["checkout", "develop"],
|
|
236
|
+
["merge", "--no-ff", branch, "-m", f"Merge release '{version}' into develop"],
|
|
237
|
+
["branch", "-d", branch],
|
|
238
|
+
["merge", "--no-ff", main_branch, "-m", f"Merge {main_branch} into develop after release {version}"],
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def get_logs(self, limit: int = 1000, branch: str = ""):
|
|
242
|
+
"""
|
|
243
|
+
Returns parsed git log entries for the repository.
|
|
244
|
+
|
|
245
|
+
:param limit: Maximum number of commits to retrieve (default: 1000).
|
|
246
|
+
:param branch: Branch to retrieve logs for. If empty, uses the current branch.
|
|
247
|
+
:returns: List of parsed GitLog entries.
|
|
248
|
+
"""
|
|
249
|
+
# GitterLogger.log( f"**********************************\nGetting logs for branch {branch} at {self.repo}\n**********************************" )
|
|
250
|
+
|
|
251
|
+
if self.repo is None:
|
|
252
|
+
return "No repository initialized"
|
|
253
|
+
|
|
254
|
+
args = ["git", "-C", self.repo, "log", "--decorate", "-n", f"{limit}"]
|
|
255
|
+
if (branch != ""):
|
|
256
|
+
args.append( branch)
|
|
257
|
+
|
|
258
|
+
theLogs = subprocess.run( args, capture_output=True, text=True)
|
|
259
|
+
|
|
260
|
+
# GitterLogger.log( f"Git logs: {theLogs.stdout}" )
|
|
261
|
+
# GitterLogger.log( f"Git error: {theLogs.stderr}" )
|
|
262
|
+
|
|
263
|
+
return GitLog.parse_logstring( theLogs.stdout )
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
class TomlHelper:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.filename = Path(__file__).parent.parent / "pyproject.toml"
|
|
8
|
+
|
|
9
|
+
"""Helper class for working with TOML files."""
|
|
10
|
+
def get_version(self) -> str:
|
|
11
|
+
with open(self.filename, "r") as file:
|
|
12
|
+
for line in file:
|
|
13
|
+
strip_line = line.strip()
|
|
14
|
+
if strip_line.startswith("version"):
|
|
15
|
+
return strip_line.split("=")[1].strip()
|
|
16
|
+
|
|
17
|
+
return "ERROR: Version not found in pyproject.toml"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
helper = TomlHelper()
|
|
22
|
+
version = helper.get_version()
|
|
23
|
+
print(f"Project version: {version}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# MIT License
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Bobby Skinner
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the “Software”), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
**THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.**
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitterApp
|
|
3
|
+
Version: 0.1.10
|
|
4
|
+
Summary: Git repo manager get status of multiple git repos in one place
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE.md
|
|
8
|
+
Requires-Dist: gitstatus>=1.2
|
|
9
|
+
Requires-Dist: textual>=8.1.1
|
|
10
|
+
Requires-Dist: textual-dev>=1.8.0
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# Gitter
|
|
14
|
+
|
|
15
|
+
A terminal dashboard for monitoring multiple Git repositories. Built with Python and [Textual](https://github.com/Textualize/textual).
|
|
16
|
+
|
|
17
|
+
Gitter shows the status, current release, and upcoming issues for all your configured repos at a glance — and lets you stage files and write commits without leaving the terminal.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **Multi-repo dashboard** — see branch status, release version, and next-release issues for all projects in one view
|
|
24
|
+
- **Release notes** — commits are parsed into tagged releases and issues, displayed as a Markdown panel
|
|
25
|
+
- **Git commit modal** — type/issue/summary fields auto-filled from the branch name, with a staging view for reviewing changes before committing
|
|
26
|
+
- **Stage/unstage files** — double-click files to stage or unstage; one-click stage all / unstage all
|
|
27
|
+
- **CLI interface** — `status` and `issues` commands with Rich-formatted tables for use in scripts or pipelines
|
|
28
|
+
- **Auto-refresh** — repos are polled every 90 seconds in the background
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.9+
|
|
33
|
+
- Git installed and available on `PATH`
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/bobbyskinnerart/Gitter.git
|
|
39
|
+
cd Gitter
|
|
40
|
+
python -m venv .venv
|
|
41
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
42
|
+
pip install -r requirements.txt
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### TUI (interactive dashboard)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python main.py tui
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### CLI commands
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python main.py status # Table of all projects with status and release info
|
|
57
|
+
python main.py issues # All issues grouped by project and release
|
|
58
|
+
python main.py issues -p <project> # Filter by project name
|
|
59
|
+
python main.py issues -r <release> # Filter by release tag
|
|
60
|
+
python main.py version # Print version
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Key bindings (TUI)
|
|
64
|
+
|
|
65
|
+
| Key | Action |
|
|
66
|
+
|-----|--------|
|
|
67
|
+
| `Ctrl+K` | Commit selected project |
|
|
68
|
+
| `Ctrl+A` | Add project |
|
|
69
|
+
| `Ctrl+E` | Edit selected project |
|
|
70
|
+
| `Ctrl+D` | Delete selected project |
|
|
71
|
+
| `Ctrl+R` | Toggle release notes panel |
|
|
72
|
+
| `Ctrl+L` | Toggle log panel |
|
|
73
|
+
| `Ctrl+Q` | Quit |
|
|
74
|
+
|
|
75
|
+
Double-click a project row to edit it.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
Projects are stored in `~/.gitter` as JSON. Add a repo via `Ctrl+A` in the TUI or by editing the file directly.
|
|
80
|
+
|
|
81
|
+
Each project entry supports:
|
|
82
|
+
|
|
83
|
+
| Field | Description |
|
|
84
|
+
|-------|-------------|
|
|
85
|
+
| `name` | Display name |
|
|
86
|
+
| `directory` | Absolute path to the git repo |
|
|
87
|
+
| `tagBranch` | Branch where release tags live (e.g. `main`) |
|
|
88
|
+
| `issuePrefixes` | Issue prefixes to detect in commit messages (e.g. `GIT`, `PROJ`) |
|
|
89
|
+
| `prPatterns` | PR number patterns (regex) |
|
|
90
|
+
| `groups` | Optional grouping labels |
|
|
91
|
+
| `favorite` | Mark as favorite |
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT — see [LICENSE.md](LICENSE.md) for details.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Gitter
|
|
2
|
+
|
|
3
|
+
A terminal dashboard for monitoring multiple Git repositories. Built with Python and [Textual](https://github.com/Textualize/textual).
|
|
4
|
+
|
|
5
|
+
Gitter shows the status, current release, and upcoming issues for all your configured repos at a glance — and lets you stage files and write commits without leaving the terminal.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Multi-repo dashboard** — see branch status, release version, and next-release issues for all projects in one view
|
|
12
|
+
- **Release notes** — commits are parsed into tagged releases and issues, displayed as a Markdown panel
|
|
13
|
+
- **Git commit modal** — type/issue/summary fields auto-filled from the branch name, with a staging view for reviewing changes before committing
|
|
14
|
+
- **Stage/unstage files** — double-click files to stage or unstage; one-click stage all / unstage all
|
|
15
|
+
- **CLI interface** — `status` and `issues` commands with Rich-formatted tables for use in scripts or pipelines
|
|
16
|
+
- **Auto-refresh** — repos are polled every 90 seconds in the background
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Python 3.9+
|
|
21
|
+
- Git installed and available on `PATH`
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/bobbyskinnerart/Gitter.git
|
|
27
|
+
cd Gitter
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
30
|
+
pip install -r requirements.txt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### TUI (interactive dashboard)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python main.py tui
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### CLI commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python main.py status # Table of all projects with status and release info
|
|
45
|
+
python main.py issues # All issues grouped by project and release
|
|
46
|
+
python main.py issues -p <project> # Filter by project name
|
|
47
|
+
python main.py issues -r <release> # Filter by release tag
|
|
48
|
+
python main.py version # Print version
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Key bindings (TUI)
|
|
52
|
+
|
|
53
|
+
| Key | Action |
|
|
54
|
+
|-----|--------|
|
|
55
|
+
| `Ctrl+K` | Commit selected project |
|
|
56
|
+
| `Ctrl+A` | Add project |
|
|
57
|
+
| `Ctrl+E` | Edit selected project |
|
|
58
|
+
| `Ctrl+D` | Delete selected project |
|
|
59
|
+
| `Ctrl+R` | Toggle release notes panel |
|
|
60
|
+
| `Ctrl+L` | Toggle log panel |
|
|
61
|
+
| `Ctrl+Q` | Quit |
|
|
62
|
+
|
|
63
|
+
Double-click a project row to edit it.
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
Projects are stored in `~/.gitter` as JSON. Add a repo via `Ctrl+A` in the TUI or by editing the file directly.
|
|
68
|
+
|
|
69
|
+
Each project entry supports:
|
|
70
|
+
|
|
71
|
+
| Field | Description |
|
|
72
|
+
|-------|-------------|
|
|
73
|
+
| `name` | Display name |
|
|
74
|
+
| `directory` | Absolute path to the git repo |
|
|
75
|
+
| `tagBranch` | Branch where release tags live (e.g. `main`) |
|
|
76
|
+
| `issuePrefixes` | Issue prefixes to detect in commit messages (e.g. `GIT`, `PROJ`) |
|
|
77
|
+
| `prPatterns` | PR number patterns (regex) |
|
|
78
|
+
| `groups` | Optional grouping labels |
|
|
79
|
+
| `favorite` | Mark as favorite |
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT — see [LICENSE.md](LICENSE.md) for details.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from textual import on
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.screen import ModalScreen
|
|
8
|
+
from textual.widgets import Button, Label
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
|
|
11
|
+
from model.Project import Project
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FinishFeatureModal(ModalScreen[bool]):
|
|
15
|
+
"""Modal confirming finish of the current git-flow feature branch.
|
|
16
|
+
|
|
17
|
+
Dismisses with True on Finish, or False on Cancel.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, project: Project):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self._project = project
|
|
23
|
+
branch = project.status.branch if project.status else ""
|
|
24
|
+
self._feature_name = branch.split("/", 1)[1] if "/" in branch else branch
|
|
25
|
+
|
|
26
|
+
def compose(self) -> ComposeResult:
|
|
27
|
+
yield Vertical(
|
|
28
|
+
Label("Finish Feature", id="finish_feature_title"),
|
|
29
|
+
Label(
|
|
30
|
+
f"Do you want to finish the feature: [bold cyan]{self._feature_name}[/bold cyan]?",
|
|
31
|
+
id="finish_feature_message",
|
|
32
|
+
),
|
|
33
|
+
Horizontal(
|
|
34
|
+
Button("Cancel", variant="default", id="btn_cancel"),
|
|
35
|
+
Button("Finish", variant="primary", id="btn_finish"),
|
|
36
|
+
id="finish_feature_buttons",
|
|
37
|
+
),
|
|
38
|
+
id="finish_feature_container",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def on_mount(self) -> None:
|
|
42
|
+
self.query_one("#btn_finish", Button).focus()
|
|
43
|
+
|
|
44
|
+
@on(Button.Pressed, "#btn_cancel")
|
|
45
|
+
def handle_cancel(self) -> None:
|
|
46
|
+
self.dismiss(False)
|
|
47
|
+
|
|
48
|
+
@on(Button.Pressed, "#btn_finish")
|
|
49
|
+
def handle_finish(self) -> None:
|
|
50
|
+
self.dismiss(True)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def feature_name(self) -> str:
|
|
54
|
+
return self._feature_name
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual import on
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.screen import ModalScreen
|
|
6
|
+
from textual.widgets import Button, Label
|
|
7
|
+
from textual.containers import Horizontal, Vertical
|
|
8
|
+
|
|
9
|
+
from model.Project import Project
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FinishReleaseModal(ModalScreen[bool]):
|
|
13
|
+
"""Modal confirming finish of the current git-flow release branch.
|
|
14
|
+
|
|
15
|
+
Dismisses with True on Finish, or False on Cancel.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, project: Project):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self._project = project
|
|
21
|
+
branch = project.status.branch if project.status else ""
|
|
22
|
+
self._version = branch.split("/", 1)[1] if "/" in branch else branch
|
|
23
|
+
|
|
24
|
+
def compose(self) -> ComposeResult:
|
|
25
|
+
yield Vertical(
|
|
26
|
+
Label("Finish Release", id="finish_release_title"),
|
|
27
|
+
Label(
|
|
28
|
+
f"Do you want to finish release: [bold cyan]{self._version}[/bold cyan]?",
|
|
29
|
+
id="finish_release_message",
|
|
30
|
+
),
|
|
31
|
+
Horizontal(
|
|
32
|
+
Button("Cancel", variant="default", id="btn_cancel"),
|
|
33
|
+
Button("Finish", variant="primary", id="btn_finish"),
|
|
34
|
+
id="finish_release_buttons",
|
|
35
|
+
),
|
|
36
|
+
id="finish_release_container",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def on_mount(self) -> None:
|
|
40
|
+
self.query_one("#btn_finish", Button).focus()
|
|
41
|
+
|
|
42
|
+
@on(Button.Pressed, "#btn_cancel")
|
|
43
|
+
def handle_cancel(self) -> None:
|
|
44
|
+
self.dismiss(False)
|
|
45
|
+
|
|
46
|
+
@on(Button.Pressed, "#btn_finish")
|
|
47
|
+
def handle_finish(self) -> None:
|
|
48
|
+
self.dismiss(True)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def version(self) -> str:
|
|
52
|
+
return self._version
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from textual import on
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.screen import ModalScreen
|
|
8
|
+
from textual.widgets import Button, Input, Label
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
|
|
11
|
+
from model.Project import Project
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StartFeatureModal(ModalScreen[Optional[str]]):
|
|
15
|
+
"""Modal for starting a new git-flow feature branch.
|
|
16
|
+
|
|
17
|
+
Dismisses with the feature name string on Start, or None on Cancel.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, project: Project):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self._project = project
|
|
23
|
+
|
|
24
|
+
def compose(self) -> ComposeResult:
|
|
25
|
+
yield Vertical(
|
|
26
|
+
Label(f"Start Feature — {self._project.name}", id="start_feature_title"),
|
|
27
|
+
Label("Feature name", classes="start_feature_label"),
|
|
28
|
+
Input(placeholder="my-feature", id="feature_name"),
|
|
29
|
+
Horizontal(
|
|
30
|
+
Button("Cancel", variant="default", id="btn_cancel"),
|
|
31
|
+
Button("Start", variant="primary", id="btn_start"),
|
|
32
|
+
id="start_feature_buttons",
|
|
33
|
+
),
|
|
34
|
+
id="start_feature_container",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def on_mount(self) -> None:
|
|
38
|
+
self.query_one("#feature_name", Input).focus()
|
|
39
|
+
|
|
40
|
+
@on(Button.Pressed, "#btn_cancel")
|
|
41
|
+
def handle_cancel(self) -> None:
|
|
42
|
+
self.dismiss(None)
|
|
43
|
+
|
|
44
|
+
@on(Button.Pressed, "#btn_start")
|
|
45
|
+
def handle_start(self) -> None:
|
|
46
|
+
name = self.query_one("#feature_name", Input).value.strip()
|
|
47
|
+
if name:
|
|
48
|
+
self.dismiss(name)
|