index 48f1a1ec643e8ecd9f2202df0b93e6344dc05bb8..4fac53286e2beeecda9b688d90fda3bac10f3eec 100644 (file)
--- a/tig.c
+++ b/tig.c
static void report(const char *msg, ...);
static void set_nonblocking_input(bool loading);
static size_t utf8_length(const char *string, int *width, size_t max_width, int *trimmed, bool reserve);
-static bool prompt_yesno(const char *prompt);
static int load_refs(void);
#define ABS(x) ((x) >= 0 ? (x) : -(x))
return ERR;
}
+enum input_status {
+ INPUT_OK,
+ INPUT_SKIP,
+ INPUT_STOP,
+ INPUT_CANCEL
+};
+
+typedef enum input_status (*input_handler)(void *data, char *buf, int c);
+
+static char *prompt_input(const char *prompt, input_handler handler, void *data);
+static bool prompt_yesno(const char *prompt);
/*
* String helpers
@@ -666,6 +676,7 @@ static int read_properties(struct io *io, const char *separators, int (*read)(ch
REQ_(ENTER, "Enter current line and scroll"), \
REQ_(NEXT, "Move to next"), \
REQ_(PREVIOUS, "Move to previous"), \
+ REQ_(PARENT, "Move to parent"), \
REQ_(VIEW_NEXT, "Move focus to next view"), \
REQ_(REFRESH, "Reload and refresh"), \
REQ_(MAXIMIZE, "Maximize the current view"), \
@@ -677,7 +688,6 @@ static int read_properties(struct io *io, const char *separators, int (*read)(ch
REQ_(STATUS_REVERT, "Revert file changes"), \
REQ_(STATUS_MERGE, "Merge file using external tool"), \
REQ_(STAGE_NEXT, "Find next chunk to stage"), \
- REQ_(TREE_PARENT, "Switch to parent directory in tree view"), \
\
REQ_GROUP("Cursor navigation") \
REQ_(MOVE_UP, "Move cursor one line up"), \
static iconv_t opt_iconv = ICONV_NONE;
static char opt_search[SIZEOF_STR] = "";
static char opt_cdup[SIZEOF_STR] = "";
+static char opt_prefix[SIZEOF_STR] = "";
static char opt_git_dir[SIZEOF_STR] = "";
static signed char opt_is_inside_work_tree = -1; /* set to TRUE or FALSE */
static char opt_editor[SIZEOF_STR] = "";
LINE(MAIN_REF, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
LINE(MAIN_HEAD, "", COLOR_CYAN, COLOR_DEFAULT, A_BOLD), \
LINE(MAIN_REVGRAPH,"", COLOR_MAGENTA, COLOR_DEFAULT, 0), \
-LINE(TREE_DIR, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
+LINE(TREE_PARENT, "", COLOR_DEFAULT, COLOR_DEFAULT, A_BOLD), \
+LINE(TREE_MODE, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
+LINE(TREE_DIR, "", COLOR_YELLOW, COLOR_DEFAULT, A_NORMAL), \
LINE(TREE_FILE, "", COLOR_DEFAULT, COLOR_DEFAULT, A_NORMAL), \
LINE(STAT_HEAD, "", COLOR_YELLOW, COLOR_DEFAULT, 0), \
LINE(STAT_SECTION, "", COLOR_CYAN, COLOR_DEFAULT, 0), \
{ '!', REQ_STATUS_REVERT },
{ 'M', REQ_STATUS_MERGE },
{ '@', REQ_STAGE_NEXT },
- { ',', REQ_TREE_PARENT },
+ { ',', REQ_PARENT },
{ 'e', REQ_EDIT },
};
request = get_request(argv[2]);
if (request == REQ_NONE) {
- const char *obsolete[] = { "cherry-pick", "screen-resize" };
+ struct {
+ const char *name;
+ enum request request;
+ } obsolete[] = {
+ { "cherry-pick", REQ_NONE },
+ { "screen-resize", REQ_NONE },
+ { "tree-parent", REQ_PARENT },
+ };
size_t namelen = strlen(argv[2]);
int i;
for (i = 0; i < ARRAY_SIZE(obsolete); i++) {
- if (namelen == strlen(obsolete[i]) &&
- !string_enum_compare(obsolete[i], argv[2], namelen)) {
- config_msg = "Obsolete request name";
- return ERR;
- }
+ if (namelen != strlen(obsolete[i].name) ||
+ string_enum_compare(obsolete[i].name, argv[2], namelen))
+ continue;
+ if (obsolete[i].request != REQ_NONE)
+ add_keybinding(keymap, obsolete[i].request, key);
+ config_msg = "Obsolete request name";
+ return ERR;
}
}
if (request == REQ_NONE && *argv[2]++ == '!')
if (!dirty)
return;
- redrawwin(view->win);
if (input_mode)
wnoutrefresh(view->win);
else
break;
}
- redrawwin(view->win);
if (input_mode)
wnoutrefresh(view->win);
else
draw_view_line(view, view->lineno - view->offset);
}
- redrawwin(view->win);
wrefresh(view->win);
report("");
}
/* Draw the current line */
draw_view_line(view, view->lineno - view->offset);
- redrawwin(view->win);
wrefresh(view->win);
report("");
}
if (view_is_displayed(view)) {
draw_view_line(view, old_lineno);
draw_view_line(view, view->lineno - view->offset);
- redrawwin(view->win);
wrefresh(view->win);
} else {
view->ops->select(view, &view->line[view->lineno]);
}
+/*
+ * View backend utilities
+ */
+
+/* Parse author lines where the name may be empty:
+ * author <email@address.tld> 1138474660 +0100
+ */
+static void
+parse_author_line(char *ident, char *author, size_t authorsize, struct tm *tm)
+{
+ char *nameend = strchr(ident, '<');
+ char *emailend = strchr(ident, '>');
+
+ if (nameend && emailend)
+ *nameend = *emailend = 0;
+ ident = chomp_string(ident);
+ if (!*ident) {
+ if (nameend)
+ ident = chomp_string(nameend + 1);
+ if (!*ident)
+ ident = "Unknown";
+ }
+
+ string_ncopy_do(author, authorsize, ident, strlen(ident));
+
+ /* Parse epoch and timezone */
+ if (emailend && emailend[1] == ' ') {
+ char *secs = emailend + 2;
+ char *zone = strchr(secs, ' ');
+ time_t time = (time_t) atol(secs);
+
+ if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
+ long tz;
+
+ zone++;
+ tz = ('0' - zone[1]) * 60 * 60 * 10;
+ tz += ('0' - zone[2]) * 60 * 60;
+ tz += ('0' - zone[3]) * 60;
+ tz += ('0' - zone[4]) * 60;
+
+ if (zone[0] == '-')
+ tz = -tz;
+
+ time -= tz;
+ }
+
+ gmtime_r(&time, tm);
+ }
+}
+
+static enum input_status
+select_commit_parent_handler(void *data, char *buf, int c)
+{
+ size_t parents = *(size_t *) data;
+ int parent = 0;
+
+ if (!isdigit(c))
+ return INPUT_SKIP;
+
+ if (*buf)
+ parent = atoi(buf) * 10;
+ parent += c - '0';
+
+ if (parent > parents)
+ return INPUT_SKIP;
+ return INPUT_OK;
+}
+
+static bool
+select_commit_parent(const char *id, char rev[SIZEOF_REV])
+{
+ char buf[SIZEOF_STR * 4];
+ const char *revlist_argv[] = {
+ "git", "rev-list", "-1", "--parents", id, NULL
+ };
+ int parents;
+
+ if (!run_io_buf(revlist_argv, buf, sizeof(buf)) ||
+ !*chomp_string(buf) ||
+ (parents = (strlen(buf) / 40) - 1) < 0) {
+ report("Failed to get parent information");
+ return FALSE;
+
+ } else if (parents == 0) {
+ report("The selected commit has no parents");
+ return FALSE;
+ }
+
+ if (parents > 1) {
+ char prompt[SIZEOF_STR];
+ char *result;
+
+ if (!string_format(prompt, "Which parent? [1..%d] ", parents))
+ return FALSE;
+ result = prompt_input(prompt, select_commit_parent_handler, &parents);
+ if (!result)
+ return FALSE;
+ parents = atoi(result);
+ }
+
+ string_copy_rev(rev, &buf[41 * parents]);
+ return TRUE;
+}
+
/*
* Pager backend
*/
static bool
help_open(struct view *view)
{
- int lines = ARRAY_SIZE(req_info) + 2;
+ char buf[SIZEOF_STR];
+ size_t bufpos;
int i;
if (view->lines > 0)
return TRUE;
- for (i = 0; i < ARRAY_SIZE(req_info); i++)
- if (!req_info[i].request)
- lines++;
-
- lines += run_requests + 1;
-
- view->line = calloc(lines, sizeof(*view->line));
- if (!view->line)
- return FALSE;
-
add_line_text(view, "Quick reference for tig keybindings:", LINE_DEFAULT);
for (i = 0; i < ARRAY_SIZE(req_info); i++) {
if (!*key)
key = "(no key defined)";
- add_line_format(view, LINE_DEFAULT, " %-25s %s",
- key, req_info[i].help);
+ for (bufpos = 0; bufpos <= req_info[i].namelen; bufpos++) {
+ buf[bufpos] = tolower(req_info[i].name[bufpos]);
+ if (buf[bufpos] == '_')
+ buf[bufpos] = '-';
+ }
+
+ add_line_format(view, LINE_DEFAULT, " %-25s %-20s %s",
+ key, buf, req_info[i].help);
}
if (run_requests) {
for (i = 0; i < run_requests; i++) {
struct run_request *req = get_run_request(REQ_NONE + i + 1);
const char *key;
- char cmd[SIZEOF_STR];
- size_t bufpos;
int argc;
if (!req)
key = "(no key defined)";
for (bufpos = 0, argc = 0; req->argv[argc]; argc++)
- if (!string_format_from(cmd, &bufpos, "%s%s",
+ if (!string_format_from(buf, &bufpos, "%s%s",
argc ? " " : "", req->argv[argc]))
return REQ_NONE;
add_line_format(view, LINE_DEFAULT, " %-10s %-14s `%s`",
- keymap_table[req->keymap].name, key, cmd);
+ keymap_table[req->keymap].name, key, buf);
}
return TRUE;
#define SIZEOF_TREE_ATTR \
STRING_SIZE("100644 blob ed09fe897f3c7c9af90bcf80cae92558ea88ae38\t")
-#define TREE_UP_FORMAT "040000 tree %s\t.."
+#define SIZEOF_TREE_MODE \
+ STRING_SIZE("100644 ")
+
+#define TREE_ID_OFFSET \
+ STRING_SIZE("100644 blob ")
+
+struct tree_entry {
+ char id[SIZEOF_REV];
+ mode_t mode;
+ struct tm time; /* Date from the author ident. */
+ char author[75]; /* Author of the commit. */
+ char name[1];
+};
static const char *
tree_path(struct line *line)
{
- const char *path = line->data;
-
- return path + SIZEOF_TREE_ATTR;
+ return ((struct tree_entry *) line->data)->name;
}
+
static int
tree_compare_entry(struct line *line1, struct line *line2)
{
return strcmp(tree_path(line1), tree_path(line2));
}
+static struct line *
+tree_entry(struct view *view, enum line_type type, const char *path,
+ const char *mode, const char *id)
+{
+ struct tree_entry *entry = calloc(1, sizeof(*entry) + strlen(path));
+ struct line *line = entry ? add_line_data(view, entry, type) : NULL;
+
+ if (!entry || !line) {
+ free(entry);
+ return NULL;
+ }
+
+ strncpy(entry->name, path, strlen(path));
+ if (mode)
+ entry->mode = strtoul(mode, NULL, 8);
+ if (id)
+ string_copy_rev(entry->id, id);
+
+ return line;
+}
+
+static bool
+tree_read_date(struct view *view, char *text, bool *read_date)
+{
+ static char author_name[SIZEOF_STR];
+ static struct tm author_time;
+
+ if (!text && *read_date) {
+ *read_date = FALSE;
+ return TRUE;
+
+ } else if (!text) {
+ char *path = *opt_path ? opt_path : ".";
+ /* Find next entry to process */
+ const char *log_file[] = {
+ "git", "log", "--no-color", "--pretty=raw",
+ "--cc", "--raw", view->id, "--", path, NULL
+ };
+ struct io io = {};
+
+ if (!run_io_rd(&io, log_file, FORMAT_NONE)) {
+ report("Failed to load tree data");
+ return TRUE;
+ }
+
+ done_io(view->pipe);
+ view->io = io;
+ *read_date = TRUE;
+ return FALSE;
+
+ } else if (*text == 'a' && get_line_type(text) == LINE_AUTHOR) {
+ parse_author_line(text + STRING_SIZE("author "),
+ author_name, sizeof(author_name), &author_time);
+
+ } else if (*text == ':') {
+ char *pos;
+ size_t annotated = 1;
+ size_t i;
+
+ pos = strchr(text, '\t');
+ if (!pos)
+ return TRUE;
+ text = pos + 1;
+ if (*opt_prefix && !strncmp(text, opt_prefix, strlen(opt_prefix)))
+ text += strlen(opt_prefix);
+ if (*opt_path && !strncmp(text, opt_path, strlen(opt_path)))
+ text += strlen(opt_path);
+ pos = strchr(text, '/');
+ if (pos)
+ *pos = 0;
+
+ for (i = 1; i < view->lines; i++) {
+ struct line *line = &view->line[i];
+ struct tree_entry *entry = line->data;
+
+ annotated += !!*entry->author;
+ if (*entry->author || strcmp(entry->name, text))
+ continue;
+
+ string_copy(entry->author, author_name);
+ memcpy(&entry->time, &author_time, sizeof(entry->time));
+ line->dirty = 1;
+ break;
+ }
+
+ if (annotated == view->lines)
+ kill_io(view->pipe);
+ }
+ return TRUE;
+}
+
static bool
tree_read(struct view *view, char *text)
{
- size_t textlen = text ? strlen(text) : 0;
+ static bool read_date = FALSE;
+ struct tree_entry *data;
struct line *entry, *line;
enum line_type type;
+ size_t textlen = text ? strlen(text) : 0;
+ char *path = text + SIZEOF_TREE_ATTR;
+
+ if (read_date || !text)
+ return tree_read_date(view, text, &read_date);
- if (!text)
- return TRUE;
if (textlen <= SIZEOF_TREE_ATTR)
return FALSE;
-
- type = text[STRING_SIZE("100644 ")] == 't'
- ? LINE_TREE_DIR : LINE_TREE_FILE;
-
if (view->lines == 0 &&
- !add_line_format(view, LINE_DEFAULT, "Directory path /%s", opt_path))
+ !tree_entry(view, LINE_TREE_PARENT, opt_path, NULL, NULL))
return FALSE;
/* Strip the path part ... */
if (*opt_path) {
size_t pathlen = textlen - SIZEOF_TREE_ATTR;
size_t striplen = strlen(opt_path);
- char *path = text + SIZEOF_TREE_ATTR;
if (pathlen > striplen)
memmove(path, path + striplen,
/* Insert "link" to parent directory. */
if (view->lines == 1 &&
- !add_line_format(view, LINE_TREE_DIR, TREE_UP_FORMAT, view->ref))
+ !tree_entry(view, LINE_TREE_DIR, "..", "040000", view->ref))
return FALSE;
}
- entry = add_line_text(view, text, type);
+ type = text[SIZEOF_TREE_MODE] == 't' ? LINE_TREE_DIR : LINE_TREE_FILE;
+ entry = tree_entry(view, type, path, text, text + TREE_ID_OFFSET);
if (!entry)
return FALSE;
- text = entry->data;
+ data = entry->data;
/* Skip "Directory ..." and ".." line. */
for (line = &view->line[1 + !!*opt_path]; line < entry; line++) {
memmove(line + 1, line, (entry - line) * sizeof(*entry));
- line->data = text;
+ line->data = data;
line->type = type;
for (; line <= entry; line++)
line->dirty = line->cleareol = 1;
return TRUE;
}
+static bool
+tree_draw(struct view *view, struct line *line, unsigned int lineno)
+{
+ struct tree_entry *entry = line->data;
+
+ if (line->type == LINE_TREE_PARENT) {
+ if (draw_text(view, line->type, "Directory path /", TRUE))
+ return TRUE;
+ } else {
+ char mode[11] = "-r--r--r--";
+
+ if (S_ISDIR(entry->mode)) {
+ mode[3] = mode[6] = mode[9] = 'x';
+ mode[0] = 'd';
+ }
+ if (S_ISLNK(entry->mode))
+ mode[0] = 'l';
+ if (entry->mode & S_IWUSR)
+ mode[2] = 'w';
+ if (entry->mode & S_IXUSR)
+ mode[3] = 'x';
+ if (entry->mode & S_IXGRP)
+ mode[6] = 'x';
+ if (entry->mode & S_IXOTH)
+ mode[9] = 'x';
+ if (draw_field(view, LINE_TREE_MODE, mode, 11, TRUE))
+ return TRUE;
+
+ if (opt_author &&
+ draw_field(view, LINE_MAIN_AUTHOR, entry->author, opt_author_cols, TRUE))
+ return TRUE;
+
+ if (opt_date && draw_date(view, *entry->author ? &entry->time : NULL))
+ return TRUE;
+ }
+ if (draw_text(view, line->type, entry->name, TRUE))
+ return TRUE;
+ return TRUE;
+}
+
static void
open_blob_editor()
{
}
return REQ_NONE;
- case REQ_TREE_PARENT:
+ case REQ_PARENT:
if (!*opt_path) {
/* quit view if at top of tree */
return REQ_VIEW_CLOSE;
break;
default:
- return TRUE;
+ return REQ_NONE;
}
open_view(view, request, flags);
- if (request == REQ_VIEW_TREE) {
+ if (request == REQ_VIEW_TREE)
view->lineno = tree_lineno;
- }
return REQ_NONE;
}
static void
tree_select(struct view *view, struct line *line)
{
- char *text = (char *)line->data + STRING_SIZE("100644 blob ");
+ struct tree_entry *entry = line->data;
if (line->type == LINE_TREE_FILE) {
- string_copy_rev(ref_blob, text);
+ string_copy_rev(ref_blob, entry->id);
string_format(opt_file, "%s%s", opt_path, tree_path(line));
} else if (line->type != LINE_TREE_DIR) {
return;
}
- string_copy_rev(view->ref, text);
+ string_copy_rev(view->ref, entry->id);
}
static const char *tree_argv[SIZEOF_ARG] = {
tree_argv,
NULL,
tree_read,
- pager_draw,
+ tree_draw,
tree_request,
pager_grep,
tree_select,
}
break;
+ case REQ_PARENT:
+ if (check_blame_commit(blame) &&
+ select_commit_parent(blame->commit->id, opt_ref))
+ open_view(view, REQ_VIEW_BLAME, OPEN_REFRESH);
+ break;
+
case REQ_ENTER:
if (!blame->commit) {
report("No commit loaded yet");
break;
case LINE_AUTHOR:
- {
- /* Parse author lines where the name may be empty:
- * author <email@address.tld> 1138474660 +0100
- */
- char *ident = line + STRING_SIZE("author ");
- char *nameend = strchr(ident, '<');
- char *emailend = strchr(ident, '>');
-
- if (!nameend || !emailend)
- break;
-
+ parse_author_line(line + STRING_SIZE("author "),
+ commit->author, sizeof(commit->author),
+ &commit->time);
update_rev_graph(view, graph);
graph = graph->next;
-
- *nameend = *emailend = 0;
- ident = chomp_string(ident);
- if (!*ident) {
- ident = chomp_string(nameend + 1);
- if (!*ident)
- ident = "Unknown";
- }
-
- string_ncopy(commit->author, ident, strlen(ident));
- view->line[view->lines - 1].dirty = 1;
-
- /* Parse epoch and timezone */
- if (emailend[1] == ' ') {
- char *secs = emailend + 2;
- char *zone = strchr(secs, ' ');
- time_t time = (time_t) atol(secs);
-
- if (zone && strlen(zone) == STRING_SIZE(" +0700")) {
- long tz;
-
- zone++;
- tz = ('0' - zone[1]) * 60 * 60 * 10;
- tz += ('0' - zone[2]) * 60 * 60;
- tz += ('0' - zone[3]) * 60;
- tz += ('0' - zone[4]) * 60;
-
- if (zone[0] == '-')
- tz = -tz;
-
- time -= tz;
- }
-
- gmtime_r(&time, &commit->time);
- }
break;
- }
+
default:
/* Fill in the commit title if it has not already been set. */
if (commit->title[0])
}
}
-static bool
-prompt_yesno(const char *prompt)
-{
- enum { WAIT, STOP, CANCEL } status = WAIT;
- bool answer = FALSE;
-
- while (status == WAIT) {
- int key;
-
- mvwprintw(status_win, 0, 0, "%s [Yy]/[Nn]", prompt);
- wclrtoeol(status_win);
-
- key = get_input(TRUE);
- switch (key) {
- case 'y':
- case 'Y':
- answer = TRUE;
- status = STOP;
- break;
-
- case KEY_ESC:
- case KEY_RETURN:
- case KEY_ENTER:
- case KEY_BACKSPACE:
- case 'n':
- case 'N':
- case '\n':
- default:
- answer = FALSE;
- status = CANCEL;
- }
- }
-
- /* Clear the status window */
- status_empty = FALSE;
- report("");
-
- return answer;
-}
-
static char *
-read_prompt(const char *prompt)
+prompt_input(const char *prompt, input_handler handler, void *data)
{
- enum { READING, STOP, CANCEL } status = READING;
+ enum input_status status = INPUT_OK;
static char buf[SIZEOF_STR];
- int pos = 0;
+ size_t pos = 0;
+
+ buf[pos] = 0;
- while (status == READING) {
+ while (status == INPUT_OK || status == INPUT_SKIP) {
int key;
mvwprintw(status_win, 0, 0, "%s%.*s", prompt, pos, buf);
case KEY_RETURN:
case KEY_ENTER:
case '\n':
- status = pos ? STOP : CANCEL;
+ status = pos ? INPUT_STOP : INPUT_CANCEL;
break;
case KEY_BACKSPACE:
if (pos > 0)
- pos--;
+ buf[--pos] = 0;
else
- status = CANCEL;
+ status = INPUT_CANCEL;
break;
case KEY_ESC:
- status = CANCEL;
+ status = INPUT_CANCEL;
break;
default:
return NULL;
}
- if (isprint(key))
+ status = handler(data, buf, key);
+ if (status == INPUT_OK)
buf[pos++] = (char) key;
}
}
status_empty = FALSE;
report("");
- if (status == CANCEL)
+ if (status == INPUT_CANCEL)
return NULL;
buf[pos++] = 0;
return buf;
}
+static enum input_status
+prompt_yesno_handler(void *data, char *buf, int c)
+{
+ if (c == 'y' || c == 'Y')
+ return INPUT_STOP;
+ if (c == 'n' || c == 'N')
+ return INPUT_CANCEL;
+ return INPUT_SKIP;
+}
+
+static bool
+prompt_yesno(const char *prompt)
+{
+ char prompt2[SIZEOF_STR];
+
+ if (!string_format(prompt2, "%s [Yy/Nn]", prompt))
+ return FALSE;
+
+ return !!prompt_input(prompt2, prompt_yesno_handler, NULL);
+}
+
+static enum input_status
+read_prompt_handler(void *data, char *buf, int c)
+{
+ return isprint(c) ? INPUT_OK : INPUT_SKIP;
+}
+
+static char *
+read_prompt(const char *prompt)
+{
+ return prompt_input(prompt, read_prompt_handler, NULL);
+}
+
/*
* Repository properties
*/
* the option else either "true" or "false" is printed.
* Default to true for the unknown case. */
opt_is_inside_work_tree = strcmp(name, "false") ? TRUE : FALSE;
- } else {
+
+ } else if (*name == '.') {
string_ncopy(opt_cdup, name, namelen);
+
+ } else {
+ string_ncopy(opt_prefix, name, namelen);
}
return OK;
};
const char *rev_parse_argv[] = {
"git", "rev-parse", "--git-dir", "--is-inside-work-tree",
- "--show-cdup", NULL
+ "--show-cdup", "--show-prefix", NULL
};
if (run_io_buf(head_argv, opt_head, sizeof(opt_head))) {