chatgh 0.2.2__tar.gz → 0.2.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.
Files changed (29) hide show
  1. {chatgh-0.2.2/src/chatgh.egg-info → chatgh-0.2.3}/PKG-INFO +21 -54
  2. {chatgh-0.2.2 → chatgh-0.2.3}/README.md +20 -53
  3. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/__init__.py +1 -1
  4. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/cli.py +2 -1
  5. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/github/api.py +32 -40
  6. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/github/cli.py +80 -1
  7. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/github/commands.py +30 -0
  8. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/github/requests.py +115 -0
  9. {chatgh-0.2.2 → chatgh-0.2.3/src/chatgh.egg-info}/PKG-INFO +21 -54
  10. {chatgh-0.2.2 → chatgh-0.2.3}/tests/test_version.py +1 -1
  11. {chatgh-0.2.2 → chatgh-0.2.3}/LICENSE +0 -0
  12. {chatgh-0.2.2 → chatgh-0.2.3}/pyproject.toml +0 -0
  13. {chatgh-0.2.2 → chatgh-0.2.3}/setup.cfg +0 -0
  14. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/_generated/__init__.py +0 -0
  15. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/adapters/__init__.py +0 -0
  16. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/adapters/client.py +0 -0
  17. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/commands/__init__.py +0 -0
  18. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/commands/pr.py +0 -0
  19. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/config.py +0 -0
  20. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/github/__init__.py +0 -0
  21. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/github/client.py +0 -0
  22. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh/github/render.py +0 -0
  23. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh.egg-info/SOURCES.txt +0 -0
  24. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh.egg-info/dependency_links.txt +0 -0
  25. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh.egg-info/entry_points.txt +0 -0
  26. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh.egg-info/requires.txt +0 -0
  27. {chatgh-0.2.2 → chatgh-0.2.3}/src/chatgh.egg-info/top_level.txt +0 -0
  28. {chatgh-0.2.2 → chatgh-0.2.3}/tests/test_adapter_client.py +0 -0
  29. {chatgh-0.2.2 → chatgh-0.2.3}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatgh
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: GitHub CLI and Python helpers for ChatArch
5
5
  Author-email: ChatArch <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -43,7 +43,7 @@ pip install -e ".[dev]"
43
43
  默认行为:
44
44
 
45
45
  - `repo`:优先使用显式 `--repo owner/repo`,未传时从当前 git remote 推断。
46
- - `token`:优先使用显式 `--token`,其次读取当前仓库对应的 git credential,再回退 typed env 里的 `GITHUB_ACCESS_TOKEN`。
46
+ - `token`:优先使用显式 `--token`,其次读取当前仓库 `.git/config` 中 repo-local HTTPS auth header,再回退 typed env 里的 `GITHUB_ACCESS_TOKEN`。
47
47
  - 输出:默认是人类可读格式;支持 `--json-output` 的命令会输出稳定 JSON,适合脚本消费。
48
48
 
49
49
  ### Token 来源
@@ -51,7 +51,7 @@ pip install -e ".[dev]"
51
51
  Token 解析顺序稳定为:
52
52
 
53
53
  1. 显式 `--token`。
54
- 2. repo-scoped git credential,路径为规范化后的 `owner/repo`,不带 `.git` 后缀。
54
+ 2. 当前仓库 `.git/config` 中的 repo-local HTTPS auth header,路径为规范化后的 `https://github.com/owner/repo.git`。
55
55
  3. typed env 中的 `GITHUB_ACCESS_TOKEN`。
56
56
 
57
57
  可以用 `chatenv` 查看或配置 typed env:
@@ -72,13 +72,14 @@ chatenv cat -t gh
72
72
  - `git@github.com:octocat/Hello-World.git`
73
73
  - `ssh://git@github.com/octocat/Hello-World.git`
74
74
 
75
- 写入 repo-scoped credential 时,path 会规范化为 `octocat/Hello-World`。
75
+ 写入 repo-local HTTPS auth header 时,path 会规范化为 `https://github.com/octocat/Hello-World.git`。
76
76
 
77
77
  ## CLI 入口
78
78
 
79
79
  ```bash
80
80
  chatgh --help
81
81
  chatgh pr --help
82
+ chatgh repo --help
82
83
  chatgh run --help
83
84
  chatgh repo-perms --help
84
85
  chatgh set-token --help
@@ -89,7 +90,9 @@ chatgh set-token --help
89
90
  - `chatgh pr list`:generated-layer PR 列表。
90
91
  - `chatgh pr view NUMBER`:generated-layer PR 详情。
91
92
  - `chatgh pr checks NUMBER`:generated-layer PR head commit check runs。
92
- - `chatgh pr create/comment/merge/edit`:PR 创建、评论、合并和编辑操作。
93
+ - 当前公开 `chatgh pr` 命令面只包含 `list/view/checks`;PR 创建、评论、合并和编辑流程仍由内部 helper 支持,后续作为独立 CLI 命令恢复前不在帮助与文档中承诺。
94
+ - `chatgh repo list`:列出 user/org 下的仓库;默认 table,支持 `--json-output`、`--limit`、`--sort updated|created|pushed|name|stars|open-prs|open-issues`、`--direction asc|desc`,字段包含 visibility、stars、open PRs、open issues、created/updated time 等。
95
+ - `chatgh repo create`:创建仓库;默认 private,可用 `--public` 显式创建公开仓库。
93
96
  - `chatgh run view`:查看 workflow run 和 jobs。
94
97
  - `chatgh run logs`:查看 job 日志,支持 tail 和落盘。
95
98
  - `chatgh repo-perms`:查看 token 权限和派生 capabilities。
@@ -99,26 +102,7 @@ chatgh set-token --help
99
102
 
100
103
  ### 创建 PR
101
104
 
102
- 优先使用 `--body-file`,避免 shell 转义破坏 Markdown:
103
-
104
- ```bash
105
- cat > /tmp/pr_body.md <<'EOF'
106
- ## Summary
107
- - migrate GitHub helpers to chatgh
108
-
109
- ## Testing
110
- - python -m pytest -q
111
- EOF
112
-
113
- chatgh pr create \
114
- --repo octocat/Hello-World \
115
- --base main \
116
- --head feature-branch \
117
- --title "feat: migrate github helpers" \
118
- --body-file /tmp/pr_body.md
119
- ```
120
-
121
- 缺少 `base` / `head` / `title` 且终端可交互时,命令会自动补问;显式 `-I` 会禁用补问。
105
+ 当前版本未公开 `chatgh pr create` CLI 命令。需要创建 PR 时,请使用项目既有 GitHub 流程或直接调用 GitHub API;等写操作命令恢复为稳定 CLI surface 后再补回示例。
122
106
 
123
107
  ### 查看 PR
124
108
 
@@ -135,12 +119,10 @@ chatgh pr view 123 --repo octocat/Hello-World --json-output
135
119
  - `mergeable` 和 `mergeable_state`。
136
120
  - created/updated/merged timestamps。
137
121
 
138
- ### 查看和等待 CI
122
+ ### 查看 CI
139
123
 
140
124
  ```bash
