qodev-gitlab-cli 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.
- qodev_gitlab_cli/__init__.py +1 -0
- qodev_gitlab_cli/__main__.py +5 -0
- qodev_gitlab_cli/app.py +93 -0
- qodev_gitlab_cli/commands/__init__.py +1 -0
- qodev_gitlab_cli/commands/issues.py +102 -0
- qodev_gitlab_cli/commands/jobs.py +52 -0
- qodev_gitlab_cli/commands/mrs.py +202 -0
- qodev_gitlab_cli/commands/pipelines.py +63 -0
- qodev_gitlab_cli/commands/projects.py +35 -0
- qodev_gitlab_cli/commands/releases.py +48 -0
- qodev_gitlab_cli/commands/variables.py +61 -0
- qodev_gitlab_cli/context.py +62 -0
- qodev_gitlab_cli/formatters/__init__.py +1 -0
- qodev_gitlab_cli/formatters/generic.py +94 -0
- qodev_gitlab_cli/formatters/issues.py +60 -0
- qodev_gitlab_cli/formatters/jobs.py +39 -0
- qodev_gitlab_cli/formatters/mrs.py +101 -0
- qodev_gitlab_cli/formatters/pipelines.py +67 -0
- qodev_gitlab_cli/formatters/projects.py +39 -0
- qodev_gitlab_cli/formatters/releases.py +33 -0
- qodev_gitlab_cli/formatters/variables.py +34 -0
- qodev_gitlab_cli/output.py +116 -0
- qodev_gitlab_cli/project.py +102 -0
- qodev_gitlab_cli-0.1.0.dist-info/METADATA +14 -0
- qodev_gitlab_cli-0.1.0.dist-info/RECORD +27 -0
- qodev_gitlab_cli-0.1.0.dist-info/WHEEL +4 -0
- qodev_gitlab_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""GitLab CLI — agent-friendly CLI for the GitLab API."""
|
qodev_gitlab_cli/app.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Root App definition, global options, and error handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
from cyclopts import App, Group, Parameter
|
|
9
|
+
from qodev_gitlab_api import APIError, AuthenticationError, ConfigurationError, NotFoundError
|
|
10
|
+
|
|
11
|
+
import qodev_gitlab_cli.context as _ctx
|
|
12
|
+
|
|
13
|
+
app = App(
|
|
14
|
+
name="qodev-gitlab",
|
|
15
|
+
help="Agent-friendly CLI for the GitLab API.",
|
|
16
|
+
version_flags=[],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
app.meta.group_parameters = Group("Global Options", sort_key=0)
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Import and register command groups
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
from qodev_gitlab_cli.commands.issues import issues_app # noqa: E402
|
|
25
|
+
from qodev_gitlab_cli.commands.jobs import jobs_app # noqa: E402
|
|
26
|
+
from qodev_gitlab_cli.commands.mrs import mrs_app # noqa: E402
|
|
27
|
+
from qodev_gitlab_cli.commands.pipelines import pipelines_app # noqa: E402
|
|
28
|
+
from qodev_gitlab_cli.commands.projects import projects_app # noqa: E402
|
|
29
|
+
from qodev_gitlab_cli.commands.releases import releases_app # noqa: E402
|
|
30
|
+
from qodev_gitlab_cli.commands.variables import variables_app # noqa: E402
|
|
31
|
+
|
|
32
|
+
app.command(projects_app)
|
|
33
|
+
app.command(mrs_app)
|
|
34
|
+
app.command(pipelines_app)
|
|
35
|
+
app.command(jobs_app)
|
|
36
|
+
app.command(issues_app)
|
|
37
|
+
app.command(releases_app)
|
|
38
|
+
app.command(variables_app)
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Exit codes
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
EXIT_AUTH = 80
|
|
44
|
+
EXIT_NOT_FOUND = 81
|
|
45
|
+
EXIT_API = 82
|
|
46
|
+
EXIT_VALIDATION = 83
|
|
47
|
+
EXIT_CONFIG = 84
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Meta launcher — global options & error handling
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
@app.meta.default
|
|
54
|
+
def launcher(
|
|
55
|
+
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
|
|
56
|
+
json: Annotated[bool, Parameter(name="--json", help="Output as JSON", negative="")] = False,
|
|
57
|
+
token: Annotated[
|
|
58
|
+
str | None, Parameter(name="--token", help="GitLab token (overrides GITLAB_TOKEN)", show=False)
|
|
59
|
+
] = None,
|
|
60
|
+
url: Annotated[str | None, Parameter(name="--url", help="GitLab URL (overrides GITLAB_URL)", show=False)] = None,
|
|
61
|
+
project: Annotated[str | None, Parameter(name=["--project", "-p"], help="Project ID or path")] = None,
|
|
62
|
+
limit: Annotated[int, Parameter(name="--limit", help="Results per page")] = 25,
|
|
63
|
+
page: Annotated[int, Parameter(name="--page", help="Page number")] = 1,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""GitLab CLI — manage projects, merge requests, pipelines, and more."""
|
|
66
|
+
_ctx.ctx.configure(json_mode=json, token=token, base_url=url, project=project, limit=limit, page=page)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
app(tokens)
|
|
70
|
+
except AuthenticationError as exc:
|
|
71
|
+
_handle_error(str(exc), code="authentication", exit_code=EXIT_AUTH)
|
|
72
|
+
except NotFoundError as exc:
|
|
73
|
+
_handle_error(str(exc), code="not_found", exit_code=EXIT_NOT_FOUND)
|
|
74
|
+
except APIError as exc:
|
|
75
|
+
_handle_error(str(exc), code="api_error", exit_code=EXIT_API)
|
|
76
|
+
except ConfigurationError as exc:
|
|
77
|
+
_handle_error(str(exc), code="configuration", exit_code=EXIT_CONFIG)
|
|
78
|
+
except SystemExit:
|
|
79
|
+
raise
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
sys.exit(130)
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
_handle_error(f"Unexpected error: {exc}", code="unknown", exit_code=1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _handle_error(message: str, *, code: str, exit_code: int) -> None:
|
|
87
|
+
from qodev_gitlab_cli.output import error
|
|
88
|
+
|
|
89
|
+
error(message, ctx=_ctx.ctx, code=code, exit_code=exit_code)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> None:
|
|
93
|
+
app.meta()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command groups for the GitLab CLI."""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Issue commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from qodev_gitlab_cli.context import ctx
|
|
10
|
+
from qodev_gitlab_cli.formatters.issues import format_issue_detail, format_issue_list, format_note_list
|
|
11
|
+
from qodev_gitlab_cli.output import output, output_list
|
|
12
|
+
|
|
13
|
+
issues_app = App(name="issues", help="Manage issues.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@issues_app.command
|
|
17
|
+
def list(
|
|
18
|
+
*,
|
|
19
|
+
state: Annotated[str, Parameter(name="--state", help="Filter: opened, closed, all")] = "opened",
|
|
20
|
+
labels: Annotated[str | None, Parameter(name="--labels", help="Filter by labels")] = None,
|
|
21
|
+
milestone: Annotated[str | None, Parameter(name="--milestone", help="Filter by milestone")] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""List issues."""
|
|
24
|
+
client = ctx.client()
|
|
25
|
+
project = ctx.resolve_project()
|
|
26
|
+
items = client.get_issues(project, state=state, labels=labels, milestone=milestone)
|
|
27
|
+
output_list(items=items, ctx=ctx, format_fn=format_issue_list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@issues_app.command
|
|
31
|
+
def get(
|
|
32
|
+
iid: Annotated[int, Parameter(help="Issue IID")],
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Get issue details."""
|
|
35
|
+
client = ctx.client()
|
|
36
|
+
project = ctx.resolve_project()
|
|
37
|
+
result = client.get_issue(project, iid)
|
|
38
|
+
output(result, ctx=ctx, format_fn=format_issue_detail)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@issues_app.command
|
|
42
|
+
def create(
|
|
43
|
+
*,
|
|
44
|
+
title: Annotated[str, Parameter(name="--title", help="Issue title")],
|
|
45
|
+
description: Annotated[str | None, Parameter(name="--description", help="Issue description")] = None,
|
|
46
|
+
labels: Annotated[str | None, Parameter(name="--labels", help="Comma-separated labels")] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Create a new issue."""
|
|
49
|
+
client = ctx.client()
|
|
50
|
+
project = ctx.resolve_project()
|
|
51
|
+
result = client.create_issue(project, title=title, description=description, labels=labels)
|
|
52
|
+
output(result, ctx=ctx, format_fn=format_issue_detail)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@issues_app.command
|
|
56
|
+
def update(
|
|
57
|
+
iid: Annotated[int, Parameter(help="Issue IID")],
|
|
58
|
+
*,
|
|
59
|
+
title: Annotated[str | None, Parameter(name="--title", help="New title")] = None,
|
|
60
|
+
description: Annotated[str | None, Parameter(name="--description", help="New description")] = None,
|
|
61
|
+
labels: Annotated[str | None, Parameter(name="--labels", help="New labels")] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Update an issue."""
|
|
64
|
+
client = ctx.client()
|
|
65
|
+
project = ctx.resolve_project()
|
|
66
|
+
result = client.update_issue(project, iid, title=title, description=description, labels=labels)
|
|
67
|
+
output(result, ctx=ctx, format_fn=format_issue_detail)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@issues_app.command
|
|
71
|
+
def close(
|
|
72
|
+
iid: Annotated[int, Parameter(help="Issue IID")],
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Close an issue."""
|
|
75
|
+
client = ctx.client()
|
|
76
|
+
project = ctx.resolve_project()
|
|
77
|
+
result = client.close_issue(project, iid)
|
|
78
|
+
output(result, ctx=ctx, format_fn=format_issue_detail)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@issues_app.command
|
|
82
|
+
def comment(
|
|
83
|
+
iid: Annotated[int, Parameter(help="Issue IID")],
|
|
84
|
+
*,
|
|
85
|
+
body: Annotated[str, Parameter(name="--body", help="Comment text")],
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Comment on an issue."""
|
|
88
|
+
client = ctx.client()
|
|
89
|
+
project = ctx.resolve_project()
|
|
90
|
+
result = client.create_issue_note(project, iid, body)
|
|
91
|
+
output(result, ctx=ctx)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@issues_app.command
|
|
95
|
+
def notes(
|
|
96
|
+
iid: Annotated[int, Parameter(help="Issue IID")],
|
|
97
|
+
) -> None:
|
|
98
|
+
"""List comments/notes on an issue."""
|
|
99
|
+
client = ctx.client()
|
|
100
|
+
project = ctx.resolve_project()
|
|
101
|
+
items = client.get_issue_notes(project, iid)
|
|
102
|
+
output_list(items=items, ctx=ctx, format_fn=format_note_list)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Job commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from qodev_gitlab_cli.context import ctx
|
|
10
|
+
from qodev_gitlab_cli.formatters.jobs import format_job_detail
|
|
11
|
+
from qodev_gitlab_cli.output import output, output_markdown
|
|
12
|
+
|
|
13
|
+
jobs_app = App(name="jobs", help="Manage jobs.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@jobs_app.command
|
|
17
|
+
def get(
|
|
18
|
+
id: Annotated[int, Parameter(help="Job ID")],
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Get job details."""
|
|
21
|
+
client = ctx.client()
|
|
22
|
+
project = ctx.resolve_project()
|
|
23
|
+
result = client.get_job(project, id)
|
|
24
|
+
output(result, ctx=ctx, format_fn=format_job_detail)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@jobs_app.command
|
|
28
|
+
def log(
|
|
29
|
+
id: Annotated[int, Parameter(help="Job ID")],
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Get job log output."""
|
|
32
|
+
client = ctx.client()
|
|
33
|
+
project = ctx.resolve_project()
|
|
34
|
+
log_text = client.get_job_log(project, id)
|
|
35
|
+
|
|
36
|
+
if ctx.json_mode:
|
|
37
|
+
from qodev_gitlab_cli.output import output_json
|
|
38
|
+
|
|
39
|
+
output_json({"job_id": id, "log": log_text})
|
|
40
|
+
else:
|
|
41
|
+
output_markdown(f"# Job #{id} Log\n\n```\n{log_text}\n```")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@jobs_app.command
|
|
45
|
+
def retry(
|
|
46
|
+
id: Annotated[int, Parameter(help="Job ID")],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Retry a failed job."""
|
|
49
|
+
client = ctx.client()
|
|
50
|
+
project = ctx.resolve_project()
|
|
51
|
+
result = client.retry_job(project, id)
|
|
52
|
+
output(result, ctx=ctx, format_fn=format_job_detail)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Merge request commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from qodev_gitlab_cli.context import ctx
|
|
10
|
+
from qodev_gitlab_cli.formatters.mrs import (
|
|
11
|
+
format_approval_detail,
|
|
12
|
+
format_commit_list,
|
|
13
|
+
format_discussion_list,
|
|
14
|
+
format_mr_detail,
|
|
15
|
+
format_mr_list,
|
|
16
|
+
)
|
|
17
|
+
from qodev_gitlab_cli.output import output, output_list, output_markdown
|
|
18
|
+
|
|
19
|
+
mrs_app = App(name="mrs", help="Manage merge requests.")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mrs_app.command
|
|
23
|
+
def list(
|
|
24
|
+
*,
|
|
25
|
+
state: Annotated[str, Parameter(name="--state", help="Filter by state: opened, closed, merged, all")] = "opened",
|
|
26
|
+
) -> None:
|
|
27
|
+
"""List merge requests."""
|
|
28
|
+
client = ctx.client()
|
|
29
|
+
project = ctx.resolve_project()
|
|
30
|
+
items = client.get_merge_requests(project, state=state)
|
|
31
|
+
output_list(items=items, ctx=ctx, format_fn=format_mr_list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mrs_app.command
|
|
35
|
+
def get(
|
|
36
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Get merge request details."""
|
|
39
|
+
client = ctx.client()
|
|
40
|
+
project = ctx.resolve_project()
|
|
41
|
+
result = client.get_merge_request(project, iid)
|
|
42
|
+
output(result, ctx=ctx, format_fn=format_mr_detail)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mrs_app.command
|
|
46
|
+
def create(
|
|
47
|
+
*,
|
|
48
|
+
title: Annotated[str, Parameter(name="--title", help="MR title")],
|
|
49
|
+
source: Annotated[str | None, Parameter(name="--source", help="Source branch (default: current)")] = None,
|
|
50
|
+
target: Annotated[str, Parameter(name="--target", help="Target branch")] = "main",
|
|
51
|
+
description: Annotated[str | None, Parameter(name="--description", help="MR description")] = None,
|
|
52
|
+
labels: Annotated[str | None, Parameter(name="--labels", help="Comma-separated labels")] = None,
|
|
53
|
+
squash: Annotated[bool | None, Parameter(name="--squash", help="Squash commits on merge")] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Create a new merge request."""
|
|
56
|
+
client = ctx.client()
|
|
57
|
+
project = ctx.resolve_project()
|
|
58
|
+
|
|
59
|
+
if source is None:
|
|
60
|
+
from qodev_gitlab_cli.project import get_current_branch
|
|
61
|
+
|
|
62
|
+
source = get_current_branch()
|
|
63
|
+
if not source:
|
|
64
|
+
from qodev_gitlab_cli.output import error
|
|
65
|
+
|
|
66
|
+
error("Could not detect current branch. Use --source.", ctx=ctx)
|
|
67
|
+
|
|
68
|
+
result = client.create_merge_request(
|
|
69
|
+
project,
|
|
70
|
+
source_branch=source, # type: ignore[arg-type]
|
|
71
|
+
target_branch=target,
|
|
72
|
+
title=title,
|
|
73
|
+
description=description,
|
|
74
|
+
labels=labels,
|
|
75
|
+
squash=squash,
|
|
76
|
+
)
|
|
77
|
+
output(result, ctx=ctx, format_fn=format_mr_detail)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@mrs_app.command
|
|
81
|
+
def update(
|
|
82
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
83
|
+
*,
|
|
84
|
+
title: Annotated[str | None, Parameter(name="--title", help="New title")] = None,
|
|
85
|
+
description: Annotated[str | None, Parameter(name="--description", help="New description")] = None,
|
|
86
|
+
labels: Annotated[str | None, Parameter(name="--labels", help="New labels")] = None,
|
|
87
|
+
target: Annotated[str | None, Parameter(name="--target", help="New target branch")] = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Update a merge request."""
|
|
90
|
+
client = ctx.client()
|
|
91
|
+
project = ctx.resolve_project()
|
|
92
|
+
result = client.update_mr(project, iid, title=title, description=description, labels=labels, target_branch=target)
|
|
93
|
+
output(result, ctx=ctx, format_fn=format_mr_detail)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mrs_app.command
|
|
97
|
+
def merge(
|
|
98
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
99
|
+
*,
|
|
100
|
+
squash: Annotated[bool | None, Parameter(name="--squash", help="Squash commits")] = None,
|
|
101
|
+
when_pipeline_succeeds: Annotated[
|
|
102
|
+
bool, Parameter(name="--when-pipeline-succeeds", help="Merge when pipeline succeeds", negative="")
|
|
103
|
+
] = False,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Merge a merge request."""
|
|
106
|
+
client = ctx.client()
|
|
107
|
+
project = ctx.resolve_project()
|
|
108
|
+
result = client.merge_mr(project, iid, squash=squash, merge_when_pipeline_succeeds=when_pipeline_succeeds)
|
|
109
|
+
output(result, ctx=ctx, format_fn=format_mr_detail)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@mrs_app.command
|
|
113
|
+
def close(
|
|
114
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Close a merge request."""
|
|
117
|
+
client = ctx.client()
|
|
118
|
+
project = ctx.resolve_project()
|
|
119
|
+
result = client.close_mr(project, iid)
|
|
120
|
+
output(result, ctx=ctx, format_fn=format_mr_detail)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@mrs_app.command
|
|
124
|
+
def discussions(
|
|
125
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
126
|
+
) -> None:
|
|
127
|
+
"""List discussions on a merge request."""
|
|
128
|
+
client = ctx.client()
|
|
129
|
+
project = ctx.resolve_project()
|
|
130
|
+
items = client.get_mr_discussions(project, iid)
|
|
131
|
+
output_list(items=items, ctx=ctx, format_fn=format_discussion_list)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mrs_app.command
|
|
135
|
+
def changes(
|
|
136
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Show changes/diff for a merge request."""
|
|
139
|
+
client = ctx.client()
|
|
140
|
+
project = ctx.resolve_project()
|
|
141
|
+
result = client.get_mr_changes(project, iid)
|
|
142
|
+
|
|
143
|
+
if ctx.json_mode:
|
|
144
|
+
from qodev_gitlab_cli.output import output_json
|
|
145
|
+
|
|
146
|
+
output_json(result)
|
|
147
|
+
else:
|
|
148
|
+
diffs = result.get("changes", [])
|
|
149
|
+
lines = [f"# Changes for !{iid}", ""]
|
|
150
|
+
for diff in diffs:
|
|
151
|
+
lines.append(f"## {diff.get('new_path', '?')}")
|
|
152
|
+
lines.append(f"```diff\n{diff.get('diff', '')}\n```")
|
|
153
|
+
lines.append("")
|
|
154
|
+
output_markdown("\n".join(lines))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mrs_app.command
|
|
158
|
+
def commits(
|
|
159
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
160
|
+
) -> None:
|
|
161
|
+
"""List commits in a merge request."""
|
|
162
|
+
client = ctx.client()
|
|
163
|
+
project = ctx.resolve_project()
|
|
164
|
+
items = client.get_mr_commits(project, iid)
|
|
165
|
+
output_list(items=items, ctx=ctx, format_fn=format_commit_list)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@mrs_app.command
|
|
169
|
+
def approvals(
|
|
170
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Show approval status for a merge request."""
|
|
173
|
+
client = ctx.client()
|
|
174
|
+
project = ctx.resolve_project()
|
|
175
|
+
result = client.get_mr_approvals(project, iid)
|
|
176
|
+
output(result, ctx=ctx, format_fn=format_approval_detail)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@mrs_app.command
|
|
180
|
+
def comment(
|
|
181
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
182
|
+
*,
|
|
183
|
+
body: Annotated[str, Parameter(name="--body", help="Comment text")],
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Comment on a merge request."""
|
|
186
|
+
client = ctx.client()
|
|
187
|
+
project = ctx.resolve_project()
|
|
188
|
+
result = client.create_mr_note(project, iid, body)
|
|
189
|
+
output(result, ctx=ctx)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@mrs_app.command
|
|
193
|
+
def pipelines(
|
|
194
|
+
iid: Annotated[int, Parameter(help="Merge request IID")],
|
|
195
|
+
) -> None:
|
|
196
|
+
"""List pipelines for a merge request."""
|
|
197
|
+
client = ctx.client()
|
|
198
|
+
project = ctx.resolve_project()
|
|
199
|
+
items = client.get_mr_pipelines(project, iid)
|
|
200
|
+
from qodev_gitlab_cli.formatters.pipelines import format_pipeline_list
|
|
201
|
+
|
|
202
|
+
output_list(items=items, ctx=ctx, format_fn=format_pipeline_list)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Pipeline commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from qodev_gitlab_cli.context import ctx
|
|
10
|
+
from qodev_gitlab_cli.formatters.jobs import format_job_list
|
|
11
|
+
from qodev_gitlab_cli.formatters.pipelines import format_pipeline_detail, format_pipeline_list, format_wait_result
|
|
12
|
+
from qodev_gitlab_cli.output import output, output_list
|
|
13
|
+
|
|
14
|
+
pipelines_app = App(name="pipelines", help="Manage pipelines.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pipelines_app.command
|
|
18
|
+
def list(
|
|
19
|
+
*,
|
|
20
|
+
ref: Annotated[str | None, Parameter(name="--ref", help="Filter by branch/tag")] = None,
|
|
21
|
+
limit: Annotated[int, Parameter(name="--limit", help="Max pipelines to return")] = 20,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""List pipelines."""
|
|
24
|
+
client = ctx.client()
|
|
25
|
+
project = ctx.resolve_project()
|
|
26
|
+
items = client.get_pipelines(project, ref=ref, per_page=limit, max_pages=1)
|
|
27
|
+
output_list(items=items, ctx=ctx, format_fn=format_pipeline_list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pipelines_app.command
|
|
31
|
+
def get(
|
|
32
|
+
id: Annotated[int, Parameter(help="Pipeline ID")],
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Get pipeline details."""
|
|
35
|
+
client = ctx.client()
|
|
36
|
+
project = ctx.resolve_project()
|
|
37
|
+
result = client.get_pipeline(project, id)
|
|
38
|
+
output(result, ctx=ctx, format_fn=format_pipeline_detail)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pipelines_app.command
|
|
42
|
+
def jobs(
|
|
43
|
+
id: Annotated[int, Parameter(help="Pipeline ID")],
|
|
44
|
+
) -> None:
|
|
45
|
+
"""List jobs for a pipeline."""
|
|
46
|
+
client = ctx.client()
|
|
47
|
+
project = ctx.resolve_project()
|
|
48
|
+
items = client.get_pipeline_jobs(project, id)
|
|
49
|
+
output_list(items=items, ctx=ctx, format_fn=format_job_list)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pipelines_app.command
|
|
53
|
+
def wait(
|
|
54
|
+
id: Annotated[int, Parameter(help="Pipeline ID")],
|
|
55
|
+
*,
|
|
56
|
+
timeout: Annotated[int, Parameter(name="--timeout", help="Timeout in seconds")] = 3600,
|
|
57
|
+
interval: Annotated[int, Parameter(name="--interval", help="Check interval in seconds")] = 10,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Wait for a pipeline to complete."""
|
|
60
|
+
client = ctx.client()
|
|
61
|
+
project = ctx.resolve_project()
|
|
62
|
+
result = client.wait_for_pipeline(project, id, timeout_seconds=timeout, check_interval=interval)
|
|
63
|
+
output(result, ctx=ctx, format_fn=format_wait_result)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Project commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from qodev_gitlab_cli.context import ctx
|
|
10
|
+
from qodev_gitlab_cli.formatters.projects import format_project_detail, format_project_list
|
|
11
|
+
from qodev_gitlab_cli.output import output, output_list
|
|
12
|
+
|
|
13
|
+
projects_app = App(name="projects", help="Manage projects.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@projects_app.command
|
|
17
|
+
def list(
|
|
18
|
+
*,
|
|
19
|
+
owned: Annotated[bool, Parameter(name="--owned", help="Only owned projects", negative="")] = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""List projects."""
|
|
22
|
+
client = ctx.client()
|
|
23
|
+
items = client.get_projects(owned=owned)
|
|
24
|
+
output_list(items=items, ctx=ctx, format_fn=format_project_list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@projects_app.command
|
|
28
|
+
def get(
|
|
29
|
+
id: Annotated[str | None, Parameter(help="Project ID or path (defaults to auto-detected)")] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Get project details."""
|
|
32
|
+
client = ctx.client()
|
|
33
|
+
project_id = id or ctx.resolve_project()
|
|
34
|
+
result = client.get_project(project_id)
|
|
35
|
+
output(result, ctx=ctx, format_fn=format_project_detail)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Release commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from qodev_gitlab_cli.context import ctx
|
|
10
|
+
from qodev_gitlab_cli.formatters.releases import format_release_detail, format_release_list
|
|
11
|
+
from qodev_gitlab_cli.output import output, output_list
|
|
12
|
+
|
|
13
|
+
releases_app = App(name="releases", help="Manage releases.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@releases_app.command
|
|
17
|
+
def list() -> None:
|
|
18
|
+
"""List releases."""
|
|
19
|
+
client = ctx.client()
|
|
20
|
+
project = ctx.resolve_project()
|
|
21
|
+
items = client.get_releases(project)
|
|
22
|
+
output_list(items=items, ctx=ctx, format_fn=format_release_list)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@releases_app.command
|
|
26
|
+
def get(
|
|
27
|
+
tag: Annotated[str, Parameter(help="Tag name")],
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Get release details."""
|
|
30
|
+
client = ctx.client()
|
|
31
|
+
project = ctx.resolve_project()
|
|
32
|
+
result = client.get_release(project, tag)
|
|
33
|
+
output(result, ctx=ctx, format_fn=format_release_detail)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@releases_app.command
|
|
37
|
+
def create(
|
|
38
|
+
*,
|
|
39
|
+
tag: Annotated[str, Parameter(name="--tag", help="Tag name for the release")],
|
|
40
|
+
name: Annotated[str | None, Parameter(name="--name", help="Release title")] = None,
|
|
41
|
+
description: Annotated[str | None, Parameter(name="--description", help="Release notes")] = None,
|
|
42
|
+
ref: Annotated[str | None, Parameter(name="--ref", help="Commit SHA, branch, or tag")] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Create a new release."""
|
|
45
|
+
client = ctx.client()
|
|
46
|
+
project = ctx.resolve_project()
|
|
47
|
+
result = client.create_release(project, tag_name=tag, name=name, description=description, ref=ref)
|
|
48
|
+
output(result, ctx=ctx, format_fn=format_release_detail)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""CI/CD variable commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from qodev_gitlab_cli.context import ctx
|
|
10
|
+
from qodev_gitlab_cli.formatters.variables import format_variable_detail, format_variable_list
|
|
11
|
+
from qodev_gitlab_cli.output import output, output_list
|
|
12
|
+
|
|
13
|
+
variables_app = App(name="variables", help="Manage CI/CD variables.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@variables_app.command
|
|
17
|
+
def list() -> None:
|
|
18
|
+
"""List CI/CD variables (values hidden)."""
|
|
19
|
+
client = ctx.client()
|
|
20
|
+
project = ctx.resolve_project()
|
|
21
|
+
items = client.list_project_variables(project)
|
|
22
|
+
output_list(items=items, ctx=ctx, format_fn=format_variable_list)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@variables_app.command
|
|
26
|
+
def get(
|
|
27
|
+
key: Annotated[str, Parameter(help="Variable key")],
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Get a CI/CD variable."""
|
|
30
|
+
client = ctx.client()
|
|
31
|
+
project = ctx.resolve_project()
|
|
32
|
+
result = client.get_project_variable(project, key)
|
|
33
|
+
if result is None:
|
|
34
|
+
from qodev_gitlab_cli.output import error
|
|
35
|
+
|
|
36
|
+
error(f"Variable '{key}' not found.", ctx=ctx, code="not_found", exit_code=81)
|
|
37
|
+
else:
|
|
38
|
+
output(result, ctx=ctx, format_fn=format_variable_detail)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@variables_app.command
|
|
42
|
+
def set(
|
|
43
|
+
key: Annotated[str, Parameter(help="Variable key")],
|
|
44
|
+
value: Annotated[str, Parameter(help="Variable value")],
|
|
45
|
+
*,
|
|
46
|
+
protected: Annotated[bool, Parameter(name="--protected", help="Only in protected branches", negative="")] = False,
|
|
47
|
+
masked: Annotated[bool, Parameter(name="--masked", help="Hidden in job logs", negative="")] = False,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Set a CI/CD variable (create or update)."""
|
|
50
|
+
client = ctx.client()
|
|
51
|
+
project = ctx.resolve_project()
|
|
52
|
+
var, action = client.set_project_variable(project, key, value, protected=protected, masked=masked)
|
|
53
|
+
|
|
54
|
+
if ctx.json_mode:
|
|
55
|
+
from qodev_gitlab_cli.output import output_json
|
|
56
|
+
|
|
57
|
+
output_json({"variable": var, "action": action})
|
|
58
|
+
else:
|
|
59
|
+
from qodev_gitlab_cli.output import output_markdown
|
|
60
|
+
|
|
61
|
+
output_markdown(f"Variable `{key}` **{action}** successfully.")
|