From: Mikhail Subject: Re: tog: limit feature To: Mark Jamsek Cc: Omar Polo , gameoftrees@openbsd.org Date: Wed, 17 Aug 2022 20:14:59 +0300 On Thu, Aug 18, 2022 at 01:33:08AM +1000, Mark Jamsek wrote: > I also agree with op's suggestions here. > > I, too, don't have the time to review just now but I did apply the patch > and had a quick play. This diff will need something like: > > if (s->first_displayed_entry == NULL) > return NULL; > > when we enter log_move_cursor_down() now too. If you try: > > $ tog > L > xyz > j > > it will segv as it's now possible to display a log with no commits. > This could even be added now. > > But this also made me think of something else for the sake of UX: if > there are no matches, would it be better to display something like "no > matches found" in the status/headline like we do for '/' rather than > display an empty log? This could even be added later if it's indeed > worthwhile; I just thought of this as it happened. Otherwise this works > very well in my quick test and will be a useful addition. > > I finish early tomorrow so will be able to review then if not already > done. This version incorporates yours and Omar's suggestions. Appreciated for the suggestions and the review. diff refs/heads/main refs/heads/limit3 commit - 4fcc9f7404ca2e0dd2ee085f09d6246587c6c503 commit + e97eba33d529e9107268ef7d013c9d84b6a1710b blob - cbd5233dd61d780f245c5bcb24d95b030a5a7676 blob + de7505037613146ca4b909f7375df7a15f95fbdb --- tog/tog.1 +++ tog/tog.1 @@ -220,6 +220,9 @@ This can then be used to open a new view for arbitrary branches and tags. .It Cm @ Toggle between showing the author and the committer name. +.It Cm & +Limit commits to the subset of matching the pattern. +Use empty string as a pattern to display all commits. .El .Pp The options for blob - 6ec8b5f0c155fafe55ee2aab0653d259876e6254 blob + 389d3fc95356d38745aa3ae0ea5769c703bef6a8 --- tog/tog.c +++ tog/tog.c @@ -356,13 +356,20 @@ struct tog_log_thread_args { int *searching; int *search_next_done; regex_t *regex; + int *limiting; + int limit_match; + regex_t *limit_regex; + struct commit_queue *limit_commits; + struct commit_queue_entry **saved_first_displayed_entry; + struct commit_queue_entry **saved_selected_entry; }; struct tog_log_view_state { - struct commit_queue commits; + struct commit_queue *commits; struct commit_queue_entry *first_displayed_entry; struct commit_queue_entry *last_displayed_entry; struct commit_queue_entry *selected_entry; + struct commit_queue real_commits; int selected; char *in_repo_path; char *head_ref_name; @@ -376,6 +383,13 @@ struct tog_log_view_state { struct commit_queue_entry *search_entry; struct tog_colors colors; int use_committer; + int limit_view; + regex_t limit_regex; + struct commit_queue limit_commits; + struct commit_queue *saved_commits; + struct commit_queue_entry *saved_first_displayed_entry; + struct commit_queue_entry *saved_last_displayed_entry; + struct commit_queue_entry *saved_selected_entry; }; #define TOG_COLOR_DIFF_MINUS 1 @@ -935,8 +949,8 @@ resize_log_view(struct tog_view *view, int increase) * Request commits to account for the increased * height so we have enough to populate the view. */ - if (s->commits.ncommits < n) { - view->nscrolled = n - s->commits.ncommits + increase + 1; + if (s->commits->ncommits < n) { + view->nscrolled = n - s->commits->ncommits + increase + 1; err = request_log_commits(view); } @@ -2167,6 +2181,7 @@ queue_commits(struct tog_log_thread_args *a) struct got_object_id *id; struct got_commit_object *commit; struct commit_queue_entry *entry; + int limit_match = 0; int errcode; err = got_commit_graph_iter_next(&id, a->graph, a->repo, @@ -2194,14 +2209,49 @@ queue_commits(struct tog_log_thread_args *a) TAILQ_INSERT_TAIL(&a->commits->head, entry, entry); a->commits->ncommits++; + if (*a->limiting) { + err = match_commit(&limit_match, id, commit, + a->limit_regex); + if (err) + break; + + if (limit_match) { + struct commit_queue_entry *matched; + + matched = + malloc(sizeof(struct commit_queue_entry)); + memcpy(matched, entry, + sizeof(struct commit_queue_entry)); + + matched->idx = a->limit_commits->ncommits; + TAILQ_INSERT_TAIL(&a->limit_commits->head, matched, + entry); + a->limit_commits->ncommits++; + } + + /* + * This is how we signal to log_thread(), that we have + * found match, and that it should be accounted as new + * entry for the view. + */ + a->limit_match = limit_match; + + } + if (*a->searching == TOG_SEARCH_FORWARD && !*a->search_next_done) { int have_match; err = match_commit(&have_match, id, commit, a->regex); if (err) break; - if (have_match) - *a->search_next_done = TOG_SEARCH_HAVE_MORE; + + if (*a->limiting) { + if (limit_match && have_match) + *a->search_next_done = + TOG_SEARCH_HAVE_MORE; + } else if (have_match) + *a->search_next_done = + TOG_SEARCH_HAVE_MORE; } errcode = pthread_mutex_unlock(&tog_mutex); @@ -2271,7 +2321,7 @@ draw_commits(struct tog_view *view) if (s->thread_args.commits_needed > 0 || s->thread_args.load_all) { if (asprintf(&ncommits_str, " [%d/%d] %s", - entry ? entry->idx + 1 : 0, s->commits.ncommits, + entry ? entry->idx + 1 : 0, s->commits->ncommits, (view->searching && !view->search_next_done) ? "searching..." : "loading...") == -1) { err = got_error_from_errno("asprintf"); @@ -2279,6 +2329,7 @@ draw_commits(struct tog_view *view) } } else { const char *search_str = NULL; + const char *limit_str = NULL; if (view->searching) { if (view->search_next_done == TOG_SEARCH_NO_MORE) @@ -2289,10 +2340,15 @@ draw_commits(struct tog_view *view) search_str = "searching..."; } - if (asprintf(&ncommits_str, " [%d/%d] %s", - entry ? entry->idx + 1 : 0, s->commits.ncommits, + if (s->limit_view) + if (s->commits->ncommits == 0) + limit_str = "no limit matches found"; + + if (asprintf(&ncommits_str, " [%d/%d] %s %s", + entry ? entry->idx + 1 : 0, s->commits->ncommits, search_str ? search_str : - (refs_str ? refs_str : "")) == -1) { + (refs_str ? refs_str : ""), + limit_str ? limit_str : "") == -1) { err = got_error_from_errno("asprintf"); goto done; } @@ -2419,7 +2475,7 @@ log_scroll_up(struct tog_log_view_state *s, int maxscr struct commit_queue_entry *entry; int nscrolled = 0; - entry = TAILQ_FIRST(&s->commits.head); + entry = TAILQ_FIRST(&s->commits->head); if (s->first_displayed_entry == entry) return; @@ -2504,13 +2560,13 @@ log_scroll_down(struct tog_view *view, int maxscroll) return NULL; ncommits_needed = s->last_displayed_entry->idx + 1 + maxscroll; - if (s->commits.ncommits < ncommits_needed && + if (s->commits->ncommits < ncommits_needed && !s->thread_args.log_complete) { /* * Ask the log thread for required amount of commits. */ s->thread_args.commits_needed += - ncommits_needed - s->commits.ncommits; + ncommits_needed - s->commits->ncommits; err = trigger_log_thread(view, 1); if (err) return err; @@ -2750,8 +2806,13 @@ log_thread(void *arg) goto done; err = NULL; done = 1; - } else if (a->commits_needed > 0 && !a->load_all) - a->commits_needed--; + } else if (a->commits_needed > 0 && !a->load_all) { + if (*a->limiting) { + if (a->limit_match) + a->commits_needed--; + } else + a->commits_needed--; + } errcode = pthread_mutex_lock(&tog_mutex); if (errcode) { @@ -2760,6 +2821,13 @@ log_thread(void *arg) goto done; } else if (*a->quit) done = 1; + else if (*a->limiting && + *a->first_displayed_entry == NULL) { + *a->first_displayed_entry = + TAILQ_FIRST(&a->limit_commits->head); + *a->selected_entry = + *a->first_displayed_entry; + } else if (*a->first_displayed_entry == NULL) { *a->first_displayed_entry = TAILQ_FIRST(&a->commits->head); @@ -2867,7 +2935,7 @@ close_log_view(struct tog_view *view) if (errcode && err == NULL) err = got_error_set_errno(errcode, "pthread_cond_destroy"); - free_commits(&s->commits); + free_commits(s->commits); free(s->in_repo_path); s->in_repo_path = NULL; free(s->start_id); @@ -2877,7 +2945,128 @@ close_log_view(struct tog_view *view) return err; } +/* + * We use two queues to implement limit feature, first one consists of commits + * matching current limit_regex, second one is the real queue, of all known + * commits (real_commits). Then, when use starts the limiting we swap the + * queues, because of that all movement and displaying functionality works with + * very slight change. + */ static const struct got_error * +limit_log_view(struct tog_view *view) +{ + struct tog_log_view_state *s = &view->state.log; + struct commit_queue_entry *entry; + struct tog_view *v = view; + const struct got_error *err = NULL; + char pattern[1024]; + int ret; + + if (view_is_hsplit_top(view)) + v = view->child; + else if (view->mode == TOG_VIEW_SPLIT_VERT && view->parent) + v = view->parent; + + /* Get the pattern */ + wmove(v->window, v->nlines - 1, 0); + wclrtoeol(v->window); + mvwaddstr(v->window, v->nlines - 1, 0, "limit: "); + nodelay(v->window, FALSE); + nocbreak(); + echo(); + ret = wgetnstr(v->window, pattern, sizeof(pattern)); + cbreak(); + noecho(); + nodelay(v->window, TRUE); + if (ret == ERR) + return NULL; + + if (strcmp(pattern, "") == 0) { + /* + * Safety measures for situation when user limit 'all' without + * previously limiting anything. + */ + if (s->limit_view == 1) { + s->first_displayed_entry = + s->saved_first_displayed_entry; + s->last_displayed_entry = + s->saved_last_displayed_entry; + s->selected_entry = s->saved_selected_entry; + s->commits = &s->real_commits; + s->limit_view = 0; + } + return NULL; + } else { + /* + * This check is needed because user can issue limit "inside" + * limit, in this case don't overwrite saved entries, and start + * the procedure again. + */ + if (s->limit_view == 0) { + s->limit_view = 1; + s->saved_first_displayed_entry = + s->first_displayed_entry; + s->saved_last_displayed_entry = + s->last_displayed_entry; + s->saved_selected_entry = + s->selected_entry; + } + s->first_displayed_entry = NULL; + s->last_displayed_entry = NULL; + s->selected_entry = NULL; + s->commits = &s->limit_commits; + regcomp(&s->limit_regex, pattern, REG_EXTENDED | REG_NEWLINE); + + /* Prepare limit queue for new search */ + while ((entry = TAILQ_FIRST(&s->limit_commits.head))) { + TAILQ_REMOVE(&s->limit_commits.head, entry, entry); + free(entry); + } + s->limit_commits.ncommits = 0; + + /* First process commits, which are in queue already */ + TAILQ_FOREACH(entry, &s->real_commits.head, entry) { + struct commit_queue_entry *matched; + int have_match = 0; + + err = match_commit(&have_match, entry->id, + entry->commit, &s->limit_regex); + if (err) + return err; + + if (have_match) { + matched = + malloc(sizeof(struct commit_queue_entry)); + memcpy(matched, entry, + sizeof(struct commit_queue_entry)); + + matched->idx = s->limit_commits.ncommits; + TAILQ_INSERT_TAIL(&s->limit_commits.head, + matched, entry); + s->limit_commits.ncommits++; + } + + } + + /* Second process all the commits, until we fill the screen */ + while (1) { + if (s->limit_commits.ncommits < view->nlines - 1 && + !s->thread_args.log_complete) { + s->thread_args.commits_needed++; + trigger_log_thread(view, 1); + } else + break; + } + } + + s->first_displayed_entry = TAILQ_FIRST(&s->commits->head); + s->selected_entry = TAILQ_FIRST(&s->commits->head); + s->selected = 0; + + return NULL; +} + +static const struct got_error * search_start_log_view(struct tog_view *view) { struct tog_log_view_state *s = &view->state.log; @@ -3023,9 +3212,17 @@ open_log_view(struct tog_view *view, struct got_object } /* The commit queue only contains commits being displayed. */ - TAILQ_INIT(&s->commits.head); - s->commits.ncommits = 0; + TAILQ_INIT(&s->real_commits.head); + s->real_commits.ncommits = 0; + s->commits = &s->real_commits; + TAILQ_INIT(&s->limit_commits.head); + s->limit_view = 0; + s->saved_first_displayed_entry = NULL; + s->saved_last_displayed_entry = NULL; + s->saved_selected_entry = NULL; + s->limit_commits.ncommits = 0; + s->repo = repo; if (head_ref_name) { s->head_ref_name = strdup(head_ref_name); @@ -3099,7 +3296,7 @@ open_log_view(struct tog_view *view, struct got_object s->thread_args.commits_needed = view->nlines; s->thread_args.graph = thread_graph; - s->thread_args.commits = &s->commits; + s->thread_args.commits = s->commits; s->thread_args.in_repo_path = s->in_repo_path; s->thread_args.start_id = s->start_id; s->thread_args.repo = thread_repo; @@ -3110,6 +3307,13 @@ open_log_view(struct tog_view *view, struct got_object s->thread_args.searching = &view->searching; s->thread_args.search_next_done = &view->search_next_done; s->thread_args.regex = &view->regex; + s->thread_args.limiting = &s->limit_view; + s->thread_args.limit_regex = &s->limit_regex; + s->thread_args.limit_commits = &s->limit_commits; + s->thread_args.saved_first_displayed_entry = + &s->saved_first_displayed_entry; + s->thread_args.saved_selected_entry = + &s->saved_selected_entry; done: if (err) close_log_view(view); @@ -3142,19 +3346,19 @@ log_move_cursor_up(struct tog_view *view, int page, in { struct tog_log_view_state *s = &view->state.log; + if (s->first_displayed_entry == NULL) + return; if (s->selected_entry->idx == 0) view->count = 0; - if (s->first_displayed_entry == NULL) - return; - if ((page && TAILQ_FIRST(&s->commits.head) == s->first_displayed_entry) + if ((page && TAILQ_FIRST(&s->commits->head) == s->first_displayed_entry) || home) s->selected = home ? 0 : MAX(0, s->selected - page - 1); if (!page && !home && s->selected > 0) --s->selected; else - log_scroll_up(s, home ? s->commits.ncommits : MAX(page, 1)); + log_scroll_up(s, home ? s->commits->ncommits : MAX(page, 1)); select_commit(s); return; @@ -3167,15 +3371,18 @@ log_move_cursor_down(struct tog_view *view, int page) const struct got_error *err = NULL; int eos = view->nlines - 2; + if (s->first_displayed_entry == NULL) + return NULL; + if (s->thread_args.log_complete && - s->selected_entry->idx >= s->commits.ncommits - 1) + s->selected_entry->idx >= s->commits->ncommits - 1) return NULL; if (view_is_hsplit_top(view)) --eos; /* border consumes the last line */ if (!page) { - if (s->selected < MIN(eos, s->commits.ncommits - 1)) + if (s->selected < MIN(eos, s->commits->ncommits - 1)) ++s->selected; else err = log_scroll_down(view, 1); @@ -3184,7 +3391,7 @@ log_move_cursor_down(struct tog_view *view, int page) int n; s->selected = 0; - entry = TAILQ_LAST(&s->commits.head, commit_queue_head); + entry = TAILQ_LAST(&s->commits->head, commit_queue_head); s->last_displayed_entry = entry; for (n = 0; n <= eos; n++) { if (entry == NULL) @@ -3195,10 +3402,10 @@ log_move_cursor_down(struct tog_view *view, int page) if (n > 0) s->selected = n - 1; } else { - if (s->last_displayed_entry->idx == s->commits.ncommits - 1 && + if (s->last_displayed_entry->idx == s->commits->ncommits - 1 && s->thread_args.log_complete) s->selected += MIN(page, - s->commits.ncommits - s->selected_entry->idx - 1); + s->commits->ncommits - s->selected_entry->idx - 1); else err = log_scroll_down(view, page); } @@ -3218,7 +3425,7 @@ log_move_cursor_down(struct tog_view *view, int page) select_commit(s); if (s->thread_args.log_complete && - s->selected_entry->idx == s->commits.ncommits - 1) + s->selected_entry->idx == s->commits->ncommits - 1) view->count = 0; return NULL; @@ -3314,7 +3521,7 @@ input_log_view(struct tog_view **new_view, struct tog_ if (ch == CTRL('g') || ch == KEY_BACKSPACE) s->thread_args.load_all = 0; else if (s->thread_args.log_complete) { - err = log_move_cursor_down(view, s->commits.ncommits); + err = log_move_cursor_down(view, s->commits->ncommits); s->thread_args.load_all = 0; } if (err) @@ -3329,6 +3536,9 @@ input_log_view(struct tog_view **new_view, struct tog_ return log_goto_line(view, eos); switch (ch) { + case '&': + err = limit_log_view(view); + break; case 'q': s->quit = 1; break; @@ -3391,7 +3601,7 @@ input_log_view(struct tog_view **new_view, struct tog_ s->thread_args.load_all = 1; if (!s->thread_args.log_complete) return trigger_log_thread(view, 0); - err = log_move_cursor_down(view, s->commits.ncommits); + err = log_move_cursor_down(view, s->commits->ncommits); s->thread_args.load_all = 0; break; } @@ -3408,13 +3618,13 @@ input_log_view(struct tog_view **new_view, struct tog_ case KEY_RESIZE: if (s->selected > view->nlines - 2) s->selected = view->nlines - 2; - if (s->selected > s->commits.ncommits - 1) - s->selected = s->commits.ncommits - 1; + if (s->selected > s->commits->ncommits - 1) + s->selected = s->commits->ncommits - 1; select_commit(s); - if (s->commits.ncommits < view->nlines - 1 && + if (s->commits->ncommits < view->nlines - 1 && !s->thread_args.log_complete) { s->thread_args.commits_needed += (view->nlines - 1) - - s->commits.ncommits; + s->commits->ncommits; err = trigger_log_thread(view, 1); } break; @@ -3484,7 +3694,7 @@ input_log_view(struct tog_view **new_view, struct tog_ s->start_id, s->repo, NULL, NULL); if (err) return err; - free_commits(&s->commits); + free_commits(s->commits); s->first_displayed_entry = NULL; s->last_displayed_entry = NULL; s->selected_entry = NULL;