git-glimpse-cli 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_glimpse_cli-0.1.0/LICENSE +21 -0
- git_glimpse_cli-0.1.0/PKG-INFO +83 -0
- git_glimpse_cli-0.1.0/README.md +71 -0
- git_glimpse_cli-0.1.0/git_glimpse_cli.egg-info/PKG-INFO +83 -0
- git_glimpse_cli-0.1.0/git_glimpse_cli.egg-info/SOURCES.txt +15 -0
- git_glimpse_cli-0.1.0/git_glimpse_cli.egg-info/dependency_links.txt +1 -0
- git_glimpse_cli-0.1.0/git_glimpse_cli.egg-info/entry_points.txt +2 -0
- git_glimpse_cli-0.1.0/git_glimpse_cli.egg-info/requires.txt +3 -0
- git_glimpse_cli-0.1.0/git_glimpse_cli.egg-info/top_level.txt +1 -0
- git_glimpse_cli-0.1.0/glimpsecli/__init__.py +0 -0
- git_glimpse_cli-0.1.0/glimpsecli/api.py +71 -0
- git_glimpse_cli-0.1.0/glimpsecli/config.py +116 -0
- git_glimpse_cli-0.1.0/glimpsecli/helpers.py +97 -0
- git_glimpse_cli-0.1.0/glimpsecli/main.py +270 -0
- git_glimpse_cli-0.1.0/glimpsecli/utils.py +130 -0
- git_glimpse_cli-0.1.0/pyproject.toml +18 -0
- git_glimpse_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antoni "sutaC" Siekański
|
|
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,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-glimpse-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool to manage and update shared GitHub repos with GitGlimpse.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: httpx>=0.28.1
|
|
9
|
+
Requires-Dist: rich>=15.0.0
|
|
10
|
+
Requires-Dist: typer>=0.27.2
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# GitGlimpse CLI
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+

