sase-github 0.1.1__tar.gz → 0.1.3__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.
- sase_github-0.1.3/.release-please-manifest.json +3 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/CHANGELOG.md +25 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/PKG-INFO +1 -1
- {sase_github-0.1.1 → sase_github-0.1.3}/README.md +1 -0
- sase_github-0.1.3/docs/architecture.md +108 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/docs/configuration.md +7 -6
- {sase_github-0.1.1 → sase_github-0.1.3}/pyproject.toml +1 -1
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/workspace_plugin.py +17 -15
- {sase_github-0.1.1 → sase_github-0.1.3}/tests/test_workspace_plugin.py +35 -9
- sase_github-0.1.1/.release-please-manifest.json +0 -3
- sase_github-0.1.1/docs/architecture.md +0 -105
- {sase_github-0.1.1 → sase_github-0.1.3}/.github/workflows/ci.yml +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/.github/workflows/pr-title.yml +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/.github/workflows/publish.yml +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/.sase_beads/beads.db +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/.sase_beads/config.json +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/.sase_beads/issues.jsonl +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/CLAUDE.md +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/Justfile +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/LICENSE +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/docs/xprompts.md +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/release-please-config.json +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/__init__.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/config.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/default_config.yml +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/plugin.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/scripts/__init__.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/scripts/gh_setup.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/scripts/new_pr_desc_get_context.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/xprompts/gh.yml +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/xprompts/new_pr_desc.yml +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/xprompts/pr_diff.yml +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/src/sase_github/xprompts/prdd.yml +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/tests/__init__.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/tests/test_github_plugin.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/tests/test_submit_with_recorded_pr.py +0 -0
- {sase_github-0.1.1 → sase_github-0.1.3}/uv.lock +0 -0
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.3](https://github.com/sase-org/sase-github/compare/v0.1.2...v0.1.3) (2026-06-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* write project display names for repo refs ([65ddc1d](https://github.com/sase-org/sase-github/commit/65ddc1d6dd9efe152897449ffa7eb421d8694802))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* add tiny field note ([d1d6563](https://github.com/sase-org/sase-github/commit/d1d6563822f3e48ec2504fed9156d1d0bbabb8a1))
|
|
14
|
+
|
|
15
|
+
## [0.1.2](https://github.com/sase-org/sase-github/compare/v0.1.1...v0.1.2) (2026-06-13)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* restore sase-github PyPI publish workflow ([077e91a](https://github.com/sase-org/sase-github/commit/077e91a57e836856d1e263cdc348cf3761516b4c))
|
|
21
|
+
* use trusted publisher workflow path ([6a9fea0](https://github.com/sase-org/sase-github/commit/6a9fea00701428903f086ac871ac6c103521ae1d))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
* add PyPI version badge ([58f0145](https://github.com/sase-org/sase-github/commit/58f014514664d6c8402b04bbaea37e0402099312))
|
|
27
|
+
|
|
3
28
|
## 0.1.1 (2026-06-09)
|
|
4
29
|
|
|
5
30
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# sase-github — GitHub VCS Plugin for sase
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/sase-github/)
|
|
3
4
|
[](https://github.com/astral-sh/ruff)
|
|
4
5
|
[](https://mypy-lang.org/)
|
|
5
6
|
[](https://docs.pytest.org/)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
sase-github is structured as two pluggy-based plugins that integrate with sase core via Python entry points.
|
|
4
|
+
|
|
5
|
+
## Plugin System
|
|
6
|
+
|
|
7
|
+
### Entry Points
|
|
8
|
+
|
|
9
|
+
Registered in `pyproject.toml`:
|
|
10
|
+
|
|
11
|
+
| Entry Point | Plugin | Purpose |
|
|
12
|
+
| --------------------------- | ----------------------- | --------------------------------------------------------------- |
|
|
13
|
+
| `sase_vcs:github` | `GitHubPlugin` | VCS operations (push, PR creation, PR info) |
|
|
14
|
+
| `sase_workspace:github` | `GitHubWorkspacePlugin` | Workspace orchestration (ref resolution, submission, mail prep) |
|
|
15
|
+
| `sase_xprompts:sase_github` | — | Makes `#gh`, `#new_pr_desc`, `#prdd` xprompts discoverable |
|
|
16
|
+
| `sase_config:sase_github` | — | Contributes `default_config.yml` to the sase config chain |
|
|
17
|
+
|
|
18
|
+
### GitHubPlugin (`plugin.py`)
|
|
19
|
+
|
|
20
|
+
Extends `GitCommon` from sase core. Handles low-level VCS operations by wrapping `git` and `gh` CLI commands.
|
|
21
|
+
|
|
22
|
+
**Hook implementations:**
|
|
23
|
+
|
|
24
|
+
| Hook | Behavior |
|
|
25
|
+
| --------------------------- | ------------------------------------------------------------------------------------- |
|
|
26
|
+
| `vcs_classify_repo()` | Claims repos with `github.com` in their origin URL |
|
|
27
|
+
| `vcs_get_change_url()` | Returns PR URL via `gh pr view --json url` |
|
|
28
|
+
| `vcs_get_cl_number()` | Returns PR number via `gh pr view --json number` |
|
|
29
|
+
| `vcs_mail()` | Pushes branch (`git push -u origin`) and creates PR if needed (`gh pr create --fill`) |
|
|
30
|
+
| `vcs_create_pull_request()` | Creates a PR with an AI-generated title and body |
|
|
31
|
+
|
|
32
|
+
### GitHubWorkspacePlugin (`workspace_plugin.py`)
|
|
33
|
+
|
|
34
|
+
Handles higher-level workflow orchestration. Implements workspace hooks for GitHub-hosted projects.
|
|
35
|
+
|
|
36
|
+
**Hook implementations:**
|
|
37
|
+
|
|
38
|
+
| Hook | Behavior |
|
|
39
|
+
| -------------------------------------- | --------------------------------------------------------------------------------- |
|
|
40
|
+
| `ws_get_workflow_metadata()` | Returns metadata for the `gh` workflow type (ref pattern `#gh`, vcs family `git`) |
|
|
41
|
+
| `ws_detect_workflow_type()` | Returns `"gh"` for repos with a remote origin URL (non-local) |
|
|
42
|
+
| `ws_get_change_label()` | Returns `"PR"` for GitHub projects |
|
|
43
|
+
| `ws_resolve_ref()` | Resolves `#gh` references (see [Reference Resolution](#reference-resolution)) |
|
|
44
|
+
| `ws_extract_change_identifier()` | Extracts PR number from GitHub PR URLs |
|
|
45
|
+
| `ws_generate_submitted_check_script()` | Generates a bash script that checks if a PR is merged via `gh pr view` |
|
|
46
|
+
| `ws_supports_reviewer_comments()` | Returns `False` for GitHub URLs (critique comments not supported) |
|
|
47
|
+
| `ws_get_workspace_directory()` | Ensures git clone exists via `ensure_git_clone()` |
|
|
48
|
+
| `ws_prepare_mail()` | Displays branch/description and prompts user before pushing |
|
|
49
|
+
| `ws_format_commit_description()` | Prepends `[project]` prefix to commit messages |
|
|
50
|
+
| `ws_submit()` | Submits a ChangeSpec by merging its PR via `gh pr merge --merge --delete-branch` |
|
|
51
|
+
|
|
52
|
+
## Reference Resolution
|
|
53
|
+
|
|
54
|
+
The `resolve_gh_ref()` function supports three dispatch modes for `#gh` references:
|
|
55
|
+
|
|
56
|
+
### Mode 1: Repo Path (`user/project`)
|
|
57
|
+
|
|
58
|
+
When the ref contains `/`, it's treated as a GitHub repo path:
|
|
59
|
+
|
|
60
|
+
- Derives workspace from `~/projects/github/<user>/<project>/`
|
|
61
|
+
- Clones the repo if it doesn't exist (SSH for orgs in `github_orgs`, HTTPS otherwise)
|
|
62
|
+
- Reuses an existing SASE ProjectSpec whose normalized `WORKSPACE_DIR` already points at that workspace
|
|
63
|
+
- Otherwise creates a canonical project name from the repo identity, normally `gh_<user>__<project>`
|
|
64
|
+
- Adds a deterministic suffix such as `-2` only if that canonical project name is already occupied by a different
|
|
65
|
+
project, `PROJECT_NAME`, or alias
|
|
66
|
+
- Sets `WORKSPACE_DIR` in the canonical project file
|
|
67
|
+
- Sets a useful `PROJECT_NAME` for the repo basename, suffixing duplicate basenames as `<project>_1`, `<project>_2`, and
|
|
68
|
+
higher
|
|
69
|
+
- Checks out the default branch
|
|
70
|
+
|
|
71
|
+
This preserves legacy basename projects. If `~/.sase/projects/foo/foo.sase` already points at
|
|
72
|
+
`~/projects/github/foo-org/foo/`, resolving `#gh:foo-org/foo` keeps using project `foo`; it does not rename or migrate
|
|
73
|
+
the ProjectSpec. Existing auto-aliased GitHub projects are also left as-is and continue resolving through their alias.
|
|
74
|
+
|
|
75
|
+
### Mode 2: Project Shorthand (`myproject`)
|
|
76
|
+
|
|
77
|
+
When the ref matches an existing project directory, `PROJECT_NAME`, or project alias:
|
|
78
|
+
|
|
79
|
+
- Resolves friendly names before project-directory lookup, so a generated `PROJECT_NAME` like `foo_1` points at its
|
|
80
|
+
canonical directory-key project
|
|
81
|
+
- Looks up `~/.sase/projects/<name>/<name>.sase` (legacy `.gp` is read as a fallback)
|
|
82
|
+
- Reads `WORKSPACE_DIR` from the project file
|
|
83
|
+
- Checks out the default branch
|
|
84
|
+
|
|
85
|
+
### Mode 3: ChangeSpec Name
|
|
86
|
+
|
|
87
|
+
When the ref matches an existing ChangeSpec:
|
|
88
|
+
|
|
89
|
+
- Searches all changespecs for a matching name
|
|
90
|
+
- Reads `WORKSPACE_DIR` from the changespec's project file
|
|
91
|
+
- Checks out `origin/<name>` (the ChangeSpec's branch)
|
|
92
|
+
|
|
93
|
+
## Submission Flow
|
|
94
|
+
|
|
95
|
+
When submitting a GitHub ChangeSpec (`ws_submit`):
|
|
96
|
+
|
|
97
|
+
1. Kill and persist all running processes on the ChangeSpec
|
|
98
|
+
2. Verify no active child ChangeSpecs exist
|
|
99
|
+
3. Claim a workspace and checkout the ChangeSpec branch
|
|
100
|
+
4. Check for an existing PR on the branch
|
|
101
|
+
5. Merge via `gh pr merge --merge --delete-branch`
|
|
102
|
+
6. Finalize submission (update ChangeSpec status)
|
|
103
|
+
7. Release the workspace
|
|
104
|
+
|
|
105
|
+
## Config Helper
|
|
106
|
+
|
|
107
|
+
`config.py` provides `get_github_orgs()` which reads the `github_orgs` list from the merged sase config. This determines
|
|
108
|
+
whether repos are cloned via SSH (for orgs the user has push access to) or HTTPS.
|
|
@@ -35,8 +35,8 @@ Currently the default config defines:
|
|
|
35
35
|
## Workspace Layout
|
|
36
36
|
|
|
37
37
|
Primary GitHub workspaces are stored under `~/projects/github/<user>/<project>/` when first resolved from a
|
|
38
|
-
`#gh(user/project)` reference. Numbered parallel-work checkouts follow SASE's shared `workspace.root` policy: by
|
|
39
|
-
|
|
38
|
+
`#gh(user/project)` reference. Numbered parallel-work checkouts follow SASE's shared `workspace.root` policy: by default
|
|
39
|
+
they live under the platform state-root namespace, while explicit `workspace.root: adjacent` keeps the legacy
|
|
40
40
|
`~/projects/github/<user>/<project>_<N>/` sibling layout.
|
|
41
41
|
|
|
42
42
|
## Project Files
|
|
@@ -47,9 +47,10 @@ when you first use an `#gh:<user>/<project>` ref.
|
|
|
47
47
|
|
|
48
48
|
For new `owner/repo` refs, the project name is based on the full GitHub identity, normally `gh_<user>__<project>`, so
|
|
49
49
|
two owners can have repositories with the same basename. If that canonical name is already occupied by a different
|
|
50
|
-
project or alias, sase-github adds a deterministic suffix such as `-2`.
|
|
50
|
+
project, `PROJECT_NAME`, or alias, sase-github adds a deterministic suffix such as `-2`.
|
|
51
51
|
|
|
52
|
-
sase-github also writes
|
|
53
|
-
|
|
52
|
+
sase-github also writes `PROJECT_NAME` to the repo basename when it is valid and useful. The first `owner/foo` repo can
|
|
53
|
+
get `PROJECT_NAME: foo`; a second `owner/foo` repo gets the next available display name such as `foo_1`. Existing
|
|
54
54
|
basename ProjectSpecs are reused when their `WORKSPACE_DIR` already matches the GitHub workspace, so no automatic
|
|
55
|
-
migration or rename is required.
|
|
55
|
+
migration or rename is required. Existing auto-aliased GitHub projects are also left unchanged and keep resolving via
|
|
56
|
+
their `PROJECT_ALIASES` entry.
|
|
@@ -401,7 +401,7 @@ def _find_project_record_for_alias(
|
|
|
401
401
|
alias: str,
|
|
402
402
|
) -> ProjectRecordWire | None:
|
|
403
403
|
for record in records:
|
|
404
|
-
if alias in record.aliases:
|
|
404
|
+
if alias == getattr(record, "display_name", None) or alias in record.aliases:
|
|
405
405
|
return record
|
|
406
406
|
return None
|
|
407
407
|
|
|
@@ -422,10 +422,12 @@ def _canonical_project_name_base(user: str, project: str) -> str:
|
|
|
422
422
|
return base
|
|
423
423
|
|
|
424
424
|
|
|
425
|
-
def
|
|
425
|
+
def _project_refs(records: Sequence[ProjectRecordWire]) -> set[str]:
|
|
426
426
|
occupied: set[str] = set()
|
|
427
427
|
for record in records:
|
|
428
428
|
occupied.add(record.project_name)
|
|
429
|
+
if display_name := getattr(record, "display_name", None):
|
|
430
|
+
occupied.add(display_name)
|
|
429
431
|
occupied.update(record.aliases)
|
|
430
432
|
return occupied
|
|
431
433
|
|
|
@@ -436,7 +438,7 @@ def _allocate_canonical_project_name(
|
|
|
436
438
|
records: Sequence[ProjectRecordWire],
|
|
437
439
|
) -> str:
|
|
438
440
|
base = _canonical_project_name_base(user, project)
|
|
439
|
-
occupied =
|
|
441
|
+
occupied = _project_refs(records)
|
|
440
442
|
|
|
441
443
|
candidate = base
|
|
442
444
|
suffix = 2
|
|
@@ -450,7 +452,7 @@ def _project_file_for(projects_base: Path, project_name: str) -> str:
|
|
|
450
452
|
return preferred_project_spec_path(str(projects_base / project_name), project_name)
|
|
451
453
|
|
|
452
454
|
|
|
453
|
-
def
|
|
455
|
+
def _ensure_useful_repo_name(
|
|
454
456
|
project_name: str,
|
|
455
457
|
repo_name: str,
|
|
456
458
|
*,
|
|
@@ -460,24 +462,24 @@ def _ensure_useful_repo_alias(
|
|
|
460
462
|
return
|
|
461
463
|
|
|
462
464
|
from sase.project_aliases import (
|
|
463
|
-
|
|
464
|
-
|
|
465
|
+
allocate_project_name,
|
|
466
|
+
ensure_project_name_locked,
|
|
465
467
|
)
|
|
466
468
|
|
|
467
469
|
attempts = 3
|
|
468
470
|
for attempt in range(attempts):
|
|
469
471
|
records = _list_project_records(projects_base)
|
|
470
|
-
|
|
472
|
+
display_name = allocate_project_name(
|
|
471
473
|
repo_name,
|
|
472
474
|
records,
|
|
473
475
|
project_name=project_name,
|
|
474
476
|
)
|
|
475
|
-
if
|
|
477
|
+
if display_name == project_name:
|
|
476
478
|
return
|
|
477
479
|
try:
|
|
478
|
-
|
|
480
|
+
ensure_project_name_locked(
|
|
479
481
|
project_name,
|
|
480
|
-
|
|
482
|
+
display_name,
|
|
481
483
|
projects_root=projects_base,
|
|
482
484
|
)
|
|
483
485
|
return
|
|
@@ -520,15 +522,15 @@ def _resolve_repo_path_ref(user: str, project: str) -> ResolvedRef:
|
|
|
520
522
|
project_file = _project_file_for(projects_base, project_name)
|
|
521
523
|
if not set_workspace_dir(project_file, primary_workspace_dir):
|
|
522
524
|
raise ValueError(f"Failed to write WORKSPACE_DIR for '{project_name}'")
|
|
525
|
+
_ensure_useful_repo_name(
|
|
526
|
+
project_name,
|
|
527
|
+
project,
|
|
528
|
+
projects_base=projects_base,
|
|
529
|
+
)
|
|
523
530
|
else:
|
|
524
531
|
project_name = existing_record.project_name
|
|
525
532
|
project_file = existing_record.project_file
|
|
526
533
|
|
|
527
|
-
_ensure_useful_repo_alias(
|
|
528
|
-
project_name,
|
|
529
|
-
project,
|
|
530
|
-
projects_base=projects_base,
|
|
531
|
-
)
|
|
532
534
|
checkout_target = get_default_branch(primary_workspace_dir)
|
|
533
535
|
|
|
534
536
|
return ResolvedRef(
|
|
@@ -38,7 +38,7 @@ class TestResolveGhRef:
|
|
|
38
38
|
@patch(
|
|
39
39
|
"sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
|
|
40
40
|
)
|
|
41
|
-
def
|
|
41
|
+
def test_repo_path_creates_canonical_project_and_name(
|
|
42
42
|
self, mock_branch: MagicMock
|
|
43
43
|
) -> None:
|
|
44
44
|
with tempfile.TemporaryDirectory() as d:
|
|
@@ -61,12 +61,13 @@ class TestResolveGhRef:
|
|
|
61
61
|
assert result.primary_workspace_dir == primary
|
|
62
62
|
assert result.checkout_target == "origin/main"
|
|
63
63
|
assert f"WORKSPACE_DIR: {primary}\n" in content
|
|
64
|
-
assert "
|
|
64
|
+
assert "PROJECT_NAME: myrepo\n" in content
|
|
65
|
+
assert "PROJECT_ALIASES" not in content
|
|
65
66
|
|
|
66
67
|
@patch(
|
|
67
68
|
"sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
|
|
68
69
|
)
|
|
69
|
-
def
|
|
70
|
+
def test_duplicate_repo_basename_gets_distinct_name(
|
|
70
71
|
self, mock_branch: MagicMock
|
|
71
72
|
) -> None:
|
|
72
73
|
with tempfile.TemporaryDirectory() as d:
|
|
@@ -82,8 +83,8 @@ class TestResolveGhRef:
|
|
|
82
83
|
second_file = Path(second.project_file)
|
|
83
84
|
assert first.project_name == "gh_foo-org__foo"
|
|
84
85
|
assert second.project_name == "gh_bar-org__foo"
|
|
85
|
-
assert "
|
|
86
|
-
assert "
|
|
86
|
+
assert "PROJECT_NAME: foo\n" in first_file.read_text(encoding="utf-8")
|
|
87
|
+
assert "PROJECT_NAME: foo_1\n" in second_file.read_text(encoding="utf-8")
|
|
87
88
|
|
|
88
89
|
@patch(
|
|
89
90
|
"sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
|
|
@@ -108,6 +109,30 @@ class TestResolveGhRef:
|
|
|
108
109
|
assert result.project_file == str(project_file)
|
|
109
110
|
assert "PROJECT_ALIASES" not in content
|
|
110
111
|
|
|
112
|
+
@patch(
|
|
113
|
+
"sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
|
|
114
|
+
)
|
|
115
|
+
def test_repo_path_reuses_existing_auto_aliased_project_without_migration(
|
|
116
|
+
self, mock_branch: MagicMock
|
|
117
|
+
) -> None:
|
|
118
|
+
with tempfile.TemporaryDirectory() as d:
|
|
119
|
+
home = Path(d)
|
|
120
|
+
primary = _github_workspace(home, "alice", "myrepo")
|
|
121
|
+
project_file = _write_project(
|
|
122
|
+
home,
|
|
123
|
+
"gh_alice__myrepo",
|
|
124
|
+
f"WORKSPACE_DIR: {primary}\nPROJECT_ALIASES: myrepo\nNAME: legacy\n",
|
|
125
|
+
)
|
|
126
|
+
path_patch, env_patch = _home_patches(home)
|
|
127
|
+
with path_patch, env_patch:
|
|
128
|
+
result = resolve_gh_ref("alice/myrepo")
|
|
129
|
+
|
|
130
|
+
content = project_file.read_text(encoding="utf-8")
|
|
131
|
+
assert result.project_name == "gh_alice__myrepo"
|
|
132
|
+
assert result.project_file == str(project_file)
|
|
133
|
+
assert "PROJECT_ALIASES: myrepo\n" in content
|
|
134
|
+
assert "PROJECT_NAME" not in content
|
|
135
|
+
|
|
111
136
|
@patch(
|
|
112
137
|
"sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
|
|
113
138
|
)
|
|
@@ -129,7 +154,8 @@ class TestResolveGhRef:
|
|
|
129
154
|
content = Path(result.project_file).read_text(encoding="utf-8")
|
|
130
155
|
assert result.project_name == "gh_alice__foo"
|
|
131
156
|
assert result.primary_workspace_dir == primary
|
|
132
|
-
assert "
|
|
157
|
+
assert "PROJECT_NAME: foo_1\n" in content
|
|
158
|
+
assert "PROJECT_ALIASES" not in content
|
|
133
159
|
|
|
134
160
|
@patch(
|
|
135
161
|
"sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
|
|
@@ -150,14 +176,14 @@ class TestResolveGhRef:
|
|
|
150
176
|
result = resolve_gh_ref("alice/foo")
|
|
151
177
|
|
|
152
178
|
assert result.project_name == "gh_alice__foo-2"
|
|
153
|
-
assert "
|
|
179
|
+
assert "PROJECT_NAME: foo\n" in Path(result.project_file).read_text(
|
|
154
180
|
encoding="utf-8"
|
|
155
181
|
)
|
|
156
182
|
|
|
157
183
|
@patch(
|
|
158
184
|
"sase_github.workspace_plugin.get_default_branch", return_value="origin/main"
|
|
159
185
|
)
|
|
160
|
-
def
|
|
186
|
+
def test_project_name_shorthand_resolves_canonical_project(
|
|
161
187
|
self, mock_branch: MagicMock
|
|
162
188
|
) -> None:
|
|
163
189
|
with tempfile.TemporaryDirectory() as d:
|
|
@@ -168,7 +194,7 @@ class TestResolveGhRef:
|
|
|
168
194
|
with path_patch, env_patch:
|
|
169
195
|
resolve_gh_ref("foo-org/foo")
|
|
170
196
|
canonical = resolve_gh_ref("bar-org/foo")
|
|
171
|
-
alias = resolve_gh_ref("
|
|
197
|
+
alias = resolve_gh_ref("foo_1")
|
|
172
198
|
|
|
173
199
|
assert alias.project_name == canonical.project_name
|
|
174
200
|
assert alias.project_file == canonical.project_file
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
sase-github is structured as two pluggy-based plugins that integrate with sase core via Python entry points.
|
|
4
|
-
|
|
5
|
-
## Plugin System
|
|
6
|
-
|
|
7
|
-
### Entry Points
|
|
8
|
-
|
|
9
|
-
Registered in `pyproject.toml`:
|
|
10
|
-
|
|
11
|
-
| Entry Point | Plugin | Purpose |
|
|
12
|
-
|---|---|---|
|
|
13
|
-
| `sase_vcs:github` | `GitHubPlugin` | VCS operations (push, PR creation, PR info) |
|
|
14
|
-
| `sase_workspace:github` | `GitHubWorkspacePlugin` | Workspace orchestration (ref resolution, submission, mail prep) |
|
|
15
|
-
| `sase_xprompts:sase_github` | — | Makes `#gh`, `#new_pr_desc`, `#prdd` xprompts discoverable |
|
|
16
|
-
| `sase_config:sase_github` | — | Contributes `default_config.yml` to the sase config chain |
|
|
17
|
-
|
|
18
|
-
### GitHubPlugin (`plugin.py`)
|
|
19
|
-
|
|
20
|
-
Extends `GitCommon` from sase core. Handles low-level VCS operations by wrapping `git` and `gh` CLI commands.
|
|
21
|
-
|
|
22
|
-
**Hook implementations:**
|
|
23
|
-
|
|
24
|
-
| Hook | Behavior |
|
|
25
|
-
|---|---|
|
|
26
|
-
| `vcs_classify_repo()` | Claims repos with `github.com` in their origin URL |
|
|
27
|
-
| `vcs_get_change_url()` | Returns PR URL via `gh pr view --json url` |
|
|
28
|
-
| `vcs_get_cl_number()` | Returns PR number via `gh pr view --json number` |
|
|
29
|
-
| `vcs_mail()` | Pushes branch (`git push -u origin`) and creates PR if needed (`gh pr create --fill`) |
|
|
30
|
-
| `vcs_create_pull_request()` | Creates a PR with an AI-generated title and body |
|
|
31
|
-
|
|
32
|
-
### GitHubWorkspacePlugin (`workspace_plugin.py`)
|
|
33
|
-
|
|
34
|
-
Handles higher-level workflow orchestration. Implements workspace hooks for GitHub-hosted projects.
|
|
35
|
-
|
|
36
|
-
**Hook implementations:**
|
|
37
|
-
|
|
38
|
-
| Hook | Behavior |
|
|
39
|
-
|---|---|
|
|
40
|
-
| `ws_get_workflow_metadata()` | Returns metadata for the `gh` workflow type (ref pattern `#gh`, vcs family `git`) |
|
|
41
|
-
| `ws_detect_workflow_type()` | Returns `"gh"` for repos with a remote origin URL (non-local) |
|
|
42
|
-
| `ws_get_change_label()` | Returns `"PR"` for GitHub projects |
|
|
43
|
-
| `ws_resolve_ref()` | Resolves `#gh` references (see [Reference Resolution](#reference-resolution)) |
|
|
44
|
-
| `ws_extract_change_identifier()` | Extracts PR number from GitHub PR URLs |
|
|
45
|
-
| `ws_generate_submitted_check_script()` | Generates a bash script that checks if a PR is merged via `gh pr view` |
|
|
46
|
-
| `ws_supports_reviewer_comments()` | Returns `False` for GitHub URLs (critique comments not supported) |
|
|
47
|
-
| `ws_get_workspace_directory()` | Ensures git clone exists via `ensure_git_clone()` |
|
|
48
|
-
| `ws_prepare_mail()` | Displays branch/description and prompts user before pushing |
|
|
49
|
-
| `ws_format_commit_description()` | Prepends `[project]` prefix to commit messages |
|
|
50
|
-
| `ws_submit()` | Submits a ChangeSpec by merging its PR via `gh pr merge --merge --delete-branch` |
|
|
51
|
-
|
|
52
|
-
## Reference Resolution
|
|
53
|
-
|
|
54
|
-
The `resolve_gh_ref()` function supports three dispatch modes for `#gh` references:
|
|
55
|
-
|
|
56
|
-
### Mode 1: Repo Path (`user/project`)
|
|
57
|
-
|
|
58
|
-
When the ref contains `/`, it's treated as a GitHub repo path:
|
|
59
|
-
|
|
60
|
-
- Derives workspace from `~/projects/github/<user>/<project>/`
|
|
61
|
-
- Clones the repo if it doesn't exist (SSH for orgs in `github_orgs`, HTTPS otherwise)
|
|
62
|
-
- Reuses an existing SASE ProjectSpec whose normalized `WORKSPACE_DIR` already points at that workspace
|
|
63
|
-
- Otherwise creates a canonical project name from the repo identity, normally `gh_<user>__<project>`
|
|
64
|
-
- Adds a deterministic suffix such as `-2` only if that canonical project name is already occupied by a different
|
|
65
|
-
project or alias
|
|
66
|
-
- Sets `WORKSPACE_DIR` in the canonical project file
|
|
67
|
-
- Ensures a useful short alias for the repo basename, suffixing duplicate basenames as `<project>-2`, `<project>-3`,
|
|
68
|
-
and higher
|
|
69
|
-
- Checks out the default branch
|
|
70
|
-
|
|
71
|
-
This preserves legacy basename projects. If `~/.sase/projects/foo/foo.sase` already points at
|
|
72
|
-
`~/projects/github/foo-org/foo/`, resolving `#gh:foo-org/foo` keeps using project `foo`; it does not rename or migrate
|
|
73
|
-
the ProjectSpec.
|
|
74
|
-
|
|
75
|
-
### Mode 2: Project Shorthand (`myproject`)
|
|
76
|
-
|
|
77
|
-
When the ref matches an existing project directory or project alias:
|
|
78
|
-
|
|
79
|
-
- Resolves aliases before project-directory lookup, so a generated alias like `foo-2` points at its canonical project
|
|
80
|
-
- Looks up `~/.sase/projects/<name>/<name>.sase` (legacy `.gp` is read as a fallback)
|
|
81
|
-
- Reads `WORKSPACE_DIR` from the project file
|
|
82
|
-
- Checks out the default branch
|
|
83
|
-
|
|
84
|
-
### Mode 3: ChangeSpec Name
|
|
85
|
-
|
|
86
|
-
When the ref matches an existing ChangeSpec:
|
|
87
|
-
- Searches all changespecs for a matching name
|
|
88
|
-
- Reads `WORKSPACE_DIR` from the changespec's project file
|
|
89
|
-
- Checks out `origin/<name>` (the ChangeSpec's branch)
|
|
90
|
-
|
|
91
|
-
## Submission Flow
|
|
92
|
-
|
|
93
|
-
When submitting a GitHub ChangeSpec (`ws_submit`):
|
|
94
|
-
|
|
95
|
-
1. Kill and persist all running processes on the ChangeSpec
|
|
96
|
-
2. Verify no active child ChangeSpecs exist
|
|
97
|
-
3. Claim a workspace and checkout the ChangeSpec branch
|
|
98
|
-
4. Check for an existing PR on the branch
|
|
99
|
-
5. Merge via `gh pr merge --merge --delete-branch`
|
|
100
|
-
6. Finalize submission (update ChangeSpec status)
|
|
101
|
-
7. Release the workspace
|
|
102
|
-
|
|
103
|
-
## Config Helper
|
|
104
|
-
|
|
105
|
-
`config.py` provides `get_github_orgs()` which reads the `github_orgs` list from the merged sase config. This determines whether repos are cloned via SSH (for orgs the user has push access to) or HTTPS.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|