141
125
  chatgh pr checks 123 --repo octocat/Hello-World
142
- chatgh pr checks --repo octocat/Hello-World --number 123 --wait
143
- chatgh pr checks --repo octocat/Hello-World --number 123 --wait --interval 10 --timeout 600
144
126
  chatgh pr checks 123 --repo octocat/Hello-World --json-output
145
127
  ```
146
128
 
@@ -150,11 +132,7 @@ chatgh pr checks 123 --repo octocat/Hello-World --json-output
150
132
  - check runs
151
133
  - workflow runs
152
134
 
153
- `--wait` 会持续轮询直到 statuses、check runs 和 workflow runs 都结束:
154
-
155
- - 默认不设超时,会一直等。
156
- - `--interval <seconds>` 控制轮询间隔。
157
- - 只有显式传 `--timeout <seconds>` 时才会超时报错。
135
+ 当前公开 CLI 不提供 `--wait` / `--interval` / `--timeout` 参数;需要等待终态时,在外层流程中轮询 `chatgh pr checks`。
158
136
 
159
137
  如果 GitHub token 无权读取 check-runs API,命令会把 check-runs 错误放进 payload,同时仍尽量展示 combined status 和 workflow runs。
160
138
 
@@ -173,25 +151,7 @@ chatgh run logs --repo octocat/Hello-World --job-id 987654321 --tail 200 --outpu
173
151
 
174
152
  ### 评论、合并和编辑 PR
175
153
 
176
- ```bash
177
- chatgh pr comment --repo octocat/Hello-World --number 123 --body "Looks good"
178
-
179
- chatgh pr merge --repo octocat/Hello-World --number 123 --method squash
180
- chatgh pr merge --repo octocat/Hello-World --number 123 --method squash --check
181
-
182
- chatgh pr edit --repo octocat/Hello-World --number 123 --title "New title"
183
- chatgh pr edit --repo octocat/Hello-World --number 123 --body-file /tmp/pr_body.md
184
- chatgh pr edit --repo octocat/Hello-World --number 123 --state closed
185
- chatgh pr edit --repo octocat/Hello-World --number 123 --base main
186
- ```
187
-
188
- `pr merge --check` 会在合并前拒绝以下情况:
189
-
190
- - PR 当前 `mergeable=False`。
191
- - `mergeable_state` 为 `dirty`、`blocked`、`behind`、`draft` 或 `unknown`。
192
- - combined status、check runs 或 workflow runs 存在失败、取消或未完成项。
193
-
194
- 不带 `--check` 时,命令保持直接调用 GitHub merge API 的行为。
154
+ 当前版本未公开 `chatgh pr comment/merge/edit` CLI 命令。相关业务函数已保留在 `chatgh.github.commands` 中,后续恢复为稳定 CLI surface 前,建议通过项目既有流程或 GitHub API 执行写操作。
195
155
 
196
156
  ### 配置和检查 token
197
157
 
@@ -209,7 +169,14 @@ chatgh set-token --token "$GITHUB_ACCESS_TOKEN" --save-env
209
169
  - GitHub 返回的 `permissions`。
210
170
  - 派生 capabilities:`can_read_pr`、`can_comment_pr`、`can_merge_pr`、`can_view_checks`、`can_view_actions`。
211
171
 
212
- `set-token` 只在当前目录能识别 GitHub remote 时生效。默认只写入 repo-scoped git credential;传 `--save-env` 时会同步写入 typed env 的 `GITHUB_ACCESS_TOKEN`。
172
+ `set-token` 只在当前目录能识别 GitHub remote 时生效。默认只写入当前仓库自己的 `.git/config`:
173
+
174
+ ```ini
175
+ [http "https://github.com/octocat/Hello-World.git"]
176
+ extraHeader = Authorization: Basic <base64(x-access-token:TOKEN)>
177
+ ```
178
+
179
+ 不要把 token 写进 remote URL,也不要把 raw `extraHeader` 输出到日志。传 `--save-env` 时会同步写入 typed env 的 `GITHUB_ACCESS_TOKEN`。
213
180
 
214
181
  ## 交互模式
215
182
 
@@ -233,7 +200,7 @@ git fetch origin main
233
200
 
234
201
  - `chatgh pr view` / `chatgh pr checks` 显示 `mergeable` 不是 `False`,`mergeable_state` 不是 `dirty`。
235
202
  - 本地基于最新 base 做过 merge 或 rebase 演练,并在该结果上跑过最相关测试。
236
- - CI 需要终态时用 `chatgh pr checks --wait`,不要只看一次快照。
203
+ - CI 需要终态时,在外层流程中轮询 `chatgh pr checks`,不要只看一次快照。
237
204
 
238
205
  ## Python API
239
206
 
@@ -15,7 +15,7 @@ pip install -e ".[dev]"
15
15
  默认行为:
16
16
 
17
17
  - `repo`:优先使用显式 `--repo owner/repo`,未传时从当前 git remote 推断。
18
- - `token`:优先使用显式 `--token`,其次读取当前仓库对应的 git credential,再回退 typed env 里的 `GITHUB_ACCESS_TOKEN`。
18
+ - `token`:优先使用显式 `--token`,其次读取当前仓库 `.git/config` 中 repo-local HTTPS auth header,再回退 typed env 里的 `GITHUB_ACCESS_TOKEN`。
19
19
  - 输出:默认是人类可读格式;支持 `--json-output` 的命令会输出稳定 JSON,适合脚本消费。
20
20
 
21
21
  ### Token 来源
@@ -23,7 +23,7 @@ pip install -e ".[dev]"
23
23
  Token 解析顺序稳定为:
24
24
 
25
25
  1. 显式 `--token`。
26
- 2. repo-scoped git credential,路径为规范化后的 `owner/repo`,不带 `.git` 后缀。
26
+ 2. 当前仓库 `.git/config` 中的 repo-local HTTPS auth header,路径为规范化后的 `https://github.com/owner/repo.git`。
27
27
  3. typed env 中的 `GITHUB_ACCESS_TOKEN`。
