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.
15 #include "tig/refdb.h"
17 #include "tig/options.h"
18 #include "tig/parse.h"
19 #include "tig/display.h"
20 #include "tig/prompt.h"
22 #include "tig/search.h"
25 #include "tig/watch.h"
26 #include "tig/status.h"
27 #include "tig/stage.h"
33 static char status_onbranch
[SIZEOF_STR
];
35 /* This should work even for the "On branch" line. */
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
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;
55 old_mode
[-1] != ':' ||
56 new_mode
[-1] != ' ' ||
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;
76 status_run(struct view
*view
, const char *argv
[], char status
, enum line_type type
)
78 struct status
*unmerged
= NULL
;
82 if (!io_run(&io
, IO_RD
, repo
.cdup
, NULL
, argv
))
85 add_line_nodata(view
, type
);
87 while (io_get(&io
, &buf
, 0, true)) {
89 struct status parsed
= {0};
90 struct status
*file
= &parsed
;
92 /* Parse diff info part. */
94 file
->status
= status
;
96 string_copy(file
->old
.rev
, NULL_ID
);
99 if (!status_get_diff(&parsed
, buf
.data
, buf
.size
))
102 if (!io_get(&io
, &buf
, 0, true))
106 /* Grab the old name for rename/copy. */
107 if (!*file
->old
.name
&&
108 (file
->status
== 'R' || file
->status
== 'C')) {
109 string_ncopy(file
->old
.name
, buf
.data
, buf
.size
);
111 if (!io_get(&io
, &buf
, 0, true))
115 /* git-ls-files just delivers a NUL separated list of
116 * file names similar to the second half of the
117 * git-diff-* output. */
118 string_ncopy(file
->new.name
, buf
.data
, buf
.size
);
119 if (!*file
->old
.name
)
120 string_copy(file
->old
.name
, file
->new.name
);
122 /* Collapse all modified entries that follow an associated
124 if (unmerged
&& !strcmp(unmerged
->new.name
, file
->new.name
)) {
125 unmerged
->status
= 'U';
130 line
= add_line_alloc(view
, &file
, type
, 0, false);
134 view_column_info_update(view
, line
);
135 if (file
->status
== 'U')
145 if (!view
->line
[view
->lines
- 1].data
) {
146 add_line_nodata(view
, LINE_STAT_NONE
);
147 if (type
== LINE_STAT_STAGED
)
148 watch_apply(&view
->watch
, WATCH_INDEX_STAGED_NO
);
149 else if (type
== LINE_STAT_UNSTAGED
)
150 watch_apply(&view
->watch
, WATCH_INDEX_UNSTAGED_NO
);
152 if (type
== LINE_STAT_STAGED
)
153 watch_apply(&view
->watch
, WATCH_INDEX_STAGED_YES
);
154 else if (type
== LINE_STAT_UNSTAGED
)
155 watch_apply(&view
->watch
, WATCH_INDEX_UNSTAGED_YES
);
162 static const char *status_diff_index_argv
[] = { GIT_DIFF_STAGED_FILES("-z") };
163 static const char *status_diff_files_argv
[] = { GIT_DIFF_UNSTAGED_FILES("-z") };
165 static const char *status_list_other_argv
[] = {
166 "git", "ls-files", "-z", "--others", "--exclude-standard", NULL
, NULL
, NULL
169 static const char *status_list_no_head_argv
[] = {
170 "git", "ls-files", "-z", "--cached", "--exclude-standard", NULL
173 /* Restore the previous line number to stay in the context or select a
174 * line with something that can be updated. */
176 status_restore(struct view
*view
)
178 if (!check_position(&view
->prev_pos
))
181 if (view
->prev_pos
.lineno
>= view
->lines
)
182 view
->prev_pos
.lineno
= view
->lines
- 1;
183 while (view
->prev_pos
.lineno
< view
->lines
&& !view
->line
[view
->prev_pos
.lineno
].data
)
184 view
->prev_pos
.lineno
++;
185 while (view
->prev_pos
.lineno
> 0 && !view
->line
[view
->prev_pos
.lineno
].data
)
186 view
->prev_pos
.lineno
--;
188 /* If the above fails, always skip the "On branch" line. */
189 if (view
->prev_pos
.lineno
< view
->lines
)
190 view
->pos
.lineno
= view
->prev_pos
.lineno
;
192 view
->pos
.lineno
= 1;
194 if (view
->prev_pos
.offset
> view
->pos
.lineno
)
195 view
->pos
.offset
= view
->pos
.lineno
;
196 else if (view
->prev_pos
.offset
< view
->lines
)
197 view
->pos
.offset
= view
->prev_pos
.offset
;
199 clear_position(&view
->prev_pos
);
203 status_branch_tracking_info(char *buf
, size_t buf_len
, const char *head
,
206 if (!string_nformat(buf
, buf_len
, NULL
, "%s...%s",
211 const char *tracking_info_argv
[] = {
212 "git", "rev-list", "--left-right", buf
, NULL
217 if (!io_run(&io
, IO_RD
, repo
.cdup
, NULL
, tracking_info_argv
)) {
221 struct buffer result
= { 0 };
225 while (io_get(&io
, &result
, '\n', true)) {
226 if (result
.size
> 0 && result
.data
) {
227 if (result
.data
[0] == '<') {
229 } else if (result
.data
[0] == '>') {
235 bool io_failed
= io_error(&io
);
242 if (ahead
== 0 && behind
== 0) {
243 return string_nformat(buf
, buf_len
, NULL
,
244 "Your branch is up-to-date with '%s'.",
246 } else if (ahead
> 0 && behind
> 0) {
247 return string_nformat(buf
, buf_len
, NULL
,
248 "Your branch and '%s' have diverged, "
249 "and have %d and %d different commits "
250 "each, respectively",
251 remote
, ahead
, behind
);
252 } else if (ahead
> 0) {
253 return string_nformat(buf
, buf_len
, NULL
,
254 "Your branch is ahead of '%s' by "
255 "%d commit%s.", remote
, ahead
,
256 ahead
> 1 ? "s" : "");
257 } else if (behind
> 0) {
258 return string_nformat(buf
, buf_len
, NULL
,
259 "Your branch is behind '%s' by "
260 "%d commit%s.", remote
, behind
,
261 behind
> 1 ? "s" : "");
268 status_update_onbranch(void)
270 static const char *paths
[][3] = {
271 { "rebase-apply/rebasing", "rebase-apply/head-name", "Rebasing" },
272 { "rebase-apply/applying", "rebase-apply/head-name", "Applying mailbox to" },
273 { "rebase-apply/", "rebase-apply/head-name", "Rebasing mailbox onto" },
274 { "rebase-merge/interactive", "rebase-merge/head-name", "Interactive rebase" },
275 { "rebase-merge/", "rebase-merge/head-name", "Rebase merge" },
276 { "MERGE_HEAD", NULL
, "Merging" },
277 { "BISECT_LOG", NULL
, "Bisecting" },
278 { "HEAD", NULL
, "On branch" },
280 char buf
[SIZEOF_STR
];
284 if (is_initial_commit()) {
285 string_copy(status_onbranch
, "Initial commit");
289 for (i
= 0; i
< ARRAY_SIZE(paths
); i
++) {
290 const char *prefix
= paths
[i
][2];
291 const char *head
= repo
.head
;
292 const char *tracking_info
= "";
294 if (!string_format(buf
, "%s/%s", repo
.git_dir
, paths
[i
][0]) ||
295 lstat(buf
, &stat
) < 0)
301 if (io_open(&io
, "%s/%s", repo
.git_dir
, paths
[i
][1]) &&
302 io_read_buf(&io
, buf
, sizeof(buf
), false)) {
304 if (!prefixcmp(head
, "refs/heads/"))
305 head
+= STRING_SIZE("refs/heads/");
309 if (!*head
&& !strcmp(paths
[i
][0], "HEAD") && *repo
.head_id
) {
310 const struct ref
*ref
= get_canonical_ref(repo
.head_id
);
312 prefix
= "HEAD detached at";
315 if (ref
&& strcmp(ref
->name
, "HEAD"))
317 } else if (!paths
[i
][1] && *repo
.remote
) {
318 if (status_branch_tracking_info(buf
, sizeof(buf
),
319 head
, repo
.remote
)) {
324 const char *fmt
= *tracking_info
== '\0' ? "%s %s" : "%s %s. %s";
326 if (!string_format(status_onbranch
, fmt
,
327 prefix
, head
, tracking_info
))
328 string_copy(status_onbranch
, repo
.head
);
332 string_copy(status_onbranch
, "Not currently on any branch");
335 /* First parse staged info using git-diff-index(1), then parse unstaged
336 * info using git-diff-files(1), and finally untracked files using
337 * git-ls-files(1). */
339 status_open(struct view
*view
, enum open_flags flags
)
341 const char **staged_argv
= is_initial_commit() ?
342 status_list_no_head_argv
: status_diff_index_argv
;
343 char staged_status
= staged_argv
== status_list_no_head_argv
? 'A' : 0;
345 if (repo
.is_inside_work_tree
== false) {
346 report("The status view requires a working tree");
352 /* FIXME: Watch untracked files and on-branch info. */
353 watch_register(&view
->watch
, WATCH_INDEX
);
355 add_line_nodata(view
, LINE_HEADER
);
356 status_update_onbranch();
360 status_list_other_argv
[ARRAY_SIZE(status_list_other_argv
) - 3] =
361 opt_status_untracked_dirs
? NULL
: "--directory";
362 status_list_other_argv
[ARRAY_SIZE(status_list_other_argv
) - 2] =
363 opt_status_untracked_dirs
? NULL
: "--no-empty-directory";
365 if (!status_run(view
, staged_argv
, staged_status
, LINE_STAT_STAGED
) ||
366 !status_run(view
, status_diff_files_argv
, 0, LINE_STAT_UNSTAGED
) ||
367 !status_run(view
, status_list_other_argv
, '?', LINE_STAT_UNTRACKED
)) {
368 report("Failed to load status data");
372 /* Restore the exact position or use the specialized restore
374 status_restore(view
);
379 status_get_column_data(struct view
*view
, const struct line
*line
, struct view_column_data
*column_data
)
381 struct status
*status
= line
->data
;
384 static struct view_column group_column
;
388 column_data
->section
= &group_column
;
389 column_data
->section
->type
= VIEW_COLUMN_SECTION
;
391 switch (line
->type
) {
392 case LINE_STAT_STAGED
:
394 text
= "Changes to be committed:";
397 case LINE_STAT_UNSTAGED
:
399 text
= "Changes not staged for commit:";
402 case LINE_STAT_UNTRACKED
:
404 text
= "Untracked files:";
409 text
= " (no files)";
414 text
= status_onbranch
;
421 column_data
->section
->opt
.section
.text
= text
;
422 column_data
->section
->opt
.section
.type
= type
;
425 column_data
->status
= &status
->status
;
426 column_data
->file_name
= status
->new.name
;
432 status_enter(struct view
*view
, struct line
*line
)
434 struct status
*status
= line
->data
;
435 enum open_flags flags
= view_is_displayed(view
) ? OPEN_SPLIT
: OPEN_DEFAULT
;
437 if (line
->type
== LINE_STAT_NONE
||
438 (!status
&& line
[1].type
== LINE_STAT_NONE
)) {
439 report("No file to diff");
443 switch (line
->type
) {
444 case LINE_STAT_STAGED
:
445 case LINE_STAT_UNSTAGED
:
448 case LINE_STAT_UNTRACKED
:
450 report("No file to show");
454 if (!suffixcmp(status
->new.name
, -1, "/")) {
455 report("Cannot display a directory");
461 report("Nothing to enter");
465 open_stage_view(view
, status
, line
->type
, flags
);
470 status_exists(struct view
*view
, struct status
*status
, enum line_type type
)
472 unsigned long lineno
;
476 for (lineno
= 0; lineno
< view
->lines
; lineno
++) {
477 struct line
*line
= &view
->line
[lineno
];
478 struct status
*pos
= line
->data
;
480 if (line
->type
!= type
)
482 if ((!pos
&& (!status
|| !status
->status
) && line
[1].data
) ||
483 (pos
&& !strcmp(status
->new.name
, pos
->new.name
))) {
484 select_view_line(view
, lineno
);
485 status_restore(view
);
495 status_update_prepare(struct io
*io
, enum line_type type
)
497 const char *staged_argv
[] = {
498 "git", "update-index", "-z", "--index-info", NULL
500 const char *others_argv
[] = {
501 "git", "update-index", "-z", "--add", "--remove", "--stdin", NULL
505 case LINE_STAT_STAGED
:
506 return io_run(io
, IO_WR
, repo
.cdup
, NULL
, staged_argv
);
508 case LINE_STAT_UNSTAGED
:
509 case LINE_STAT_UNTRACKED
:
510 return io_run(io
, IO_WR
, repo
.cdup
, NULL
, others_argv
);
513 die("line type %d not handled in switch", type
);
519 status_update_write(struct io
*io
, struct status
*status
, enum line_type type
)
522 case LINE_STAT_STAGED
:
523 return io_printf(io
, "%06o %s\t%s%c", status
->old
.mode
,
524 status
->old
.rev
, status
->old
.name
, 0);
526 case LINE_STAT_UNSTAGED
:
527 case LINE_STAT_UNTRACKED
:
528 return io_printf(io
, "%s%c", status
->new.name
, 0);
531 die("line type %d not handled in switch", type
);
537 status_update_file(struct status
*status
, enum line_type type
)
539 const char *name
= status
->new.name
;
543 if (type
== LINE_STAT_UNTRACKED
&& !suffixcmp(name
, strlen(name
), "/")) {
544 const char *add_argv
[] = { "git", "add", "--", name
, NULL
};
546 return io_run_bg(add_argv
, repo
.cdup
);
549 if (!status_update_prepare(&io
, type
))
552 result
= status_update_write(&io
, status
, type
);
553 return io_done(&io
) && result
;
557 status_update_files(struct view
*view
, struct line
*line
)
559 char buf
[sizeof(view
->ref
)];
565 int cursor_y
= -1, cursor_x
= -1;
567 if (!status_update_prepare(&io
, line
->type
))
570 for (pos
= line
; view_has_line(view
, pos
) && pos
->data
; pos
++)
573 string_copy(buf
, view
->ref
);
574 get_cursor_pos(cursor_y
, cursor_x
);
575 for (file
= 0, done
= 5; result
&& file
< files
; line
++, file
++) {
576 int almost_done
= file
* 100 / files
;
578 if (almost_done
> done
&& view_is_displayed(view
)) {
580 string_format(view
->ref
, "updating file %u of %u (%d%% done)",
582 update_view_title(view
);
583 set_cursor_pos(cursor_y
, cursor_x
);
586 result
= status_update_write(&io
, line
->data
, line
->type
);
588 string_copy(view
->ref
, buf
);
590 return io_done(&io
) && result
;
594 status_update(struct view
*view
)
596 struct line
*line
= &view
->line
[view
->pos
.lineno
];
601 if (status_has_none(view
, line
)) {
602 report("Nothing to update");
606 if (!status_update_files(view
, line
+ 1)) {
607 report("Failed to update file status");
611 } else if (!status_update_file(line
->data
, line
->type
)) {
612 report("Failed to update file status");
620 status_revert(struct status
*status
, enum line_type type
, bool has_none
)
622 if (!status
|| type
!= LINE_STAT_UNSTAGED
) {
623 if (type
== LINE_STAT_STAGED
) {
624 report("Cannot revert changes to staged files");
625 } else if (type
== LINE_STAT_UNTRACKED
) {
626 report("Cannot revert changes to untracked files");
627 } else if (has_none
) {
628 report("Nothing to revert");
630 report("Cannot revert changes to multiple files");
633 } else if (prompt_yesno("Are you sure you want to revert changes?")) {
634 char mode
[10] = "100644";
635 const char *reset_argv
[] = {
636 "git", "update-index", "--cacheinfo", mode
,
637 status
->old
.rev
, status
->old
.name
, NULL
639 const char *checkout_argv
[] = {
640 "git", "checkout", "--", status
->old
.name
, NULL
643 if (status
->status
== 'U') {
644 string_format(mode
, "%5o", status
->old
.mode
);
646 if (status
->old
.mode
== 0 && status
->new.mode
== 0) {
647 reset_argv
[2] = "--force-remove";
648 reset_argv
[3] = status
->old
.name
;
649 reset_argv
[4] = NULL
;
652 if (!io_run_fg(reset_argv
, repo
.cdup
))
654 if (status
->old
.mode
== 0 && status
->new.mode
== 0)
658 return io_run_fg(checkout_argv
, repo
.cdup
);
665 open_mergetool(const char *file
)
667 const char *mergetool_argv
[] = { "git", "mergetool", file
, NULL
};
669 open_external_viewer(mergetool_argv
, repo
.cdup
, false, true, true, "");
673 status_request(struct view
*view
, enum request request
, struct line
*line
)
675 struct status
*status
= line
->data
;
678 case REQ_STATUS_UPDATE
:
679 if (!status_update(view
))
683 case REQ_STATUS_REVERT
:
684 if (!status_revert(status
, line
->type
, status_has_none(view
, line
)))
688 case REQ_STATUS_MERGE
:
689 if (!status
|| status
->status
!= 'U') {
690 report("Merging only possible for files with unmerged status ('U').");
693 open_mergetool(status
->new.name
);
699 if (status
->status
== 'D') {
700 report("File has been deleted.");
704 open_editor(status
->new.name
, 0);
708 if (line
->type
== LINE_STAT_UNTRACKED
|| !status
) {
709 report("Nothing to blame here");
713 view
->env
->ref
[0] = 0;
717 /* After returning the status view has been split to
718 * show the stage view. No further reloading is
720 return status_enter(view
, line
);
723 /* Load the current branch information and then the view. */
737 status_stage_info_(char *buf
, size_t bufsize
,
738 enum line_type type
, struct status
*status
)
740 const char *file
= status
? status
->new.name
: "";
744 case LINE_STAT_STAGED
:
745 if (status
&& status
->status
)
746 info
= "Staged changes to %s";
748 info
= "Staged changes";
751 case LINE_STAT_UNSTAGED
:
752 if (status
&& status
->status
)
753 info
= "Unstaged changes to %s";
755 info
= "Unstaged changes";
758 case LINE_STAT_UNTRACKED
:
759 info
= "Untracked file %s";
767 return string_nformat(buf
, bufsize
, NULL
, info
, file
);
771 status_select(struct view
*view
, struct line
*line
)
773 struct status
*status
= line
->data
;
774 char file
[SIZEOF_STR
] = "all files";
778 if (status
&& !string_format(file
, "'%s'", status
->new.name
))
781 if (!status
&& line
[1].type
== LINE_STAT_NONE
)
784 switch (line
->type
) {
785 case LINE_STAT_STAGED
:
786 text
= "Press %s to unstage %s for commit";
789 case LINE_STAT_UNSTAGED
:
790 text
= "Press %s to stage %s for commit";
793 case LINE_STAT_UNTRACKED
:
794 text
= "Press %s to stage %s for addition";
798 text
= "Nothing to update";
801 if (status
&& status
->status
== 'U') {
802 text
= "Press %s to resolve conflict in %s";
803 key
= get_view_key(view
, REQ_STATUS_MERGE
);
806 key
= get_view_key(view
, REQ_STATUS_UPDATE
);
809 string_format(view
->ref
, text
, key
, file
);
810 status_stage_info(view
->env
->status
, line
->type
, status
);
812 string_copy(view
->env
->file
, status
->new.name
);
815 static struct view_ops status_ops
= {
818 VIEW_CUSTOM_STATUS
| VIEW_SEND_CHILD_ENTER
| VIEW_STATUS_LIKE
| VIEW_REFRESH
,
827 view_column_bit(FILE_NAME
) | view_column_bit(LINE_NUMBER
) |
828 view_column_bit(STATUS
),
829 status_get_column_data
,
834 /* vim: set ts=8 sw=8 noexpandtab: */