Browse Source

✨ feat: 完善仓库相关接口

Pchen. 3 months ago
parent
commit
9e4b4fb7b6

+ 34 - 14
apis/Repos/GetRepoList.js

@@ -1,11 +1,13 @@
-const API = require("../../lib/API");
-const db = require("../../plugin/DataBase/db");
-const AccessControl = require("../../lib/AccessControl");
-const { BaseStdResponse } = require("../../BaseStdResponse");
+const API = require("../../lib/API")
+const db = require("../../plugin/DataBase/db")
+const AccessControl = require("../../lib/AccessControl")
+const { BaseStdResponse } = require("../../BaseStdResponse")
+const axios = require("axios")
+const { core_url } = require('../../config.json')
 
 class GetRepoList extends API {
     constructor() {
-        super();
+        super()
 
         this.setPath('/Repos/List')
         this.setMethod('GET')
@@ -29,27 +31,45 @@ class GetRepoList extends API {
                 ...BaseStdResponse.ACCESS_DENIED
             })
 
-        let sql = 'SELECT * FROM repos WHERE create_user = ?'
+        let sql = 'SELECT id, name, url, create_time, state, logo, update_time FROM repos WHERE create_user = ?'
         let params = [uuid]
-        
+
         if ((keyword ?? '').trim() !== '') {
             sql += ' AND name LIKE ?'
             params.push(`%${keyword}%`)
         }
-        
+
         let rows = await db.query(sql, params)
-        
+
         if (!rows || rows.length === 0)
             return res.json({
                 ...BaseStdResponse.OK,
                 data: []
             })
 
-        res.json({
-            ...BaseStdResponse.OK,
-            data: rows
-        })
+        const status_url = core_url + '/git/status'
+
+        try {
+            // 并行执行所有请求
+            const statusPromises = rows.map(item =>
+                axios.post(status_url, { uuid, repo_url: item.url })
+                    .then(res => ({ ...item, status: res.data }))
+                    .catch(() => ({ ...item, status: null }))
+            )
+
+            const items = await Promise.all(statusPromises)
+
+            res.json({
+                ...BaseStdResponse.OK,
+                data: items
+            })
+        } catch (error) {
+            res.json({
+                ...BaseStdResponse.ERR,
+                message: '获取仓库状态失败'
+            })
+        }
     }
 }
 
-module.exports.GetRepoList = GetRepoList;
+module.exports.GetRepoList = GetRepoList

+ 58 - 0
apis/Repos/GetRepoLog.js

@@ -0,0 +1,58 @@
+const API = require("../../lib/API")
+const AccessControl = require("../../lib/AccessControl")
+const { BaseStdResponse } = require("../../BaseStdResponse")
+const db = require("../../plugin/DataBase/db")
+const axios = require('axios')
+const { core_url } = require('../../config.json')
+
+class GetRepoLog extends API {
+    constructor() {
+        super()
+
+        this.setMethod("GET")
+        this.setPath("/Repos/Log")
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id } = req.query
+
+        if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = 'SELECT url FROM repos WHERE create_user = ? AND id = ?'
+        let r = await db.query(sql, [uuid, id])
+        if (r && r.length === 0)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '未找到仓库'
+            })
+
+        try {
+            const clone_url = core_url + '/git/log'
+            const res = await axios.post(clone_url, { uuid, repo_url: r[0].url })
+            if(!res || !res.data || res.data.code !== 200)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: '获取仓库历史失败!'
+                })
+            res.json({
+                ...BaseStdResponse.OK,
+                data: res.data
+            })
+    
+        } catch (error) {
+            this.logger.error('获取仓库历史失败!' + error.stack)
+        }
+
+    }
+}
+
+module.exports.GetRepoLog = GetRepoLog

+ 58 - 0
apis/Repos/GetRepoStatus.js

@@ -0,0 +1,58 @@
+const API = require("../../lib/API")
+const AccessControl = require("../../lib/AccessControl")
+const { BaseStdResponse } = require("../../BaseStdResponse")
+const db = require("../../plugin/DataBase/db")
+const axios = require('axios')
+const { core_url } = require('../../config.json')
+
+class GetRepoStatus extends API {
+    constructor() {
+        super()
+
+        this.setMethod("GET")
+        this.setPath("/Repos/Status")
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id } = req.query
+
+        if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = 'SELECT url FROM repos WHERE create_user = ? AND id = ?'
+        let r = await db.query(sql, [uuid, id])
+        if (r && r.length === 0)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '未找到仓库'
+            })
+
+        try {
+            const clone_url = core_url + '/git/status'
+            const res = await axios.post(clone_url, { uuid, repo_url: r[0].url })
+            if(!res || !res.data)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: '获取仓库状态失败!'
+                })
+            res.json({
+                ...BaseStdResponse.OK,
+                data: res.data
+            })
+    
+        } catch (error) {
+            this.logger.error('获取仓库状态失败!' + error.stack)
+        }
+
+    }
+}
+
+module.exports.GetRepoStatus = GetRepoStatus

