Introduce common section color
[tig.git] / src / status.c
blob62502badeeca1a73f30f32ff2cc57c743853d50e
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/status.h"
25 #include "tig/stage.h"
28 * Status backend
31 static char status_onbranch[SIZEOF_STR];
32 struct status stage_status;
33 enum line_type stage_line_type;
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 char *buf;
80 struct io io;
82 if (!io_run(&io, IO_RD, repo.cdup, opt_env, argv))
83 return FALSE;
85 add_line_nodata(view, type);
87 while ((buf = io_get(&io, 0, TRUE))) {
88 struct line *line = NULL;
89 struct status *file = unmerged;
91 if (!file) {
92 line = add_line_alloc(view, &file, type, 0, FALSE);
93 if (!line)
94 goto error_out;
97 /* Parse diff info part. */
98 if (status) {
99 file->status = status;
100 if (status == 'A')
101 string_copy(file->old.rev, NULL_ID);
103 } else if (!file->status || file == unmerged) {
104 if (!status_get_diff(file, buf, strlen(buf)))
105 goto error_out;
107 buf = io_get(&io, 0, TRUE);
108 if (!buf)
109 break;
111 /* Collapse all modified entries that follow an
112 * associated unmerged entry. */
113 if (unmerged == file) {
114 unmerged->status = 'U';
115 unmerged = NULL;
116 } else if (file->status == 'U') {
117 unmerged = file;
121 /* Grab the old name for rename/copy. */
122 if (!*file->old.name &&
123 (file->status == 'R' || file->status == 'C')) {
124 string_ncopy(file->old.name, buf, strlen(buf));
126 buf = io_get(&io, 0, TRUE);
127 if (!buf)
128 break;
131 /* git-ls-files just delivers a NUL separated list of
132 * file names similar to the second half of the
133 * git-diff-* output. */
134 string_ncopy(file->new.name, buf, strlen(buf));
135 if (!*file->old.name)
136 string_copy(file->old.name, file->new.name);
137 file = NULL;
138 if (line)
139 view_column_info_update(view, line);
142 if (io_error(&io)) {
143 error_out:
144 io_done(&io);
145 return FALSE;
148 if (!view->line[view->lines - 1].data)
149 add_line_nodata(view, LINE_STAT_NONE);
151 io_done(&io);
152 return TRUE;
155 static const char *status_diff_index_argv[] = { GIT_DIFF_STAGED_FILES("-z") };
156 static const char *status_diff_files_argv[] = { GIT_DIFF_UNSTAGED_FILES("-z") };
158 static const char *status_list_other_argv[] = {
159 "git", "ls-files", "-z", "--others", "--exclude-standard", repo.prefix, NULL, NULL, NULL
162 static const char *status_list_no_head_argv[] = {
163 "git", "ls-files", "-z", "--cached", "--exclude-standard", NULL
166 const char *update_index_argv[] = {
167 "git", "update-index", "-q", "--unmerged", "--refresh", NULL
170 /* Restore the previous line number to stay in the context or select a
171 * line with something that can be updated. */
172 void
173 status_restore(struct view *view)
175 if (!check_position(&view->prev_pos))
176 return;
178 if (view->prev_pos.lineno >= view->lines)
179 view->prev_pos.lineno = view->lines - 1;
180 while (view->prev_pos.lineno < view->lines && !view->line[view->prev_pos.lineno].data)
181 view->prev_pos.lineno++;
182 while (view->prev_pos.lineno > 0 && !view->line[view->prev_pos.lineno].data)
183 view->prev_pos.lineno--;
185 /* If the above fails, always skip the "On branch" line. */
186 if (view->prev_pos.lineno < view->lines)
187 view->pos.lineno = view->prev_pos.lineno;
188 else
189 view->pos.lineno = 1;
191 if (view->prev_pos.offset > view->pos.lineno)
192 view->pos.offset = view->pos.lineno;
193 else if (view->prev_pos.offset < view->lines)
194 view->pos.offset = view->prev_pos.offset;
196 clear_position(&view->prev_pos);
199 static void
200 status_update_onbranch(void)
202 static const char *paths[][2] = {
203 { "rebase-apply/rebasing", "Rebasing" },
204 { "rebase-apply/applying", "Applying mailbox" },
205 { "rebase-apply/", "Rebasing mailbox" },
206 { "rebase-merge/interactive", "Interactive rebase" },
207 { "rebase-merge/", "Rebase merge" },
208 { "MERGE_HEAD", "Merging" },
209 { "BISECT_LOG", "Bisecting" },
210 { "HEAD", "On branch" },
212 char buf[SIZEOF_STR];
213 struct stat stat;
214 int i;
216 if (is_initial_commit()) {
217 string_copy(status_onbranch, "Initial commit");
218 return;
221 for (i = 0; i < ARRAY_SIZE(paths); i++) {
222 char *head = repo.head;
224 if (!string_format(buf, "%s/%s", repo.git_dir, paths[i][0]) ||
225 lstat(buf, &stat) < 0)
226 continue;
228 if (!*repo.head) {
229 struct io io;
231 if (io_open(&io, "%s/rebase-merge/head-name", repo.git_dir) &&
232 io_read_buf(&io, buf, sizeof(buf))) {
233 head = buf;
234 if (!prefixcmp(head, "refs/heads/"))
235 head += STRING_SIZE("refs/heads/");
239 if (!string_format(status_onbranch, "%s %s", paths[i][1], head))
240 string_copy(status_onbranch, repo.head);
241 return;
244 string_copy(status_onbranch, "Not currently on any branch");
247 /* First parse staged info using git-diff-index(1), then parse unstaged
248 * info using git-diff-files(1), and finally untracked files using
249 * git-ls-files(1). */
250 static bool
251 status_open(struct view *view, enum open_flags flags)
253 const char **staged_argv = is_initial_commit() ?
254 status_list_no_head_argv : status_diff_index_argv;
255 char staged_status = staged_argv == status_list_no_head_argv ? 'A' : 0;
257 if (repo.is_inside_work_tree == FALSE) {
258 report("The status view requires a working tree");
259 return FALSE;
262 reset_view(view);
264 add_line_nodata(view, LINE_HEADER);
265 status_update_onbranch();
267 io_run_bg(update_index_argv);
269 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 3] =
270 opt_status_untracked_dirs ? NULL : "--directory";
271 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 2] =
272 opt_status_untracked_dirs ? NULL : "--no-empty-directory";
274 if (!status_run(view, staged_argv, staged_status, LINE_STAT_STAGED) ||
275 !status_run(view, status_diff_files_argv, 0, LINE_STAT_UNSTAGED) ||
276 !status_run(view, status_list_other_argv, '?', LINE_STAT_UNTRACKED)) {
277 report("Failed to load status data");
278 return FALSE;
281 /* Restore the exact position or use the specialized restore
282 * mode? */
283 status_restore(view);
284 return TRUE;
287 static bool
288 status_get_column_data(struct view *view, const struct line *line, struct view_column_data *column_data)
290 struct status *status = line->data;
292 if (!status) {
293 static struct view_column group_column;
294 const char *text;
295 enum line_type type;
297 column_data->section = &group_column;
298 column_data->section->type = VIEW_COLUMN_SECTION;
300 switch (line->type) {
301 case LINE_STAT_STAGED:
302 type = LINE_SECTION;
303 text = "Changes to be committed:";
304 break;
306 case LINE_STAT_UNSTAGED:
307 type = LINE_SECTION;
308 text = "Changed but not updated:";
309 break;
311 case LINE_STAT_UNTRACKED:
312 type = LINE_SECTION;
313 text = "Untracked files:";
314 break;
316 case LINE_STAT_NONE:
317 type = LINE_DEFAULT;
318 text = " (no files)";
319 break;
321 case LINE_HEADER:
322 type = LINE_HEADER;
323 text = status_onbranch;
324 break;
326 default:
327 return FALSE;
330 column_data->section->opt.section.text = text;
331 column_data->section->opt.section.type = type;
333 } else {
334 column_data->status = &status->status;
335 column_data->file_name = status->new.name;
337 return TRUE;
340 static enum request
341 status_enter(struct view *view, struct line *line)
343 struct status *status = line->data;
344 enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT;
346 if (line->type == LINE_STAT_NONE ||
347 (!status && line[1].type == LINE_STAT_NONE)) {
348 report("No file to diff");
349 return REQ_NONE;
352 switch (line->type) {
353 case LINE_STAT_STAGED:
354 case LINE_STAT_UNSTAGED:
355 break;
357 case LINE_STAT_UNTRACKED:
358 if (!status) {
359 report("No file to show");
360 return REQ_NONE;
363 if (!suffixcmp(status->new.name, -1, "/")) {
364 report("Cannot display a directory");
365 return REQ_NONE;
367 break;
369 default:
370 report("Nothing to enter");
371 return REQ_NONE;
374 if (status) {
375 stage_status = *status;
376 } else {
377 memset(&stage_status, 0, sizeof(stage_status));
380 stage_line_type = line->type;
382 open_stage_view(view, flags);
383 return REQ_NONE;
386 bool
387 status_exists(struct view *view, struct status *status, enum line_type type)
389 unsigned long lineno;
391 for (lineno = 0; lineno < view->lines; lineno++) {
392 struct line *line = &view->line[lineno];
393 struct status *pos = line->data;
395 if (line->type != type)
396 continue;
397 if (!pos && (!status || !status->status) && line[1].data) {
398 select_view_line(view, lineno);
399 return TRUE;
401 if (pos && !strcmp(status->new.name, pos->new.name)) {
402 select_view_line(view, lineno);
403 return TRUE;
407 return FALSE;
411 static bool
412 status_update_prepare(struct io *io, enum line_type type)
414 const char *staged_argv[] = {
415 "git", "update-index", "-z", "--index-info", NULL
417 const char *others_argv[] = {
418 "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL
421 switch (type) {
422 case LINE_STAT_STAGED:
423 return io_run(io, IO_WR, repo.cdup, opt_env, staged_argv);
425 case LINE_STAT_UNSTAGED:
426 case LINE_STAT_UNTRACKED:
427 return io_run(io, IO_WR, repo.cdup, opt_env, others_argv);
429 default:
430 die("line type %d not handled in switch", type);
431 return FALSE;
435 static bool
436 status_update_write(struct io *io, struct status *status, enum line_type type)
438 switch (type) {
439 case LINE_STAT_STAGED:
440 return io_printf(io, "%06o %s\t%s%c", status->old.mode,
441 status->old.rev, status->old.name, 0);
443 case LINE_STAT_UNSTAGED:
444 case LINE_STAT_UNTRACKED:
445 return io_printf(io, "%s%c", status->new.name, 0);
447 default:
448 die("line type %d not handled in switch", type);
449 return FALSE;
453 bool
454 status_update_file(struct status *status, enum line_type type)
456 const char *name = status->new.name;
457 struct io io;
458 bool result;
460 if (type == LINE_STAT_UNTRACKED && !suffixcmp(name, strlen(name), "/")) {
461 const char *add_argv[] = { "git", "add", "--", name, NULL };
463 return io_run_bg(add_argv);
466 if (!status_update_prepare(&io, type))
467 return FALSE;
469 result = status_update_write(&io, status, type);
470 return io_done(&io) && result;
473 bool
474 status_update_files(struct view *view, struct line *line)
476 char buf[sizeof(view->ref)];
477 struct io io;
478 bool result = TRUE;
479 struct line *pos;
480 int files = 0;
481 int file, done;
482 int cursor_y = -1, cursor_x = -1;
484 if (!status_update_prepare(&io, line->type))
485 return FALSE;
487 for (pos = line; view_has_line(view, pos) && pos->data; pos++)
488 files++;
490 string_copy(buf, view->ref);
491 getsyx(cursor_y, cursor_x);
492 for (file = 0, done = 5; result && file < files; line++, file++) {
493 int almost_done = file * 100 / files;
495 if (almost_done > done) {
496 done = almost_done;
497 string_format(view->ref, "updating file %u of %u (%d%% done)",
498 file, files, done);
499 update_view_title(view);
500 setsyx(cursor_y, cursor_x);
501 doupdate();
503 result = status_update_write(&io, line->data, line->type);
505 string_copy(view->ref, buf);
507 return io_done(&io) && result;
510 static bool
511 status_update(struct view *view)
513 struct line *line = &view->line[view->pos.lineno];
515 assert(view->lines);
517 if (!line->data) {
518 if (status_has_none(view, line)) {
519 report("Nothing to update");
520 return FALSE;
523 if (!status_update_files(view, line + 1)) {
524 report("Failed to update file status");
525 return FALSE;
528 } else if (!status_update_file(line->data, line->type)) {
529 report("Failed to update file status");
530 return FALSE;
533 return TRUE;
536 bool
537 status_revert(struct status *status, enum line_type type, bool has_none)
539 if (!status || type != LINE_STAT_UNSTAGED) {
540 if (type == LINE_STAT_STAGED) {
541 report("Cannot revert changes to staged files");
542 } else if (type == LINE_STAT_UNTRACKED) {
543 report("Cannot revert changes to untracked files");
544 } else if (has_none) {
545 report("Nothing to revert");
546 } else {
547 report("Cannot revert changes to multiple files");
550 } else if (prompt_yesno("Are you sure you want to revert changes?")) {
551 char mode[10] = "100644";
552 const char *reset_argv[] = {
553 "git", "update-index", "--cacheinfo", mode,
554 status->old.rev, status->old.name, NULL
556 const char *checkout_argv[] = {
557 "git", "checkout", "--", status->old.name, NULL
560 if (status->status == 'U') {
561 string_format(mode, "%5o", status->old.mode);
563 if (status->old.mode == 0 && status->new.mode == 0) {
564 reset_argv[2] = "--force-remove";
565 reset_argv[3] = status->old.name;
566 reset_argv[4] = NULL;
569 if (!io_run_fg(reset_argv, repo.cdup))
570 return FALSE;
571 if (status->old.mode == 0 && status->new.mode == 0)
572 return TRUE;
575 return io_run_fg(checkout_argv, repo.cdup);
578 return FALSE;
581 static void
582 open_mergetool(const char *file)
584 const char *mergetool_argv[] = { "git", "mergetool", file, NULL };
586 open_external_viewer(mergetool_argv, repo.cdup, TRUE, "");
589 static enum request
590 status_request(struct view *view, enum request request, struct line *line)
592 struct status *status = line->data;
594 switch (request) {
595 case REQ_STATUS_UPDATE:
596 if (!status_update(view))
597 return REQ_NONE;
598 break;
600 case REQ_STATUS_REVERT:
601 if (!status_revert(status, line->type, status_has_none(view, line)))
602 return REQ_NONE;
603 break;
605 case REQ_STATUS_MERGE:
606 if (!status || status->status != 'U') {
607 report("Merging only possible for files with unmerged status ('U').");
608 return REQ_NONE;
610 open_mergetool(status->new.name);
611 break;
613 case REQ_EDIT:
614 if (!status)
615 return request;
616 if (status->status == 'D') {
617 report("File has been deleted.");
618 return REQ_NONE;
621 open_editor(status->new.name, 0);
622 break;
624 case REQ_VIEW_BLAME:
625 if (line->type == LINE_STAT_UNTRACKED || !status) {
626 report("Nothing to blame here");
627 return REQ_NONE;
629 if (status)
630 view->env->ref[0] = 0;
631 return request;
633 case REQ_ENTER:
634 /* After returning the status view has been split to
635 * show the stage view. No further reloading is
636 * necessary. */
637 return status_enter(view, line);
639 case REQ_REFRESH:
640 /* Load the current branch information and then the view. */
641 load_refs(TRUE);
642 break;
644 default:
645 return request;
648 refresh_view(view);
650 return REQ_NONE;
653 bool
654 status_stage_info_(char *buf, size_t bufsize,
655 enum line_type type, struct status *status)
657 const char *file = status ? status->new.name : "";
658 const char *info;
660 switch (type) {
661 case LINE_STAT_STAGED:
662 if (status && status->status)
663 info = "Staged changes to %s";
664 else
665 info = "Staged changes";
666 break;
668 case LINE_STAT_UNSTAGED:
669 if (status && status->status)
670 info = "Unstaged changes to %s";
671 else
672 info = "Unstaged changes";
673 break;
675 case LINE_STAT_UNTRACKED:
676 info = "Untracked file %s";
677 break;
679 case LINE_HEADER:
680 default:
681 info = "";
684 return string_nformat(buf, bufsize, NULL, info, file);
687 static void
688 status_select(struct view *view, struct line *line)
690 struct status *status = line->data;
691 char file[SIZEOF_STR] = "all files";
692 const char *text;
693 const char *key;
695 if (status && !string_format(file, "'%s'", status->new.name))
696 return;
698 if (!status && line[1].type == LINE_STAT_NONE)
699 line++;
701 switch (line->type) {
702 case LINE_STAT_STAGED:
703 text = "Press %s to unstage %s for commit";
704 break;
706 case LINE_STAT_UNSTAGED:
707 text = "Press %s to stage %s for commit";
708 break;
710 case LINE_STAT_UNTRACKED:
711 text = "Press %s to stage %s for addition";
712 break;
714 default:
715 text = "Nothing to update";
718 if (status && status->status == 'U') {
719 text = "Press %s to resolve conflict in %s";
720 key = get_view_key(view, REQ_STATUS_MERGE);
722 } else {
723 key = get_view_key(view, REQ_STATUS_UPDATE);
726 string_format(view->ref, text, key, file);
727 status_stage_info(view->env->status, line->type, status);
728 if (status)
729 string_copy(view->env->file, status->new.name);
732 static struct view_ops status_ops = {
733 "file",
735 VIEW_CUSTOM_STATUS | VIEW_SEND_CHILD_ENTER | VIEW_STATUS_LIKE | VIEW_REFRESH,
737 status_open,
738 NULL,
739 view_column_draw,
740 status_request,
741 view_column_grep,
742 status_select,
743 NULL,
744 view_column_bit(FILE_NAME) | view_column_bit(LINE_NUMBER) |
745 view_column_bit(STATUS),
746 status_get_column_data,
749 DEFINE_VIEW(status);
751 /* vim: set ts=8 sw=8 noexpandtab: */