Code

Use menus with the commit subject to present selectable commit parents
[tig.git] / tig.c
diff --git a/tig.c b/tig.c
index a4eae7d01e624b3a65d5e81da1934b6026899134..60d73d52cdb0e8d58629c41b5bfa798268e1a883 100644 (file)
--- a/tig.c
+++ b/tig.c
@@ -68,7 +68,6 @@ static void __NORETURN die(const char *err, ...);
 static void warn(const char *msg, ...);
 static void report(const char *msg, ...);
 static void set_nonblocking_input(bool loading);
-static int load_refs(void);
 static size_t utf8_length(const char **string, size_t col, int *width, size_t max_width, int *trimmed, bool reserve);
 
 #define ABS(x)         ((x) >= 0  ? (x) : -(x))
@@ -129,18 +128,24 @@ static size_t utf8_length(const char **string, size_t col, int *width, size_t ma
 
 
 struct ref {
-       char *name;             /* Ref name; tag or head names are shortened. */
        char id[SIZEOF_REV];    /* Commit SHA1 ID */
        unsigned int head:1;    /* Is it the current HEAD? */
        unsigned int tag:1;     /* Is it a tag? */
        unsigned int ltag:1;    /* If so, is the tag local? */
        unsigned int remote:1;  /* Is it a remote ref? */
        unsigned int tracked:1; /* Is it the remote for the current HEAD? */
-       unsigned int next:1;    /* For ref lists: are there more refs? */
+       char name[1];           /* Ref name; tag or head names are shortened. */
+};
+
+struct ref_list {
+       char id[SIZEOF_REV];    /* Commit SHA1 ID */
+       size_t size;            /* Number of refs. */
+       struct ref **refs;      /* References for this ID. */
 };
 
-static struct ref **get_refs(const char *id);
+static struct ref_list *get_ref_list(const char *id);
 static void foreach_ref(bool (*visitor)(void *data, struct ref *ref), void *data);
+static int load_refs(void);
 
 enum format_flags {
        FORMAT_ALL,             /* Perform replacement in all arguments. */
@@ -162,6 +167,14 @@ 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);
 
+struct menu_item {
+       int hotkey;
+       const char *text;
+       void *data;
+};
+
+static bool prompt_menu(const char *prompt, const struct menu_item *items, int *selected);
+
 /*
  * Allocation helpers ... Entering macro hell to never be seen again.
  */
@@ -826,6 +839,7 @@ run_io_load(const char **argv, const char *separators,
        REQ_(FIND_PREV,         "Find previous search match"), \
        \
        REQ_GROUP("Option manipulation") \
+       REQ_(OPTIONS,           "Open option menu"), \
        REQ_(TOGGLE_LINENO,     "Toggle line numbers"), \
        REQ_(TOGGLE_DATE,       "Toggle date display"), \
        REQ_(TOGGLE_AUTHOR,     "Toggle author display"), \
@@ -1141,6 +1155,7 @@ static const struct keybinding default_keybindings[] = {
        { 'z',          REQ_STOP_LOADING },
        { 'v',          REQ_SHOW_VERSION },
        { 'r',          REQ_SCREEN_REDRAW },
+       { 'o',          REQ_OPTIONS },
        { '.',          REQ_TOGGLE_LINENO },
        { 'D',          REQ_TOGGLE_DATE },
        { 'A',          REQ_TOGGLE_AUTHOR },
@@ -1369,6 +1384,7 @@ static void
 add_builtin_run_requests(void)
 {
        const char *cherry_pick[] = { "git", "cherry-pick", "%(commit)", NULL };
+       const char *commit[] = { "git", "commit", NULL };
        const char *gc[] = { "git", "gc", NULL };
        struct {
                enum keymap keymap;
@@ -1377,6 +1393,7 @@ add_builtin_run_requests(void)
                const char **argv;
        } reqs[] = {
                { KEYMAP_MAIN,    'C', ARRAY_SIZE(cherry_pick) - 1, cherry_pick },
+               { KEYMAP_STATUS,  'C', ARRAY_SIZE(commit) - 1, commit },
                { KEYMAP_GENERIC, 'G', ARRAY_SIZE(gc) - 1, gc },
        };
        int i;
@@ -2279,6 +2296,23 @@ toggle_view_option(bool *option, const char *help)
        report("%sabling %s", *option ? "En" : "Dis", help);
 }
 
+static void
+open_option_menu(void)
+{
+       const struct menu_item menu[] = {
+               { '.', "line numbers", &opt_line_number },
+               { 'D', "date display", &opt_date },
+               { 'A', "author display", &opt_author },
+               { 'g', "revision graph display", &opt_rev_graph },
+               { 'F', "reference display", &opt_show_refs },
+               { 0 }
+       };
+       int selected = 0;
+
+       if (prompt_menu("Toggle option", menu, &selected))
+               toggle_view_option(menu[selected].data, menu[selected].text);
+}
+
 static void
 maximize_view(struct view *view)
 {
@@ -3225,7 +3259,9 @@ view_driver(struct view *view, enum request request)
                   (view == VIEW(REQ_VIEW_STAGE) &&
                     view->parent == VIEW(REQ_VIEW_STATUS)) ||
                   (view == VIEW(REQ_VIEW_BLOB) &&
-                    view->parent == VIEW(REQ_VIEW_TREE))) {
+                    view->parent == VIEW(REQ_VIEW_TREE)) ||
+                  (view == VIEW(REQ_VIEW_MAIN) &&
+                    view->parent == VIEW(REQ_VIEW_BRANCH))) {
                        int line;
 
                        view = view->parent;
@@ -3267,6 +3303,10 @@ view_driver(struct view *view, enum request request)
                        maximize_view(view);
                break;
 
+       case REQ_OPTIONS:
+               open_option_menu();
+               break;
+
        case REQ_TOGGLE_LINENO:
                toggle_view_option(&opt_line_number, "line numbers");
                break;
@@ -3477,22 +3517,39 @@ parse_author_line(char *ident, const char **author, time_t *time)
        }
 }
 
-static enum input_status
-select_commit_parent_handler(void *data, char *buf, int c)
+static bool
+open_commit_parent_menu(char buf[SIZEOF_STR], int *parents)
 {
-       size_t parents = *(size_t *) data;
-       int parent = 0;
+       char rev[SIZEOF_REV];
+       const char *revlist_argv[] = {
+               "git", "log", "--no-color", "-1", "--pretty=format:%s", rev, NULL
+       };
+       struct menu_item *items;
+       char text[SIZEOF_STR];
+       bool ok = TRUE;
+       int i;
 
-       if (!isdigit(c))
-               return INPUT_SKIP;
+       items = calloc(*parents + 1, sizeof(*items));
+       if (items)
+               return FALSE;
 
-       if (*buf)
-               parent = atoi(buf) * 10;
-       parent += c - '0';
+       for (i = 0; i < *parents; i++) {
+               string_copy_rev(rev, &buf[SIZEOF_REV * i]);
+               if (!run_io_buf(revlist_argv, text, sizeof(text)) ||
+                   !(items[i].text = strdup(text))) {
+                       ok = FALSE;
+                       break;
+               }
+       }
 
-       if (parent > parents)
-               return INPUT_SKIP;
-       return INPUT_OK;
+       if (ok) {
+               *parents = 0;
+               ok = prompt_menu("Select parent", items, parents);
+       }
+       for (i = 0; items[i].text; i++)
+               free((char *) items[i].text);
+       free(items);
+       return ok;
 }
 
 static bool
@@ -3500,12 +3557,13 @@ select_commit_parent(const char *id, char rev[SIZEOF_REV], const char *path)
 {
        char buf[SIZEOF_STR * 4];
        const char *revlist_argv[] = {
-               "git", "rev-list", "-1", "--parents", id, "--", path, NULL
+               "git", "log", "--no-color", "-1",
+                       "--pretty=format:%P", id, "--", path, NULL
        };
        int parents;
 
        if (!run_io_buf(revlist_argv, buf, sizeof(buf)) ||
-           (parents = (strlen(buf) / 40) - 1) < 0) {
+           (parents = strlen(buf) / 40) < 0) {
                report("Failed to get parent information");
                return FALSE;
 
@@ -3517,17 +3575,8 @@ select_commit_parent(const char *id, char rev[SIZEOF_REV], const char *path)
                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);
-       }
+       if (parents > 1 && !open_commit_parent_menu(buf, &parents))
+               return FALSE;
 
        string_copy_rev(rev, &buf[41 * parents]);
        return TRUE;
@@ -3571,22 +3620,22 @@ add_pager_refs(struct view *view, struct line *line)
 {
        char buf[SIZEOF_STR];
        char *commit_id = (char *)line->data + STRING_SIZE("commit ");
-       struct ref **refs;
-       size_t bufpos = 0, refpos = 0;
+       struct ref_list *list;
+       size_t bufpos = 0, i;
        const char *sep = "Refs: ";
        bool is_tag = FALSE;
 
        assert(line->type == LINE_COMMIT);
 
-       refs = get_refs(commit_id);
-       if (!refs) {
+       list = get_ref_list(commit_id);
+       if (!list) {
                if (view == VIEW(REQ_VIEW_DIFF))
                        goto try_add_describe_ref;
                return;
        }
 
-       do {
-               struct ref *ref = refs[refpos];
+       for (i = 0; i < list->size; i++) {
+               struct ref *ref = list->refs[i];
                const char *fmt = ref->tag    ? "%s[%s]" :
                                  ref->remote ? "%s<%s>" : "%s%s";
 
@@ -3595,7 +3644,7 @@ add_pager_refs(struct view *view, struct line *line)
                sep = ", ";
                if (ref->tag)
                        is_tag = TRUE;
-       } while (refs[refpos++]->next);
+       }
 
        if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
 try_add_describe_ref:
@@ -4721,6 +4770,30 @@ struct branch {
        struct ref *ref;                /* Name and commit ID information. */
 };
 
+static const enum sort_field branch_sort_fields[] = {
+       ORDERBY_NAME, ORDERBY_DATE, ORDERBY_AUTHOR
+};
+static struct sort_state branch_sort_state = SORT_STATE(branch_sort_fields);
+
+static int
+branch_compare(const void *l1, const void *l2)
+{
+       const struct branch *branch1 = ((const struct line *) l1)->data;
+       const struct branch *branch2 = ((const struct line *) l2)->data;
+
+       switch (get_sort_field(branch_sort_state)) {
+       case ORDERBY_DATE:
+               return sort_order(branch_sort_state, branch1->time - branch2->time);
+
+       case ORDERBY_AUTHOR:
+               return sort_order(branch_sort_state, strcmp(branch1->author, branch2->author));
+
+       case ORDERBY_NAME:
+       default:
+               return sort_order(branch_sort_state, strcmp(branch1->ref->name, branch2->ref->name));
+       }
+}
+
 static bool
 branch_draw(struct view *view, struct line *line, unsigned int lineno)
 {
@@ -4746,6 +4819,11 @@ branch_request(struct view *view, enum request request, struct line *line)
                open_view(view, REQ_VIEW_BRANCH, OPEN_REFRESH);
                return REQ_NONE;
 
+       case REQ_TOGGLE_SORT_FIELD:
+       case REQ_TOGGLE_SORT_ORDER:
+               sort_view(view, request, &branch_sort_state, branch_compare);
+               return REQ_NONE;
+
        case REQ_ENTER:
                open_view(view, REQ_VIEW_MAIN, OPEN_SPLIT);
                return REQ_NONE;
@@ -4828,6 +4906,7 @@ branch_open(struct view *view)
 
        setup_update(view, view->id);
        foreach_ref(branch_open_visitor, view);
+       view->p_restore = TRUE;
 
        return TRUE;
 }
@@ -5869,7 +5948,7 @@ struct commit {
        char title[128];                /* First line of the commit message. */
        const char *author;             /* Author of the commit. */
        time_t time;                    /* Date from the author ident. */
-       struct ref **refs;              /* Repository references. */
+       struct ref_list *refs;          /* Repository references. */
        chtype graph[SIZEOF_REVGRAPH];  /* Ancestry chain graphics. */
        size_t graph_size;              /* The width of the graph array. */
        bool has_parents;               /* Rewritten --parents seen. */
@@ -6108,10 +6187,10 @@ main_draw(struct view *view, struct line *line, unsigned int lineno)
                return TRUE;
 
        if (opt_show_refs && commit->refs) {
-               size_t i = 0;
+               size_t i;
 
-               do {
-                       struct ref *ref = commit->refs[i];
+               for (i = 0; i < commit->refs->size; i++) {
+                       struct ref *ref = commit->refs->refs[i];
                        enum line_type type;
 
                        if (ref->head)
@@ -6134,7 +6213,7 @@ main_draw(struct view *view, struct line *line, unsigned int lineno)
 
                        if (draw_text(view, LINE_DEFAULT, " ", TRUE))
                                return TRUE;
-               } while (commit->refs[i++]->next);
+               }
        }
 
        draw_text(view, LINE_DEFAULT, commit->title, TRUE);
@@ -6183,7 +6262,7 @@ main_read(struct view *view, char *line)
                }
 
                string_copy_rev(commit->id, line);
-               commit->refs = get_refs(commit->id);
+               commit->refs = get_ref_list(commit->id);
                graph->commit = commit;
                add_line_data(view, commit, LINE_MAIN_COMMIT);
 
@@ -6260,17 +6339,18 @@ main_request(struct view *view, enum request request, struct line *line)
 }
 
 static bool
-grep_refs(struct ref **refs, regex_t *regex)
+grep_refs(struct ref_list *list, regex_t *regex)
 {
        regmatch_t pmatch;
-       size_t i = 0;
+       size_t i;
 
-       if (!opt_show_refs || !refs)
+       if (!opt_show_refs || !list)
                return FALSE;
-       do {
-               if (regexec(regex, refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
+
+       for (i = 0; i < list->size; i++) {
+               if (regexec(regex, list->refs[i]->name, 1, &pmatch, 0) != REG_NOMATCH)
                        return TRUE;
-       } while (refs[i++]->next);
+       }
 
        return FALSE;
 }
@@ -6752,20 +6832,80 @@ read_prompt(const char *prompt)
        return prompt_input(prompt, read_prompt_handler, NULL);
 }
 
+static bool prompt_menu(const char *prompt, const struct menu_item *items, int *selected)
+{
+       enum input_status status = INPUT_OK;
+       int size = 0;
+
+       while (items[size].text)
+               size++;
+
+       while (status == INPUT_OK) {
+               const struct menu_item *item = &items[*selected];
+               int key;
+               int i;
+
+               mvwprintw(status_win, 0, 0, "%s (%d of %d) ",
+                         prompt, *selected + 1, size);
+               if (item->hotkey)
+                       wprintw(status_win, "[%c] ", (char) item->hotkey);
+               wprintw(status_win, "%s", item->text);
+               wclrtoeol(status_win);
+
+               key = get_input(COLS - 1);
+               switch (key) {
+               case KEY_RETURN:
+               case KEY_ENTER:
+               case '\n':
+                       status = INPUT_STOP;
+                       break;
+
+               case KEY_LEFT:
+               case KEY_UP:
+                       *selected = *selected - 1;
+                       if (*selected < 0)
+                               *selected = size - 1;
+                       break;
+
+               case KEY_RIGHT:
+               case KEY_DOWN:
+                       *selected = (*selected + 1) % size;
+                       break;
+
+               case KEY_ESC:
+                       status = INPUT_CANCEL;
+                       break;
+
+               default:
+                       for (i = 0; items[i].text; i++)
+                               if (items[i].hotkey == key) {
+                                       *selected = i;
+                                       status = INPUT_STOP;
+                                       break;
+                               }
+               }
+       }
+
+       /* Clear the status window */
+       status_empty = FALSE;
+       report("");
+
+       return status != INPUT_CANCEL;
+}
+
 /*
  * Repository properties
  */
 
-static struct ref *refs = NULL;
+static struct ref **refs = NULL;
 static size_t refs_size = 0;
 
-/* Id <-> ref store */
-static struct ref ***id_refs = NULL;
-static size_t id_refs_size = 0;
+static struct ref_list **ref_lists = NULL;
+static size_t ref_lists_size = 0;
 
-DEFINE_ALLOCATOR(realloc_refs, struct ref, 256)
+DEFINE_ALLOCATOR(realloc_refs, struct ref *, 256)
 DEFINE_ALLOCATOR(realloc_refs_list, struct ref *, 8)
-DEFINE_ALLOCATOR(realloc_refs_lists, struct ref **, 8)
+DEFINE_ALLOCATOR(realloc_ref_lists, struct ref_list *, 8)
 
 static int
 compare_refs(const void *ref1_, const void *ref2_)
@@ -6792,65 +6932,57 @@ foreach_ref(bool (*visitor)(void *data, struct ref *ref), void *data)
        size_t i;
 
        for (i = 0; i < refs_size; i++)
-               if (!visitor(data, &refs[i]))
+               if (!visitor(data, refs[i]))
                        break;
 }
 
-static struct ref **
-get_refs(const char *id)
+static struct ref_list *
+get_ref_list(const char *id)
 {
-       struct ref **ref_list = NULL;
-       size_t ref_list_size = 0;
+       struct ref_list *list;
        size_t i;
 
-       for (i = 0; i < id_refs_size; i++)
-               if (!strcmp(id, id_refs[i][0]->id))
-                       return id_refs[i];
+       for (i = 0; i < ref_lists_size; i++)
+               if (!strcmp(id, ref_lists[i]->id))
+                       return ref_lists[i];
 
-       if (!realloc_refs_lists(&id_refs, id_refs_size, 1))
+       if (!realloc_ref_lists(&ref_lists, ref_lists_size, 1))
+               return NULL;
+       list = calloc(1, sizeof(*list));
+       if (!list)
                return NULL;
 
        for (i = 0; i < refs_size; i++) {
-               if (strcmp(id, refs[i].id))
-                       continue;
-
-               if (!realloc_refs_list(&ref_list, ref_list_size, 1))
-                       break;
-
-               ref_list[ref_list_size] = &refs[i];
-               /* XXX: The properties of the commit chains ensures that we can
-                * safely modify the shared ref. The repo references will
-                * always be similar for the same id. */
-               ref_list[ref_list_size]->next = 1;
-               ref_list_size++;
+               if (!strcmp(id, refs[i]->id) &&
+                   realloc_refs_list(&list->refs, list->size, 1))
+                       list->refs[list->size++] = refs[i];
        }
 
-       if (ref_list) {
-               qsort(ref_list, ref_list_size, sizeof(*ref_list), compare_refs);
-               ref_list[ref_list_size - 1]->next = 0;
-               id_refs[id_refs_size++] = ref_list;
+       if (!list->refs) {
+               free(list);
+               return NULL;
        }
 
-       return ref_list;
+       qsort(list->refs, list->size, sizeof(*list->refs), compare_refs);
+       ref_lists[ref_lists_size++] = list;
+       return list;
 }
 
 static int
 read_ref(char *id, size_t idlen, char *name, size_t namelen)
 {
-       struct ref *ref;
+       struct ref *ref = NULL;
        bool tag = FALSE;
        bool ltag = FALSE;
        bool remote = FALSE;
        bool tracked = FALSE;
-       bool check_replace = FALSE;
        bool head = FALSE;
+       int from = 0, to = refs_size - 1;
 
        if (!prefixcmp(name, "refs/tags/")) {
                if (!suffixcmp(name, namelen, "^{}")) {
                        namelen -= 3;
                        name[namelen] = 0;
-                       if (refs_size > 0 && refs[refs_size - 1].ltag == TRUE)
-                               check_replace = TRUE;
                } else {
                        ltag = TRUE;
                }
@@ -6875,27 +7007,38 @@ read_ref(char *id, size_t idlen, char *name, size_t namelen)
                return OK;
        }
 
-       if (check_replace && !strcmp(name, refs[refs_size - 1].name)) {
-               /* it's an annotated tag, replace the previous SHA1 with the
-                * resolved commit id; relies on the fact git-ls-remote lists
-                * the commit id of an annotated tag right before the commit id
-                * it points to. */
-               refs[refs_size - 1].ltag = ltag;
-               string_copy_rev(refs[refs_size - 1].id, id);
+       /* If we are reloading or it's an annotated tag, replace the
+        * previous SHA1 with the resolved commit id; relies on the fact
+        * git-ls-remote lists the commit id of an annotated tag right
+        * before the commit id it points to. */
+       while (from <= to) {
+               size_t pos = (to + from) / 2;
+               int cmp = strcmp(name, refs[pos]->name);
 
-               return OK;
-       }
+               if (!cmp) {
+                       ref = refs[pos];
+                       break;
+               }
 
-       if (!realloc_refs(&refs, refs_size, 1))
-               return ERR;
+               if (cmp < 0)
+                       to = pos - 1;
+               else
+                       from = pos + 1;
+       }
 
-       ref = &refs[refs_size++];
-       ref->name = malloc(namelen + 1);
-       if (!ref->name)
-               return ERR;
+       if (!ref) {
+               if (!realloc_refs(&refs, refs_size, 1))
+                       return ERR;
+               ref = calloc(1, sizeof(*ref) + namelen);
+               if (!ref)
+                       return ERR;
+               memmove(refs + from + 1, refs + from,
+                       (refs_size - from) * sizeof(*refs));
+               refs[from] = ref;
+               strncpy(ref->name, name, namelen);
+               refs_size++;
+       }
 
-       strncpy(ref->name, name, namelen);
-       ref->name[namelen] = 0;
        ref->head = head;
        ref->tag = tag;
        ref->ltag = ltag;
@@ -6916,6 +7059,7 @@ load_refs(void)
                "git", "ls-remote", opt_git_dir, NULL
        };
        static bool init = FALSE;
+       size_t i;
 
        if (!init) {
                argv_from_env(ls_remote_argv, "TIG_LS_REMOTE");
@@ -6932,12 +7076,24 @@ load_refs(void)
                memmove(opt_head, offset, strlen(offset) + 1);
        }
 
-       while (refs_size > 0)
-               free(refs[--refs_size].name);
-       while (id_refs_size > 0)
-               free(id_refs[--id_refs_size]);
+       for (i = 0; i < refs_size; i++)
+               refs[i]->id[0] = 0;
+
+       if (run_io_load(ls_remote_argv, "\t", read_ref) == ERR)
+               return ERR;
+
+       /* Update the ref lists to reflect changes. */
+       for (i = 0; i < ref_lists_size; i++) {
+               struct ref_list *list = ref_lists[i];
+               size_t old, new;
 
-       return run_io_load(ls_remote_argv, "\t", read_ref);
+               for (old = new = 0; old < list->size; old++)
+                       if (!strcmp(list->id, list->refs[old]->id))
+                               list->refs[new++] = list->refs[old];
+               list->size = new;
+       }
+
+       return OK;
 }
 
 static void