28
28
 
29
29
  可以用 `chatenv` 查看或配置 typed env:
@@ -44,13 +44,14 @@ chatenv cat -t gh
44
44
  - `git@github.com:octocat/Hello-World.git`
45
45
  - `ssh://git@github.com/octocat/Hello-World.git`
46
46
 
47
- 写入 repo-scoped credential 时,path 会规范化为 `octocat/Hello-World`。
47
+ 写入 repo-local HTTPS auth header 时,path 会规范化为 `https://github.com/octocat/Hello-World.git`。
48
48
 
49
49
  ## CLI 入口
50
50
 
51
51
  ```bash
52
52
  chatgh --help
53
53
  chatgh pr --help
54
+ chatgh repo --help
54
55
  chatgh run --help
55
56
  chatgh repo-perms --help
56
57
  chatgh set-token --help
@@ -61,7 +62,9 @@ chatgh set-token --help
61
62
  - `chatgh pr list`:generated-layer PR 列表。
62
63
  - `chatgh pr view NUMBER`:generated-layer PR 详情。
63
64
  - `chatgh pr checks NUMBER`:generated-layer PR head commit check runs。
64
- - `chatgh pr create/comment/merge/edit`:PR 创建、评论、合并和编辑操作。
65
+ - 当前公开 `chatgh pr` 命令面只包含 `list/view/checks`;PR 创建、评论、合并和编辑流程仍由内部 helper 支持,后续作为独立 CLI 命令恢复前不在帮助与文档中承诺。
66
+ - `chatgh repo list`:列出 user/org 下的仓库;默认 table,支持 `--json-output`、`--limit`、`--sort updated|created|pushed|name|stars|open-prs|open-issues`、`--direction asc|desc`,字段包含 visibility、stars、open PRs、open issues、created/updated time 等。
67
+ - `chatgh repo create`:创建仓库;默认 private,可用 `--public` 显式创建公开仓库。
65
68
  - `chatgh run view`:查看 workflow run 和 jobs。
66
69
  - `chatgh run logs`:查看 job 日志,支持 tail 和落盘。
67
70
  - `chatgh repo-perms`:查看 token 权限和派生 capabilities。
@@ -71,26 +74,7 @@ chatgh set-token --help
71
74
 
72
75
  ### 创建 PR
73
76
 
74
- 优先使用 `--body-file`,避免 shell 转义破坏 Markdown:
75
-
76
- ```bash
77
- cat > /tmp/pr_body.md <<'EOF'
78
- ## Summary
79
- - migrate GitHub helpers to chatgh
80
-
81
- ## Testing
82
- - python -m pytest -q
83
- EOF
84
-
85
- chatgh pr create \
86
- --repo octocat/Hello-World \
87
- --base main \
88
- --head feature-branch \
89
- --title "feat: migrate github helpers" \
90
- --body-file /tmp/pr_body.md
91
- ```
92
-
93
- 缺少 `base` / `head` / `title` 且终端可交互时,命令会自动补问;显式 `-I` 会禁用补问。
77
+ 当前版本未公开 `chatgh pr create` CLI 命令。需要创建 PR 时,请使用项目既有 GitHub 流程或直接调用 GitHub API;等写操作命令恢复为稳定 CLI surface 后再补回示例。
94
78
 
95
79
  ### 查看 PR
96
80
 
@@ -107,12 +91,10 @@ chatgh pr view 123 --repo octocat/Hello-World --json-output
107
91
  - `mergeable` 和 `mergeable_state`。
108
92
  - created/updated/merged timestamps。
109
93
 
110
- ### 查看和等待 CI
94
+ ### 查看 CI
111
95
 
112
96
  ```bash
113
97
  chatgh pr checks 123 --repo octocat/Hello-World
114
- chatgh pr checks --repo octocat/Hello-World --number 123 --wait
115
- chatgh pr checks --repo octocat/Hello-World --number 123 --wait --interval 10 --timeout 600
116
98
  chatgh pr checks 123 --repo octocat/Hello-World --json-output
