Статьи

Веселиться с Git

Я недавно прочитал книгу Git . Когда я проходил через части Git Internals, меня поразило, насколько проста и элегантна структура Git на самом деле. Я решил, что мне просто нужно создать свою маленькую библиотеку для работы с репозиториями Git (как и вы). Я называю результат Silly Jgit . В этой статье я буду идти по коду.

Эта статья для вас, если вы хотите немного глубже понять Git или, возможно, даже хотите работать непосредственно с Git-репозиторием на вашем любимом языке программирования. Я пройдусь по четырем темам: 1) Чтение необработанного коммита из репозитория, 2) Чтение хеша дерева корня коммита, 3) Анализ списка файлов дерева каталогов и 4) Чтение содержимого файла из подкаталог коммит корня.

Чтение головы коммит из репозитория

Первое, что нам нужно сделать, чтобы прочитать заголовок коммита, это выяснить, какой коммит является главой репозитория. Файл .git / HEAD представляет собой простой текстовый файл, который содержит имя файла в каталоге .git / refs /head. Если вы проверили мастер, это будет .git / refs /head / master. Этот файл представляет собой простой текстовый файл, который содержит хэш , то есть шестнадцатеричное число из 40 цифр. Хеш может быть преобразован в имя файла Git Object в .git / objects. Этот файл является сжатым файлом, содержащим информацию о коммите. Вот код для чтения:

File repository = new File(".git");
File headFile = new File(repository,
         Util.asString(new File(repository, "HEAD")).split(" ")[1].trim());
 
String commitHash =  Util.asString(headFile).trim();
File commitFile = new File(repository,
         "objects/" + commitHash.substring(0,2) + "/" + commitHash.substring(2));
try(final InputStream inputStream = new InflaterInputStream(new FileInputStream(commitFile))) {
    System.out.println(Util.asString(inputStream));
}

Запуск этого кода приводит к следующему выводу (обратите внимание, что некоторые пробелы в выводе на самом деле являются нулевыми байтами в файле):

commit 237 tree c03265971361724e18e31cc83e5c60cd0e0f5754
parent 141f5d5a2cc0c268e7b05be17a49c1c0dc61efad
author Johannes Brodwall  1379445359 +0200
committer Johannes Brodwall  1379445359 +0200

This is the commit comment

Нахождение дерева каталогов коммита

Когда у нас есть информация о коммите, мы можем проанализировать ее, чтобы найти хэш дерева . Хеш дерева ссылается на другой файл в .git / objects, который содержит индекс корневого каталога файлов в коммите. В приведенном выше примере хеш дерева «c03265971361724e18e31cc83e5c60cd0e0f5754». Но прежде чем читать хеш дерева, мы должны прочитать тип объекта (в данном случае «коммит») и размер (в данном случае 237).

String treeHash;
try(final InputStream inputStream = new InflaterInputStream(new FileInputStream(commitFile))) {
    String type = Util.stringUntil(inputStream, ' ');
    long length = Long.valueOf(Util.stringUntil(inputStream, (char)0));
    Util.stringUntil(inputStream, ' ');
    treeHash = Util.stringUntil(inputStream, '\n');
    System.out.println("Tree hash: " + treeHash);
}
 
File rootTreeFile = new File(repository,
       "objects/" + treeHash.substring(0,2) + "/" + treeHash.substring(2));
try(final InputStream inputStream = new InflaterInputStream(new FileInputStream(rootTreeFile))) {
    System.out.println(Util.asString(inputStream));
}

Однако посмотреть на хеш-файл дерева не так просто:

