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/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/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/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/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/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); +} 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; diff --git a/src/tig.c b/src/tig.c index 141432c6f..734ba049e 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" @@ -306,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) @@ -382,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 @@ -482,6 +488,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 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