|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
A command-line interface to manage, synchronize, and monitor shared GitHub repositories via the GitGlimpse platform.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
**GitGlimpse website:** [gitglimpse.sutac.pl](https://gitglimpse.sutac.pl/)
|
|
24
|
+
|
|
25
|
+
**GitGlimpse on GitHub:** [github.com/sutaC/git-glimpse-cli](https://github.com/sutaC/git-glimpse-cli/)
|
|
26
|
+
|
|
27
|
+
**Author:** [sutaC](https://github.com/sutaC)
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install the latest version globally via pip:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install git-glimpse-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
1. Log in with your GitGlimpse CLI token. It will be saved to your global user configuration.
|
|
40
|
+
> You will be able to create one on your GitGlimpse user panel at https://gitglimpse.sutac.pl/user#hCliTokens
|
|
41
|
+
```bash
|
|
42
|
+
glimpse login <YOUR_API_TOKEN>
|
|
43
|
+
```
|
|
44
|
+
2. Initialize a Repository.
|
|
45
|
+
> Navigate to your local Git repository, ensure it is uplaoded to GitHub and link it to GitGlimpse. This creates a .shared-repo.json file to track the project.
|
|
46
|
+
```bash
|
|
47
|
+
cd my-project
|
|
48
|
+
glimpse init <new/link>
|
|
49
|
+
```
|
|
50
|
+
3. Check status.
|
|
51
|
+
```bash
|
|
52
|
+
glimpse status
|
|
53
|
+
```
|
|
54
|
+
4. Schedule the build to update GitGlimpse display.
|
|
55
|
+
```bash
|
|
56
|
+
glimpse build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
GitGlimpse CLI uses holds your configuration at `~/.config/glimpsecli/config.json`, and local repository settings are stored at `./.shared-repo.json`.
|
|
62
|
+
|
|
63
|
+
### Advanced Settings
|
|
64
|
+
|
|
65
|
+
| Command | Description |
|
|
66
|
+
| ------------------------ | --------------------------------------------------------------------------- |
|
|
67
|
+
| `glimpse config debug` | Enables verbose debug mode, printing stack traces. |
|
|
68
|
+
| `glimpse config set-url` | Overrides the default production server with a custom backend or localhost. |
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
1. Clone the repository and install it in editable mode:
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/sutaC/git-glimpse-cli.git
|
|
75
|
+
cd git-glimpse-cli
|
|
76
|
+
python3 -m venv .venv
|
|
77
|
+
source .venv/bin/activate
|
|
78
|
+
pip install -e .
|
|
79
|
+
```
|
|
80
|
+
2. Point the CLI to your local development server:
|
|
81
|
+
```bash
|
|
82
|
+
glimpse config set-url http://localhost:5000
|
|
83
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# GitGlimpse CLI
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
A command-line interface to manage, synchronize, and monitor shared GitHub repositories via the GitGlimpse platform.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
**GitGlimpse website:** [gitglimpse.sutac.pl](https://gitglimpse.sutac.pl/)
|
|
12
|
+
|
|
13
|
+
**GitGlimpse on GitHub:** [github.com/sutaC/git-glimpse-cli](https://github.com/sutaC/git-glimpse-cli/)
|
|
14
|
+
|
|
15
|
+
**Author:** [sutaC](https://github.com/sutaC)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Install the latest version globally via pip:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install git-glimpse-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
1. Log in with your GitGlimpse CLI token. It will be saved to your global user configuration.
|
|
28
|
+
> You will be able to create one on your GitGlimpse user panel at https://gitglimpse.sutac.pl/user#hCliTokens
|
|
29
|
+
```bash
|
|
30
|
+
glimpse login <YOUR_API_TOKEN>
|
|
31
|
+
```
|
|
32
|
+
2. Initialize a Repository.
|
|
33
|
+
> Navigate to your local Git repository, ensure it is uplaoded to GitHub and link it to GitGlimpse. This creates a .shared-repo.json file to track the project.
|
|
34
|
+
```bash
|
|
35
|
+
cd my-project
|
|
36
|
+
glimpse init <new/link>
|
|
37
|
+
```
|
|
38
|
+
3. Check status.
|
|
39
|
+
```bash
|
|
40
|
+
glimpse status
|
|
41
|
+
```
|
|
42
|
+
4. Schedule the build to update GitGlimpse display.
|
|
43
|
+
```bash
|
|
44
|
+
glimpse build
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
GitGlimpse CLI uses holds your configuration at `~/.config/glimpsecli/config.json`, and local repository settings are stored at `./.shared-repo.json`.
|
|
50
|
+
|
|
51
|
+
### Advanced Settings
|
|
52
|
+
|
|
53
|
+
| Command | Description |
|
|
54
|
+
| ------------------------ | --------------------------------------------------------------------------- |
|
|
55
|
+
| `glimpse config debug` | Enables verbose debug mode, printing stack traces. |
|
|
56
|
+
| `glimpse config set-url` | Overrides the default production server with a custom backend or localhost. |
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
1. Clone the repository and install it in editable mode:
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/sutaC/git-glimpse-cli.git
|
|
63
|
+
cd git-glimpse-cli
|
|
64
|
+
python3 -m venv .venv
|
|
65
|
+
source .venv/bin/activate
|
|
66
|
+
pip install -e .
|
|
67
|
+
```
|
|
68
|
+
2. Point the CLI to your local development server:
|
|
69
|
+
```bash
|
|
70
|
+
glimpse config set-url http://localhost:5000
|
|
71
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: git-glimpse-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool to manage and update shared GitHub repos with GitGlimpse.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: httpx>=0.28.1
|
|
9
|
+
Requires-Dist: rich>=15.0.0
|
|
10
|
+
Requires-Dist: typer>=0.27.2
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# GitGlimpse CLI
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+

|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
A command-line interface to manage, synchronize, and monitor shared GitHub repositories via the GitGlimpse platform.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
**GitGlimpse website:** [gitglimpse.sutac.pl](https://gitglimpse.sutac.pl/)
|
|
24
|
+
|
|
25
|
+
**GitGlimpse on GitHub:** [github.com/sutaC/git-glimpse-cli](https://github.com/sutaC/git-glimpse-cli/)
|
|
26
|
+
|
|
27
|
+
**Author:** [sutaC](https://github.com/sutaC)
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install the latest version globally via pip:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install git-glimpse-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
1. Log in with your GitGlimpse CLI token. It will be saved to your global user configuration.
|
|
40
|
+
> You will be able to create one on your GitGlimpse user panel at https://gitglimpse.sutac.pl/user#hCliTokens
|
|
41
|
+
```bash
|
|
42
|
+
glimpse login <YOUR_API_TOKEN>
|
|
43
|
+
```
|
|
44
|
+
2. Initialize a Repository.
|
|
45
|
+
> Navigate to your local Git repository, ensure it is uplaoded to GitHub and link it to GitGlimpse. This creates a .shared-repo.json file to track the project.
|
|
46
|
+
```bash
|
|
47
|
+
cd my-project
|
|
48
|
+
glimpse init <new/link>
|
|
49
|
+
```
|
|
50
|
+
3. Check status.
|
|
51
|
+
```bash
|
|
52
|
+
glimpse status
|
|
53
|
+
```
|
|
54
|
+
4. Schedule the build to update GitGlimpse display.
|
|
55
|
+
```bash
|
|
56
|
+
glimpse build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
GitGlimpse CLI uses holds your configuration at `~/.config/glimpsecli/config.json`, and local repository settings are stored at `./.shared-repo.json`.
|
|
62
|
+
|
|
63
|
+
### Advanced Settings
|
|
64
|
+
|
|
65
|
+
| Command | Description |
|
|
66
|
+
| ------------------------ | --------------------------------------------------------------------------- |
|
|
67
|
+
| `glimpse config debug` | Enables verbose debug mode, printing stack traces. |
|
|
68
|
+
| `glimpse config set-url` | Overrides the default production server with a custom backend or localhost. |
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
1. Clone the repository and install it in editable mode:
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/sutaC/git-glimpse-cli.git
|
|
75
|
+
cd git-glimpse-cli
|
|
76
|
+
python3 -m venv .venv
|
|
77
|
+
source .venv/bin/activate
|
|
78
|
+
pip install -e .
|
|
79
|
+
```
|
|
80
|
+
2. Point the CLI to your local development server:
|
|
81
|
+
```bash
|
|
82
|
+
glimpse config set-url http://localhost:5000
|
|
83
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
git_glimpse_cli.egg-info/PKG-INFO
|
|
5
|
+
git_glimpse_cli.egg-info/SOURCES.txt
|
|
6
|
+
git_glimpse_cli.egg-info/dependency_links.txt
|
|
7
|
+
git_glimpse_cli.egg-info/entry_points.txt
|
|
8
|
+
git_glimpse_cli.egg-info/requires.txt
|
|
9
|
+
git_glimpse_cli.egg-info/top_level.txt
|
|
10
|
+
glimpsecli/__init__.py
|
|
11
|
+
glimpsecli/api.py
|
|
12
|
+
glimpsecli/config.py
|
|
13
|
+
glimpsecli/helpers.py
|
|
14
|
+
glimpsecli/main.py
|
|
15
|
+
glimpsecli/utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
glimpsecli
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
2
|
+
from glimpsecli import config, helpers
|
|
3
|
+
from contextlib import nullcontext
|
|
4
|
+
from json import JSONDecodeError
|
|
5
|
+
from rich import print
|
|
6
|
+
import typer
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
def request_api(
|
|
10
|
+
endpoint: str,
|
|
11
|
+
method: str = "GET",
|
|
12
|
+
token: str | None = None,
|
|
13
|
+
handle_codes: list[int] = [200],
|
|
14
|
+
payload: dict | None = None,
|
|
15
|
+
quiet=False
|
|
16
|
+
) -> httpx.Response:
|
|
17
|
+
"""Requests GitGlimpse api.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
endpoint: Path for api call (should not include `{API_BASE}/cli`).
|
|
21
|
+
method: Http metod to use.
|
|
22
|
+
token: GitGlimpse api token (if not provided will prompt user).
|
|
23
|
+
handle_codes: Response codes on which function will return response.
|
|
24
|
+
payload: Json payload.
|
|
25
|
+
quiet: Flag for disabling default progess spinner.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Response from api.
|
|
29
|
+
"""
|
|
30
|
+
if not token:
|
|
31
|
+
token = helpers.get_token()
|
|
32
|
+
url = f"{config.get_api_url()}{endpoint}"
|
|
33
|
+
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) if not quiet else nullcontext() as progress:
|
|
34
|
+
if progress:
|
|
35
|
+
progress.add_task(description="[bold green]Connecting to server...[/bold green]", total=None)
|
|
36
|
+
try:
|
|
37
|
+
response = httpx.request(
|
|
38
|
+
method,
|
|
39
|
+
url,
|
|
40
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
41
|
+
timeout=10.0,
|
|
42
|
+
json=payload
|
|
43
|
+
)
|
|
44
|
+
except httpx.RequestError:
|
|
45
|
+
print("[bold red]Connection error: Could not reach the server.")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
if response.status_code in handle_codes:
|
|
48
|
+
return response
|
|
49
|
+
elif response.status_code == 401:
|
|
50
|
+
print("[bold red]Authentication failed: Invalid or revoked token.[/bold red]")
|
|
51
|
+
try:
|
|
52
|
+
error = response.json().get("error")
|
|
53
|
+
if error: print(f"[dim]Error reason: {error}[/dim]")
|
|
54
|
+
except JSONDecodeError: pass
|
|
55
|
+
config.cliconf_save(token="")
|
|
56
|
+
print("[yellow]Your session has expired. Please run 'glimpse login' again [/yellow].")
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
elif response.status_code == 403:
|
|
59
|
+
print("[bold red]Authentication failed: Invalid or revoked token.[/bold red]")
|
|
60
|
+
try:
|
|
61
|
+
error = response.json().get("error")
|
|
62
|
+
if error: print(f"[dim]Error reason: {error}[/dim]")
|
|
63
|
+
except JSONDecodeError: pass
|
|
64
|
+
raise typer.Exit(1)
|
|
65
|
+
else:
|
|
66
|
+
print(f"[bold red]Unexpected error ocurred.\n[dim](status code: {response.status_code})[/dim][/bold red]")
|
|
67
|
+
try:
|
|
68
|
+
error = response.json().get("error")
|
|
69
|
+
if error: print(f"[dim red]Error reason: {error}[/dim red]")
|
|
70
|
+
except JSONDecodeError: pass
|
|
71
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
CLI_CONFIG_DIR = Path.home() / ".config" / "glimpsecli"
|
|
7
|
+
CLI_CONFIG_FILE = CLI_CONFIG_DIR / "config.json"
|
|
8
|
+
CONFIG_FILENAME = ".shared-repo.json"
|
|
9
|
+
DEFAULT_API_URL = "https://gitglimpse.sutac.pl"
|
|
10
|
+
|
|
11
|
+
# --- repository config
|
|
12
|
+
class RepoConfig(TypedDict):
|
|
13
|
+
repo_id: str
|
|
14
|
+
|
|
15
|
+
def repoconf_save(repo_id: str, path: Path = Path(".")) -> None:
|
|
16
|
+
"""Saves repository config to file.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
repo_id: Repository GitGlimpse id.
|
|
20
|
+
path: Path to directory holding config file.
|
|
21
|
+
"""
|
|
22
|
+
config_path = path / CONFIG_FILENAME
|
|
23
|
+
conf: RepoConfig = {"repo_id": repo_id}
|
|
24
|
+
with open(config_path, "w") as f:
|
|
25
|
+
json.dump(conf, f, indent=2)
|
|
26
|
+
|
|
27
|
+
def repoconf_load(path: Path = Path(".")) -> RepoConfig | None:
|
|
28
|
+
"""Loads repository config from file.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
path: Path to directory holding config file.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
RepoConfig if avaliable, else None.
|
|
35
|
+
"""
|
|
36
|
+
config_path = path / CONFIG_FILENAME
|
|
37
|
+
if not config_path.exists():
|
|
38
|
+
return None
|
|
39
|
+
try:
|
|
40
|
+
with open(config_path, "r") as f:
|
|
41
|
+
return json.load(f)
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def repoconf_remove(path: Path = Path(".")) -> None:
|
|
46
|
+
"""Removes repository config file if exists.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
path: Path to directory holding config file.
|
|
50
|
+
"""
|
|
51
|
+
config_path = path / CONFIG_FILENAME
|
|
52
|
+
config_path.unlink(missing_ok=True)
|
|
53
|
+
|
|
54
|
+
# --- cli config
|
|
55
|
+
class CliConfig(TypedDict, total=False):
|
|
56
|
+
token: str
|
|
57
|
+
api_url: str
|
|
58
|
+
debug: bool
|
|
59
|
+
|
|
60
|
+
def cliconf_save(
|
|
61
|
+
token: str | None = None,
|
|
62
|
+
api_url: str | None = None,
|
|
63
|
+
debug: bool | None = None
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Saves or updates cli config safely.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
token: GitGlimpse api token.
|
|
69
|
+
"""
|
|
70
|
+
CLI_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
conf = cliconf_load() or {}
|
|
72
|
+
if token is not None:
|
|
73
|
+
if token: conf["token"] = token
|
|
74
|
+
elif "token" in conf: conf.pop("token")
|
|
75
|
+
if api_url is not None:
|
|
76
|
+
if api_url: conf["api_url"] = api_url.rstrip("/")
|
|
77
|
+
elif "api_url" in conf: conf.pop("api_url")
|
|
78
|
+
if debug is not None:
|
|
79
|
+
conf["debug"] = debug
|
|
80
|
+
with open(CLI_CONFIG_FILE, "w") as f:
|
|
81
|
+
json.dump(conf, f, indent=2)
|
|
82
|
+
|
|
83
|
+
def cliconf_load() -> CliConfig | None:
|
|
84
|
+
"""Loads cli configfrom file.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
CliConfig if avaliable, else None.
|
|
88
|
+
"""
|
|
89
|
+
if not CLI_CONFIG_FILE.exists():
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
with open(CLI_CONFIG_FILE, "r") as f:
|
|
93
|
+
return json.load(f)
|
|
94
|
+
except Exception:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def cliconf_remove() -> None:
|
|
98
|
+
"""Removes cli config file."""
|
|
99
|
+
CLI_CONFIG_FILE.unlink(missing_ok=True)
|
|
100
|
+
|
|
101
|
+
def get_api_url() -> str:
|
|
102
|
+
"""Resolves active api url
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Active api url.
|
|
106
|
+
"""
|
|
107
|
+
if conf := cliconf_load():
|
|
108
|
+
if user_url := conf.get("api_url"):
|
|
109
|
+
return user_url.rstrip("/")
|
|
110
|
+
return DEFAULT_API_URL
|
|
111
|
+
|
|
112
|
+
def get_debug_mode() -> bool:
|
|
113
|
+
"""Returns True if debug is enabled in user config, else False."""
|
|
114
|
+
if conf := cliconf_load():
|
|
115
|
+
return conf.get("debug", False)
|
|
116
|
+
return False
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from glimpsecli import utils, config, api
|
|
2
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from rich import print
|
|
5
|
+
import typer
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_or_prompt_url(url: str | None = None, detect: bool = True) -> str:
|
|
10
|
+
"""Helper to get GitHub repository url.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
url: GitHub repository url.
|
|
14
|
+
detect: Flag for automatic detection.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
GitHub repository url.
|
|
18
|
+
"""
|
|
19
|
+
if detect and not url:
|
|
20
|
+
if not Path(".git").exists():
|
|
21
|
+
print("[red]Error: Current directory is not a Git repository.[/red]")
|
|
22
|
+
raise typer.Exit(1)
|
|
23
|
+
url = utils.get_git_remote_url()
|
|
24
|
+
if not url:
|
|
25
|
+
print("[dim]Could not detect GitHub url.[/dim]")
|
|
26
|
+
if not url:
|
|
27
|
+
while True:
|
|
28
|
+
url = typer.prompt("Enter GitHub repository URL (e.g., https://github.com/user/repo.git)")
|
|
29
|
+
if url: url = url.strip()
|
|
30
|
+
if url and utils.is_valid_repo_url(url): break
|
|
31
|
+
print("[bold red]Invalid url, try again...[/bold red]")
|
|
32
|
+
return url
|
|
33
|
+
|
|
34
|
+
def finalize_init(repo_id: str) -> None:
|
|
35
|
+
"""Helper to save config and update gitignore.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
repo_id: Repository GitGlimpse id.
|
|
39
|
+
"""
|
|
40
|
+
# Add repo id to local file
|
|
41
|
+
config.repoconf_save(repo_id)
|
|
42
|
+
print("[dim]Created local .shared-repo.json configuration file for this repository.[/dim]")
|
|
43
|
+
# Auto-add to .gitignore
|
|
44
|
+
gitignore = Path(".gitignore")
|
|
45
|
+
ignore_entry = "\n.shared-repo.json\n"
|
|
46
|
+
if gitignore.exists():
|
|
47
|
+
if ".shared-repo.json" not in gitignore.read_text():
|
|
48
|
+
with open(gitignore, "a") as f:
|
|
49
|
+
f.write(ignore_entry)
|
|
50
|
+
print("[dim]Added .shared-repo.json to .gitignore[/dim]")
|
|
51
|
+
else:
|
|
52
|
+
gitignore.write_text(ignore_entry)
|
|
53
|
+
print("[dim]Created .gitignore and added .shared-repo.json[/dim]")
|
|
54
|
+
|
|
55
|
+
def poll_build_status(token: str, repo_id: str) -> None:
|
|
56
|
+
"""Helper to poll build status from server.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
token: GitGlimpse api token.
|
|
60
|
+
repo_id: Repository GitGlimpse id.
|
|
61
|
+
"""
|
|
62
|
+
print(f"[dim]Repository build was queued.[/dim]")
|
|
63
|
+
try:
|
|
64
|
+
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as progress:
|
|
65
|
+
task_id = progress.add_task(description="Connecting to server...", total=None)
|
|
66
|
+
last_status = None
|
|
67
|
+
while True:
|
|
68
|
+
poll_response = api.request_api(f"/repos/build/{repo_id}/status", token=token, quiet=True)
|
|
69
|
+
status = poll_response.json().get("status")
|
|
70
|
+
if status != last_status:
|
|
71
|
+
progress.update(task_id, description=f"Build status: {utils.enrich_status(status)}")
|
|
72
|
+
last_status = status
|
|
73
|
+
if status in ["success", "failed", "violation"]: break
|
|
74
|
+
time.sleep(2)
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
print("\n[yellow]Polling interrupted. The build is still running on the server.[/yellow]")
|
|
77
|
+
print(f"Check status later at: {config.get_api_url()}/repos/details/{repo_id}")
|
|
78
|
+
raise typer.Exit(0)
|
|
79
|
+
print(f"Build has finished with status: {utils.enrich_status(status)}")
|
|
80
|
+
print(f"View details at: {config.get_api_url()}/repos/details/{repo_id}")
|
|
81
|
+
|
|
82
|
+
def get_repo_config() -> config.RepoConfig:
|
|
83
|
+
"""Helper for getting repository config."""
|
|
84
|
+
conf = config.repoconf_load()
|
|
85
|
+
if not conf:
|
|
86
|
+
print("[yellow]Not initialised. Run 'glimpse init' first.[/yellow]")
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
return conf
|
|
89
|
+
|
|
90
|
+
def get_token() -> str:
|
|
91
|
+
"""Helper for getting api token."""
|
|
92
|
+
conf = config.cliconf_load() or {}
|
|
93
|
+
token = conf.get("token")
|
|
94
|
+
if not conf or not token:
|
|
95
|
+
print("[yellow]Not logged in. Run 'glimpse login' first.[/yellow]")
|
|
96
|
+
raise typer.Exit(code=1)
|
|
97
|
+
return token
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from glimpsecli import config, utils, api, helpers
|
|
2
|
+
from json import JSONDecodeError
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich import print
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
import typer
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
_DEBUG_MODE = True
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="CLI tool to manage and update shared GitHub repos with GitGlimpse.", no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
# --- main
|
|
14
|
+
def version_callback(value: bool) -> None:
|
|
15
|
+
"""Display version of package."""
|
|
16
|
+
if not value: return
|
|
17
|
+
version = importlib.metadata.metadata("git-glimpse-cli").get("version")
|
|
18
|
+
print(f"GitGlimpse CLI version: {version or "?"}")
|
|
19
|
+
raise typer.Exit(0)
|
|
20
|
+
|
|
21
|
+
@app.callback()
|
|
22
|
+
def main(
|
|
23
|
+
version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True, help="Display version of package.")
|
|
24
|
+
):
|
|
25
|
+
"""GitGlimpse CLI."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
# --- login
|
|
29
|
+
@app.command()
|
|
30
|
+
def login(
|
|
31
|
+
token: str = typer.Option(
|
|
32
|
+
...,
|
|
33
|
+
prompt="Enter your CLI token",
|
|
34
|
+
hide_input=True,
|
|
35
|
+
help="Your personal access cli token from dashboard."),
|
|
36
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing session without prompting.")
|
|
37
|
+
):
|
|
38
|
+
"""Authenticate cli with server."""
|
|
39
|
+
conf = config.cliconf_load()
|
|
40
|
+
if not force and conf and conf.get("token"):
|
|
41
|
+
print("[yellow]You are already logged in.[/yellow]")
|
|
42
|
+
if not typer.confirm("Do you want to overwrite your existing session?"):
|
|
43
|
+
print("[dim]Login cancelled.[/dim]")
|
|
44
|
+
raise typer.Exit(0)
|
|
45
|
+
response = api.request_api("/cli/user", token=token)
|
|
46
|
+
config.cliconf_save(token)
|
|
47
|
+
username = response.json().get("username")
|
|
48
|
+
print(f"[bold green]Success![/bold green] Authenticated as [cyan]{username}[/cyan].")
|
|
49
|
+
|
|
50
|
+
# --- whoami
|
|
51
|
+
@app.command()
|
|
52
|
+
def whoami():
|
|
53
|
+
"""Check the currently logged-in user."""
|
|
54
|
+
response = api.request_api("/cli/user")
|
|
55
|
+
username = response.json().get("username")
|
|
56
|
+
print(f"Authenticated as [cyan]{username}[/cyan].")
|
|
57
|
+
|
|
58
|
+
# --- logout
|
|
59
|
+
@app.command()
|
|
60
|
+
def logout():
|
|
61
|
+
"""Clear stored local credentials."""
|
|
62
|
+
conf = config.cliconf_load()
|
|
63
|
+
if not conf or not conf.get("token"):
|
|
64
|
+
print("[yellow]You are not currently logged in.[/yellow]")
|
|
65
|
+
raise typer.Exit(0)
|
|
66
|
+
config.cliconf_save(token="")
|
|
67
|
+
print("[bold green]Logged out successfully.[/bold green]\nStored credentials removed.")
|
|
68
|
+
|
|
69
|
+
# --- init
|
|
70
|
+
init_app = typer.Typer(help="Link or upload the current Git repository to GitGlimpse server.", no_args_is_help=True)
|
|
71
|
+
app.add_typer(init_app, name="init")
|
|
72
|
+
|
|
73
|
+
@init_app.command("link")
|
|
74
|
+
def init_link(
|
|
75
|
+
url: str | None = typer.Option(None, help="GitHub repository URL."),
|
|
76
|
+
detect: bool = typer.Option(True, help="Do you want to detect repository url automatically.")
|
|
77
|
+
):
|
|
78
|
+
"""Link current Git repository to existing one on GitGlimpse."""
|
|
79
|
+
token = helpers.get_token()
|
|
80
|
+
if config.repoconf_load():
|
|
81
|
+
print("Shared repository already initialised.")
|
|
82
|
+
raise typer.Exit(0)
|
|
83
|
+
url = helpers.get_or_prompt_url(url, detect)
|
|
84
|
+
print("[dim]Linking to existing GitGlimpse repository.[/dim]")
|
|
85
|
+
response = api.request_api("/cli/repos/fetch", method="POST", payload={"url": url}, token=token, handle_codes=[200, 404])
|
|
86
|
+
if response.status_code == 404:
|
|
87
|
+
print(f"[bold red]Could not find this repository ([cyan]'{url}'[/cyan]) on GitGlimpse server, provide a valid url.[/bold red]")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
# response.status_code == 200
|
|
90
|
+
repo_id = response.json().get('repo_id')
|
|
91
|
+
print("[green]Repo was found on GitGlimpse server.[/green]")
|
|
92
|
+
helpers.finalize_init(repo_id)
|
|
93
|
+
print(f"Repository details: {config.get_api_url()}/repos/details/{repo_id}")
|
|
94
|
+
|
|
95
|
+
@init_app.command("new")
|
|
96
|
+
def init_new(
|
|
97
|
+
url: str | None = typer.Option(None, help="GitHub repository URL."),
|
|
98
|
+
is_private: bool | None = typer.Option(None, "--private/--public", help="Is repository public or private?"),
|
|
99
|
+
force: bool = typer.Option(False, "--force", "-f", help="Upload without prompting about GitHub publish."),
|
|
100
|
+
detect: bool = typer.Option(True, help="Do you want to detect repository url automatically.")
|
|
101
|
+
):
|
|
102
|
+
"""Upload and register current Git repository to GitGlimpse."""
|
|
103
|
+
token = helpers.get_token()
|
|
104
|
+
if config.repoconf_load():
|
|
105
|
+
print("Shared repository already initialised.")
|
|
106
|
+
raise typer.Exit(0)
|
|
107
|
+
url = helpers.get_or_prompt_url(url, detect)
|
|
108
|
+
print("[dim]Adding repository to GitGlimpse.[/dim]")
|
|
109
|
+
if not force:
|
|
110
|
+
print("[bold]This repository have to be uploaded to GitHub under given url first.[/bold]")
|
|
111
|
+
typer.confirm("Continue?", abort=True)
|
|
112
|
+
# Private/Public
|
|
113
|
+
if is_private is None:
|
|
114
|
+
is_private = typer.confirm("Are you uploading a private repository? (This will require SSH key generation)")
|
|
115
|
+
if is_private: # Private
|
|
116
|
+
print("[dim]Generating local SSH keys at '.git/shared_repo_keys/'.[/dim]")
|
|
117
|
+
try:
|
|
118
|
+
private_key, public_key = utils.generate_local_ssh_key()
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"[red]Failed to generate local SSH key: {e}[/red]")
|
|
121
|
+
raise typer.Exit(code=1)
|
|
122
|
+
print("\n[bold yellow]Action Required:[/bold yellow] Add this deploy key to your GitHub repository settings ([dim]Settings > Deploy keys > Add deploy key[/dim]):")
|
|
123
|
+
print(Panel(public_key, title="Public Deploy Key", border_style="yellow"))
|
|
124
|
+
typer.confirm("Continue?", abort=True)
|
|
125
|
+
if url.startswith("https://"):
|
|
126
|
+
url = utils.create_alt_repo_url(url)
|
|
127
|
+
else: # Public
|
|
128
|
+
if not url.startswith("https://"):
|
|
129
|
+
url = utils.create_alt_repo_url(url)
|
|
130
|
+
private_key = None
|
|
131
|
+
# Upload
|
|
132
|
+
response = api.request_api(
|
|
133
|
+
"/cli/repos/add",
|
|
134
|
+
method="POST",
|
|
135
|
+
payload={"url": url, "ssh_key": private_key},
|
|
136
|
+
token=token,
|
|
137
|
+
handle_codes=[202, 409, 420]
|
|
138
|
+
)
|
|
139
|
+
if response.status_code == 409:
|
|
140
|
+
print(f"[yellow]This repository is already registered on the server.[/yellow]")
|
|
141
|
+
raise typer.Exit(1)
|
|
142
|
+
elif response.status_code == 420:
|
|
143
|
+
print(f"[yellow]You have reached your usage limits.[/yellow]")
|
|
144
|
+
try:
|
|
145
|
+
error = response.json().get("error")
|
|
146
|
+
if error: print(f"[dim]Error reason: {error}[/dim]")
|
|
147
|
+
except JSONDecodeError: pass
|
|
148
|
+
raise typer.Exit(1)
|
|
149
|
+
# response.status_code == 202
|
|
150
|
+
data = response.json()
|
|
151
|
+
repo_id = data.get('repo_id')
|
|
152
|
+
helpers.finalize_init(repo_id)
|
|
153
|
+
helpers.poll_build_status(token, repo_id)
|
|
154
|
+
|
|
155
|
+
# --- limits
|
|
156
|
+
@app.command()
|
|
157
|
+
def limits():
|
|
158
|
+
"""Display user build and repository limits."""
|
|
159
|
+
response = api.request_api("/cli/user/limits")
|
|
160
|
+
data = response.json()
|
|
161
|
+
repo_limit = data.get("repo_limit")
|
|
162
|
+
repo_count = data.get("repo_count")
|
|
163
|
+
build_limit = data.get("build_limit")
|
|
164
|
+
build_count = data.get("build_count")
|
|
165
|
+
print("[bold]User limits:[/bold]")
|
|
166
|
+
repo_str = f"[red]{repo_count}[/red]/[red]{repo_limit}[/red]" if repo_count == repo_limit else f"{repo_count}/{repo_limit}"
|
|
167
|
+
print(f"Repositories: {repo_str}")
|
|
168
|
+
build_str = f"[red]{build_count}[/red]/[red]{build_limit}[/red]" if build_count == build_limit else f"{build_count}/{build_limit}"
|
|
169
|
+
print(f"Builds: {build_str}")
|
|
170
|
+
|
|
171
|
+
# --- status
|
|
172
|
+
@app.command()
|
|
173
|
+
def status():
|
|
174
|
+
"""Display status info of current repository."""
|
|
175
|
+
token = helpers.get_token()
|
|
176
|
+
conf = helpers.get_repo_config()
|
|
177
|
+
response = api.request_api(f"/cli/repos/build/{conf['repo_id']}/status", token=token)
|
|
178
|
+
data = response.json()
|
|
179
|
+
build_status = data.get("status", "")
|
|
180
|
+
print(f"Repository id: [dark_cyan]{conf['repo_id']}[/dark_cyan]")
|
|
181
|
+
print(f"Build status: {utils.enrich_status(build_status)}")
|
|
182
|
+
print(f"Repository details: {config.get_api_url()}/repos/details/{conf['repo_id']}")
|
|
183
|
+
|
|
184
|
+
# --- remove
|
|
185
|
+
@app.command()
|
|
186
|
+
def remove(
|
|
187
|
+
force: bool = typer.Option(False, "--force", "-f", help="Remove without prompting.")
|
|
188
|
+
):
|
|
189
|
+
"""Remove current repository from GitGlimpse."""
|
|
190
|
+
token = helpers.get_token()
|
|
191
|
+
conf = helpers.get_repo_config()
|
|
192
|
+
print(f"[dim]Current repository id: '{conf['repo_id']}'[/dim]")
|
|
193
|
+
if not force: typer.confirm(f"Are you sure, you want to remove this repository from GitGlimpse?", abort=True)
|
|
194
|
+
api.request_api(f"/cli/repos/remove/{conf['repo_id']}", method="POST", token=token, handle_codes=[204])
|
|
195
|
+
config.repoconf_remove()
|
|
196
|
+
print("Removed repository from GitGlimpse.")
|
|
197
|
+
|
|
198
|
+
# --- build
|
|
199
|
+
@app.command()
|
|
200
|
+
def build(
|
|
201
|
+
force: bool = typer.Option(False, "--force", "-f", help="Schedule without prompting.")
|
|
202
|
+
):
|
|
203
|
+
"""Schedule a build for current repository."""
|
|
204
|
+
token = helpers.get_token()
|
|
205
|
+
conf = helpers.get_repo_config()
|
|
206
|
+
if not force:
|
|
207
|
+
print("Ensure that you pushed recent changes to GitHub.")
|
|
208
|
+
typer.confirm("Continue?", abort=True)
|
|
209
|
+
print(f"[dim]Scheduling a build for current repository (id: '{conf['repo_id']}')[/dim]")
|
|
210
|
+
response = api.request_api(f"/cli/repos/build/{conf['repo_id']}", method="POST", token=token, handle_codes=[202, 420, 425])
|
|
211
|
+
if response.status_code == 420:
|
|
212
|
+
print(f"[yellow]You have reached your usage limits.[/yellow]")
|
|
213
|
+
try:
|
|
214
|
+
error = response.json().get("error")
|
|
215
|
+
if error: print(f"[dim]Error reason: {error}[/dim]")
|
|
216
|
+
except JSONDecodeError: pass
|
|
217
|
+
raise typer.Exit(1)
|
|
218
|
+
elif response.status_code == 425:
|
|
219
|
+
print(f"[yellow]This repository already has a pending build.[/yellow]")
|
|
220
|
+
raise typer.Exit(1)
|
|
221
|
+
# response.status_code == 202
|
|
222
|
+
helpers.poll_build_status(token, conf['repo_id'])
|
|
223
|
+
|
|
224
|
+
# --- config
|
|
225
|
+
config_app = typer.Typer(help="Configure your cli.", no_args_is_help=True)
|
|
226
|
+
app.add_typer(config_app, name="config")
|
|
227
|
+
|
|
228
|
+
@config_app.command("set-url")
|
|
229
|
+
def config_set_url(
|
|
230
|
+
url: str = typer.Argument(..., help="Api base url (e.g., http://localhost:8000 or https://gitglimpse.sutac.pl)")
|
|
231
|
+
):
|
|
232
|
+
"""Set the active api server url."""
|
|
233
|
+
config.cliconf_save(api_url=url)
|
|
234
|
+
if url:
|
|
235
|
+
print(f"Api url updated to: {url}")
|
|
236
|
+
else:
|
|
237
|
+
print("Api url unset.")
|
|
238
|
+
|
|
239
|
+
@config_app.command("get-url")
|
|
240
|
+
def config_get_url():
|
|
241
|
+
"""Get the active api server url."""
|
|
242
|
+
conf = config.cliconf_load() or {}
|
|
243
|
+
url = conf.get("api_url")
|
|
244
|
+
if url: print(url)
|
|
245
|
+
|
|
246
|
+
@config_app.command("debug")
|
|
247
|
+
def config_debug(
|
|
248
|
+
enabled: bool = typer.Argument(..., help="True to enable debug mode, False to disable.")
|
|
249
|
+
):
|
|
250
|
+
"""Enable or disable verbose debug output and full crash stack traces."""
|
|
251
|
+
config.cliconf_save(debug=enabled)
|
|
252
|
+
status = "[bold green]enabled[/bold green]" if enabled else "[bold yellow]disabled[/bold yellow]"
|
|
253
|
+
print(f"Debug mode {status}.")
|
|
254
|
+
|
|
255
|
+
# ---
|
|
256
|
+
def cli():
|
|
257
|
+
try:
|
|
258
|
+
app()
|
|
259
|
+
except Exception as exc:
|
|
260
|
+
if isinstance(exc, typer.Exit):
|
|
261
|
+
raise exc
|
|
262
|
+
if config.get_debug_mode():
|
|
263
|
+
print("[bold yellow][DEBUG MODE ACTIVE] Full stack trace below:[/bold yellow]\n")
|
|
264
|
+
raise exc
|
|
265
|
+
print(f"[bold red]Fatal Error:[/bold red] {str(exc)}")
|
|
266
|
+
print("[dim]Enable debug with 'glimpse config debug' for full diagnostic details.[/dim]")
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
cli()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import subprocess
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
_GITHUB_URL_REGEX = re.compile(r'^(?:https:\/\/github\.com\/|git@github\.com:)[\w\-]+\/[\w\-]+(?:\.git)?$')
|
|
6
|
+
_GITHUB_URL_OWNER_REPO = r"(?:https?://github\.com/|git@github\.com:)([\w-]+)/([\w-]+?)(?:\.git)?$"
|
|
7
|
+
|
|
8
|
+
def _parse_github_owner_repo(url: str) -> tuple[str, str]:
|
|
9
|
+
"""Gets owner and repository name from GitHub url.
|
|
10
|
+
|
|
11
|
+
[!] Requires valid GitHub url.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
url: GitHub repository url.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Tuple of (owner, repository).
|
|
18
|
+
"""
|
|
19
|
+
match = re.match(_GITHUB_URL_OWNER_REPO, url.strip())
|
|
20
|
+
assert match
|
|
21
|
+
owner, repo = match.groups()
|
|
22
|
+
return owner, repo
|
|
23
|
+
|
|
24
|
+
# --- exported
|
|
25
|
+
def get_git_remote_url() -> str | None:
|
|
26
|
+
"""Get git remote repository url from git.
|
|
27
|
+
|
|
28
|
+
[!] Uses `git` command.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Git remote repository url if was able to fetch it, else `None`.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
["git", "remote", "get-url", "origin"],
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
check=True,
|
|
39
|
+
)
|
|
40
|
+
return result.stdout.strip()
|
|
41
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def is_valid_repo_url(url: str) -> bool:
|
|
45
|
+
"""Validate repository URL.
|
|
46
|
+
|
|
47
|
+
Valid URL will follow this pattern:
|
|
48
|
+
- `https://github.com/user/repo.git`
|
|
49
|
+
- `git@github.com:user/repo.git`.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
url: URL to validate.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if URL is valid.
|
|
56
|
+
|
|
57
|
+
Notes:
|
|
58
|
+
This function checks only format. It does not verify
|
|
59
|
+
URL resource existence.
|
|
60
|
+
"""
|
|
61
|
+
return bool(_GITHUB_URL_REGEX.match(url))
|
|
62
|
+
|
|
63
|
+
def create_alt_repo_url(url: str):
|
|
64
|
+
"""Creates alternative GitHub repository url.
|
|
65
|
+
|
|
66
|
+
For https urls returns ssh url and the oposite way.
|
|
67
|
+
[!] Requires valid Github url.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
url: Github repository url.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Alternative GitHub repository url.
|
|
74
|
+
"""
|
|
75
|
+
assert is_valid_repo_url(url)
|
|
76
|
+
owner, repo = _parse_github_owner_repo(url)
|
|
77
|
+
if url.startswith("https://"):
|
|
78
|
+
return f"git@github.com:{owner}/{repo}.git"
|
|
79
|
+
else:
|
|
80
|
+
return f"https://github.com/{owner}/{repo}.git"
|
|
81
|
+
|
|
82
|
+
def generate_local_ssh_key() -> tuple[str, str]:
|
|
83
|
+
"""Generates a temporary project-specific SSH key pair locally if it doesn't exist.
|
|
84
|
+
|
|
85
|
+
Keys are generated in directory `.git/shared_repo_keys/` using rsa algorithm.
|
|
86
|
+
[!] Uses `ssh-keygen` command.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Tuple of (private_key, public_key).
|
|
90
|
+
"""
|
|
91
|
+
key_dir = Path(".git") / "shared_repo_keys"
|
|
92
|
+
key_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
private_key_path = key_dir / "id_rsa"
|
|
94
|
+
public_key_path = key_dir / "id_rsa.pub"
|
|
95
|
+
if not private_key_path.exists():
|
|
96
|
+
subprocess.run(
|
|
97
|
+
[
|
|
98
|
+
"ssh-keygen",
|
|
99
|
+
"-t",
|
|
100
|
+
"rsa",
|
|
101
|
+
"-N",
|
|
102
|
+
"",
|
|
103
|
+
"-f",
|
|
104
|
+
str(private_key_path),
|
|
105
|
+
"-C",
|
|
106
|
+
"git-glimpse-cli",
|
|
107
|
+
],
|
|
108
|
+
check=True,
|
|
109
|
+
capture_output=True,
|
|
110
|
+
)
|
|
111
|
+
private_key = private_key_path.read_text()
|
|
112
|
+
public_key = public_key_path.read_text().strip()
|
|
113
|
+
return private_key, public_key
|
|
114
|
+
|
|
115
|
+
def enrich_status(status: str) -> str:
|
|
116
|
+
"""Adds color coding for build status codes.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
status: Build status code.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Color coded build status code.
|
|
123
|
+
"""
|
|
124
|
+
match status:
|
|
125
|
+
case "pending": return "[bold blue]pending[/bold blue]"
|
|
126
|
+
case "success": return "[bold green]success[/bold green]"
|
|
127
|
+
case "failed": return "[bold red]failed[/bold red]"
|
|
128
|
+
case "violation": return "[bold magenta]violation[/bold magenta]"
|
|
129
|
+
case "running": return "[/bold cyan]running[/bold cyan]"
|
|
130
|
+
case _: return "[bold dim]?[/bold dim]"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "git-glimpse-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI tool to manage and update shared GitHub repos with GitGlimpse."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.28.1",
|
|
13
|
+
"rich>=15.0.0",
|
|
14
|
+
"typer>=0.27.2"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
glimpse = "glimpsecli.main:cli"
|