关于开源:可视化探索开源项目的-contributor-关系

24次阅读

共计 9897 个字符,预计需要花费 25 分钟才能阅读完成。

引语:作为国内外最大的代码托管平台,依据最新的 GitHub 数据,它领有超 372,000,000 个仓库,其中有 28,000,000 是公开仓。分布式图数据库 NebulaGraph 便是其中之一,同其余开源我的项目一样,NebulaGrpah 也有本人的 contributor 们,他们是何时,通过哪个 pr 与 NebulaGraph 产生分割的呢?本文尝试用可视化形式,来摸索这些 contributor 的痕迹。

世界上有两种需要,一种是能做的,另外一种是不能做的;当然依照正当不合理角度,大多数的需要都是正当但能做的,就像本文的需要一样——用可视化的形式,来“窥探”nebula 开源社区中 contributor 同我的项目的关系,及他们留下的 pr 痕迹。

故事从两个月前讲起,有一天我司研发 liuyu 同学装了一款名叫 ClickHouse 的数据库,他发现 CK 有一个感人的 contributor 零碎表,这不得让咱们的经营来“借鉴”下么?

当初,咱们来看看打动我司研发的 ClickHouse 是怎么样的存在。

让人打动的 ClickHouse Contributor 零碎表

简略来说,只有你装了 CK 数据库,不须要连贯任何数据库,零碎自带一个数据表,你能够执行以下 SQL

select count() from system.contributors

就能失去一个现有的 CK contributor 总量(上面数据存在肯定滞后性):

也能够依照下列形式随机取得 20 位 contributor 名单:

select * from system.contributors limit 20;

这种用 SQL 形式查看 contributor 的形式还挺 cool 的,毕竟 contributor 是一群通过提交 pr 来欠缺、迭代产品的人,其中很大一部分的 contributor 是工程师,SQL 更是信手拈来。

当初问题来了,作为一个不会写 SQL 的经营,如何满足我司研发提出的让他打动一下的 contributor 零碎表?沉着下,ClickHouse 的这个 SQL 看 contributor 的形式诚然很酷,然而终归到底是要查看贡献者同开源我的项目的关系。说到“搞关系”,还不得是咱们的图数据库。巧的是,NebulaGraph 就是一款图数据库,尽管在本文的数据集过于简略用,也不是什么大规模数据,用图数据库有点“杀鸡用牛刀”,但不妨一试。看看,不会写 SQL 的经营怎么用可视化的形式来查看 contributor 和我的项目关系。

看得见的 contributor 和 pr 关系

成果后行,在这个章节,咱们来看下 NebulaGraph 开源社区的 contributor 和 pr 状况,而这些数据是如何生成、展现的实操局部在前面。

开源社区全览

这里收录了所有 NebulaGraph 相干的公开仓的奉献状况,大略是这样的:

加上时序之后,能看到一个个 contributor(方形图)呈现在画布上,同各个 repo(圆形图)连贯在一起。这里仅仅展现了所有 contributor 第一次提交 pr,更多的查问在前面的「可视化图摸索」局部。

上面的章节为实操内容,一起看看如何生成可视化的 contributor 和开源我的项目的关系图吧。

手把手带你可视化摸索数据

上面着重介绍下本文的可视化工具——NebulaGraph Explorer,具体介绍看文档:https://docs.nebula-graph.com.cn/3.4.1/nebula-explorer/about-explorer/ex-ug-what-is-explorer/。对我而言,Explorer 有两大特点:易上手 所见即所得。我能够白嫖我司线上 Explorer 环境,不必搭建本人的数据库就能间接用,当然你如果想和我一样有个收费的线上环境,预计得用 NebulaGraph Cloud,它配有可视化图摸索工具 NebulaGrpah Explorer。

用来进行数据摸索的工具有了,当初就是数据哪里来的问题了。

简略建模

在采集数据之前,咱们须要简略建模(我从未见过如此简略的图模型)理解须要采集的数据。下图为图模型:

这个图模型中有两种点类型:repocontributor,它们之间由 pr 这个边分割在一起形成了最根底的点边图模型。在分布式图数据库 NebulaGraph 中点的类型用 tag 来示意,边类型有 edgetype,一个点能够有若干种 tag,点的 ID 为 vid,像是你的身份证一样为惟一标识。

  • tag

    • repo,领有仓库名 name,次要编程语言 language 以及仓库门路 path 等三种属性;
    • contributor,领有贡献者名 name,贡献者编号 number,诞生日 anniversary,是否为 NebulaGraph 开发商雇员 is_vesoft,第一个被合并 pr 所属仓 first_repo。退出了判断“是否为 NebulaGraph 开发商雇员”的属性是为了防止超大节点,因为一个企业雇员的 pr 产量不同于其余的非雇员贡献者。(这点会在前面的可视化展现中体现)
  • edgetype

    • pr,领有 pr 编号 number,提交工夫 created_time,敞开工夫 closed_time,合并工夫 merged_time,是否被合并 is_merged,变更状况:ins_code_linedes_code_linefile_number。下面的工夫字段能够用来筛选出某个工夫区间里的 pr 边;

