Fix and improve status on-branch information
[tig.git] / src / status.c
blob54cc0194e7565465548b437f596304b0fb0bafe8
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[][3] = {
205 { "rebase-apply/rebasing", "rebase-apply/head-name", "Rebasing" },
206 { "rebase-apply/applying", "rebase-apply/head-name", "Applying mailbox to" },
207 { "rebase-apply/", "rebase-apply/head-name", "Rebasing mailbox onto" },
208 { "rebase-merge/interactive", "rebase-merge/head-name", "Interactive rebase" },
209 { "rebase-merge/", "rebase-merge/head-name", "Rebase merge" },
210 { "MERGE_HEAD", NULL, "Merging" },
211 { "BISECT_LOG", NULL, "Bisecting" },
212 { "HEAD", NULL, "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 const char *prefix = paths[i][2];
225 char *head = repo.head;
227 if (!string_format(buf, "%s/%s", repo.git_dir, paths[i][0]) ||
228 lstat(buf, &stat) < 0)
229 continue;
231 if (paths[i][1]) {
232 struct io io;
234 if (io_open(&io, "%s/%s", repo.git_dir, paths[i][1]) &&
235 io_read_buf(&io, buf, sizeof(buf))) {
236 head = buf;
237 if (!prefixcmp(head, "refs/heads/"))
238 head += STRING_SIZE("refs/heads/");
242 if (!strcmp(head, "HEAD") && !strcmp(paths[i][0], "HEAD") && *repo.head_id) {
243 prefix = "On detached head";
244 head = repo.head_id;
247 if (!string_format(status_onbranch, "%s %s", prefix, head))
248 string_copy(status_onbranch, repo.head);
249 return;
252 string_copy(status_onbranch, "Not currently on any branch");
255 /* First parse staged info using git-diff-index(1), then parse unstaged
256 * info using git-diff-files(1), and finally untracked files using
257 * git-ls-files(1). */
258 static bool
259 status_open(struct view *view, enum open_flags flags)
261 const char **staged_argv = is_initial_commit() ?
262 status_list_no_head_argv : status_diff_index_argv;
263 char staged_status = staged_argv == status_list_no_head_argv ? 'A' : 0;
265 if (repo.is_inside_work_tree == FALSE) {
266 report("The status view requires a working tree");
267 return FALSE;
270 reset_view(view);
272 /* FIXME: Watch untracked files and on-branch info. */
273 watch_register(&view->watch, WATCH_INDEX);
275 add_line_nodata(view, LINE_HEADER);
276 status_update_onbranch();
278 update_index();
280 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 3] =
281 opt_status_untracked_dirs ? NULL : "--directory";
282 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 2] =
283 opt_status_untracked_dirs ? NULL : "--no-empty-directory";
285 if (!status_run(view, staged_argv, staged_status, LINE_STAT_STAGED) ||
286 !status_run(view, status_diff_files_argv, 0, LINE_STAT_UNSTAGED) ||
287 !status_run(view, status_list_other_argv, '?', LINE_STAT_UNTRACKED)) {
288 report("Failed to load status data");
289 return FALSE;
292 /* Restore the exact position or use the specialized restore
293 * mode? */
294 status_restore(view);
295 return TRUE;
298 static bool
299 status_get_column_data(struct view *view, const struct line *line, struct view_column_data *column_data)
301 struct status *status = line->data;
303 if (!status) {
304 static struct view_column group_column;
305 const char *text;
306 enum line_type type;
308 column_data->section = &group_column;
309 column_data->section->type = VIEW_COLUMN_SECTION;
311 switch (line->type) {
312 case LINE_STAT_STAGED:
313 type = LINE_SECTION;
314 text = "Changes to be committed:";
315 break;
317 case LINE_STAT_UNSTAGED:
318 type = LINE_SECTION;
319 text = "Changed but not updated:";
320 break;
322 case LINE_STAT_UNTRACKED:
323 type = LINE_SECTION;
324 text = "Untracked files:";
325 break;
327 case LINE_STAT_NONE:
328 type = LINE_DEFAULT;
329 text = " (no files)";
330 break;
332 case LINE_HEADER:
333 type = LINE_HEADER;
334 text = status_onbranch;
335 break;
337 default:
338 return FALSE;
341 column_data->section->opt.section.text = text;
342 column_data->section->opt.section.type = type;
344 } else {
345 column_data->status = &status->status;
346 column_data->file_name = status->new.name;
348 return TRUE;
351 static enum request
352 status_enter(struct view *view, struct line *line)
354 struct status *status = line->data;
355 enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT;
357 if (line->type == LINE_STAT_NONE ||
358 (!status && line[1].type == LINE_STAT_NONE)) {
359 report("No file to diff");
360 return REQ_NONE;
363 switch (line->type) {
364 case LINE_STAT_STAGED:
365 case LINE_STAT_UNSTAGED:
366 break;
368 case LINE_STAT_UNTRACKED:
369 if (!status) {
370 report("No file to show");
371 return REQ_NONE;
374 if (!suffixcmp(status->new.name, -1, "/")) {
375 report("Cannot display a directory");
376 return REQ_NONE;
378 break;
380 default:
381 report("Nothing to enter");
382 return REQ_NONE;
385 open_stage_view(view, status, line->type, flags);
386 return REQ_NONE;
389 bool
390 status_exists(struct view *view, struct status *status, enum line_type type)
392 unsigned long lineno;
394 for (lineno = 0; lineno < view->lines; lineno++) {
395 struct line *line = &view->line[lineno];
396 struct status *pos = line->data;
398 if (line->type != type)
399 continue;
400 if ((!pos && (!status || !status->status) && line[1].data) ||
401 (pos && !strcmp(status->new.name, pos->new.name))) {
402 select_view_line(view, lineno);
403 status_restore(view);
404 return TRUE;
408 return FALSE;
412 static bool
413 status_update_prepare(struct io *io, enum line_type type)
415 const char *staged_argv[] = {
416 "git", "update-index", "-z", "--index-info", NULL
418 const char *others_argv[] = {
419 "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL
422 switch (type) {
423 case LINE_STAT_STAGED:
424 return io_run(io, IO_WR, repo.cdup, opt_env, staged_argv);
426 case LINE_STAT_UNSTAGED:
427 case LINE_STAT_UNTRACKED:
428 return io_run(io, IO_WR, repo.cdup, opt_env, others_argv);
430 default:
431 die("line type %d not handled in switch", type);
432 return FALSE;
436 static bool
437 status_update_write(struct io *io, struct status *status, enum line_type type)
439 switch (type) {
440 case LINE_STAT_STAGED:
441 return io_printf(io, "%06o %s\t%s%c", status->old.mode,
442 status->old.rev, status->old.name, 0);
444 case LINE_STAT_UNSTAGED:
445 case LINE_STAT_UNTRACKED:
446 return io_printf(io, "%s%c", status->new.name, 0);
448 default:
449 die("line type %d not handled in switch", type);
450 return FALSE;
454 bool
455 status_update_file(struct status *status, enum line_type type)
457 const char *name = status->new.name;
458 struct io io;
459 bool result;
461 if (type == LINE_STAT_UNTRACKED && !suffixcmp(name, strlen(name), "/")) {
462 const char *add_argv[] = { "git", "add", "--", name, NULL };
464 return io_run_bg(add_argv);
467 if (!status_update_prepare(&io, type))
468 return FALSE;
470 result = status_update_write(&io, status, type);
471 return io_done(&io) && result;
474 bool
475 status_update_files(struct view *view, struct line *line)
477 char buf[sizeof(view->ref)];
478 struct io io;
479 bool result = TRUE;
480 struct line *pos;
481 int files = 0;
482 int file, done;
483 int cursor_y = -1, cursor_x = -1;
485 if (!status_update_prepare(&io, line->type))
486 return FALSE;
488 for (pos = line; view_has_line(view, pos) && pos->data; pos++)
489 files++;
491 string_copy(buf, view->ref);
492 getsyx(cursor_y, cursor_x);
493 for (file = 0, done = 5; result && file < files; line++, file++) {
494 int almost_done = file * 100 / files;
496 if (almost_done > done) {
497 done = almost_done;
498 string_format(view->ref, "updating file %u of %u (%d%% done)",
499 file, files, done);
500 update_view_title(view);
501 setsyx(cursor_y, cursor_x);
502 doupdate();
504 result = status_update_write(&io, line->data, line->type);
506 string_copy(view->ref, buf);
508 return io_done(&io) && result;
511 static bool
512 status_update(struct view *view)
514 struct line *line = &view->line[view->pos.lineno];
516 assert(view->lines);
518 if (!line->data) {
519 if (status_has_none(view, line)) {
520 report("Nothing to update");
521 return FALSE;
524 if (!status_update_files(view, line + 1)) {
525 report("Failed to update file status");
526 return FALSE;
529 } else if (!status_update_file(line->data, line->type)) {
530 report("Failed to update file status");
531 return FALSE;
534 return TRUE;
537 bool
538 status_revert(struct status *status, enum line_type type, bool has_none)
540 if (!status || type != LINE_STAT_UNSTAGED) {
541 if (type == LINE_STAT_STAGED) {
542 report("Cannot revert changes to staged files");
543 } else if (type == LINE_STAT_UNTRACKED) {
544 report("Cannot revert changes to untracked files");
545 } else if (has_none) {
546 report("Nothing to revert");
547 } else {
548 report("Cannot revert changes to multiple files");
551 } else if (prompt_yesno("Are you sure you want to revert changes?")) {
552 char mode[10] = "100644";
553 const char *reset_argv[] = {
554 "git", "update-index", "--cacheinfo", mode,
555 status->old.rev, status->old.name, NULL
557 const char *checkout_argv[] = {
558 "git", "checkout", "--", status->old.name, NULL
561 if (status->status == 'U') {
562 string_format(mode, "%5o", status->old.mode);
564 if (status->old.mode == 0 && status->new.mode == 0) {
565 reset_argv[2] = "--force-remove";
566 reset_argv[3] = status->old.name;
567 reset_argv[4] = NULL;
570 if (!io_run_fg(reset_argv, repo.cdup))
571 return FALSE;
572 if (status->old.mode == 0 && status->new.mode == 0)
573 return TRUE;
576 return io_run_fg(checkout_argv, repo.cdup);
579 return FALSE;
582 static void
583 open_mergetool(const char *file)
585 const char *mergetool_argv[] = { "git", "mergetool", file, NULL };
587 open_external_viewer(mergetool_argv, repo.cdup, FALSE, TRUE, TRUE, "");
590 static enum request
591 status_request(struct view *view, enum request request, struct line *line)
593 struct status *status = line->data;
595 switch (request) {
596 case REQ_STATUS_UPDATE:
597 if (!status_update(view))
598 return REQ_NONE;
599 break;
601 case REQ_STATUS_REVERT:
602 if (!status_revert(status, line->type, status_has_none(view, line)))
603 return REQ_NONE;
604 break;
606 case REQ_STATUS_MERGE:
607 if (!status || status->status != 'U') {
608 report("Merging only possible for files with unmerged status ('U').");
609 return REQ_NONE;
611 open_mergetool(status->new.name);
612 break;
614 case REQ_EDIT:
615 if (!status)
616 return request;
617 if (status->status == 'D') {
618 report("File has been deleted.");
619 return REQ_NONE;
622 open_editor(status->new.name, 0);
623 break;
625 case REQ_VIEW_BLAME:
626 if (line->type == LINE_STAT_UNTRACKED || !status) {
627 report("Nothing to blame here");
628 return REQ_NONE;
630 if (status)
631 view->env->ref[0] = 0;
632 return request;
634 case REQ_ENTER:
635 /* After returning the status view has been split to
636 * show the stage view. No further reloading is
637 * necessary. */
638 return status_enter(view, line);
640 case REQ_REFRESH:
641 /* Load the current branch information and then the view. */
642 load_repo_head();
643 break;
645 default:
646 return request;
649 refresh_view(view);
651 return REQ_NONE;
654 bool
655 status_stage_info_(char *buf, size_t bufsize,
656 enum line_type type, struct status *status)
658 const char *file = status ? status->new.name : "";
659 const char *info;
661 switch (type) {
662 case LINE_STAT_STAGED:
663 if (status && status->status)
664 info = "Staged changes to %s";
665 else
666 info = "Staged changes";
667 break;
669 case LINE_STAT_UNSTAGED:
670 if (status && status->status)
671 info = "Unstaged changes to %s";
672 else
673 info = "Unstaged changes";
674 break;
676 case LINE_STAT_UNTRACKED:
677 info = "Untracked file %s";
678 break;
680 case LINE_HEADER:
681 default:
682 info = "";
685 return string_nformat(buf, bufsize, NULL, info, file);
688 static void
689 status_select(struct view *view, struct line *line)
691 struct status *status = line->data;
692 char file[SIZEOF_STR] = "all files";
693 const char *text;
694 const char *key;
696 if (status && !string_format(file, "'%s'", status->new.name))
697 return;
699 if (!status && line[1].type == LINE_STAT_NONE)
700 line++;
702 switch (line->type) {
703 case LINE_STAT_STAGED:
704 text = "Press %s to unstage %s for commit";
705 break;
707 case LINE_STAT_UNSTAGED:
708 text = "Press %s to stage %s for commit";
709 break;
711 case LINE_STAT_UNTRACKED:
712 text = "Press %s to stage %s for addition";
713 break;
715 default:
716 text = "Nothing to update";
719 if (status && status->status == 'U') {
720 text = "Press %s to resolve conflict in %s";
721 key = get_view_key(view, REQ_STATUS_MERGE);
723 } else {
724 key = get_view_key(view, REQ_STATUS_UPDATE);
727 string_format(view->ref, text, key, file);
728 status_stage_info(view->env->status, line->type, status);
729 if (status)
730 string_copy(view->env->file, status->new.name);
733 static struct view_ops status_ops = {
734 "file",
736 VIEW_CUSTOM_STATUS | VIEW_SEND_CHILD_ENTER | VIEW_STATUS_LIKE | VIEW_REFRESH,
738 status_open,
739 NULL,
740 view_column_draw,
741 status_request,
742 view_column_grep,
743 status_select,
744 NULL,
745 view_column_bit(FILE_NAME) | view_column_bit(LINE_NUMBER) |
746 view_column_bit(STATUS),
747 status_get_column_data,
750 DEFINE_VIEW(status);
752 /* vim: set ts=8 sw=8 noexpandtab: */