天天看點

可視化探索開源項目的 contributor 關系

作者:NebulaGraph
可視化探索開源項目的 contributor 關系
引語:作為國内外最大的代碼托管平台,根據最新的 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 總量(下面資料存在一定滞後性):

可視化探索開源項目的 contributor 關系

也可以按照下列方式随機獲得 20 位 contributor 名單:

select * from system.contributors limit 20;
           
可視化探索開源項目的 contributor 關系

這種用 SQL 方式檢視 contributor 的方式還挺 cool 的,畢竟 contributor 是一群通過送出 pr 來完善、疊代産品的人,其中很大一部分的 contributor 是工程師,SQL 更是信手拈來。

現在問題來了,作為一個不會寫 SQL 的營運,如何滿足我司研發提出的讓他感動一下的 contributor 系統表?冷靜下,ClickHouse 的這個 SQL 看 contributor 的方式固然很酷,但是終歸到底是要檢視貢獻者同開源項目的關系。說到 “搞關系”,還不得是我們的圖資料庫。巧的是,NebulaGraph 就是一款圖資料庫,雖然在本文的資料集過于簡單用,也不是什麼大規模資料,用圖資料庫有點 “殺雞用牛刀”,但不妨一試。看看,不會寫 SQL 的營運怎麼用可視化的方式來檢視 contributor 和項目關系。

看得見的 contributor 和 pr 關系

效果先行,在這個章節,我們來看下 NebulaGraph 開源社群的 contributor 和 pr 情況,而這些資料是如何生成、展示的實操部分在後面。

開源社群全覽

這裡收錄了所有 NebulaGraph 相關的公開倉的貢獻情況,大概是這樣的:

可視化探索開源項目的 contributor 關系

加上時序之後,能看到一個個 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。

用來進行資料探索的工具有了,現在就是資料哪裡來的問題了。

簡單模組化

在采集資料之前,我們需要簡單模組化(我從未見過如此簡單的圖模型)了解需要采集的資料。下圖為圖模型:

可視化探索開源項目的 contributor 關系

這個圖模型中有兩種點類型:repo 和 contributor,它們之間由 pr 這個邊聯系在一起構成了最基礎的點邊圖模型。在分布式圖資料庫 NebulaGraph 中點的類型用 tag 來表示,邊類型有 edgetype,一個點可以有若幹種 tag,點的 ID 為 vid,像是你的身份證一樣為唯一辨別。

  • tagrepo,擁有倉庫名 name,主要程式設計語言 language 以及倉庫路徑 path 等三種屬性;contributor,擁有貢獻者名 name,貢獻者編号 number,誕生日 anniversary,是否為 NebulaGraph 開發商雇員 is_vesoft,第一個被合并 pr 所屬倉 first_repo。加入了判斷 “是否為 NebulaGraph 開發商雇員” 的屬性是為了避免超大節點,因為一個企業雇員的 pr 産量不同于其他的非雇員貢獻者。(這點會在後面的可視化展示中展現)
  • edgetypepr,擁有 pr 編号 number,送出時間 created_time,關閉時間 closed_time,合并時間 merged_time,是否被合并 is_merged,變更情況:ins_code_line、des_code_line、file_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):

可視化探索開源項目的 contributor 關系

效果同下面的 nGQL 語言:

# nebula-contributor-2023 是這個圖空間名字,其他預設;
CREATE SPACE 'nebula-contributor-2023'(partition_num = 10, vid_type = FIXED_STRING(32))
           

建立完圖空間之後,再建立兩個點類型和一個邊類型,二者建立方式類似。

下面,以建立相對複雜的 contributor 點類型為例:

可視化探索開源項目的 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 的關聯。類似下圖:

可視化探索開源項目的 contributor 關系

在整個資料集中,我們有兩種點: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 列,仔細看,上面的 rank 和 number 都是讀取的同一列 Column 2 資料。

可視化圖探索

現在我們有資料了,可以進入到可視化圖探索模式了。

可視化探索開源項目的 contributor 關系

在 “Visual Query” 菜單下,拖拽兩個 tag:contributor 和 repos,選擇 pr 邊,【運作】,就能看到所有 contributor 送出的 pr 資料。它的效果等同于下面這句 nGQL 查詢語言:

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

我們随意加入一點像是下面這種小細節:

可視化探索開源項目的 contributor 關系

我們把點的頭像全部換下,這裡為了節省時間找研發小哥龍仔開了個綠色通道批量上傳了 contributor 和 repo 點的頭像。現在,整圖的效果展示是這樣的:

可視化探索開源項目的 contributor 關系

因為 nebula 最大的貢獻來源于其雇員(員工),是以這裡我們除去雇員,檢視下非雇員的貢獻情況,效果同查詢語言:

match (v0:contributor) -[e:pr]-> (v1:repo) where (v0.contributor.is_vesoft == false) return e limit 15000
           
可視化探索開源項目的 contributor 關系

上圖是将 nGQL 查詢結果導入到畫布,對應的 NebulaGraph Explorer 操作為點選【導入圖探索】,再進行同類型邊合并,放大 contributor 點的大小,選擇輻射模式,就呈現了最終效果:

可視化探索開源項目的 contributor 關系

看看倉庫程式設計語言為 C++、Python、Go、Java 各自的貢獻者情況:

可視化探索開源項目的 contributor 關系
可視化探索開源項目的 contributor 關系
可視化探索開源項目的 contributor 關系
可視化探索開源項目的 contributor 關系

可以看到,核心倉 nebula 采用了 C++,不少相關的周邊工具也用了 C++。是以,整個開源項目中 C++ 的貢獻者(點)還是比較多的。反之,目前隻有 Python 用戶端 nebula-python、同步工具 auto_sync 和安裝工具 nebula-ansible 使用 Python 語言開發,是以相較于其他程式設計語言,contributor 數量并不多。

說到核心倉,我們來看看核心倉 nebula 的非雇員貢獻者情況:

可視化探索開源項目的 contributor 關系

通過合并同類型 pr 邊,根據邊的粗細我們可以看到核心倉的活躍貢獻者。留意上面那個 Java logo 的圖像,并非是 nebula 同 Java 聯誼了,而是 2020 年的 Committer ChenXU 用了 Java 的 logo 作為頭像(狗頭)。

再來看看 2021 年誕生的非雇員 contributor 他們的貢獻情況:

可視化探索開源項目的 contributor 關系

最後,來看看有哪些 pr 還沒被 merge,這裡需要用到 pr 邊的 is_merged 屬性(記得建立個索引哦~):

可視化探索開源項目的 contributor 關系
可視化探索開源項目的 contributor 關系

祝上面所有未被 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

謝謝你讀完本文 (///▽///)

繼續閱讀