Git相关:Gitee的PR显示权限形同虚设?

838 阅读4分钟

Git相关:Gitee的PR显示权限形同虚设?

Pull Request介绍

当我们参与开源项目或者是任意一个其他的项目的时候,我们是无法直接对其他人的Git远程仓库进行修改的,一般需要经过以下几个步骤:

  1. fork别人的原仓库,也就是拷贝一份跟别人一模一样的内容到自己的远程仓库里。
  2. 从自己的远程仓库clone到本地,在本地完成修改后push自己的远程仓库当中。
  3. 别人的原仓库提交Pull Request,请求将自己新增的部分合并到别人的仓库中。
  4. 原仓库的所有者测试你提交的Pull Request,同意合并。

简单来讲,就是我们告诉原仓库的所有者,我们对你的仓库的内容进行了部分的新增或者更改,你将新的内容合并过去吧!

Gitee的PR显示权限

一般情况下开发者提交 PR 时,如果是公开项目那么这个 PR 是对所有人都可见的,如果是私有项目,PR 对项目组成员都可见。这样无法避免学生在提交作业或者代码比赛时相互借鉴的情况。

为了方便老师使用收集作业或者进行代码考试、比赛,码云平台新增功能 —— Pull Request 显示权限设置。

启用方式:[项目主页] -> 管理 -> 基本设置 -> 开启的 Pull Requests 显示权限。如下图所示:

1.jpg

一旦启用该设置,那么开发者提交的 PR 只有管理员、审查者以及测试者可见,其他开发者不可见。此功能主要用于高校版中,敦促学生独立完成作业。

拉取Gitee的PR内容到本地仓库

我们首先查看一个未设置PR权限的仓库的PR内容,

2.png

点开任意一个PR,可以通过右上角的克隆按钮,查看到拉取到本地的命令:

3.png

这个命令为:

git fetch https://gitee.com/<username>/public.git pull/4/head:pr_4

构建拉取其他PR内容的命令

当你使用一个没有权限的账号去查看PR内容的时候,会发现被403了,无法查看内容,也就无法获取克隆命令了:

7.png

但是,观察之前的命令我们会发现,命令中的"4"是我们的提交序号,也就是说,我们只需要将4替换成其他的序号,就可以构建出拉取其他PR内容的命令了。

我们通过观察PR列表页面的内容,可以从中获取到所有PR的序号,如下图所示:

4.png

我们在这个序号处右击,弹出浏览器菜单,选择"检查",可以看到html内容:

5.png

我们关注它的某一级父标签含有git-pull-requestsid属性,它自身是在一个span标签当中,我们通过CSS选择器的语法#git-pull-requests>div>div>span,可以找出这个标签。

由于一共有三类PR:开启的已合并已关闭,所以我们需要过滤已合并已关闭的PR。

通过查看html内容可以看出:

6.png

另外两类PR的class属性里分别含有closedmerged,我们可以通过这个将其过滤掉。

GO语言实现

由于最近转型到GO语言了,因此决定用GO语言来实现一下这个自动化拉取所有PR内容的小工具。

解析html内容的包选择了"github.com/PuerkitoBio/goquery"

核心代码:

// url里放的是远程仓库的地址,如:https://gitee.com/<username>/repository
resp, _ := http.Get(url + "/pulls?page=")
defer resp.Body.Close()

// 使用GoQuery库来解析PR列表的html页面
doc, _ := goquery.NewDocumentFromReader(resp.Body)

// 第一个判断以"!"开头的字符串,用于定位当前提交的序号
// 第二个判断过滤掉已关闭的PR
// 第三个判断过滤掉已合并的PR
// 最后构建拉取命令,并执行
doc.Find("#git-pull-requests>div>div>span").Each(func(i int, selection *goquery.Selection) {
    if strings.HasPrefix(selection.Text(), "!") &&
    !selection.Parent().Parent().HasClass("closed") &&
    !selection.Parent().Parent().HasClass("merged") {
        s := selection.Text()[1:]
        branches = append(branches, "pr_" + s)
        fmt.Println("git", "fetch", url + ".git", "pull/" + s + "/head:pr_" + s)
        cmd := exec.Command("git", "fetch", url + ".git", "pull/" + s + "/head:pr_" + s)
        cmd.Dir = path
        _ = cmd.Run()
    }
})

