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