gout

A static git page generator
git clone https://git.bracken.jp/gout.git
Log | Files | Refs | README | LICENSE

git.c (11321B)


      1 #include "git.h"
      2 
      3 #include <assert.h>
      4 #include <err.h>
      5 #include <git2.h>
      6 #include <stdlib.h>
      7 #include <string.h>
      8 #include <sys/stat.h>
      9 
     10 #include "git/internal.h"
     11 #include "third_party/openbsd/reallocarray.h"
     12 
     13 /* Global const data. */
     14 static_assert(GOUT_OID_MAX_SIZE == GIT_OID_MAX_HEXSIZE + 1,
     15               "GOUT_OID_MAX_SIZE must match libgit2's OID size");
     16 
     17 /* Maximum file size to load into memory, in bytes. */
     18 static const ssize_t kMaxFileSizeBytes = 16 * 1024 * 1024;
     19 
     20 /* Forward declarations for VTable functions. */
     21 static void libgit2_initialize(Git* git);
     22 static void libgit2_shutdown(Git* git);
     23 static void libgit2_for_repo(Git* git,
     24                              const char* path,
     25                              RepoCallback cb,
     26                              void* user_data);
     27 static void libgit2_for_each_commit(Git* git,
     28                                     CommitCallback cb,
     29                                     void* user_data);
     30 static void libgit2_for_commit(Git* git,
     31                                const char* spec,
     32                                CommitCallback cb,
     33                                void* user_data);
     34 static void libgit2_for_each_reference(Git* git,
     35                                        ReferenceCallback cb,
     36                                        void* user_data);
     37 static void libgit2_for_each_file(Git* git, FileCallback cb, void* user_data);
     38 
     39 /* Internal libgit2 utilities. */
     40 static int oid_for_spec(git_repository* repo, const char* spec, git_oid* oid);
     41 static size_t string_count_lines(const char* str, ssize_t str_len);
     42 static bool gitrepo_walk_tree_files(git_repository* repo,
     43                                     git_tree* tree,
     44                                     const char* path,
     45                                     FileCallback cb,
     46                                     void* user_data);
     47 
     48 static void libgit2_initialize(Git* git) {
     49   assert(git != NULL);
     50   (void)git;
     51   /* do not search outside the git repository:
     52      GIT_CONFIG_LEVEL_APP is the highest level currently */
     53   git_libgit2_init();
     54   for (int i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) {
     55     git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "");
     56   }
     57   /* do not require the git repository to be owned by the current user */
     58   git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
     59 }
     60 
     61 static void libgit2_shutdown(Git* git) {
     62   assert(git != NULL);
     63   (void)git;
     64   git_libgit2_shutdown();
     65 }
     66 
     67 Git* gout_git_create(const FileSystem* fs) {
     68   assert(fs != NULL);
     69   Git* git = ecalloc(1, sizeof(Git));
     70   git->impl = NULL;
     71   git->fs = fs;
     72   git->repo = NULL;
     73 
     74   git->initialize = libgit2_initialize;
     75   git->shutdown = libgit2_shutdown;
     76   git->for_repo = libgit2_for_repo;
     77   git->for_each_commit = libgit2_for_each_commit;
     78   git->for_commit = libgit2_for_commit;
     79   git->for_each_reference = libgit2_for_each_reference;
     80   git->for_each_file = libgit2_for_each_file;
     81 
     82   return git;
     83 }
     84 
     85 void gout_git_free(Git* git) {
     86   if (!git) {
     87     return;
     88   }
     89   if (git->impl) {
     90     git_repository_free((git_repository*)git->impl);
     91   }
     92   if (git->repo) {
     93     gitrepo_free(git->repo);
     94   }
     95   free(git);
     96 }
     97 
     98 /* VTable Implementation */
     99 
    100 static void libgit2_for_repo(Git* git,
    101                              const char* path,
    102                              RepoCallback cb,
    103                              void* user_data) {
    104   assert(git != NULL);
    105   assert(path != NULL);
    106   assert(cb != NULL);
    107   git->impl = gitrepo_open_repository(path);
    108   git->repo = gitrepo_create_from_repository(git->fs, path, git->impl);
    109   cb(git, user_data);
    110   gitrepo_free(git->repo);
    111   git->repo = NULL;
    112   git_repository_free((git_repository*)git->impl);
    113   git->impl = NULL;
    114 }
    115 
    116 static void libgit2_for_each_commit(Git* git,
    117                                     CommitCallback cb,
    118                                     void* user_data) {
    119   assert(git != NULL);
    120   assert(cb != NULL);
    121   git_repository* repo = (git_repository*)git->impl;
    122   git_oid start_oid;
    123   if (oid_for_spec(repo, "HEAD", &start_oid) != 0) {
    124     return;
    125   }
    126 
    127   git_revwalk* revwalk = NULL;
    128   if (git_revwalk_new(&revwalk, repo) != 0) {
    129     errx(1, "git_revwalk_new");
    130   }
    131   git_revwalk_push(revwalk, &start_oid);
    132 
    133   git_oid current;
    134   while (!git_revwalk_next(&current, revwalk)) {
    135     GitCommit* commit = gitcommit_create(&current, repo);
    136     cb(commit, user_data);
    137     gitcommit_free(commit);
    138   }
    139   git_revwalk_free(revwalk);
    140 }
    141 
    142 static void libgit2_for_commit(Git* git,
    143                                const char* spec,
    144                                CommitCallback cb,
    145                                void* user_data) {
    146   assert(git != NULL);
    147   assert(spec != NULL);
    148   assert(cb != NULL);
    149   git_repository* repo = (git_repository*)git->impl;
    150   git_oid start_oid;
    151   if (oid_for_spec(repo, spec, &start_oid) != 0) {
    152     return;
    153   }
    154 
    155   git_revwalk* revwalk = NULL;
    156   if (git_revwalk_new(&revwalk, repo) != 0) {
    157     errx(1, "git_revwalk_new");
    158   }
    159   git_revwalk_push(revwalk, &start_oid);
    160 
    161   git_oid current;
    162   if (!git_revwalk_next(&current, revwalk)) {
    163     GitCommit* commit = gitcommit_create(&current, repo);
    164     cb(commit, user_data);
    165     gitcommit_free(commit);
    166   }
    167   git_revwalk_free(revwalk);
    168 }
    169 
    170 static void libgit2_for_each_reference(Git* git,
    171                                        ReferenceCallback cb,
    172                                        void* user_data) {
    173   assert(git != NULL);
    174   assert(cb != NULL);
    175   git_repository* repo = (git_repository*)git->impl;
    176   git_reference_iterator* it = NULL;
    177   if (git_reference_iterator_new(&it, repo) != 0) {
    178     errx(1, "git_reference_iterator_new");
    179   }
    180 
    181   GitReference** refs = NULL;
    182   size_t refs_len = 0;
    183   git_reference* current = NULL;
    184   while (!git_reference_next(&current, it)) {
    185     if (!git_reference_is_branch(current) && !git_reference_is_tag(current)) {
    186       git_reference_free(current);
    187       continue;
    188     }
    189     char* refname = estrdup(git_reference_shorthand(current));
    190     GitReference* ref = gitreference_create(repo, current);
    191     if (!ref) {
    192       warnx("skipping reference with missing target: %s", refname);
    193       free(refname);
    194       continue;
    195     }
    196     free(refname);
    197     refs = reallocarray(refs, refs_len + 1, sizeof(GitReference*));
    198     if (!refs) {
    199       err(1, "reallocarray");
    200     }
    201     refs[refs_len++] = ref;
    202   }
    203   git_reference_iterator_free(it);
    204   if (refs_len > 0) {
    205     qsort(refs, refs_len, sizeof(GitReference*), gitreference_compare);
    206   }
    207 
    208   for (size_t i = 0; i < refs_len; i++) {
    209     cb(refs[i], user_data);
    210     gitreference_free(refs[i]);
    211   }
    212   free(refs);
    213 }
    214 
    215 static void libgit2_for_each_file(Git* git, FileCallback cb, void* user_data) {
    216   assert(git != NULL);
    217   assert(cb != NULL);
    218   git_repository* repo = (git_repository*)git->impl;
    219   git_commit* commit = NULL;
    220   git_oid id;
    221   if (oid_for_spec(repo, "HEAD", &id) != 0 ||
    222       git_commit_lookup(&commit, repo, &id) != 0) {
    223     return;
    224   }
    225   git_tree* tree = NULL;
    226   if (git_commit_tree(&tree, commit) != 0) {
    227     git_commit_free(commit);
    228     return;
    229   }
    230   git_commit_free(commit);
    231 
    232   if (!gitrepo_walk_tree_files(repo, tree, "", cb, user_data)) {
    233     git_tree_free(tree);
    234     return;
    235   }
    236   git_tree_free(tree);
    237 }
    238 
    239 /* Utilities */
    240 
    241 static int oid_for_spec(git_repository* repo, const char* spec, git_oid* oid) {
    242   assert(repo != NULL);
    243   assert(spec != NULL);
    244   assert(oid != NULL);
    245   git_object* obj = NULL;
    246   if (git_revparse_single(&obj, repo, spec)) {
    247     return -1;
    248   }
    249   git_oid_cpy(oid, git_object_id(obj));
    250   git_object_free(obj);
    251   return 0;
    252 }
    253 
    254 static size_t string_count_lines(const char* str, ssize_t str_len) {
    255   if (!str || str_len <= 0) {
    256     return 0;
    257   }
    258   size_t lines = 0;
    259   for (ssize_t i = 0; i < str_len; i++) {
    260     if (str[i] == '\n') {
    261       lines++;
    262     }
    263   }
    264   return str[str_len - 1] == '\n' ? lines : lines + 1;
    265 }
    266 
    267 char git_get_filetype(git_filemode_t m) {
    268   switch (m & S_IFMT) {
    269     case S_IFREG:
    270       return '-';
    271     case S_IFBLK:
    272       return 'b';
    273     case S_IFCHR:
    274       return 'c';
    275     case S_IFDIR:
    276       return 'd';
    277     case S_IFIFO:
    278       return 'p';
    279     case S_IFLNK:
    280       return 'l';
    281     case S_IFSOCK:
    282       return 's';
    283     default:
    284       return '?';
    285   }
    286 }
    287 
    288 char* git_format_filemode(git_filemode_t m) {
    289   char* mode = estrdup("----------");
    290 
    291   mode[0] = git_get_filetype(m);
    292   static const char chars[] = "rwxrwxrwx";
    293   static const int masks[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP,
    294                               S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH};
    295 
    296   for (size_t i = 0; i < 9; i++) {
    297     if (m & masks[i]) {
    298       mode[i + 1] = chars[i];
    299     }
    300   }
    301 
    302   if (m & S_ISUID) {
    303     mode[3] = (mode[3] == 'x') ? 's' : 'S';
    304   }
    305   if (m & S_ISGID) {
    306     mode[6] = (mode[6] == 'x') ? 's' : 'S';
    307   }
    308   if (m & S_ISVTX) {
    309     mode[9] = (mode[9] == 'x') ? 't' : 'T';
    310   }
    311 
    312   return mode;
    313 }
    314 
    315 static bool gitrepo_walk_tree_files(git_repository* repo,
    316                                     git_tree* tree,
    317                                     const char* path,
    318                                     FileCallback cb,
    319                                     void* user_data) {
    320   assert(repo != NULL);
    321   assert(tree != NULL);
    322   assert(path != NULL);
    323   assert(cb != NULL);
    324 
    325   for (size_t i = 0; i < git_tree_entrycount(tree); i++) {
    326     const git_tree_entry* entry = git_tree_entry_byindex(tree, i);
    327     if (!entry) {
    328       return false;
    329     }
    330 
    331     const char* entryname = git_tree_entry_name(entry);
    332     if (!entryname) {
    333       return false;
    334     }
    335 
    336     char* entrypath = path_concat(path, entryname);
    337 
    338     git_filemode_t mode = git_tree_entry_filemode(entry);
    339     if (mode == GIT_FILEMODE_COMMIT) {
    340       char oid_str[GOUT_OID_MAX_SIZE];
    341       git_oid_tostr(oid_str, sizeof(oid_str), git_tree_entry_id(entry));
    342       GitFile* fileinfo =
    343           gitfile_create(kFileTypeSubmodule, "m---------", entrypath,
    344                          ".gitmodules", oid_str, -1, -1, "");
    345       cb(fileinfo, user_data);
    346       gitfile_free(fileinfo);
    347       free(entrypath);
    348       continue;
    349     }
    350 
    351     git_object* obj = NULL;
    352     if (git_tree_entry_to_object(&obj, repo, entry) != 0) {
    353       warnx("failed to get object for tree entry: %s", entrypath);
    354       free(entrypath);
    355       continue;
    356     }
    357 
    358     switch (git_object_type(obj)) {
    359       case GIT_OBJECT_BLOB:
    360         break;
    361       case GIT_OBJECT_TREE: {
    362         if (!gitrepo_walk_tree_files(repo, (git_tree*)obj, entrypath, cb,
    363                                      user_data)) {
    364           git_object_free(obj);
    365           free(entrypath);
    366           return false;
    367         }
    368         git_object_free(obj);
    369         free(entrypath);
    370         continue;
    371       }
    372       default:
    373         git_object_free(obj);
    374         free(entrypath);
    375         continue;
    376     }
    377 
    378     git_blob* blob = (git_blob*)obj;
    379     ssize_t size_bytes = git_blob_rawsize(blob);
    380     ssize_t size_lines = -1;
    381     const char* content = NULL;
    382 
    383     if (size_bytes > kMaxFileSizeBytes) {
    384       size_lines = -2; /* oversized file */
    385     } else if (!git_blob_is_binary(blob)) {
    386       content = (const char*)git_blob_rawcontent(blob);
    387       size_lines = string_count_lines(content, size_bytes);
    388     }
    389 
    390     char* filemode = git_format_filemode(git_tree_entry_filemode(entry));
    391     GitFile* fileinfo =
    392         gitfile_create(kFileTypeFile, filemode, entrypath, entrypath, "",
    393                        size_bytes, size_lines, content);
    394     cb(fileinfo, user_data);
    395     gitfile_free(fileinfo);
    396     git_object_free(obj);
    397     free(filemode);
    398     free(entrypath);
    399   }
    400   return true;
    401 }