go-git
CC Attribution 2.0 Belgium:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor.
Mola, Santiago M.
[go-git] is a Git library written entirely inGo. It is designed to be developer-friendly and highly extensible. In thistalk we will introduce go-git, the reasons why we created it, its currentdevelopment status and its roadmap. We use go-git to fetch every Gitrepository that has been published (all GitHub, Bitbucket, self-hosted...) andanalyze all source code history and we hope that it can support many other usecases. go-git intends to be the way to go for Git with Go and a solid alternative tolibgit2 and jgit. Currently the library has 22k LOC's, has full read-featureparity with libgit2, has a dedicated team committed to it, and is the 3rd mostpopular git implementation. While it's still has a long way to go, we havealready used it to fetch and analyze 17M GitHub repositories, so it has a goodstability level for the feature set that we have implemented so far.Developers who are looking into building software to manipulate repositoriesat low level will find go-git a tool that can boost their productivity.
 And one thing is that we use it. The first step to this is analyzing all online open source. We are currently analyzing 70 million repositories. That's every repository on GitHub that is not a fork. And we are now creating a new pipeline to analyze more than 60 million repositories. So that includes all GitHub forks, but also GitHub repositories hosted everywhere else. So this is a test that requires intensive use of Git. So our first problem was to see a Git implementation. The usual choices are the Git command line interface or Libit 2, which is a library that has bindings for many languages. And JGit, which is also a very nice library in Java by Google. But the language of choice has sort this goal. So none of these as they fit our stack nicely. So when we started our choosing goal where Git to go, which are the usual bindings for Libit 2, which are really complete, but it requires a goal. It's not that nice for us. It's not possible for us to extend it in the way we needed from Go. So that was kind of discarded. There's also some nice command line interface wrappers. They work really fast because, well, it just takes a good Git. The official Git implementation is awesome. It's really fast. But they are very inflectable. We can extend the underlying Git behavior. And there were a lot of Go implementations. Most of them were abandoned just at the very early stage because I guess a lot of people start a Git implementation, but then figures out it's actually much bigger than support he thinks. So our choice was doing it from scratch. That's how Go Git was born. So we wrote the full implementation in PGO with idiomatic API for both high and low level in Git technology that's known as plumbing and porcelain commands. And we have focused on extensibility. I will talk more about that later. So we aim to be compatible feature-wise with Git 2 and Git. We are still not there, but we are working towards it. We already put two years of development into it. So we are on track. So why do we want to extend? Why do we focus on extensibility? One of the first things that we needed to extend is storage backends. Storage backends in Go Git define how Git objects, references, configuration, and so on are stored. So we provide two implementations with Go Git itself. One just in memory storage backend that you can use for testing or also for processing repositories really fast if they fit in memory. And then a virtual file system implementation that uses a virtual file system abstraction. The open source is you can check in this Go Billy. But you don't plug it around. In fact, we have implementations that use databases as Cassandra, as a backend. We have implementations that are storage backend wrappers for doing catching. So there are a lot of possibilities to this. Another interesting extension points are work-based as virtual file systems. So you are not limited to take out a commit into the disk. You can take out in memory or database or anywhere that you implement a virtual file system interface. And we can also plug transports. So we implement all standard Git transports, SSH, HTTPS, and so on. But we can also plug our own. We actually use it for performance use in some cases. So let's go through our use case. The first building block for analyzing all repositories online is having a local copy of every repository in our cluster so we can proceed faster on demand. So we are fetching repositories from GitHub, GitHub, Bitbac, and some on a self-host repository that we could all from Google. We are trying to be very extensive here. But this leads to the first problem, which is for redundancy. The vast majority of repositories that you find online are forks. In fact, in GitHub, there are 70 million repositories that are like 60 million if you add forks. So if we store master branch, only the master branch of all we have known forks, we get around 72 bytes. If we add forks to that, we have 240 terabytes. That's only the master branch. So this is a lower bound. But most of the data is just redundant, most forks are just a copy of the original repository plus a couple of commits. So if blue represents forks, we don't really want to store all of those copies that are 90% redundant data. They just take too much space in our cluster for nothing. So we call it by looking forks just as a namespace set of references of the original repository. So for us, we can see the forks, it's just the same as the original repository, but it just has some branches that are pointing to a different part of the history. So we store all the forks of the same repository together in the same local repository in our storage. So they share all objects, if a commit or a file is present in multiple forks of the same repository, that commit, that file is not stored twice. We just prefix each reference or branch with the idea of the repository that it was fetched from, so we can still distinguish between them. Let's see an example. Let's say that this is the history of the GoGear repository, the real one is actually longer, but we have three commits here, and we have a couple of forks, and they just share most of their history with the original repository, and they just add a commit, probably for doing a full request. So if we store them together, it just looks like this, a single repository, three branches, and we prefix each branch with the original repository so we can still distinguish. But there's still a problem. So how do we say that the repository we fetch is a fork from another? We could rely on name, but there are forks that have different names that original repositories and there are repositories that have the same name and are completely independent. For example, there are five repositories that are called GoGear, and they are completely related implementations. We could rely also on the GitHub API, but that works only on GitHub, of course, and not even on GitHub, it only works if you created a fork using the GitHub web interface. So that's going to fly for us. What we do is take advantage of the fact that a fork starts at the same set of initial commits as the original repository. So it might have longer or shorter history, it might have more branches, but if you go to the first commit that was done to the repository, the hash of that commit is actually the same for all forks. So in our local storage, we don't store a repository per remote name of the repository, but we create a repository for each hash of initial commit that we saw. And then we fetch a repository, we push each branch to a local repository that corresponds to the hash of its initial commit. So this works nicely across all Git providers because just relies on Git and not in any realistic or any external API. So this is actually how our local storage looks like. We don't store forks, we just store their incremental parts, and we merge together, see if these writes are forks from the same repository, we just store one of them containing the information of all of them. So then we reduce the required storage minimum, but still have access to all information. The next problem for building this mirror and using it for analyzing is that it's where do we spread. So we need a storage that is oriented to both repository analysis. So when we open our repository, we open it for analyzing the whole history. In our use cases, it's unlikely that we want to open a repository to get just a commit. So we distributed file system for this, we have used a GFS and Google Clotorats. The good thing about these file systems is that they have really high throughput, they have the counterpart, they have high latency. And Git repository is composed of many files. If you look into the.git directory of your repository, you will see one or more files, you will see index files, reference files, you will see a config file. So we want to analyze our repository, we need all of that. But in some cases, there's the time that we spend, the latency of accessing all of these files at SAP. At some point, for some real stories, we spend more time just waiting for input operations to start rather than the actual data transfer. And this gets worse every time we update the repositories because every time we pull, we get more files. So this is performance care that just gets worse over time. So how do we solve it with Google it? We create an efficient argument format, efficient for this use case, it's called SIVA. We archive each repository as a single file. We have a custom transbackend, bake it to go Git, that operates on archive repositories transparently without extracting them and performance updates to them in the most efficient way for both SIVA and HDFS Google Cloud Storage. So we baked all the details of this into this storage backend and now we can use go Git transparent with this kind of repository. You can read more details, reputation details about this in our blog. All the details about how these files are designed and so on are there and it's also open source, so you can use it for other stuff. So what I told you up to now is one of our main use cases. I would like to highlight a couple of projects that use go Git and that might be interesting for the Go audience. One is Git URL, which is a SQL interface to Git and you can just use it to write on like any Git repository and you can write SQL queries against the repository. Very soon we'll be implemented as a code database driver so you will be able to connect, I don't know, your ORM, your Git repository. It's pretty crazy thing to do but you can. We also have a ghost table. It's a service, it's a self-hosted service. It's like the gopacch.in service. Who of you know gopacch.in? With gopacch.in you can provide URLs to Git repositories that go into specific branches and you can use that in your Go imports for making imports to the table and it will depend on the master branch. Gopacch.in is the most popular service for doing that but you can just go stable to run your own on your domain with public and private repositories. So it's recap. Go Git is working with Git and go very simple on idiomatic. We have been using it for more than one year in production with millions of repositories. So even if the feature set is not complete, there's already a very solid base there. We have implemented a lot of advanced use cases in Go that would otherwise be quite a help to implement in, for example, like, recently a new kind of Git search was presented by Microsoft and they do something that is similar to our storage backends but using the command line interface. So they had to implement a kernel driver with a new virtual system for doing that in a pretty complex way and in every case that was just like 300 lines of Go. So here you have the project. And URL like this have the stable version import, the development version import. You see what Go stable does. We have this fancy URL for our imports. Well, that's all. Thank you. I think we have quite a lot of time for questions. Yeah, I was like, lightning talk. So. Hi. How big is the project currently? Maybe a number of lines of code or something. And how much would grow in the future to get to the same capability as LibGit or JGit? Yeah, so size, I would have to check out. Does anyone of my colleagues know what the size in landscape code? Well, the thing about having time is that. Sorry? Yeah, this is quite tricky because my screen is blacked out. So I have to type like this. But I don't know what's maybe 30,000 lines of code. Maybe I'm not sure. So we implement. So up to now we have implemented everything that we need for reporting analysis, which is not what most users want. So we can clone feeds, pull, push. We have client and server side for both of those. We can traverse repositories. We can generate this or all that kind of stuff. So it's pretty complete on that side. One of the major missing parts is creating commits. So if you actually want to create new commits, that's not there yet. But it should be the next stable release. When we will be on par with JGit or LibGit 2, so maybe fully on par with Polypro triggers. So yeah, not any time soon. But for features that implement, we implement a lot of possibilities for extending the behavior that you do not have with those libraries. So yeah, that's a hard question. I'll give you the other mic. OK, thanks. Thanks, Vartoco. You said that you are identifying the repositories for the first commit. How do you deal with the boilerplate repositories that are used just based for many projects? Yeah. So there are a couple of cases. One easy one is the boilerplate that you copy as an initialization, such as the git ignore file. But in those cases, the hash of the first commit is different because the commit includes your author name, the timestamp, and the commit message. So if you start a repository with the same content as a different one, well, you have like OckPress or these static page generators that use clone. So currently, we just store all of them as forks, which is actually what Git has done internally. And we don't differentiate among them. We see all the web pages completely independent web pages done by different people as forks of the same project. We analyze all of them, but we interpret them as forks. So we didn't get into the problem of solving this. But we might try some stuff in the future, such as trying to detect a point in time where the code base diverted a lot in them. I guess it would be definitely possible to do that. The only thing is that that's very computationally expensive. So in our case, for example, but only because of our use case, we are focusing on code. We don't care that much, for example, about the user example of static generated pages. Actually they are bad for us because we are trying to understand who people codes and those contain a lot of other generated code. We don't want to interpret your other generated code as if it was writing my view because you are not writing code. But yeah, I guess we'll have to do something in the future. So I have a question regarding your fork detection algorithm. Do you support the teaching Git graphs when you graph two repositories in one? Yeah, it's really tricky. Because I saw a simple example where there was only one initial commit. But if you take the Git repository, it has seven initial commits or seven root commits. In our case, in our application, we make the distinction of root commit, which all roots have no parent, and we made this arbitrary definition of the initial commit, which is root commit. So for our reference, is the root commit that you reach by going through the history only following the first parent. So when there is a merge, we only follow the first parent, and we always reach the same initial commit. So Git has seven root commits, but only one of them is initial commit. It's actually the proper first Git commit, and the other were tools and things that were merged into that over there. And we store all of that in the initial commit. If you have a fork that, in the case that you merge two different repositories, you still have some forks that might have a different initial commit, even if they share part of the history. They will store independently in our storage. And we are all trying to figure out, so now currently that duplicates that information in some cases. Now we are trying to figure out if we use Git alternates to even share information in those cases. That's still a working progress. Thank you. So my question is regarding the extensibility of it. So you mentioned that it's easy to write new start backends, but only supports static leaking, right? So you have to kind of work the project and create it and make a kind of compiling plugin or something like that? I mean, go Git is hybrid, it's not meant for writing a command line client. You could write a command client with go Git, which in fact we are kind of doing, but just for as an example. So if you do your own storage back end, you will be using an application and in that application you will be also importing go Git, you will be importing your storage back end and you will be passing it go Git. So you hold the static leaking, affect that. I mean, that's the use case. It will be much like tricky if you want to do the Git line where you can plug a storage back end as a plugin and runtime, write something like that. We don't try to do that at the moment. That would be a good contribution. Cool. Any other question? All right. So thank you. Thank you.
 And one thing is that we use it.  The first step to this is analyzing all online open source.  We are currently analyzing 70 million repositories.  That's every repository on GitHub that is not a fork.  And we are now creating a new pipeline to analyze more than 60 million repositories.  So that includes all GitHub forks, but also GitHub repositories hosted everywhere else.  So this is a test that requires intensive use of Git.  So our first problem was to see a Git implementation.  The usual choices are the Git command line interface or Libit 2, which is a library that  has bindings for many languages.  And JGit, which is also a very nice library in Java by Google.  But the language of choice has sort this goal.  So none of these as they fit our stack nicely.  So when we started our choosing goal where Git to go, which are the usual bindings for  Libit 2, which are really complete, but it requires a goal.  It's not that nice for us.  It's not possible for us to extend it in the way we needed from Go.  So that was kind of discarded.  There's also some nice command line interface wrappers.  They work really fast because, well, it just takes a good Git.  The official Git implementation is awesome.  It's really fast.  But they are very inflectable.  We can extend the underlying Git behavior.  And there were a lot of Go implementations.  Most of them were abandoned just at the very early stage because I guess a lot of people  start a Git implementation, but then figures out it's actually much bigger than support  he thinks.  So our choice was doing it from scratch.  That's how Go Git was born.  So we wrote the full implementation in PGO with idiomatic API for both high and low level  in Git technology that's known as plumbing and porcelain commands.  And we have focused on extensibility.  I will talk more about that later.  So we aim to be compatible feature-wise with Git 2 and Git.  We are still not there, but we are working towards it.  We already put two years of development into it.  So we are on track.  So why do we want to extend?  Why do we focus on extensibility?  One of the first things that we needed to extend is storage backends.  Storage backends in Go Git define how Git objects, references, configuration, and so  on are stored.  So we provide two implementations with Go Git itself.  One just in memory storage backend that you can use for testing or also for processing  repositories really fast if they fit in memory.  And then a virtual file system implementation that uses a virtual file system abstraction.  The open source is you can check in this Go Billy.  But you don't plug it around.  In fact, we have implementations that use databases as Cassandra, as a backend.  We have implementations that are storage backend wrappers for doing catching.  So there are a lot of possibilities to this.  Another interesting extension points are work-based as virtual file systems.  So you are not limited to take out a commit into the disk.  You can take out in memory or database or anywhere that you implement a virtual file  system interface.  And we can also plug transports.  So we implement all standard Git transports, SSH, HTTPS, and so on.  But we can also plug our own.  We actually use it for performance use in some cases.  So let's go through our use case.  The first building block for analyzing all repositories online is having a local copy of  every repository in our cluster so we can proceed faster on demand.  So we are fetching repositories from GitHub, GitHub, Bitbac, and some on a self-host repository  that we could all from Google.  We are trying to be very extensive here.  But this leads to the first problem, which is for redundancy.  The vast majority of repositories that you find online are forks.  In fact, in GitHub, there are 70 million repositories that are like 60 million if you add forks.  So if we store master branch, only the master branch of all we have known forks, we get  around 72 bytes.  If we add forks to that, we have 240 terabytes.  That's only the master branch.  So this is a lower bound.  But most of the data is just redundant, most forks are just a copy of the original repository  plus a couple of commits.  So if blue represents forks, we don't really want to store all of those copies that are  90% redundant data.  They just take too much space in our cluster for nothing.  So we call it by looking forks just as a namespace set of references of the original repository.  So for us, we can see the forks, it's just the same as the original repository, but it  just has some branches that are pointing to a different part of the history.  So we store all the forks of the same repository together in the same local repository in our  storage.  So they share all objects, if a commit or a file is present in multiple forks of the  same repository, that commit, that file is not stored twice.  We just prefix each reference or branch with the idea of the repository that it was fetched  from, so we can still distinguish between them.  Let's see an example.  Let's say that this is the history of the GoGear repository, the real one is actually  longer, but we have three commits here, and we have a couple of forks, and they just share  most of their history with the original repository, and they just add a commit, probably for doing  a full request.  So if we store them together, it just looks like this, a single repository, three branches,  and we prefix each branch with the original repository so we can still distinguish.  But there's still a problem.  So how do we say that the repository we fetch is a fork from another?  We could rely on name, but there are forks that have different names that original repositories  and there are repositories that have the same name and are completely independent.  For example, there are five repositories that are called GoGear, and they are completely  related implementations.  We could rely also on the GitHub API, but that works only on GitHub, of course, and  not even on GitHub, it only works if you created a fork using the GitHub web interface.  So that's going to fly for us.  What we do is take advantage of the fact that a fork starts at the same set of initial  commits as the original repository.  So it might have longer or shorter history, it might have more branches, but if you go  to the first commit that was done to the repository, the hash of that commit is actually the same  for all forks.  So in our local storage, we don't store a repository per remote name of the repository,  but we create a repository for each hash of initial commit that we saw.  And then we fetch a repository, we push each branch to a local repository that corresponds  to the hash of its initial commit.  So this works nicely across all Git providers because just relies on Git and not in any  realistic or any external API.  So this is actually how our local storage looks like.  We don't store forks, we just store their incremental parts, and we merge together,  see if these writes are forks from the same repository, we just store one of them containing  the information of all of them.  So then we reduce the required storage minimum, but still have access to all information.  The next problem for building this mirror and using it for analyzing is that it's where  do we spread.  So we need a storage that is oriented to both repository analysis.  So when we open our repository, we open it for analyzing the whole history.  In our use cases, it's unlikely that we want to open a repository to get just a commit.  So we distributed file system for this, we have used a GFS and Google Clotorats.  The good thing about these file systems is that they have really high throughput, they  have the counterpart, they have high latency.  And Git repository is composed of many files.  If you look into the.git directory of your repository, you will see one or more files,  you will see index files, reference files, you will see a config file.  So we want to analyze our repository, we need all of that.  But in some cases, there's the time that we spend, the latency of accessing all of these  files at SAP.  At some point, for some real stories, we spend more time just waiting for input operations  to start rather than the actual data transfer.  And this gets worse every time we update the repositories because every time we pull, we  get more files.  So this is performance care that just gets worse over time.  So how do we solve it with Google it?  We create an efficient argument format, efficient for this use case, it's called SIVA.  We archive each repository as a single file.  We have a custom transbackend, bake it to go Git, that operates on archive repositories  transparently without extracting them and performance updates to them in the most efficient  way for both SIVA and HDFS Google Cloud Storage.  So we baked all the details of this into this storage backend and now we can use go  Git transparent with this kind of repository.  You can read more details, reputation details about this in our blog.  All the details about how these files are designed and so on are there and it's also  open source, so you can use it for other stuff.  So what I told you up to now is one of our main use cases.  I would like to highlight a couple of projects that use go Git and that might be interesting  for the Go audience.  One is Git URL, which is a SQL interface to Git and you can just use it to write on  like any Git repository and you can write SQL queries against the repository.  Very soon we'll be implemented as a code database driver so you will be able to connect, I don't  know, your ORM, your Git repository.  It's pretty crazy thing to do but you can.  We also have a ghost table.  It's a service, it's a self-hosted service.  It's like the gopacch.in service.  Who of you know gopacch.in?  With gopacch.in you can provide URLs to Git repositories that go into specific branches  and you can use that in your Go imports for making imports to the table and it will depend  on the master branch.  Gopacch.in is the most popular service for doing that but you can just go stable to run  your own on your domain with public and private repositories.  So it's recap.  Go Git is working with Git and go very simple on idiomatic.  We have been using it for more than one year in production with millions of repositories.  So even if the feature set is not complete, there's already a very solid base there.  We have implemented a lot of advanced use cases in Go that would otherwise be quite  a help to implement in, for example, like, recently a new kind of Git search was presented  by Microsoft and they do something that is similar to our storage backends but using  the command line interface.  So they had to implement a kernel driver with a new virtual system for doing that in a pretty  complex way and in every case that was just like 300 lines of Go.  So here you have the project.  And URL like this have the stable version import, the development version import.  You see what Go stable does.  We have this fancy URL for our imports.  Well, that's all.  Thank you.  I think we have quite a lot of time for questions.  Yeah, I was like, lightning talk.  So.  Hi.  How big is the project currently?  Maybe a number of lines of code or something.  And how much would grow in the future to get to the same capability as LibGit or JGit?  Yeah, so size, I would have to check out.  Does anyone of my colleagues know what the size in landscape code?  Well, the thing about having time is that.  Sorry?  Yeah, this is quite tricky because my screen is blacked out.  So I have to type like this.  But I don't know what's maybe 30,000 lines of code.  Maybe I'm not sure.  So we implement.  So up to now we have implemented everything that we need for reporting analysis, which  is not what most users want.  So we can clone feeds, pull, push.  We have client and server side for both of those.  We can traverse repositories.  We can generate this or all that kind of stuff.  So it's pretty complete on that side.  One of the major missing parts is creating commits.  So if you actually want to create new commits, that's not there yet.  But it should be the next stable release.  When we will be on par with JGit or LibGit 2, so maybe fully on par with Polypro triggers.  So yeah, not any time soon.  But for features that implement, we implement a lot of possibilities for extending the behavior  that you do not have with those libraries.  So yeah, that's a hard question.  I'll give you the other mic.  OK, thanks.  Thanks, Vartoco.  You said that you are identifying the repositories for the first commit.  How do you deal with the boilerplate repositories that are used just based for many projects?  Yeah.  So there are a couple of cases.  One easy one is the boilerplate that you copy as an initialization, such as the git ignore  file.  But in those cases, the hash of the first commit is different because the commit includes  your author name, the timestamp, and the commit message.  So if you start a repository with the same content as a different one, well, you have  like OckPress or these static page generators that use clone.  So currently, we just store all of them as forks, which is actually what Git has done  internally.  And we don't differentiate among them.  We see all the web pages completely independent web pages done by different people as forks  of the same project.  We analyze all of them, but we interpret them as forks.  So we didn't get into the problem of solving this.  But we might try some stuff in the future, such as trying to detect a point in time where  the code base diverted a lot in them.  I guess it would be definitely possible to do that.  The only thing is that that's very computationally expensive.  So in our case, for example, but only because of our use case, we are focusing on code.  We don't care that much, for example, about the user example of static generated pages.  Actually they are bad for us because we are trying to understand who people codes and  those contain a lot of other generated code.  We don't want to interpret your other generated code as if it was writing my view because  you are not writing code.  But yeah, I guess we'll have to do something in the future.  So I have a question regarding your fork detection algorithm.  Do you support the teaching Git graphs when you graph two repositories in one?  Yeah, it's really tricky.  Because I saw a simple example where there was only one initial commit.  But if you take the Git repository, it has seven initial commits or seven root commits.  In our case, in our application, we make the distinction of root commit, which all roots  have no parent, and we made this arbitrary definition of the initial commit, which is  root commit.  So for our reference, is the root commit that you reach by going through the history only  following the first parent.  So when there is a merge, we only follow the first parent, and we always reach the same  initial commit.  So Git has seven root commits, but only one of them is initial commit.  It's actually the proper first Git commit, and the other were tools and things that were  merged into that over there.  And we store all of that in the initial commit.  If you have a fork that, in the case that you merge two different repositories, you still  have some forks that might have a different initial commit, even if they share part of  the history.  They will store independently in our storage.  And we are all trying to figure out, so now currently that duplicates that information  in some cases.  Now we are trying to figure out if we use Git alternates to even share information in  those cases.  That's still a working progress.  Thank you.  So my question is regarding the extensibility of it.  So you mentioned that it's easy to write new start backends, but only supports static  leaking, right?  So you have to kind of work the project and create it and make a kind of compiling plugin  or something like that?  I mean, go Git is hybrid, it's not meant for writing a command line client.  You could write a command client with go Git, which in fact we are kind of doing, but just  for as an example.  So if you do your own storage back end, you will be using an application and in that application  you will be also importing go Git, you will be importing your storage back end and you  will be passing it go Git.  So you hold the static leaking, affect that.  I mean, that's the use case.  It will be much like tricky if you want to do the Git line where you can plug a storage  back end as a plugin and runtime, write something like that.  We don't try to do that at the moment.  That would be a good contribution.  Cool.  Any other question?  All right.  So thank you.  Thank you.
