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 	}