1 /* 2 * libgit2 "status" example - shows how to use the status APIs 3 * 4 * Written by the libgit2 contributors 5 * 6 * To the extent possible under law, the author(s) have dedicated all copyright 7 * and related and neighboring rights to this software to the public domain 8 * worldwide. This software is distributed without any warranty. 9 * 10 * You should have received a copy of the CC0 Public Domain Dedication along 11 * with this software. If not, see 12 * <https://creativecommons.org/publicdomain/zero/1.0/>. 13 */ 14 /** 15 * This example demonstrates the use of the libgit2 status APIs, 16 * particularly the `libgit2.types.git_status_list` object, to roughly simulate the 17 * output of running `git status`. It serves as a simple example of 18 * using those APIs to get basic status information. 19 * 20 * This does not have: 21 * 22 * - Robust error handling 23 * - Colorized or paginated output formatting 24 * 25 * This does have: 26 * 27 * - Examples of translating command line arguments to the status 28 * options settings to mimic `git status` results. 29 * - A sample status formatter that matches the default "long" format 30 * from `git status` 31 * - A sample status formatter that matches the "short" format 32 * 33 * License: $(LINK2 https://creativecommons.org/publicdomain/zero/1.0/, CC0 1.0 Universal) 34 */ 35 module libgit2.example.status; 36 37 38 private static import core.stdc.stdio; 39 private static import core.stdc.string; 40 private static import libgit2.diff; 41 private static import libgit2.errors; 42 private static import libgit2.example.args; 43 private static import libgit2.example.common; 44 private static import libgit2.refs; 45 private static import libgit2.repository; 46 private static import libgit2.status; 47 private static import libgit2.submodule; 48 private static import libgit2.types; 49 50 public enum 51 { 52 FORMAT_DEFAULT = 0, 53 FORMAT_LONG = 1, 54 FORMAT_SHORT = 2, 55 FORMAT_PORCELAIN = 3, 56 } 57 58 public enum MAX_PATHSPEC = 8; 59 60 extern (C) 61 public struct status_opts 62 { 63 libgit2.status.git_status_options statusopt; 64 const (char)* repodir; 65 char*[.MAX_PATHSPEC] pathspec; 66 int npaths; 67 int format; 68 int zterm; 69 int showbranch; 70 int showsubmod; 71 int repeat; 72 } 73 74 extern (C) 75 nothrow @nogc 76 public int lg2_status(libgit2.types.git_repository* repo, int argc, char** argv) 77 78 in 79 { 80 } 81 82 do 83 { 84 libgit2.types.git_status_list* status; 85 .status_opts o = {libgit2.status.GIT_STATUS_OPTIONS_INIT(), ".".ptr}; 86 87 o.statusopt.show = libgit2.status.git_status_show_t.GIT_STATUS_SHOW_INDEX_AND_WORKDIR; 88 o.statusopt.flags = libgit2.status.git_status_opt_t.GIT_STATUS_OPT_INCLUDE_UNTRACKED | libgit2.status.git_status_opt_t.GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | libgit2.status.git_status_opt_t.GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; 89 90 .parse_opts(&o, argc, argv); 91 92 if (libgit2.repository.git_repository_is_bare(repo)) { 93 libgit2.example.common.fatal("Cannot report status on bare repository", libgit2.repository.git_repository_path(repo)); 94 } 95 96 show_status: 97 if (o.repeat) { 98 core.stdc.stdio.printf("\033[H\033[2J"); 99 } 100 101 /** 102 * Run status on the repository 103 * 104 * We use `libgit2.status.git_status_list_new()` to generate a list of status 105 * information which lets us iterate over it at our 106 * convenience and extract the data we want to show out of 107 * each entry. 108 * 109 * You can use `libgit2.status.git_status_foreach()` or 110 * `libgit2.status.git_status_foreach_ext()` if you'd prefer to execute a 111 * callback for each entry. The latter gives you more control 112 * about what results are presented. 113 */ 114 libgit2.example.common.check_lg2(libgit2.status.git_status_list_new(&status, repo, &o.statusopt), "Could not get status", null); 115 116 if (o.showbranch) { 117 .show_branch(repo, o.format); 118 } 119 120 if (o.showsubmod) { 121 int submod_count = 0; 122 libgit2.example.common.check_lg2(libgit2.submodule.git_submodule_foreach(repo, &.print_submod, &submod_count), "Cannot iterate submodules", o.repodir); 123 } 124 125 if (o.format == .FORMAT_LONG) { 126 .print_long(status); 127 } else { 128 .print_short(repo, status); 129 } 130 131 libgit2.status.git_status_list_free(status); 132 133 if (o.repeat) { 134 libgit2.example.common.sleep(o.repeat); 135 136 goto show_status; 137 } 138 139 return 0; 140 } 141 142 /** 143 * If the user asked for the branch, let's show the short name of the 144 * branch. 145 */ 146 nothrow @nogc 147 private void show_branch(libgit2.types.git_repository* repo, int format) 148 149 in 150 { 151 } 152 153 do 154 { 155 const (char)* branch = null; 156 libgit2.types.git_reference* head = null; 157 158 int error = libgit2.repository.git_repository_head(&head, repo); 159 160 if ((error == libgit2.errors.git_error_code.GIT_EUNBORNBRANCH) || (error == libgit2.errors.git_error_code.GIT_ENOTFOUND)) { 161 branch = null; 162 } else if (!error) { 163 branch = libgit2.refs.git_reference_shorthand(head); 164 } else { 165 libgit2.example.common.check_lg2(error, "failed to get current branch", null); 166 } 167 168 if (format == .FORMAT_LONG) { 169 core.stdc.stdio.printf("# On branch %s\n", (branch) ? (branch) : ("Not currently on any branch.")); 170 } else { 171 core.stdc.stdio.printf("## %s\n", (branch) ? (branch) : ("HEAD (no branch)")); 172 } 173 174 libgit2.refs.git_reference_free(head); 175 } 176 177 /** 178 * This function print out an output similar to git's status command 179 * in long form, including the command-line hints. 180 */ 181 nothrow @nogc 182 private void print_long(libgit2.types.git_status_list* status) 183 184 in 185 { 186 } 187 188 do 189 { 190 size_t maxi = libgit2.status.git_status_list_entrycount(status); 191 const (libgit2.status.git_status_entry)* s; 192 int header = 0; 193 int rm_in_workdir = 0; 194 195 /** Print index changes. */ 196 197 for (size_t i = 0; i < maxi; ++i) { 198 const (char)* istatus = null; 199 200 s = libgit2.status.git_status_byindex(status, i); 201 202 if (s.status == libgit2.status.git_status_t.GIT_STATUS_CURRENT) { 203 continue; 204 } 205 206 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_DELETED) { 207 rm_in_workdir = 1; 208 } 209 210 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_NEW) { 211 istatus = "new file: "; 212 } 213 214 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_MODIFIED) { 215 istatus = "modified: "; 216 } 217 218 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_DELETED) { 219 istatus = "deleted: "; 220 } 221 222 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_RENAMED) { 223 istatus = "renamed: "; 224 } 225 226 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_TYPECHANGE) { 227 istatus = "typechange:"; 228 } 229 230 if (istatus == null) { 231 continue; 232 } 233 234 if (!header) { 235 core.stdc.stdio.printf("# Changes to be committed:\n"); 236 core.stdc.stdio.printf("# (use \"git reset HEAD <file>...\" to unstage)\n"); 237 core.stdc.stdio.printf("#\n"); 238 header = 1; 239 } 240 241 const (char)* old_path = s.head_to_index.old_file.path; 242 const (char)* new_path = s.head_to_index.new_file.path; 243 244 if ((old_path) && (new_path) && (core.stdc..string.strcmp(old_path, new_path))) { 245 core.stdc.stdio.printf("#\t%s %s . %s\n", istatus, old_path, new_path); 246 } else { 247 core.stdc.stdio.printf("#\t%s %s\n", istatus, (old_path) ? (old_path) : (new_path)); 248 } 249 } 250 251 int changes_in_index = 0; 252 253 if (header) { 254 changes_in_index = 1; 255 core.stdc.stdio.printf("#\n"); 256 } 257 258 header = 0; 259 260 /** Print workdir changes to tracked files. */ 261 262 for (size_t i = 0; i < maxi; ++i) { 263 const (char)* wstatus = null; 264 265 s = libgit2.status.git_status_byindex(status, i); 266 267 /** 268 * With `libgit2.status.git_status_opt_t.GIT_STATUS_OPT_INCLUDE_UNMODIFIED` (not used in this example) 269 * `index_to_workdir` may not be `null` even if there are 270 * no differences, in which case it will be a `libgit2.diff.git_delta_t.GIT_DELTA_UNMODIFIED`. 271 */ 272 if ((s.status == libgit2.status.git_status_t.GIT_STATUS_CURRENT) || (s.index_to_workdir == null)) { 273 continue; 274 } 275 276 /** Print out the output since we know the file has some changes */ 277 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_MODIFIED) { 278 wstatus = "modified: "; 279 } 280 281 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_DELETED) { 282 wstatus = "deleted: "; 283 } 284 285 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_RENAMED) { 286 wstatus = "renamed: "; 287 } 288 289 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_TYPECHANGE) { 290 wstatus = "typechange:"; 291 } 292 293 if (wstatus == null) { 294 continue; 295 } 296 297 if (!header) { 298 core.stdc.stdio.printf("# Changes not staged for commit:\n"); 299 core.stdc.stdio.printf("# (use \"git add%s <file>...\" to update what will be committed)\n", (rm_in_workdir) ? (&("/rm\0"[0])) : (&("\0"[0]))); 300 core.stdc.stdio.printf("# (use \"git checkout -- <file>...\" to discard changes in working directory)\n"); 301 core.stdc.stdio.printf("#\n"); 302 header = 1; 303 } 304 305 const (char)* old_path = s.index_to_workdir.old_file.path; 306 const (char)* new_path = s.index_to_workdir.new_file.path; 307 308 if ((old_path) && (new_path) && (core.stdc..string.strcmp(old_path, new_path))) { 309 core.stdc.stdio.printf("#\t%s %s . %s\n", wstatus, old_path, new_path); 310 } else { 311 core.stdc.stdio.printf("#\t%s %s\n", wstatus, (old_path) ? (old_path) : (new_path)); 312 } 313 } 314 315 int changed_in_workdir = 0; 316 317 if (header) { 318 changed_in_workdir = 1; 319 core.stdc.stdio.printf("#\n"); 320 } 321 322 /** Print untracked files. */ 323 324 header = 0; 325 326 for (size_t i = 0; i < maxi; ++i) { 327 s = libgit2.status.git_status_byindex(status, i); 328 329 if (s.status == libgit2.status.git_status_t.GIT_STATUS_WT_NEW) { 330 if (!header) { 331 core.stdc.stdio.printf("# Untracked files:\n"); 332 core.stdc.stdio.printf("# (use \"git add <file>...\" to include in what will be committed)\n"); 333 core.stdc.stdio.printf("#\n"); 334 header = 1; 335 } 336 337 core.stdc.stdio.printf("#\t%s\n", s.index_to_workdir.old_file.path); 338 } 339 } 340 341 header = 0; 342 343 /** Print ignored files. */ 344 345 for (size_t i = 0; i < maxi; ++i) { 346 s = libgit2.status.git_status_byindex(status, i); 347 348 if (s.status == libgit2.status.git_status_t.GIT_STATUS_IGNORED) { 349 if (!header) { 350 core.stdc.stdio.printf("# Ignored files:\n"); 351 core.stdc.stdio.printf("# (use \"git add -f <file>...\" to include in what will be committed)\n"); 352 core.stdc.stdio.printf("#\n"); 353 header = 1; 354 } 355 356 core.stdc.stdio.printf("#\t%s\n", s.index_to_workdir.old_file.path); 357 } 358 } 359 360 if ((!changes_in_index) && (changed_in_workdir)) { 361 core.stdc.stdio.printf("no changes added to commit (use \"git add\" and/or \"git commit -a\")\n"); 362 } 363 } 364 365 /** 366 * This version of the output prefixes each path with two status 367 * columns and shows submodule status information. 368 */ 369 nothrow @nogc 370 private void print_short(libgit2.types.git_repository* repo, libgit2.types.git_status_list* status) 371 372 in 373 { 374 } 375 376 do 377 { 378 size_t maxi = libgit2.status.git_status_list_entrycount(status); 379 380 for (size_t i = 0; i < maxi; ++i) { 381 const (libgit2.status.git_status_entry)* s = libgit2.status.git_status_byindex(status, i); 382 383 if (s.status == libgit2.status.git_status_t.GIT_STATUS_CURRENT) { 384 continue; 385 } 386 387 const (char)* c = null; 388 const (char)* b = null; 389 const (char)* a = null; 390 char wstatus = ' '; 391 char istatus = ' '; 392 const (char)* extra = ""; 393 394 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_NEW) { 395 istatus = 'A'; 396 } 397 398 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_MODIFIED) { 399 istatus = 'M'; 400 } 401 402 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_DELETED) { 403 istatus = 'D'; 404 } 405 406 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_RENAMED) { 407 istatus = 'R'; 408 } 409 410 if (s.status & libgit2.status.git_status_t.GIT_STATUS_INDEX_TYPECHANGE) { 411 istatus = 'T'; 412 } 413 414 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_NEW) { 415 if (istatus == ' ') { 416 istatus = '?'; 417 } 418 419 wstatus = '?'; 420 } 421 422 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_MODIFIED) { 423 wstatus = 'M'; 424 } 425 426 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_DELETED) { 427 wstatus = 'D'; 428 } 429 430 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_RENAMED) { 431 wstatus = 'R'; 432 } 433 434 if (s.status & libgit2.status.git_status_t.GIT_STATUS_WT_TYPECHANGE) { 435 wstatus = 'T'; 436 } 437 438 if (s.status & libgit2.status.git_status_t.GIT_STATUS_IGNORED) { 439 istatus = '!'; 440 wstatus = '!'; 441 } 442 443 if (istatus == '?' && wstatus == '?') { 444 continue; 445 } 446 447 /** 448 * A commit in a tree is how submodules are stored, so 449 * let's go take a look at its status. 450 */ 451 if ((s.index_to_workdir) && (s.index_to_workdir.new_file.mode == libgit2.types.git_filemode_t.GIT_FILEMODE_COMMIT)) { 452 uint smstatus = 0; 453 454 if (!libgit2.submodule.git_submodule_status(&smstatus, repo, s.index_to_workdir.new_file.path, libgit2.types.git_submodule_ignore_t.GIT_SUBMODULE_IGNORE_UNSPECIFIED)) { 455 if (smstatus & libgit2.submodule.git_submodule_status_t.GIT_SUBMODULE_STATUS_WD_MODIFIED) { 456 extra = " (new commits)"; 457 } else if (smstatus & libgit2.submodule.git_submodule_status_t.GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED) { 458 extra = " (modified content)"; 459 } else if (smstatus & libgit2.submodule.git_submodule_status_t.GIT_SUBMODULE_STATUS_WD_WD_MODIFIED) { 460 extra = " (modified content)"; 461 } else if (smstatus & libgit2.submodule.git_submodule_status_t.GIT_SUBMODULE_STATUS_WD_UNTRACKED) { 462 extra = " (untracked content)"; 463 } 464 } 465 } 466 467 /** 468 * Now that we have all the information, format the output. 469 */ 470 471 if (s.head_to_index) { 472 a = s.head_to_index.old_file.path; 473 b = s.head_to_index.new_file.path; 474 } 475 476 if (s.index_to_workdir) { 477 if (a == null) { 478 a = s.index_to_workdir.old_file.path; 479 } 480 481 if (b == null) { 482 b = s.index_to_workdir.old_file.path; 483 } 484 485 c = s.index_to_workdir.new_file.path; 486 } 487 488 if (istatus == 'R') { 489 if (wstatus == 'R') { 490 core.stdc.stdio.printf("%c%c %s %s %s%s\n", istatus, wstatus, a, b, c, extra); 491 } else { 492 core.stdc.stdio.printf("%c%c %s %s%s\n", istatus, wstatus, a, b, extra); 493 } 494 } else { 495 if (wstatus == 'R') { 496 core.stdc.stdio.printf("%c%c %s %s%s\n", istatus, wstatus, a, c, extra); 497 } else { 498 core.stdc.stdio.printf("%c%c %s%s\n", istatus, wstatus, a, extra); 499 } 500 } 501 } 502 503 for (size_t i = 0; i < maxi; ++i) { 504 const (libgit2.status.git_status_entry)* s = libgit2.status.git_status_byindex(status, i); 505 506 if (s.status == libgit2.status.git_status_t.GIT_STATUS_WT_NEW) { 507 core.stdc.stdio.printf("?? %s\n", s.index_to_workdir.old_file.path); 508 } 509 } 510 } 511 512 extern (C) 513 nothrow @nogc 514 private int print_submod(libgit2.types.git_submodule* sm, const (char)* name, void* payload) 515 516 in 517 { 518 } 519 520 do 521 { 522 //cast(void)(name); 523 int* count = cast(int*)(payload); 524 525 if (*count == 0) { 526 core.stdc.stdio.printf("# Submodules\n"); 527 } 528 529 (*count)++; 530 531 core.stdc.stdio.printf("# - submodule '%s' at %s\n", libgit2.submodule.git_submodule_name(sm), libgit2.submodule.git_submodule_path(sm)); 532 533 return 0; 534 } 535 536 /** 537 * Parse options that git's status command supports. 538 */ 539 nothrow @nogc 540 private void parse_opts(.status_opts* o, int argc, char** argv) 541 542 in 543 { 544 } 545 546 do 547 { 548 libgit2.example.args.args_info args = libgit2.example.args.ARGS_INFO_INIT(argc, argv); 549 550 for (args.pos = 1; args.pos < argc; ++args.pos) { 551 char* a = argv[args.pos]; 552 553 if (a[0] != '-') { 554 if (o.npaths < .MAX_PATHSPEC) { 555 o.pathspec[o.npaths++] = a; 556 } else { 557 libgit2.example.common.fatal("Example only supports a limited pathspec", null); 558 } 559 } else if ((!core.stdc..string.strcmp(a, "-s")) || (!core.stdc..string.strcmp(a, "--short"))) { 560 o.format = .FORMAT_SHORT; 561 } else if (!core.stdc..string.strcmp(a, "--long")) { 562 o.format = .FORMAT_LONG; 563 } else if (!core.stdc..string.strcmp(a, "--porcelain")) { 564 o.format = .FORMAT_PORCELAIN; 565 } else if ((!core.stdc..string.strcmp(a, "-b")) || (!core.stdc..string.strcmp(a, "--branch"))) { 566 o.showbranch = 1; 567 } else if (!core.stdc..string.strcmp(a, "-z")) { 568 o.zterm = 1; 569 570 if (o.format == .FORMAT_DEFAULT) { 571 o.format = .FORMAT_PORCELAIN; 572 } 573 } else if (!core.stdc..string.strcmp(a, "--ignored")) { 574 o.statusopt.flags |= libgit2.status.git_status_opt_t.GIT_STATUS_OPT_INCLUDE_IGNORED; 575 } else if ((!core.stdc..string.strcmp(a, "-uno")) || (!core.stdc..string.strcmp(a, "--untracked-files=no"))) { 576 o.statusopt.flags &= ~libgit2.status.git_status_opt_t.GIT_STATUS_OPT_INCLUDE_UNTRACKED; 577 } else if ((!core.stdc..string.strcmp(a, "-unormal")) || (!core.stdc..string.strcmp(a, "--untracked-files=normal"))) { 578 o.statusopt.flags |= libgit2.status.git_status_opt_t.GIT_STATUS_OPT_INCLUDE_UNTRACKED; 579 } else if ((!core.stdc..string.strcmp(a, "-uall")) || (!core.stdc..string.strcmp(a, "--untracked-files=all"))) { 580 o.statusopt.flags |= libgit2.status.git_status_opt_t.GIT_STATUS_OPT_INCLUDE_UNTRACKED | libgit2.status.git_status_opt_t.GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; 581 } else if (!core.stdc..string.strcmp(a, "--ignore-submodules=all")) { 582 o.statusopt.flags |= libgit2.status.git_status_opt_t.GIT_STATUS_OPT_EXCLUDE_SUBMODULES; 583 } else if (!core.stdc..string.strncmp(a, "--git-dir=", core.stdc..string.strlen("--git-dir="))) { 584 o.repodir = a + core.stdc..string.strlen("--git-dir="); 585 } else if (!core.stdc..string.strcmp(a, "--repeat")) { 586 o.repeat = 10; 587 } else if (libgit2.example.args.match_int_arg(&o.repeat, &args, "--repeat", 0)) { 588 /* okay */ 589 } else if (!core.stdc..string.strcmp(a, "--list-submodules")) { 590 o.showsubmod = 1; 591 } else { 592 libgit2.example.common.check_lg2(-1, "Unsupported option", a); 593 } 594 } 595 596 if (o.format == .FORMAT_DEFAULT) { 597 o.format = .FORMAT_LONG; 598 } 599 600 if (o.format == .FORMAT_LONG) { 601 o.showbranch = 1; 602 } 603 604 if (o.npaths > 0) { 605 o.statusopt.pathspec.strings = &(o.pathspec[0]); 606 o.statusopt.pathspec.count = o.npaths; 607 } 608 }