Git相关:Gitee的PR显示权限形同虚设?
Pull Request介绍
当我们参与开源项目或者是任意一个其他的项目的时候,我们是无法直接对其他人的Git远程仓库进行修改的,一般需要经过以下几个步骤:
fork别人的原仓库,也就是拷贝一份跟别人一模一样的内容到自己的远程仓库里。- 从自己的远程仓库
clone到本地,在本地完成修改后push到自己的远程仓库当中。 - 向别人的原仓库提交
Pull Request,请求将自己新增的部分合并到别人的仓库中。 - 原仓库的所有者测试你提交的
Pull Request,同意合并。
简单来讲,就是我们告诉原仓库的所有者,我们对你的仓库的内容进行了部分的新增或者更改,你将新的内容合并过去吧!
Gitee的PR显示权限
一般情况下开发者提交 PR 时,如果是公开项目那么这个 PR 是对所有人都可见的,如果是私有项目,PR 对项目组成员都可见。这样无法避免学生在提交作业或者代码比赛时相互借鉴的情况。
为了方便老师使用收集作业或者进行代码考试、比赛,码云平台新增功能 —— Pull Request 显示权限设置。
启用方式:[项目主页] -> 管理 -> 基本设置 -> 开启的 Pull Requests 显示权限。如下图所示:
一旦启用该设置,那么开发者提交的 PR 只有管理员、审查者以及测试者可见,其他开发者不可见。此功能主要用于高校版中,敦促学生独立完成作业。
拉取Gitee的PR内容到本地仓库
我们首先查看一个未设置PR权限的仓库的PR内容,
点开任意一个PR,可以通过右上角的克隆按钮,查看到拉取到本地的命令:
这个命令为:
git fetch https://gitee.com/<username>/public.git pull/4/head:pr_4
构建拉取其他PR内容的命令
当你使用一个没有权限的账号去查看PR内容的时候,会发现被403了,无法查看内容,也就无法获取克隆命令了:
但是,观察之前的命令我们会发现,命令中的"4"是我们的提交序号,也就是说,我们只需要将4替换成其他的序号,就可以构建出拉取其他PR内容的命令了。
我们通过观察PR列表页面的内容,可以从中获取到所有PR的序号,如下图所示:
我们在这个序号处右击,弹出浏览器菜单,选择"检查",可以看到html内容:
我们关注它的某一级父标签含有git-pull-requests的id属性,它自身是在一个span标签当中,我们通过CSS选择器的语法#git-pull-requests>div>div>span,可以找出这个标签。
由于一共有三类PR:开启的、已合并、已关闭,所以我们需要过滤已合并和已关闭的PR。
通过查看html内容可以看出:
另外两类PR的class属性里分别含有closed和merged,我们可以通过这个将其过滤掉。
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!")
}