gogit_A_pure_Go_implementation_of_Gi_0_24083.png gogit_A_pure_Go_implementation_of_Gi_24084_24995.png gogit_A_pure_Go_implementation_of_Gi_24996_25043.png gogit_A_pure_Go_implementation_of_Gi_25044_25475.png gogit_A_pure_Go_implementation_of_Gi_25476_28007.png gogit_A_pure_Go_implementation_of_Gi_28008_28391.png gogit_A_pure_Go_implementation_of_Gi_28392_29087.png gogit_A_pure_Go_implementation_of_Gi_29088_29147.png gogit_A_pure_Go_implementation_of_Gi_29148_31271.png gogit_A_pure_Go_implementation_of_Gi_31272_31343.png gogit_A_pure_Go_implementation_of_Gi_31344_31859.png gogit_A_pure_Go_implementation_of_Gi_31860_32615.png gogit_A_pure_Go_implementation_of_Gi_32616_33467.png gogit_A_pure_Go_implementation_of_Gi_33468_34139.png gogit_A_pure_Go_implementation_of_Gi_34140_34391.png gogit_A_pure_Go_implementation_of_Gi_34392_34739.png gogit_A_pure_Go_implementation_of_Gi_34740_35999.png gogit_A_pure_Go_implementation_of_Gi_36000_36443.png gogit_A_pure_Go_implementation_of_Gi_36444_36791.png gogit_A_pure_Go_implementation_of_Gi_36792_37511.png