contributor 数据采集

上面这段代码是托付我司优良的 IT 工程师乔治编写的,那些须要配置、填上你本人信息的中央,我用正文进行了标注:

# Copyright @Shinji-IkariG
from github import Github
from datetime import datetime
import sh
from sh import curl
import csv
import requests
import time

def main():
# 你的 GitHub ID
    GH_USER = 'xxx'
# 你的集体 token,能够返回 GitHub 设置中的 Developer settings 生成本人的 token
    GH_PAT = 'xxx'
    github = Github(GH_PAT)
# 你须要爬取的开源组织的组织名
    org = github.get_organization('vesoft-inc')
    repos = org.get_repos(type='all', sort='full_name', direction='asc')
# 命名寄存爬下来的 pr 数据的文件
    with open('all-prs.csv', 'w', newline='') as csvfile:
# 爬取哪些数据
        fieldnames = ['pr num','repo','author', 'create date','close date','merged date','version','labels1','state','branch','assignee','reviewed(commented)','reviewd(approved)','request reviewer','code line(+)','code line(-)','files number']
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()


        for repo in repos:
            print(repo)
            Apulls = repo.get_pulls(state='all', sort='created')
            prs = []
            for a in Apulls:
                prs.append(a)

            for i in prs:
                github = Github(GH_PAT)
                print('rate_limite' , github.rate_limiting[0])
                if github.rate_limiting[0] < 500:
                    if github.rate_limiting_resettime - time.time() > 0:
                        time.sleep(github.rate_limiting_resettime - time.time()+900)
                    else:time.sleep(3700)
                else:
                    print(i.number)
                    prUrl = 'https://api.github.com/repos/'+ str(repo.full_name) + '/pulls/' + str(i.number)
                    pr = requests.get(prUrl, auth=(GH_USER, GH_PAT))



                    assigneesList = []
                    if pr.json().get('assignees'):
                        for assignee in pr.json().get('assignees'):
                            assigneesList.append(assignee.get('login'))
                    else: ""



                    reviewerCList = []
                    reviewerAList = []
                    reviewers = requests.get(prUrl + '/reviews', auth=(GH_USER, GH_PAT))
                    if reviewers.json():
                        for reviewer in reviewers.json():
                            if reviewer.get('state') == 'COMMENTED':
                                if reviewer.get('user'): 
                                    reviewerCList.append(reviewer.get('user').get('login'))
                                else: reviewerCList.append('GHOST USER')
                            elif reviewer.get('state') == 'APPROVED':
                                if reviewer.get('user'): 
                                    reviewerAList.append(reviewer.get('user').get('login'))
                                else: reviewerAList.append('GHOST USER')
                            else : print(reviewer.get('state'), 'TYPE REVIEWS')
                    else: ""


                    reqReviewersList = []
                    reqReviewers = requests.get(prUrl + '/requested_reviewers', auth=(GH_USER, GH_PAT))
                    if reqReviewers.json().get('users'):
                        for reqReviewer in reqReviewers.json().get('users'):
                            reqReviewersList.append(reqReviewer.get('login'))
                        print(reqReviewersList)
                    else: ""



                    labelList = []
                    if pr.json().get('labels'):
                        for label in pr.json().get('labels'):
                            labelList.append(label.get('name'))
                    else: ""milestone = pr.json().get('milestone').get('title') if pr.json().get('milestone') else""



                    writer.writerow({'pr num': i.number,'repo': repo.full_name,'author': pr.json().get('user').get('login'), 'create date': pr.json().get('created_at'),'close date': pr.json().get('closed_at'),'merged date': pr.json().get('merged_at'),'version': milestone,'labels1': ",".join(labelList),'state': pr.json().get('state'),'branch': pr.json().get('base').get('ref'),'assignee': ",".join(assigneesList),'reviewed(commented)': ",".join(reviewerCList),'reviewd(approved)': ",".join(reviewerAList),'request reviewer': ",".join(reqReviewersList),'code line(+)': pr.json().get('additions'),'code line(-)': pr.json().get('deletions'),'files number': pr.json().get('changed_files')})

