repo.c (13568B)
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 = 263 gitrepo_has_blob(repo->repo, "HEAD:.gitmodules") ? ".gitmodules" : ""; 264 repo->readme = gitrepo_first_matching_file(repo->repo, kReadmes, kReadmesLen); 265 repo->license = 266 gitrepo_first_matching_file(repo->repo, kLicenses, kLicensesLen); 267 return repo; 268 } 269 270 void gitrepo_free(GitRepo* repo) { 271 if (!repo) { 272 return; 273 } 274 free(repo->name); 275 repo->name = NULL; 276 free(repo->short_name); 277 repo->short_name = NULL; 278 free(repo->owner); 279 repo->owner = NULL; 280 free(repo->description); 281 repo->description = NULL; 282 free(repo->clone_url); 283 repo->clone_url = NULL; 284 git_repository_free(repo->repo); 285 repo->repo = NULL; 286 free(repo); 287 } 288 289 const char* gitrepo_name(const GitRepo* repo) { 290 return repo->name; 291 } 292 293 const char* gitrepo_short_name(const GitRepo* repo) { 294 return repo->short_name; 295 } 296 297 const char* gitrepo_owner(const GitRepo* repo) { 298 return repo->owner; 299 } 300 301 const char* gitrepo_description(const GitRepo* repo) { 302 return repo->description; 303 } 304 305 const char* gitrepo_clone_url(const GitRepo* repo) { 306 return repo->clone_url; 307 } 308 309 const char* gitrepo_submodules(const GitRepo* repo) { 310 return repo->submodules; 311 } 312 313 const char* gitrepo_readme(const GitRepo* repo) { 314 return repo->readme; 315 } 316 317 const char* gitrepo_license(const GitRepo* repo) { 318 return repo->license; 319 } 320 321 void gitrepo_for_commit(GitRepo* repo, 322 const char* spec, 323 CommitCallback cb, 324 void* user_data) { 325 const git_oid* start_oid = oid_for_spec(repo->repo, spec); 326 if (start_oid == NULL) { 327 return; 328 } 329 330 git_revwalk* revwalk = NULL; 331 if (git_revwalk_new(&revwalk, repo->repo) != 0) { 332 errx(1, "git_revwalk_new"); 333 } 334 git_revwalk_push(revwalk, start_oid); 335 336 git_oid current; 337 if (!git_revwalk_next(¤t, revwalk)) { 338 GitCommit* commit = gitcommit_create(¤t, repo->repo); 339 cb(commit, user_data); 340 gitcommit_free(commit); 341 } 342 git_revwalk_free(revwalk); 343 } 344 345 void gitrepo_for_each_commit(GitRepo* repo, 346 CommitCallback cb, 347 void* user_data) { 348 const git_oid* start_oid = oid_for_spec(repo->repo, "HEAD"); 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 while (!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_reference(GitRepo* repo, 366 ReferenceCallback cb, 367 void* user_data) { 368 git_reference_iterator* it = NULL; 369 if (git_reference_iterator_new(&it, repo->repo) != 0) { 370 errx(1, "git_reference_iterator_new"); 371 } 372 373 GitReference** repos = NULL; 374 size_t repos_len = 0; 375 git_reference* current = NULL; 376 while (!git_reference_next(¤t, it)) { 377 if (!git_reference_is_branch(current) && !git_reference_is_tag(current)) { 378 git_reference_free(current); 379 continue; 380 } 381 // Hand ownership of current to GitReference. 382 GitReference* ref = gitreference_create(repo->repo, current); 383 repos = reallocarray(repos, repos_len + 1, sizeof(GitReference*)); 384 if (!repos) { 385 err(1, "reallocarray"); 386 } 387 repos[repos_len++] = ref; 388 } 389 git_reference_iterator_free(it); 390 qsort(repos, repos_len, sizeof(GitReference*), gitreference_compare); 391 392 for (size_t i = 0; i < repos_len; i++) { 393 cb(repos[i], user_data); 394 gitreference_free(repos[i]); 395 repos[i] = NULL; 396 } 397 free(repos); 398 } 399 400 bool gitrepo_walk_tree_files(git_repository* repo, 401 git_tree* tree, 402 const char* path, 403 FileCallback cb, 404 void* user_data) { 405 for (size_t i = 0; i < git_tree_entrycount(tree); i++) { 406 const git_tree_entry* entry = git_tree_entry_byindex(tree, i); 407 if (!entry) { 408 return false; 409 } 410 411 const char* entryname = git_tree_entry_name(entry); 412 if (!entryname) { 413 return false; 414 } 415 416 char entrypath[PATH_MAX]; 417 if (path[0] == '\0') { 418 estrlcpy(entrypath, entryname, sizeof(entrypath)); 419 } else { 420 char full_path[PATH_MAX]; 421 path_concat(full_path, sizeof(full_path), path, entryname); 422 estrlcpy(entrypath, full_path, sizeof(entrypath)); 423 } 424 425 git_object* obj = NULL; 426 if (git_tree_entry_to_object(&obj, repo, entry) != 0) { 427 char oid_str[GIT_OID_SHA1_HEXSIZE + 1]; 428 git_oid_tostr(oid_str, sizeof(oid_str), git_tree_entry_id(entry)); 429 GitFile* fileinfo = 430 gitfile_create(kFileTypeSubmodule, "m---------", entrypath, 431 ".gitmodules", oid_str, -1, -1, ""); 432 cb(fileinfo, user_data); 433 gitfile_free(fileinfo); 434 } else { 435 switch (git_object_type(obj)) { 436 case GIT_OBJECT_BLOB: 437 break; 438 case GIT_OBJECT_TREE: { 439 /* NOTE: recurses */ 440 if (!gitrepo_walk_tree_files(repo, (git_tree*)obj, entrypath, cb, 441 user_data)) { 442 git_object_free(obj); 443 return false; 444 } 445 git_object_free(obj); 446 continue; 447 } 448 default: 449 git_object_free(obj); 450 continue; 451 } 452 453 git_blob* blob = (git_blob*)obj; 454 ssize_t size_bytes = git_blob_rawsize(blob); 455 ssize_t size_lines = -1; 456 const char* content = ""; 457 458 if (size_bytes > kMaxFileSizeBytes) { 459 size_lines = -2; /* oversized file */ 460 } else if (!git_blob_is_binary(blob)) { 461 content = (const char*)git_blob_rawcontent(blob); 462 size_lines = string_count_lines(content, size_bytes); 463 } 464 465 char* filemode = format_filemode(git_tree_entry_filemode(entry)); 466 GitFile* fileinfo = 467 gitfile_create(kFileTypeFile, filemode, entrypath, entrypath, "", 468 size_bytes, size_lines, content); 469 cb(fileinfo, user_data); 470 gitfile_free(fileinfo); 471 git_object_free(obj); 472 free(filemode); 473 } 474 } 475 return true; 476 } 477 478 void gitrepo_for_each_file(GitRepo* repo, FileCallback cb, void* user_data) { 479 git_commit* commit = NULL; 480 const git_oid* id = oid_for_spec(repo->repo, "HEAD"); 481 if (git_commit_lookup(&commit, repo->repo, id) != 0) { 482 return; 483 } 484 git_tree* tree = NULL; 485 if (git_commit_tree(&tree, commit) != 0) { 486 git_commit_free(commit); 487 return; 488 } 489 git_commit_free(commit); 490 491 if (!gitrepo_walk_tree_files(repo->repo, tree, "", cb, user_data)) { 492 git_tree_free(tree); 493 return; 494 } 495 git_tree_free(tree); 496 }