Git之深入解析如何在應用中嵌入Git
一、前言
到目前為止,我們已經了解了 Git 基本的運作機制和使用方式,學習了許多 Git 提供的工具簡單且有效地使用它,可以高效地幫助我們工作,提升我們的效率。
如果還不清楚 Git 的基礎使用流程、分支的管理、托管服務器的技術以及分布式工作流程等相關的技術和能力,請參考:
Git之深入解析Git的安裝流程與初次運行Git前的環境配置;
Git之深入解析本地倉庫的基本操作·倉庫的獲取更新和提交歷史的查看撤銷以及標簽別名的使用;
Git之深入解析Git的殺手級特性·分支管理與變基的開發工作流以及遠程分支的跟蹤;
Git之深入解析如何運行自己的Git倉庫托管服務器;
Git之深入解析如何使用Git的分布式工作流程與如何管理多人開發貢獻的項目。
Git 的相關工具,請參考:
Git之深入解析如何選擇修訂的版本;
Git之深入解析如何交互式暫存;
Git之深入解析如何貯藏工作分支與清理工作目錄;
Git之深入解析如何通過GPG簽署和驗證工作;
Git之深入解析如何重寫提交歷史;
Git之深入解析reset命令原理以及與checkout命令的區別;
Git之深入解析高級合并;
Git之深入解析Rerere重用記錄的解決方案;
Git之深入解析如何使用Git調試項目源碼中的問題;
Git之深入解析在沒有合適的網絡或者可共享倉庫情況下的git bundle打包操作;
Git之深入解析如何替換數據庫中的Git對象;
Git之深入解析憑證存儲;
Git 的內部原理分析,請參考:
Git內部原理之深入解析Git對象;
Git內部原理之深入解析Git的引用和包文件;
Git內部原理之深入解析引用規范;
Git內部原理之深入解析傳輸協議;
Git內部原理之深入解析維護與數據恢復;
Git內部原理之深入解析環境變量。
如果我們的應用程序的目標用戶是開發者,那么在其中集成源碼控制功能會讓他們從中受益,甚至對于文檔編輯器等并非面向程序員的應用,也可以從版本控制系統中受益,Git 的工作模式在多種場景下表現得都非常出色。
如果想將 Git 整合進我們的應用程序中,那么通常有兩種可行的選擇:啟動 shell 來調用 Git 的命令行程序,或者將 Git 庫嵌入到應用中。
二、命令行 Git 方式
一種方式就是啟動一個 shell 進程并在里面使用 Git 的命令行工具來完成任務,這種方式看起來很循規蹈矩,但是它的優點也因此而來,就是支持所有的 Git 的特性。它也碰巧相當簡單,因為幾乎所有運行時環境都有一個相對簡單的方式來調用一個帶有命令行參數的進程,然而這種方式也有一些固有的缺點。
首先就是所有的輸出都是純文本格式,這意味著你將被迫解析 Git 的有時會改變的輸出格式,以隨時了解它工作的進度和結果。更糟糕的是,這可能是毫無效率并且容易出錯的。
另外一個就是令人捉急的錯誤修復能力,如果一個版本庫被莫名其妙地損毀,或者用戶使用了一個奇奇怪怪的配置,Git 只會簡單地拒絕進行一些操作。
還有一個就是進程的管理,Git 會要求在一個獨立的進程中維護一個 shell 環境,這可能會無謂地增加復雜性,試圖協調許許多多的類似的進程(尤其是在某些情況下,當不同的進程在訪問相同的版本庫時)是對我們的能力的極大挑戰。
三、Libgit2
另外一種可以供使用的是 Libgit2,Libgit2 是一個 Git 的非依賴性的工具,它致力于為其他程序使用 Git 提供更好的 API,可以在 libgit2 中找到它。
首先,來看一下 C API 長啥樣,這是一個旋風式旅行:
// 打開一個版本庫 git_repository *repo; int error = git_repository_open(&repo, "/path/to/repository"); // 逆向引用 HEAD 到一個提交 git_object *head_commit; error = git_revparse_single(&head_commit, repo, "HEAD^{commit}"); git_commit *commit = (git_commit*)head_commit; // 顯示這個提交的一些詳情 printf("%s", git_commit_message(commit)); const git_signature *author = git_commit_author(commit); printf("%s <%s>\n", author->name, author->email); const git_oid *tree_id = git_commit_tree_id(commit); // 清理現場 git_commit_free(commit); git_repository_free(repo);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
前兩行打開一個 Git 版本庫,這個 git_repository 類型代表了一個在內存中帶有緩存的指向一個版本庫的句柄。這是最簡單的方法,只是必須知道一個版本庫的工作目錄或者一個 .git 文件夾的精確路徑;另外還有 git_repository_open_ext,它包括了帶選項的搜索,git_clone 及其同類可以用來做遠程版本庫的本地克隆,git_repository_init 則可以創建一個全新的版本庫。
第二段代碼使用了一種 rev-parse 語法(了解更多,請參考 Git之深入解析如何選擇修訂的版本的“分支引用”)來得到 HEAD 真正指向的提交,返回類型是一個 git_object 指針,它指代位于版本庫里的 Git 對象數據庫中的某個東西。git_object 實際上是幾種不同的對象的“父”類型,每個“子”類型的內存布局和 git_object 是一樣的,所以能安全地把它們轉換為正確的類型。在上面的例子中,git_object_type(commit) 會返回 GIT_OBJ_COMMIT,所以轉換成 git_commit 指針是安全的。
如下展示了如何訪問一個提交的詳情,最后一行使用了 git_oid 類型,這是 Libgit2 用來表示一個 SHA-1 哈希的方法。從這個例子中,可以看到一些模式:
如果聲明了一個指針,并在一個 Libgit2 調用中傳遞一個引用,那么這個調用可能返回一個 int 類型的錯誤碼,值 0 表示成功,比它小的則是一個錯誤;
如果 Libgit2 為我們填入一個指針,那么我們有責任釋放它;
如果 Libgit2 在一個調用中返回一個 const 指針,我們不需要釋放它,但是當它所指向的對象被釋放時它將不可用;
用 C 來寫有點蛋疼。
最后一點意味著應該不會在使用 Libgit2 時編寫 C 語言程序。但幸運的是,有許多可用的各種語言的綁定,能在特定的語言和環境中更加容易的操作 Git 版本庫。我們來看一下下面這個用 Libgit2 的 Ruby 綁定寫成的例子,它叫 Rugged,可以在 Rugged 找到它:
repo = Rugged::Repository.new('path/to/repository') commit = repo.head.target puts commit.message puts "#{commit.author[:name]} <#{commit.author[:email]}>" tree = commit.tree
1
2
3
4
5
可以發現,代碼看起來更加清晰了。首先,Rugged 使用異常機制,它可以拋出類似于 ConfigError 或者 ObjectError 之類的東西來告知錯誤的情況;其次,不需要明確資源釋放,因為 Ruby 是支持垃圾回收的。來看一個稍微復雜一點的例子:從頭開始制作一個提交。
blob_id = repo.write("Blob contents", :blob) # (1) index = repo.index index.read_tree(repo.head.target.tree) index.add(:path => 'newfile.txt', :oid => blob_id) # (2) sig = { :email => "bob@example.com", :name => "Bob User", :time => Time.now, } commit_id = Rugged::Commit.create(repo, :tree => index.write_tree(repo), # (3) :author => sig, :committer => sig, # (4) :message => "Add newfile.txt", # (5) :parents => repo.empty? ? [] : [ repo.head.target ].compact, # (6) :update_ref => 'HEAD', # (7) ) commit = repo.lookup(commit_id) # (8)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
分析說明:
(1) 創建一個新的 blob,它包含了一個新文件的內容;
(2) 將 HEAD 提交樹填入索引,并在路徑 newfile.txt 增加新文件;
(3) 這就在 ODB 中創建了一個新的樹,并在一個新的提交中使用它;
(4) 在 author 欄和 committer 欄使用相同的簽名;
(5) 提交的信息;
(6) 當創建一個提交時,必須指定這個新提交的父提交,這里使用了 HEAD 的末尾作為單一的父提交;
(7) 在做一個提交的過程中,Rugged (和 Libgit2 )能在需要時更新引用;
(8) 返回值是一個新提交對象的 SHA-1 哈希,可以用它來獲得一個 Commit 對象。
Ruby 的代碼很好很簡潔,另一方面因為 Libgit2 做了大量工作,所以代碼運行起來其實速度也不賴。
Libgit2 有幾個超過核心 Git 的能力,例如它的可定制性:Libgit2 允許為一些不同類型的操作自定義的“后端”,讓我們得以使用與原生 Git 不同的方式存儲東西,Libgit2 允許為自定義后端指定配置、引用的存儲以及對象數據庫。
我們來看一下它究竟是怎么工作的,如下所示,借用自 Libgit2 團隊提供的后端樣本集 (可以在 ibgit2-backends 上找到)。一個對象數據庫的自定義后端是這樣建立的:
git_odb *odb; int error = git_odb_new(&odb); // (1) git_odb_backend *my_backend; error = git_odb_backend_mine(&my_backend, /*…*/); // (2) error = git_odb_add_backend(odb, my_backend, 1); // (3) git_repository *repo; error = git_repository_open(&repo, "some-path"); error = git_repository_set_odb(repo, odb); // (4)
1
2
3
4
5
6
7
8
9
10
11
分析說明:
(1) 初始化一個空的對象數據庫( ODB )“前端”,它將被作為一個用來做真正的工作的“后端”的容器;
(2) 初始化一個自定義 ODB 后端;
(3) 為這個前端增加一個后端;
(4) 打開一個版本庫,并讓它使用我們的 ODB 來尋找對象。
但是 git_odb_backend_mine 是個什么東西呢? 這是一個我們自己的 ODB 實現的構造器,并且能在那里做任何想做的事,前提是能正確地填寫 git_odb_backend 結構。它看起來應該是這樣的:
typedef struct { git_odb_backend parent; // 其它的一些東西 void *custom_context; } my_backend_struct; int git_odb_backend_mine(git_odb_backend **backend_out, /*…*/) { my_backend_struct *backend; backend = calloc(1, sizeof (my_backend_struct)); backend->custom_context = …; backend->parent.read = &my_backend__read; backend->parent.read_prefix = &my_backend__read_prefix; backend->parent.read_header = &my_backend__read_header; // …… *backend_out = (git_odb_backend *) backend; return GIT_SUCCESS; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
my_backend_struct 的第一個成員必須是一個 git_odb_backend 結構,這是一個微妙的限制:這樣就能確保內存布局是 Libgit2 的代碼所期望的樣子,其余都是隨意的,這個結構的大小可以隨心所欲。
這個初始化函數為該結構分配內存,設置自定義的上下文,然后填寫它支持的 parent 結構的成員。閱讀 Libgit2 的 include/git2/sys/odb_backend.h 源碼以了解全部調用簽名,特定的使用環境會決定使用哪一種調用簽名。
Libgit2 有很多種語言的綁定,在上文中,我們展現了一個使用了幾個更加完整的綁定包的小例子,這些庫存在于許多種語言中,包括 C++、Go、Node.js、Erlang 以及 JVM,它們的成熟度各不相同。官方的綁定集合可以通過瀏覽這個版本庫得到:libgit2。 我們寫的代碼將返回當前 HEAD 指向的提交的提交信息(就像 git log -1 那樣)。
LibGit2Sharp:
如果在編寫一個 .NET 或者 Mono 應用,那么 LibGit2Sharp 就是所需要的,這個綁定是用 C# 寫成的,并且已經采取許多措施來用令人感到自然的 CLR API 包裝原始的 Libgit2 的調用。我們的例子看起來就像這樣:
new Repository(@"C:\path\to\repo").Head.Tip.Message;
1
對于 Windows 桌面應用,一個叫做 NuGet 的包會讓我們快速上手。
objective-git:
如果應用運行在一個 Apple 平臺上,很有可能使用 Objective-C 作為實現語言。Objective-Git 是這個環境下的 Libgit2 綁定。如下所示:
GTRepository *repo = [[GTRepository alloc] initWithURL:[NSURL fileURLWithPath: @"/path/to/repo"] error:NULL]; NSString *msg = [[[epo headReferenceWithError:NULL] resolvedTarget] message];
1
2
3
Objective-git 與 Swift 完美兼容,所以把 Objective-C 落在一邊的時候不用恐懼。
pygit2:
Python 的 Libgit2 綁定叫做 Pygit2,可以在 pygit2 - libgit2 bindings in Python 找到它,示例程序:
pygit2.Repository("/path/to/repo") # 打開代碼倉庫 .head # 獲取當前分支 .peel(pygit2.Commit) # 找到對應的提交 .message # 讀取提交信息
1
2
3
4
四、JGit
如果想在一個 Java 程序中使用 Git,有一個功能齊全的 Git 庫,那就是 JGit。JGit 是一個用 Java 寫成的功能相對健全的 Git 的實現,它在 Java 社區中被廣泛使用。JGit 項目由 Eclipse 維護,它的主頁在 JGit。
有很多種方式可以讓 JGit 連接項目,并依靠它去寫代碼。最簡單的方式也許就是使用 Maven,可以通過在 pom.xml 文件里的 標簽中增加像下面這樣的片段來完成這個整合:
1
2
3
4
5
在讀到這段文字時 version 很可能已經更新了,所以請瀏覽 JGit Core 以獲取最新的倉庫信息,當這一步完成之后,Maven 就會自動獲取并使用所需要的 JGit 庫。
如果想自己管理二進制的依賴包,那么可以從 eclipse 獲得預構建的 JGit 二進制文件,可以像下面這樣執行一個命令來將它們構建進項目:
javac -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App.java java -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App
1
2
JGit 的 API 有兩種基本的層次:底層命令和高層命令,這個兩個術語都來自 Git,并且 JGit 也被按照相同的方式粗略地劃分:高層 API 是一個面向普通用戶級別功能的友好的前端(一系列普通用戶使用 Git 命令行工具時可能用到的東西),底層 API 則直接作用于低級的倉庫對象。
大多數 JGit 會話會以 Repository 類作為起點,首先要做的事就是創建一個它的實例。對于一個基于文件系統的倉庫來說(JGit 允許其它的存儲模型),用 FileRepositoryBuilder 完成它:
// 創建一個新倉庫 Repository newlyCreatedRepo = FileRepositoryBuilder.create( new File("/tmp/new_repo/.git")); newlyCreatedRepo.create(); // 打開一個存在的倉庫 Repository existingRepo = new FileRepositoryBuilder() .setGitDir(new File("my_repo/.git")) .build();
1
2
3
4
5
6
7
8
9
無論程序是否知道倉庫的確切位置,builder 中的那個流暢的 API 都可以提供給它尋找倉庫所需所有信息,它可以使用環境變量 (.readEnvironment()),從工作目錄的某處開始并搜索 (.setWorkTree(…).findGitDir()),或者僅僅只是像上面那樣打開一個已知的 .git 目錄。
當擁有一個 Repository 實例后,就能對它做各種各樣的事,如下是一個速覽:
// 獲取引用 Ref master = repo.getRef("master"); // 獲取該引用所指向的對象 ObjectId masterTip = master.getObjectId(); // Rev-parse ObjectId obj = repo.resolve("HEAD^{tree}"); // 裝載對象原始內容 ObjectLoader loader = repo.open(masterTip); loader.copyTo(System.out); // 創建分支 RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1"); createBranch1.setNewObjectId(masterTip); createBranch1.update(); // 刪除分支 RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1"); deleteBranch1.setForceUpdate(true); deleteBranch1.delete(); // 配置 Config cfg = repo.getConfig(); String name = cfg.getString("user", null, "name");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
分析說明:
第一行獲取一個指向 master 引用的指針,JGit 自動抓取位于 refs/heads/master 的 真正的 master 引用,并返回一個允許獲取該引用的信息的對象,可以獲取它的名字(.getName()),或者一個直接引用的目標對象(.getObjectId()),或者一個指向該引用的符號指針 (.getTarget())。 引用對象也經常被用來表示標簽的引用和對象,所以可以詢問某個標簽是否被“削除”了,或者說它指向一個標簽對象的(也許很長的)字符串的最終目標。
第二行獲得以 master 引用的目標,它返回一個 ObjectId 實例,不管是否存在于一個 Git 對象的數據庫,ObjectId 都會代表一個對象的 SHA-1 哈希。
第三行與此相似,但是它展示了 JGit 如何處理 rev-parse 語法(要了解更多,請參考 Git之深入解析如何選擇修訂的版本 的“分支引用”),可以傳入任何 Git 了解的對象說明符,然后 JGit 會返回該對象的一個有效的 ObjectId,或者 null。
接下來兩行展示了如何裝載一個對象的原始內容,在這個例子中,我們調用 ObjectLoader.copyTo() 直接向標準輸出流輸出對象的內容,除此之外 ObjectLoader 還帶有讀取對象的類型和長度并將它以字節數組返回的方法。對于一個( .isLarge() 返回 true 的)大的對象,可以調用 .openStream() 來獲得一個類似 InputStream 的對象,它可以在沒有一次性將所有數據拉到內存的前提下讀取對象的原始數據。
接下來幾行展現了如何創建一個新的分支,我們創建一個 RefUpdate 實例,配置一些參數,然后調用 .update() 來確認這個更改,刪除相同分支的代碼就在這行下面。記住必須先 .setForceUpdate(true) 才能讓它工作,否則調用 .delete() 只會返回 REJECTED,然后什么都沒有發生。
最后一個例子展示了如何從 Git 配置文件中獲取 user.name 的值,這個 Config 實例使用我們先前打開的倉庫做本地配置,但是它也會自動地檢測并讀取全局和系統的配置文件。
這只是底層 API 的冰山一角,另外還有許多可以使用的方法和類。還有一個沒有放在這里說明的,就是 JGit 是用異常機制來處理錯誤的。JGit API 有時使用標準的 Java 異常(例如 IOException ),但是它也提供了大量 JGit 自己定義的異常類型(例如 NoRemoteRepositoryException、 CorruptObjectException 和 NoMergeBaseException)。
底層 API 更加完善,但是有時將它們串起來以實現普通的目的非常困難,例如將一個文件添加到索引,或者創建一個新的提交。為了解決這個問題,JGit 提供了一系列高層 API,使用這些 API 的入口點就是 Git 類:
Repository repo; // 構建倉庫…… Git git = new Git(repo);
1
2
3
Git 類有一系列非常好的構建器風格的高層方法,它可以用來構造一些復雜的行為。我們來看一個例子,做一件類似 git ls-remote 的事:
CredentialsProvider cp = new UsernamePasswordCredentialsProvider("username", "p4ssw0rd"); Collection remoteRefs = git.lsRemote() .setCredentialsProvider(cp) .setRemote("origin") .setTags(true) .setHeads(false) .call(); for (Ref ref : remoteRefs) { System.out.println(ref.getName() + " -> " + ref.getObjectId().name()); }
1
2
3
4
5
6
7
8
9
10
這是一個 Git 類的公共樣式,這個方法返回一個可以串連若干方法調用來設置參數的命令對象,當調用 .call() 時它們就會被執行。在這情況下,我們只是請求了 origin 遠程的標簽,而不是頭部。還要注意用于驗證的 CredentialsProvider 對象的使用。
在 Git 類中還可以使用許多其它的命令,包括但不限于 add、blame、commit、clean、push、rebase、revert 和 reset。
五、go-git
如果想要將Git集成到用 Golang 編寫的服務中,還有一個純 Go 庫實現,此實現沒有任何本機依賴項,因此不容易出現手動內存管理錯誤。對于標準的 Golang 性能分析工具(如 CPU、內存分析器、競賽檢測器等)來說,它也是透明的。
go-git 專注于擴展性、兼容性,并支持大多數管道 API,具體請參考:go-git。
如下是一個使用 Go API 的基本例子:
import "gopkg.in/src-d/go-git.v4" r, err := git.PlainClone("/tmp/foo", false, &git.CloneOptions{ URL: "https://github.com/src-d/go-git", Progress: os.Stdout, })
1
2
3
4
5
6
一旦有了 Repository 實例,就可以訪問信息并對其進行修改:
// retrieves the branch pointed by HEAD ref, err := r.Head() // get the commit object, pointed by ref commit, err := r.CommitObject(ref.Hash()) // retrieves the commit history history, err := commit.History() // iterates over the commits and print each for _, c := range history { fmt.Println(c) }
1
2
3
4
5
6
7
8
9
10
11
12
13
go-git 有一些值得注意的高級特性,其中之一是一個可插拔的存儲系統,它類似于 Libgit2 后端,默認的實現是內存存儲,它非常快。
r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ URL: "https://github.com/src-d/go-git", })
1
2
3
可插拔存儲提供了許多有趣的選擇,例如,go-git 允許在 Aerospike 數據庫中存儲引用、對象和配置。
另一個特性是靈活的文件系統抽象,使用 go-billy 可以很容易地以不同的方式存儲所有文件,例如將所有文件打包到磁盤上的單個歸檔文件,或者將所有文件保存在內存中。
另一個高級用例包括一個可調優的 HTTP 客戶機,如在 go-git 上找到的客戶機。
customClient := &http.Client{ Transport: &http.Transport{ // accept any certificate (might be useful for testing) TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, Timeout: 15 * time.Second, // 15 second timeout CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse // don't follow redirect }, } // Override http(s) default protocol to use our custom client client.InstallProtocol("https", githttp.NewClient(customClient)) // Clone repository using the new client if the protocol is https:// r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{URL: url})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
六、Dulwich
還有一個純 python 的 Git 實現——Dulwich,該項目托管在 Dulwich下。它旨在提供一個接口,Git 存儲庫(本地和遠程),不直接調用 Git,而是使用純 Python,它有一個可選的 C 擴展,可以顯著提高性能。
Dulwich 遵循 Git 設計,并將 API 分為兩個基本層次:管道和瓷器。
如下,是一個使用低級 API 訪問上次提交的提交消息的例子:
from dulwich.repo import Repo r = Repo('.') r.head() # '57fbe010446356833a6ad1600059d80b1e731e15' c = r[r.head()] c #
1
2
3
4
5
6
7
8
9
10
11
要使用高級的 API 打印提交日志,可以使用:
from dulwich import porcelain porcelain.log('.', max_entries=1) #commit: 57fbe010446356833a6ad1600059d80b1e731e15 #Author: Jelmer Vernoo?
1
2
3
4
5
6
Git 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。