if __name__ == "__main__":
    main()

#pip3 install sh pygithub

等你运行完下面代码,便能失去一个名叫“all-prs.csv”。脚本爬取的是 vesoft-inc(NebulaGraph 开发商)组织下的所有仓,这里并没有辨别仓库状态,这就意味着它也会将公有仓的数据爬取下来。因而,咱们要对数据进行二次解决。这里略过我简略解决数据的过程,解决完的 pr 数据中能够抽取相干的 contributor 数据。

下面提到过每个点都有 vid,因而将 contributor 的 vid 设定为他 / 她的 GitHub ID,repo 的 vid 则采纳缩写,而边的数据中终点和起点就为下面的 contributor vid 和 repo vid。

当初咱们有了,contributor.csv,pr.csv,repo.csv 三个文件,格局相似:

# contributor.csv
wenhaocs,haowen,148,2021-09-24 16:53:33,1,nebula
lopn,lopn,149,2021-09-26 06:02:11,0,nebula-docs-cn
liwenhui-soul,liwenhui-soul,150,2021-09-26 13:38:20,1,nebula
Reid00,Reid00,151,2021-10-08 06:20:24,0,nebula-http-gateway
...

# pr.csv
nevermore3,nebula,4095,2022-03-29 11:23:15,2022-04-13 03:29:44,2022-04-13 03:29:44,1,2310,3979,31
cooper-lzy,docs_cn,1614,2022-03-30 03:21:35,2022-04-07 07:28:31,2022-04-07 07:28:31,1,107,2,4
wuxiaobai24,nebula,4098,2022-03-30 05:51:14,2022-04-11 10:54:04,2022-04-11 10:54:03,1,53,0,3
NicolaCage,website,876,2022-03-30 06:08:02,2022-03-30 06:09:21,2022-03-30 06:09:21,1,4,2,1
...

#repo.csv
clients,nebula-clients,vesoft-inc/nebula-clients,Java
common,nebula-common,vesoft-inc/nebula-common,C++
community,nebula-community,vesoft-inc/nebula-community,Markdown
console,nebula-console,vesoft-inc/nebula-console,Go
...

数据导入

数据导入之前须要创立相干的 Schema 进行数据映射。

创立 Schema

当初咱们须要把图构造模型变成 NebulaGraph 能辨认的 Schema,有两种形式来创立 Schema:一是用查询语言 nGQL 来编写 Schama,另外一种则是用可视化图摸索工具 NebulaGraph Explorer 提供的可视化界面填写信息实现。和我一样对查询语言不相熟的小伙伴,倡议首选后者。

登陆到 NebulaGraph Explorer 之后,先创立一个图空间(相似 MySQL 中的 Table):

成果同上面的 nGQL 语言:

# nebula-contributor-2023 是这个图空间名字,其余默认;CREATE SPACE 'nebula-contributor-2023'(partition_num = 10, vid_type = FIXED_STRING(32))

创立完图空间之后,再创立两个点类型和一个边类型,二者创立形式相似。

上面,以创立绝对简单的 contributor 点类型为例:

同效于这条 nGQL 语句:

CREATE tag contributor (name string NULL, number int16 NULL, anniversary datetime NULL, is_vesoft bool NULL, first_merged string NULL) COMMENT = "贡献者"

同样的 repo 和 pr 边能够用上面的 nGQL 或同上图一样用 Explorer。

# 创立 repo tag
CREATE tag repo (repo_name string NULL, language string NULL, path string NULL) COMMENT = "仓库"

# 创立 pr edge
CREATE edge pr (number int NULL, created_time datetime NULL, closed_time datetime NULL DEFAULT NULL, merged_time datetime NULL DEFAULT NULL, is_merged bool NULL, ins_code_line int NULL, des_code_line int NULL, file_changed_num int NULL)

导入数据

因为用了可视化工具 Explorer,所以上传数据也能够用“看得见的办法”。在创立完 Schema 之后,点击这个右上角的菜单栏“Import”,开始数据导入。

数据源抉择本地,找到下面筹备的 3 个 csv 文件所在门路,把文件上传之后。开始【导入】过程,在这个步骤次要是实现本地数据文件同 Schema 的关联。相似下图:

在整个数据集中,咱们有两种点:vertices 1 关联 repo 的 csv 数据,vertices 2 关联 contributor 数据,指定各自的 VID 和相干属性的所在列之后,就能够导入数据了。在边数据关联这块,因为咱们之前曾经在 csv 中退出了 repo 和 contributor 的各自 VID,所以这里同点的关联一样,简略勾选哪列是终点(Column 0)、哪列是起点(对应上图的 Column 1)。

