gerrit.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import subprocess
  2. import logging
  3. import os
  4. from typing import List, Dict, Optional
  5. logger = logging.getLogger(__name__)
  6. class GerritError(Exception):
  7. """Gerrit 操作异常"""
  8. pass
  9. def _run_ssh(host: str, port: int, user: str, command: str) -> str:
  10. """执行 Gerrit SSH 命令并返回标准输出"""
  11. cmd = [
  12. 'ssh',
  13. '-p', str(port),
  14. f'{user}@{host}',
  15. 'gerrit', *command.split()
  16. ]
  17. try:
  18. result = subprocess.run(cmd, capture_output=True, text=True, check=True)
  19. return result.stdout
  20. except subprocess.CalledProcessError as e:
  21. error_msg = e.stderr.strip()
  22. logger.error("Gerrit SSH command failed: %s", error_msg)
  23. raise GerritError(error_msg) from e
  24. def query_changes(host: str, port: int, user: str,
  25. change_ids: List[str]) -> List[Dict[str, str]]:
  26. """
  27. 查询 Gerrit 上的变更信息。
  28. Args:
  29. host: Gerrit 主机
  30. port: SSH 端口
  31. user: Gerrit 用户名
  32. change_ids: 变更 ID 列表(如 ['12345', '12346'])
  33. Returns:
  34. 每个变更的信息字典列表,包含 change_id, project, revision, ref
  35. """
  36. changes_info = []
  37. for cid in change_ids:
  38. output = _run_ssh(host, port, user,
  39. f"query --current-patch-set status:open change:{cid}")
  40. project = revision = ref = None
  41. for line in output.splitlines():
  42. line = line.strip()
  43. if line.startswith('project:'):
  44. project = line.split(':', 1)[1].strip()
  45. elif line.startswith('revision:'):
  46. revision = line.split(':', 1)[1].strip()
  47. elif line.startswith('ref:') and cid in line:
  48. # ref 行格式:ref: refs/changes/xx/12345/1
  49. ref = line.split('ref:', 1)[1].strip()
  50. # 确保提取到 refs/changes/... 部分
  51. if 'refs/' in ref:
  52. ref = ref[ref.index('refs/'):]
  53. if not project or not revision:
  54. raise GerritError(f"Cannot parse project/revision for change {cid}")
  55. changes_info.append({
  56. 'change_id': cid,
  57. 'project': project,
  58. 'revision': revision,
  59. 'ref': ref or ''
  60. })
  61. logger.info("Change %s: project=%s, revision=%s", cid, project, revision)
  62. return changes_info
  63. def _find_repo_root(project: str, mcu_dir: str, soc_dir: str) -> Optional[str]:
  64. """
  65. 根据 project 名称,找到它属于 MCU 还是 SOC 工程。
  66. 优先检查 manifest/default.xml 中是否包含该项目。
  67. """
  68. for repo_dir in [soc_dir, mcu_dir]:
  69. manifest = os.path.join(repo_dir, '.repo', 'manifests', 'default.xml')
  70. if os.path.isfile(manifest):
  71. try:
  72. with open(manifest, 'r') as f:
  73. if project in f.read():
  74. return repo_dir
  75. except Exception:
  76. continue
  77. # 备选:使用 repo list 命令查找
  78. for repo_dir in [soc_dir, mcu_dir]:
  79. try:
  80. result = subprocess.run(
  81. ['repo', 'list'], cwd=repo_dir,
  82. capture_output=True, text=True, check=False
  83. )
  84. if project in result.stdout:
  85. return repo_dir
  86. except Exception:
  87. continue
  88. return None
  89. def apply_changes(changes_info: List[Dict], mcu_dir: str, soc_dir: str):
  90. """
  91. 将 Gerrit 变更通过 repo download 应用到对应的工程。
  92. Args:
  93. changes_info: query_changes 的返回结果
  94. mcu_dir: MCU 工程的根目录
  95. soc_dir: SOC 工程的根目录
  96. """
  97. for info in changes_info:
  98. project = info['project']
  99. ref = info['ref']
  100. repo_root = _find_repo_root(project, mcu_dir, soc_dir)
  101. if repo_root is None:
  102. raise GerritError(f"Project {project} not found in MCU or SOC manifest")
  103. # ref 可能带前缀,确保为 refs/changes/... 的形式
  104. if not ref.startswith('refs/'):
  105. ref = 'refs/changes/' + ref.lstrip('/')
  106. # repo download 需要 'project_path change/ref' 的形式
  107. # 如果 ref 已包含 'refs/changes/...',直接用;否则拼接
  108. download_ref = f"{ref}" if '/' in ref else f"refs/changes/{ref}"
  109. # 原脚本中 download_gerrit_project 使用了 change$change_index,即直接使用查询到的 ref 部分
  110. # 这里我们直接使用 ref 即可(通常是 refs/changes/<NN>/<change>/<patchset>)
  111. cmd = ['repo', 'download', project, ref]
  112. logger.info("Applying change %s: %s", info['change_id'], ' '.join(cmd))
  113. try:
  114. subprocess.run(cmd, cwd=repo_root, check=True)
  115. except subprocess.CalledProcessError as e:
  116. raise GerritError(f"repo download failed for {info['change_id']}: {e}") from e
  117. def label_verified(host: str, port: int, user: str,
  118. changes_info: List[Dict]):
  119. """
  120. 给所有变更打上 Verified +1 标签。
  121. Args:
  122. changes_info: query_changes 的返回结果
  123. """
  124. for info in changes_info:
  125. revision = info['revision']
  126. _run_ssh(host, port, user, f"review --verified +1 {revision}")
  127. logger.info("Verified +1 on change %s", info['change_id'])
  128. def get_history(host: str, port: int, user: str,
  129. project: str, branch: str, output_file: str):
  130. """
  131. 查询已合并的评审记录,并保存到指定文件。
  132. Args:
  133. project: Git 项目名(如 INVO/MCU/APP/canbus_mng)
  134. branch: 分支名
  135. output_file: 输出文件路径
  136. """
  137. # 查询已合并的状态,起始时间定为较早的时间,确保覆盖所有历史
  138. query = f"query --format=TEXT status:merged project:{project} branch:{branch} after:2020-01-01"
  139. output = _run_ssh(host, port, user, query)
  140. with open(output_file, 'w', encoding='utf-8') as f:
  141. f.write(output)
  142. logger.info("Saved merged history for %s to %s", project, output_file)