摸鱼大师使用AI评估GIT项目

386 阅读9分钟

项目概述

项目核心功能就是利用 AI (ChatGPT) 来分析处理非结构化的 Git Log 信息。

具体来说,它做了以下几件事:

  1. 提取 Git Log: 通过 git log 命令获取项目的提交历史记录,包括提交者、提交时间、提交信息等。
  2. 调用 ChatGPT API: 将提取的 Git Log 发送给 ChatGPT API,并要求它完成以下任务:
    • 分析贡献: 根据提交历史,判断每个贡献者在项目中扮演的角色和贡献程度。
    • 评选 MVP: 根据贡献分析,评选出最有价值的贡献者 (MVP),并给出选择理由。
  3. 格式化输出: 将 ChatGPT 返回的分析结果解析成结构化的 JSON 格式,方便后续使用和展示。

总而言之,这段代码利用了 AI 的自然语言处理能力,将原本难以处理的非结构化 Git Log 信息转化为可理解、可分析的结构化数据,为项目管理和决策提供了 valuable insights.

代码实现

为何JSON输出如此重要?对于程序员来说,结构化数据的准确输出具有不可或缺的重要性。与人类自然语言交流不同,计算机与计算机之间的通讯需要标准化的格式来保证信息的完整与可读性。JSON(Java Object Notation)作为一种轻量级的数据交换格式,以其简单明了的结构广泛应用于API和客户端-服务器通讯中。

OpenAI重磅更新:GPT-4o实现100%准确的JSON输出 —— 由于本文的方法也能稳定输出JSON,故此未使用该解决方案实现。

Introducing Structured Outputs in the API | OpenAI

📝 仓库 https://xxx.com/*******/******* 开始评估
仓库 ta******* 已更新
 ✅ 克隆或更新成功
 ✅ 信息获取成功
    📅 最新更新时间: 2024-07-03 11:42:13 +08:00
    📝 更新内容: Merge branch 'main' of https://xxx.com/*******/*******  
    👤 bj******* 提交次数: 14
    👤 Zh******* 提交次数: 20
    👤 ic******* 提交次数: 3
    👤 yw******* 提交次数: 2
    👤 Ca******* 提交次数: 5
    👥 提交总数: 44
    --------------------
    用户名: bj*******
    用户角色: 项目发起人,负责项目的整体规划和管理,提交了大量关于游戏特色、市场调研、文档更新等方面的内容。
    理由: 虽然提交次数较多,但主要集中在文档更新和游戏特色调整,对项目整体推动 贡献较大的是其他成员。
    --------------------
    --------------------
    用户名: Zh*******
    用户角色: 项目经理,负责项目任务分配、需求分析、文档更新等工作,提交次数较多且涵盖了项目的多个关键方面。
    是否为 MVP: 🏆
    理由: Zh*******在项目中承担了项目经理的重要角色,提交次数多且涵盖了项目的多个关键方面,对项目的推动起到了关键作用。
    --------------------
    --------------------
    用户名: ic*******
    用户角色: 团队成员,提交次数较少,贡献主要集中在文档合并方面。
    理由: 贡献较少,主要集中在文档合并,对项目整体推动影响有限。
    --------------------
    --------------------
    用户名: yw*******
    用户角色: 团队成员,提交次数较少,主要贡献是创建个人信息文档和部分文档更新 。
    理由: 贡献较少,提交次数较少,对项目整体推动影响有限。
    --------------------
    --------------------
    用户名: Ca*******
    用户角色: 团队成员,提交次数较少,贡献主要集中在文档更新方面。
    理由: 贡献较少,提交次数较少,对项目整体推动影响有限。
    --------------------
 ✅ 总结和评估成功
    📒 仓库总结: 该 README.md 文件详细介绍了一个名为 ta******* 的游戏项目,包括游戏简介、菜单页面设计、方块类型、道具汇总、玩家行为、游戏对象测试、不同类型的 坦克、关卡模式、商店设计、游戏总体体验和开发流程等内容。其中涵盖了游戏的各个方 面,包括玩法、角色、道具、关卡设计等。同时给出了开发流程和小组成员信息。整体内容比较详细,但有些部分需要进一步补充细节。
    ⭐ 仓库评分: 7