117
99
  ```
118
100
 
@@ -122,11 +104,7 @@ chatgh pr checks 123 --repo octocat/Hello-World --json-output
122
104
  - check runs
123
105
  - workflow runs
124
106
 
125
- `--wait` 会持续轮询直到 statuses、check runs 和 workflow runs 都结束:
126
-
127
- - 默认不设超时,会一直等。
128
- - `--interval <seconds>` 控制轮询间隔。
129
- - 只有显式传 `--timeout <seconds>` 时才会超时报错。
107
+ 当前公开 CLI 不提供 `--wait` / `--interval` / `--timeout` 参数;需要等待终态时,在外层流程中轮询 `chatgh pr checks`。
130
108
 
131
109
  如果 GitHub token 无权读取 check-runs API,命令会把 check-runs 错误放进 payload,同时仍尽量展示 combined status 和 workflow runs。
132
110
 
@@ -145,25 +123,7 @@ chatgh run logs --repo octocat/Hello-World --job-id 987654321 --tail 200 --outpu
145
123
 
146
124
  ### 评论、合并和编辑 PR
147
125
 
148
- ```bash
149
- chatgh pr comment --repo octocat/Hello-World --number 123 --body "Looks good"
150
-
151
- chatgh pr merge --repo octocat/Hello-World --number 123 --method squash
152
- chatgh pr merge --repo octocat/Hello-World --number 123 --method squash --check
153
-
154
- chatgh pr edit --repo octocat/Hello-World --number 123 --title "New title"
155
- chatgh pr edit --repo octocat/Hello-World --number 123 --body-file /tmp/pr_body.md
156
- chatgh pr edit --repo octocat/Hello-World --number 123 --state closed
157
- chatgh pr edit --repo octocat/Hello-World --number 123 --base main
158
- ```
159
-
160
- `pr merge --check` 会在合并前拒绝以下情况:
161
-
162
- - PR 当前 `mergeable=False`。
163
- - `mergeable_state` 为 `dirty`、`blocked`、`behind`、`draft` 或 `unknown`。
164
- - combined status、check runs 或 workflow runs 存在失败、取消或未完成项。
165
-
166
- 不带 `--check` 时,命令保持直接调用 GitHub merge API 的行为。
126
+ 当前版本未公开 `chatgh pr comment/merge/edit` CLI 命令。相关业务函数已保留在 `chatgh.github.commands` 中,后续恢复为稳定 CLI surface 前,建议通过项目既有流程或 GitHub API 执行写操作。
167
127
 
168
128
  ### 配置和检查 token
169
129
 
@@ -181,7 +141,14 @@ chatgh set-token --token "$GITHUB_ACCESS_TOKEN" --save-env
181
141
  - GitHub 返回的 `permissions`。
182
142
  - 派生 capabilities:`can_read_pr`、`can_comment_pr`、`can_merge_pr`、`can_view_checks`、`can_view_actions`。
183
143
 
184
- `set-token` 只在当前目录能识别 GitHub remote 时生效。默认只写入 repo-scoped git credential;传 `--save-env` 时会同步写入 typed env 的 `GITHUB_ACCESS_TOKEN`。
144
+ `set-token` 只在当前目录能识别 GitHub remote 时生效。默认只写入当前仓库自己的 `.git/config`:
145
+
146
+ ```ini
147
+ [http "https://github.com/octocat/Hello-World.git"]
148
+ extraHeader = Authorization: Basic <base64(x-access-token:TOKEN)>
149
+ ```
150
+
151
+ 不要把 token 写进 remote URL,也不要把 raw `extraHeader` 输出到日志。传 `--save-env` 时会同步写入 typed env 的 `GITHUB_ACCESS_TOKEN`。
185
152
 
186
153
  ## 交互模式
187
154
 
@@ -205,7 +172,7 @@ git fetch origin main
205
172
 
206
173
  - `chatgh pr view` / `chatgh pr checks` 显示 `mergeable` 不是 `False`,`mergeable_state` 不是 `dirty`。
207
174
  - 本地基于最新 base 做过 merge 或 rebase 演练,并在该结果上跑过最相关测试。
208
- - CI 需要终态时用 `chatgh pr checks --wait`,不要只看一次快照。
175
+ - CI 需要终态时,在外层流程中轮询 `chatgh pr checks`,不要只看一次快照。
209
176
 
210
177
  ## Python API
211
178
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.2"
5
+ __version__ = "0.2.3"
@@ -3,7 +3,7 @@
3
3
  import click
4
4
 
5
5
  from chatgh.commands.pr import pr_group
6
- from chatgh.github.cli import repo_permissions, run_group, set_repo_token
6
+ from chatgh.github.cli import repo_group, repo_permissions, run_group, set_repo_token
7
7
 
8
8
 
9
9
  @click.group()
@@ -12,6 +12,7 @@ def main() -> None:
12
12
 
13
13
 
14
14
  main.add_command(pr_group, name="pr")
15
+ main.add_command(repo_group, name="repo")
15
16
  main.add_command(run_group, name="run")
16
17
  main.add_command(repo_permissions, name="repo-perms")
17
18
  main.add_command(set_repo_token, name="set-token")
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  from urllib.parse import urlparse
4
5
  import subprocess
5
6
  from typing import Optional, TypedDict
@@ -74,7 +75,7 @@ def resolve_token_with_source(
74
75
  credential_path, exact_only=exact_only
75
76
  )
76
77
  if credential_token:
77
- return {"token": credential_token, "source": "git credential"}
78
+ return {"token": credential_token, "source": "repo git config"}
78
79
  credential_token = read_github_token_from_credentials(
79
80
  credential_path, exact_only=exact_only
80
81
  )
@@ -212,20 +213,22 @@ def read_github_token_from_git(
212
213
  ) -> Optional[str]:
213
214
  if not credential_path:
214
215
  return None
215
- return _git_credential_fill(credential_path)
216
+ return _read_repo_local_extraheader(credential_path)
216
217
 
217
218
 
218
- def _git_credential_fill(credential: CredentialQuery) -> Optional[str]:
219
- credential_input = (
220
- f"protocol={credential['protocol']}\n"
221
- f"host={credential['host']}\n"
222
- f"path={credential['path']}\n"
223
- )
224
- credential_input += "\n"
219
+ def _github_https_url(credential: CredentialQuery) -> str:
220
+ path = credential["path"].strip().removeprefix("/").removesuffix(".git")
221
+ return f"https://{credential['host']}/{path}.git"
222
+
223
+
224
+ def _extraheader_config_key(credential: CredentialQuery) -> str:
225
+ return f"http.{_github_https_url(credential)}.extraHeader"
226
+
227
+
228
+ def _read_repo_local_extraheader(credential: CredentialQuery) -> Optional[str]:
225
229
  try:
226
230
  result = subprocess.run(
227
- ["git", "credential", "fill"],
228
- input=credential_input,
231
+ ["git", "config", "--local", "--get", _extraheader_config_key(credential)],
229
232
  check=False,
230
233
  capture_output=True,
231
234
  text=True,
@@ -234,41 +237,30 @@ def _git_credential_fill(credential: CredentialQuery) -> Optional[str]:
234
237
  return None
235
238
  if result.returncode != 0:
236
239
  return None
237
- values: dict[str, str] = {}
238
- for line in result.stdout.splitlines():
239
- if "=" not in line:
240
- continue
241
- key, value = line.split("=", 1)
242
- values[key] = value
243
- if values.get("host") != credential["host"]:
240
+ return _token_from_extraheader(result.stdout.strip())
241
+
242
+
243
+ def _token_from_extraheader(header: str) -> Optional[str]:
244
+ prefix = "Authorization: Basic "
245
+ if not header.startswith(prefix):
246
+ return None
247
+ encoded = header[len(prefix) :].strip()
248
+ try:
249
+ decoded = base64.b64decode(encoded).decode("utf-8")
250
+ except Exception:
251
+ return None
252
+ username, sep, password = decoded.partition(":")
253
+ if sep != ":" or username != "x-access-token" or not password:
244
254
  return None
245
- return values.get("password") or None
255
+ return password
246
256
 
247
257
 
248
258
  def configure_github_https_token(credential: CredentialQuery, token: str) -> None:
249
259
  try:
260
+ raw = f"x-access-token:{token}".encode("utf-8")
261
+ header = "Authorization: Basic " + base64.b64encode(raw).decode("ascii")
250
262
  subprocess.run(
251
- ["git", "config", "--global", "credential.helper", "store"],
252
- check=True,
253
- capture_output=True,
254
- text=True,
255
- )
256
- subprocess.run(
257
- ["git", "config", "--global", "credential.useHttpPath", "true"],
258
- check=True,
259
- capture_output=True,
260
- text=True,
261
- )
262
- credential_input = (
263
- f"protocol={credential['protocol']}\n"
264
- f"host={credential['host']}\n"
265
- f"path={credential['path']}\n"
266
- "username=x-access-token\n"
267
- f"password={token}\n\n"
268
- )
269
- subprocess.run(
270
- ["git", "credential", "approve"],
271
- input=credential_input,
263
+ ["git", "config", "--local", _extraheader_config_key(credential), header],
272
264
  check=True,
273
265
  capture_output=True,
274
266
  text=True,
@@ -14,7 +14,7 @@ from chatstyle.core.interactive import is_interactive_available
14
14
  from chatstyle.tui.prompt import BACK_VALUE, ask_text
15
15
 
16
16
  from chatgh.github.api import resolve_token
17
- from chatgh.github.commands import repo_perms, set_token, view_job_logs, view_run
17
+ from chatgh.github.commands import create_repo, list_repos, repo_perms, set_token, view_job_logs, view_run
18
18
  from chatgh.github.render import echo_workflow_job, echo_workflow_run, format_optional
19
19
 
20
20
 
@@ -44,6 +44,85 @@ def run_group() -> None:
44
44
  """GitHub Actions helpers."""
45
45
 
46
46
 
47
+ @cli.group(name="repo")
48
+ def repo_group() -> None:
49
+ """Repository helpers."""
50
+
51
+
52
+ @repo_group.command(name="list")
53
+ @click.option("--owner", required=True, help="GitHub owner or organization.")
54
+ @click.option("--limit", default=50, type=click.IntRange(min=1), show_default=True)
55
+ @click.option(
56
+ "--sort",
57
+ type=click.Choice(["updated", "created", "pushed", "name", "stars", "open-prs", "open-issues"]),
58
+ default="updated",
59
+ show_default=True,
60
+ help="Sort repositories before applying --limit.",
61
+ )
62
+ @click.option("--direction", type=click.Choice(["asc", "desc"]), default="desc", show_default=True)
63
+ @click.option("--json-output", is_flag=True, help="Output JSON.")
64
+ @click.option("--token", default=None, help="GitHub token.")
65
+ def repo_list(owner, limit, sort, direction, json_output, token):
66
+ """List repositories for an owner or organization."""
67
+ payload = list_repos(owner, limit, sort, direction, token)
68
+ if json_output:
69
+ click.echo(json.dumps(payload, ensure_ascii=False, indent=2))
70
+ return
71
+ _echo_repo_table(payload)
72
+
73
+
74
+ def _echo_repo_table(items: list[dict]) -> None:
75
+ columns = [
76
+ ("repo", "full_name"),
77
+ ("vis", "visibility"),
78
+ ("stars", "stars"),
79
+ ("prs", "open_prs"),
80
+ ("issues", "open_issues"),
81
+ ("updated", "updated_at"),
82
+ ("created", "created_at"),
83
+ ]
84
+ rows = []
85
+ for item in items:
86
+ row = []
87
+ for _, key in columns:
88
+ value = item.get(key)
89
+ if key.endswith("_at"):
90
+ value = str(value or "")[:10]
91
+ row.append(str(value if value is not None else ""))
92
+ rows.append(row)
93
+ widths = [len(label) for label, _ in columns]
94
+ for row in rows:
95
+ for index, value in enumerate(row):
96
+ widths[index] = min(max(widths[index], len(value)), 48)
97
+ header = " ".join(label.ljust(widths[index]) for index, (label, _) in enumerate(columns))
98
+ click.echo(header)
99
+ click.echo(" ".join("-" * width for width in widths))
100
+ for row in rows:
101
+ clipped = [value[: widths[index]] for index, value in enumerate(row)]
102
+ click.echo(" ".join(value.ljust(widths[index]) for index, value in enumerate(clipped)))
103
+
104
+
105
+ @repo_group.command(name="create")
106
+ @click.option("--owner", required=True, help="GitHub owner or organization.")
107
+ @click.option("--name", required=True, help="Repository name.")
108
+ @click.option("--description", default=None, help="Repository description.")
109
+ @click.option("--public", "public_repo", is_flag=True, help="Create a public repository. Defaults to private.")
110
+ @click.option("--if-exists", type=click.Choice(["error", "use"]), default="error", show_default=True)
111
+ @click.option("--json-output", is_flag=True, help="Output JSON.")
112
+ @click.option("--token", default=None, help="GitHub token.")
113
+ def repo_create(owner, name, description, public_repo, if_exists, json_output, token):
114
+ """Create a repository. Repositories are private by default."""
115
+ payload = create_repo(owner, name, not public_repo, description, if_exists, token)
116
+ if json_output:
117
+ click.echo(json.dumps(payload, ensure_ascii=False, indent=2))
118
+ return
119
+ created = "created" if payload.get("created") else "existing"
120
+ private = "private" if payload.get("private") else "public"
121
+ click.echo(f"{created}: {payload['full_name']} ({private})")
122
+ if payload.get("html_url"):
123
+ click.echo(payload["html_url"])
124
+
125
+
47
126
  @run_group.command(name="view")
48
127
  @click.option("--repo", required=False, help="Repository in owner/name form.")
49
128
  @click.option("--run-id", required=False, type=int, help="Workflow run id.")
@@ -27,12 +27,14 @@ from chatgh.github.requests import (
27
27
  get_pr_checks,
28
28
  get_pr_list,
29
29
  get_pr_view,
30
+ get_repo_list,
30
31
  get_repo_permissions,
31
32
  get_run_view,
32
33
  patch_pr_edit,
33
34
  post_pr_comment,
34
35
  post_pr_create,
35
36
  post_pr_merge,
37
+ post_repo_create,
36
38
  )
37
39
 
38
40
 
@@ -43,6 +45,34 @@ def resolve_repo_and_credential_path(repo: Optional[str]) -> tuple[str, Credenti
43
45
  return resolve_repo_from_git_remote()
44
46
 
45
47
 
48
+ def list_repos(
49
+ owner: str,
50
+ limit: int,
51
+ sort: str,
52
+ direction: str,
53
+ token: Optional[str],
54
+ ) -> list[dict]:
55
+ credential_path = credential_path_from_repo(f"{owner}/_")
56
+ client = get_client(token, require_token=True, credential_path=credential_path)
57
+ return get_repo_list(client, owner, limit, sort, direction)
58
+
59
+
60
+ def create_repo(
61
+ owner: str,
62
+ name: str,
63
+ private: bool,
64
+ description: Optional[str],
65
+ if_exists: str,
66
+ token: Optional[str],
67
+ ) -> dict:
68
+ credential_path = credential_path_from_repo(f"{owner}/{name}")
69
+ client = get_client(token, require_token=True, credential_path=credential_path)
70
+ try:
71
+ return post_repo_create(client, owner, name, private, description, if_exists)
72
+ except ValueError as exc:
73
+ raise click.ClickException(str(exc)) from exc
74
+
75
+
46
76
  def create_pr(
47
77
  repo: Optional[str],
48
78
  base: str,
@@ -1,10 +1,61 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from datetime import datetime, timezone
3
4
  from typing import Optional
4
5
 
5
6
  from chatgh.github.api import github_api_get_json, github_api_get_text
6
7
 
7
8
 
9
+ def get_repo_list(
10
+ client,
11
+ owner: str,
12
+ limit: int,
13
+ sort: str,
14
+ direction: str,
15
+ ) -> list[dict]:
16
+ owner_obj = _get_owner(client, owner)
17
+ items: list[dict] = []
18
+ for repo in owner_obj.get_repos():
19
+ items.append(_build_repo_payload(repo))
20
+ items.sort(
21
+ key=lambda item: _repo_sort_key(item, sort),
22
+ reverse=(direction == "desc"),
23
+ )
24
+ return items[:limit]
25
+
26
+
27
+ def post_repo_create(
28
+ client,
29
+ owner: str,
30
+ name: str,
31
+ private: bool,
32
+ description: Optional[str],
33
+ if_exists: str,
34
+ ) -> dict:
35
+ full_name = f"{owner}/{name}"
36
+ try:
37
+ existing = client.get_repo(full_name)
38
+ except Exception as exc:
39
+ if getattr(exc, "status", None) != 404:
40
+ raise
41
+ else:
42
+ if if_exists == "use":
43
+ payload = _build_repo_payload(existing)
44
+ payload["created"] = False
45
+ return payload
46
+ raise ValueError(f"Repository already exists: {full_name}")
47
+
48
+ owner_obj = _get_owner(client, owner)
49
+ repo = owner_obj.create_repo(
50
+ name=name,
51
+ private=private,
52
+ description=description or "",
53
+ )
54
+ payload = _build_repo_payload(repo)
55
+ payload["created"] = True
56
+ return payload
57
+
58
+
8
59
  def get_pr_list(client, repo: str, state: str, limit: int) -> list[dict]:
9
60
  repo_obj = client.get_repo(repo)
10
61
  items: list[dict] = []
@@ -21,6 +72,70 @@ def get_pr_view(client, repo: str, number: int) -> dict:
21
72
  return _build_pr_view_payload(pr)
22
73
 
23
74
 
75
+ def _get_owner(client, owner: str):
76
+ try:
77
+ return client.get_organization(owner)
78
+ except Exception as exc:
79
+ if getattr(exc, "status", None) not in {403, 404}:
80
+ raise
81
+ return client.get_user(owner)
82
+
83
+
84
+ def _build_repo_payload(repo) -> dict:
85
+ open_prs = _safe_count(lambda: repo.get_pulls(state="open"))
86
+ open_issues_reported = int(getattr(repo, "open_issues_count", 0) or 0)
87
+ open_issues = max(open_issues_reported - open_prs, 0) if open_prs is not None else None
88
+ return {
89
+ "name": repo.name,
90
+ "full_name": repo.full_name,
91
+ "private": repo.private,
92
+ "visibility": getattr(repo, "visibility", None) or ("private" if repo.private else "public"),
93
+ "stars": int(getattr(repo, "stargazers_count", 0) or 0),
94
+ "forks": int(getattr(repo, "forks_count", 0) or 0),
95
+ "open_prs": open_prs,
96
+ "open_issues": open_issues,
97
+ "open_issues_reported": open_issues_reported,
98
+ "archived": bool(getattr(repo, "archived", False)),
99
+ "fork": bool(getattr(repo, "fork", False)),
100
+ "created_at": _isoformat(getattr(repo, "created_at", None)),
101
+ "updated_at": _isoformat(getattr(repo, "updated_at", None)),
102
+ "pushed_at": _isoformat(getattr(repo, "pushed_at", None)),
103
+ "html_url": repo.html_url,
104
+ "clone_url": getattr(repo, "clone_url", None),
105
+ "ssh_url": getattr(repo, "ssh_url", None),
106
+ "default_branch": getattr(repo, "default_branch", None),
107
+ "description": getattr(repo, "description", None),
108
+ }
109
+
110
+
111
+ def _safe_count(factory) -> Optional[int]:
112
+ try:
113
+ return int(factory().totalCount)
114
+ except Exception:
115
+ return None
116
+
117
+
118
+ def _repo_sort_key(item: dict, sort: str):
119
+ if sort in {"updated", "created", "pushed"}:
120
+ return _parse_time(item.get(f"{sort}_at"))
121
+ if sort == "stars":
122
+ return int(item.get("stars") or 0)
123
+ if sort == "open-prs":
124
+ return int(item.get("open_prs") or 0)
125
+ if sort == "open-issues":
126
+ return int(item.get("open_issues") or 0)
127
+ return str(item.get("name") or item.get("full_name") or "").lower()
128
+
129
+
130
+ def _parse_time(value: Optional[str]) -> datetime:
131
+ if not value:
132
+ return datetime.min.replace(tzinfo=timezone.utc)
133
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
134
+ if parsed.tzinfo is None:
135
+ return parsed.replace(tzinfo=timezone.utc)
136
+ return parsed
137
+
138
+
24
139
  def get_pr_checks(
25
140
  client,
26
141
  repo: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatgh
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: GitHub CLI and Python helpers for ChatArch
5
5
  Author-email: ChatArch <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -43,7 +43,7 @@ pip install -e ".[dev]"
43
43
  默认行为:
44
44
 
45
45
  - `repo`:优先使用显式 `--repo owner/repo`,未传时从当前 git remote 推断。
46
- - `token`:优先使用显式 `--token`,其次读取当前仓库对应的 git credential,再回退 typed env 里的 `GITHUB_ACCESS_TOKEN`。
46
+ - `token`:优先使用显式 `--token`,其次读取当前仓库 `.git/config` 中 repo-local HTTPS auth header,再回退 typed env 里的 `GITHUB_ACCESS_TOKEN`。
47
47
  - 输出:默认是人类可读格式;支持 `--json-output` 的命令会输出稳定 JSON,适合脚本消费。
48
48
 
49
49
  ### Token 来源
@@ -51,7 +51,7 @@ pip install -e ".[dev]"
51
51
  Token 解析顺序稳定为:
52
52
 
53
53
  1. 显式 `--token`。
54
- 2. repo-scoped git credential,路径为规范化后的 `owner/repo`,不带 `.git` 后缀。
54
+ 2. 当前仓库 `.git/config` 中的 repo-local HTTPS auth header,路径为规范化后的 `https://github.com/owner/repo.git`。
55
55
  3. typed env 中的 `GITHUB_ACCESS_TOKEN`。
