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