配置文件

{
  "chatgpt": {
    "domain": "",
    "apiKey": ""
  },
  "email": {
    "host": "smtp.163.com",
    "port": 465,
    "secure": true,
    "user": "",
    "pass": "",
    "from": "",
    "test": ""
  }
}

源代码

import os
import json
import subprocess
import re
import requests
from datetime import datetime, timedelta
import smtplib
from email.mime.text import MIMEText

# --- 配置 ---
# --- 读取配置文件 ---
config_path = os.path.join(os.path.dirname(__file__), "env.json")
with open(config_path, "r", encoding="utf-8") as f:
    config = json.load(f)

chatgpt_config = config["chatgpt"]
mail_config = config["email"]

# --- 函数定义 ---


def get_repo_urls():
    """读取 README.md 文件并提取 GitHub/Gitee 仓库地址"""
    with open("README.md", "r", encoding="utf-8") as f:
        readme_content = f.read()

    repo_urls = re.findall(
        r"(https://(?:gitee\.com|github\.com)/[^)\s]+)", readme_content
    )
    return repo_urls


def clone_or_update_repo(repo_url):
    """克隆或更新仓库"""
    repo_name = repo_url.split("/")[-1].replace(".git", "")
    repo_path = os.path.join(os.path.dirname(__file__), "repositories", repo_name)

    try:
        if os.path.exists(repo_path):
            subprocess.run(["git", "pull"], cwd=repo_path, check=True)
            print(f"✅ 仓库 {repo_name} 已更新")
        else:
            subprocess.run(
                ["git", "clone", repo_url],
                cwd=os.path.join(os.path.dirname(__file__), "repositories"),
                check=True,
            )
            print(f"✅ 仓库 {repo_name} 已克隆")
        return True
    except subprocess.CalledProcessError as e:
        print(f"❗ 仓库 {repo_name} 操作失败:", e)
        return False


def get_repo_info(repo_url):
    """获取仓库信息"""
    repo_name = repo_url.split("/")[-1].replace(".git", "")
    repo_path = os.path.join(os.path.dirname(__file__), "repositories", repo_name)

    # 获取最新更新时间和更新内容
    git_log_output = subprocess.run(
        ["git", "log", "-1", "--pretty=format:%ci||%s"],
        cwd=repo_path,
        check=True,
        text=True,
        capture_output=True,
        encoding="utf-8",  # 指定编码为 utf-8
    )
    latest_update_time, update_content = git_log_output.stdout.strip().split("||")

    # 读取 README.md 文件内容
    readme_file = next(
        (
            f
            for f in ["README.md", "readme.md"]
            if os.path.exists(os.path.join(repo_path, f))
        ),
        "",
    )
    readme_content = (
        open(os.path.join(repo_path, readme_file), "r", encoding="utf-8").read()
        if readme_file
        else ""
    )

    # 获取项目文件树
    file_tree = get_project_file_tree(repo_path)
    if file_tree:
        print(" 📦 项目包含 DevOps 标识文件\n    " + "\n    ".join(file_tree))
        file_tree_content = "\n\n## 项目文件树\n\n" + "\n\t\t".join(file_tree)
    else:
        file_tree_content = ""

    project_content = f"{readme_content}{file_tree_content}"

    total_commits, authors = get_commit_author_info(repo_path)

    return latest_update_time, update_content, project_content, total_commits, authors


