gitout

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

repo.c (13468B)


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