Partly restore old refresh behavior
[tig.git] / src / status.c
blob194e88d68c75bf1656c2b6e4966bc8d32be58b77
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 struct buffer 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 (io_get(&io, &buf, 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.data, buf.size))
99 goto error_out;
101 if (!io_get(&io, &buf, 0, TRUE))
102 break;
105 /* Grab the old name for rename/copy. */
106 if (!*file->old.name &&
107 (file->status == 'R' || file->status == 'C')) {
108 string_ncopy(file->old.name, buf.data, buf.size);
110 if (!io_get(&io, &buf, 0, TRUE))
111 break;
114 /* git-ls-files just delivers a NUL separated list of
115 * file names similar to the second half of the
116 * git-diff-* output. */
117 string_ncopy(file->new.name, buf.data, buf.size);
118 if (!*file->old.name)
119 string_copy(file->old.name, file->new.name);
121 /* Collapse all modified entries that follow an associated
122 * unmerged entry. */
123 if (unmerged && !strcmp(unmerged->new.name, file->new.name)) {
124 unmerged->status = 'U';
125 unmerged = NULL;
126 continue;
129 line = add_line_alloc(view, &file, type, 0, FALSE);
130 if (!line)
131 goto error_out;
132 *file = parsed;
133 view_column_info_update(view, line);
134 if (file->status == 'U')
135 unmerged = file;
138 if (io_error(&io)) {
139 error_out:
140 io_done(&io);
141 return FALSE;
144 if (!view->line[view->lines - 1].data) {
145 add_line_nodata(view, LINE_STAT_NONE);
146 if (type == LINE_STAT_STAGED)
147 watch_apply(&view->watch, WATCH_INDEX_STAGED_NO);
148 else if (type == LINE_STAT_UNSTAGED)
149 watch_apply(&view->watch, WATCH_INDEX_UNSTAGED_NO);
150 } else {
151 if (type == LINE_STAT_STAGED)
152 watch_apply(&view->watch, WATCH_INDEX_STAGED_YES);
153 else if (type == LINE_STAT_UNSTAGED)
154 watch_apply(&view->watch, WATCH_INDEX_UNSTAGED_YES);
157 io_done(&io);
158 return TRUE;
161 static const char *status_diff_index_argv[] = { GIT_DIFF_STAGED_FILES("-z") };
162 static const char *status_diff_files_argv[] = { GIT_DIFF_UNSTAGED_FILES("-z") };
164 static const char *status_list_other_argv[] = {
165 "git", "ls-files", "-z", "--others", "--exclude-standard", repo.prefix, NULL, NULL, NULL
168 static const char *status_list_no_head_argv[] = {
169 "git", "ls-files", "-z", "--cached", "--exclude-standard", NULL
172 /* Restore the previous line number to stay in the context or select a
173 * line with something that can be updated. */
174 static void
175 status_restore(struct view *view)
177 if (!check_position(&view->prev_pos))
178 return;
180 if (view->prev_pos.lineno >= view->lines)
181 view->prev_pos.lineno = view->lines - 1;
182 while (view->prev_pos.lineno < view->lines && !view->line[view->prev_pos.lineno].data)
183 view->prev_pos.lineno++;
184 while (view->prev_pos.lineno > 0 && !view->line[view->prev_pos.lineno].data)
185 view->prev_pos.lineno--;
187 /* If the above fails, always skip the "On branch" line. */
188 if (view->prev_pos.lineno < view->lines)
189 view->pos.lineno = view->prev_pos.lineno;
190 else
191 view->pos.lineno = 1;
193 if (view->prev_pos.offset > view->pos.lineno)
194 view->pos.offset = view->pos.lineno;
195 else if (view->prev_pos.offset < view->lines)
196 view->pos.offset = view->prev_pos.offset;
198 clear_position(&view->prev_pos);
201 static void
202 status_update_onbranch(void)
204 static const char *paths[][2] = {
205 { "rebase-apply/rebasing", "Rebasing" },
206 { "rebase-apply/applying", "Applying mailbox" },
207 { "rebase-apply/", "Rebasing mailbox" },
208 { "rebase-merge/interactive", "Interactive rebase" },
209 { "rebase-merge/", "Rebase merge" },
210 { "MERGE_HEAD", "Merging" },
211 { "BISECT_LOG", "Bisecting" },
212 { "HEAD", "On branch" },
214 char buf[SIZEOF_STR];
215 struct stat stat;
216 int i;
218 if (is_initial_commit()) {
219 string_copy(status_onbranch, "Initial commit");
220 return;
223 for (i = 0; i < ARRAY_SIZE(paths); i++) {
224 char *head = repo.head;
226 if (!string_format(buf, "%s/%s", repo.git_dir, paths[i][0]) ||
227 lstat(buf, &stat) < 0)
228 continue;
230 if (!*repo.head) {
231 struct io io;
233 if (io_open(&io, "%s/rebase-merge/head-name", repo.git_dir) &&
234 io_read_buf(&io, buf, sizeof(buf))) {
235 head = buf;
236 if (!prefixcmp(head, "refs/heads/"))
237 head += STRING_SIZE("refs/heads/");
241 if (!string_format(status_onbranch, "%s %s", paths[i][1], head))
242 string_copy(status_onbranch, repo.head);
243 return;
246 string_copy(status_onbranch, "Not currently on any branch");
249 /* First parse staged info using git-diff-index(1), then parse unstaged
250 * info using git-diff-files(1), and finally untracked files using
251 * git-ls-files(1). */
252 static bool
253 status_open(struct view *view, enum open_flags flags)
255 const char **staged_argv = is_initial_commit() ?
256 status_list_no_head_argv : status_diff_index_argv;
257 char staged_status = staged_argv == status_list_no_head_argv ? 'A' : 0;
259 if (repo.is_inside_work_tree == FALSE) {
260 report("The status view requires a working tree");
261 return FALSE;
264 reset_view(view);
266 /* FIXME: Watch untracked files and on-branch info. */
267 watch_register(&view->watch, WATCH_INDEX);
269 add_line_nodata(view, LINE_HEADER);
270 status_update_onbranch();
272 update_index();
274 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 3] =
275 opt_status_untracked_dirs ? NULL : "--directory";
276 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 2] =
277 opt_status_untracked_dirs ? NULL : "--no-empty-directory";
279 if (!status_run(view, staged_argv, staged_status, LINE_STAT_STAGED) ||
280 !status_run(view, status_diff_files_argv, 0, LINE_STAT_UNSTAGED) ||
281 !status_run(view, status_list_other_argv, '?', LINE_STAT_UNTRACKED)) {
282 report("Failed to load status data");
283 return FALSE;
286 /* Restore the exact position or use the specialized restore
287 * mode? */
288 status_restore(view);
289 return TRUE;
292 static bool
293 status_get_column_data(struct view *view, const struct line *line, struct view_column_data *column_data)
295 struct status *status = line->data;
297 if (!status) {
298 static struct view_column group_column;
299 const char *text;
300 enum line_type type;
302 column_data->section = &group_column;
303 column_data->section->type = VIEW_COLUMN_SECTION;
305 switch (line->type) {
306 case LINE_STAT_STAGED:
307 type = LINE_SECTION;
308 text = "Changes to be committed:";
309 break;
311 case LINE_STAT_UNSTAGED:
312 type = LINE_SECTION;
313 text = "Changed but not updated:";
314 break;
316 case LINE_STAT_UNTRACKED:
317 type = LINE_SECTION;
318 text = "Untracked files:";
319 break;
321 case LINE_STAT_NONE:
322 type = LINE_DEFAULT;
323 text = " (no files)";
324 break;
326 case LINE_HEADER:
327 type = LINE_HEADER;
328 text = status_onbranch;
329 break;
331 default:
332 return FALSE;
335 column_data->section->opt.section.text = text;
336 column_data->section->opt.section.type = type;
338 } else {
339 column_data->status = &status->status;
340 column_data->file_name = status->new.name;
342 return TRUE;
345 static enum request
346 status_enter(struct view *view, struct line *line)
348 struct status *status = line->data;
349 enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT;
351 if (line->type == LINE_STAT_NONE ||
352 (!status && line[1].type == LINE_STAT_NONE)) {
353 report("No file to diff");
354 return REQ_NONE;
357 switch (line->type) {
358 case LINE_STAT_STAGED:
359 case LINE_STAT_UNSTAGED:
360 break;
362 case LINE_STAT_UNTRACKED:
363 if (!status) {
364 report("No file to show");
365 return REQ_NONE;
368 if (!suffixcmp(status->new.name, -1, "/")) {
369 report("Cannot display a directory");
370 return REQ_NONE;
372 break;
374 default:
375 report("Nothing to enter");
376 return REQ_NONE;
379 open_stage_view(view, status, line->type, flags);
380 return REQ_NONE;
383 bool
384 status_exists(struct view *view, struct status *status, enum line_type type)
386 unsigned long lineno;
388 for (lineno = 0; lineno < view->lines; lineno++) {
389 struct line *line = &view->line[lineno];
390 struct status *pos = line->data;
392 if (line->type != type)
393 continue;
394 if ((!pos && (!status || !status->status) && line[1].data) ||
395 (pos && !strcmp(status->new.name, pos->new.name))) {
396 select_view_line(view, lineno);
397 status_restore(view);
398 return TRUE;
402 return FALSE;
406 static bool
407 status_update_prepare(struct io *io, enum line_type type)
409 const char *staged_argv[] = {
410 "git", "update-index", "-z", "--index-info", NULL
412 const char *others_argv[] = {
413 "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL
416 switch (type) {
417 case LINE_STAT_STAGED:
418 return io_run(io, IO_WR, repo.cdup, opt_env, staged_argv);
420 case LINE_STAT_UNSTAGED:
421 case LINE_STAT_UNTRACKED:
422 return io_run(io, IO_WR, repo.cdup, opt_env, others_argv);
424 default:
425 die("line type %d not handled in switch", type);
426 return FALSE;
430 static bool
431 status_update_write(struct io *io, struct status *status, enum line_type type)
433 switch (type) {
434 case LINE_STAT_STAGED:
435 return io_printf(io, "%06o %s\t%s%c", status->old.mode,
436 status->old.rev, status->old.name, 0);
438 case LINE_STAT_UNSTAGED:
439 case LINE_STAT_UNTRACKED:
440 return io_printf(io, "%s%c", status->new.name, 0);
442 default:
443 die("line type %d not handled in switch", type);
444 return FALSE;
448 bool
449 status_update_file(struct status *status, enum line_type type)
451 const char *name = status->new.name;
452 struct io io;
453 bool result;
455 if (type == LINE_STAT_UNTRACKED && !suffixcmp(name, strlen(name), "/")) {
456 const char *add_argv[] = { "git", "add", "--", name, NULL };
458 return io_run_bg(add_argv);
461 if (!status_update_prepare(&io, type))
462 return FALSE;
464 result = status_update_write(&io, status, type);
465 return io_done(&io) && result;
468 bool
469 status_update_files(struct view *view, struct line *line)
471 char buf[sizeof(view->ref)];
472 struct io io;
473 bool result = TRUE;
474 struct line *pos;
475 int files = 0;
476 int file, done;
477 int cursor_y = -1, cursor_x = -1;
479 if (!status_update_prepare(&io, line->type))
480 return FALSE;
482 for (pos = line; view_has_line(view, pos) && pos->data; pos++)
483 files++;
485 string_copy(buf, view->ref);
486 getsyx(cursor_y, cursor_x);
487 for (file = 0, done = 5; result && file < files; line++, file++) {
488 int almost_done = file * 100 / files;
490 if (almost_done > done) {
491 done = almost_done;
492 string_format(view->ref, "updating file %u of %u (%d%% done)",
493 file, files, done);
494 update_view_title(view);
495 setsyx(cursor_y, cursor_x);
496 doupdate();
498 result = status_update_write(&io, line->data, line->type);
500 string_copy(view->ref, buf);
502 return io_done(&io) && result;
505 static bool
506 status_update(struct view *view)
508 struct line *line = &view->line[view->pos.lineno];
510 assert(view->lines);
512 if (!line->data) {
513 if (status_has_none(view, line)) {
514 report("Nothing to update");
515 return FALSE;
518 if (!status_update_files(view, line + 1)) {
519 report("Failed to update file status");
520 return FALSE;
523 } else if (!status_update_file(line->data, line->type)) {
524 report("Failed to update file status");
525 return FALSE;
528 return TRUE;
531 bool
532 status_revert(struct status *status, enum line_type type, bool has_none)
534 if (!status || type != LINE_STAT_UNSTAGED) {
535 if (type == LINE_STAT_STAGED) {
536 report("Cannot revert changes to staged files");
537 } else if (type == LINE_STAT_UNTRACKED) {
538 report("Cannot revert changes to untracked files");
539 } else if (has_none) {
540 report("Nothing to revert");
541 } else {
542 report("Cannot revert changes to multiple files");
545 } else if (prompt_yesno("Are you sure you want to revert changes?")) {
546 char mode[10] = "100644";
547 const char *reset_argv[] = {
548 "git", "update-index", "--cacheinfo", mode,
549 status->old.rev, status->old.name, NULL
551 const char *checkout_argv[] = {
552 "git", "checkout", "--", status->old.name, NULL
555 if (status->status == 'U') {
556 string_format(mode, "%5o", status->old.mode);
558 if (status->old.mode == 0 && status->new.mode == 0) {
559 reset_argv[2] = "--force-remove";
560 reset_argv[3] = status->old.name;
561 reset_argv[4] = NULL;
564 if (!io_run_fg(reset_argv, repo.cdup))
565 return FALSE;
566 if (status->old.mode == 0 && status->new.mode == 0)
567 return TRUE;
570 return io_run_fg(checkout_argv, repo.cdup);
573 return FALSE;
576 static void
577 open_mergetool(const char *file)
579 const char *mergetool_argv[] = { "git", "mergetool", file, NULL };
581 open_external_viewer(mergetool_argv, repo.cdup, TRUE, TRUE, "");
584 static enum request
585 status_request(struct view *view, enum request request, struct line *line)
587 struct status *status = line->data;
589 switch (request) {
590 case REQ_STATUS_UPDATE:
591 if (!status_update(view))
592 return REQ_NONE;
593 break;
595 case REQ_STATUS_REVERT:
596 if (!status_revert(status, line->type, status_has_none(view, line)))
597 return REQ_NONE;
598 break;
600 case REQ_STATUS_MERGE:
601 if (!status || status->status != 'U') {
602 report("Merging only possible for files with unmerged status ('U').");
603 return REQ_NONE;
605 open_mergetool(status->new.name);
606 break;
608 case REQ_EDIT:
609 if (!status)
610 return request;
611 if (status->status == 'D') {
612 report("File has been deleted.");
613 return REQ_NONE;
616 open_editor(status->new.name, 0);
617 break;
619 case REQ_VIEW_BLAME:
620 if (line->type == LINE_STAT_UNTRACKED || !status) {
621 report("Nothing to blame here");
622 return REQ_NONE;
624 if (status)
625 view->env->ref[0] = 0;
626 return request;
628 case REQ_ENTER:
629 /* After returning the status view has been split to
630 * show the stage view. No further reloading is
631 * necessary. */
632 return status_enter(view, line);
634 case REQ_REFRESH:
635 /* Load the current branch information and then the view. */
636 load_refs(TRUE);
637 break;
639 default:
640 return request;
643 refresh_view(view);
645 return REQ_NONE;
648 bool
649 status_stage_info_(char *buf, size_t bufsize,
650 enum line_type type, struct status *status)
652 const char *file = status ? status->new.name : "";
653 const char *info;
655 switch (type) {
656 case LINE_STAT_STAGED:
657 if (status && status->status)
658 info = "Staged changes to %s";
659 else
660 info = "Staged changes";
661 break;
663 case LINE_STAT_UNSTAGED:
664 if (status && status->status)
665 info = "Unstaged changes to %s";
666 else
667 info = "Unstaged changes";
668 break;
670 case LINE_STAT_UNTRACKED:
671 info = "Untracked file %s";
672 break;
674 case LINE_HEADER:
675 default:
676 info = "";
679 return string_nformat(buf, bufsize, NULL, info, file);
682 static void
683 status_select(struct view *view, struct line *line)
685 struct status *status = line->data;
686 char file[SIZEOF_STR] = "all files";
687 const char *text;
688 const char *key;
690 if (status && !string_format(file, "'%s'", status->new.name))
691 return;
693 if (!status && line[1].type == LINE_STAT_NONE)
694 line++;
696 switch (line->type) {
697 case LINE_STAT_STAGED:
698 text = "Press %s to unstage %s for commit";
699 break;
701 case LINE_STAT_UNSTAGED:
702 text = "Press %s to stage %s for commit";
703 break;
705 case LINE_STAT_UNTRACKED:
706 text = "Press %s to stage %s for addition";
707 break;
709 default:
710 text = "Nothing to update";
713 if (status && status->status == 'U') {
714 text = "Press %s to resolve conflict in %s";
715 key = get_view_key(view, REQ_STATUS_MERGE);
717 } else {
718 key = get_view_key(view, REQ_STATUS_UPDATE);
721 string_format(view->ref, text, key, file);
722 status_stage_info(view->env->status, line->type, status);
723 if (status)
724 string_copy(view->env->file, status->new.name);
727 static struct view_ops status_ops = {
728 "file",
730 VIEW_CUSTOM_STATUS | VIEW_SEND_CHILD_ENTER | VIEW_STATUS_LIKE | VIEW_REFRESH,
732 status_open,
733 NULL,
734 view_column_draw,
735 status_request,
736 view_column_grep,
737 status_select,
738 NULL,
739 view_column_bit(FILE_NAME) | view_column_bit(LINE_NUMBER) |
740 view_column_bit(STATUS),
741 status_get_column_data,
744 DEFINE_VIEW(status);
746 /* vim: set ts=8 sw=8 noexpandtab: */