def get_chatgpt_evaluation(project_content):
    """调用 ChatGPT API 对项目相关信息进行总结和评估"""
    if not project_content:
        return {"summary": "README.md 为空,无法进行总结和评估", "rating": 0}

    try:
        response = requests.post(
            f"{chatgpt_config['domain']}/v1/chat/completions",
            json={
                "model": "gpt-4o-mini",
                "messages": [
                    {
                        "role": "system",
                        "content": '你是资深软件工程师,请对以下 README.md 文件进行总结和评估,并给出 1-10 分的评分(1 分最低,10 分最高):请使用中文,返回格式是JSON,格式是{"summary":"xxxx", "evaluation":"5"},一定不要使用Markdown格式',
                    },
                    {"role": "user", "content": project_content},
                ],
                "temperature": 0.2,
            },
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {chatgpt_config['apiKey']}",
            },
        )

        content = response.json()["choices"][0]["message"]["content"].strip()
        content_dict = json.loads(content)
        summary = content_dict["summary"]
        rating = int(content_dict["evaluation"])

        return {"summary": summary, "rating": rating}
    except Exception as e:
        print("❗ 调用 ChatGPT API 失败:", e)
        return {"summary": "❗ 无法获取总结", "rating": None}


def get_chatgpt_mvp(git_log):
    """调用 ChatGPT API 评选 MVP"""
    try:
        response = requests.post(
            f"{chatgpt_config['domain']}/v1/chat/completions",
            json={
                "model": "gpt-4o-mini",
                "messages": [
                    {
                        "role": "system",
                        "content": '你是一位经验丰富的项目经理,负责评估团队成员的贡献。请根据以下 git log 信息,分析每个团队成员在项目中担任的角色,并评选出本次项目的 MVP (最有价值成员),并给出选择该成员的理由。请使用中文,返回格式是JSON,格式是{"user1":{"userRole":"xxxx", "mvp":true, "reason":"xxxx"},"user2":{"userRole":"xxxx", "mvp":false, "reason":"xxxx"}},一定不要使用Markdown格式',
                    },
                    {"role": "user", "content": git_log},
                ],
                "temperature": 0.5,
            },
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {chatgpt_config['apiKey']}",
            },
        )

        content = response.json()["choices"][0]["message"]["content"].strip()
        return json.loads(content)
    except Exception as e:
        print("❗ 调用 ChatGPT API 失败:", e)
        return {"mvp": "❗ 无法获取 MVP", "reason": "❗ 调用 ChatGPT API 失败"}


def get_commit_author_info(repo_path):
    """获取仓库提交者信息"""
    log_output = subprocess.run(
        ["git", "log", "--pretty=format:%an||%s||%cI"],
        cwd=repo_path,
        check=True,
        text=True,
        capture_output=True,
        encoding="utf-8",  # 指定编码为 utf-8
    ).stdout
    commits = [line.split("||") for line in log_output.strip().split("\n")]
    author_counts = {}
    total_commits = 0

    for author, message, date in commits:
        if author:
            if author not in author_counts:
                author_counts[author] = {"commits": 0, "history": []}
            author_counts[author]["commits"] += 1
            author_counts[author]["history"].append({"message": message, "date": date})
            total_commits += 1

    return total_commits, author_counts


def get_project_file_tree(repo_path):
    """获取项目文件树"""
    file_tree = (
        subprocess.run(
            ["git", "ls-tree", "-r", "--name-only", "HEAD"],
            cwd=repo_path,
            check=True,
            text=True,
            capture_output=True,
            encoding="utf-8",  # 指定编码为 utf-8
        )
        .stdout.strip()
        .split("\n")
    )
    devops_files = ["Dockerfile", "Jenkinsfile", "docker-compose.yml"]
    if any(file in file_tree for file in devops_files):
        return [file for file in file_tree if file in devops_files]
    else:
        return []


