gitout

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

repo.c (13683B)


      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 = gitrepo_has_blob(repo->repo, "HEAD:.gitmodules")
    263                               ? ".gitmodules"
    264                               : "";
    265   repo->readme =
    266       gitrepo_first_matching_file(repo->repo, kReadmes, kReadmesLen);
    267   repo->license =
    268       gitrepo_first_matching_file(repo->repo, kLicenses, kLicensesLen);
    269   return repo;
    270 }
    271 
    272 void gitrepo_free(GitRepo* repo) {
    273   if (!repo) {
    274     return;
    275   }
    276   free(repo->name);
    277   repo->name = NULL;
    278   free(repo->short_name);
    279   repo->short_name = NULL;
    280   free(repo->owner);
    281   repo->owner = NULL;
    282   free(repo->description);
    283   repo->description = NULL;
    284   free(repo->clone_url);
    285   repo->clone_url = NULL;
    286   git_repository_free(repo->repo);
    287   repo->repo = NULL;
    288   free(repo);
    289 }
    290 
    291 const char* gitrepo_name(const GitRepo* repo) {
    292   return repo->name;
    293 }
    294 
    295 const char* gitrepo_short_name(const GitRepo* repo) {
    296   return repo->short_name;
    297 }
    298 
    299 const char* gitrepo_owner(const GitRepo* repo) {
    300   return repo->owner;
    301 }
    302 
    303 const char* gitrepo_description(const GitRepo* repo) {
    304   return repo->description;
    305 }
    306 
    307 const char* gitrepo_clone_url(const GitRepo* repo) {
    308   return repo->clone_url;
    309 }
    310 
    311 const char* gitrepo_submodules(const GitRepo* repo) {
    312   return repo->submodules;
    313 }
    314 
    315 const char* gitrepo_readme(const GitRepo* repo) {
    316   return repo->readme;
    317 }
    318 
    319 const char* gitrepo_license(const GitRepo* repo) {
    320   return repo->license;
    321 }
    322 
    323 void gitrepo_for_commit(GitRepo* repo,
    324                         const char* spec,
    325                         CommitCallback cb,
    326                         void* user_data) {
    327   const git_oid* start_oid = oid_for_spec(repo->repo, spec);
    328   if (start_oid == NULL) {
    329     return;
    330   }
    331 
    332   git_revwalk* revwalk = NULL;
    333   if (git_revwalk_new(&revwalk, repo->repo) != 0) {
    334     errx(1, "git_revwalk_new");
    335   }
    336   git_revwalk_push(revwalk, start_oid);
    337 
    338   git_oid current;
    339   if (!git_revwalk_next(&current, revwalk)) {
    340     GitCommit* commit = gitcommit_create(&current, repo->repo);
    341     cb(commit, user_data);
    342     gitcommit_free(commit);
    343   }
    344   git_revwalk_free(revwalk);
    345 }
    346 
    347 void gitrepo_for_each_commit(GitRepo* repo,
    348                              CommitCallback cb,
    349                              void* user_data) {
    350   const git_oid* start_oid = oid_for_spec(repo->repo, "HEAD");
    351 
    352   git_revwalk* revwalk = NULL;
    353   if (git_revwalk_new(&revwalk, repo->repo) != 0) {
    354     errx(1, "git_revwalk_new");
    355   }
    356   git_revwalk_push(revwalk, start_oid);
    357 
    358   git_oid current;
    359   while (!git_revwalk_next(&current, revwalk)) {
    360     GitCommit* commit = gitcommit_create(&current, repo->repo);
    361     cb(commit, user_data);
    362     gitcommit_free(commit);
    363   }
    364   git_revwalk_free(revwalk);
    365 }
    366 
    367 void gitrepo_for_each_reference(GitRepo* repo,
    368                                 ReferenceCallback cb,
    369                                 void* user_data) {
    370   git_reference_iterator* it = NULL;
    371   if (git_reference_iterator_new(&it, repo->repo) != 0) {
    372     errx(1, "git_reference_iterator_new");
    373   }
    374 
    375   GitReference** repos = NULL;
    376   size_t repos_len = 0;
    377   git_reference* current = NULL;
    378   while (!git_reference_next(&current, it)) {
    379     if (!git_reference_is_branch(current) && !git_reference_is_tag(current)) {
    380       git_reference_free(current);
    381       continue;
    382     }
    383     // Hand ownership of current to GitReference.
    384     GitReference* ref = gitreference_create(repo->repo, current);
    385     repos = reallocarray(repos, repos_len + 1, sizeof(GitReference*));
    386     if (!repos) {
    387       err(1, "reallocarray");
    388     }
    389     repos[repos_len++] = ref;
    390   }
    391   git_reference_iterator_free(it);
    392   qsort(repos, repos_len, sizeof(GitReference*), gitreference_compare);
    393 
    394   for (size_t i = 0; i < repos_len; i++) {
    395     cb(repos[i], user_data);
    396     gitreference_free(repos[i]);
    397     repos[i] = NULL;
    398   }
    399   free(repos);
    400 }
    401 
    402 bool gitrepo_walk_tree_files(git_repository* repo,
    403                              git_tree* tree,
    404                              const char* path,
    405                              FileCallback cb,
    406                              void* user_data) {
    407   for (size_t i = 0; i < git_tree_entrycount(tree); i++) {
    408     const git_tree_entry* entry = git_tree_entry_byindex(tree, i);
    409     if (!entry) {
    410       return false;
    411     }
    412 
    413     const char* entryname = git_tree_entry_name(entry);
    414     if (!entryname) {
    415       return false;
    416     }
    417 
    418     char entrypath[PATH_MAX];
    419     if (path[0] == '\0') {
    420       estrlcpy(entrypath, entryname, sizeof(entrypath));
    421     } else {
    422       char full_path[PATH_MAX];
    423       path_concat(full_path, sizeof(full_path), path, entryname);
    424       estrlcpy(entrypath, full_path, sizeof(entrypath));
    425     }
    426 
    427     git_object* obj = NULL;
    428     if (git_tree_entry_to_object(&obj, repo, entry) != 0) {
    429       char oid_str[GIT_OID_SHA1_HEXSIZE + 1];
    430       git_oid_tostr(oid_str, sizeof(oid_str), git_tree_entry_id(entry));
    431       GitFile* fileinfo =
    432           gitfile_create(kFileTypeSubmodule, "m---------", entrypath,
    433                          ".gitmodules", oid_str, -1, -1, "");
    434       cb(fileinfo, user_data);
    435       gitfile_free(fileinfo);
    436     } else {
    437       switch (git_object_type(obj)) {
    438         case GIT_OBJECT_BLOB:
    439           break;
    440         case GIT_OBJECT_TREE: {
    441           /* NOTE: recurses */
    442           if (!gitrepo_walk_tree_files(repo, (git_tree*)obj, entrypath, cb,
    443                                        user_data)) {
    444             git_object_free(obj);
    445             return false;
    446           }
    447           git_object_free(obj);
    448           continue;
    449         }
    450         default:
    451           git_object_free(obj);
    452           continue;
    453       }
    454 
    455       git_blob* blob = (git_blob*)obj;
    456       ssize_t size_bytes = git_blob_rawsize(blob);
    457       ssize_t size_lines = -1;
    458       const char* content = "";
    459 
    460       if (size_bytes > kMaxFileSizeBytes) {
    461         size_lines = -2;  /* oversized file */
    462       } else if (!git_blob_is_binary(blob)) {
    463         content = (const char*)git_blob_rawcontent(blob);
    464         size_lines = string_count_lines(content, size_bytes);
    465       }
    466 
    467       char* filemode = format_filemode(git_tree_entry_filemode(entry));
    468       GitFile* fileinfo =
    469           gitfile_create(kFileTypeFile, filemode, entrypath, entrypath, "",
    470                          size_bytes, size_lines, content);
    471       cb(fileinfo, user_data);
    472       gitfile_free(fileinfo);
    473       git_object_free(obj);
    474       free(filemode);
    475     }
    476   }
    477   return true;
    478 }
    479 
    480 void gitrepo_for_each_file(GitRepo* repo,
    481                            FileCallback cb,
    482                            void* user_data) {
    483   git_commit* commit = NULL;
    484   const git_oid* id = oid_for_spec(repo->repo, "HEAD");
    485   if (git_commit_lookup(&commit, repo->repo, id) != 0) {
    486     return;
    487   }
    488   git_tree* tree = NULL;
    489   if (git_commit_tree(&tree, commit) != 0) {
    490     git_commit_free(commit);
    491     return;
    492   }
    493   git_commit_free(commit);
    494 
    495   if (!gitrepo_walk_tree_files(repo->repo, tree, "", cb, user_data)) {
    496     git_tree_free(tree);
    497     return;
    498   }
    499   git_tree_free(tree);
    500 }