gout

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

repo.c (13568B)


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