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