56
56
 
57
57
  可以用 `chatenv` 查看或配置 typed env:
@@ -72,13 +72,14 @@ chatenv cat -t gh
72
72
  - `git@github.com:octocat/Hello-World.git`
73
73
  - `ssh://git@github.com/octocat/Hello-World.git`
74
74
 
75
- 写入 repo-scoped credential 时,path 会规范化为 `octocat/Hello-World`。
75
+ 写入 repo-local HTTPS auth header 时,path 会规范化为 `https://github.com/octocat/Hello-World.git`。
76
76
 
77
77
  ## CLI 入口
78
78
 
79
79
  ```bash
80
80
  chatgh --help
81
81
  chatgh pr --help
82
+ chatgh repo --help
82
83
  chatgh run --help
83
84
  chatgh repo-perms --help
84
85
  chatgh set-token --help
@@ -89,7 +90,9 @@ chatgh set-token --help
89
90
  - `chatgh pr list`:generated-layer PR 列表。
90
91
  - `chatgh pr view NUMBER`:generated-layer PR 详情。
91
92
  - `chatgh pr checks NUMBER`:generated-layer PR head commit check runs。
92
- - `chatgh pr create/comment/merge/edit`:PR 创建、评论、合并和编辑操作。
93
+ - 当前公开 `chatgh pr` 命令面只包含 `list/view/checks`;PR 创建、评论、合并和编辑流程仍由内部 helper 支持,后续作为独立 CLI 命令恢复前不在帮助与文档中承诺。
94
+ - `chatgh repo list`:列出 user/org 下的仓库;默认 table,支持 `--json-output`、`--limit`、`--sort updated|created|pushed|name|stars|open-prs|open-issues`、`--direction asc|desc`,字段包含 visibility、stars、open PRs、open issues、created/updated time 等。
95
+ - `chatgh repo create`:创建仓库;默认 private,可用 `--public` 显式创建公开仓库。
93
96
  - `chatgh run view`:查看 workflow run 和 jobs。
94
97
  - `chatgh run logs`:查看 job 日志,支持 tail 和落盘。
95
98
  - `chatgh repo-perms`:查看 token 权限和派生 capabilities。
@@ -99,26 +102,7 @@ chatgh set-token --help
99
102
 
100
103
  ### 创建 PR
101
104
 
102
- 优先使用 `--body-file`,避免 shell 转义破坏 Markdown:
103
-
104
- ```bash
105
- cat > /tmp/pr_body.md <<'EOF'
106
- ## Summary
107
- - migrate GitHub helpers to chatgh
108
-
109
- ## Testing
110
- - python -m pytest -q
111
- EOF
112
-
113
- chatgh pr create \
114
- --repo octocat/Hello-World \
115
- --base main \
116
- --head feature-branch \
117
- --title "feat: migrate github helpers" \
118
- --body-file /tmp/pr_body.md
119
- ```
120
-
121
- 缺少 `base` / `head` / `title` 且终端可交互时,命令会自动补问;显式 `-I` 会禁用补问。
105
+ 当前版本未公开 `chatgh pr create` CLI 命令。需要创建 PR 时,请使用项目既有 GitHub 流程或直接调用 GitHub API;等写操作命令恢复为稳定 CLI surface 后再补回示例。
122
106
 
123
107
  ### 查看 PR
124
108
 
@@ -135,12 +119,10 @@ chatgh pr view 123 --repo octocat/Hello-World --json-output
135
119
  - `mergeable` 和 `mergeable_state`。
136
120
  - created/updated/merged timestamps。
137
121
 
138
- ### 查看和等待 CI
122
+ ### 查看 CI
139
123
 
140
124
  ```bash
