NGit Tutorial
I recently decided to use the NGit library to interact with a Git repository as part of a ServiceStack project that I'm working on.
Why NGit?
That's a great question considering there's the awesome libgit2 library available. Unfortunately, the library doesn't support doing pull/fetch/merge according to this open issue.
NGit is a semi-automated port of the JGit library from Java over to .Net and it's maintained by the Mono team. And although it's kind of frustrating to use, it supports all of Git's feature set and generally works just fine once you figure it all out.
Since I had a hard time finding examples and documentation for NGit, I'm posting some code snippets and explanations for common features I needed to use in the hopes that it helps some other developer out there in the future.
NGit Documentation Unit Tests
NGit follows the common "unit tests as documentation" pattern, so I encourage any developer to clone a copy of the NGit repository and look through their unit tests as a way to explore their API when you need to see the usage pattern of a particular command/feature.
Git Commands
NGit uses a command-based API that is built off of a Git class which is returned when you initialize, open, or clone a repository. Commands generally return themselves, so you end up chaining commands similar to working with jQuery.
Cloning a Repository
To clone a repository, you need to create a CloneCommand and set at least the local directory target for the clone on your local disk and the URI for the origin that you're cloning from:
// Let's clone the NGit repository var clone = Git.CloneRepository() .SetDirectory(@"C:\Git\NGit") .SetURI("https://github.com/mono/ngit.git"); // Execute and return the repository object we'll use for further commands var repository = clone.Call();
Specifying Credentials
For simple HTTP/HTTPS credentials, you will generally create a UsernamePasswordCredentialsProvider object and either set it on the command you're calling or set it as the default up front before executing any command:
var credentials = new UsernamePasswordCredentialsProvider("username", "password"); // On a per-command basis var fetch = repository.Fetch() .SetCredentialsProvider(credentials) .Call(); // Or globally as the default for each new command CredentialsProvider.SetDefault(credentials);
If you need to use SSH with private key authentication, things get a little more complicated. I will cover the solution to that at the bottom of my post.
Opening an Existing Repository
Opening an existing repository is simple:
var repository = Git.Open(@"C:\Git\NGit");
Fetch, Pull, Status, Clean, Add, Remove
Most commands are fairly simple:
// Fetch changes without merging them var fetch = repository.Fetch().Call(); // Pull changes (will automatically merge/commit them) var pull = repository.Pull().Call(); // Get the current branch status var status = repository.Status().Call(); // The IsClean() method is helpful to check if any changes // have been detected in the working copy. I recommend using it, // as NGit will happily make a commit with no actual file changes. bool isClean = status.IsClean(); // You can also access other collections related to the status var added = status.GetAdded(); var changed = status.GetChanged(); var removed = status.GetRemoved(); // Clean our working copy var clean = repository.Clean().Call(); // Add all files to the stage (you could also be more specific) var add = repository.Add() .AddFilePattern(".") .Call(); // Remove files from the stage var remove = repository.Rm() .AddFilePattern(".gitignore") .Call();
Reset
If we fetched changes from origin/master and want to reset our current branch to match:
var reset = repository.Reset() .SetMode(ResetCommand.ResetType.HARD) .SetRef("origin/master") .Call();
Commit, Push
To commit and push a change, you would do the following:
var author = new PersonIdent("Lance Mcnearney", "lance@mcnearney.net"); var message = "My commit message"; // Commit our changes after adding files to the stage var commit = repository.Commit() .SetMessage(message) .SetAuthor(author) .SetAll(true) // This automatically stages modified and deleted files .Call(); // Our new commit's hash var hash = commit.Id; // Push our changes back to the origin var push = repository.Push().Call();
Private Key Authentication Using SSH
Credit for figuring out how to wire up SSH authentication using a private key goes to Doug and his Stack Overflow question:
My implementation of it does not require the public key but as a trade-off you must make sure to specify the username in the ssh:// URI. I also wipe out the GIT_SSH environment variable as Jsch will use that instead of using the configured JschConfigSessionFactory when initializing its SSH connection. In my case, it will calling TortoisePlink.exe.
You must create a custom JschConfigSessionFactory class:
/// <summary> /// Handles setting up the public key authentication when using a remote SSH repository /// </summary> public class PrivateKeyConfigSessionFactory : JschConfigSessionFactory { private string PrivateKeyPath { get; set; } public PrivateKeyConfigSessionFactory(string privateKeyPath) { PrivateKeyPath = privateKeyPath; // Clear the GIT_SSH environment variable as NGit will use it for SSH transport instead of the session factory Environment.SetEnvironmentVariable("GIT_SSH", string.Empty, EnvironmentVariableTarget.Process); } protected override void Configure(OpenSshConfig.Host hc, Session session) { var config = new Properties(); config["StrictHostKeyChecking"] = "no"; config["PreferredAuthentications"] = "publickey"; session.SetConfig(config); var jsch = GetJSch(hc, FS.DETECTED); jsch.AddIdentity("KeyPair", File.ReadAllBytes(PrivateKeyPath), null, null); } }
Once you have your custom class, you can then configure NGit/Jsch to use it:
// Use our custom SSH session when accessing remote SSH:// repositories // The username must be in the repository Uri: ssh://git@host/var/git/repo.git var privateKeyPath = @"C:\Git\private.key"; var factory = new PrivateKeyConfigSessionFactory(privateKeyPath); SshSessionFactory.SetInstance(factory);
Cleaning up after NGit
Since NGit is a port from Java, it doesn't implement IDisposable when accessing files. To remove its lock on files, you can dispose of the Git object by doing the following:
// Handle disposing of NGit's locks repository.GetRepository().Close(); repository.GetRepository().ObjectDatabase.Close(); repository = null;
You may also want to recursively remove any read-only file attributes set by NGit in the repository's path if you need to remove the repository later or you will receive permission exceptions when attempting to do so.
var files = Directory.GetFiles(@"C:\Git\NGit", "*", SearchOption.AllDirectories); // Remove the read-only attribute applied by NGit to some of its files foreach (var file in files) { file.Attributes = FileAttributes.Normal; }