+ 106 - 4
apis/Repos/GitCodeStats.js

@@ -10,12 +10,78 @@ class gitCodeStats extends API {
     constructor() {
         super()
 
-        this.setMethod("POST")
+        this.setMethod("GET")
         this.setPath("/Repos/CodeStats")
     }
 
+    // 定义支持的语言后缀
+    languageExtensions = {
+        "JavaScript": [".js", ".jsx", ".mjs", ".cjs"],
+        "TypeScript": [".ts", ".tsx"],
+        "Python": [".py"],
+        "Java": [".java"],
+        "C": [".c"],
+        "C++": [".cpp", ".cc", ".cxx", ".h", ".hpp"],
+        "C#": [".cs"],
+        "Go": [".go"],
+        "PHP": [".php"],
+        "HTML": [".html", ".htm"],
+        "CSS": [".css"],
+        "SCSS": [".scss", ".sass"],
+        "JSON": [".json"],
+        "YAML": [".yml", ".yaml"],
+        "Markdown": [".md"],
+        "Shell": [".sh"],
+        "Bash": [".bash"],
+        "PowerShell": [".ps1"],
+        "Ruby": [".rb"],
+        "Vue": [".vue"],  // Vue文件
+        "LESS": [".less"],  // LESS样式文件
+        "Sass": [".sass"],  // Sass文件
+        "Haml": [".haml"],  // Haml文件
+        "Elixir": [".ex", ".exs"],  // Elixir语言文件
+        "Erlang": [".erl", ".hrl"],  // Erlang语言文件
+        "Swift": [".swift"],  // Swift语言文件
+        "Kotlin": [".kt", ".kts"],  // Kotlin语言文件
+        "Rust": [".rs"],  // Rust语言文件
+        "Dart": [".dart"],  // Dart语言文件
+        "Lua": [".lua"],  // Lua语言文件
+        "R": [".r"],  // R语言文件
+        "Perl": [".pl"],  // Perl语言文件
+        "SQL": [".sql"],  // SQL文件
+        "Dockerfile": [".dockerfile", "Dockerfile"],  // Dockerfile
+        "GraphQL": [".graphql", ".gql"],  // GraphQL文件
+        "JSON5": [".json5"],  // JSON5文件
+        "TOML": [".toml"],  // TOML文件
+        "Text": [".txt", ".text"],  // 文本文件
+        "Other": [".bin", ".dat", ".exe", ".dll", ".obj", ".so", ".class"] // 二进制及无法识别的文件
+    }
+
+    // 判断文件是否为二进制文件
+    isBinaryFile(filePath) {
+        const binaryExtensions = ['.exe', '.dll', '.obj', '.so', '.bin', '.dat']
+        const ext = path.extname(filePath).toLowerCase()
+        return binaryExtensions.includes(ext)
+    }
+
+    // 统计单个文件的行数
+    countLines(filePath) {
+        return new Promise((resolve) => {
+            if (this.isBinaryFile(filePath)) {
+                return resolve(0) // 二进制文件直接跳过
+            }
+            let lineCount = 0;
+            const stream = fs.createReadStream(filePath, 'utf8')
+            stream.on("data", (chunk) => {
+                lineCount += chunk.toString().split("\n").length
+            });
+            stream.on("end", () => resolve(lineCount))
+            stream.on("error", () => resolve(0)) // 读取错误默认返回 0
+        })
+    }
+
     async onRequest(req, res) {
-        let { uuid, session, id } = req.body
+        let { uuid, session, id } = req.query
 
         if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
             return res.json({
@@ -28,10 +94,46 @@ class gitCodeStats extends API {
                 ...BaseStdResponse.ACCESS_DENIED
             })
 
-        let sql = 'SELECT path, url FROM repos WHERE create_user = ? AND id = ?'
+        let sql = 'SELECT path FROM repos WHERE create_user = ? AND id = ?'
 
         let r = await db.query(sql, [uuid, id])
-        
+        if (r && r.length === 0)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '未找到仓库'
+            })
+
+        const git = simpleGit(r[0].path)
+        const files = await git.raw(["ls-files"]) // 获取所有 Git 跟踪的文件
+        const fileList = files.split("\n").map(f => f.trim()).filter(f => f)
+
+        let totalLines = 0
+        let languageStats = {}
+
+        for (const file of fileList) {
+            const fullPath = path.join(repoPath, file)
+            const ext = path.extname(file)
+            const language = Object.keys(this.languageExtensions).find(lang =>
+                this.languageExtensions[lang].includes(ext)
+            );
+
+            if (language) {
+                const lines = await countLines(fullPath)
+                totalLines += lines
+                languageStats[language] = (languageStats[language] || 0) + lines
+            } else {
+                // 如果文件不属于任何已知语言,归类为其他
+                const lines = await countLines(fullPath)
+                totalLines += lines
+                languageStats["Other"] = (languageStats["Other"] || 0) + lines
+            }
+        }
+
+        res.json({
+            ...BaseStdResponse.OK,
+            totalLines,
+            languageStats
+        })
     }
 }
 

