Fix the help tests
[tig.git] / src / status.c
blobfb027aa921f1c35fbb95575decf92b87183dcde7
1 /* Copyright (c) 2006-2015 Jonas Fonseca <jonas.fonseca@gmail.com>
3 * This program is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU General Public License as
5 * published by the Free Software Foundation; either version 2 of
6 * the License, or (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
14 #include "tig/io.h"
15 #include "tig/refdb.h"
16 #include "tig/repo.h"
17 #include "tig/options.h"
18 #include "tig/parse.h"
19 #include "tig/display.h"
20 #include "tig/prompt.h"
21 #include "tig/view.h"
22 #include "tig/search.h"
23 #include "tig/draw.h"
24 #include "tig/git.h"
25 #include "tig/watch.h"
26 #include "tig/status.h"
27 #include "tig/stage.h"
30 * Status backend
33 static char status_onbranch[SIZEOF_STR];
35 /* This should work even for the "On branch" line. */
36 static inline bool
37 status_has_none(struct view *view, struct line *line)
39 return view_has_line(view, line) && !line[1].data;
42 /* Get fields from the diff line:
43 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
45 static inline bool
46 status_get_diff(struct status *file, const char *buf, size_t bufsize)
48 const char *old_mode = buf + 1;
49 const char *new_mode = buf + 8;
50 const char *old_rev = buf + 15;
51 const char *new_rev = buf + 56;
52 const char *status = buf + 97;
54 if (bufsize < 98 ||
55 old_mode[-1] != ':' ||
56 new_mode[-1] != ' ' ||
57 old_rev[-1] != ' ' ||
58 new_rev[-1] != ' ' ||
59 status[-1] != ' ')
60 return false;
62 file->status = *status;
64 string_copy_rev(file->old.rev, old_rev);
65 string_copy_rev(file->new.rev, new_rev);
67 file->old.mode = strtoul(old_mode, NULL, 8);
68 file->new.mode = strtoul(new_mode, NULL, 8);
70 file->old.name[0] = file->new.name[0] = 0;
72 return true;
75 static bool
76 status_run(struct view *view, const char *argv[], char status, enum line_type type)
78 struct status *unmerged = NULL;
79 struct buffer buf;
80 struct io io;
82 if (!io_run(&io, IO_RD, repo.cdup, NULL, argv))
83 return false;
85 add_line_nodata(view, type);
87 while (io_get(&io, &buf, 0, true)) {
88 struct line *line;
89 struct status parsed = {0};
90 struct status *file = &parsed;
92 /* Parse diff info part. */
93 if (status) {
94 file->status = status;
95 if (status == 'A')
96 string_copy(file->old.rev, NULL_ID);
98 } else {
99 if (!status_get_diff(&parsed, buf.data, buf.size))
100 goto error_out;
102 if (!io_get(&io, &buf, 0, true))
103 break;
106 /* Grab the old name for rename/copy. */
107 if (!*file->old.name &&
108 (file->status == 'R' || file->status == 'C')) {
109 string_ncopy(file->old.name, buf.data, buf.size);
111 if (!io_get(&io, &buf, 0, true))
112 break;
115 /* git-ls-files just delivers a NUL separated list of
116 * file names similar to the second half of the
117 * git-diff-* output. */
118 string_ncopy(file->new.name, buf.data, buf.size);
119 if (!*file->old.name)
120 string_copy(file->old.name, file->new.name);
122 /* Collapse all modified entries that follow an associated
123 * unmerged entry. */
124 if (unmerged && !strcmp(unmerged->new.name, file->new.name)) {
125 unmerged->status = 'U';
126 unmerged = NULL;
127 continue;
130 line = add_line_alloc(view, &file, type, 0, false);
131 if (!line)
132 goto error_out;
133 *file = parsed;
134 view_column_info_update(view, line);
135 if (file->status == 'U')
136 unmerged = file;
139 if (io_error(&io)) {
140 error_out:
141 io_done(&io);
142 return false;
145 if (!view->line[view->lines - 1].data) {
146 add_line_nodata(view, LINE_STAT_NONE);
147 if (type == LINE_STAT_STAGED)
148 watch_apply(&view->watch, WATCH_INDEX_STAGED_NO);
149 else if (type == LINE_STAT_UNSTAGED)
150 watch_apply(&view->watch, WATCH_INDEX_UNSTAGED_NO);
151 } else {
152 if (type == LINE_STAT_STAGED)
153 watch_apply(&view->watch, WATCH_INDEX_STAGED_YES);
154 else if (type == LINE_STAT_UNSTAGED)
155 watch_apply(&view->watch, WATCH_INDEX_UNSTAGED_YES);
158 io_done(&io);
159 return true;
162 static const char *status_diff_index_argv[] = { GIT_DIFF_STAGED_FILES("-z") };
163 static const char *status_diff_files_argv[] = { GIT_DIFF_UNSTAGED_FILES("-z") };
165 static const char *status_list_other_argv[] = {
166 "git", "ls-files", "-z", "--others", "--exclude-standard", NULL, NULL, NULL
169 static const char *status_list_no_head_argv[] = {
170 "git", "ls-files", "-z", "--cached", "--exclude-standard", NULL
173 /* Restore the previous line number to stay in the context or select a
174 * line with something that can be updated. */
175 static void
176 status_restore(struct view *view)
178 if (!check_position(&view->prev_pos))
179 return;
181 if (view->prev_pos.lineno >= view->lines)
182 view->prev_pos.lineno = view->lines - 1;
183 while (view->prev_pos.lineno < view->lines && !view->line[view->prev_pos.lineno].data)
184 view->prev_pos.lineno++;
185 while (view->prev_pos.lineno > 0 && !view->line[view->prev_pos.lineno].data)
186 view->prev_pos.lineno--;
188 /* If the above fails, always skip the "On branch" line. */
189 if (view->prev_pos.lineno < view->lines)
190 view->pos.lineno = view->prev_pos.lineno;
191 else
192 view->pos.lineno = 1;
194 if (view->prev_pos.offset > view->pos.lineno)
195 view->pos.offset = view->pos.lineno;
196 else if (view->prev_pos.offset < view->lines)
197 view->pos.offset = view->prev_pos.offset;
199 clear_position(&view->prev_pos);
202 static bool
203 status_branch_tracking_info(char *buf, size_t buf_len, const char *head,
204 const char *remote)
206 if (!string_nformat(buf, buf_len, NULL, "%s...%s",
207 head, remote)) {
208 return false;
211 const char *tracking_info_argv[] = {
212 "git", "rev-list", "--left-right", buf, NULL
215 struct io io;
217 if (!io_run(&io, IO_RD, repo.cdup, NULL, tracking_info_argv)) {
218 return false;
221 struct buffer result = { 0 };
222 int ahead = 0;
223 int behind = 0;
225 while (io_get(&io, &result, '\n', true)) {
226 if (result.size > 0 && result.data) {
227 if (result.data[0] == '<') {
228 ahead++;
229 } else if (result.data[0] == '>') {
230 behind++;
235 bool io_failed = io_error(&io);
236 io_done(&io);
238 if (io_failed) {
239 return false;
242 if (ahead == 0 && behind == 0) {
243 return string_nformat(buf, buf_len, NULL,
244 "Your branch is up-to-date with '%s'.",
245 remote);
246 } else if (ahead > 0 && behind > 0) {
247 return string_nformat(buf, buf_len, NULL,
248 "Your branch and '%s' have diverged, "
249 "and have %d and %d different commits "
250 "each, respectively",
251 remote, ahead, behind);
252 } else if (ahead > 0) {
253 return string_nformat(buf, buf_len, NULL,
254 "Your branch is ahead of '%s' by "
255 "%d commit%s.", remote, ahead,
256 ahead > 1 ? "s" : "");
257 } else if (behind > 0) {
258 return string_nformat(buf, buf_len, NULL,
259 "Your branch is behind '%s' by "
260 "%d commit%s.", remote, behind,
261 behind > 1 ? "s" : "");
264 return false;
267 static void
268 status_update_onbranch(void)
270 static const char *paths[][3] = {
271 { "rebase-apply/rebasing", "rebase-apply/head-name", "Rebasing" },
272 { "rebase-apply/applying", "rebase-apply/head-name", "Applying mailbox to" },
273 { "rebase-apply/", "rebase-apply/head-name", "Rebasing mailbox onto" },
274 { "rebase-merge/interactive", "rebase-merge/head-name", "Interactive rebase" },
275 { "rebase-merge/", "rebase-merge/head-name", "Rebase merge" },
276 { "MERGE_HEAD", NULL, "Merging" },
277 { "BISECT_LOG", NULL, "Bisecting" },
278 { "HEAD", NULL, "On branch" },
280 char buf[SIZEOF_STR];
281 struct stat stat;
282 int i;
284 if (is_initial_commit()) {
285 string_copy(status_onbranch, "Initial commit");
286 return;
289 for (i = 0; i < ARRAY_SIZE(paths); i++) {
290 const char *prefix = paths[i][2];
291 const char *head = repo.head;
292 const char *tracking_info = "";
294 if (!string_format(buf, "%s/%s", repo.git_dir, paths[i][0]) ||
295 lstat(buf, &stat) < 0)
296 continue;
298 if (paths[i][1]) {
299 struct io io;
301 if (io_open(&io, "%s/%s", repo.git_dir, paths[i][1]) &&
302 io_read_buf(&io, buf, sizeof(buf), false)) {
303 head = buf;
304 if (!prefixcmp(head, "refs/heads/"))
305 head += STRING_SIZE("refs/heads/");
309 if (!*head && !strcmp(paths[i][0], "HEAD") && *repo.head_id) {
310 const struct ref *ref = get_canonical_ref(repo.head_id);
312 prefix = "HEAD detached at";
313 head = repo.head_id;
315 if (ref && strcmp(ref->name, "HEAD"))
316 head = ref->name;
317 } else if (!paths[i][1] && *repo.remote) {
318 if (status_branch_tracking_info(buf, sizeof(buf),
319 head, repo.remote)) {
320 tracking_info = buf;
324 const char *fmt = *tracking_info == '\0' ? "%s %s" : "%s %s. %s";
326 if (!string_format(status_onbranch, fmt,
327 prefix, head, tracking_info))
328 string_copy(status_onbranch, repo.head);
329 return;
332 string_copy(status_onbranch, "Not currently on any branch");
335 /* First parse staged info using git-diff-index(1), then parse unstaged
336 * info using git-diff-files(1), and finally untracked files using
337 * git-ls-files(1). */
338 static bool
339 status_open(struct view *view, enum open_flags flags)
341 const char **staged_argv = is_initial_commit() ?
342 status_list_no_head_argv : status_diff_index_argv;
343 char staged_status = staged_argv == status_list_no_head_argv ? 'A' : 0;
345 if (repo.is_inside_work_tree == false) {
346 report("The status view requires a working tree");
347 return false;
350 reset_view(view);
352 /* FIXME: Watch untracked files and on-branch info. */
353 watch_register(&view->watch, WATCH_INDEX);
355 add_line_nodata(view, LINE_HEADER);
356 status_update_onbranch();
358 update_index();
360 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 3] =
361 opt_status_untracked_dirs ? NULL : "--directory";
362 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 2] =
363 opt_status_untracked_dirs ? NULL : "--no-empty-directory";
365 if (!status_run(view, staged_argv, staged_status, LINE_STAT_STAGED) ||
366 !status_run(view, status_diff_files_argv, 0, LINE_STAT_UNSTAGED) ||
367 !status_run(view, status_list_other_argv, '?', LINE_STAT_UNTRACKED)) {
368 report("Failed to load status data");
369 return false;
372 /* Restore the exact position or use the specialized restore
373 * mode? */
374 status_restore(view);
375 return true;
378 static bool
379 status_get_column_data(struct view *view, const struct line *line, struct view_column_data *column_data)
381 struct status *status = line->data;
383 if (!status) {
384 static struct view_column group_column;
385 const char *text;
386 enum line_type type;
388 column_data->section = &group_column;
389 column_data->section->type = VIEW_COLUMN_SECTION;
391 switch (line->type) {
392 case LINE_STAT_STAGED:
393 type = LINE_SECTION;
394 text = "Changes to be committed:";
395 break;
397 case LINE_STAT_UNSTAGED:
398 type = LINE_SECTION;
399 text = "Changes not staged for commit:";
400 break;
402 case LINE_STAT_UNTRACKED:
403 type = LINE_SECTION;
404 text = "Untracked files:";
405 break;
407 case LINE_STAT_NONE:
408 type = LINE_DEFAULT;
409 text = " (no files)";
410 break;
412 case LINE_HEADER:
413 type = LINE_HEADER;
414 text = status_onbranch;
415 break;
417 default:
418 return false;
421 column_data->section->opt.section.text = text;
422 column_data->section->opt.section.type = type;
424 } else {
425 column_data->status = &status->status;
426 column_data->file_name = status->new.name;
428 return true;
431 static enum request
432 status_enter(struct view *view, struct line *line)
434 struct status *status = line->data;
435 enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT;
437 if (line->type == LINE_STAT_NONE ||
438 (!status && line[1].type == LINE_STAT_NONE)) {
439 report("No file to diff");
440 return REQ_NONE;
443 switch (line->type) {
444 case LINE_STAT_STAGED:
445 case LINE_STAT_UNSTAGED:
446 break;
448 case LINE_STAT_UNTRACKED:
449 if (!status) {
450 report("No file to show");
451 return REQ_NONE;
454 if (!suffixcmp(status->new.name, -1, "/")) {
455 report("Cannot display a directory");
456 return REQ_NONE;
458 break;
460 default:
461 report("Nothing to enter");
462 return REQ_NONE;
465 open_stage_view(view, status, line->type, flags);
466 return REQ_NONE;
469 bool
470 status_exists(struct view *view, struct status *status, enum line_type type)
472 unsigned long lineno;
474 refresh_view(view);
476 for (lineno = 0; lineno < view->lines; lineno++) {
477 struct line *line = &view->line[lineno];
478 struct status *pos = line->data;
480 if (line->type != type)
481 continue;
482 if ((!pos && (!status || !status->status) && line[1].data) ||
483 (pos && !strcmp(status->new.name, pos->new.name))) {
484 select_view_line(view, lineno);
485 status_restore(view);
486 return true;
490 return false;
494 static bool
495 status_update_prepare(struct io *io, enum line_type type)
497 const char *staged_argv[] = {
498 "git", "update-index", "-z", "--index-info", NULL
500 const char *others_argv[] = {
501 "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL
504 switch (type) {
505 case LINE_STAT_STAGED:
506 return io_run(io, IO_WR, repo.cdup, NULL, staged_argv);
508 case LINE_STAT_UNSTAGED:
509 case LINE_STAT_UNTRACKED:
510 return io_run(io, IO_WR, repo.cdup, NULL, others_argv);
512 default:
513 die("line type %d not handled in switch", type);
514 return false;
518 static bool
519 status_update_write(struct io *io, struct status *status, enum line_type type)
521 switch (type) {
522 case LINE_STAT_STAGED:
523 return io_printf(io, "%06o %s\t%s%c", status->old.mode,
524 status->old.rev, status->old.name, 0);
526 case LINE_STAT_UNSTAGED:
527 case LINE_STAT_UNTRACKED:
528 return io_printf(io, "%s%c", status->new.name, 0);
530 default:
531 die("line type %d not handled in switch", type);
532 return false;
536 bool
537 status_update_file(struct status *status, enum line_type type)
539 const char *name = status->new.name;
540 struct io io;
541 bool result;
543 if (type == LINE_STAT_UNTRACKED && !suffixcmp(name, strlen(name), "/")) {
544 const char *add_argv[] = { "git", "add", "--", name, NULL };
546 return io_run_bg(add_argv, repo.cdup);
549 if (!status_update_prepare(&io, type))
550 return false;
552 result = status_update_write(&io, status, type);
553 return io_done(&io) && result;
556 bool
557 status_update_files(struct view *view, struct line *line)
559 char buf[sizeof(view->ref)];
560 struct io io;
561 bool result = true;
562 struct line *pos;
563 int files = 0;
564 int file, done;
565 int cursor_y = -1, cursor_x = -1;
567 if (!status_update_prepare(&io, line->type))
568 return false;
570 for (pos = line; view_has_line(view, pos) && pos->data; pos++)
571 files++;
573 string_copy(buf, view->ref);
574 get_cursor_pos(cursor_y, cursor_x);
575 for (file = 0, done = 5; result && file < files; line++, file++) {
576 int almost_done = file * 100 / files;
578 if (almost_done > done && view_is_displayed(view)) {
579 done = almost_done;
580 string_format(view->ref, "updating file %u of %u (%d%% done)",
581 file, files, done);
582 update_view_title(view);
583 set_cursor_pos(cursor_y, cursor_x);
584 doupdate();
586 result = status_update_write(&io, line->data, line->type);
588 string_copy(view->ref, buf);
590 return io_done(&io) && result;
593 static bool
594 status_update(struct view *view)
596 struct line *line = &view->line[view->pos.lineno];
598 assert(view->lines);
600 if (!line->data) {
601 if (status_has_none(view, line)) {
602 report("Nothing to update");
603 return false;
606 if (!status_update_files(view, line + 1)) {
607 report("Failed to update file status");
608 return false;
611 } else if (!status_update_file(line->data, line->type)) {
612 report("Failed to update file status");
613 return false;
616 return true;
619 bool
620 status_revert(struct status *status, enum line_type type, bool has_none)
622 if (!status || type != LINE_STAT_UNSTAGED) {
623 if (type == LINE_STAT_STAGED) {
624 report("Cannot revert changes to staged files");
625 } else if (type == LINE_STAT_UNTRACKED) {
626 report("Cannot revert changes to untracked files");
627 } else if (has_none) {
628 report("Nothing to revert");
629 } else {
630 report("Cannot revert changes to multiple files");
633 } else if (prompt_yesno("Are you sure you want to revert changes?")) {
634 char mode[10] = "100644";
635 const char *reset_argv[] = {
636 "git", "update-index", "--cacheinfo", mode,
637 status->old.rev, status->old.name, NULL
639 const char *checkout_argv[] = {
640 "git", "checkout", "--", status->old.name, NULL
643 if (status->status == 'U') {
644 string_format(mode, "%5o", status->old.mode);
646 if (status->old.mode == 0 && status->new.mode == 0) {
647 reset_argv[2] = "--force-remove";
648 reset_argv[3] = status->old.name;
649 reset_argv[4] = NULL;
652 if (!io_run_fg(reset_argv, repo.cdup))
653 return false;
654 if (status->old.mode == 0 && status->new.mode == 0)
655 return true;
658 return io_run_fg(checkout_argv, repo.cdup);
661 return false;
664 static void
665 open_mergetool(const char *file)
667 const char *mergetool_argv[] = { "git", "mergetool", file, NULL };
669 open_external_viewer(mergetool_argv, repo.cdup, false, true, true, "");
672 static enum request
673 status_request(struct view *view, enum request request, struct line *line)
675 struct status *status = line->data;
677 switch (request) {
678 case REQ_STATUS_UPDATE:
679 if (!status_update(view))
680 return REQ_NONE;
681 break;
683 case REQ_STATUS_REVERT:
684 if (!status_revert(status, line->type, status_has_none(view, line)))
685 return REQ_NONE;
686 break;
688 case REQ_STATUS_MERGE:
689 if (!status || status->status != 'U') {
690 report("Merging only possible for files with unmerged status ('U').");
691 return REQ_NONE;
693 open_mergetool(status->new.name);
694 break;
696 case REQ_EDIT:
697 if (!status)
698 return request;
699 if (status->status == 'D') {
700 report("File has been deleted.");
701 return REQ_NONE;
704 open_editor(status->new.name, 0);
705 break;
707 case REQ_VIEW_BLAME:
708 if (line->type == LINE_STAT_UNTRACKED || !status) {
709 report("Nothing to blame here");
710 return REQ_NONE;
712 if (status)
713 view->env->ref[0] = 0;
714 return request;
716 case REQ_ENTER:
717 /* After returning the status view has been split to
718 * show the stage view. No further reloading is
719 * necessary. */
720 return status_enter(view, line);
722 case REQ_REFRESH:
723 /* Load the current branch information and then the view. */
724 load_repo_head();
725 break;
727 default:
728 return request;
731 refresh_view(view);
733 return REQ_NONE;
736 bool
737 status_stage_info_(char *buf, size_t bufsize,
738 enum line_type type, struct status *status)
740 const char *file = status ? status->new.name : "";
741 const char *info;
743 switch (type) {
744 case LINE_STAT_STAGED:
745 if (status && status->status)
746 info = "Staged changes to %s";
747 else
748 info = "Staged changes";
749 break;
751 case LINE_STAT_UNSTAGED:
752 if (status && status->status)
753 info = "Unstaged changes to %s";
754 else
755 info = "Unstaged changes";
756 break;
758 case LINE_STAT_UNTRACKED:
759 info = "Untracked file %s";
760 break;
762 case LINE_HEADER:
763 default:
764 info = "";
767 return string_nformat(buf, bufsize, NULL, info, file);
770 static void
771 status_select(struct view *view, struct line *line)
773 struct status *status = line->data;
774 char file[SIZEOF_STR] = "all files";
775 const char *text;
776 const char *key;
778 if (status && !string_format(file, "'%s'", status->new.name))
779 return;
781 if (!status && line[1].type == LINE_STAT_NONE)
782 line++;
784 switch (line->type) {
785 case LINE_STAT_STAGED:
786 text = "Press %s to unstage %s for commit";
787 break;
789 case LINE_STAT_UNSTAGED:
790 text = "Press %s to stage %s for commit";
791 break;
793 case LINE_STAT_UNTRACKED:
794 text = "Press %s to stage %s for addition";
795 break;
797 default:
798 text = "Nothing to update";
801 if (status && status->status == 'U') {
802 text = "Press %s to resolve conflict in %s";
803 key = get_view_key(view, REQ_STATUS_MERGE);
805 } else {
806 key = get_view_key(view, REQ_STATUS_UPDATE);
809 string_format(view->ref, text, key, file);
810 status_stage_info(view->env->status, line->type, status);
811 if (status)
812 string_copy(view->env->file, status->new.name);
815 static struct view_ops status_ops = {
816 "file",
818 VIEW_CUSTOM_STATUS | VIEW_SEND_CHILD_ENTER | VIEW_STATUS_LIKE | VIEW_REFRESH,
820 status_open,
821 NULL,
822 view_column_draw,
823 status_request,
824 view_column_grep,
825 status_select,
826 NULL,
827 view_column_bit(FILE_NAME) | view_column_bit(LINE_NUMBER) |
828 view_column_bit(STATUS),
829 status_get_column_data,
832 DEFINE_VIEW(status);
834 /* vim: set ts=8 sw=8 noexpandtab: */