git.c (11486B)
1 #include "git.h" 2 3 #include <assert.h> 4 #include <err.h> 5 #include <git2.h> 6 #include <stdbool.h> 7 #include <stdlib.h> 8 #include <string.h> 9 #include <sys/stat.h> 10 11 #include "git/internal.h" 12 #include "third_party/openbsd/reallocarray.h" 13 14 /* Global const data. */ 15 static_assert(GOUT_OID_MAX_SIZE == GIT_OID_MAX_HEXSIZE + 1, 16 "GOUT_OID_MAX_SIZE must match libgit2's OID size"); 17 18 /* Maximum file size to load into memory, in bytes. */ 19 static const ssize_t kMaxFileSizeBytes = 16 * 1024 * 1024; 20 21 /* Forward declarations for VTable functions. */ 22 static void libgit2_initialize(Git* git); 23 static void libgit2_shutdown(Git* git); 24 static void libgit2_for_repo(Git* git, 25 const char* path, 26 RepoCallback cb, 27 void* user_data); 28 static void libgit2_for_each_commit(const Git* git, 29 CommitCallback cb, 30 void* user_data); 31 static void libgit2_for_commit(const Git* git, 32 const char* spec, 33 CommitCallback cb, 34 void* user_data); 35 static void libgit2_for_each_reference(const Git* git, 36 ReferenceCallback cb, 37 void* user_data); 38 static void libgit2_for_each_file(const Git* git, 39 FileCallback cb, 40 void* user_data); 41 42 /* Internal libgit2 utilities. */ 43 static int oid_for_spec(git_repository* repo, const char* spec, git_oid* oid); 44 static size_t string_count_lines(const char* str, ssize_t str_len); 45 static bool gitrepo_walk_tree_files(git_repository* repo, 46 git_tree* tree, 47 const char* path, 48 FileCallback cb, 49 void* user_data); 50 51 static void libgit2_initialize(Git* git) { 52 assert(git != NULL); 53 (void)git; 54 /* do not search outside the git repository: 55 GIT_CONFIG_LEVEL_APP is the highest level currently */ 56 git_libgit2_init(); 57 for (int i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) { 58 git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, ""); 59 } 60 /* do not require the git repository to be owned by the current user */ 61 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); 62 } 63 64 static void libgit2_shutdown(Git* git) { 65 assert(git != NULL); 66 (void)git; 67 git_libgit2_shutdown(); 68 } 69 70 Git* gout_git_create(const FileSystem* fs) { 71 assert(fs != NULL); 72 Git* git = ecalloc(1, sizeof(Git)); 73 git->impl = NULL; 74 git->fs = fs; 75 git->repo = NULL; 76 77 git->initialize = libgit2_initialize; 78 git->shutdown = libgit2_shutdown; 79 git->for_repo = libgit2_for_repo; 80 git->for_each_commit = libgit2_for_each_commit; 81 git->for_commit = libgit2_for_commit; 82 git->for_each_reference = libgit2_for_each_reference; 83 git->for_each_file = libgit2_for_each_file; 84 85 return git; 86 } 87 88 void gout_git_free(Git* git) { 89 if (!git) { 90 return; 91 } 92 if (git->impl) { 93 git_repository_free((git_repository*)git->impl); 94 } 95 if (git->repo) { 96 gitrepo_free(git->repo); 97 } 98 free(git); 99 } 100 101 /* VTable Implementation */ 102 103 static void libgit2_for_repo(Git* git, 104 const char* path, 105 RepoCallback cb, 106 void* user_data) { 107 assert(git != NULL); 108 assert(path != NULL); 109 assert(cb != NULL); 110 git->impl = gitrepo_open_repository(path); 111 git->repo = gitrepo_create_from_repository(git->fs, path, git->impl); 112 cb(git, user_data); 113 gitrepo_free(git->repo); 114 git_repository_free((git_repository*)git->impl); 115 } 116 117 static void libgit2_for_each_commit(const Git* git, 118 CommitCallback cb, 119 void* user_data) { 120 assert(git != NULL); 121 assert(cb != NULL); 122 git_repository* repo = (git_repository*)git->impl; 123 git_oid start_oid; 124 if (oid_for_spec(repo, "HEAD", &start_oid) != 0) { 125 return; 126 } 127 128 git_revwalk* revwalk = NULL; 129 if (git_revwalk_new(&revwalk, repo) != 0) { 130 errx(1, "git_revwalk_new"); 131 } 132 git_revwalk_push(revwalk, &start_oid); 133 134 git_oid current; 135 while (!git_revwalk_next(¤t, revwalk)) { 136 GitCommit* commit = gitcommit_create(¤t, repo); 137 cb(commit, user_data); 138 gitcommit_free(commit); 139 } 140 git_revwalk_free(revwalk); 141 } 142 143 static void libgit2_for_commit(const Git* git, 144 const char* spec, 145 CommitCallback cb, 146 void* user_data) { 147 assert(git != NULL); 148 assert(spec != NULL); 149 assert(cb != NULL); 150 git_repository* repo = (git_repository*)git->impl; 151 git_oid start_oid; 152 if (oid_for_spec(repo, spec, &start_oid) != 0) { 153 return; 154 } 155 156 git_revwalk* revwalk = NULL; 157 if (git_revwalk_new(&revwalk, repo) != 0) { 158 errx(1, "git_revwalk_new"); 159 } 160 git_revwalk_push(revwalk, &start_oid); 161 162 git_oid current; 163 if (!git_revwalk_next(¤t, revwalk)) { 164 GitCommit* commit = gitcommit_create(¤t, repo); 165 cb(commit, user_data); 166 gitcommit_free(commit); 167 } 168 git_revwalk_free(revwalk); 169 } 170 171 static void libgit2_for_each_reference(const Git* git, 172 ReferenceCallback cb, 173 void* user_data) { 174 assert(git != NULL); 175 assert(cb != NULL); 176 git_repository* repo = (git_repository*)git->impl; 177 git_reference_iterator* it = NULL; 178 if (git_reference_iterator_new(&it, repo) != 0) { 179 errx(1, "git_reference_iterator_new"); 180 } 181 182 GitReference** refs = NULL; 183 size_t refs_len = 0; 184 git_reference* current = NULL; 185 while (!git_reference_next(¤t, it)) { 186 if (!git_reference_is_branch(current) && !git_reference_is_tag(current)) { 187 git_reference_free(current); 188 continue; 189 } 190 char* refname = estrdup(git_reference_shorthand(current)); 191 GitReference* ref = gitreference_create(repo, current); 192 if (!ref) { 193 warnx("skipping reference with missing target: %s", refname); 194 free(refname); 195 continue; 196 } 197 free(refname); 198 refs = reallocarray(refs, refs_len + 1, sizeof(GitReference*)); 199 if (!refs) { 200 err(1, "reallocarray"); 201 } 202 refs[refs_len++] = ref; 203 } 204 git_reference_iterator_free(it); 205 if (refs_len > 0) { 206 qsort(refs, refs_len, sizeof(GitReference*), gitreference_compare); 207 } 208 209 for (size_t i = 0; i < refs_len; i++) { 210 cb(refs[i], user_data); 211 gitreference_free(refs[i]); 212 } 213 free(refs); 214 } 215 216 static void libgit2_for_each_file(const Git* git, 217 FileCallback cb, 218 void* user_data) { 219 assert(git != NULL); 220 assert(cb != NULL); 221 git_repository* repo = (git_repository*)git->impl; 222 git_commit* commit = NULL; 223 git_oid id; 224 if (oid_for_spec(repo, "HEAD", &id) != 0 || 225 git_commit_lookup(&commit, repo, &id) != 0) { 226 return; 227 } 228 git_tree* tree = NULL; 229 if (git_commit_tree(&tree, commit) != 0) { 230 git_commit_free(commit); 231 return; 232 } 233 git_commit_free(commit); 234 235 if (!gitrepo_walk_tree_files(repo, tree, "", cb, user_data)) { 236 git_tree_free(tree); 237 return; 238 } 239 git_tree_free(tree); 240 } 241 242 /* Utilities */ 243 244 static int oid_for_spec(git_repository* repo, const char* spec, git_oid* oid) { 245 assert(repo != NULL); 246 assert(spec != NULL); 247 assert(oid != NULL); 248 git_object* obj = NULL; 249 if (git_revparse_single(&obj, repo, spec)) { 250 return -1; 251 } 252 git_oid_cpy(oid, git_object_id(obj)); 253 git_object_free(obj); 254 return 0; 255 } 256 257 static size_t string_count_lines(const char* str, ssize_t str_len) { 258 if (!str || str_len <= 0) { 259 return 0; 260 } 261 size_t lines = 0; 262 for (ssize_t i = 0; i < str_len; i++) { 263 if (str[i] == '\n') { 264 lines++; 265 } 266 } 267 return str[str_len - 1] == '\n' ? lines : lines + 1; 268 } 269 270 char git_get_filetype(git_filemode_t m) { 271 switch (m & S_IFMT) { 272 case S_IFREG: 273 return '-'; 274 case S_IFBLK: 275 return 'b'; 276 case S_IFCHR: 277 return 'c'; 278 case S_IFDIR: 279 return 'd'; 280 case S_IFIFO: 281 return 'p'; 282 case S_IFLNK: 283 return 'l'; 284 case S_IFSOCK: 285 return 's'; 286 default: 287 return '?'; 288 } 289 } 290 291 char* git_format_filemode(git_filemode_t m) { 292 char* mode = estrdup("----------"); 293 294 mode[0] = git_get_filetype(m); 295 static const char chars[] = "rwxrwxrwx"; 296 static const int masks[] = {S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, 297 S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH}; 298 299 for (size_t i = 0; i < 9; i++) { 300 if (m & masks[i]) { 301 mode[i + 1] = chars[i]; 302 } 303 } 304 305 if (m & S_ISUID) { 306 mode[3] = (mode[3] == 'x') ? 's' : 'S'; 307 } 308 if (m & S_ISGID) { 309 mode[6] = (mode[6] == 'x') ? 's' : 'S'; 310 } 311 if (m & S_ISVTX) { 312 mode[9] = (mode[9] == 'x') ? 't' : 'T'; 313 } 314 315 return mode; 316 } 317 318 static bool gitrepo_walk_tree_files(git_repository* repo, 319 git_tree* tree, 320 const char* path, 321 FileCallback cb, 322 void* user_data) { 323 assert(repo != NULL); 324 assert(tree != NULL); 325 assert(path != NULL); 326 assert(cb != NULL); 327 328 for (size_t i = 0; i < git_tree_entrycount(tree); i++) { 329 const git_tree_entry* entry = git_tree_entry_byindex(tree, i); 330 if (!entry) { 331 return false; 332 } 333 334 const char* entryname = git_tree_entry_name(entry); 335 if (!entryname) { 336 return false; 337 } 338 339 char* entrypath = path_concat(path, entryname); 340 341 git_filemode_t mode = git_tree_entry_filemode(entry); 342 if (mode == GIT_FILEMODE_COMMIT) { 343 char oid_str[GOUT_OID_MAX_SIZE]; 344 git_oid_tostr(oid_str, sizeof(oid_str), git_tree_entry_id(entry)); 345 GitFile* fileinfo = 346 gitfile_create(kFileTypeSubmodule, "m---------", entrypath, 347 ".gitmodules", oid_str, -1, -1, ""); 348 cb(fileinfo, user_data); 349 gitfile_free(fileinfo); 350 free(entrypath); 351 continue; 352 } 353 354 git_object* obj = NULL; 355 if (git_tree_entry_to_object(&obj, repo, entry) != 0) { 356 warnx("failed to get object for tree entry: %s", entrypath); 357 free(entrypath); 358 continue; 359 } 360 361 switch (git_object_type(obj)) { 362 case GIT_OBJECT_BLOB: 363 break; 364 case GIT_OBJECT_TREE: { 365 if (!gitrepo_walk_tree_files(repo, (git_tree*)obj, entrypath, cb, 366 user_data)) { 367 git_object_free(obj); 368 free(entrypath); 369 return false; 370 } 371 git_object_free(obj); 372 free(entrypath); 373 continue; 374 } 375 default: 376 git_object_free(obj); 377 free(entrypath); 378 continue; 379 } 380 381 git_blob* blob = (git_blob*)obj; 382 ssize_t size_bytes = git_blob_rawsize(blob); 383 ssize_t size_lines = -1; 384 const char* content = NULL; 385 386 if (size_bytes > kMaxFileSizeBytes) { 387 size_lines = -2; /* oversized file */ 388 } else if (!git_blob_is_binary(blob)) { 389 content = (const char*)git_blob_rawcontent(blob); 390 size_lines = string_count_lines(content, size_bytes); 391 } 392 393 char* filemode = git_format_filemode(git_tree_entry_filemode(entry)); 394 GitFile* fileinfo = 395 gitfile_create(kFileTypeFile, filemode, entrypath, entrypath, "", 396 size_bytes, size_lines, content); 397 cb(fileinfo, user_data); 398 gitfile_free(fileinfo); 399 git_object_free(obj); 400 free(filemode); 401 free(entrypath); 402 } 403 return true; 404 }