完整代码

由于PR列表一页仅能显示6个,所以完整的代码里增加了对于分页的判断。

package main

import (
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
)

var (
	url string
	path string
)

func main() {
	// 按回车键退出
	defer func() {
		fmt.Println("press enter to exit...")
		var input string
		_, _ = fmt.Scanln(&input)
	}()

	// 命令行参数判断
	if len(os.Args) < 3 {
		fmt.Println("key parameter required")
		fmt.Println("need repository url and local git directory name")
		fmt.Println("eg. EasyPrCheck.exe https://gitee.com/username/repository D:/git/helloworld")
		return
	}

	// 获取命令行参数
	url = os.Args[1]
	path = os.Args[2]
	
	// 检测本地仓库路径是否为绝对路径
	if !filepath.IsAbs(path) {
		currentPath, _ := os.Getwd()
		path = currentPath + string(filepath.Separator) + path
	}

	fmt.Println("url : " + url)
	fmt.Println("path : " + path)

	// 检查目录是否存在
	if _, err := os.Stat(path); os.IsNotExist(err) {
		fmt.Println("mkdir : " + path)
		_ = os.Mkdir(path, os.ModePerm)
	}

	// 初始化仓库
	fmt.Println("git", "init")
	cmd := exec.Command("git", "init")
	cmd.Dir = path
	_ = cmd.Run()

	// 拉取最新的内容
	fmt.Println("git", "pull", url + ".git")
	cmd = exec.Command("git", "pull", url + ".git")
	cmd.Dir = path
	_ = cmd.Run()

	resp, _ := http.Get(url + "/pulls?page=")
	defer resp.Body.Close()

	var branches []string

	// 拉取所有的pr到本地并新建分支
	doc, _ := goquery.NewDocumentFromReader(resp.Body)

	// 分页
	max := 1
	doc.Find("#git-discover-page>a").Each(func(i int, selection *goquery.Selection) {
		if num, err := strconv.ParseInt(selection.Text(), 10, 32); err == nil {
			if int(num) > max {
				max = int(num)
			}
		}
	})
	fmt.Println("total pages :", max)

	for i := 1; i <= max; i++ {
		resp, _ := http.Get(url + "/pulls?page=" + strconv.Itoa(i))

		// 拉取所有的pr到本地并新建分支
		doc, _ := goquery.NewDocumentFromReader(resp.Body)

		doc.Find("#git-pull-requests>div>div>span").Each(func(i int, selection *goquery.Selection) {
			if strings.HasPrefix(selection.Text(), "!") &&
				!selection.Parent().Parent().HasClass("closed") &&
				!selection.Parent().Parent().HasClass("merged") {
				s := selection.Text()[1:]
				branches = append(branches, "pr_" + s)
				fmt.Println("git", "fetch", url + ".git", "pull/" + s + "/head:pr_" + s)
				cmd := exec.Command("git", "fetch", url + ".git", "pull/" + s + "/head:pr_" + s)
				cmd.Dir = path
				_ = cmd.Run()
			}
		})

		resp.Body.Close()
	}


	// 切换至主分支
	fmt.Println("git", "checkout", "master")
	cmd = exec.Command("git", "checkout", "master")
	cmd.Dir = path
	_ = cmd.Run()

	// 合并所有分支至主分支
	for _, branch := range branches {
		fmt.Println("git", "merge", branch)
		cmd = exec.Command("git", "merge", branch)
		cmd.Dir = path
		_ = cmd.Run()
	}

	fmt.Println("finished!")

}