libgit2/examples/status.c
2013-06-20 15:10:42 -07:00

444 lines
12 KiB
C

/*
* 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 <git2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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 <file>...\" 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 <file>...\" to update what will be committed)\n", rm_in_workdir ? "/rm" : "");
printf("# (use \"git checkout -- <file>...\" 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 <file>...\" 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 <file>...\" 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;
}