[JAVA] Make a weekly git GUI client [2] stage / unstage incomplete capture

This is a continuation of Making a weekly git GUI client [1] stage / unstage basic knowledge.

I haven't talked about GUI yet

This is the trajectory from starting to use JGit to giving up. It may also be read as an introduction to JGit. I haven't talked about GUI yet.

libgit2 and Language Bindings

There is a C library called libgit2. If it is a language that can cooperate with C (?) You can use it to create git applications and libraries. https://github.com/libgit2/libgit2#language-bindings If you have a nice library in the language you want to use You can use it in your own application, or embed libgit with reference to it. [A2.2 Appendix B: Incorporate Git into your application-How to use Libgit2](https://git-scm.com/book/ja/v2/Appendix-B:-Git%E3%82%92% E3% 81% 82% E3% 81% AA% E3% 81% 9F% E3% 81% AE% E3% 82% A2% E3% 83% 97% E3% 83% AA% E3% 82% B1% E3% 83% BC% E3% 82% B7% E3% 83% A7% E3% 83% B3% E3% 81% AB% E7% B5% 84% E3% 81% BF% E8% BE% BC% E3% 82% 80-Libgit2% E3% 82% 92% E4% BD% BF% E3% 81% 86% E6% 96% B9% E6% B3% 95)

Do not fight the environment

For example, I'm an iOS / Android app developer It feels like a little JavaScript and TypeScript in a hybrid app. I wanted to write TypeScript, so I tried nodegit, but I gave up because I couldn't move it with Electron in my environment. Atom's git-utils also doesn't work with Electron.

So I'm going to Java. Since it is an Androider, it seems that you can be impressed just by saying Java 8.

The above language-bindings have jagged,

This is very experimental, very mediocre JNI bindings for libgit2.

I'm not used to JNI, so I decided to use JGit quietly here.

JGit

From the conclusion, there are not enough. Also, it seems to be an original implementation rather than binding to libgit2, and it behaves differently from the git command.

Not limited to JGit, libraries in each language may not have all the necessary git functions, I think that it will be okay until you create a GUI framework such as "acquire and display Diff".

The version used was 4.8.0.201706111038-r.

build.gradle


dependencies {
    // https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit
    compile group: 'org.eclipse.jgit', name: 'org.eclipse.jgit', version: '4.8.0.201706111038-r'
}

diff

Output as text

To get the diff as text: Passing to FileRepositoryBuilder.create () is not the top of the repository, but the .git directory.

        final File gitDir; // .git directory
        final Repository repository = FileRepositoryBuilder.create(gitDir)
        final Git git = new Git(repository);
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final List<DiffEntry> entryList = git.diff()
                .setCached(cached)
                .setOutputStream(bos)
                .call();
        final String text = new String(bos.toByteArray(), StandardCharsets.UTF_8)

Various options for DiffCommand returned by git.diff () To call call ().

Same for other commands

Commands other than diff have a similar interface.

  1. Get an instance of the class that corresponds to the git command
  2. Set options
  3. Get the result with the call () method

DiffFormatter

