diff --git a/cmd/git-mirror-detector/main.go b/cmd/git-mirror-detector/main.go new file mode 100644 index 00000000..975dd56b --- /dev/null +++ b/cmd/git-mirror-detector/main.go @@ -0,0 +1,295 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "regexp" + "strings" + "time" + + "github.com/tealeg/xlsx/v3" +) + +// GitHubRepo 结构体用于存储GitHub仓库信息 +type GitHubRepo struct { + Name string `json:"name"` + FullName string `json:"full_name"` + Desc string `json:"description"` + HTMLURL string `json:"html_url"` + CloneURL string `json:"clone_url"` + Fork bool `json:"fork"` + Parent *struct { + Name string `json:"name"` + FullName string `json:"full_name"` + HTMLURL string `json:"html_url"` + } `json:"parent"` + Source *struct { + Name string `json:"name"` + FullName string `json:"full_name"` + HTMLURL string `json:"html_url"` + } `json:"source"` +} + +// RepoInfo 用于存储处理后的仓库信息 +type RepoInfo struct { + OriginalURL string + RepoName string + IsValid bool + IsFork bool + IsMirror bool + ForkOriginalRepo string + MirrorOriginalRepo string + Description string + ErrorMessage string +} + +func main() { + inputFile := "input.xlsx" + outputFile := "output.xlsx" + + // 读取输入Excel文件 + repos, err := readExcelFile(inputFile) + if err != nil { + fmt.Printf("读取Excel文件失败: %v\n", err) + return + } + + // 处理每个GitHub链接 + var results []RepoInfo + for i, repoURL := range repos { + fmt.Printf("处理第 %d 个链接: %s\n", i+1, repoURL) + + info := processGitHubURL(repoURL) + results = append(results, info) + + // 添加延迟避免API限制 + time.Sleep(1 * time.Second) + } + + // 写入输出Excel文件 + err = writeExcelFile(outputFile, results) + if err != nil { + fmt.Printf("写入Excel文件失败: %v\n", err) + return + } + + fmt.Printf("处理完成!结果已保存到 %s\n", outputFile) +} + +// readExcelFile 读取Excel文件中的GitHub链接 +func readExcelFile(filename string) ([]string, error) { + file, err := xlsx.OpenFile(filename) + if err != nil { + return nil, fmt.Errorf("打开文件失败: %v", err) + } + + var urls []string + if len(file.Sheets) == 0 { + return nil, fmt.Errorf("Excel文件中没有工作表") + } + + sheet := file.Sheets[0] + + // 找到 git_link 列的索引 + gitLinkColumnIndex := -1 + + // 检查第一行(标题行)找到 git_link 列 + err = sheet.ForEachRow(func(r *xlsx.Row) error { + // 只处理第一行来找到列索引 + if gitLinkColumnIndex == -1 { + for i := 0; i < 10; i++ { // 最多检查前10列 + cell := r.GetCell(i) + if cell != nil { + cellValue := strings.TrimSpace(strings.ToLower(cell.String())) + if cellValue == "git_link" || cellValue == "gitlink" { + gitLinkColumnIndex = i + break + } + } + } + } else { + // 从数据行读取 git_link 列的值 + cell := r.GetCell(gitLinkColumnIndex) + if cell != nil { + cellValue := strings.TrimSpace(cell.String()) + if cellValue != "" && strings.Contains(cellValue, "github.com") { + urls = append(urls, cellValue) + } + } + } + return nil + }) + + if gitLinkColumnIndex == -1 { + return nil, fmt.Errorf("未找到 git_link 列") + } + + return urls, err +} + +// writeExcelFile 将结果写入Excel文件 +func writeExcelFile(filename string, results []RepoInfo) error { + file := xlsx.NewFile() + sheet, err := file.AddSheet("Results") + if err != nil { + return fmt.Errorf("创建工作表失败: %v", err) + } + + // 添加标题行 + headerRow := sheet.AddRow() + headers := []string{"原始链接", "是否有效", "是否Fork", "是否镜像", "Fork的原始仓库", "镜像的原始仓库"} + for _, header := range headers { + cell := headerRow.AddCell() + cell.Value = header + } + + // 添加数据行 + for _, result := range results { + row := sheet.AddRow() + + cells := []*xlsx.Cell{ + row.AddCell(), // 原始链接 + row.AddCell(), // 是否有效 + row.AddCell(), // 是否Fork + row.AddCell(), // 是否镜像 + row.AddCell(), // Fork的原始仓库 + row.AddCell(), // 镜像的原始仓库 + } + + cells[0].Value = result.OriginalURL + cells[1].Value = fmt.Sprintf("%t", result.IsValid) + cells[2].Value = fmt.Sprintf("%t", result.IsFork) + cells[3].Value = fmt.Sprintf("%t", result.IsMirror) + cells[4].Value = result.ForkOriginalRepo + cells[5].Value = result.MirrorOriginalRepo + } + + return file.Save(filename) +} + +// processGitHubURL 处理GitHub URL并获取仓库信息 +func processGitHubURL(githubURL string) RepoInfo { + info := RepoInfo{ + OriginalURL: githubURL, + IsValid: false, + } + + // 解析GitHub URL + owner, repo, err := parseGitHubURL(githubURL) + if err != nil { + info.ErrorMessage = err.Error() + return info + } + + info.RepoName = fmt.Sprintf("%s/%s", owner, repo) + + // 获取仓库信息 + repoData, err := getGitHubRepoInfo(owner, repo) + if err != nil { + info.ErrorMessage = err.Error() + return info + } + + info.IsValid = true + info.Description = repoData.Desc + info.IsFork = repoData.Fork + + // 如果是fork,获取原始仓库信息 + if repoData.Fork && repoData.Source != nil { + info.ForkOriginalRepo = repoData.Source.HTMLURL + } + + // 检查是否为镜像仓库并获取镜像的原始仓库 + info.IsMirror, info.MirrorOriginalRepo = checkIfMirrorRepo(repoData) + + return info +} + +// parseGitHubURL 解析GitHub URL获取owner和repo +func parseGitHubURL(githubURL string) (string, string, error) { + // 正则表达式匹配GitHub URL + re := regexp.MustCompile(`github\.com/([^/]+)/([^/]+)`) + matches := re.FindStringSubmatch(githubURL) + + if len(matches) < 3 { + return "", "", fmt.Errorf("无效的GitHub URL格式") + } + + owner := matches[1] + repo := strings.TrimSuffix(matches[2], ".git") + + return owner, repo, nil +} + +// getGitHubRepoInfo 通过GitHub API获取仓库信息 +func getGitHubRepoInfo(owner, repo string) (*GitHubRepo, error) { + apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s", owner, repo) + + resp, err := http.Get(apiURL) + if err != nil { + return nil, fmt.Errorf("请求GitHub API失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("GitHub API返回错误状态: %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %v", err) + } + + var repoData GitHubRepo + err = json.Unmarshal(body, &repoData) + if err != nil { + return nil, fmt.Errorf("解析JSON失败: %v", err) + } + + return &repoData, nil +} + +// checkIfMirrorRepo 检查是否为镜像仓库并返回是否为镜像和原始仓库链接 +func checkIfMirrorRepo(repoData *GitHubRepo) (bool, string) { + if repoData.Desc == "" { + return false, "" + } + + desc := strings.ToLower(repoData.Desc) + + // 检查描述中是否包含镜像相关关键词 + if strings.Contains(desc, "mirror") || + strings.Contains(desc, "镜像") || + strings.Contains(desc, "clone") || + strings.Contains(desc, "copy") || + strings.Contains(desc, "backup") || + strings.Contains(desc, "同步") || + strings.Contains(desc, "复制") { + + // 尝试从描述中提取原始仓库链接 + // 优先查找"mirror of"后面的链接(支持任何代码托管平台) + mirrorOfRe := regexp.MustCompile(`(?i)mirror\s+of\s+(https?://[^\s]+)`) + matches := mirrorOfRe.FindStringSubmatch(repoData.Desc) + + if len(matches) >= 2 { + // 返回"mirror of"后面找到的完整链接 + return true, matches[1] + } + + // 如果没有找到"mirror of"格式,尝试查找任何代码托管平台的链接 + generalRe := regexp.MustCompile(`https?://(?:github\.com|gitlab\.com|gitee\.com|bitbucket\.org|codeberg\.org|git\.sr\.ht)/[^\s]+`) + matches = generalRe.FindStringSubmatch(repoData.Desc) + + if len(matches) >= 1 { + // 返回找到的代码托管平台链接 + return true, matches[0] + } + + // 如果没有找到具体链接,但确实是镜像,返回true和空字符串 + return true, "" + } + + return false, "" +} diff --git a/cmd/gitlink_check/main.go b/cmd/gitlink_check/main.go index f3f4393e..a251bee6 100644 --- a/cmd/gitlink_check/main.go +++ b/cmd/gitlink_check/main.go @@ -1,88 +1,93 @@ +/* +Git链接检查器 +--------------------------------- +批量检查 Excel 文件中的 Git 仓库链接可访问性,支持 HTTP 和 Git 两种方式。 +结果实时输出,并将失败链接保存到新的 Excel 文件。 + +主要功能: +1. 读取 Excel 文件 git_link 列的 URL +2. 并发检查 HTTP 和 Git 连通性 +3. 输出检查结果和失败报告 + +作者:zjy +日期:2025年7月18日 +*/ + package main import ( - "bytes" - "context" - "fmt" - "log" - "net/http" - "os" - "os/exec" - "strings" - "sync" - "time" - - "github.com/xuri/excelize/v2" + "bytes" // 字节缓冲区 + "context" // 控制超时和取消 + "fmt" // 格式化输出 + "log" // 日志 + "net/http" // HTTP 客户端 + "os" // 系统接口 + "os/exec" // 外部命令 + "strings" // 字符串处理 + "sync" // 并发同步 + "time" // 时间相关 + + "github.com/xuri/excelize/v2" // Excel库 ) -// URLCheckResult 表示URL检查结果 +// 单个URL的检查结果,包含HTTP和Git两种方式 type URLCheckResult struct { - URL string - HTTPSuccess bool - HTTPError string - GitSuccess bool - GitError string - OverallStatus string + URL string // 检查的URL + HTTPSuccess bool // HTTP是否成功 + HTTPError string // HTTP错误或成功信息 + GitSuccess bool // Git是否成功 + GitError string // Git错误或成功信息 + OverallStatus string // 综合状态 } -// 优化后的超时设置 +// 检查HTTP连通性,优先HEAD,失败再GET,3秒超时 func checkHTTPConnectivity(ctx context.Context, url string) (bool, string) { client := &http.Client{ - Timeout: 10 * time.Second, // 增加到10秒,给网络更多时间 + Timeout: 3 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) >= 5 { return fmt.Errorf("重定向次数过多") } - return nil // 允许重定向 + return nil }, } - - // 先尝试HEAD请求(更快),失败再用GET methods := []string{"HEAD", "GET"} - for i, method := range methods { req, err := http.NewRequestWithContext(ctx, method, url, nil) if err != nil { - if i == len(methods)-1 { // 最后一次尝试 + if i == len(methods)-1 { return false, fmt.Sprintf("创建请求失败: %v", err) } continue } - - // 添加 GitHub 友好的 User-Agent req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") if method == "GET" { req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8") } - resp, err := client.Do(req) if err != nil { - if i == len(methods)-1 { // 最后一次尝试 + if i == len(methods)-1 { return false, fmt.Sprintf("请求失败 (%s): %v", method, err) } - continue // 尝试下一个方法 + continue } defer resp.Body.Close() - - // GitHub 等服务可能返回 200-399 都是有效的响应 if resp.StatusCode >= 200 && resp.StatusCode < 400 { return true, fmt.Sprintf("状态码 %d (%s)", resp.StatusCode, method) } - - if i == len(methods)-1 { // 最后一次尝试 + if i == len(methods)-1 { return false, fmt.Sprintf("状态码 %d (%s)", resp.StatusCode, method) } } - return false, "所有HTTP方法都失败" } -// checkGitRemote 使用git ls-remote命令来验证一个git仓库是否可访问 +// 使用 git ls-remote 检查Git仓库可访问性,3秒超时 func checkGitRemote(ctx context.Context, repoURL string) (bool, string) { if repoURL == "" { return false, "URL 不能为空" } - // 自动补全 .git + // 自动补.git后缀 if (strings.HasPrefix(repoURL, "https://github.com/") || strings.HasPrefix(repoURL, "https://gitlab.") || strings.HasPrefix(repoURL, "https://sourceware.org/") || @@ -90,28 +95,21 @@ func checkGitRemote(ctx context.Context, repoURL string) (bool, string) { !strings.HasSuffix(repoURL, ".git") { repoURL += ".git" } - - // 为Git命令创建更短的超时 - gitCtx, cancel := context.WithTimeout(ctx, 8*time.Second) + gitCtx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() - cmd := exec.CommandContext(gitCtx, "git", "ls-remote", repoURL) cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", - "GIT_SSH_COMMAND=ssh -o ConnectTimeout=5", // SSH连接超时 + "GIT_SSH_COMMAND=ssh -o ConnectTimeout=5", ) - var stderr bytes.Buffer cmd.Stderr = &stderr - err := cmd.Run() errorMsg := stderr.String() if err != nil { - // 如果只是重定向 warning,也算成功 if strings.Contains(errorMsg, "warning: redirecting to") { return true, "Git仓库可访问(重定向)" } - // 只要没有 "not found" 或 "fatal",也算成功 if !strings.Contains(errorMsg, "not found") && !strings.Contains(errorMsg, "fatal") { return true, "Git仓库可访问(exit status 非0但无致命错误)" } @@ -120,37 +118,25 @@ func checkGitRemote(ctx context.Context, repoURL string) (bool, string) { } return false, fmt.Sprintf("命令执行失败: %v, 错误详情: %s", err, strings.TrimSpace(errorMsg)) } - return true, "Git仓库可访问" } -// checkURL 综合检查URL的HTTP连通性和Git可访问性 +// 并发检查单个URL的HTTP和Git连通性,3秒超时 func checkURL(url string) URLCheckResult { result := URLCheckResult{URL: url} - - // 减少总超时时间 - ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - - // 并发执行HTTP和Git检查 var wg sync.WaitGroup wg.Add(2) - - // HTTP检查 go func() { defer wg.Done() result.HTTPSuccess, result.HTTPError = checkHTTPConnectivity(ctx, url) }() - - // Git检查 go func() { defer wg.Done() result.GitSuccess, result.GitError = checkGitRemote(ctx, url) }() - wg.Wait() - - // 确定整体状态 if result.HTTPSuccess && result.GitSuccess { result.OverallStatus = "完全成功" } else if !result.HTTPSuccess && !result.GitSuccess { @@ -160,47 +146,42 @@ func checkURL(url string) URLCheckResult { } else { result.OverallStatus = "Git失败" } - return result } +// 主流程:读取Excel,批量检查URL,输出结果和失败报告 func main() { - // 检查 'git' 命令是否存在 + // 检查Git命令 if _, err := exec.LookPath("git"); err != nil { log.Fatalf("错误: 'git' 命令未找到或不在系统的 PATH 中。请先安装 Git。") } - - // 检查命令行参数 + // 检查参数 if len(os.Args) < 2 { fmt.Println("请提供XLSX文件路径作为参数") fmt.Println("示例: go run gitlink_check.go input.xlsx") return } filePath := os.Args[1] - - // 打开Excel文件 + // 打开Excel f, err := excelize.OpenFile(filePath) if err != nil { fmt.Printf("打开文件失败: %v\n", err) return } defer f.Close() - - // 获取第一个工作表名 + // 获取第一个工作表 sheetName := f.GetSheetName(0) if sheetName == "" { fmt.Println("未找到工作表") return } - // 读取所有行 rows, err := f.GetRows(sheetName) if err != nil { fmt.Printf("读取工作表失败: %v\n", err) return } - - // 查找git_link列的索引 + // 查找git_link列 colIndex := -1 if len(rows) > 0 { for i, cell := range rows[0] { @@ -210,16 +191,14 @@ func main() { } } } - if colIndex == -1 { fmt.Println("在工作表中未找到 'git_link' 列") return } - - // 收集URL(跳过标题行) + // 收集所有URL var urls []string for i, row := range rows { - if i == 0 { // 跳过标题行 + if i == 0 { continue } if colIndex < len(row) { @@ -229,75 +208,55 @@ func main() { } } } - if len(urls) == 0 { fmt.Println("在 'git_link' 列中未找到任何有效的 URL") return } - fmt.Printf("共发现 %d 个Git链接,开始检查HTTP连通性和Git可访问性...\n\n", len(urls)) - - // 并发控制 + // 并发检查 var wg sync.WaitGroup results := make(chan URLCheckResult, len(urls)) semaphore := make(chan struct{}, 10) // 限制并发数为10 - - // 启动goroutine检查每个URL for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() - semaphore <- struct{}{} // 获取信号量 - defer func() { <-semaphore }() // 释放信号量 - + semaphore <- struct{}{} + defer func() { <-semaphore }() result := checkURL(u) results <- result }(url) } - - // 等待所有任务完成 go func() { wg.Wait() close(results) }() - - // 收集结果 + // 输出结果 var allResults []URLCheckResult var failedResults []URLCheckResult - for result := range results { allResults = append(allResults, result) - - // 实时打印结果 if result.OverallStatus == "完全成功" { fmt.Printf("[成功 ✅] %s: HTTP和Git都可访问\n", result.URL) } else { fmt.Printf("[失败 ❌] %s: %s\n", result.URL, result.OverallStatus) - if result.OverallStatus != "完全成功" { - fmt.Printf(" HTTP: %s, Git: %s\n", - getStatusText(result.HTTPSuccess, result.HTTPError), - getStatusText(result.GitSuccess, result.GitError)) - } + fmt.Printf(" HTTP: %s, Git: %s\n", + getStatusText(result.HTTPSuccess, result.HTTPError), + getStatusText(result.GitSuccess, result.GitError)) failedResults = append(failedResults, result) } } - fmt.Printf("\n所有链接检查完成。成功: %d, 失败: %d\n", len(allResults)-len(failedResults), len(failedResults)) - - // 保存失败结果到Excel + // 保存失败报告 if len(failedResults) > 0 { outF := excelize.NewFile() sheet := outF.GetSheetName(0) outF.SetSheetName(sheet, "FailedLinks") - - // 设置标题行 outF.SetCellValue("FailedLinks", "A1", "failed_git_link") outF.SetCellValue("FailedLinks", "B1", "failure_type") outF.SetCellValue("FailedLinks", "C1", "http_status") outF.SetCellValue("FailedLinks", "D1", "git_status") - - // 写入失败数据 for i, result := range failedResults { row := i + 2 outF.SetCellValue("FailedLinks", fmt.Sprintf("A%d", row), result.URL) @@ -307,7 +266,6 @@ func main() { outF.SetCellValue("FailedLinks", fmt.Sprintf("D%d", row), getStatusText(result.GitSuccess, result.GitError)) } - outputFilename := "failed_links_output.xlsx" if err := outF.SaveAs(outputFilename); err != nil { fmt.Printf("保存 %s 失败: %v\n", outputFilename, err) @@ -319,7 +277,7 @@ func main() { } } -// getStatusText 根据成功状态和错误信息返回状态文本 +// 格式化输出检查状态 func getStatusText(success bool, errorMsg string) string { if success { return "成功: " + errorMsg