须要进行非凡阐明的是,因为一个 contributor 和一个 repo 会存在屡次提交 pr 记录,即:多条同 pr 边类型的边。而对同一类型边的解决问题,图数据库 NebulaGraph 引入了 rank 字段来示意两个点之间多条同一类型,但边属性不同的边。如果你不设定 rank,插入多条同一类型边,则会进行数据笼罩操作,以最初胜利插入的边数据为准。

为了偷懒,这里 rank 我间接用了 pr 编号 number 列,认真看,下面的 ranknumber 都是读取的同一列 Column 2 数据。

可视化图摸索

当初咱们有数据了,能够进入到可视化图摸索模式了。

在“Visual Query”菜单下,拖拽两个 tag:contributor 和 repos,抉择 pr 边,【运行】,就能看到所有 contributor 提交的 pr 数据。它的成果等同于上面这句 nGQL 查询语言:

match (v0:contributor) -[e:pr]-> (v1:repo) return e limit 15000

咱们随便退出一点像是上面这种小细节:

咱们把点的头像全副换下,这里为了节省时间找研发小哥龙仔开了个绿色通道批量上传了 contributor 和 repo 点的头像。当初,整图的成果展现是这样的:

因为 nebula 最大的奉献来源于其雇员(员工),所以这里咱们除去雇员,查看下 非雇员的奉献状况,成果同查询语言:

match (v0:contributor) -[e:pr]-> (v1:repo) where (v0.contributor.is_vesoft == false) return e limit 15000

上图是将 nGQL 查问后果导入到画布,对应的 NebulaGraph Explorer 操作为点击【导入图摸索】,再进行同类型边合并,放大 contributor 点的大小,抉择辐射模式,就出现了最终成果:

看看仓库编程语言为 C++、Python、Go、Java 各自的贡献者状况

能够看到,内核仓 nebula 采纳了 C++,不少相干的周边工具也用了 C++。因而,整个开源我的项目中 C++ 的贡献者(点)还是比拟多的。反之,目前只有 Python 客户端 nebula-python、同步工具 auto_sync 和装置工具 nebula-ansible 应用 Python 语言开发,因而相较于其余编程语言,contributor 数量并不多。

说到内核仓,咱们来看看 内核仓 nebula 的非雇员贡献者 状况:

通过合并同类型 pr 边,依据边的粗细咱们能够看到外围仓的沉闷贡献者。注意下面那个 Java logo 的图像,并非是 nebula 同 Java 联谊了,而是 2020 年的 Committer ChenXU 用了 Java 的 logo 作为头像(狗头)。

再来看看 2021 年诞生的非雇员 contributor 他们的奉献状况

最初,来看看有 哪些 pr 还没被 merge,这里须要用到 pr 边的 is_merged 属性(记得创立个索引哦~):

祝下面所有未被 merged 的 pr 都能被合并(尽管这是不可能的)。

nGQL 合集

这里是下面所有查问后果的对应 nGQL 查问语句:

# 查看各个查询语言的开源仓库奉献状况
match (v0:contributor) -[e:pr]-> (v1:repo) where (v1.repo.language == "C++") return e

match (v0:contributor) -[e:pr]-> (v1:repo) where (v1.repo.language == "Python") return e

match (v0:contributor) -[e:pr]-> (v1:repo) where (v1.repo.language == "Go") return e

match (v0:contributor) -[e:pr]-> (v1:repo) where (v1.repo.language == "Java") return e

# 内核仓 nebula 的非雇员贡献者

match (v0:contributor) -[e:pr]-> (v1:repo) where (v1.repo.repo_name == "nebula" and v0.contributor.is_vesoft == false) return e

# 2021 年诞生的非雇员 contributor
match (v0:contributor) -[e:pr]-> (v1:repo) where (v0.contributor.anniversary >= datetime("2021-01-01T00:00:00") and v0.contributor.anniversary < datetime("2022-01-01T00:00:00")  ) and v0.contributor.is_vesoft ==false return e

# 目前未被合并的 pr
match (v0:contributor) -[e:pr]-> (v1:repo) where (e.is_merged == false) return e

数据集

本数据集为 NebulaGraph 公开仓数据,统计截止工夫为 2023.03.20。因为局部 datetime 属性不能为空,为空字段人为填充了为 2038-01-19 03:14:07(timestamp 类型下限)。如果你要应用该数据集,记得注意 datetime 属性值的解决。

数据集下载地址:nebula-contributor-dataset

最初,以此文感激所有 nebula 社区的 contributor 们 lol


谢谢你读完本文 (///▽///)

正文完
 0