repo.c (7785B)
1 #include "git/repo.h" 2 3 #include <assert.h> 4 #include <err.h> 5 #include <git2/blob.h> 6 #include <git2/commit.h> 7 #include <git2/object.h> 8 #include <git2/oid.h> 9 #include <git2/refs.h> 10 #include <git2/repository.h> 11 #include <git2/revparse.h> 12 #include <git2/revwalk.h> 13 #include <git2/tree.h> 14 #include <git2/types.h> 15 #include <libgen.h> 16 #include <limits.h> 17 #include <stdbool.h> 18 #include <stdio.h> 19 #include <stdlib.h> 20 #include <string.h> 21 #include <sys/stat.h> 22 #include <sys/types.h> 23 #include <sys/unistd.h> 24 #include <unistd.h> 25 26 #include "git/internal.h" 27 #include "third_party/openbsd/reallocarray.h" 28 29 /* Local const data */ 30 static const char* kLicenses[] = {"HEAD:LICENSE", "HEAD:LICENSE.md", 31 "HEAD:COPYING"}; 32 static const size_t kLicensesLen = sizeof(kLicenses) / sizeof(char*); 33 static const char* kReadmes[] = {"HEAD:README", "HEAD:README.md"}; 34 static const size_t kReadmesLen = sizeof(kReadmes) / sizeof(char*); 35 36 /* Utilities */ 37 static bool string_ends_with(const char* str, const char* suffix); 38 static char* first_line_of_file(const FileSystem* fs, const char* path); 39 40 /* GitRepo utilities. */ 41 static char* gitrepo_name_from_path(const char* repo_path); 42 static char* gitrepo_shortname_from_name(const char* name); 43 static bool gitrepo_has_blob(git_repository* repo, const char* file); 44 static const char* gitrepo_first_matching_file(git_repository* repo, 45 const char** files, 46 size_t files_len); 47 void gitrepo_load_filesystem_metadata(GitRepo* repo, 48 const FileSystem* fs, 49 const char* path); 50 static void gitrepo_load_git_metadata(GitRepo* repo, git_repository* git_repo); 51 52 static bool string_ends_with(const char* str, const char* suffix) { 53 if (!str || !suffix) { 54 return false; 55 } 56 size_t str_len = strlen(str); 57 size_t suffix_len = strlen(suffix); 58 if (str_len < suffix_len) { 59 return false; 60 } 61 return strncmp(str + str_len - suffix_len, suffix, suffix_len) == 0; 62 } 63 64 static char* first_line_of_file(const FileSystem* fs, const char* path) { 65 assert(fs != NULL); 66 assert(path != NULL); 67 FILE* f = fs->fopen(path, "r"); 68 if (!f) { 69 return estrdup(""); 70 } 71 char* buf = NULL; 72 size_t buf_size = 0; 73 ssize_t len = getline(&buf, &buf_size, f); 74 fs->fclose(f); 75 if (len == -1) { 76 free(buf); 77 return estrdup(""); 78 } 79 // Truncate at the first newline or carriage return. 80 buf[strcspn(buf, "\r\n")] = '\0'; 81 return buf; 82 } 83 84 static char* gitrepo_name_from_path(const char* repo_path) { 85 assert(repo_path != NULL); 86 char* path_copy = estrdup(repo_path); 87 const char* filename = basename(path_copy); 88 if (!filename) { 89 err(1, "basename"); 90 } 91 char* result = estrdup(filename); 92 free(path_copy); 93 return result; 94 } 95 96 static char* gitrepo_shortname_from_name(const char* name) { 97 assert(name != NULL); 98 char* short_name = estrdup(name); 99 if (string_ends_with(short_name, ".git")) { 100 size_t short_name_len = strlen(short_name); 101 size_t suffix_len = strlen(".git"); 102 short_name[short_name_len - suffix_len] = '\0'; 103 } 104 return short_name; 105 } 106 107 static bool gitrepo_has_blob(git_repository* repo, const char* file) { 108 assert(repo != NULL); 109 assert(file != NULL); 110 git_object* obj = NULL; 111 if (git_revparse_single(&obj, repo, file)) { 112 return false; 113 } 114 bool has_blob = git_object_type(obj) == GIT_OBJECT_BLOB; 115 git_object_free(obj); 116 return has_blob; 117 } 118 119 static const char* gitrepo_first_matching_file(git_repository* repo, 120 const char** files, 121 size_t files_len) { 122 assert(repo != NULL); 123 assert(files != NULL); 124 for (size_t i = 0; i < files_len; i++) { 125 const char* filename = files[i]; 126 if (gitrepo_has_blob(repo, filename)) { 127 return filename + strlen("HEAD:"); 128 } 129 } 130 return ""; 131 } 132 133 void gitrepo_load_filesystem_metadata(GitRepo* repo, 134 const FileSystem* fs, 135 const char* path) { 136 assert(repo != NULL); 137 assert(fs != NULL); 138 assert(path != NULL); 139 char* repo_path = fs->realpath(path, NULL); 140 if (!repo_path) { 141 err(1, "realpath: %s", path); 142 } 143 144 char* git_path_maybe = path_concat(repo_path, ".git"); 145 char* git_path = NULL; 146 if (fs->access(git_path_maybe, F_OK) != 0) { 147 git_path = estrdup(repo_path); 148 } else { 149 git_path = estrdup(git_path_maybe); 150 } 151 free(git_path_maybe); 152 153 repo->name = gitrepo_name_from_path(repo_path); 154 repo->short_name = gitrepo_shortname_from_name(repo->name); 155 156 static const char* metadata_files[] = {"owner", "description", "url"}; 157 char** target_fields[] = {&repo->owner, &repo->description, &repo->clone_url}; 158 159 for (size_t i = 0; i < 3; i++) { 160 char* full_path = path_concat(git_path, metadata_files[i]); 161 *target_fields[i] = first_line_of_file(fs, full_path); 162 free(full_path); 163 } 164 165 free(repo_path); 166 free(git_path); 167 } 168 169 static void gitrepo_add_special_file(GitRepo* repo, 170 const char* label, 171 const char* path) { 172 if (!path || path[0] == '\0') { 173 return; 174 } 175 repo->special_files = 176 reallocarray(repo->special_files, repo->special_files_len + 1, 177 sizeof(RepoSpecialFile)); 178 if (!repo->special_files) { 179 err(1, "reallocarray"); 180 } 181 repo->special_files[repo->special_files_len].label = estrdup(label); 182 repo->special_files[repo->special_files_len].path = estrdup(path); 183 repo->special_files_len++; 184 } 185 186 static void gitrepo_load_git_metadata(GitRepo* repo, git_repository* git_repo) { 187 assert(repo != NULL); 188 assert(git_repo != NULL); 189 190 if (gitrepo_has_blob(git_repo, "HEAD:.gitmodules")) { 191 gitrepo_add_special_file(repo, "Submodules", ".gitmodules"); 192 } 193 gitrepo_add_special_file( 194 repo, "README", 195 gitrepo_first_matching_file(git_repo, kReadmes, kReadmesLen)); 196 gitrepo_add_special_file( 197 repo, "LICENSE", 198 gitrepo_first_matching_file(git_repo, kLicenses, kLicensesLen)); 199 200 repo->last_commit_time = 0; 201 git_object* obj = NULL; 202 if (git_revparse_single(&obj, git_repo, "HEAD") == 0) { 203 git_commit* commit = NULL; 204 if (git_commit_lookup(&commit, git_repo, git_object_id(obj)) == 0) { 205 repo->last_commit_time = git_commit_author(commit)->when.time; 206 git_commit_free(commit); 207 } 208 git_object_free(obj); 209 } 210 } 211 212 git_repository* gitrepo_open_repository(const char* path) { 213 assert(path != NULL); 214 git_repository* repo = NULL; 215 git_repository_open_flag_t kRepoOpenFlags = GIT_REPOSITORY_OPEN_NO_SEARCH; 216 if (git_repository_open_ext(&repo, path, kRepoOpenFlags, NULL)) { 217 errx(1, "invalid git repository: %s", path); 218 } 219 return repo; 220 } 221 222 GitRepo* gitrepo_create_from_repository(const FileSystem* fs, 223 const char* path, 224 void* git_repo) { 225 assert(fs != NULL); 226 assert(path != NULL); 227 assert(git_repo != NULL); 228 GitRepo* repo = ecalloc(1, sizeof(GitRepo)); 229 gitrepo_load_filesystem_metadata(repo, fs, path); 230 gitrepo_load_git_metadata(repo, (git_repository*)git_repo); 231 return repo; 232 } 233 234 void gitrepo_free(GitRepo* repo) { 235 if (!repo) { 236 return; 237 } 238 free(repo->name); 239 repo->name = NULL; 240 free(repo->short_name); 241 repo->short_name = NULL; 242 free(repo->owner); 243 repo->owner = NULL; 244 free(repo->description); 245 repo->description = NULL; 246 free(repo->clone_url); 247 repo->clone_url = NULL; 248 249 for (size_t i = 0; i < repo->special_files_len; i++) { 250 free(repo->special_files[i].label); 251 free(repo->special_files[i].path); 252 } 253 free(repo->special_files); 254 repo->special_files = NULL; 255 repo->special_files_len = 0; 256 257 free(repo); 258 }