Auto-refresh views when changes are detected in the repository
[tig.git] / src / status.c
blob242ea9840311c6832b95c63908bf3a31a100db1d
1 /* Copyright (c) 2006-2014 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/draw.h"
23 #include "tig/git.h"
24 #include "tig/watch.h"
25 #include "tig/status.h"
26 #include "tig/stage.h"
29 * Status backend
32 static char status_onbranch[SIZEOF_STR];
34 /* This should work even for the "On branch" line. */
35 static inline bool
36 status_has_none(struct view *view, struct line *line)
38 return view_has_line(view, line) && !line[1].data;
41 /* Get fields from the diff line:
42 * :100644 100644 06a5d6ae9eca55be2e0e585a152e6b1336f2b20e 0000000000000000000000000000000000000000 M
44 static inline bool
45 status_get_diff(struct status *file, const char *buf, size_t bufsize)
47 const char *old_mode = buf + 1;
48 const char *new_mode = buf + 8;
49 const char *old_rev = buf + 15;
50 const char *new_rev = buf + 56;
51 const char *status = buf + 97;
53 if (bufsize < 98 ||
54 old_mode[-1] != ':' ||
55 new_mode[-1] != ' ' ||
56 old_rev[-1] != ' ' ||
57 new_rev[-1] != ' ' ||
58 status[-1] != ' ')
59 return FALSE;
61 file->status = *status;
63 string_copy_rev(file->old.rev, old_rev);
64 string_copy_rev(file->new.rev, new_rev);
66 file->old.mode = strtoul(old_mode, NULL, 8);
67 file->new.mode = strtoul(new_mode, NULL, 8);
69 file->old.name[0] = file->new.name[0] = 0;
71 return TRUE;
74 static bool
75 status_run(struct view *view, const char *argv[], char status, enum line_type type)
77 struct status *unmerged = NULL;
78 char *buf;
79 struct io io;
81 if (!io_run(&io, IO_RD, repo.cdup, opt_env, argv))
82 return FALSE;
84 add_line_nodata(view, type);
86 while ((buf = io_get(&io, 0, TRUE))) {
87 struct line *line;
88 struct status parsed = {};
89 struct status *file = &parsed;
91 /* Parse diff info part. */
92 if (status) {
93 file->status = status;
94 if (status == 'A')
95 string_copy(file->old.rev, NULL_ID);
97 } else {
98 if (!status_get_diff(&parsed, buf, strlen(buf)))
99 goto error_out;
101 buf = io_get(&io, 0, TRUE);
102 if (!buf)
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, strlen(buf));
111 buf = io_get(&io, 0, TRUE);
112 if (!buf)
113 break;
116 /* git-ls-files just delivers a NUL separated list of
117 * file names similar to the second half of the
118 * git-diff-* output. */
119 string_ncopy(file->new.name, buf, strlen(buf));
120 if (!*file->old.name)
121 string_copy(file->old.name, file->new.name);
123 /* Collapse all modified entries that follow an associated
124 * unmerged entry. */
125 if (unmerged && !strcmp(unmerged->new.name, file->new.name)) {
126 unmerged->status = 'U';
127 unmerged = NULL;
128 continue;
131 line = add_line_alloc(view, &file, type, 0, FALSE);
132 if (!line)
133 goto error_out;
134 *file = parsed;
135 view_column_info_update(view, line);
136 if (file->status == 'U')
137 unmerged = file;
140 if (io_error(&io)) {
141 error_out:
142 io_done(&io);
143 return FALSE;
146 if (!view->line[view->lines - 1].data)
147 add_line_nodata(view, LINE_STAT_NONE);
149 io_done(&io);
150 return TRUE;
153 static const char *status_diff_index_argv[] = { GIT_DIFF_STAGED_FILES("-z") };
154 static const char *status_diff_files_argv[] = { GIT_DIFF_UNSTAGED_FILES("-z") };
156 static const char *status_list_other_argv[] = {
157 "git", "ls-files", "-z", "--others", "--exclude-standard", repo.prefix, NULL, NULL, NULL
160 static const char *status_list_no_head_argv[] = {
161 "git", "ls-files", "-z", "--cached", "--exclude-standard", NULL
164 /* Restore the previous line number to stay in the context or select a
165 * line with something that can be updated. */
166 static void
167 status_restore(struct view *view)
169 if (!check_position(&view->prev_pos))
170 return;
172 if (view->prev_pos.lineno >= view->lines)
173 view->prev_pos.lineno = view->lines - 1;
174 while (view->prev_pos.lineno < view->lines && !view->line[view->prev_pos.lineno].data)
175 view->prev_pos.lineno++;
176 while (view->prev_pos.lineno > 0 && !view->line[view->prev_pos.lineno].data)
177 view->prev_pos.lineno--;
179 /* If the above fails, always skip the "On branch" line. */
180 if (view->prev_pos.lineno < view->lines)
181 view->pos.lineno = view->prev_pos.lineno;
182 else
183 view->pos.lineno = 1;
185 if (view->prev_pos.offset > view->pos.lineno)
186 view->pos.offset = view->pos.lineno;
187 else if (view->prev_pos.offset < view->lines)
188 view->pos.offset = view->prev_pos.offset;
190 clear_position(&view->prev_pos);
193 static void
194 status_update_onbranch(void)
196 static const char *paths[][2] = {
197 { "rebase-apply/rebasing", "Rebasing" },
198 { "rebase-apply/applying", "Applying mailbox" },
199 { "rebase-apply/", "Rebasing mailbox" },
200 { "rebase-merge/interactive", "Interactive rebase" },
201 { "rebase-merge/", "Rebase merge" },
202 { "MERGE_HEAD", "Merging" },
203 { "BISECT_LOG", "Bisecting" },
204 { "HEAD", "On branch" },
206 char buf[SIZEOF_STR];
207 struct stat stat;
208 int i;
210 if (is_initial_commit()) {
211 string_copy(status_onbranch, "Initial commit");
212 return;
215 for (i = 0; i < ARRAY_SIZE(paths); i++) {
216 char *head = repo.head;
218 if (!string_format(buf, "%s/%s", repo.git_dir, paths[i][0]) ||
219 lstat(buf, &stat) < 0)
220 continue;
222 if (!*repo.head) {
223 struct io io;
225 if (io_open(&io, "%s/rebase-merge/head-name", repo.git_dir) &&
226 io_read_buf(&io, buf, sizeof(buf))) {
227 head = buf;
228 if (!prefixcmp(head, "refs/heads/"))
229 head += STRING_SIZE("refs/heads/");
233 if (!string_format(status_onbranch, "%s %s", paths[i][1], head))
234 string_copy(status_onbranch, repo.head);
235 return;
238 string_copy(status_onbranch, "Not currently on any branch");
241 /* First parse staged info using git-diff-index(1), then parse unstaged
242 * info using git-diff-files(1), and finally untracked files using
243 * git-ls-files(1). */
244 static bool
245 status_open(struct view *view, enum open_flags flags)
247 const char **staged_argv = is_initial_commit() ?
248 status_list_no_head_argv : status_diff_index_argv;
249 char staged_status = staged_argv == status_list_no_head_argv ? 'A' : 0;
251 if (repo.is_inside_work_tree == FALSE) {
252 report("The status view requires a working tree");
253 return FALSE;
256 reset_view(view);
258 /* FIXME: Watch untracked files and on-branch info. */
259 watch_register(&view->watch, WATCH_INDEX_STAGED | WATCH_INDEX_UNSTAGED);
261 add_line_nodata(view, LINE_HEADER);
262 status_update_onbranch();
264 update_index();
266 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 3] =
267 opt_status_untracked_dirs ? NULL : "--directory";
268 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 2] =
269 opt_status_untracked_dirs ? NULL : "--no-empty-directory";
271 if (!status_run(view, staged_argv, staged_status, LINE_STAT_STAGED) ||
272 !status_run(view, status_diff_files_argv, 0, LINE_STAT_UNSTAGED) ||
273 !status_run(view, status_list_other_argv, '?', LINE_STAT_UNTRACKED)) {
274 report("Failed to load status data");
275 return FALSE;
278 /* Restore the exact position or use the specialized restore
279 * mode? */
280 status_restore(view);
281 return TRUE;
284 static bool
285 status_get_column_data(struct view *view, const struct line *line, struct view_column_data *column_data)
287 struct status *status = line->data;
289 if (!status) {
290 static struct view_column group_column;
291 const char *text;
292 enum line_type type;
294 column_data->section = &group_column;
295 column_data->section->type = VIEW_COLUMN_SECTION;
297 switch (line->type) {
298 case LINE_STAT_STAGED:
299 type = LINE_SECTION;
300 text = "Changes to be committed:";
301 break;
303 case LINE_STAT_UNSTAGED:
304 type = LINE_SECTION;
305 text = "Changed but not updated:";
306 break;
308 case LINE_STAT_UNTRACKED:
309 type = LINE_SECTION;
310 text = "Untracked files:";
311 break;
313 case LINE_STAT_NONE:
314 type = LINE_DEFAULT;
315 text = " (no files)";
316 break;
318 case LINE_HEADER:
319 type = LINE_HEADER;
320 text = status_onbranch;
321 break;
323 default:
324 return FALSE;
327 column_data->section->opt.section.text = text;
328 column_data->section->opt.section.type = type;
330 } else {
331 column_data->status = &status->status;
332 column_data->file_name = status->new.name;
334 return TRUE;
337 static enum request
338 status_enter(struct view *view, struct line *line)
340 struct status *status = line->data;
341 enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT;
343 if (line->type == LINE_STAT_NONE ||
344 (!status && line[1].type == LINE_STAT_NONE)) {
345 report("No file to diff");
346 return REQ_NONE;
349 switch (line->type) {
350 case LINE_STAT_STAGED:
351 case LINE_STAT_UNSTAGED:
352 break;
354 case LINE_STAT_UNTRACKED:
355 if (!status) {
356 report("No file to show");
357 return REQ_NONE;
360 if (!suffixcmp(status->new.name, -1, "/")) {
361 report("Cannot display a directory");
362 return REQ_NONE;
364 break;
366 default:
367 report("Nothing to enter");
368 return REQ_NONE;
371 open_stage_view(view, status, line->type, flags);
372 return REQ_NONE;
375 bool
376 status_exists(struct view *view, struct status *status, enum line_type type)
378 unsigned long lineno;
380 for (lineno = 0; lineno < view->lines; lineno++) {
381 struct line *line = &view->line[lineno];
382 struct status *pos = line->data;
384 if (line->type != type)
385 continue;
386 if ((!pos && (!status || !status->status) && line[1].data) ||
387 (pos && !strcmp(status->new.name, pos->new.name))) {
388 select_view_line(view, lineno);
389 status_restore(view);
390 return TRUE;
394 return FALSE;
398 static bool
399 status_update_prepare(struct io *io, enum line_type type)
401 const char *staged_argv[] = {
402 "git", "update-index", "-z", "--index-info", NULL
404 const char *others_argv[] = {
405 "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL
408 switch (type) {
409 case LINE_STAT_STAGED:
410 return io_run(io, IO_WR, repo.cdup, opt_env, staged_argv);
412 case LINE_STAT_UNSTAGED:
413 case LINE_STAT_UNTRACKED:
414 return io_run(io, IO_WR, repo.cdup, opt_env, others_argv);
416 default:
417 die("line type %d not handled in switch", type);
418 return FALSE;
422 static bool
423 status_update_write(struct io *io, struct status *status, enum line_type type)
425 switch (type) {
426 case LINE_STAT_STAGED:
427 return io_printf(io, "%06o %s\t%s%c", status->old.mode,
428 status->old.rev, status->old.name, 0);
430 case LINE_STAT_UNSTAGED:
431 case LINE_STAT_UNTRACKED:
432 return io_printf(io, "%s%c", status->new.name, 0);
434 default:
435 die("line type %d not handled in switch", type);
436 return FALSE;
440 bool
441 status_update_file(struct status *status, enum line_type type)
443 const char *name = status->new.name;
444 struct io io;
445 bool result;
447 if (type == LINE_STAT_UNTRACKED && !suffixcmp(name, strlen(name), "/")) {
448 const char *add_argv[] = { "git", "add", "--", name, NULL };
450 return io_run_bg(add_argv);
453 if (!status_update_prepare(&io, type))
454 return FALSE;
456 result = status_update_write(&io, status, type);
457 return io_done(&io) && result;
460 bool
461 status_update_files(struct view *view, struct line *line)
463 char buf[sizeof(view->ref)];
464 struct io io;
465 bool result = TRUE;
466 struct line *pos;
467 int files = 0;
468 int file, done;
469 int cursor_y = -1, cursor_x = -1;
471 if (!status_update_prepare(&io, line->type))
472 return FALSE;
474 for (pos = line; view_has_line(view, pos) && pos->data; pos++)
475 files++;
477 string_copy(buf, view->ref);
478 getsyx(cursor_y, cursor_x);
479 for (file = 0, done = 5; result && file < files; line++, file++) {
480 int almost_done = file * 100 / files;
482 if (almost_done > done) {
483 done = almost_done;
484 string_format(view->ref, "updating file %u of %u (%d%% done)",
485 file, files, done);
486 update_view_title(view);
487 setsyx(cursor_y, cursor_x);
488 doupdate();
490 result = status_update_write(&io, line->data, line->type);
492 string_copy(view->ref, buf);
494 return io_done(&io) && result;
497 static bool
498 status_update(struct view *view)
500 struct line *line = &view->line[view->pos.lineno];
502 assert(view->lines);
504 if (!line->data) {
505 if (status_has_none(view, line)) {
506 report("Nothing to update");
507 return FALSE;
510 if (!status_update_files(view, line + 1)) {
511 report("Failed to update file status");
512 return FALSE;
515 } else if (!status_update_file(line->data, line->type)) {
516 report("Failed to update file status");
517 return FALSE;
520 return TRUE;
523 bool
524 status_revert(struct status *status, enum line_type type, bool has_none)
526 if (!status || type != LINE_STAT_UNSTAGED) {
527 if (type == LINE_STAT_STAGED) {
528 report("Cannot revert changes to staged files");
529 } else if (type == LINE_STAT_UNTRACKED) {
530 report("Cannot revert changes to untracked files");
531 } else if (has_none) {
532 report("Nothing to revert");
533 } else {
534 report("Cannot revert changes to multiple files");
537 } else if (prompt_yesno("Are you sure you want to revert changes?")) {
538 char mode[10] = "100644";
539 const char *reset_argv[] = {
540 "git", "update-index", "--cacheinfo", mode,
541 status->old.rev, status->old.name, NULL
543 const char *checkout_argv[] = {
544 "git", "checkout", "--", status->old.name, NULL
547 if (status->status == 'U') {
548 string_format(mode, "%5o", status->old.mode);
550 if (status->old.mode == 0 && status->new.mode == 0) {
551 reset_argv[2] = "--force-remove";
552 reset_argv[3] = status->old.name;
553 reset_argv[4] = NULL;
556 if (!io_run_fg(reset_argv, repo.cdup))
557 return FALSE;
558 if (status->old.mode == 0 && status->new.mode == 0)
559 return TRUE;
562 return io_run_fg(checkout_argv, repo.cdup);
565 return FALSE;
568 static void
569 open_mergetool(const char *file)
571 const char *mergetool_argv[] = { "git", "mergetool", file, NULL };
573 open_external_viewer(mergetool_argv, repo.cdup, TRUE, "");
576 static enum request
577 status_request(struct view *view, enum request request, struct line *line)
579 struct status *status = line->data;
581 switch (request) {
582 case REQ_STATUS_UPDATE:
583 if (!status_update(view))
584 return REQ_NONE;
585 break;
587 case REQ_STATUS_REVERT:
588 if (!status_revert(status, line->type, status_has_none(view, line)))
589 return REQ_NONE;
590 break;
592 case REQ_STATUS_MERGE:
593 if (!status || status->status != 'U') {
594 report("Merging only possible for files with unmerged status ('U').");
595 return REQ_NONE;
597 open_mergetool(status->new.name);
598 break;
600 case REQ_EDIT:
601 if (!status)
602 return request;
603 if (status->status == 'D') {
604 report("File has been deleted.");
605 return REQ_NONE;
608 open_editor(status->new.name, 0);
609 break;
611 case REQ_VIEW_BLAME:
612 if (line->type == LINE_STAT_UNTRACKED || !status) {
613 report("Nothing to blame here");
614 return REQ_NONE;
616 if (status)
617 view->env->ref[0] = 0;
618 return request;
620 case REQ_ENTER:
621 /* After returning the status view has been split to
622 * show the stage view. No further reloading is
623 * necessary. */
624 return status_enter(view, line);
626 case REQ_REFRESH:
627 /* Load the current branch information and then the view. */
628 load_refs(TRUE);
629 break;
631 default:
632 return request;
635 refresh_view(view);
637 return REQ_NONE;
640 bool
641 status_stage_info_(char *buf, size_t bufsize,
642 enum line_type type, struct status *status)
644 const char *file = status ? status->new.name : "";
645 const char *info;
647 switch (type) {
648 case LINE_STAT_STAGED:
649 if (status && status->status)
650 info = "Staged changes to %s";
651 else
652 info = "Staged changes";
653 break;
655 case LINE_STAT_UNSTAGED:
656 if (status && status->status)
657 info = "Unstaged changes to %s";
658 else
659 info = "Unstaged changes";
660 break;
662 case LINE_STAT_UNTRACKED:
663 info = "Untracked file %s";
664 break;
666 case LINE_HEADER:
667 default:
668 info = "";
671 return string_nformat(buf, bufsize, NULL, info, file);
674 static void
675 status_select(struct view *view, struct line *line)
677 struct status *status = line->data;
678 char file[SIZEOF_STR] = "all files";
679 const char *text;
680 const char *key;
682 if (status && !string_format(file, "'%s'", status->new.name))
683 return;
685 if (!status && line[1].type == LINE_STAT_NONE)
686 line++;
688 switch (line->type) {
689 case LINE_STAT_STAGED:
690 text = "Press %s to unstage %s for commit";
691 break;
693 case LINE_STAT_UNSTAGED:
694 text = "Press %s to stage %s for commit";
695 break;
697 case LINE_STAT_UNTRACKED:
698 text = "Press %s to stage %s for addition";
699 break;
701 default:
702 text = "Nothing to update";
705 if (status && status->status == 'U') {
706 text = "Press %s to resolve conflict in %s";
707 key = get_view_key(view, REQ_STATUS_MERGE);
709 } else {
710 key = get_view_key(view, REQ_STATUS_UPDATE);
713 string_format(view->ref, text, key, file);
714 status_stage_info(view->env->status, line->type, status);
715 if (status)
716 string_copy(view->env->file, status->new.name);
719 static struct view_ops status_ops = {
720 "file",
722 VIEW_CUSTOM_STATUS | VIEW_SEND_CHILD_ENTER | VIEW_STATUS_LIKE | VIEW_REFRESH,
724 status_open,
725 NULL,
726 view_column_draw,
727 status_request,
728 view_column_grep,
729 status_select,
730 NULL,
731 view_column_bit(FILE_NAME) | view_column_bit(LINE_NUMBER) |
732 view_column_bit(STATUS),
733 status_get_column_data,
736 DEFINE_VIEW(status);
738 /* vim: set ts=8 sw=8 noexpandtab: */