[DiffEntry] with git.diff (). Call () (http://download.eclipse.org/jgit/site/4.8.0.201706111038-r/apidocs/org/eclipse/jgit/diff/DiffEntry.html) You can also get it and then spit it out to OutputStream again. Use the class DiffFormatter. (Even if OutputStream is set in DiffCommand, DiffFormatter is used inside the call () method.)

There are some caveats when using DiffFormatter and DiffEntry. When looking at the diff of a workspace that has not staged the change (not registered in the index). (Of course, when you look at a diff in your workspace, that is, with git diff, it's normal that you're not staged.)

        final Git git = new Git(repository);
        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        final List<DiffEntry> entryList = git.diff()
                .setCached(false)
//                .setOutputStream(bos)
                .call();

        final DiffFormatter fmt = new DiffFormatter(bos);
        fmt.setRepository(repository);
        fmt.format(entryList);
        final String text = new String(bos.toByteArray(), StandardCharsets.UTF_8);

This will throw an org.eclipse.jgit.errors.MissingObjectException. There is no problem with git.diff (). SetCached (true) .call (), which is equivalent to git add --cached.

** Next, I will write the cause and remedy, but there is no practical merit. ** ** Dedicated to those who are in trouble or have trouble writing code like the one above.

There are two types of diff targets

There are two types of diff targets in git.

The git database allows you to extract a compressed file by the hash value of the file, which is a blob object. git add In other words, when you stage, it will be calculated and created from the changed file and registered in the index. You can also create it with the git hash-object -w command.

DiffFormatter is a pair of ContentSource for diff. .Pair](http://download.eclipse.org/jgit/site/4.8.0.201706111038-r/apidocs/org/eclipse/jgit/diff/ContentSource.Pair.html). There are also two ways to create a ContentSource, depending on the two types of diff targets.

However, when format (DiffEntry ent) is done, both ContentSource.Pair are ContentSource for blob objects. And the workspace change (changed file) that is not stage does not exist as a blob object in the git database.

The result is org.eclipse.jgit.errors.MissingObjectException. This is also the case with the FileHeader to FileHeader (DiffEntry ent) method.

In such a case, inside git.diff (). call (), the code is as follows (excluding judgment and others). If you do the same thing, it will be output to OutputStream normally.

        fmt.setRepository(repository)
        final AbstractTreeIterator oldTree = new DirCacheIterator(repository.readDirCache());
        final AbstractTreeIterator newTree = new FileTreeIterator(repository);
        fmt.scan(oldTree, newTree);

This [List <DiffEntry> scan (AbstractTreeIterator a, AbstractTreeIterator b)](http://download.eclipse.org/jgit/site/4.8.0.201706111038-r/apidocs/org/eclipse/jgit/diff/DiffFormatter. The html # scan-org.eclipse.jgit.treewalk.AbstractTreeIterator-org.eclipse.jgit.treewalk.AbstractTreeIterator-) method doesn't exist for this purpose, but there is another DiffFormatter ContentSource. There is no way to change it from the outside.

Now, to output a diff from the DiffEntry that is the return value of ** git.diff (). call (), use the scan method and format method of DiffFormatter **. However, since the return value of the scan method is also DiffEntry, git.diff (). Call () is unnecessary, thank you.

untracked files

I don't know if it's the intended result or not implemented, but JGit's DiffCommand also includes ** untracked files ** in the diff. In other words, git.diff (). Call (), which should be a command equivalent to git diff, compares the untracked filelob object registered in index with the file in the workspace as it is.

command old new
git diff Workspace files (untracked files)except Blob object registered in index
git.diff().call() by JGit Workspace files (untracked files)Including Blob object registered in index

I don't think there are many when you want to see diff and you don't want to see untracked files, so I think JGit's behavior is not so problematic. It would be best to specify it as an option.

Issuing a diff for untracked files with the git command is a bit tedious.

git ls-files -o --exclude-standard

You can get the list with and get the diff with the following command.

git diff --no-index /dev/null [untracked file name]

Rest assured that Windows will also compare with / dev / null. If you just want to see the difference, it's a new file, so you can look at the file itself, In the current context, what I want is a patch-style diff that can be applied, so I introduced it here.

Workspace? Work tree?

When we say "works-space diff", it seems strange that the diff doesn't contain untracked files.

But is git diff a" workspace "and index diff in the first place? At least "workspace" is not a git term, so Perhaps the reality is that you are confused by using arbitrary words.

The word workspace is rarely used in official documentation, https://git-scm.com/search/results?search=workspace https://git-scm.com/search/results?search=work%20space

The word work tree is often used. https://git-scm.com/search/results?search=worktree https://git-scm.com/search/results?search=work%20tree

This is a challenge because we don't know exactly what the word worktree means.

apply

ApplyCommand and git apply --cached

** The conclusion I got in the previous issue was "To stage line by line, create a patch that reflects the line change and do git apply --cached "**. (Git apply -R --cached for line-by-line unstage.)

Like DiffCommand for diff, apply has ApplyCommand .. And the description of the JavaDoc class says:

Apply a patch to files and/or to the index.

However, ApplyCommand cannot apply patch to index. can not. Unlike DiffCommand, there is no method like setCached (). Even if you look at Source, it finally It seemed like I was just writing the file and setting the permissions.

https://github.com/eclipse/jgit/blob/6dab29f4b578bc164acb77083440e4087ed94ff4/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java#L262


		FileWriter fw = new FileWriter(f);
		fw.write(sb.toString());
		fw.close();

		getRepository().getFS().setExecute(f, fh.getNewMode() == FileMode.EXECUTABLE_FILE);

I was troubled.

Git apply as a omnibus so far

I have confirmed that you can do it only with "Apply a patch to files" by following the steps below.

  1. Modify the file
  2. Create a patch
  3. Revert file changes
  4. Pass the patch to ApplyCommand

In step 2, use DiffCommand to output to a file, and in step 4, pass the file to ApplyCommand. It's a omnibus so far.

AddCommand and ResetCommand

Git add to perform stage for each file, equivalent [AddCommand](http://download.eclipse.org/jgit/site/4.8.0.201706111038-r/apidocs/org/eclipse/jgit/api/AddCommand There is .html). It's hard to think that it has more than git add in the first place, JGit is not a complete copy of git, so if you look at the method list while expecting it, ʻAddCommand setWorkingTreeIterator (WorkingTreeIterator f)` There is a slight hope that the power of WorkingTreeIterator that can be passed to it may be somehow managed. However, ResetCommand does not have a similar method, so follow up. Let's stop.

I tried to find a file like that seriously and appropriately, but I couldn't find it. It was a short relationship, but it seems that it's time to say goodbye to JGit.

CRLF

Another point before farewell.

Normally (?) In a Windows environment, a line break is made with CRLF.

Then set the autocrlf function to true, commit with LF, and fetch with CRLF remotely. That should be officially recommended. I feel that the installer says recommend. (However, it is annoying because there are times when you want to fetch as LF.)

JGit checks if the workspace and patch's assumptions match before applying. It seems that CRLF is not taken into account in this comparison, and each line is interpreted as having a CR at the end. cr.PNG And because each line does not match, it throws org.eclipse.jgit.api.errors.PatchApplyException.

If the patch itself is set to CRLF, it will not be recognized as a valid patch. This also applies to the git command, It seems that CL is correct.

Next issue notice

git.exe

There is also a way to embark on a journey to explore the supreme library, Not limited to JGit, but also binding to libgit2 and other libraries You may run into problems with lack of functionality. Therefore, from the next issue, we will use git.exe, which is a git application that boasts the best functionality. There may be challenges in dealing with different processes, It seems that you will be able to acquire knowledge that can be understood in other languages and environments.

workaround

Surprisingly (?) I don't know if it's a bug or a specification, Whether it's a git command or the git-gui that comes with git itself There are some patterns that I just noticed that cannot be staged / unstaged normally.

The next issue will mainly introduce these issues and workarounds.

Perhaps the world isn't doing stage / unstage on a line-by-line basis. Then it makes sense to make it yourself.

Editor's Note

I felt it before writing the previous issue "Basic Knowledge". The more you use the git command, the more familiar you are with the CLI. Be aware that some people lose the motivation to create a git GUI client.

Recommended Posts

Make a weekly git GUI client [2] stage / unstage incomplete capture
Make a weekly git GUI client [4] diff et al.
Create a weekly git GUI client [5] First desktop app