-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_ops.py
More file actions
129 lines (110 loc) · 4.29 KB
/
Copy pathgithub_ops.py
File metadata and controls
129 lines (110 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import logging
from datetime import datetime, timezone
from typing import Optional
from github import Github, GithubException
log = logging.getLogger("ccloop")
BOT_MARKER = "<!-- ccloop:auto -->"
_BOT_EMOJI_PREFIXES = ("🔄", "✅", "⚠️")
def _is_bot_comment(body: str) -> bool:
"""判断是否为 ccloop 自动评论"""
text = (body or "").strip()
if BOT_MARKER in text:
return True
return any(text.startswith(p) for p in _BOT_EMOJI_PREFIXES)
class GitHubOps:
def __init__(self, token: str, repo_name: str, default_branch: str,
processed_label: str, dry_run: bool = False):
self.dry_run = dry_run
self._token = token
self._repo_name = repo_name
self.default_branch = default_branch
self.processed_label = processed_label
self._gh = None
self._repo = None
@property
def gh(self):
if self._gh is None:
self._gh = Github(self._token)
return self._gh
@property
def repo(self):
if self._repo is None:
self._repo = self.gh.get_repo(self._repo_name)
return self._repo
def get_open_issues(self, label: str = None) -> list:
labels = [label] if label else []
issues = self.repo.get_issues(state="open", labels=labels)
return [i for i in issues if not i.pull_request]
def get_latest_user_comment(self, issue) -> str:
"""获取 issue 中最新一条用户评论(过滤机器人评论)"""
for comment in reversed(list(issue.get_comments())):
body = (comment.body or "").strip()
if not _is_bot_comment(body):
return body
return ""
def has_new_comments(self, issue, since: str = None) -> bool:
"""检测 issue 是否有 since 之后的人类新评论(过滤自动回复)"""
if not since:
return False
try:
since_dt = datetime.fromisoformat(since)
if since_dt.tzinfo is None:
since_dt = since_dt.replace(tzinfo=timezone.utc)
else:
since_dt = since_dt.astimezone(timezone.utc)
except (ValueError, TypeError):
return False
for comment in issue.get_comments(since=since_dt):
if not _is_bot_comment(comment.body or ""):
return True
return False
def remove_label(self, issue, label: str):
if self.dry_run:
log.info(f"[DRY-RUN] 移除标签 {label} from #{issue.number}")
return
try:
issue.remove_from_labels(label)
except GithubException as e:
log.warning(f"移除标签 {label} 失败: {e}")
def create_pr(self, branch: str, issue_num: int, title: str,
body: str, changed_files: list) -> Optional[int]:
files_str = "\n".join(f"- `{f}`" for f in changed_files)
pr_body = f"""## 自动解决 Issue #{issue_num}
{body[:1500]}
## 修改文件
{files_str}
---
_由 [ccloop](https://github.com/yuzebin/ccloop) 自动创建,请审查后合并。_"""
if self.dry_run:
log.info(f"[DRY-RUN] 会创建 PR: [ccloop] {title[:72]}")
log.info(f"[DRY-RUN] 分支: {branch} → {self.default_branch}")
return 0 # 假 PR 编号
try:
pr = self.repo.create_pull(
title=f"[ccloop] {title[:72]}",
body=pr_body,
head=branch,
base=self.default_branch
)
log.info(f"已创建 PR #{pr.number}")
return pr.number
except GithubException as e:
log.error(f"创建 PR 失败: {e}")
return None
def mark_processed(self, issue):
if self.dry_run:
log.info(f"[DRY-RUN] 会标记 issue #{issue.number} 为已处理")
return
try:
issue.add_to_labels(self.processed_label)
except GithubException:
self.repo.create_label(self.processed_label, "0e8a16")
issue.add_to_labels(self.processed_label)
def comment(self, issue, msg: str):
if self.dry_run:
log.info(f"[DRY-RUN] 评论 #{issue.number}:\n{msg[:200]}")
return
try:
issue.create_comment(msg)
except Exception as e:
log.warning(f"评论失败: {e}")