gitRouter.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import asyncio
  2. import os, json,hashlib,re,shutil,time
  3. from fastapi import APIRouter, BackgroundTasks
  4. from base_config import path, avatar_url
  5. from git import Repo, GitCommandError
  6. from pydantic import BaseModel
  7. from models.gitModels import Repos
  8. class RequestBody(BaseModel):
  9. uuid: str
  10. repo_url: str
  11. class CommitHash(BaseModel):
  12. uuid: str
  13. repo_url: str
  14. commit_hash: str
  15. def generate_repo_path(uuid, repo_url):
  16. repo_name = repo_url.split("/")[-1].replace(".git", "")
  17. base_path = os.path.join(path, uuid)
  18. return os.path.join(base_path, repo_name), repo_name
  19. def get_repo(uuid, repo_url):
  20. path, _ = generate_repo_path(uuid, repo_url)
  21. if not os.path.exists(path):
  22. return 0
  23. return Repo(path)
  24. def git_stats_to_json(text):
  25. pattern = r",?\s*(\d+)\s*files changed|,?\s*(\d+)\s*insertions\(\+\)|,?\s*(\d+)\s+deletions\(\-\)"
  26. data = re.findall(pattern, text)
  27. result = {}
  28. for item in data:
  29. if item[0]:
  30. result["files_changed"] = int(item[0])
  31. if item[1]:
  32. result["insertions"] = int(item[1])
  33. if item[2]:
  34. result["deletions"] = int(item[2])
  35. return result
  36. gitrouter = APIRouter()
  37. async def clone_task(repo_url, local_path,uuid,repo_name):
  38. current_time = int(time.time() * 1000)
  39. print(f"开始克隆仓库: {repo_url}")
  40. try:
  41. loop = asyncio.get_event_loop()
  42. await loop.run_in_executor(None, Repo.clone_from, repo_url, local_path)
  43. await Repos.filter(create_user=uuid,name=repo_name).update(path=local_path, state=1, update_time=current_time)
  44. print(f"克隆仓库成功: {repo_url}")
  45. except:
  46. print(f"克隆仓库失败: {repo_url}")
  47. await Repos.filter(create_user=uuid,name=repo_name).update(path=local_path, state=0, update_time=current_time)
  48. shutil.rmtree(local_path)
  49. @gitrouter.post("/clone")
  50. async def clone(request: RequestBody, background_tasks: BackgroundTasks):
  51. local_path, repo_name = generate_repo_path(request.uuid, request.repo_url)
  52. if os.path.exists(local_path):
  53. return {"status": "400", "msg": "仓库已存在", "uuid": request.uuid, "repo_url": request.repo_url,
  54. "path": local_path}
  55. else:
  56. background_tasks.add_task(clone_task, request.repo_url, local_path, request.uuid, repo_name)
  57. response = {"status": "200", "msg": "成功创建克隆任务", "uuid": request.uuid, "repo_name": repo_name,
  58. "local_path": local_path}
  59. return response
  60. @gitrouter.post("/log")
  61. async def log(request: RequestBody):
  62. local_path, _ = generate_repo_path(request.uuid, request.repo_url)
  63. repo = get_repo(request.uuid, request.repo_url)
  64. if not repo:
  65. return {
  66. "status": "404",
  67. "msg": "仓库不存在",
  68. "uuid": request.uuid,
  69. "repo_url": request.repo_url,
  70. "local_path": local_path
  71. }
  72. # 使用git log --numstat一次性获取所有必要信息
  73. git_log_format = '--pretty=format:%h|%an|%ce|%s|%cd'
  74. try:
  75. log_output = repo.git.log(
  76. git_log_format,
  77. '--numstat',
  78. '--no-renames',
  79. date='format:%Y-%m-%d %H:%M'
  80. )
  81. except GitCommandError as e:
  82. return {"status": "500", "msg": f"获取日志失败: {str(e)}"}
  83. log = []
  84. current_commit = None
  85. for line in log_output.split('\n'):
  86. if not line.strip():
  87. continue # 跳过空行
  88. if '\t' in line and len(line.split('\t')) == 3:
  89. # 处理numstat行,例如 "10\t5\tfile.txt"
  90. if current_commit is None:
  91. continue # 防止数据错误
  92. insertions_str, deletions_str, _ = line.split('\t')
  93. try:
  94. insertions = int(insertions_str) if insertions_str != '-' else 0
  95. deletions = int(deletions_str) if deletions_str != '-' else 0
  96. except ValueError:
  97. insertions, deletions = 0, 0
  98. current_commit['change']['insertions'] += insertions
  99. current_commit['change']['deletions'] += deletions
  100. current_commit['change']['files'] += 1
  101. else:
  102. # 处理提交信息行,例如 "abc123|Author|email|summary|2023-10-01 12:34"
  103. if current_commit is not None:
  104. # 生成avatar的md5
  105. email = current_commit['email']
  106. email_md5 = hashlib.md5(email.encode('utf-8')).hexdigest()
  107. current_commit['avatar'] = f"{avatar_url}{email_md5}?d=identicon"
  108. log.append(current_commit)
  109. try:
  110. commit_hash, author, email, summary, date = line.split('|', 4)
  111. current_commit = {
  112. "commit": commit_hash,
  113. "author": author,
  114. "email": email,
  115. "summary": summary,
  116. "date": date,
  117. "avatar": "",
  118. "change": {
  119. "insertions": 0,
  120. "deletions": 0,
  121. "files": 0
  122. }
  123. }
  124. except ValueError:
  125. current_commit = None # 忽略格式错误行
  126. # 添加最后一个提交
  127. if current_commit is not None:
  128. email = current_commit['email']
  129. email_md5 = hashlib.md5(email.encode('utf-8')).hexdigest()
  130. current_commit['avatar'] = f"{avatar_url}{email_md5}?d=identicon"
  131. log.append(current_commit)
  132. # 按时间倒序排列(git log默认最新在前)
  133. return {
  134. "status": "200",
  135. "msg": "成功获取日志",
  136. "uuid": request.uuid,
  137. "repo_url": request.repo_url,
  138. "local_path": local_path,
  139. "git_log": log
  140. }
  141. @gitrouter.post("/status")
  142. async def status(request: RequestBody):
  143. repo = get_repo(request.uuid, request.repo_url)
  144. # 手动获取所有数据
  145. active_branch = repo.active_branch
  146. tracking_branch = active_branch.tracking_branch()
  147. ahead = sum(1 for _ in repo.iter_commits(f"{active_branch}..{tracking_branch}"))
  148. behind = sum(1 for _ in repo.iter_commits(f"{tracking_branch}..{active_branch}"))
  149. conflicts = repo.index.unmerged_blobs()
  150. conflicted = [path for path, entries in conflicts.items()]
  151. created_files = repo.untracked_files
  152. current = repo.active_branch.name
  153. head_commit = repo.head.commit
  154. tree = head_commit.tree
  155. all_files = [item.path for item in tree.traverse() if item.type == 'blob']
  156. diffs = repo.index.diff(None)
  157. deleted = [d.a_path for d in diffs if d.change_type == 'D']
  158. detached = repo.head.is_detached
  159. ignored_files = repo.git.execute(["git", "ls-files", "--others", "--ignored", "--exclude-standard"]).split("\n")
  160. modified_files = [d.a_path for d in diffs]
  161. untracked_files = repo.untracked_files
  162. staged_entries = repo.index.entries
  163. staged = [path[0] for path, _ in staged_entries.items()]
  164. tracking = active_branch.tracking_branch().name
  165. status = {"ahead": ahead, "behind": behind, "conflicted": conflicted, "created": created_files,
  166. "current": current, "deleted": deleted, "detached": detached, "files": all_files,
  167. "ignored": ignored_files,
  168. "modified": modified_files, "not_added": untracked_files, "staged": staged, "tracking": tracking}
  169. return status
  170. @gitrouter.post("/change")
  171. async def change(request: CommitHash):
  172. repo = get_repo(request.uuid, request.repo_url)
  173. if not repo:
  174. return {"status": "404", "msg": "仓库不存在", "uuid": request.uuid, "repo_url": request.repo_url}
  175. commit = repo.commit(request.commit_hash)
  176. if not commit.parents:
  177. print("首次提交,无父提交对比")
  178. return
  179. parent = commit.parents[0]
  180. diffs = commit.diff(commit,create_patch=True, no_renames=True)
  181. print(diffs)
  182. for diff in diffs:
  183. print(f"文件 {diff.a_path} ({diff.change_type}):")
  184. print(diff.diff.decode('utf-8'))