+ 109 - 0
apis/Repos/GitContributors.js

@@ -0,0 +1,109 @@
+const API = require("../../lib/API")
+const AccessControl = require("../../lib/AccessControl")
+const { BaseStdResponse } = require("../../BaseStdResponse")
+const db = require("../../plugin/DataBase/db")
+const simpleGit = require('simple-git')
+const crypto = require("crypto")
+
+class GitContributors extends API {
+    constructor() {
+        super()
+
+        this.setMethod("GET")
+        this.setPath("/Repos/Contributors")
+    }
+
+    getGravatarUrl(email) {
+        if (!email) return null
+        const hash = crypto.createHash("md5").update(email.trim().toLowerCase()).digest("hex")
+        return `https://gravatar.loli.net/avatar/${hash}?s=512&r=pg`
+    }
+    
+    async analyzeGitContributors(repoPath) {
+        try {
+            const git = simpleGit()
+            await git.cwd(repoPath)
+    
+            // 获取提交日志,包括作者姓名和邮箱
+            const logs = await git.raw([
+                "log",
+                "--format=%an <%ae>",
+                "--shortstat"
+            ])
+    
+            const lines = logs.split("\n").map(l => l.trim()).filter(l => l)
+            let contributors = {}
+            let currentAuthor = null
+            for (const line of lines) {
+                if (line.includes("<") && line.includes(">")) {
+                    const match = line.match(/(.*?) <(.*?)>/)
+                    if (!match) continue
+                    const name = match[1].trim()
+                    const email = match[2].trim()
+                    currentAuthor = name
+                    if (!contributors[currentAuthor]) {
+                        contributors[currentAuthor] = {
+                            name,
+                            email,
+                            avatar: this.getGravatarUrl(email),
+                            commits: 0,
+                            linesChanged: 0
+                        }
+                    }
+                    contributors[currentAuthor].commits += 1
+                } else if (line.includes("file changed")) {
+                    const match = line.match(/(\d+) insertions?\(\+\)|(\d+) deletions?\(-\)/g)
+                    if (match) {
+                        const changes = match.map(m => parseInt(m.match(/\d+/)[0], 10)).reduce((a, b) => a + b, 0)
+                        contributors[currentAuthor].linesChanged += changes
+                    }
+                }
+            }
+    
+            const sortedContributors = Object.values(contributors)
+                .sort((a, b) => b.linesChanged - a.linesChanged || b.commits - a.commits)
+    
+            return sortedContributors
+        } catch (error) {
+            console.error("获取仓库开发者失败:", error)
+            throw new Error("获取仓库开发者失败")
+        }
+    }
+    
+    async onRequest(req, res) {
+        let { uuid, session, id } = req.query
+
+        if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = 'SELECT path FROM repos WHERE create_user = ? AND id = ?'
+        let r = await db.query(sql, [uuid, id])
+        if (r && r.length === 0)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '未找到仓库'
+            })
+
+        try {
+            const contributors = await this.analyzeGitContributors(r[0].path)
+            res.json({
+                ...BaseStdResponse.OK,
+                data: contributors
+            })
+    
+        } catch (error) {
+            this.logger.error('获取仓库开发者失败!' + error.stack)
+        }
+
+    }
+}
+
+module.exports.GitContributors = GitContributors