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