tig-2.1.1
[tig.git] / src / status.c
blob42c59a86efe1d1c5224e2e58882906b48e60bc68
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/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 = {0};
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", 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 const 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 (!*head && !strcmp(paths[i][0], "HEAD") && *repo.head_id) {
243 const struct ref *ref = get_canonical_ref(repo.head_id);
245 prefix = "HEAD detached at";
246 head = repo.head_id;
248 if (ref && strcmp(ref->name, "HEAD"))
249 head = ref->name;
252 if (!string_format(status_onbranch, "%s %s", prefix, head))
253 string_copy(status_onbranch, repo.head);
254 return;
257 string_copy(status_onbranch, "Not currently on any branch");
260 /* First parse staged info using git-diff-index(1), then parse unstaged
261 * info using git-diff-files(1), and finally untracked files using
262 * git-ls-files(1). */
263 static bool
264 status_open(struct view *view, enum open_flags flags)
266 const char **staged_argv = is_initial_commit() ?
267 status_list_no_head_argv : status_diff_index_argv;
268 char staged_status = staged_argv == status_list_no_head_argv ? 'A' : 0;
270 if (repo.is_inside_work_tree == FALSE) {
271 report("The status view requires a working tree");
272 return FALSE;
275 reset_view(view);
277 /* FIXME: Watch untracked files and on-branch info. */
278 watch_register(&view->watch, WATCH_INDEX);
280 add_line_nodata(view, LINE_HEADER);
281 status_update_onbranch();
283 update_index();
285 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 3] =
286 opt_status_untracked_dirs ? NULL : "--directory";
287 status_list_other_argv[ARRAY_SIZE(status_list_other_argv) - 2] =
288 opt_status_untracked_dirs ? NULL : "--no-empty-directory";
290 if (!status_run(view, staged_argv, staged_status, LINE_STAT_STAGED) ||
291 !status_run(view, status_diff_files_argv, 0, LINE_STAT_UNSTAGED) ||
292 !status_run(view, status_list_other_argv, '?', LINE_STAT_UNTRACKED)) {
293 report("Failed to load status data");
294 return FALSE;
297 /* Restore the exact position or use the specialized restore
298 * mode? */
299 status_restore(view);
300 return TRUE;
303 static bool
304 status_get_column_data(struct view *view, const struct line *line, struct view_column_data *column_data)
306 struct status *status = line->data;
308 if (!status) {
309 static struct view_column group_column;
310 const char *text;
311 enum line_type type;
313 column_data->section = &group_column;
314 column_data->section->type = VIEW_COLUMN_SECTION;
316 switch (line->type) {
317 case LINE_STAT_STAGED:
318 type = LINE_SECTION;
319 text = "Changes to be committed:";
320 break;
322 case LINE_STAT_UNSTAGED:
323 type = LINE_SECTION;
324 text = "Changed but not updated:";
325 break;
327 case LINE_STAT_UNTRACKED:
328 type = LINE_SECTION;
329 text = "Untracked files:";
330 break;
332 case LINE_STAT_NONE:
333 type = LINE_DEFAULT;
334 text = " (no files)";
335 break;
337 case LINE_HEADER:
338 type = LINE_HEADER;
339 text = status_onbranch;
340 break;
342 default:
343 return FALSE;
346 column_data->section->opt.section.text = text;
347 column_data->section->opt.section.type = type;
349 } else {
350 column_data->status = &status->status;
351 column_data->file_name = status->new.name;
353 return TRUE;
356 static enum request
357 status_enter(struct view *view, struct line *line)
359 struct status *status = line->data;
360 enum open_flags flags = view_is_displayed(view) ? OPEN_SPLIT : OPEN_DEFAULT;
362 if (line->type == LINE_STAT_NONE ||
363 (!status && line[1].type == LINE_STAT_NONE)) {
364 report("No file to diff");
365 return REQ_NONE;
368 switch (line->type) {
369 case LINE_STAT_STAGED:
370 case LINE_STAT_UNSTAGED:
371 break;
373 case LINE_STAT_UNTRACKED:
374 if (!status) {
375 report("No file to show");
376 return REQ_NONE;
379 if (!suffixcmp(status->new.name, -1, "/")) {
380 report("Cannot display a directory");
381 return REQ_NONE;
383 break;
385 default:
386 report("Nothing to enter");
387 return REQ_NONE;
390 open_stage_view(view, status, line->type, flags);
391 return REQ_NONE;
394 bool
395 status_exists(struct view *view, struct status *status, enum line_type type)
397 unsigned long lineno;
399 refresh_view(view);
401 for (lineno = 0; lineno < view->lines; lineno++) {
402 struct line *line = &view->line[lineno];
403 struct status *pos = line->data;
405 if (line->type != type)
406 continue;
407 if ((!pos && (!status || !status->status) && line[1].data) ||
408 (pos && !strcmp(status->new.name, pos->new.name))) {
409 select_view_line(view, lineno);
410 status_restore(view);
411 return TRUE;
415 return FALSE;
419 static bool
420 status_update_prepare(struct io *io, enum line_type type)
422 const char *staged_argv[] = {
423 "git", "update-index", "-z", "--index-info", NULL
425 const char *others_argv[] = {
426 "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL
429 switch (type) {
430 case LINE_STAT_STAGED:
431 return io_run(io, IO_WR, repo.cdup, opt_env, staged_argv);
433 case LINE_STAT_UNSTAGED:
434 case LINE_STAT_UNTRACKED:
435 return io_run(io, IO_WR, repo.cdup, opt_env, others_argv);
437 default:
438 die("line type %d not handled in switch", type);
439 return FALSE;
443 static bool
444 status_update_write(struct io *io, struct status *status, enum line_type type)
446 switch (type) {
447 case LINE_STAT_STAGED:
448 return io_printf(io, "%06o %s\t%s%c", status->old.mode,
449 status->old.rev, status->old.name, 0);
451 case LINE_STAT_UNSTAGED:
452 case LINE_STAT_UNTRACKED:
453 return io_printf(io, "%s%c", status->new.name, 0);
455 default:
456 die("line type %d not handled in switch", type);
457 return FALSE;
461 bool
462 status_update_file(struct status *status, enum line_type type)
464 const char *name = status->new.name;
465 struct io io;
466 bool result;
468 if (type == LINE_STAT_UNTRACKED && !suffixcmp(name, strlen(name), "/")) {
469 const char *add_argv[] = { "git", "add", "--", name, NULL };
471 return io_run_bg(add_argv);
474 if (!status_update_prepare(&io, type))
475 return FALSE;
477 result = status_update_write(&io, status, type);
478 return io_done(&io) && result;
481 bool
482 status_update_files(struct view *view, struct line *line)
484 char buf[sizeof(view->ref)];
485 struct io io;
486 bool result = TRUE;
487 struct line *pos;
488 int files = 0;
489 int file, done;
490 int cursor_y = -1, cursor_x = -1;
492 if (!status_update_prepare(&io, line->type))
493 return FALSE;
495 for (pos = line; view_has_line(view, pos) && pos->data; pos++)
496 files++;
498 string_copy(buf, view->ref);
499 getsyx(cursor_y, cursor_x);
500 for (file = 0, done = 5; result && file < files; line++, file++) {
501 int almost_done = file * 100 / files;
503 if (almost_done > done && view_is_displayed(view)) {
504 done = almost_done;
505 string_format(view->ref, "updating file %u of %u (%d%% done)",
506 file, files, done);
507 update_view_title(view);
508 setsyx(cursor_y, cursor_x);
509 doupdate();
511 result = status_update_write(&io, line->data, line->type);
513 string_copy(view->ref, buf);
515 return io_done(&io) && result;
518 static bool
519 status_update(struct view *view)
521 struct line *line = &view->line[view->pos.lineno];
523 assert(view->lines);
525 if (!line->data) {
526 if (status_has_none(view, line)) {
527 report("Nothing to update");
528 return FALSE;
531 if (!status_update_files(view, line + 1)) {
532 report("Failed to update file status");
533 return FALSE;
536 } else if (!status_update_file(line->data, line->type)) {
537 report("Failed to update file status");
538 return FALSE;
541 return TRUE;
544 bool
545 status_revert(struct status *status, enum line_type type, bool has_none)
547 if (!status || type != LINE_STAT_UNSTAGED) {
548 if (type == LINE_STAT_STAGED) {
549 report("Cannot revert changes to staged files");
550 } else if (type == LINE_STAT_UNTRACKED) {
551 report("Cannot revert changes to untracked files");
552 } else if (has_none) {
553 report("Nothing to revert");
554 } else {
555 report("Cannot revert changes to multiple files");
558 } else if (prompt_yesno("Are you sure you want to revert changes?")) {
559 char mode[10] = "100644";
560 const char *reset_argv[] = {
561 "git", "update-index", "--cacheinfo", mode,
562 status->old.rev, status->old.name, NULL
564 const char *checkout_argv[] = {
565 "git", "checkout", "--", status->old.name, NULL
568 if (status->status == 'U') {
569 string_format(mode, "%5o", status->old.mode);
571 if (status->old.mode == 0 && status->new.mode == 0) {
572 reset_argv[2] = "--force-remove";
573 reset_argv[3] = status->old.name;
574 reset_argv[4] = NULL;
577 if (!io_run_fg(reset_argv, repo.cdup))
578 return FALSE;
579 if (status->old.mode == 0 && status->new.mode == 0)
580 return TRUE;
583 return io_run_fg(checkout_argv, repo.cdup);
586 return FALSE;
589 static void
590 open_mergetool(const char *file)
592 const char *mergetool_argv[] = { "git", "mergetool", file, NULL };
594 open_external_viewer(mergetool_argv, repo.cdup, FALSE, TRUE, TRUE, "");
597 static enum request
598 status_request(struct view *view, enum request request, struct line *line)
600 struct status *status = line->data;
602 switch (request) {
603 case REQ_STATUS_UPDATE:
604 if (!status_update(view))
605 return REQ_NONE;
606 break;
608 case REQ_STATUS_REVERT:
609 if (!status_revert(status, line->type, status_has_none(view, line)))
610 return REQ_NONE;
611 break;
613 case REQ_STATUS_MERGE:
614 if (!status || status->status != 'U') {
615 report("Merging only possible for files with unmerged status ('U').");
616 return REQ_NONE;
618 open_mergetool(status->new.name);
619 break;
621 case REQ_EDIT:
622 if (!status)
623 return request;
624 if (status->status == 'D') {
625 report("File has been deleted.");
626 return REQ_NONE;
629 open_editor(status->new.name, 0);
630 break;
632 case REQ_VIEW_BLAME:
633 if (line->type == LINE_STAT_UNTRACKED || !status) {
634 report("Nothing to blame here");
635 return REQ_NONE;
637 if (status)
638 view->env->ref[0] = 0;
639 return request;
641 case REQ_ENTER:
642 /* After returning the status view has been split to
643 * show the stage view. No further reloading is
644 * necessary. */
645 return status_enter(view, line);
647 case REQ_REFRESH:
648 /* Load the current branch information and then the view. */
649 load_repo_head();
650 break;
652 default:
653 return request;
656 refresh_view(view);
658 return REQ_NONE;
661 bool
662 status_stage_info_(char *buf, size_t bufsize,
663 enum line_type type, struct status *status)
665 const char *file = status ? status->new.name : "";
666 const char *info;
668 switch (type) {
669 case LINE_STAT_STAGED:
670 if (status && status->status)
671 info = "Staged changes to %s";
672 else
673 info = "Staged changes";
674 break;
676 case LINE_STAT_UNSTAGED:
677 if (status && status->status)
678 info = "Unstaged changes to %s";
679 else
680 info = "Unstaged changes";
681 break;
683 case LINE_STAT_UNTRACKED:
684 info = "Untracked file %s";
685 break;
687 case LINE_HEADER:
688 default:
689 info = "";
692 return string_nformat(buf, bufsize, NULL, info, file);
695 static void
696 status_select(struct view *view, struct line *line)
698 struct status *status = line->data;
699 char file[SIZEOF_STR] = "all files";
700 const char *text;
701 const char *key;
703 if (status && !string_format(file, "'%s'", status->new.name))
704 return;
706 if (!status && line[1].type == LINE_STAT_NONE)
707 line++;
709 switch (line->type) {
710 case LINE_STAT_STAGED:
711 text = "Press %s to unstage %s for commit";
712 break;
714 case LINE_STAT_UNSTAGED:
715 text = "Press %s to stage %s for commit";
716 break;
718 case LINE_STAT_UNTRACKED:
719 text = "Press %s to stage %s for addition";
720 break;
722 default:
723 text = "Nothing to update";
726 if (status && status->status == 'U') {
727 text = "Press %s to resolve conflict in %s";
728 key = get_view_key(view, REQ_STATUS_MERGE);
730 } else {
731 key = get_view_key(view, REQ_STATUS_UPDATE);
734 string_format(view->ref, text, key, file);
735 status_stage_info(view->env->status, line->type, status);
736 if (status)
737 string_copy(view->env->file, status->new.name);
740 static struct view_ops status_ops = {
741 "file",
743 VIEW_CUSTOM_STATUS | VIEW_SEND_CHILD_ENTER | VIEW_STATUS_LIKE | VIEW_REFRESH,
745 status_open,
746 NULL,
747 view_column_draw,
748 status_request,
749 view_column_grep,
750 status_select,
751 NULL,
752 view_column_bit(FILE_NAME) | view_column_bit(LINE_NUMBER) |
753 view_column_bit(STATUS),
754 status_get_column_data,
757 DEFINE_VIEW(status);
759 /* vim: set ts=8 sw=8 noexpandtab: */