Simplify open_script usage
[tig.git] / src / status.c
blob82ed0342855acc3f316ab2ed6551ef1afd9574ab
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 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 for (lineno = 0; lineno < view->lines; lineno++) {
400 struct line *line = &view->line[lineno];
401 struct status *pos = line->data;
403 if (line->type != type)
404 continue;
405 if ((!pos && (!status || !status->status) && line[1].data) ||
406 (pos && !strcmp(status->new.name, pos->new.name))) {
407 select_view_line(view, lineno);
408 status_restore(view);
409 return TRUE;
413 return FALSE;
417 static bool
418 status_update_prepare(struct io *io, enum line_type type)
420 const char *staged_argv[] = {
421 "git", "update-index", "-z", "--index-info", NULL
423 const char *others_argv[] = {
424 "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL
427 switch (type) {
428 case LINE_STAT_STAGED:
429 return io_run(io, IO_WR, repo.cdup, opt_env, staged_argv);
431 case LINE_STAT_UNSTAGED:
432 case LINE_STAT_UNTRACKED:
433 return io_run(io, IO_WR, repo.cdup, opt_env, others_argv);
435 default:
436 die("line type %d not handled in switch", type);
437 return FALSE;
441 static bool
442 status_update_write(struct io *io, struct status *status, enum line_type type)
444 switch (type) {
445 case LINE_STAT_STAGED:
446 return io_printf(io, "%06o %s\t%s%c", status->old.mode,
447 status->old.rev, status->old.name, 0);
449 case LINE_STAT_UNSTAGED:
450 case LINE_STAT_UNTRACKED:
451 return io_printf(io, "%s%c", status->new.name, 0);
453 default:
454 die("line type %d not handled in switch", type);
455 return FALSE;
459 bool
460 status_update_file(struct status *status, enum line_type type)
462 const char *name = status->new.name;
463 struct io io;
464 bool result;
466 if (type == LINE_STAT_UNTRACKED && !suffixcmp(name, strlen(name), "/")) {
467 const char *add_argv[] = { "git", "add", "--", name, NULL };
469 return io_run_bg(add_argv);
472 if (!status_update_prepare(&io, type))
473 return FALSE;
475 result = status_update_write(&io, status, type);
476 return io_done(&io) && result;
479 bool
480 status_update_files(struct view *view, struct line *line)
482 char buf[sizeof(view->ref)];
483 struct io io;
484 bool result = TRUE;
485 struct line *pos;
486 int files = 0;
487 int file, done;
488 int cursor_y = -1, cursor_x = -1;
490 if (!status_update_prepare(&io, line->type))
491 return FALSE;
493 for (pos = line; view_has_line(view, pos) && pos->data; pos++)
494 files++;
496 string_copy(buf, view->ref);
497 getsyx(cursor_y, cursor_x);
498 for (file = 0, done = 5; result && file < files; line++, file++) {
499 int almost_done = file * 100 / files;
501 if (almost_done > done && view_is_displayed(view)) {
502 done = almost_done;
503 string_format(view->ref, "updating file %u of %u (%d%% done)",
504 file, files, done);
505 update_view_title(view);
506 setsyx(cursor_y, cursor_x);
507 doupdate();
509 result = status_update_write(&io, line->data, line->type);
511 string_copy(view->ref, buf);
513 return io_done(&io) && result;
516 static bool
517 status_update(struct view *view)
519 struct line *line = &view->line[view->pos.lineno];
521 assert(view->lines);
523 if (!line->data) {
524 if (status_has_none(view, line)) {
525 report("Nothing to update");
526 return FALSE;
529 if (!status_update_files(view, line + 1)) {
530 report("Failed to update file status");
531 return FALSE;
534 } else if (!status_update_file(line->data, line->type)) {
535 report("Failed to update file status");
536 return FALSE;
539 return TRUE;
542 bool
543 status_revert(struct status *status, enum line_type type, bool has_none)
545 if (!status || type != LINE_STAT_UNSTAGED) {
546 if (type == LINE_STAT_STAGED) {
547 report("Cannot revert changes to staged files");
548 } else if (type == LINE_STAT_UNTRACKED) {
549 report("Cannot revert changes to untracked files");
550 } else if (has_none) {
551 report("Nothing to revert");
552 } else {
553 report("Cannot revert changes to multiple files");
556 } else if (prompt_yesno("Are you sure you want to revert changes?")) {
557 char mode[10] = "100644";
558 const char *reset_argv[] = {
559 "git", "update-index", "--cacheinfo", mode,
560 status->old.rev, status->old.name, NULL
562 const char *checkout_argv[] = {
563 "git", "checkout", "--", status->old.name, NULL
566 if (status->status == 'U') {
567 string_format(mode, "%5o", status->old.mode);
569 if (status->old.mode == 0 && status->new.mode == 0) {
570 reset_argv[2] = "--force-remove";
571 reset_argv[3] = status->old.name;
572 reset_argv[4] = NULL;
575 if (!io_run_fg(reset_argv, repo.cdup))
576 return FALSE;
577 if (status->old.mode == 0 && status->new.mode == 0)
578 return TRUE;
581 return io_run_fg(checkout_argv, repo.cdup);
584 return FALSE;
587 static void
588 open_mergetool(const char *file)
590 const char *mergetool_argv[] = { "git", "mergetool", file, NULL };
592 open_external_viewer(mergetool_argv, repo.cdup, FALSE, TRUE, TRUE, "");
595 static enum request
596 status_request(struct view *view, enum request request, struct line *line)
598 struct status *status = line->data;
600 switch (request) {
601 case REQ_STATUS_UPDATE:
602 if (!status_update(view))
603 return REQ_NONE;
604 break;
606 case REQ_STATUS_REVERT:
607 if (!status_revert(status, line->type, status_has_none(view, line)))
608 return REQ_NONE;
609 break;
611 case REQ_STATUS_MERGE:
612 if (!status || status->status != 'U') {
613 report("Merging only possible for files with unmerged status ('U').");
614 return REQ_NONE;
616 open_mergetool(status->new.name);
617 break;
619 case REQ_EDIT:
620 if (!status)
621 return request;
622 if (status->status == 'D') {
623 report("File has been deleted.");
624 return REQ_NONE;
627 open_editor(status->new.name, 0);
628 break;
630 case REQ_VIEW_BLAME:
631 if (line->type == LINE_STAT_UNTRACKED || !status) {
632 report("Nothing to blame here");
633 return REQ_NONE;
635 if (status)
636 view->env->ref[0] = 0;
637 return request;
639 case REQ_ENTER:
640 /* After returning the status view has been split to
641 * show the stage view. No further reloading is
642 * necessary. */
643 return status_enter(view, line);
645 case REQ_REFRESH:
646 /* Load the current branch information and then the view. */
647 load_repo_head();
648 break;
650 default:
651 return request;
654 refresh_view(view);
656 return REQ_NONE;
659 bool
660 status_stage_info_(char *buf, size_t bufsize,
661 enum line_type type, struct status *status)
663 const char *file = status ? status->new.name : "";
664 const char *info;
666 switch (type) {
667 case LINE_STAT_STAGED:
668 if (status && status->status)
669 info = "Staged changes to %s";
670 else
671 info = "Staged changes";
672 break;
674 case LINE_STAT_UNSTAGED:
675 if (status && status->status)
676 info = "Unstaged changes to %s";
677 else
678 info = "Unstaged changes";
679 break;
681 case LINE_STAT_UNTRACKED:
682 info = "Untracked file %s";
683 break;
685 case LINE_HEADER:
686 default:
687 info = "";
690 return string_nformat(buf, bufsize, NULL, info, file);
693 static void
694 status_select(struct view *view, struct line *line)
696 struct status *status = line->data;
697 char file[SIZEOF_STR] = "all files";
698 const char *text;
699 const char *key;
701 if (status && !string_format(file, "'%s'", status->new.name))
702 return;
704 if (!status && line[1].type == LINE_STAT_NONE)
705 line++;
707 switch (line->type) {
708 case LINE_STAT_STAGED:
709 text = "Press %s to unstage %s for commit";
710 break;
712 case LINE_STAT_UNSTAGED:
713 text = "Press %s to stage %s for commit";
714 break;
716 case LINE_STAT_UNTRACKED:
717 text = "Press %s to stage %s for addition";
718 break;
720 default:
721 text = "Nothing to update";
724 if (status && status->status == 'U') {
725 text = "Press %s to resolve conflict in %s";
726 key = get_view_key(view, REQ_STATUS_MERGE);
728 } else {
729 key = get_view_key(view, REQ_STATUS_UPDATE);
732 string_format(view->ref, text, key, file);
733 status_stage_info(view->env->status, line->type, status);
734 if (status)
735 string_copy(view->env->file, status->new.name);
738 static struct view_ops status_ops = {
739 "file",
741 VIEW_CUSTOM_STATUS | VIEW_SEND_CHILD_ENTER | VIEW_STATUS_LIKE | VIEW_REFRESH,
743 status_open,
744 NULL,
745 view_column_draw,
746 status_request,
747 view_column_grep,
748 status_select,
749 NULL,
750 view_column_bit(FILE_NAME) | view_column_bit(LINE_NUMBER) |
751 view_column_bit(STATUS),
752 status_get_column_data,
755 DEFINE_VIEW(status);
757 /* vim: set ts=8 sw=8 noexpandtab: */