tree 130 100644 FOO æ?â?²ÑÖCK?)®wZØÂä?S?
100644 FOO.txt ýc?Õô¹ìmìªGAk?X?ï'&
100644 README Wýs?ºyâx+@îR°X040000 lib ?ñG»Ñ?¼>&8´. ?úË¢i[o

Следующая часть этой статьи покажет, как с этим бороться.

Разбор дерева каталогов

В дереве есть много мусора. Но не паникуйте. Как и в случае с объектом коммита, объект дерева начинается с типа («дерево») и размера (130). После этого он перечислит каждый файл или каталог. Каждая запись дерева состоит из прав (которые также сообщают нам, является ли это файл или каталог), имени файла и хеша записи, но на этот раз в виде двоичного числа . Мы можем прочитать записи и найти нужный файл. Затем мы можем просто распечатать содержимое этого файла:

File rootTreeFile = new File(repository,
        "objects/" + treeHash.substring(0,2) + "/" + treeHash.substring(2));
Map<string ,String> entries = new HashMap<>();
try(final InputStream inputStream = new InflaterInputStream(new FileInputStream(rootTreeFile))) {
    String type = Util.stringUntil(inputStream, ' ');
    long length = Long.valueOf(Util.stringUntil(inputStream, (char)0));
 
    while (true) {
        String octalMode = Util.leftPad(Util.stringUntil(inputStream, ' '), 6, '0');
        if (octalMode == null) break;
 
        String path = Util.stringUntil(inputStream, (char)0);
        StringBuilder hash = new StringBuilder();
        for (int i=0; i<20; i++) {
            hash.append(Util.leftPad(Integer.toHexString(inputStream.read()), 2, '0'));
        }
        entries.put(path, hash.toString());
    }
}
 
System.out.println(entries);
</string>

Вот пример разобранного списка каталогов. Я не показал octalMode для каждого файла, но это может быть чрезвычайно полезно для разделения между каталогами (которые octalMode начинается с 0) и файлами:

{FOO.txt=fd6385d5f4b9ec6decaa47416b7f96588aef2726,
lib=8ff147bbd18fbc3e2638b42ea09cfacba2695b6f,
README=57fd19a7738eba1e79e2782b161a40ee52b05801,
FOO=e69de29bb2d1d6434b8b29ae775ad8c2e48c5391}

Чтение файла

Это приводит нас к концу нашего путешествия — как читать содержимое файла. Как только у нас есть записи дерева, нужно просто найти хеш для имени файла и проанализировать этот файл. Как и прежде, содержимое файла будет начинаться с типа («blob» — что означает «данные», я думаю) и размера файла:

String blobHash = entries.get("README");
File blobFile = new File(repository, "objects/" + blobHash.substring(0,2) + "/" + blobHash.substring(2));
try(final InputStream inputStream = new InflaterInputStream(new FileInputStream(blobFile))) {
    String type = Util.stringUntil(inputStream, ' ');
    long length = Long.valueOf(Util.stringUntil(inputStream, (char)0));
 
    System.out.println(Util.asString(inputStream));
}

Это печатает содержимое нашего файла. Очевидно, что если вы хотите найти файл в подкаталоге, вам придется проделать немного больше работы: проанализировать другой объект дерева и посмотреть и запись в этом объекте и т. Д.

Выводы

В этом сообщении блога показано, как менее чем за 50 строк кода, без каких-либо зависимостей (но с небольшим вспомогательным классом утилит), мы можем найти главный коммит репозитория git, проанализировать список файлов корня дерева файлов для этого коммита и распечатайте содержимое файла. Самым сложным было выяснить, что InflaterInputStreamдля распаковки git-объекта нужен был не Zip или Gzip, а сам Zip.

Мой проект silly-jgit поддерживает чтение и запись коммитов , деревьев и хэшей из .git / objects. Это только основной поднабор команд Git. Более того, когда я писал статью, я заметил, что git часто упаковывает объекты в .git / objects / pack. Это добавляет совершенно новое измерение, с которым я раньше не сталкивался.

Я надеюсь, что никто не настолько безумен, чтобы использовать мою глупую библиотеку Git для Java. Но я очень надеюсь, что эта статья дала вам ощущение мастерства в Git.