141
125
  chatgh pr checks 123 --repo octocat/Hello-World
142
- chatgh pr checks --repo octocat/Hello-World --number 123 --wait
143
- chatgh pr checks --repo octocat/Hello-World --number 123 --wait --interval 10 --timeout 600
144
126
  chatgh pr checks 123 --repo octocat/Hello-World --json-output
145
127
  ```
146
128
 
@@ -150,11 +132,7 @@ chatgh pr checks 123 --repo octocat/Hello-World --json-output
150
132
  - check runs
151
133
  - workflow runs
152
134
 
153
- `--wait` 会持续轮询直到 statuses、check runs 和 workflow runs 都结束:
154
-
155
- - 默认不设超时,会一直等。
156
- - `--interval <seconds>` 控制轮询间隔。
157
- - 只有显式传 `--timeout <seconds>` 时才会超时报错。
135
+ 当前公开 CLI 不提供 `--wait` / `--interval` / `--timeout` 参数;需要等待终态时,在外层流程中轮询 `chatgh pr checks`。
158
136
 
159
137
  如果 GitHub token 无权读取 check-runs API,命令会把 check-runs 错误放进 payload,同时仍尽量展示 combined status 和 workflow runs。
160
138
 
@@ -173,25 +151,7 @@ chatgh run logs --repo octocat/Hello-World --job-id 987654321 --tail 200 --outpu
173
151
 
174
152
  ### 评论、合并和编辑 PR
175
153
 
176
- ```bash
177
- chatgh pr comment --repo octocat/Hello-World --number 123 --body "Looks good"
178
-
179
- chatgh pr merge --repo octocat/Hello-World --number 123 --method squash
180
- chatgh pr merge --repo octocat/Hello-World --number 123 --method squash --check
181
-
182
- chatgh pr edit --repo octocat/Hello-World --number 123 --title "New title"
183
- chatgh pr edit --repo octocat/Hello-World --number 123 --body-file /tmp/pr_body.md
184
- chatgh pr edit --repo octocat/Hello-World --number 123 --state closed
185
- chatgh pr edit --repo octocat/Hello-World --number 123 --base main
186
- ```
187
-
188
- `pr merge --check` 会在合并前拒绝以下情况:
189
-
190
- - PR 当前 `mergeable=False`。
191
- - `mergeable_state` 为 `dirty`、`blocked`、`behind`、`draft` 或 `unknown`。
192
- - combined status、check runs 或 workflow runs 存在失败、取消或未完成项。
193
-
194
- 不带 `--check` 时,命令保持直接调用 GitHub merge API 的行为。
154
+ 当前版本未公开 `chatgh pr comment/merge/edit` CLI 命令。相关业务函数已保留在 `chatgh.github.commands` 中,后续恢复为稳定 CLI surface 前,建议通过项目既有流程或 GitHub API 执行写操作。
195
155
 
196
156
  ### 配置和检查 token
197
157
 
@@ -209,7 +169,14 @@ chatgh set-token --token "$GITHUB_ACCESS_TOKEN" --save-env
209
169
  - GitHub 返回的 `permissions`。
210
170
  - 派生 capabilities:`can_read_pr`、`can_comment_pr`、`can_merge_pr`、`can_view_checks`、`can_view_actions`。
211
171
 
212
- `set-token` 只在当前目录能识别 GitHub remote 时生效。默认只写入 repo-scoped git credential;传 `--save-env` 时会同步写入 typed env 的 `GITHUB_ACCESS_TOKEN`。
172
+ `set-token` 只在当前目录能识别 GitHub remote 时生效。默认只写入当前仓库自己的 `.git/config`:
173
+
174
+ ```ini
175
+ [http "https://github.com/octocat/Hello-World.git"]
176
+ extraHeader = Authorization: Basic <base64(x-access-token:TOKEN)>
177
+ ```
178
+
179
+ 不要把 token 写进 remote URL,也不要把 raw `extraHeader` 输出到日志。传 `--save-env` 时会同步写入 typed env 的 `GITHUB_ACCESS_TOKEN`。
213
180
 
214
181
  ## 交互模式
215
182
 
@@ -233,7 +200,7 @@ git fetch origin main
233
200
 
234
201
  - `chatgh pr view` / `chatgh pr checks` 显示 `mergeable` 不是 `False`,`mergeable_state` 不是 `dirty`。
235
202
  - 本地基于最新 base 做过 merge 或 rebase 演练,并在该结果上跑过最相关测试。
236
- - CI 需要终态时用 `chatgh pr checks --wait`,不要只看一次快照。
203
+ - CI 需要终态时,在外层流程中轮询 `chatgh pr checks`,不要只看一次快照。
237
204
 
238
205
  ## Python API
239
206
 
@@ -2,4 +2,4 @@ from chatgh import __version__
2
2
 
3
3
 
4
4
  def test_version_present():
5
- assert __version__ == "0.2.2"
5
+ assert __version__ == "0.2.3"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes