From 79d3bb5528fd56ae8a8761af6e4303ca9d838d0f Mon Sep 17 00:00:00 2001 From: Aurelien Aptel Date: Sat, 13 Apr 2019 14:26:11 +0200 Subject: [PATCH 1/6] add bplist module BP is short for BackPort. When you need to do a lot of backporting it is useful to be able to mark/unmark and load/save lists of commits. Currently tig has one global bplist, but the implementation is generic enough and works on bplist instances so that in the future we could have multiple bplist. * bplist_init(): initialize a bplist * bplist_read(): load a bplist from a file into an initialized bplist * bplist_has_rev(): checks whether a bplist contains a commit rev * bplist_add_rev(): adds a rev to a bplist if it is not already there * bplist_rem_rev(): removes a rev from a bplist if it holds it * bplist_toggle_rev(): adds/remove rev from a bplist * bplist_write(): dump a bplist to a file A bplist file is a plain text file where each line is in the form of [ ] Lines who do not match this format will be added as-is and not be considered as commit. When writing a bplist, commits are sorted by commit date. Non-commit lines get a commit date of 0 and they end up at the top of the line as a result. --- Makefile | 1 + include/tig/bplist.h | 35 ++++ src/bplist.c | 458 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 494 insertions(+) create mode 100644 include/tig/bplist.h create mode 100644 src/bplist.c diff --git a/Makefile b/Makefile index 603e85d11..542766a0a 100644 --- a/Makefile +++ b/Makefile @@ -312,6 +312,7 @@ TIG_OBJS = \ src/grep.o \ src/ui.o \ src/apps.o \ + src/bplist.o \ $(GRAPH_OBJS) \ $(COMPAT_OBJS) diff --git a/include/tig/bplist.h b/include/tig/bplist.h new file mode 100644 index 000000000..ae33fe99b --- /dev/null +++ b/include/tig/bplist.h @@ -0,0 +1,35 @@ +/* Copyright (c) 2019 Aurelien Aptel + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#ifndef TIG_BPLIST_H +#define TIG_BPLIST_H + +extern struct bplist global_bplist; + +void bplist_init(struct bplist *bpl, size_t capacity, const char *fn); + +const char * bplist_get_fn(struct bplist *bpl); +void bplist_set_fn(struct bplist *bpl, const char *fn); + +bool bplist_has_rev(struct bplist *bpl, const char *rev); +void bplist_add_line(struct bplist *bpl, const char *line); +int bplist_add_rev(struct bplist *bpl, const char *rev, const char *sline); +void bplist_rem_rev(struct bplist *bpl, const char *rev); +bool bplist_toggle_rev(struct bplist *bpl, const char *rev); + +int bplist_read(struct bplist *bpl, const char *fn); +int bplist_write(struct bplist *bpl, const char *fn); + +void init_bplist(void); + +#endif diff --git a/src/bplist.c b/src/bplist.c new file mode 100644 index 000000000..f83009401 --- /dev/null +++ b/src/bplist.c @@ -0,0 +1,458 @@ +/* Copyright (c) 2019 Aurelien Aptel + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "tig/util.h" +#include "tig/map.h" +#include "tig/repo.h" +#include "tig/io.h" +#include "tig/bplist.h" + +/* + * BP is short for BackPort. When you need to do a lot of backporting it + * is useful to be able to mark/unmark and load/save lists of commits. + * + * Currently tig has one global bplist, but the implementation is generic + * enough and works on bplist instances so that in the future we could + * have multiple bplist. + * + * - bplist_init(): initialize a bplist + * - bplist_read(): load a bplist from a file into an initialized bplist + * - bplist_has_rev(): checks whether a bplist contains a commit rev + * - bplist_add_rev(): adds a rev to a bplist if it is not already there + * - bplist_rem_rev(): removes a rev from a bplist if it holds it + * - bplist_toggle_rev(): adds/remove rev from a bplist + * - bplist_write(): dump a bplist to a file + * + * A bplist file is a plain text file where each line is in the form of + * + * [ ] + * + * Lines who do not match this format will be added as-is and not be + * considered as commit. When writing a bplist, commits are sorted by + * commit date. + * + * Non-commit lines get a commit date of 0 and they end up at the top of + * the line as a result. + */ + +/* + * key/value struct for the rev => line hashtable + */ +struct cval { + char rev[SIZEOF_REV]; + struct line *line; +}; + +struct line { + char *s; + long cdate; +}; + +struct bplist { + const char *fn; + struct string_map commits; /* maps revs to line */ + struct line **lines; + size_t nlines; + size_t capacity; +}; + +/* + * tig global bplist instance + */ + +struct bplist global_bplist = {0}; + + +/* + * Helpers for string_map + */ + +static const char * +commits_key(const void *v) +{ + return ((struct cval *)v)->rev; +} + +static string_map_key_t +commits_hash(const void *v) +{ + return string_map_hash_helper(commits_key(v)); +} + +/* + * Expand an abbrev rev to a full one + */ +static int +expand_rev(char *dst, const char *rev) +{ + const char *rev_argv[] = { "git", "rev-parse", rev, NULL }; + bool ok; + + ok = io_run_buf(rev_argv, dst, SIZEOF_REV, repo.cdup, true); + + if (!ok) + die("io_run_buf <%s>", rev); + return 0; +} + +/* + * Get commit title + */ +static char * +get_title(const char *fullrev) +{ + char buf[1024] = {0}; + char *eol; + const char *argv[] = { "git", "log", "--oneline", "--format=%B", + "-n1", fullrev, NULL }; + + if (!io_run_buf(argv, buf, sizeof(buf), repo.cdup, true)) + die("io_run_buf <%s>", fullrev); + + eol = strchr(buf, '\n'); + if (eol) + *eol = 0; + + return strdup(buf); +} + + +/* + * Get commit date + */ +static long +get_cdate(const char *fullrev) +{ + char buf[1024] = {0}; + char *eol; + const char *argv[] = { "git", "show", "-s", "--format=%ct", + fullrev, NULL }; + + if (!io_run_buf(argv, buf, sizeof(buf), repo.cdup, true)) + return 0; + + eol = strchr(buf, '\n'); + if (eol) + *eol = 0; + + return (long)atol(buf); +} + +/* + * Helper for sorting struct lines array + */ +static int +line_cmp(const void *pa, const void *pb) +{ + struct line * const *a = pa; + struct line * const *b = pb; + + return (*a)->cdate - (*b)->cdate; +} + +static void +sort_lines(struct bplist *bpl) +{ + qsort(bpl->lines, bpl->nlines, sizeof(*bpl->lines), line_cmp); +} + +static struct line * +_add_line(struct bplist *bpl, char *s, long cdate) +{ + struct line *line; + + line = calloc(1, sizeof(*line)); + if (!line) + die("OOM"); + + line->s = s; + line->cdate = cdate; + + if (bpl->nlines >= bpl->capacity) { + struct line **p; + size_t newcapa = bpl->capacity + 20; + p = realloc(bpl->lines, sizeof(*p)*newcapa); + if (!p) + die("OOM"); + bpl->lines = p; + bpl->capacity = newcapa; + } + + bpl->lines[bpl->nlines++] = line; + return line; +} + +const char * +bplist_get_fn(struct bplist *bpl) +{ + return bpl->fn; +} + +void +bplist_set_fn(struct bplist *bpl, const char *fn) +{ + bpl->fn = strdup(fn); + if (!bpl->fn) + die("OOM"); +} + +/* + * Add/remove a commit from a bpline. Returns true if the commit is + * added, false if removed. + */ +bool +bplist_toggle_rev(struct bplist *bpl, const char *rev) +{ + if (bplist_has_rev(bpl, rev)) { + bplist_rem_rev(bpl, rev); + return false; + } else { + bplist_add_rev(bpl, rev, NULL); + return true; + } +} + +/* + * Checks if a commit is in a bplist + */ +bool +bplist_has_rev(struct bplist *bpl, const char *rev) +{ + return string_map_get(&bpl->commits, rev) != NULL; +} + +/* + * Adds a line to a bplist. If the line is a valid commit line the + * commit is added to the bplist, otherwise it will just get appended + * to the bplist lines + */ +void +bplist_add_line(struct bplist *bpl, const char *line) +{ + char rev[SIZEOF_REV] = {0}; + char full[SIZEOF_REV] = {0}; + const char *s = line; + const char *beg, *end; + size_t len; + int rc; + + while (*s && isspace(*s)) + s++; + + if (!*s) { + _add_line(bpl, strdup(line), 0); + return; + } + + beg = s; + + while (*s && isxdigit(*s)) + s++; + + end = s; + len = end - beg; + + if (len < 5 || len > SIZEOF_REV-1) { + _add_line(bpl, strdup(line), 0); + return; + } + + memcpy(rev, beg, len); + rc = expand_rev(full, rev); + if (rc) { + _add_line(bpl, strdup(line), 0); + return; + } + + bplist_add_rev(bpl, full, line); +} + +/* + * Adds a commit to a bplist. If line is NULL, a commit line will be + * generated and added the bplist lines + */ +int +bplist_add_rev(struct bplist *bpl, const char *rev, const char *sline) +{ + struct cval *kv; + struct line *line; + char *title; + char *final; + + kv = string_map_get(&bpl->commits, rev); + if (kv) + return 0; + + + line = calloc(1, sizeof(*line)); + if (!line) + die("OOM"); + + if (sline) { + final = strdup(sline); + } else { + title = get_title(rev); + if (!title) + die("OOM"); + + final = calloc(256, 1); + if (!final) + die("OOM"); + + snprintf(final, 255, "%s %s", rev, title); + free(title); + } + + line = _add_line(bpl, final, get_cdate(rev)); + + kv = calloc(1, sizeof(*kv)); + if (!kv) + die("OOM"); + + kv->line = line; + memcpy(kv->rev, rev, SIZEOF_REV); + if (!string_map_put(&bpl->commits, kv->rev, kv)) + die("string_map_put"); + + return 0; +} + +/* + * Removes a commit and its respective line from the bplist + */ +void +bplist_rem_rev(struct bplist *bpl, const char *rev) +{ + struct cval *kv; + struct line *line; + size_t i; + + kv = string_map_remove(&bpl->commits, rev); + if (!kv) + return; + + for (i = 0; i < bpl->nlines; i++) { + if (bpl->lines[i] == kv->line) { + memmove(bpl->lines + i, + bpl->lines + i+1, + sizeof(*bpl->lines)*(bpl->nlines-i-1)); + bpl->lines[bpl->nlines-1] = NULL; + bpl->nlines--; + break; + } + } + free(kv->line->s); + kv->line->s = NULL; + free(kv->line); + kv->line = NULL; + free(kv); +} + +/* + * Initialize a bplist. Stores fn as potential file to read/write the + * bplist but doesn't actually touch it yet. + */ +void +bplist_init(struct bplist *bpl, size_t capacity, const char *fn) +{ + memset(bpl, 0, sizeof(*bpl)); + bpl->fn = fn ? strdup(fn) : NULL; + bpl->commits = (struct string_map){ + commits_hash, + commits_key, + 128, + }; + bpl->capacity = capacity; + bpl->lines = calloc(bpl->capacity, sizeof(*bpl->lines)); +} + +/* + * Load a bplist from a file + */ +int +bplist_read(struct bplist *bpl, const char *fn) +{ + FILE *fh; + char linebuf[2048] = {0}; + int rc = 0; + + fh = fopen(fn, "r"); + if (!fh) { + rc = errno; + errno = 0; + return rc; + } + + while (1) { + char *s; + s = fgets(linebuf, sizeof(linebuf), fh); + if (!s && feof(fh)) { + break; + } + bplist_add_line(bpl, s); + } + fclose(fh); + + bpl->fn = strdup(fn); + if (!bpl->fn) + die("OOM"); + return 0; +} + +/* + * Sort the bplist lines by commit date and dump them to a file + */ +int +bplist_write(struct bplist *bpl, const char *fn) +{ + FILE *fh; + size_t i; + int rc; + + fh = fopen(fn ? fn : bpl->fn, "w+"); + if (!fh) { + rc = errno; + errno = 0; + return rc; + } + + sort_lines(bpl); + + for (i = 0; i < bpl->nlines; i++) { + const char *s; + size_t len; + + s = bpl->lines[i]->s; + s = s ? s : ""; + len = strlen(s); + fprintf(fh, len > 0 && s[len-1] == '\n' ? "%s" : "%s\n", s); + } + + rc = fclose(fh); + if (rc) { + rc = errno; + errno = 0; + return rc; + } + + return 0; +} + +/* + * Module init function + */ +void +init_bplist(void) +{ + bplist_init(&global_bplist, 10, NULL); +} From 6e83b08b1dc80831d0767cf584ea1964f189ed4a Mon Sep 17 00:00:00 2001 From: Aurelien Aptel Date: Sat, 13 Apr 2019 15:00:30 +0200 Subject: [PATCH 2/6] bplist: add -l option, read bplist, write bplist on quit * initialize the bplist in main() before parsing options * add -l to load a bplist file or specify a file to save to on exit * handle -lfoo and -l foo * write bplist on exit if a file was specified --- src/tig.c | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/tig.c b/src/tig.c index 141432c6f..a4fb1b282 100644 --- a/src/tig.c +++ b/src/tig.c @@ -33,6 +33,7 @@ #include "tig/draw.h" #include "tig/display.h" #include "tig/prompt.h" +#include "tig/bplist.h" /* Views. */ #include "tig/blame.h" @@ -482,6 +483,30 @@ parse_options(int argc, const char *argv[], bool pager_mode) if (chdir(opt + 2)) die("Failed to change directory to %s", opt + 2); continue; + } else if (!strncmp(opt, "-l", 2)) { + const char *fn = opt+2; + int rc; + + if (*fn == '\0') { + if (i+1 Date: Sat, 13 Apr 2019 15:09:17 +0200 Subject: [PATCH 3/6] bplist: show marked commits in alternate style adds main-bp-mark line style option. --- include/tig/line.h | 1 + src/draw.c | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/include/tig/line.h b/include/tig/line.h index bea8bf80b..cff96acad 100644 --- a/include/tig/line.h +++ b/include/tig/line.h @@ -71,6 +71,7 @@ struct ref; _(MAIN_TRACKED, ""), \ _(MAIN_REF, ""), \ _(MAIN_HEAD, ""), \ + _(MAIN_BP_MARK, ""), \ _(STAT_NONE, ""), \ _(STAT_STAGED, ""), \ _(STAT_UNSTAGED, ""), \ diff --git a/src/draw.c b/src/draw.c index bec721089..ec8bdaa97 100644 --- a/src/draw.c +++ b/src/draw.c @@ -15,6 +15,7 @@ #include "tig/graph.h" #include "tig/draw.h" #include "tig/options.h" +#include "tig/bplist.h" #include "compat/hashtab.h" static const enum line_type palette_colors[] = { @@ -439,14 +440,20 @@ draw_graph(struct view *view, const struct graph *graph, const struct graph_canv static bool draw_commit_title(struct view *view, struct view_column *column, const struct graph *graph, const struct graph_canvas *graph_canvas, - const struct ref *refs, const char *commit_title) + const struct ref *refs, const char *commit_title, const char *commit_id) { + enum line_type ltype = LINE_DEFAULT; + if (graph && graph_canvas && column->opt.commit_title.graph && draw_graph(view, graph, graph_canvas)) return true; if (draw_refs(view, column, refs)) return true; - return draw_text_overflow(view, commit_title, LINE_DEFAULT, + + if (commit_id && bplist_has_rev(&global_bplist, commit_id)) + ltype = LINE_MAIN_BP_MARK; + + return draw_text_overflow(view, commit_title, ltype, column->opt.commit_title.overflow, 0); } @@ -510,7 +517,7 @@ view_column_draw(struct view *view, struct line *line, unsigned int lineno) case VIEW_COLUMN_COMMIT_TITLE: if (draw_commit_title(view, column, column_data.graph, column_data.graph_canvas, - column_data.refs, column_data.commit_title)) + column_data.refs, column_data.commit_title, column_data.id)) return true; continue; From f11c1675d075ca78f5360642ced4ff9a7f62814d Mon Sep 17 00:00:00 2001 From: Aurelien Aptel Date: Sat, 13 Apr 2019 15:11:37 +0200 Subject: [PATCH 4/6] bplist: add toggle-bp-mark user request --- include/tig/request.h | 1 + src/tig.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/include/tig/request.h b/include/tig/request.h index 5c926ed2f..f0f957e7b 100644 --- a/include/tig/request.h +++ b/include/tig/request.h @@ -78,6 +78,7 @@ REQ_(PROMPT, "Open the prompt"), \ REQ_(OPTIONS, "Open the options menu"), \ REQ_(SCREEN_REDRAW, "Redraw the screen"), \ + REQ_(TOGGLE_BP_MARK, "Toggle BP mark"), \ REQ_(STOP_LOADING, "Stop all loading views"), \ REQ_(SHOW_VERSION, "Show version information"), \ REQ_(NONE, "Do nothing") diff --git a/src/tig.c b/src/tig.c index a4fb1b282..2aed98bea 100644 --- a/src/tig.c +++ b/src/tig.c @@ -307,6 +307,10 @@ view_driver(struct view *view, enum request request) report("Moving between merge commits is not supported by the %s view", view->name); break; + case REQ_TOGGLE_BP_MARK: + bplist_toggle_rev(&global_bplist, argv_env.commit); + break; + case REQ_STOP_LOADING: foreach_view(view, i) { if (view->pipe) From 647fef0f3e98fdb926676c269bae800b66a3db50 Mon Sep 17 00:00:00 2001 From: Aurelien Aptel Date: Sat, 13 Apr 2019 15:12:04 +0200 Subject: [PATCH 5/6] tigrc: add default color and bindings related to BP --- tigrc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tigrc b/tigrc index 35fa0b541..59bdff2f4 100644 --- a/tigrc +++ b/tigrc @@ -220,6 +220,7 @@ bind generic O maximize # Maximize the current view bind generic q view-close # Close the current view bind generic Q quit # Close all views and quit bind generic quit # Close all views and quit +bind generic toggle-bp-mark # Toggle bplist mark # View specific bind status u status-update # Stage/unstage changes in file @@ -391,6 +392,7 @@ color main-replace cyan default color main-tracked yellow default bold color main-ref cyan default color main-head cyan default bold +color main-bp-mark yellow default bold color stat-none default default color stat-staged magenta default color stat-unstaged magenta default From 64ca4defc10e279461bf097a25b740699ae4ff57 Mon Sep 17 00:00:00 2001 From: Aurelien Aptel Date: Sat, 13 Apr 2019 15:56:02 +0200 Subject: [PATCH 6/6] document new marking feature --- doc/manual.adoc | 28 ++++++++++++++++++++++++++++ doc/tig.1.adoc | 4 ++++ doc/tigrc.5.adoc | 2 ++ src/tig.c | 1 + 4 files changed, 35 insertions(+) diff --git a/doc/manual.adoc b/doc/manual.adoc index f75ad3fd9..fa3f148d2 100644 --- a/doc/manual.adoc +++ b/doc/manual.adoc @@ -237,6 +237,33 @@ be appended: [main] 77d9e40fbcea3238015aea403e06f61542df9a31 - commit 1 of 779 (0%) 5s ----------------------------------------------------------------------------- +[[bp-mark-list]] +Commit Marks +------------ + +When doing large backports or similar work that involves a lot of +cherry picking, it can be useful to maintain lists of commits. While +in the main view, Tig can mark commits via the toggle-bp-mark action, +which is bound to by default. Marked commits title will be +displayed using an alternative style (main-bp-mark). + +If a path is specified when running Tig (see `-l` option). Tig can +read and write marked commits. Tig tries to import that file on +startup if it exists, and writes to that file on exit. + +The format of this file is simple, each line must have the following format: + +----------------------------------------------------------------------------- +[ ] +----------------------------------------------------------------------------- + +Tig tries to keep the content of each line when it imports +it. Additional commits added during the Tig session will use the +commit title as text when exporting. + +On exit, Tig sorts the marked commits by commit date (not author date) +and exports the marked lists to the file specified by the `-l` option. + [[env-variables]] Environment Variables --------------------- @@ -465,6 +492,7 @@ Misc |: |Open prompt. This allows you to specify what command to run and also to jump to a specific line, e.g. `:23` |e |Open file in editor. +| |Toggle the BP mark on current commit. |============================================================================= [[prompt]] diff --git a/doc/tig.1.adoc b/doc/tig.1.adoc index ecae067c1..d43ed1a5e 100644 --- a/doc/tig.1.adoc +++ b/doc/tig.1.adoc @@ -74,6 +74,10 @@ grep:: -C:: Run as if Tig was started in instead of the current working directory. +-l:: + If exists, load marked commits and dump marked commits to it on exit. + Otherwise, create and dump marked commits to it on exit. + PAGER MODE ---------- diff --git a/doc/tigrc.5.adoc b/doc/tigrc.5.adoc index cd907bd38..e3d85e0a9 100644 --- a/doc/tigrc.5.adoc +++ b/doc/tigrc.5.adoc @@ -846,6 +846,7 @@ View manipulation |view-close |Close the current view |view-close-no-quit |Close the current view without quitting |quit |Close all views and quit +|toggle-bp-mark |Toggle BP mark |============================================================================= View-specific actions @@ -1038,6 +1039,7 @@ setting the *default* color option. |main-local-tag |Label of a local tag. |main-ref |Label of any other reference. |main-replace |Label of replaced reference. +|main-bp-mark |Title of marked commit (BP mark) |============================================================================= .Status view diff --git a/src/tig.c b/src/tig.c index 2aed98bea..734ba049e 100644 --- a/src/tig.c +++ b/src/tig.c @@ -387,6 +387,7 @@ static const char usage_string[] = " + Select line in the first view\n" " -v, --version Show version and exit\n" " -h, --help Show help message and exit\n" +" -l File to read/write marked commits to\n" " -C Start in "; void