def send_notify_email(mail):
    """发送通知邮件"""
    try:
        msg = MIMEText(
            f'更新时间: {mail["latestUpdateTime"]},更新内容: {mail["updateContent"]}',
            "plain",
            "utf-8",
        )
        msg["Subject"] = f'🔥项目[{mail["url"]}]已经更新'
        msg["From"] = mail_config["from"]
        msg["To"] = mail_config["test"]

        with smtplib.SMTP_SSL(mail_config["host"], mail_config["port"]) if mail_config[
            "secure"
        ] else smtplib.SMTP(mail_config["host"], mail_config["port"]) as server:
            server.login(mail_config["user"], mail_config["pass"])
            server.sendmail(mail_config["from"], [mail_config["test"]], msg.as_string())

        print(f'📨 邮件通知成功 {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    except Exception as e:
        print("❗ 邮件通知失败:", e)


def main():
    # 1. 获取仓库地址列表
    repo_urls = get_repo_urls()

    # 2. 创建 repositories 目录
    repos_path = os.path.join(os.getcwd(), "repositories")
    if not os.path.exists(repos_path):
        os.mkdir(repos_path)

    # 3. 遍历仓库地址列表
    results = []
    for repo_url in repo_urls:
        # results中已经处理过的repoUrl不再处理
        if any(result["repoUrl"] == repo_url for result in results):
            continue
        # repoUrl中包括gitee.com/lgc653/的不处理
        if "gitee.com/lgc653/" in repo_url:
            continue

        print(f"📝 仓库 {repo_url} 开始评估")

        # 3.1 克隆或更新仓库
        if not clone_or_update_repo(repo_url):
            continue

        # 3.2 获取仓库信息
        latest_update_time, update_content, project_content, total_commits, authors = (
            get_repo_info(repo_url)
        )
        git_log = json.dumps(authors, indent=2)
        latest_update_time_dt = datetime.strptime(
            latest_update_time, "%Y-%m-%d %H:%M:%S %z"
        )

        print(" ✅ 信息获取成功")
        print(
            f'    📅 最新更新时间: {latest_update_time_dt.strftime("%Y-%m-%d %H:%M:%S %z")}'
        )
        print(f"    📝 更新内容: {update_content}")

        # 遍历 authors,显示每个提交者的提交次数
        for author, details in authors.items():
            print(f'    👤 {author} 提交次数: {details["commits"]}')
        print(f"    👥 提交总数: {total_commits}")

        # 调用 getChatgptMvp 函数评选 MVP
        users = get_chatgpt_mvp(git_log)
        for user_name, user in users.items():
            print("    --------------------")
            print(f"    用户名: {user_name}")
            print("    --------------------")
            print(f'    用户角色: {user["userRole"]}')
            if user.get("mvp"):
                print(f'    MVP: {"🏆" if user["mvp"] else ""}')
            print(f'    理由: {user["reason"]}')

        # 如果内容在1小时内更新过,重点提醒
        one_hour_ago = datetime.now().astimezone() - timedelta(hours=1)
        if latest_update_time_dt > one_hour_ago:
            print(" ⏰ 仓库更新时间在 1 小时内,请关注更新情况")
            # 邮件配置中 host 或者 user 如果为空,不发送邮件
            if not mail_config["host"] or not mail_config["user"]:
                print(" ❗ 邮件通知未配置,请配置邮件服务后重试")
            else:
                send_notify_email(
                    {
                        "url": repo_url,
                        "latestUpdateTime": latest_update_time_dt.strftime(
                            "%Y-%m-%d %H:%M:%S %z"
                        ),
                        "updateContent": update_content,
                    }
                )

        # 3.3 调用 ChatGPT API 进行总结和评估
        evaluation = get_chatgpt_evaluation(project_content)
        summary = evaluation["summary"]
        rating = evaluation["rating"]

        print(" ✅ 总结和评估成功")
        print(f"    📒 仓库总结: {summary}")
        print(f"    ⭐ 仓库评分: {rating}")

        # 3.4 将结果添加到数组中
        results.append(
            {
                "repoUrl": repo_url,
                "latestUpdateTime": latest_update_time,
                "updateContent": update_content,
                "summary": summary,
                "rating": rating,
            }
        )

    # 4. 打印 JSON 结果
    print(json.dumps(results, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()