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