gout

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

git.c (11486B)


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