Add :exec prompt command and inline test commands that uses key bindings
[tig.git] / src / main.c
blobe4b6fd3c1cdf020a59d2a8ee51e11a640d3a9f96
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/repo.h"
15 #include "tig/options.h"
16 #include "tig/parse.h"
17 #include "tig/watch.h"
18 #include "tig/graph.h"
19 #include "tig/display.h"
20 #include "tig/view.h"
21 #include "tig/draw.h"
22 #include "tig/git.h"
23 #include "tig/status.h"
24 #include "tig/stage.h"
25 #include "tig/main.h"
26 #include "tig/diff.h"
29 * Main view backend
32 DEFINE_ALLOCATOR(realloc_reflogs, char *, 32)
34 static void
35 main_register_commit(struct view *view, struct commit *commit, const char *ids, bool is_boundary)
37 struct main_state *state = view->private;
39 string_copy_rev(commit->id, ids);
40 if (state->with_graph)
41 graph_add_commit(&state->graph, &commit->graph, commit->id, ids, is_boundary);
44 static struct commit *
45 main_add_commit(struct view *view, enum line_type type, struct commit *template,
46 const char *title, bool custom)
48 struct main_state *state = view->private;
49 size_t titlelen;
50 struct commit *commit;
51 char buf[SIZEOF_STR / 2];
52 struct line *line;
54 /* FIXME: More graceful handling of titles; append "..." to
55 * shortened titles, etc. */
56 string_expand(buf, sizeof(buf), title, 1);
57 title = buf;
58 titlelen = strlen(title);
60 line = add_line_alloc(view, &commit, type, titlelen, custom);
61 if (!line)
62 return NULL;
64 *commit = *template;
65 strncpy(commit->title, title, titlelen);
66 state->graph.canvas = &commit->graph;
67 memset(template, 0, sizeof(*template));
68 state->reflogmsg[0] = 0;
70 view_column_info_update(view, line);
71 return commit;
74 static inline void
75 main_flush_commit(struct view *view, struct commit *commit)
77 if (*commit->id)
78 main_add_commit(view, LINE_MAIN_COMMIT, commit, "", FALSE);
81 static bool
82 main_add_changes_commit(struct view *view, enum line_type type, const char *parent, const char *title)
84 char ids[SIZEOF_STR] = NULL_ID " ";
85 struct main_state *state = view->private;
86 struct commit commit = {};
87 struct timeval now;
88 struct timezone tz;
90 if (!parent)
91 return TRUE;
93 if (*parent)
94 string_copy_rev(ids + STRING_SIZE(NULL_ID " "), parent);
95 else
96 ids[STRING_SIZE(NULL_ID)] = 0;
98 if (!gettimeofday(&now, &tz)) {
99 commit.time.tz = tz.tz_minuteswest * 60;
100 commit.time.sec = now.tv_sec - commit.time.tz;
103 commit.author = &unknown_ident;
104 main_register_commit(view, &commit, ids, FALSE);
105 if (!main_add_commit(view, type, &commit, title, TRUE))
106 return FALSE;
108 if (state->with_graph) {
109 if (*parent)
110 return graph_render_parents(&state->graph);
111 state->add_changes_parents = TRUE;
114 return TRUE;
117 static bool
118 main_add_changes_commits(struct view *view, struct main_state *state, const char *parent)
120 const char *staged_parent = NULL_ID;
121 const char *unstaged_parent = parent;
122 struct index_diff diff;
124 if (!index_diff(&diff, FALSE, FALSE))
125 return FALSE;
127 if (!diff.unstaged) {
128 unstaged_parent = NULL;
129 staged_parent = parent;
130 watch_apply(&view->watch, WATCH_INDEX_UNSTAGED_NO);
131 } else {
132 watch_apply(&view->watch, WATCH_INDEX_UNSTAGED_YES);
135 if (!diff.staged) {
136 staged_parent = NULL;
137 watch_apply(&view->watch, WATCH_INDEX_STAGED_NO);
138 } else {
139 watch_apply(&view->watch, WATCH_INDEX_STAGED_YES);
142 return main_add_changes_commit(view, LINE_STAT_STAGED, staged_parent, "Staged changes")
143 && main_add_changes_commit(view, LINE_STAT_UNSTAGED, unstaged_parent, "Unstaged changes");
146 static bool
147 main_check_argv(struct view *view, const char *argv[])
149 struct main_state *state = view->private;
150 bool with_reflog = FALSE;
151 int i;
153 for (i = 0; argv[i]; i++) {
154 const char *arg = argv[i];
155 struct rev_flags rev_flags = {};
157 if (!strcmp(arg, "--graph")) {
158 struct view_column *column = get_view_column(view, VIEW_COLUMN_COMMIT_TITLE);
160 if (column) {
161 column->opt.commit_title.graph = TRUE;
162 if (opt_commit_order != COMMIT_ORDER_REVERSE)
163 state->with_graph = TRUE;
165 argv[i] = "";
166 continue;
169 if (!strcmp(arg, "--first-parent"))
170 state->first_parent = TRUE;
172 if (!argv_parse_rev_flag(arg, &rev_flags))
173 continue;
175 if (rev_flags.with_reflog)
176 with_reflog = TRUE;
177 if (!rev_flags.with_graph)
178 state->with_graph = FALSE;
179 arg += rev_flags.search_offset;
180 if (*arg && !*view->env->search)
181 string_ncopy(view->env->search, arg, strlen(arg));
184 return with_reflog;
187 static enum graph_display
188 main_with_graph(struct view *view, enum open_flags flags)
190 struct view_column *column = get_view_column(view, VIEW_COLUMN_COMMIT_TITLE);
192 return column && opt_commit_order != COMMIT_ORDER_REVERSE && !open_in_pager_mode(flags)
193 ? column->opt.commit_title.graph : GRAPH_DISPLAY_NO;
196 static bool
197 main_open(struct view *view, enum open_flags flags)
199 enum graph_display graph_display = main_with_graph(view, flags);
200 const char *pretty_custom_argv[] = {
201 GIT_MAIN_LOG_CUSTOM(encoding_arg, commit_order_arg_with_graph(graph_display),
202 "%(cmdlineargs)", "%(revargs)", "%(fileargs)")
204 const char *pretty_raw_argv[] = {
205 GIT_MAIN_LOG_RAW(encoding_arg, commit_order_arg_with_graph(graph_display),
206 "%(cmdlineargs)", "%(revargs)", "%(fileargs)")
208 struct main_state *state = view->private;
209 const char **main_argv = pretty_custom_argv;
210 enum watch_trigger changes_triggers = WATCH_NONE;
212 if (opt_show_changes && repo.is_inside_work_tree)
213 changes_triggers |= WATCH_INDEX;
215 state->with_graph = graph_display != GRAPH_DISPLAY_NO;
217 if (opt_rev_args && main_check_argv(view, opt_rev_args))
218 main_argv = pretty_raw_argv;
220 if (open_in_pager_mode(flags)) {
221 changes_triggers = WATCH_NONE;
224 /* This calls reset_view() so must be before adding changes commits. */
225 if (!begin_update(view, NULL, main_argv, flags))
226 return FALSE;
228 /* Register watch before changes commits are added to record the
229 * start. */
230 if (view_can_refresh(view))
231 watch_register(&view->watch, WATCH_HEAD | WATCH_REFS | changes_triggers);
233 if (changes_triggers)
234 main_add_changes_commits(view, state, "");
236 return TRUE;
239 void
240 main_done(struct view *view)
242 struct main_state *state = view->private;
243 int i;
245 for (i = 0; i < view->lines; i++) {
246 struct commit *commit = view->line[i].data;
248 free(commit->graph.symbols);
251 for (i = 0; i < state->reflogs; i++)
252 free(state->reflog[i]);
253 free(state->reflog);
256 #define main_check_commit_refs(line) !((line)->no_commit_refs)
257 #define main_mark_no_commit_refs(line) (((struct line *) (line))->no_commit_refs = 1)
259 static inline struct ref_list *
260 main_get_commit_refs(const struct line *line, struct commit *commit)
262 struct ref_list *refs = NULL;
264 if (main_check_commit_refs(line) && !(refs = get_ref_list(commit->id)))
265 main_mark_no_commit_refs(line);
267 return refs;
270 bool
271 main_get_column_data(struct view *view, const struct line *line, struct view_column_data *column_data)
273 struct main_state *state = view->private;
274 struct commit *commit = line->data;
275 struct ref_list *refs = NULL;
277 column_data->author = commit->author;
278 column_data->date = &commit->time;
279 column_data->id = commit->id;
280 if (state->reflogs)
281 column_data->reflog = state->reflog[line->lineno - 1];
283 column_data->commit_title = commit->title;
284 if (state->with_graph)
285 column_data->graph = &commit->graph;
287 if ((refs = main_get_commit_refs(line, commit)))
288 column_data->refs = refs;
290 return TRUE;
293 static bool
294 main_add_reflog(struct view *view, struct main_state *state, char *reflog)
296 char *end = strchr(reflog, ' ');
297 int id_width;
299 if (!end)
300 return FALSE;
301 *end = 0;
303 if (!realloc_reflogs(&state->reflog, state->reflogs, 1)
304 || !(reflog = strdup(reflog)))
305 return FALSE;
307 state->reflog[state->reflogs++] = reflog;
308 id_width = strlen(reflog);
309 if (state->reflog_width < id_width) {
310 struct view_column *column = get_view_column(view, VIEW_COLUMN_ID);
312 state->reflog_width = id_width;
313 if (column && column->opt.id.display)
314 view->force_redraw = TRUE;
317 return TRUE;
320 /* Reads git log --pretty=raw output and parses it into the commit struct. */
321 bool
322 main_read(struct view *view, struct buffer *buf)
324 struct main_state *state = view->private;
325 struct graph *graph = &state->graph;
326 enum line_type type;
327 struct commit *commit = &state->current;
328 char *line;
330 if (!buf) {
331 main_flush_commit(view, commit);
333 if (failed_to_load_initial_view(view))
334 die("No revisions match the given arguments.");
335 if (view->lines > 0) {
336 struct commit *last = view->line[view->lines - 1].data;
338 view->line[view->lines - 1].dirty = 1;
339 if (!last->author) {
340 view->lines--;
341 free(last);
345 if (state->with_graph)
346 done_graph(graph);
347 return TRUE;
350 line = buf->data;
351 type = get_line_type(line);
352 if (type == LINE_COMMIT) {
353 bool is_boundary;
354 char *author;
356 state->in_header = TRUE;
357 line += STRING_SIZE("commit ");
358 is_boundary = *line == '-';
359 while (*line && !isalnum(*line))
360 line++;
362 if (state->add_changes_parents) {
363 state->add_changes_parents = FALSE;
364 if (!graph_add_parent(graph, line))
365 return FALSE;
366 graph->has_parents = TRUE;
367 graph_render_parents(graph);
370 main_flush_commit(view, commit);
372 author = io_memchr(buf, line, 0);
374 if (state->first_parent) {
375 char *parent = strchr(line, ' ');
376 char *parent_end = parent ? strchr(parent + 1, ' ') : NULL;
378 if (parent_end)
379 *parent_end = 0;
381 io_trace("[parent] %s\n", line);
384 main_register_commit(view, &state->current, line, is_boundary);
386 if (author) {
387 char *title = io_memchr(buf, author, 0);
389 parse_author_line(author, &commit->author, &commit->time);
390 if (state->with_graph)
391 graph_render_parents(graph);
392 if (title)
393 main_add_commit(view, LINE_MAIN_COMMIT, commit, title, FALSE);
396 return TRUE;
399 if (!*commit->id)
400 return TRUE;
402 /* Empty line separates the commit header from the log itself. */
403 if (*line == '\0')
404 state->in_header = FALSE;
406 switch (type) {
407 case LINE_PP_REFLOG:
408 if (!main_add_reflog(view, state, line + STRING_SIZE("Reflog: ")))
409 return FALSE;
410 break;
412 case LINE_PP_REFLOGMSG:
413 line += STRING_SIZE("Reflog message: ");
414 string_ncopy(state->reflogmsg, line, strlen(line));
415 break;
417 case LINE_PARENT:
418 if (state->with_graph && !graph->has_parents)
419 graph_add_parent(graph, line + STRING_SIZE("parent "));
420 break;
422 case LINE_AUTHOR:
423 parse_author_line(line + STRING_SIZE("author "),
424 &commit->author, &commit->time);
425 if (state->with_graph)
426 graph_render_parents(graph);
427 break;
429 default:
430 /* Fill in the commit title if it has not already been set. */
431 if (*commit->title)
432 break;
434 /* Skip lines in the commit header. */
435 if (state->in_header)
436 break;
438 /* Require titles to start with a non-space character at the
439 * offset used by git log. */
440 if (strncmp(line, " ", 4))
441 break;
442 line += 4;
443 /* Well, if the title starts with a whitespace character,
444 * try to be forgiving. Otherwise we end up with no title. */
445 while (isspace(*line))
446 line++;
447 if (*line == '\0')
448 break;
449 if (*state->reflogmsg)
450 line = state->reflogmsg;
451 main_add_commit(view, LINE_MAIN_COMMIT, commit, line, FALSE);
454 return TRUE;
457 enum request
458 main_request(struct view *view, enum request request, struct line *line)
460 enum open_flags flags = (view_is_displayed(view) && request != REQ_VIEW_DIFF)
461 ? OPEN_SPLIT : OPEN_DEFAULT;
463 switch (request) {
464 case REQ_NEXT:
465 case REQ_PREVIOUS:
466 if (view_is_displayed(view) && display[0] != view)
467 return request;
468 /* Do not pass navigation requests to the branch view
469 * when the main view is maximized. (GH #38) */
470 return request == REQ_NEXT ? REQ_MOVE_DOWN : REQ_MOVE_UP;
472 case REQ_VIEW_DIFF:
473 case REQ_ENTER:
474 if (view_is_displayed(view) && display[0] != view)
475 maximize_view(view, TRUE);
477 if (line->type == LINE_STAT_UNSTAGED
478 || line->type == LINE_STAT_STAGED)
479 open_stage_view(view, NULL, line->type, flags);
480 else
481 open_diff_view(view, flags);
482 break;
484 case REQ_REFRESH:
485 load_refs(TRUE);
486 refresh_view(view);
487 break;
489 default:
490 return request;
493 return REQ_NONE;
496 static void
497 main_update_env(struct view *view, struct line *line, struct commit *commit)
499 struct ref_list *list = main_get_commit_refs(line, commit);
500 size_t i;
502 for (i = 0; list && i < list->size; i++)
503 ref_update_env(view->env, list->refs[list->size - i - 1], !i);
506 void
507 main_select(struct view *view, struct line *line)
509 struct commit *commit = line->data;
511 if (line->type == LINE_STAT_STAGED || line->type == LINE_STAT_UNSTAGED) {
512 string_ncopy(view->ref, commit->title, strlen(commit->title));
513 status_stage_info(view->env->status, line->type, NULL);
514 } else {
515 string_copy_rev(view->ref, commit->id);
516 main_update_env(view, line, commit);
518 string_copy_rev(view->env->commit, commit->id);
521 static struct view_ops main_ops = {
522 "commit",
523 argv_env.head,
524 VIEW_SEND_CHILD_ENTER | VIEW_FILE_FILTER | VIEW_LOG_LIKE | VIEW_REFRESH,
525 sizeof(struct main_state),
526 main_open,
527 main_read,
528 view_column_draw,
529 main_request,
530 view_column_grep,
531 main_select,
532 main_done,
533 view_column_bit(AUTHOR) | view_column_bit(COMMIT_TITLE) |
534 view_column_bit(DATE) | view_column_bit(ID) |
535 view_column_bit(LINE_NUMBER),
536 main_get_column_data,
539 DEFINE_VIEW(main);
541 /* vim: set ts=8 sw=8 noexpandtab: */