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