gout

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

repo.c (13889B)


      1 #include "git/repo.h"
      2 
      3 #include <err.h>
      4 #include <git2/blob.h>
      5 #include <git2/commit.h>
      6 #include <git2/object.h>
      7 #include <git2/oid.h>
      8 #include <git2/refs.h>
      9 #include <git2/repository.h>
     10 #include <git2/revparse.h>
     11 #include <git2/revwalk.h>
     12 #include <git2/tree.h>
     13 #include <git2/types.h>
     14 #include <libgen.h>
     15 #include <limits.h>
     16 #include <stdbool.h>
     17 #include <stdio.h>
     18 #include <stdlib.h>
     19 #include <string.h>
     20 #include <sys/stat.h>
     21 #include <sys/types.h>
     22 #include <sys/unistd.h>
     23 #include <unistd.h>
     24 
     25 #include "git/internal.h"
     26 #include "third_party/openbsd/reallocarray.h"
     27 #include "utils.h"
     28 
     29 struct GitRepo {
     30   char* name;
     31   char* short_name;
     32   char* owner;
     33   char* description;
     34   char* clone_url;
     35   const char* submodules;
     36   const char* readme;
     37   const char* license;
     38 
     39   void* repo;
     40 };
     41 
     42 /* Local const data */
     43 static const char* kLicenses[] = {"HEAD:LICENSE", "HEAD:LICENSE.md",
     44                                   "HEAD:COPYING"};
     45 static const size_t kLicensesLen = sizeof(kLicenses) / sizeof(char*);
     46 static const char* kReadmes[] = {"HEAD:README", "HEAD:README.md"};
     47 static const size_t kReadmesLen = sizeof(kReadmes) / sizeof(char*);
     48 
     49 /* Maximum file size to load into memory, in bytes. */
     50 static const ssize_t kMaxFileSizeBytes = 16 * 1024 * 1024;
     51 
     52 /* Utilities */
     53 static size_t string_count_lines(const char* str, ssize_t size_bytes);
     54 static bool string_ends_with(const char* str, const char* suffix);
     55 static char* first_line_of_file(const char* path);
     56 static const git_oid* oid_for_spec(git_repository* repo, const char* spec);
     57 static char get_filetype(git_filemode_t m);
     58 static char* format_filemode(git_filemode_t m);
     59 
     60 /* GitRepo utilities. */
     61 static char* gitrepo_name_from_path(const char* repo_path);
     62 static char* gitrepo_shortname_from_name(const char* name);
     63 static bool gitrepo_has_blob(git_repository* repo, const char* file);
     64 static const char* gitrepo_first_matching_file(git_repository* repo,
     65                                                const char** files,
     66                                                size_t files_len);
     67 static void gitrepo_load_metadata(GitRepo* repo, const char* path);
     68 static bool gitrepo_walk_tree_files(git_repository* repo,
     69                                     git_tree* tree,
     70                                     const char* path,
     71                                     FileCallback cb,
     72                                     void* user_data);
     73 
     74 size_t string_count_lines(const char* str, ssize_t str_len) {
     75   if (str_len <= 0) {
     76     return 0;
     77   }
     78   size_t lines = 0;
     79   for (ssize_t i = 0; i < str_len; i++) {
     80     if (str[i] == '\n') {
     81       lines++;
     82     }
     83   }
     84   return str[str_len - 1] == '\n' ? lines : lines + 1;
     85 }
     86 
     87 bool string_ends_with(const char* str, const char* suffix) {
     88   if (!str || !suffix) {
     89     return false;
     90   }
     91   size_t str_len = strlen(str);
     92   size_t suffix_len = strlen(suffix);
     93   if (str_len < suffix_len) {
     94     return false;
     95   }
     96   return strncmp(str + str_len - suffix_len, suffix, suffix_len) == 0;
     97 }
     98 
     99 char* first_line_of_file(const char* path) {
    100   FILE* f = fopen(path, "r");
    101   if (!f) {
    102     return estrdup("");
    103   }
    104   char* buf = NULL;
    105   size_t buf_size = 0;
    106   ssize_t len = getline(&buf, &buf_size, f);
    107   fclose(f);
    108   if (len == -1) {
    109     free(buf);
    110     return estrdup("");
    111   }
    112   // Remove trailing newline.
    113   if (len > 0 && buf[len - 1] == '\n') {
    114     buf[len - 1] = '\0';
    115   }
    116   return buf;
    117 }
    118 
    119 const git_oid* oid_for_spec(git_repository* repo, const char* spec) {
    120   git_object* obj = NULL;
    121   if (git_revparse_single(&obj, repo, spec)) {
    122     return NULL;
    123   }
    124   const git_oid* oid = git_object_id(obj);
    125   git_object_free(obj);
    126   return oid;
    127 }
    128 
    129 char get_filetype(git_filemode_t m) {
    130   switch (m & S_IFMT) {
    131     case S_IFREG:
    132       return '-';
    133     case S_IFBLK:
    134       return 'b';
    135     case S_IFCHR:
    136       return 'c';
    137     case S_IFDIR:
    138       return 'd';
    139     case S_IFIFO:
    140       return 'p';
    141     case S_IFLNK:
    142       return 'l';
    143     case S_IFSOCK:
    144       return 's';
    145     default:
    146       return '?';
    147   }
    148 }
    149 
    150 char* format_filemode(git_filemode_t m) {
    151   char* mode = ecalloc(11, sizeof(char));
    152   memset(mode, '-', 10);
    153   mode[10] = '\0';
    154 
    155   mode[0] = get_filetype(m);
    156   if (m & S_IRUSR) {
    157     mode[1] = 'r';
    158   }
    159   if (m & S_IWUSR) {
    160     mode[2] = 'w';
    161   }
    162   if (m & S_IXUSR) {
    163     mode[3] = 'x';
    164   }
    165   if (m & S_IRGRP) {
    166     mode[4] = 'r';
    167   }
    168   if (m & S_IWGRP) {
    169     mode[5] = 'w';
    170   }
    171   if (m & S_IXGRP) {
    172     mode[6] = 'x';
    173   }
    174   if (m & S_IROTH) {
    175     mode[7] = 'r';
    176   }
    177   if (m & S_IWOTH) {
    178     mode[8] = 'w';
    179   }
    180   if (m & S_IXOTH) {
    181     mode[9] = 'x';
    182   }
    183 
    184   if (m & S_ISUID) {
    185     mode[3] = (mode[3] == 'x') ? 's' : 'S';
    186   }
    187   if (m & S_ISGID) {
    188     mode[6] = (mode[6] == 'x') ? 's' : 'S';
    189   }
    190   if (m & S_ISVTX) {
    191     mode[9] = (mode[9] == 'x') ? 't' : 'T';
    192   }
    193 
    194   return mode;
    195 }
    196 
    197 char* gitrepo_name_from_path(const char* repo_path) {
    198   char* path_copy = estrdup(repo_path);
    199   const char* filename = basename(path_copy);
    200   if (!filename) {
    201     err(1, "basename");
    202   }
    203   char* result = estrdup(filename);
    204   free(path_copy);
    205   return result;
    206 }
    207 
    208 char* gitrepo_shortname_from_name(const char* name) {
    209   char* short_name = estrdup(name);
    210   if (string_ends_with(short_name, ".git")) {
    211     size_t short_name_len = strlen(short_name);
    212     size_t suffix_len = strlen(".git");
    213     short_name[short_name_len - suffix_len] = '\0';
    214   }
    215   return short_name;
    216 }
    217 
    218 bool gitrepo_has_blob(git_repository* repo, const char* file) {
    219   git_object* obj = NULL;
    220   if (git_revparse_single(&obj, repo, file)) {
    221     return false;
    222   }
    223   bool has_blob = git_object_type(obj) == GIT_OBJECT_BLOB;
    224   git_object_free(obj);
    225   return has_blob;
    226 }
    227 
    228 const char* gitrepo_first_matching_file(git_repository* repo,
    229                                         const char** files,
    230                                         size_t files_len) {
    231   for (size_t i = 0; i < files_len; i++) {
    232     const char* filename = files[i];
    233     if (gitrepo_has_blob(repo, filename)) {
    234       return filename + strlen("HEAD:");
    235     }
    236   }
    237   return "";
    238 }
    239 
    240 void gitrepo_load_metadata(GitRepo* repo, const char* path) {
    241   char* repo_path = realpath(path, NULL);
    242   if (!repo_path) {
    243     err(1, "realpath");
    244   }
    245 
    246   char* git_path_maybe = path_concat(repo_path, ".git");
    247   char* git_path = NULL;
    248   if (access(git_path_maybe, F_OK) != 0) {
    249     git_path = estrdup(repo_path);
    250   } else {
    251     git_path = estrdup(git_path_maybe);
    252   }
    253   free(git_path_maybe);
    254 
    255   char* owner_path = path_concat(git_path, "owner");
    256   char* desc_path = path_concat(git_path, "description");
    257   char* url_path = path_concat(git_path, "url");
    258 
    259   repo->name = gitrepo_name_from_path(repo_path);
    260   repo->short_name = gitrepo_shortname_from_name(repo->name);
    261   repo->owner = first_line_of_file(owner_path);
    262   repo->description = first_line_of_file(desc_path);
    263   repo->clone_url = first_line_of_file(url_path);
    264   repo->submodules =
    265       gitrepo_has_blob(repo->repo, "HEAD:.gitmodules") ? ".gitmodules" : "";
    266   repo->readme = gitrepo_first_matching_file(repo->repo, kReadmes, kReadmesLen);
    267   repo->license =
    268       gitrepo_first_matching_file(repo->repo, kLicenses, kLicensesLen);
    269 
    270   free(repo_path);
    271   free(git_path);
    272   free(owner_path);
    273   free(desc_path);
    274   free(url_path);
    275 }
    276 
    277 GitRepo* gitrepo_create(const char* path) {
    278   GitRepo* repo = ecalloc(1, sizeof(GitRepo));
    279   git_repository* git_repo = NULL;
    280   git_repository_open_flag_t kRepoOpenFlags = GIT_REPOSITORY_OPEN_NO_SEARCH;
    281   if (git_repository_open_ext(&git_repo, path, kRepoOpenFlags, NULL)) {
    282     errx(1, "invalid git repository: %s", path);
    283   }
    284   repo->repo = git_repo;
    285 
    286   gitrepo_load_metadata(repo, path);
    287   return repo;
    288 }
    289 
    290 void gitrepo_free(GitRepo* repo) {
    291   if (!repo) {
    292     return;
    293   }
    294   free(repo->name);
    295   repo->name = NULL;
    296   free(repo->short_name);
    297   repo->short_name = NULL;
    298   free(repo->owner);
    299   repo->owner = NULL;
    300   free(repo->description);
    301   repo->description = NULL;
    302   free(repo->clone_url);
    303   repo->clone_url = NULL;
    304   git_repository_free(repo->repo);
    305   repo->repo = NULL;
    306   free(repo);
    307 }
    308 
    309 const char* gitrepo_name(const GitRepo* repo) {
    310   return repo->name;
    311 }
    312 
    313 const char* gitrepo_short_name(const GitRepo* repo) {
    314   return repo->short_name;
    315 }
    316 
    317 const char* gitrepo_owner(const GitRepo* repo) {
    318   return repo->owner;
    319 }
    320 
    321 const char* gitrepo_description(const GitRepo* repo) {
    322   return repo->description;
    323 }
    324 
    325 const char* gitrepo_clone_url(const GitRepo* repo) {
    326   return repo->clone_url;
    327 }
    328 
    329 const char* gitrepo_submodules(const GitRepo* repo) {
    330   return repo->submodules;
    331 }
    332 
    333 const char* gitrepo_readme(const GitRepo* repo) {
    334   return repo->readme;
    335 }
    336 
    337 const char* gitrepo_license(const GitRepo* repo) {
    338   return repo->license;
    339 }
    340 
    341 void gitrepo_for_commit(GitRepo* repo,
    342                         const char* spec,
    343                         CommitCallback cb,
    344                         void* user_data) {
    345   const git_oid* start_oid = oid_for_spec(repo->repo, spec);
    346   if (start_oid == NULL) {
    347     return;
    348   }
    349 
    350   git_revwalk* revwalk = NULL;
    351   if (git_revwalk_new(&revwalk, repo->repo) != 0) {
    352     errx(1, "git_revwalk_new");
    353   }
    354   git_revwalk_push(revwalk, start_oid);
    355 
    356   git_oid current;
    357   if (!git_revwalk_next(&current, revwalk)) {
    358     GitCommit* commit = gitcommit_create(&current, repo->repo);
    359     cb(commit, user_data);
    360     gitcommit_free(commit);
    361   }
    362   git_revwalk_free(revwalk);
    363 }
    364 
    365 void gitrepo_for_each_commit(GitRepo* repo,
    366                              CommitCallback cb,
    367                              void* user_data) {
    368   const git_oid* start_oid = oid_for_spec(repo->repo, "HEAD");
    369 
    370   git_revwalk* revwalk = NULL;
    371   if (git_revwalk_new(&revwalk, repo->repo) != 0) {
    372     errx(1, "git_revwalk_new");
    373   }
    374   git_revwalk_push(revwalk, start_oid);
    375 
    376   git_oid current;
    377   while (!git_revwalk_next(&current, revwalk)) {
    378     GitCommit* commit = gitcommit_create(&current, repo->repo);
    379     cb(commit, user_data);
    380     gitcommit_free(commit);
    381   }
    382   git_revwalk_free(revwalk);
    383 }
    384 
    385 void gitrepo_for_each_reference(GitRepo* repo,
    386                                 ReferenceCallback cb,
    387                                 void* user_data) {
    388   git_reference_iterator* it = NULL;
    389   if (git_reference_iterator_new(&it, repo->repo) != 0) {
    390     errx(1, "git_reference_iterator_new");
    391   }
    392 
    393   GitReference** repos = NULL;
    394   size_t repos_len = 0;
    395   git_reference* current = NULL;
    396   while (!git_reference_next(&current, it)) {
    397     if (!git_reference_is_branch(current) && !git_reference_is_tag(current)) {
    398       git_reference_free(current);
    399       continue;
    400     }
    401     // Hand ownership of current to GitReference.
    402     GitReference* ref = gitreference_create(repo->repo, current);
    403     repos = reallocarray(repos, repos_len + 1, sizeof(GitReference*));
    404     if (!repos) {
    405       err(1, "reallocarray");
    406     }
    407     repos[repos_len++] = ref;
    408   }
    409   git_reference_iterator_free(it);
    410   qsort(repos, repos_len, sizeof(GitReference*), gitreference_compare);
    411 
    412   for (size_t i = 0; i < repos_len; i++) {
    413     cb(repos[i], user_data);
    414     gitreference_free(repos[i]);
    415     repos[i] = NULL;
    416   }
    417   free(repos);
    418 }
    419 
    420 bool gitrepo_walk_tree_files(git_repository* repo,
    421                              git_tree* tree,
    422                              const char* path,
    423                              FileCallback cb,
    424                              void* user_data) {
    425   for (size_t i = 0; i < git_tree_entrycount(tree); i++) {
    426     const git_tree_entry* entry = git_tree_entry_byindex(tree, i);
    427     if (!entry) {
    428       return false;
    429     }
    430 
    431     const char* entryname = git_tree_entry_name(entry);
    432     if (!entryname) {
    433       return false;
    434     }
    435 
    436     char* entrypath;
    437     if (path[0] == '\0') {
    438       entrypath = estrdup(entryname);
    439     } else {
    440       entrypath = path_concat(path, entryname);
    441     }
    442 
    443     git_object* obj = NULL;
    444     if (git_tree_entry_to_object(&obj, repo, entry) != 0) {
    445       char oid_str[GIT_OID_SHA1_HEXSIZE + 1];
    446       git_oid_tostr(oid_str, sizeof(oid_str), git_tree_entry_id(entry));
    447       GitFile* fileinfo =
    448           gitfile_create(kFileTypeSubmodule, "m---------", entrypath,
    449                          ".gitmodules", oid_str, -1, -1, "");
    450       cb(fileinfo, user_data);
    451       gitfile_free(fileinfo);
    452     } else {
    453       switch (git_object_type(obj)) {
    454         case GIT_OBJECT_BLOB:
    455           break;
    456         case GIT_OBJECT_TREE: {
    457           /* NOTE: recurses */
    458           if (!gitrepo_walk_tree_files(repo, (git_tree*)obj, entrypath, cb,
    459                                        user_data)) {
    460             git_object_free(obj);
    461             free(entrypath);
    462             return false;
    463           }
    464           git_object_free(obj);
    465           free(entrypath);
    466           continue;
    467         }
    468         default:
    469           git_object_free(obj);
    470           free(entrypath);
    471           continue;
    472       }
    473 
    474       git_blob* blob = (git_blob*)obj;
    475       ssize_t size_bytes = git_blob_rawsize(blob);
    476       ssize_t size_lines = -1;
    477       const char* content = "";
    478 
    479       if (size_bytes > kMaxFileSizeBytes) {
    480         size_lines = -2; /* oversized file */
    481       } else if (!git_blob_is_binary(blob)) {
    482         content = (const char*)git_blob_rawcontent(blob);
    483         size_lines = string_count_lines(content, size_bytes);
    484       }
    485 
    486       char* filemode = format_filemode(git_tree_entry_filemode(entry));
    487       GitFile* fileinfo =
    488           gitfile_create(kFileTypeFile, filemode, entrypath, entrypath, "",
    489                          size_bytes, size_lines, content);
    490       cb(fileinfo, user_data);
    491       gitfile_free(fileinfo);
    492       git_object_free(obj);
    493       free(filemode);
    494     }
    495     free(entrypath);
    496   }
    497   return true;
    498 }
    499 
    500 void gitrepo_for_each_file(GitRepo* repo, FileCallback cb, void* user_data) {
    501   git_commit* commit = NULL;
    502   const git_oid* id = oid_for_spec(repo->repo, "HEAD");
    503   if (git_commit_lookup(&commit, repo->repo, id) != 0) {
    504     return;
    505   }
    506   git_tree* tree = NULL;
    507   if (git_commit_tree(&tree, commit) != 0) {
    508     git_commit_free(commit);
    509     return;
    510   }
    511   git_commit_free(commit);
    512 
    513   if (!gitrepo_walk_tree_files(repo->repo, tree, "", cb, user_data)) {
    514     git_tree_free(tree);
    515     return;
    516   }
    517   git_tree_free(tree);
    518 }