/* * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include #include #include #include enum { FORMAT_DEFAULT = 0, FORMAT_LONG = 1, FORMAT_SHORT = 2, FORMAT_PORCELAIN = 3, }; #define MAX_PATHSPEC 8 /* * This example demonstrates the use of the libgit2 status APIs, * particularly the `git_status_list` object, to roughly simulate the * output of running `git status`. It serves as a simple example of * using those APIs to get basic status information. * * This does not have: * - Robust error handling * - Colorized or paginated output formatting * * This does have: * - Examples of translating command line arguments to the status * options settings to mimic `git status` results. * - A sample status formatter that matches the default "long" format * from `git status` * - A sample status formatter that matches the "short" format */ static void check(int error, const char *message, const char *extra) { const git_error *lg2err; const char *lg2msg = "", *lg2spacer = ""; if (!error) return; if ((lg2err = giterr_last()) != NULL && lg2err->message != NULL) { lg2msg = lg2err->message; lg2spacer = " - "; } if (extra) fprintf(stderr, "%s '%s' [%d]%s%s\n", message, extra, error, lg2spacer, lg2msg); else fprintf(stderr, "%s [%d]%s%s\n", message, error, lg2spacer, lg2msg); exit(1); } static void fail(const char *message) { check(-1, message, NULL); } static void show_branch(git_repository *repo, int format) { int error = 0; const char *branch = NULL; git_reference *head = NULL; error = git_repository_head(&head, repo); if (error == GIT_EORPHANEDHEAD || error == GIT_ENOTFOUND) branch = NULL; else if (!error) { branch = git_reference_name(head); if (!strncmp(branch, "refs/heads/", strlen("refs/heads/"))) branch += strlen("refs/heads/"); } else check(error, "failed to get current branch", NULL); if (format == FORMAT_LONG) printf("# On branch %s\n", branch ? branch : "Not currently on any branch."); else printf("## %s\n", branch ? branch : "HEAD (no branch)"); git_reference_free(head); } static void print_long(git_repository *repo, git_status_list *status) { size_t i, maxi = git_status_list_entrycount(status); const git_status_entry *s; int header = 0, changes_in_index = 0; int changed_in_workdir = 0, rm_in_workdir = 0; const char *old_path, *new_path; (void)repo; /* print index changes */ for (i = 0; i < maxi; ++i) { char *istatus = NULL; s = git_status_byindex(status, i); if (s->status == GIT_STATUS_CURRENT) continue; if (s->status & GIT_STATUS_WT_DELETED) rm_in_workdir = 1; if (s->status & GIT_STATUS_INDEX_NEW) istatus = "new file: "; if (s->status & GIT_STATUS_INDEX_MODIFIED) istatus = "modified: "; if (s->status & GIT_STATUS_INDEX_DELETED) istatus = "deleted: "; if (s->status & GIT_STATUS_INDEX_RENAMED) istatus = "renamed: "; if (s->status & GIT_STATUS_INDEX_TYPECHANGE) istatus = "typechange:"; if (istatus == NULL) continue; if (!header) { printf("# Changes to be committed:\n"); printf("# (use \"git reset HEAD ...\" to unstage)\n"); printf("#\n"); header = 1; } old_path = s->head_to_index->old_file.path; new_path = s->head_to_index->new_file.path; if (old_path && new_path && strcmp(old_path, new_path)) printf("#\t%s %s -> %s\n", istatus, old_path, new_path); else printf("#\t%s %s\n", istatus, old_path ? old_path : new_path); } if (header) { changes_in_index = 1; printf("#\n"); } header = 0; /* print workdir changes to tracked files */ for (i = 0; i < maxi; ++i) { char *wstatus = NULL; s = git_status_byindex(status, i); if (s->status == GIT_STATUS_CURRENT || s->index_to_workdir == NULL) continue; if (s->status & GIT_STATUS_WT_MODIFIED) wstatus = "modified: "; if (s->status & GIT_STATUS_WT_DELETED) wstatus = "deleted: "; if (s->status & GIT_STATUS_WT_RENAMED) wstatus = "renamed: "; if (s->status & GIT_STATUS_WT_TYPECHANGE) wstatus = "typechange:"; if (wstatus == NULL) continue; if (!header) { printf("# Changes not staged for commit:\n"); printf("# (use \"git add%s ...\" to update what will be committed)\n", rm_in_workdir ? "/rm" : ""); printf("# (use \"git checkout -- ...\" to discard changes in working directory)\n"); printf("#\n"); header = 1; } old_path = s->index_to_workdir->old_file.path; new_path = s->index_to_workdir->new_file.path; if (old_path && new_path && strcmp(old_path, new_path)) printf("#\t%s %s -> %s\n", wstatus, old_path, new_path); else printf("#\t%s %s\n", wstatus, old_path ? old_path : new_path); } if (header) { changed_in_workdir = 1; printf("#\n"); } header = 0; /* print untracked files */ header = 0; for (i = 0; i < maxi; ++i) { s = git_status_byindex(status, i); if (s->status == GIT_STATUS_WT_NEW) { if (!header) { printf("# Untracked files:\n"); printf("# (use \"git add ...\" to include in what will be committed)\n"); printf("#\n"); header = 1; } printf("#\t%s\n", s->index_to_workdir->old_file.path); } } header = 0; /* print ignored files */ for (i = 0; i < maxi; ++i) { s = git_status_byindex(status, i); if (s->status == GIT_STATUS_IGNORED) { if (!header) { printf("# Ignored files:\n"); printf("# (use \"git add -f ...\" to include in what will be committed)\n"); printf("#\n"); header = 1; } printf("#\t%s\n", s->index_to_workdir->old_file.path); } } if (!changes_in_index && changed_in_workdir) printf("no changes added to commit (use \"git add\" and/or \"git commit -a\")\n"); } static void print_short(git_repository *repo, git_status_list *status) { size_t i, maxi = git_status_list_entrycount(status); const git_status_entry *s; char istatus, wstatus; const char *extra, *a, *b, *c; for (i = 0; i < maxi; ++i) { s = git_status_byindex(status, i); if (s->status == GIT_STATUS_CURRENT) continue; a = b = c = NULL; istatus = wstatus = ' '; extra = ""; if (s->status & GIT_STATUS_INDEX_NEW) istatus = 'A'; if (s->status & GIT_STATUS_INDEX_MODIFIED) istatus = 'M'; if (s->status & GIT_STATUS_INDEX_DELETED) istatus = 'D'; if (s->status & GIT_STATUS_INDEX_RENAMED) istatus = 'R'; if (s->status & GIT_STATUS_INDEX_TYPECHANGE) istatus = 'T'; if (s->status & GIT_STATUS_WT_NEW) { if (istatus == ' ') istatus = '?'; wstatus = '?'; } if (s->status & GIT_STATUS_WT_MODIFIED) wstatus = 'M'; if (s->status & GIT_STATUS_WT_DELETED) wstatus = 'D'; if (s->status & GIT_STATUS_WT_RENAMED) wstatus = 'R'; if (s->status & GIT_STATUS_WT_TYPECHANGE) wstatus = 'T'; if (s->status & GIT_STATUS_IGNORED) { istatus = '!'; wstatus = '!'; } if (istatus == '?' && wstatus == '?') continue; if (s->index_to_workdir && s->index_to_workdir->new_file.mode == GIT_FILEMODE_COMMIT) { git_submodule *sm = NULL; unsigned int smstatus = 0; if (!git_submodule_lookup( &sm, repo, s->index_to_workdir->new_file.path) && !git_submodule_status(&smstatus, sm)) { if (smstatus & GIT_SUBMODULE_STATUS_WD_MODIFIED) extra = " (new commits)"; else if (smstatus & GIT_SUBMODULE_STATUS_WD_INDEX_MODIFIED) extra = " (modified content)"; else if (smstatus & GIT_SUBMODULE_STATUS_WD_WD_MODIFIED) extra = " (modified content)"; else if (smstatus & GIT_SUBMODULE_STATUS_WD_UNTRACKED) extra = " (untracked content)"; } } if (s->head_to_index) { a = s->head_to_index->old_file.path; b = s->head_to_index->new_file.path; } if (s->index_to_workdir) { if (!a) a = s->index_to_workdir->old_file.path; if (!b) b = s->index_to_workdir->old_file.path; c = s->index_to_workdir->new_file.path; } if (istatus == 'R') { if (wstatus == 'R') printf("%c%c %s %s %s%s\n", istatus, wstatus, a, b, c, extra); else printf("%c%c %s %s%s\n", istatus, wstatus, a, b, extra); } else { if (wstatus == 'R') printf("%c%c %s %s%s\n", istatus, wstatus, a, c, extra); else printf("%c%c %s%s\n", istatus, wstatus, a, extra); } } for (i = 0; i < maxi; ++i) { s = git_status_byindex(status, i); if (s->status == GIT_STATUS_WT_NEW) printf("?? %s\n", s->index_to_workdir->old_file.path); } } int main(int argc, char *argv[]) { git_repository *repo = NULL; int i, npaths = 0, format = FORMAT_DEFAULT, zterm = 0, showbranch = 0; git_status_options opt = GIT_STATUS_OPTIONS_INIT; git_status_list *status; char *repodir = ".", *pathspec[MAX_PATHSPEC]; opt.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR; opt.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY; for (i = 1; i < argc; ++i) { if (argv[i][0] != '-') { if (npaths < MAX_PATHSPEC) pathspec[npaths++] = argv[i]; else fail("Example only supports a limited pathspec"); } else if (!strcmp(argv[i], "-s") || !strcmp(argv[i], "--short")) format = FORMAT_SHORT; else if (!strcmp(argv[i], "--long")) format = FORMAT_LONG; else if (!strcmp(argv[i], "--porcelain")) format = FORMAT_PORCELAIN; else if (!strcmp(argv[i], "-b") || !strcmp(argv[i], "--branch")) showbranch = 1; else if (!strcmp(argv[i], "-z")) { zterm = 1; if (format == FORMAT_DEFAULT) format = FORMAT_PORCELAIN; } else if (!strcmp(argv[i], "--ignored")) opt.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED; else if (!strcmp(argv[i], "-uno") || !strcmp(argv[i], "--untracked-files=no")) opt.flags &= ~GIT_STATUS_OPT_INCLUDE_UNTRACKED; else if (!strcmp(argv[i], "-unormal") || !strcmp(argv[i], "--untracked-files=normal")) opt.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED; else if (!strcmp(argv[i], "-uall") || !strcmp(argv[i], "--untracked-files=all")) opt.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS; else if (!strcmp(argv[i], "--ignore-submodules=all")) opt.flags |= GIT_STATUS_OPT_EXCLUDE_SUBMODULES; else if (!strncmp(argv[i], "--git-dir=", strlen("--git-dir="))) repodir = argv[i] + strlen("--git-dir="); else check(-1, "Unsupported option", argv[i]); } if (format == FORMAT_DEFAULT) format = FORMAT_LONG; if (format == FORMAT_LONG) showbranch = 1; if (npaths > 0) { opt.pathspec.strings = pathspec; opt.pathspec.count = npaths; } /* * Try to open the repository at the given path (or at the current * directory if none was given). */ check(git_repository_open_ext(&repo, repodir, 0, NULL), "Could not open repository", repodir); if (git_repository_is_bare(repo)) fail("Cannot report status on bare repository"); /* * Run status on the repository * * Because we want to simluate a full "git status" run and want to * support some command line options, we use `git_status_foreach_ext()` * instead of just the plain status call. This allows (a) iterating * over the index and then the workdir and (b) extra flags that control * which files are included. If you just want simple status (e.g. to * enumerate files that are modified) then you probably don't need the * extended API. */ check(git_status_list_new(&status, repo, &opt), "Could not get status", NULL); if (showbranch) show_branch(repo, format); if (format == FORMAT_LONG) print_long(repo, status); else print_short(repo, status); git_status_list_free(status); git_repository_free(repo); return 0; }