Index: CHANGES.md ================================================================== --- CHANGES.md +++ CHANGES.md @@ -1,5 +1,30 @@ +**fnc 0.10** 2022-03-24 + +- fix gcc 9.3 compiler warnings (i.e., unused variable) (reported by stephan) +- restrict `C` key map for diffing local changes to check-in artifacts +- ensure timeline --branch option ignores cancelled branches (reported by sean) +- fix landlock initialisation of handled fs access perms (patch by Ashish) +- tighten landlock ruleset depending on which fnc command is called +- improve branch view rendering of last modified date and hash id +- colour branch header and make (in)active behaviour consistent with other views +- improve branch parsing of imported repositories (reported by Dan) +- fix OB1 error when branch entry length exceeds COLUMNS +- add timezone path to landlock ruleset (reported by Ashish) +- implement `P` key map to write a patch file of the currently viewed diff +- fix invalid write in landlock initialisation code (reported by Ashish) +- ensure unveil(2) initialisation handles `-R|--repo` invocations +- add create file to landlock ruleset for permitted dirs (reported by Ashish) +- make mandir if needed in makefile install target (reported by Dan) +- document `backspace` key map to cancel tl search/traversal (reported by Dan) +- return to blocking on user input when tl search is aborted (reported by Dan) +- implement persistent diff options for global and per-repo defaults +- implement `--whitespace-eol` and `W` key map to only ignore eol whitespace +- implement horizontal scroll in the in-app help +- add `Q` key map to in-app help to directly quit fnc +- implement blame navigation from diff view with `C-{j,k}` key maps + **fnc 0.9** 2022-03-04 - Add blame command `--line` option to open annotated file at the specified line - merge upstream libfossil changes that eliminate gcc compiler warnings - adopt libfossil diff v1 implementation into fnc tree to replace v2 API Index: README.md ================================================================== --- README.md +++ README.md @@ -1,8 +1,8 @@ # README -# fnc 0.9 +# fnc 0.10 ## An interactive ncurses browser for [Fossil][0] repositories. `fnc` uses [libfossil][1] to create a [`fossil ui`][2] experience in the terminal. @@ -25,11 +25,15 @@ * **OpenBSD** - `doas pkg_add fnc` * **macOS** - `sudo port install fnc` -* **[Download](/uv/download.html)** and install the binary on your path +* **FreeBSD** + - package: `pkg install fnc` + - port: `cd /usr/ports/devel/fnc/ && make install clean` +* **Linux** + - [Download](/uv/download.html) and install the binary on your path # Build 1. clone the repository - `fossil clone https://fnc.bsdbox.org` Index: fnc.bld.mk ================================================================== --- fnc.bld.mk +++ fnc.bld.mk @@ -4,11 +4,11 @@ # CONFIGURATION CC ?= cc PREFIX ?= /usr/local MANDIR ?= /share/man -VERSION ?= 0.9 +VERSION ?= 0.10 # FLAGS NEEDED TO BUILD SQLITE3 SQLITE_CFLAGS = ${CFLAGS} -Wall -Werror -Wno-sign-compare -pedantic -std=c99 \ -DNDEBUG=1 \ -DSQLITE_DQS=0 \ @@ -65,10 +65,11 @@ src/fnc: src/fnc.o src/diff.o lib/libfossil.o lib/sqlite3.o fnc.bld.mk ${CC} -o $@ src/fnc.o src/diff.o lib/libfossil.o lib/sqlite3.o \ ${FNC_LDFLAGS} install: + mkdir -p -m 0755 ${PREFIX}${MANDIR}/man1 install -s -m 0755 src/fnc ${PREFIX}/bin/fnc install -m 0644 src/fnc.1 ${PREFIX}${MANDIR}/man1/fnc.1 uninstall: rm -f ${PREFIX}/bin/fnc ${PREFIX}${MANDIR}/man1/fnc.1 Index: include/settings.h ================================================================== --- include/settings.h +++ include/settings.h @@ -18,11 +18,11 @@ #define GEN_ENUM(name, pfx, info) \ enum name { info(pfx, GEN_ENUM_SYM) }; #define GEN_STR_SYM(pfx, id) #pfx"_"#id #define GEN_STR(name, pfx, info) \ - static const char *name[] = { info(pfx, GEN_STR_SYM) }; + const char *name[] = { info(pfx, GEN_STR_SYM) }; /* * All configurable fnc settings, which can be stored in either the fossil(1) * repository (e.g., ./repo.fossil) or shell envvars with `export SETTING=val`. */ @@ -44,10 +44,12 @@ _(pfx, COLOUR_BRANCH_CLOSED), \ _(pfx, COLOUR_BRANCH_CURRENT), \ _(pfx, COLOUR_BRANCH_PRIVATE), \ _(pfx, COLOUR_HL_LINE), \ _(pfx, COLOUR_HL_SEARCH), \ + _(pfx, DIFF_CONTEXT), \ + _(pfx, DIFF_FLAGS), \ _(pfx, VIEW_SPLIT_MODE), \ _(pfx, VIEW_SPLIT_WIDTH), \ _(pfx, VIEW_SPLIT_HEIGHT), \ _(pfx, EOF_SETTINGS) @@ -91,11 +93,5 @@ _(line_type, LINE, LINE_TYPE_ENUM) \ _(view_mode, VIEW_SPLIT, VIEW_MODE_ENUM) #define GEN_ENUMS(name, pfx, info) GEN_ENUM(name, pfx, info) ENUM_INFO(GEN_ENUMS) - -#define STR_INFO(_) \ - _(fnc_opt_name, FNC, USER_OPTIONS) - -#define GEN_STRINGS(name, pfx, info) GEN_STR(name, pfx, info) -STR_INFO(GEN_STRINGS) DELETED signify/fnc-08-release.pub Index: signify/fnc-08-release.pub ================================================================== --- signify/fnc-08-release.pub +++ signify/fnc-08-release.pub @@ -1,2 +0,0 @@ -untrusted comment: fnc 0.8 public key -RWRvIlnJtmD7IOR9LK/Ed1KrC+OsoACBv6wlQvDm0d117Lbvn48ez+2L ADDED signify/fnc-10-release.pub Index: signify/fnc-10-release.pub ================================================================== --- signify/fnc-10-release.pub +++ signify/fnc-10-release.pub @@ -0,0 +1,2 @@ +untrusted comment: fnc 0.10 public key +RWS6x8wL/CQL9o5F+7/DWH43PNhA+TqWxODvTKDcgslIuGfRUslHXb+W Index: src/diff.c ================================================================== --- src/diff.c +++ src/diff.c @@ -1302,11 +1302,11 @@ * default if no width is specified. */ int sbsdiff_width(uint64_t flags) { - int w = (flags & FNC_DIFF_WIDTH_MASK - 0xf) / FNC_DIFF_CONTEXT_MASK; + int w = (flags & (FNC_DIFF_WIDTH_MASK - 0xf)) / FNC_DIFF_CONTEXT_MASK; if (!w) w = 80; return w; Index: src/fnc.1 ================================================================== --- src/fnc.1 +++ src/fnc.1 @@ -44,11 +44,11 @@ .Op Fl t Ar type .Op Fl u Ar user .Op Ar path .Nm .Cm diff -.Op Fl CilPqsw +.Op Fl CilPqsWw .Op Fl R Ar path .Op Fl x Ar number .Op Ar artifact1 Op Ar artifact2 .Op Ar path ... .Nm @@ -407,16 +407,26 @@ the timeline is consumed. .It Cm N Find the previous commit that matches the current search term. The search will continue until either a match is found or the latest commit on the timeline is consumed. +.It Cm Backspace +Cancel the current search or timeline traversal +.Po +i.e., +.Sy / , +.Sy G , +or +.Sy End +.Pc . .El .Tg di .It Cm diff Oo Fl C | -no-colour Oc Oo Fl h | -help Oc Oo Fl i | -invert Oc \ Oo Fl l | -line-numbers Oc Oo Fl P | -no-prototype Oc Oo Fl q | -quiet Oc \ -Oo Fl R | -repo Ar path Oc Oo Fl s | -sbs Oc Oo Fl w | -whitespace Oc \ -Oo Fl x | -context Ar n Oc Oo Ar artifact1 Oo Ar artifact2 Oc Oc Op Ar path ... +Oo Fl R | -repo Ar path Oc Oo Fl s | -sbs Oc Oo Fl W | -whitespace-eol Oc \ +Oo Fl w | -whitespace Oc Oo Fl x | -context Ar n Oc \ +Oo Ar artifact1 Oo Ar artifact2 Oc Oc Op Ar path ... .Dl Pq alias: Cm di Display the differences between two repository artifacts, or between the local changes on disk and a given commit. If neither .Ar artifact1 nor @@ -474,19 +484,22 @@ diff view key binding. .It Fl P , -no-prototype Disable chunk header display of which function or scope each change is in, which is enabled by default. The heuristic will produce reliable results for all C-like languages -.Pq e.g., Java, Python, JavaScript, Rust ; +.Pq e.g., C/C++, Java, Python, JavaScript, Rust ; however, Lisps and non-source code .Pq e.g., Markdown, reStructuredText -will return meaningless results. This option can be toggled in-session with the +will return meaningless results. +Function prototype cannot be displayed in the chunk header with either +.Fl l|-line-numbers +or +.Fl s|-sbs +formatted diffs. +This option can be toggled in-session with the .Sy p key binding as documented below. -.Po Mutually exclusive with -.Fl l , -line-numbers -.Pc .It Fl q , -quiet Disable verbose output; that is, do not output complete content of newly added or deleted files, which are displayed by default. Verbosity can also be toggled with the .Sy v @@ -513,18 +526,34 @@ .It Fl s , -sbs Display a side-by-side formatted diff. As documented below, this option can also be toggled in-session with the .Sy s key binding. +.Po Mutually exclusive with +.Fl l , -line-numbers +.Pc +.It Fl W , -whitespace-eol +Ignore end-of-line whitespace-only changes when displaying the diff. .It Fl w , -whitespace Ignore whitespace-only changes when displaying the diff. .It Fl x , -context Ar n Set .Ar n -context lines to be shown in the diff. By default, 5 context lines are -shown. Negative values are a no-op. +context lines to be shown in the diff such that 0 \*(Le n \*(Le 64. +By default, 5 context lines are shown. Illegal values are a no-op. .El +.Pp +All the above options +.Po +sans +.Fl h +and +.Fl R +.Pc +can be made persistent as global or per-repo settings. See +.Sx ENVIRONMENT +for details. .Pp Key bindings for .Cm fnc diff are as follows: .Bl -tag -width Ds @@ -563,19 +592,25 @@ .It Cm G, End Scroll to the end of the view (i.e., last line of diff output). .It Cm gg, Home Scroll to the top of the view (i.e., first line of diff output). .It Cm C-k, K, <, \&, -Move up the +If the diff is derived from a .Cm timeline -to the previous (i.e., newer) commit and display its diff. (Only available -if the diff was accessed from the timeline.) +view, move up the timeline +to the previous (i.e., newer) commit and display its diff. +If the diff is derived from a +.Cm blame +view, display the commit diff of the previous line in the annotated file. .It Cm C-j, J, >, \&. -Move down the +If the diff is derived from a .Cm timeline -to the next (i.e., older) commit and display its diff. (Only available -if the diff was accessed from the timeline.) +view, move down the timeline +to the next (i.e., older) commit and display its diff. +If the diff is derived from a +.Cm blame +view, display the commit diff of the next line in the annotated file. .It Cm \&-, \&_ Decrease the number of context lines shown in diff output. .It Cm \&=, \&+ Increase the number of context lines shown in diff output. .It Cm # @@ -592,10 +627,21 @@ Open prompt to enter file number and navigate to that file in the diff. .It Cm i Toggle inversion of diff output. .It Cm L Toggle display of file line numbers in the diff. +.It Cm P +Write the currently viewed diff to a patch file. +.Nm +will prompt the user for a file path, which must be absolute or relative to +the current working directory. If no path is input and the +.Sy return +key is entered, the patch will be written to the current working directory +using the first ten characters of the current artifact hash as the filename +with a +.Sy .patch +extension. .It Cm p In the diff chunk header, toggle display of which function each change is in; for example: .Sy @@ -2360,10 +2361,11 @@ draw_commits(struct fnc_view *view) .It Cm S @@ -1092,10 +1138,55 @@ no-op. Default: .Qq 80 . .El .Pp +Similarly, +.Cm diff +options can be persistently applied to all diff views +.Po +i.e., whether directly accessed with +.Nm Cm diff +or via the +.Cm timeline +or +.Cm branch +commands' corresponding key maps +.Pc +by configuring the following options: +.Bl -tag -width FNC_DIFF_CONTEXT +.It Ev FNC_DIFF_FLAGS +String containing any or all of the available short form +.Cm diff +boolean flag options documented above +.Po +i.e., CilPqsWw +.Pc . +If mutually exclusive options +.Qq l +and +.Qq s +are both specified, whichever is last will take precedence; for example, +.Qq lqs +will display side-by-side formatted diffs. Default: NULL. +.It Ev FNC_DIFF_CONTEXT +Numeric value as per +the above documented +.Fl x|--context +option +.Po +i.e., +0 \*(Le n \*(Le 64 +.Pc +specifying the number of context lines. Illegal values are a no-op. +Default: 5. +.El +.Pp +Any options passed to +.Nm Cm diff +will override the above settings. +.Pp .Nm displays coloured output by default in supported terminals. Each colour object identified below can be defined by either exporting environment variables .Po e.g., .Cm export FNC_COLOUR_COMMIT=red Index: src/fnc.c ================================================================== --- src/fnc.c +++ src/fnc.c @@ -76,10 +76,15 @@ #include "libfossil.h" #include "diff.h" #define FNC_VERSION VERSION /* cf. Makefile */ +/* User options: include/settings.h:29 */ +#define STR_INFO(_) _(fnc_opt_name, FNC, USER_OPTIONS) +#define GEN_STRINGS(name, pfx, info) GEN_STR(name, pfx, info) +STR_INFO(GEN_STRINGS) + /* Utility macros. */ #define MIN(_a, _b) ((_a) < (_b) ? (_a) : (_b)) #define MAX(_a, _b) ((_a) > (_b) ? (_a) : (_b)) #define ABS(_n) ((_n) >= 0 ? (_n) : -(_n)) #ifndef CTRL @@ -151,11 +156,11 @@ # ifndef STAILQ_HEAD # define STAILQ SIMPLEQ # endif /* STAILQ_HEAD */ #endif /* OpenBSD */ -#if defined __linux__ +#ifdef __linux__ # ifndef strlcat # define strlcat(_d, _s, _sz) fsl_strlcat(_d, _s, _sz) # endif /* strlcat */ # ifndef strlcpy # define strlcpy(_d, _s, _sz) fsl_strlcpy(_d, _s, _sz) @@ -208,17 +213,22 @@ /* Blame options. */ const char *lineno; /* Line to open blame view. */ /* Diff options. */ - const char *context; /* Number of context lines. */ + union { + const char *str; + long num; + } context; /* Number of context lines. */ bool sbs; /* Display side-by-side diff. */ bool ws; /* Ignore whitespace-only changes. */ + bool eol; /* Ignore eol whitespace-only changes */ bool nocolour; /* Disable colour in diff output. */ - bool quiet; /* Disable verbose diff output. */ + bool verbose; /* Disable verbose diff output. */ bool invert; /* Toggle inverted diff output. */ bool showln; /* Display line numbers in diff. */ + bool proto; /* Display function prototype. */ /* Branch options. */ const char *before; /* Last branch change before date. */ const char *after; /* Last branch change after date. */ const char *sort; /* Lexicographical, MRU, open/closed. */ @@ -233,11 +243,11 @@ /* Command line flags and help. */ fcli_help_info fnc_help; /* Global help. */ fcli_cliflag cliflags_global[3]; /* Global options. */ fcli_command cmd_args[7]; /* App commands. */ fcli_cliflag cliflags_timeline[13]; /* Timeline options. */ - fcli_cliflag cliflags_diff[10]; /* Diff options. */ + fcli_cliflag cliflags_diff[12]; /* Diff options. */ fcli_cliflag cliflags_tree[5]; /* Tree options. */ fcli_cliflag cliflags_blame[8]; /* Blame options. */ fcli_cliflag cliflags_branch[11]; /* Branch options. */ fcli_cliflag cliflags_config[5]; /* Config options. */ } fnc_init = { @@ -247,25 +257,27 @@ 0, /* err fnc error state. */ false, /* hflag if --help is requested. */ false, /* vflag if --version is requested. */ false, /* reverse branch sort/annotation defaults to off. */ {NULL, 0}, /* filter_types defaults to indiscriminate. */ - {0}, /* nrecords defaults to all commits. */ + {NULL}, /* nrecords defaults to all commits. */ NULL, /* filter_tag defaults to indiscriminate. */ NULL, /* filter_branch defaults to indiscriminate. */ NULL, /* filter_user defaults to indiscriminate. */ NULL, /* filter_type temp placeholder for filter_types cb. */ NULL, /* glob filter defaults to off; all commits are shown */ false, /* utc defaults to off (i.e., show user local time). */ NULL, /* lineno default: open blame at the first line. */ - NULL, /* context defaults to five context lines. */ + {NULL}, /* context defaults to five context lines. */ false, /* sbs diff defaults to false (show unified diff). */ - false, /* ws defaults to acknowledge whitespace. */ + false, /* ws defaults to acknowledge all whitespace. */ + false, /* eol defaults to acknowledge eol whitespace. */ false, /* nocolour defaults to off (i.e., use diff colours). */ - false, /* quiet defaults to off (i.e., verbose diff is on). */ + true, /* verbose defaults to on. */ false, /* invert diff defaults to off. */ false, /* showln in diff defaults to off. */ + true, /* proto in diff chunk header defaults to on. */ NULL, /* before defaults to any time. */ NULL, /* after defaults to any time. */ NULL, /* sort by MRU or open/closed (dflt: lexicographical) */ false, /* closed only branches is off (defaults to all). */ false, /* open only branches is off by (defaults to all). */ @@ -368,11 +380,14 @@ "Invert difference between artifacts. Inversion can also be " "toggled\n with the 'i' key binding in diff view."), FCLI_FLAG_BOOL("l", "line-numbers", &fnc_init.showln, "Show file line numbers in diff output. Line numbers can also be " "toggled\n with the 'L' key binding in diff view."), - FCLI_FLAG_BOOL("q", "quiet", &fnc_init.quiet, + FCLI_FLAG_BOOL_INVERT("P", "no-prototype", &fnc_init.proto, + "Disable display of the enclosing function prototype in diff chunk" + "headers."), + FCLI_FLAG_BOOL_INVERT("q", "quiet", &fnc_init.verbose, "Disable verbose diff output; that is, do not output complete" " content\n of newly added or deleted files. Verbosity can also" " be toggled with\n the 'v' key binding in diff view."), FCLI_FLAG_CSTR("R", "repo", "", NULL, "Use the fossil(1) repository located at for this diff\n " @@ -379,10 +394,14 @@ "invocation."), FCLI_FLAG_BOOL("s", "sbs", &fnc_init.sbs, "Display a side-by-side, rather than the default unified, diff. " "This\n option can alse be toggled with the 'S' key binding in " "diff view."), + FCLI_FLAG_BOOL("W", "whitespace-eol", &fnc_init.eol, + "Ignore end-of-line whitespace-only changes when displaying diff.\n" + " This option can also be toggled with the 'W' key binding in " + "diff view."), FCLI_FLAG_BOOL("w", "whitespace", &fnc_init.ws, "Ignore whitespace-only changes when displaying diff. This option " "can\n also be toggled with the 'w' key binding in diff view."), FCLI_FLAG("x", "context", "", &fnc_init.context, "Show context lines when displaying diff; is capped at 64." @@ -528,10 +547,11 @@ enum fnc_search_state { SEARCH_WAITING, SEARCH_CONTINUE, SEARCH_COMPLETE, SEARCH_NO_MATCH, + SEARCH_ABORTED, SEARCH_FOR_END }; enum fnc_diff_type { FNC_DIFF_CKOUT, @@ -547,10 +567,11 @@ int flags; #define SR_CLREOL 1 << 0 #define SR_UPDATE 1 << 1 #define SR_SLEEP 1 << 2 #define SR_RESET 1 << 3 +#define SR_ALL SR_CLREOL | SR_UPDATE | SR_SLEEP | SR_RESET char buf[BUFSIZ]; long ret; }; struct fnc_colour { @@ -719,11 +740,11 @@ uint32_t n; uint32_t idx; }; struct fnc_diff_view_state { - struct fnc_view *timeline_view; + struct fnc_view *parent_view; struct fnc_commit_artifact *selected_entry; struct fnc_pathlist_head *paths; fsl_buffer buf; struct fnc_colours colours; struct index index; @@ -748,10 +769,11 @@ off_t *line_offsets; bool eof; bool colour; bool showmeta; bool showln; + bool patch; }; TAILQ_HEAD(fnc_parent_trees, fnc_parent_tree); struct fnc_tree_view_state { /* Parent trees of the- */ struct fnc_parent_trees parents; /* -current subtree. */ @@ -1000,13 +1022,14 @@ regex_t *); static int init_diff_view(struct fnc_view **, int, int, struct fnc_commit_artifact *, struct fnc_view *, bool); static int open_diff_view(struct fnc_view *, - struct fnc_commit_artifact *, int, bool, bool, - bool, bool, bool, struct fnc_view *, bool, - struct fnc_pathlist_head *); + struct fnc_commit_artifact *, + struct fnc_pathlist_head *, + struct fnc_view *, bool); +static void set_diff_opt(struct fnc_diff_view_state *); static void show_diff_status(struct fnc_view *); static int create_diff(struct fnc_diff_view_state *); static int create_changeset(struct fnc_commit_artifact *); static int write_commit_meta(struct fnc_diff_view_state *); static int countlines(const char *); @@ -1171,12 +1194,18 @@ static int sitrep(struct fnc_view *, const char *, int); static char *fnc_strsep (char **, const char *); static bool fnc_str_has_upper(const char *); static int fnc_make_sql_glob(char **, char **, const char *, bool); +#ifndef HAVE_LANDLOCK static int init_unveil(const char *, const char *, bool); +#else static int init_landlock(const char **, const int); +static const char *gettzfile(void); +#define init_unveil(_r, _c, _s) \ + init_landlock((const char*[]){_r, _c, P_tmpdir, gettzfile()}, 4) +#endif /* HAVE_LANDLOCK */ static const char *getdirname(const char *, fsl_int_t, bool); static int set_colours(struct fnc_colours *, enum fnc_view_id); static int set_colour_scheme(struct fnc_colours *, const int (*)[2], const char **, int); static int init_colour(enum fnc_opt_id); @@ -1335,13 +1364,10 @@ rc = init_curses(); if (rc) goto end; rc = init_unveil(REPODB, CKOUTDIR, false); if (rc) - goto end; - rc = init_landlock((const char*[]){REPODIR, CKOUTDIR, P_tmpdir}, 3); - if (rc) goto end; rc = init_timeline_view(&v, 0, 0, rid, path, glob); if (!rc) rc = view_loop(v); @@ -1790,10 +1816,12 @@ !fnc_str_has_upper(fnc_init.filter_branch)); if (rc) goto end; idtag = fsl_db_g_id(db, 0, "SELECT tagid FROM tag WHERE tagname %q 'sym-%q'" + " AND EXISTS(SELECT 1 FROM tagxref" + " WHERE tag.tagid = tagxref.tagid AND tagtype > 0)" " ORDER BY tagid DESC", op, str); if (idtag) { rc = fsl_buffer_appendf(&sql, " AND EXISTS(SELECT 1 FROM tagxref" " WHERE tagid=%"FSL_ID_T_PFMT @@ -2471,22 +2499,30 @@ if (tcx->ncommits_needed > 0 && !tcx->eotl) { if ((idxstr = fsl_mprintf(" [%d/%d] %s", entry ? entry->idx + 1 : 0, s->commits.ncommits, (view->searching && !view->search_status) ? - "searching..." : "loading...")) == NULL) { + "searching..." : view->search_status == SEARCH_ABORTED ? + "aborted" : "loading...")) == NULL) { rc = RC(FSL_RC_RANGE, "%s", "fsl_mprintf"); goto end; } } else { if (view->searching) { - if (view->search_status == SEARCH_COMPLETE) + switch (view->search_status) { + case SEARCH_COMPLETE: search_str = "no more matches"; - else if (view->search_status == SEARCH_NO_MATCH) + break; + case SEARCH_NO_MATCH: search_str = "no matches found"; - else if (view->search_status == SEARCH_WAITING) + break; + case SEARCH_WAITING: search_str = "searching..."; + /* FALL THROUGH */ + default: + break; + } } if ((idxstr = fsl_mprintf("%s [%d/%d] %s", !fsl_strcmp(uuid, s->curr_ckout_uuid) ? " [current]" : "", entry ? entry->idx + 1 : 0, s->commits.ncommits, @@ -2943,11 +2979,15 @@ rc = cycle_view(view); break; case KEY_F(1): case 'H': case '?': - help(view); + rc = help(view); + if (rc == FSL_RC_BREAK) { + rc = FSL_RC_OK; + *done = 1; + } break; case 'q': if (view->parent && view->parent->vid == FNC_VIEW_TIMELINE && view->mode == VIEW_SPLIT_HRZN) { /* May need more commits to fill fullscreen. */ @@ -3072,10 +3112,14 @@ {" C-f,PgDn ", " ❬C-f❭❬PgDn❭ "}, {" C-u, ", " ❬C-u❭ "}, {" C-d, ", " ❬C-d❭ "}, {" gg,Home ", " ❬gg❭❬Home❭ "}, {" G,End ", " ❬G❭❬End❭ "}, + {" l ", " ❬l❭❬→❭ "}, + {" h ", " ❬h❭❬←❭ "}, + {" $ ", " ❬$❭ "}, + {" 0 ", " ❬0❭ "}, {" Tab ", " ❬TAB❭ "}, {" c ", " ❬c❭ "}, {" f ", " ❬f❭ "}, {" / ", " ❬/❭ "}, {" n ", " ❬n❭ "}, @@ -3090,30 +3134,29 @@ {" Space ", " ❬Space❭ "}, {" b ", " ❬b❭ "}, {" C ", " ❬C❭ "}, {" F ", " ❬F❭ "}, {" t ", " ❬t❭ "}, + {" ", " ❬⌫❭ "}, {""}, {""}, /* Diff */ {" Space ", " ❬Space❭ "}, {" # ", " ❬#❭ "}, {" @ ", " ❬@❭ "}, - {" $ ", " ❬$❭ "}, - {" 0 ", " ❬0❭ "}, {" C-e ", " ❬C-e❭ "}, {" C-y ", " ❬C-y❭ "}, {" C-n ", " ❬C-n❭ "}, {" C-p ", " ❬C-p❭ "}, - {" l ", " ❬l❭❬→❭ "}, - {" h ", " ❬h❭❬←❭ "}, {" b ", " ❬b❭ "}, {" F ", " ❬F❭ "}, {" i ", " ❬i❭ "}, {" L ", " ❬L❭ "}, + {" P ", " ❬P❭ "}, {" p ", " ❬p❭ "}, {" S ", " ❬S❭ "}, {" v ", " ❬v❭ "}, + {" W ", " ❬W❭ "}, {" w ", " ❬w❭ "}, {" -,_ ", " ❬-❭❬_❭ "}, {" +,= ", " ❬+❭❬=❭ "}, {" C-k,K,<,, ", " ❬C-k❭❬K❭❬<❭❬,❭ "}, {" C-j,J,>,. ", " ❬C-j❭❬J❭❬>❭❬.❭ "}, @@ -3129,14 +3172,10 @@ {""}, /* Blame */ {" Space ", " ❬Space❭ "}, {" Enter ", " ❬Enter❭ "}, {" # ", " ❬#❭ "}, {" @ ", " ❬@❭ "}, - {" $ ", " ❬$❭ "}, - {" 0 ", " ❬0❭ "}, - {" l ", " ❬l❭❬→❭ "}, - {" h ", " ❬h❭❬←❭ "}, {" b ", " ❬b❭ "}, {" p ", " ❬p❭ "}, {" B ", " ❬B❭ "}, {" T ", " ❬T❭ "}, {""}, @@ -3161,10 +3200,14 @@ "Scroll view down one page", "Scroll view up one half page", "Scroll view down one half page", "Jump to first line or start of the view", "Jump to last line or end of the view", + "Scroll the view right (diff, blame, help)", + "Scroll the view left (diff, blame, help)", + "Scroll right to the end of the longest line (diff, blame, help)", + "Scroll left to the beginning of the line (diff, blame, help)", "Switch focus between open views", "Toggle coloured output", "Toggle fullscreen", "Open prompt to enter search term (not available in this view)", "Find next line or token matching the current search term", @@ -3179,35 +3222,34 @@ "(Un)tag (or diff) the selected (against the tagged) commit", "Open and populate branch view with all repository branches", "Diff local changes in the checkout against selected commit", "Open prompt to enter term with which to filter new timeline view", "Display a tree reflecting the state of the selected commit", + "Cancel the current search or timeline traversal", "", "Diff", "Scroll down one page of diff output", "Toggle display of diff view line numbers", "Open prompt to enter line number and navigate to line", - "Scroll the view right to the end of the longest line", - "Scroll the view left to the beginning of the line", "Scroll the view down in the buffer", "Scroll the view up in the buffer", "Navigate to next file in the diff", "Navigate to previous file in the diff", - "Scroll the view right", - "Scroll the view left", "Open and populate branch view with all repository branches", "Open prompt to enter file number and navigate to file", "Toggle inversion of diff output", "Toggle display of file line numbers", + "Prompt for path to write a patch of the currently viewed diff", "Toggle display of function name in chunk header", "Display side-by-side formatted diff", "Toggle verbosity of diff output", + "Toggle ignore end-of-line whitespace-only changes in diff", "Toggle ignore whitespace-only changes in diff", "Decrease the number of context lines", "Increase the number of context lines", - "Display diff of next (newer) commit in the timeline", - "Display diff of previous (older) commit in the timeline", + "Display commit diff of next line in the file / timeline entry", + "Display commit diff of previous line in the file / timeline entry", "", "Tree", "Move into the selected directory", "Return to the parent directory", "Open and populate branch view with all repository branches", @@ -3218,14 +3260,10 @@ "Blame", "Scroll down one page", "Display the diff of the commit corresponding to the selected line", "Toggle display of file line numbers", "Open prompt to enter line number and navigate to line", - "Scroll the view right to the end of the longest line", - "Scroll the view left to the beginning of the line", - "Scroll the view right", - "Scroll the view left", "Blame the version of the file found in the selected line's commit", "Blame the version of the file found in the selected line's parent " "commit", "Reload the previous blamed version of the file", "Open and populate branch view with all repository branches", @@ -3286,15 +3324,15 @@ { WINDOW *win, *content; char *line = NULL; ssize_t linelen; size_t linesz; - int ch, cury, end, wy, wx, x0, y0; + int ch, cury, curx, end, wy, wx, x0, y0, rc = FSL_RC_OK; x0 = 4; /* Number of columns to border window. */ y0 = 2; /* Number of lines to border window. */ - cury = 0; + cury = curx = 0; wx = getmaxx(view->window) - ((x0 + 1) * 2); /* Width of window. */ wy = getmaxy(view->window) - ((y0 + 1) * 2); /* Height of window */ ch = ERR; if ((win = newwin(wy, wx, y0, x0)) == 0) @@ -3307,11 +3345,11 @@ doupdate(); keypad(content, TRUE); /* Write text content to pad. */ if (title) - centerprint(content, 0, 0, width, title, 0); + centerprint(content, 0, 0, wx, title, 0); while ((linelen = getline(&line, &linesz, txt)) != -1) waddstr(content, line); fsl_free(line); end = (getcury(content) - (wy - 3)); /* No. lines past end of pad. */ @@ -3342,10 +3380,25 @@ cury += wy - 3; if (cury > end) cury = end; } break; + case '0': + curx = 0; + break; + case '$': + curx = MAX(width - wx / 2, 0); + break; + case KEY_LEFT: + case 'h': + curx -= MIN(curx, 2); + break; + case KEY_RIGHT: + case 'l': + if (curx + wx / 2 < width) + curx += 2; + break; case 'g': if (!fnc_home(view)) break; /* FALL THROUGH */ case KEY_HOME: @@ -3353,20 +3406,23 @@ break; case KEY_END: case 'G': cury = end; break; + case 'Q': + rc = FSL_RC_BREAK; + /* FALL THROUGH */ case ERR: default: break; } werase(win); box(win, 0, 0); wnoutrefresh(win); - pnoutrefresh(content, cury, 0, y0 + 1, x0 + 1, wy, wx); + pnoutrefresh(content, cury, curx, y0 + 1, x0 + 1, wy, wx); doupdate(); - } while ((ch = wgetch(content)) != 'q' && ch != KEY_ESCAPE + } while (!rc && (ch = wgetch(content)) != 'q' && ch != KEY_ESCAPE && ch != ERR); /* Destroy window. */ werase(win); wrefresh(win); @@ -3376,11 +3432,11 @@ /* Restore fnc window content. */ touchwin(view->window); wnoutrefresh(view->window); doupdate(); - return 0; + return rc; } static void centerprint(WINDOW *win, int starty, int startx, int cols, const char *str, chtype colour) @@ -3464,21 +3520,25 @@ s->commits.ncommits; rc = signal_tl_thread(view, 1); } break; case 'C': { + if (s->selected_entry->commit->type[0] != 'c') { + rc = sitrep(view, "-- requires check-in artifact --", + SR_ALL); + break; + } fsl_cx *const f = fcli_cx(); /* * XXX This is not good but I can't think of an alternative * without patching libf: fsl_ckout_changes_scan() returns a * db lock error via fsl_vfile_changes_scan() when versioned * files are modified in-session. Clear it and notify user. */ rc = fsl_ckout_changes_scan(f); if (rc == FSL_RC_DB) { - rc = sitrep(view, "-- checkout db busy --", - SR_CLREOL | SR_UPDATE | SR_SLEEP | SR_RESET); + rc = sitrep(view, "-- checkout db busy --", SR_ALL); break; } else if (rc) return RC(rc, "%s", "fsl_ckout_changes_scan"); if (!fsl_ckout_has_changes(f)) { sitrep(view, "-- no local changes --", @@ -3507,12 +3567,11 @@ if (rc) return rc; s->glob = input.buf; rc = request_view(new_view, view, FNC_VIEW_TIMELINE); if (rc == FSL_RC_BREAK) { - rc = sitrep(view, "-- no matching commits --", - SR_CLREOL | SR_UPDATE | SR_SLEEP | SR_RESET); + rc = sitrep(view, "-- no matching commits --", SR_ALL); } break; } case 't': if (s->selected_entry == NULL) @@ -4081,12 +4140,12 @@ ch = wgetch(view->window); if ((rc = pthread_mutex_lock(&fnc_mutex))) return RC(fsl_errno_to_rc(rc, FSL_RC_ACCESS), "%s", "pthread_mutex_lock"); if (ch == KEY_BACKSPACE) { - view->search_status = SEARCH_CONTINUE; - return rc; + view->search_status = SEARCH_ABORTED; + goto end; } if (view->searching == SEARCH_FORWARD) entry = TAILQ_NEXT(s->search_commit, entries); else entry = TAILQ_PREV(s->search_commit, commit_tailhead, @@ -4116,12 +4175,11 @@ if (s->thread_cx.eotl || view->searching == SEARCH_REVERSE) { view->search_status = (s->matched_commit == NULL ? SEARCH_NO_MATCH : SEARCH_COMPLETE); s->search_commit = NULL; - cbreak(); - return rc; + goto end; } /* * Wake the timeline thread to produce more commits. * Search will resume at s->search_commit upon return. */ @@ -4144,24 +4202,26 @@ } if (s->matched_commit) { int cur = s->selected_entry->idx; while (cur < s->matched_commit->idx) { - if ((rc = tl_input_handler(NULL, view, KEY_DOWN))) + rc = tl_input_handler(NULL, view, KEY_DOWN); + if (rc) return rc; ++cur; } while (cur > s->matched_commit->idx) { - if ((rc = tl_input_handler(NULL, view, KEY_UP))) + rc = tl_input_handler(NULL, view, KEY_UP); + if (rc) return rc; --cur; } } s->search_commit = NULL; +end: cbreak(); - return rc; } static bool find_commit_match(struct fnc_commit_artifact *commit, @@ -4319,73 +4379,57 @@ return 0; } static int init_diff_view(struct fnc_view **new_view, int start_col, int start_ln, - struct fnc_commit_artifact *commit, struct fnc_view *timeline_view, + struct fnc_commit_artifact *commit, struct fnc_view *parent_view, bool showmeta) { struct fnc_view *diff_view; int rc = 0; diff_view = view_open(0, 0, start_ln, start_col, FNC_VIEW_DIFF); if (diff_view == NULL) return RC(FSL_RC_ERROR, "%s", "view_open"); - rc = open_diff_view(diff_view, commit, DEF_DIFF_CTX, fnc_init.sbs, - fnc_init.ws, fnc_init.invert, !fnc_init.quiet, fnc_init.showln, - timeline_view, showmeta, NULL); + rc = open_diff_view(diff_view, commit, NULL, parent_view, showmeta); if (!rc) *new_view = diff_view; return rc; } static int open_diff_view(struct fnc_view *view, struct fnc_commit_artifact *commit, - int context, bool sbs, bool ignore_ws, bool invert, bool verbosity, - bool showln, struct fnc_view *timeline_view, bool showmeta, - struct fnc_pathlist_head *paths) + struct fnc_pathlist_head *paths, struct fnc_view *parent_view, + bool showmeta) { struct fnc_diff_view_state *s = &view->state.diff; - char *opt; - int rc = 0; + int rc = FSL_RC_OK; - opt = fnc_conf_getopt(FNC_COLOUR_HL_LINE, false); - if (!fsl_stricmp(opt, "mono")) - s->sline = SLINE_MONO; - fsl_free(opt); + set_diff_opt(s); s->index.n = 0; s->index.idx = 0; s->paths = paths; s->selected_entry = commit; s->first_line_onscreen = 1; s->last_line_onscreen = view->nlines; s->selected_line = 1; s->f = NULL; - s->context = context; - s->sbs = 0; - FLAG_SET(s->diff_flags, FNC_DIFF_PROTOTYPE); - sbs ? FLAG_SET(s->diff_flags, FNC_DIFF_SIDEBYSIDE) : 0; - verbosity ? FLAG_SET(s->diff_flags, FNC_DIFF_VERBOSE) : 0; - ignore_ws ? FLAG_SET(s->diff_flags, FNC_DIFF_IGNORE_ALLWS) : 0; - invert ? FLAG_SET(s->diff_flags, FNC_DIFF_INVERT) : 0; - showln ? FLAG_SET(s->diff_flags, FNC_DIFF_LINENO) : 0; - s->timeline_view = timeline_view; - s->colour = !fnc_init.nocolour && has_colors(); + s->parent_view = parent_view; s->showmeta = showmeta; if (s->colour) { STAILQ_INIT(&s->colours); rc = set_colours(&s->colours, FNC_VIEW_DIFF); if (rc) return rc; } - if (timeline_view && screen_is_split(view)) - show_timeline_view(timeline_view); /* draw vborder */ + if (parent_view && screen_is_split(view)) + show_timeline_view(parent_view); /* draw vborder */ show_diff_status(view); s->line_offsets = NULL; s->nlines = 0; s->dlines = NULL; @@ -4404,10 +4448,100 @@ view->grep_init = diff_grep_init; view->grep = find_next_match; return rc; } + +/* + * Set diff options. Precedence is: + * 1. CLI options passed to 'fnc diff' (see: fnc diff -h) + * 2. global options set via envvars + * - FNC_DIFF_CONTEXT: n + * - FNC_DIFF_FLAGS: CilPqsw (see: fnc diff -h for all boolean flags) + * - FNC_COLOUR_HL_LINE: mono, auto + * 3. repo options set via 'fnc set' + * - same as (2) global + * 4. fnc default options + * Input is validated; supplant bogus values with defaults. + */ +static void +set_diff_opt(struct fnc_diff_view_state *s) +{ + char *opt; + char ch; + long ctx = DEF_DIFF_CTX; + int i = 0; + + /* Command line options. */ + fnc_init.verbose ? FLAG_SET(s->diff_flags, FNC_DIFF_VERBOSE) : 0; + fnc_init.proto ? FLAG_SET(s->diff_flags, FNC_DIFF_PROTOTYPE) : 0; + fnc_init.ws ? FLAG_SET(s->diff_flags, FNC_DIFF_IGNORE_ALLWS) : 0; + fnc_init.eol ? FLAG_SET(s->diff_flags, FNC_DIFF_IGNORE_EOLWS) : 0; + fnc_init.invert ? FLAG_SET(s->diff_flags, FNC_DIFF_INVERT) : 0; + s->colour = !fnc_init.nocolour && has_colors(); + + if (fnc_init.context.str) /* fnc diff -x|--context */ + opt = fsl_strdup(fnc_init.context.str); + else /* fnc set option */ + opt = fnc_conf_getopt(FNC_DIFF_CONTEXT, false); + if (opt) + strtonumcheck(&ctx, opt, 0, MAX_DIFF_CTX); + + s->context = ctx; + fsl_free(opt); + + /* Persistent options (i.e., 'fnc set' or envvars). */ + opt = fnc_conf_getopt(FNC_COLOUR_HL_LINE, false); + if (!fsl_stricmp(opt, "mono")) + s->sline = SLINE_MONO; + fsl_free(opt); + + opt = fnc_conf_getopt(FNC_DIFF_FLAGS, false); + while (opt && (ch = opt[i++])) { + switch (ch) { + case 'C': + s->colour = false; + break; + case 'i': + FLAG_SET(s->diff_flags, FNC_DIFF_INVERT); + break; + case 'l': + if (FLAG_CHK(s->diff_flags, FNC_DIFF_SIDEBYSIDE)) + FLAG_CLR(s->diff_flags, FNC_DIFF_SIDEBYSIDE); + FLAG_SET(s->diff_flags, FNC_DIFF_LINENO); + break; + case 'P': + FLAG_CLR(s->diff_flags, FNC_DIFF_PROTOTYPE); + break; + case 'q': + FLAG_CLR(s->diff_flags, FNC_DIFF_VERBOSE); + break; + case 's': + if (FLAG_CHK(s->diff_flags, FNC_DIFF_LINENO)) + FLAG_CLR(s->diff_flags, FNC_DIFF_LINENO); + FLAG_SET(s->diff_flags, FNC_DIFF_SIDEBYSIDE); + break; + case 'W': + FLAG_SET(s->diff_flags, FNC_DIFF_IGNORE_EOLWS); + break; + case 'w': + FLAG_SET(s->diff_flags, FNC_DIFF_IGNORE_ALLWS); + /* FALL THROUGH */ + default: + break; + } + } + fsl_free(opt); + + fnc_init.sbs ? FLAG_SET(s->diff_flags, FNC_DIFF_SIDEBYSIDE) : 0; + if (fnc_init.showln) { + /* Can't be activated if sbs is already set so clear it. */ + if (FLAG_CHK(s->diff_flags, FNC_DIFF_SIDEBYSIDE)) + FLAG_CLR(s->diff_flags, FNC_DIFF_SIDEBYSIDE); + FLAG_SET(s->diff_flags, FNC_DIFF_LINENO); + } +} static void show_diff_status(struct fnc_view *view) { mvwaddstr(view->window, 0, 0, "generating diff..."); @@ -4504,10 +4638,26 @@ */ if (s->selected_entry->diff_type == FNC_DIFF_COMMIT) diff_commit(s); else if (s->selected_entry->diff_type == FNC_DIFF_CKOUT) diff_checkout(s); + + if (s->patch) { + char *dflt = fsl_mprintf("path [%.10s.patch]: ", s->id2); + struct input in = {NULL, dflt, INPUT_ALPHA, SR_CLREOL}; + fnc_prompt_input(s->parent_view ? + s->parent_view->child : NULL, &in); + fsl_free(dflt); + if (!in.buf[0]) { + fsl_strlcpy(in.buf, s->id2, 11); + fsl_strlcat(in.buf, ".patch", sizeof(in.buf)); + } + s->patch = false; + rc = fsl_buffer_to_filename(&s->buf, in.buf); + if (rc) + goto end; + } /* * Parse the diff buffer line-by-line to record byte offsets of each * line for scrolling and searching in diff view. */ @@ -4609,18 +4759,19 @@ off_t off = 0; int rc = FSL_RC_OK, n = 7; /* Min lines in commit meta */ n += countlines(s->selected_entry->comment); n += s->selected_entry->changeset.used; + rc = add_line_type(&s->dlines, LINE_DIFF_META, &s->ndlines, n, true); + if (rc) + goto end; if ((n = fprintf(s->f,"%s %s\n", s->selected_entry->type, s->selected_entry->uuid)) < 0) goto end; off += n; - rc = add_line_type(&s->dlines, LINE_DIFF_META, &s->ndlines, n, true); - if (!rc) - rc = add_line_offset(&s->line_offsets, &s->nlines, off); + rc = add_line_offset(&s->line_offsets, &s->nlines, off); if (rc) goto end; if ((n = fprintf(s->f,"user: %s\n", s->selected_entry->user)) < 0) goto end; @@ -5895,18 +6046,19 @@ static int diff_input_handler(struct fnc_view **new_view, struct fnc_view *view, int ch) { struct fnc_view *branch_view; struct fnc_diff_view_state *s = &view->state.diff; + struct fnc_blame_view_state *bs; struct fnc_tl_view_state *tlstate; struct commit_entry *previous_selection; char *line = NULL; ssize_t linelen; size_t linesz = 0; int nlines, i = 0, rc = FSL_RC_OK; uint16_t nscroll = view->nlines - 2; - bool tl_down = false; + bool down = false; nlines = s->nlines; s->lineno = s->first_line_onscreen - 1 + s->selected_line; switch (ch) { @@ -6113,20 +6265,25 @@ view->focus_child = true; } else *new_view = branch_view; break; } + case 'P': + s->patch = true; + rc = create_diff(s); + break; case 'c': case 'i': case 'L': case 'p': case 'S': case 'v': + case 'W': case 'w': if (ch == 'c') s->colour = !s->colour; - /* LSipvw key maps don't apply to tag or ticket artifacts. */ + /* LSipvWw key maps don't apply to tag or ticket artifacts. */ if (*s->selected_entry->type == 't' && (s->selected_entry->type[1] == 'a' || s->selected_entry->type[1] == 'i')) break; else if (ch == 'i') @@ -6137,10 +6294,12 @@ FLAG_TOG(s->diff_flags, FNC_DIFF_PROTOTYPE); else if (ch == 'S') FLAG_TOG(s->diff_flags, FNC_DIFF_SIDEBYSIDE); else if (ch == 'v') FLAG_TOG(s->diff_flags, FNC_DIFF_VERBOSE); + else if (ch == 'W') + FLAG_TOG(s->diff_flags, FNC_DIFF_IGNORE_EOLWS); else if (ch == 'w') FLAG_TOG(s->diff_flags, FNC_DIFF_IGNORE_ALLWS); rc = reset_diff_view(view, true); break; case '-': @@ -6159,36 +6318,53 @@ break; case CTRL('j'): case '>': case '.': case 'J': - tl_down = true; + down = true; /* FALL THROUGH */ case CTRL('k'): case '<': case ',': case 'K': - if (s->timeline_view == NULL) - break; - tlstate = &s->timeline_view->state.timeline; - previous_selection = tlstate->selected_entry; - - rc = tl_input_handler(NULL, s->timeline_view, tl_down ? - KEY_DOWN : KEY_UP); - if (rc) - break; - - if (previous_selection == tlstate->selected_entry) - break; - - rc = set_selected_commit(s, tlstate->selected_entry); - if (rc) - break; - - s->selected_line = 1; - reset_diff_view(view, false); - break; + if (s->parent_view == NULL) + break; + if (s->parent_view->vid == FNC_VIEW_TIMELINE) { + tlstate = &s->parent_view->state.timeline; + previous_selection = tlstate->selected_entry; + + rc = tl_input_handler(NULL, s->parent_view, down ? + KEY_DOWN : KEY_UP); + if (rc) + break; + + if (previous_selection == tlstate->selected_entry) + break; + + rc = set_selected_commit(s, tlstate->selected_entry); + } else if (s->parent_view->vid == FNC_VIEW_BLAME) { + bs = &s->parent_view->state.blame; + fsl_uuid_cstr prev_id = bs->selected_entry->uuid; + + rc = blame_input_handler(&view, s->parent_view, + down ? KEY_DOWN : KEY_UP); + if (rc) + break; + + if (!fsl_uuidcmp(get_selected_commit_id(bs->blame.lines, + bs->blame.nlines, bs->first_line_onscreen, + bs->selected_line), prev_id)) + break; + + rc = blame_input_handler(&view, s->parent_view, + KEY_ENTER); + } + if (!rc) { + s->selected_line = 1; + rc = reset_diff_view(view, false); + } + /* FALL THROUGH */ default: break; } return rc; @@ -6627,13 +6803,13 @@ static void usage_diff(void) { fsl_fprintf(fnc_init.err ? stderr : stdout, " usage: %s diff [-C|--no-colour] [-R path] [-h|--help] " - "[-i|--invert] [-l|--line-numbers] [-q|--quiet] [-s|--sbs] " - "[-w|--whitespace] [-x|--context n] [artifact1 [artifact2]] " - "[path ...]\n" + "[-i|--invert] [-l|--line-numbers] [-P|--no-prototype] " + "[-q|--quiet] [-s|--sbs] [-W|--whitespace-eol] [-w|--whitespace] " + "[-x|--context n] [artifact1 [artifact2]] [path ...]\n" " e.g.: %s diff --sbs d34db33f c0ff33 src/*.c\n\n", fcli_progname(), fcli_progname()); } static void @@ -6688,11 +6864,10 @@ fsl_deck d = fsl_deck_empty; fsl_stmt *q = NULL; const char *artifact1 = NULL, *artifact2 = NULL; char *path0 = NULL; fsl_id_t prid = -1, rid = -1; - long context = DEF_DIFF_CTX; int rc = FSL_RC_OK; unsigned short blob = 0; enum fnc_diff_type diff_type = FNC_DIFF_CKOUT; bool showmeta = false; @@ -6836,30 +7011,18 @@ if (rc) goto end; rc = init_unveil(REPODB, CKOUTDIR, false); if (rc) goto end; - rc = init_landlock((const char*[]){REPODIR, CKOUTDIR, P_tmpdir}, 3); - if (rc) - goto end; - - if (fnc_init.context) { - if ((rc = strtonumcheck(&context, fnc_init.context, INT_MIN, - INT_MAX))) - goto end; - context = MIN(MAX_DIFF_CTX, context); - } view = view_open(0, 0, 0, 0, FNC_VIEW_DIFF); if (view == NULL) { rc = RC(FSL_RC_ERROR, "%s", "view_open"); goto end; } - rc = open_diff_view(view, commit, context, fnc_init.sbs, fnc_init.ws, - fnc_init.invert, !fnc_init.quiet, fnc_init.showln, NULL, - showmeta, &paths); + rc = open_diff_view(view, commit, &paths, NULL, showmeta); if (!rc) rc = view_loop(view); end: fsl_free(path0); fsl_deck_finalize(&d); @@ -6948,13 +7111,10 @@ rc = init_curses(); if (rc) goto end; rc = init_unveil(REPODB, CKOUTDIR, false); if (rc) - goto end; - rc = init_landlock((const char*[]){REPODIR, CKOUTDIR, P_tmpdir}, 3); - if (rc) goto end; view = view_open(0, 0, 0, 0, FNC_VIEW_TREE); if (view == NULL) { RC(FSL_RC_ERROR, "%s", "view_open"); @@ -7192,11 +7352,16 @@ *tree = fsl_malloc(sizeof(**tree)); if (*tree == NULL) return RC(FSL_RC_ERROR, "%s", "fsl_malloc"); memset(*tree, 0, sizeof(**tree)); - /* Count how many elements will comprise the tree to be allocated. */ + /* + * Count how many elements will comprise the tree to be allocated. + * If dir is the root of the repository tree (i.e., "/"), only tree + * nodes (tn) with no parent_dir belong to this tree. Otherwise, tree + * nodes whose parent_dir matches dir will comprise the requested tree. + */ for(tn = repo->head; tn; tn = tn->next) { if ((!tn->parent_dir && fsl_strcmp(dir, "/")) || (tn->parent_dir && fsl_strcmp(dir, tn->parent_dir->path))) continue; ++i; @@ -8237,13 +8402,10 @@ enum fnc_opt_id setid; int rc = FSL_RC_OK; rc = init_unveil(REPODIR, CKOUTDIR, true); if (rc) - return rc; - rc = init_landlock((const char*[]){REPODIR, CKOUTDIR, P_tmpdir}, 3); - if (rc) return rc; rc = fcli_process_flags(argv->flags); if (rc || (rc = fcli_has_unused_flags(false))) return rc; @@ -8433,11 +8595,11 @@ nitems(regexp_blame)); break; } case FNC_VIEW_BRANCH: { static const char *regexp_branch[] = { - "^\\ +", "^ -", "@$", "\\*$" + "^\\[[+]] ", "^\\[[-]] ", "@$", "\\*$" }; const int pairs_branch[][2] = { {FNC_COLOUR_BRANCH_OPEN, init_colour(FNC_COLOUR_BRANCH_OPEN)}, {FNC_COLOUR_BRANCH_CLOSED, @@ -8754,13 +8916,10 @@ rc = init_curses(); if (rc) goto end; rc = init_unveil(REPODB, CKOUTDIR, false); if (rc) - goto end; - rc = init_landlock((const char*[]){REPODIR, CKOUTDIR, P_tmpdir}, 3); - if (rc) goto end; view = view_open(0, 0, 0, 0, FNC_VIEW_BLAME); if (view == NULL) { rc = RC(FSL_RC_ERROR, "%s", view_open); @@ -9417,11 +9576,11 @@ return true; } static int -blame_input_handler(struct fnc_view **new_view, struct fnc_view *view, int ch) +blame_input_handler(struct fnc_view **alt_view, struct fnc_view *view, int ch) { struct fnc_view *branch_view, *diff_view; struct fnc_blame_view_state *s = &view->state.blame; int start_col = 0, rc = FSL_RC_OK; uint16_t nscroll = view->nlines - 2; @@ -9442,12 +9601,10 @@ case 'h': view->pos.col -= MIN(view->pos.col, 2); break; case 'q': s->done = true; - if (s->selected_entry) - fnc_commit_artifact_close(s->selected_entry); break; case 'c': s->colour = !s->colour; break; case 'g': @@ -9633,11 +9790,11 @@ if (rc) return rc; view_set_child(view, branch_view); view->focus_child = true; } else - *new_view = branch_view; + *alt_view = branch_view; break; case KEY_ENTER: case '\r': { fsl_cx *const f = fcli_cx(); struct fnc_commit_artifact *commit = NULL; @@ -9657,39 +9814,44 @@ fsl_stmt_finalize(q); if (rc) { fnc_commit_artifact_close(commit); break; } - if (view_is_parent(view)) - start_col = view_split_start_col(view->start_col); - diff_view = view_open(0, 0, 0, start_col, FNC_VIEW_DIFF); - if (diff_view == NULL) { - fnc_commit_artifact_close(commit); - rc = RC(FSL_RC_ERROR, "%s", "view_open"); - break; - } - rc = open_diff_view(diff_view, commit, DEF_DIFF_CTX, - fnc_init.sbs, fnc_init.ws, fnc_init.invert, - !fnc_init.quiet, fnc_init.showln, NULL, true, NULL); + if (*alt_view) { /* release diff resources before opening new */ + rc = close_diff_view(*alt_view); + if (rc) + break; + diff_view = *alt_view; + } else { + if (view_is_parent(view)) + start_col = view_split_start_col(view->start_col); + diff_view = view_open(0, 0, 0, start_col, FNC_VIEW_DIFF); + if (diff_view == NULL) { + fnc_commit_artifact_close(commit); + rc = RC(FSL_RC_ERROR, "%s", "view_open"); + break; + } + } + rc = open_diff_view(diff_view, commit, NULL, view, true); s->selected_entry = commit; if (rc) { fnc_commit_artifact_close(commit); view_close(diff_view); break; } + if (*alt_view) /* view is already active */ + break; view->active = false; diff_view->active = true; if (view_is_parent(view)) { rc = view_close_child(view); - if (rc) - break; - view_set_child(view, diff_view); - view->focus_child = true; + if (!rc) { + view_set_child(view, diff_view); + view->focus_child = true; + } } else - *new_view = diff_view; - if (rc) - break; + *alt_view = diff_view; break; } case KEY_RESIZE: if (s->selected_line > view->nlines - 2) { s->selected_line = MIN(s->blame.nlines, @@ -9758,10 +9920,12 @@ fnc_commit_qid_free(blamed_commit); } fsl_free(s->path); free_colours(&s->colours); + if (s->selected_entry) + fnc_commit_artifact_close(s->selected_entry); return rc; } static int @@ -9891,13 +10055,10 @@ rc = init_curses(); if (rc) goto end; rc = init_unveil(REPODB, CKOUTDIR, false); if (rc) - goto end; - rc = init_landlock((const char*[]){REPODIR, CKOUTDIR, P_tmpdir}, 3); - if (rc) goto end; view = view_open(0, 0, 0, 0, FNC_VIEW_BRANCH); if (view == NULL) { rc = RC(FSL_RC_ERROR, "%s", "view_open"); @@ -10033,12 +10194,12 @@ const char *brname = fsl_stmt_g_text(stmt, 0, NULL); bool priv = (curr_branch && fsl_stmt_g_int32(stmt, 1) == 1); bool open = fsl_stmt_g_int32(stmt, 2) == 0; double mtime = fsl_stmt_g_int64(stmt, 3); bool curr = curr_branch && !fsl_strcmp(curr_branch, brname); - if ((s->when > 0 && mtime < s->dateline) || - (s->when < 0 && mtime > s->dateline)) + if (!brname || !*brname || (s->when > 0 && mtime < s->dateline) + || (s->when < 0 && mtime > s->dateline)) continue; rc = alloc_branch(&new_branch, brname, mtime, open, priv, curr); if (rc) goto end; rc = fnc_branchlist_insert(&be, &s->branches, new_branch); @@ -10183,10 +10344,11 @@ struct fnc_branchlist_entry *be; struct fnc_colour *c = NULL; char *line = NULL; wchar_t *wline; int limit, n, width, rc = 0; + attr_t rx = A_BOLD; werase(view->window); s->ndisplayed = 0; limit = view->nlines; @@ -10202,19 +10364,23 @@ rc = formatln(&wline, &width, line, view->ncols, 0, false); if (rc) { fsl_free(line); return rc; } - if (screen_is_shared(view)) - wattron(view->window, A_REVERSE); + if (screen_is_shared(view) || view->active) + rx |= A_REVERSE; + if (s->colour) + c = get_colour(&s->colours, FNC_COLOUR_BRANCH_CURRENT); + if (c) + rx |= COLOR_PAIR(c->scheme); + wattron(view->window, rx); waddwstr(view->window, wline); while (width < view->ncols) { waddch(view->window, ' '); ++width; } - if (screen_is_shared(view)) - wattroff(view->window, A_REVERSE); + wattroff(view->window, rx); fsl_free(wline); wline = NULL; fsl_free(line); line = NULL; if (width < view->ncols - 1) @@ -10224,17 +10390,18 @@ n = 0; while (be && limit > 0) { char *line = NULL; - line = fsl_mprintf(" %c%s%s%s%s%s%s%c %s", - be->branch->open ? '+' : '-', be->branch->name, + line = fsl_mprintf("[%c] %s%s%s%s%s%s%s", + be->branch->open ? '+' : '-', + s->show_id ? be->branch->id : "", s->show_id ? " " : "", + s->show_date ? be->branch->date : "", + s->show_date ? " " : "", + be->branch->name, be->branch->private ? "*" : "", - be->branch->current ? "@" : "", s->show_id ? " [" : "", - s->show_id ? be->branch->id : "", s->show_id ? "]" : "", - s->show_date ? ':' : 0, - s->show_date ? be->branch->date : 0); + be->branch->current ? "@" : ""); if (line == NULL) return RC(FSL_RC_ERROR, "%s", "fsl_mprintf"); if (s->colour) c = match_colour(&s->colours, line); @@ -10253,11 +10420,11 @@ if (c) wattr_on(view->window, COLOR_PAIR(c->scheme), NULL); waddwstr(view->window, wline); if (c) wattr_off(view->window, COLOR_PAIR(c->scheme), NULL); - if (width < view->ncols - 1) + if (width < view->ncols) waddch(view->window, '\n'); if (n == s->selected && view->active) wattr_off(view->window, A_REVERSE, NULL); fsl_free(line); @@ -10746,16 +10913,17 @@ } static int fnc_prompt_input(struct fnc_view *view, struct input *input) { - int rc = FSL_RC_OK; + int rc = FSL_RC_OK; if (input->prompt) sitrep(view, input->prompt, input->flags); - rc = cook_input(input->buf, sizeof(input->buf), view->window); + rc = cook_input(input->buf, sizeof(input->buf), + view ? view->window : stdscr); if (rc || !input->buf[0]) return rc; if (input->type == INPUT_NUMERIC) { long n = 0; @@ -10764,15 +10932,13 @@ min = *(int *)input->data; max = ((int *)input->data)[1]; } rc = strtonumcheck(&n, input->buf, min, max); if (rc == FSL_RC_MISUSE) - rc = sitrep(view, "-- numeric input only --", - SR_CLREOL | SR_UPDATE | SR_SLEEP | SR_RESET); + rc = sitrep(view, "-- numeric input only --", SR_ALL); else if (rc == FSL_RC_RANGE || n < min || n > max) - rc = sitrep(view, "-- line outside range --", - SR_CLREOL | SR_UPDATE | SR_SLEEP | SR_RESET); + rc = sitrep(view, "-- line outside range --", SR_ALL); else input->ret = n; } return rc; @@ -10794,15 +10960,18 @@ } static int sitrep(struct fnc_view *view, const char *msg, int flags) { - wattr_on(view->window, A_BOLD, NULL); - mvwaddstr(view->window, view->nlines - 1, 0, msg); + WINDOW *win = view ? view->window : stdscr; + int line = (view ? view->nlines : LINES) - 1; + + wattr_on(win, A_BOLD, NULL); + mvwaddstr(win, line, 0, msg); if (FLAG_CHK(flags, SR_CLREOL)) - wclrtoeol(view->window); - wattr_off(view->window, A_BOLD, NULL); + wclrtoeol(win); + wattr_off(win, A_BOLD, NULL); if (FLAG_CHK(flags, SR_UPDATE)) { update_panels(); doupdate(); } if (FLAG_CHK(flags, SR_RESET)) @@ -10914,11 +11083,11 @@ *glob = fsl_mprintf("*%s*", str); if (*glob == NULL) return RC(FSL_RC_ERROR, "%s", "fsl_mprintf"); } - return 0; + return FSL_RC_OK; } static const char * getdirname(const char *path, fsl_int_t len, bool slash) { @@ -10947,10 +11116,11 @@ * operations. Write and create permissions are briefly listed inline, but we * effectively veil the entire fs except the repo db, ckout, and /tmp dirs. * The create permissions for the repository and checkout dirs are (perhaps * unintuitively) needed as fossil(1) creates temporary journal files in both. */ +#ifndef HAVE_LANDLOCK static int init_unveil(const char *repodb, const char *ckoutdir, bool cfg) { #ifdef __OpenBSD__ /* wc repo db for 'fnc config' command: fnc_conf_setopt(). */ @@ -10957,13 +11127,13 @@ if (unveil(repodb, cfg ? "rwc" : "rw") == -1) return RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS), "unveil(%s, \"rw\")", repodb); /* wc .fslckout for fsl_ckout_changes_scan() in cmd_diff(). */ - if (unveil(ckoutdir, "rwc") == -1) + if (ckoutdir && unveil(ckoutdir, "rwc") == -1) return RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS), - "unveil(%s, \"rw\")", ckoutdir); + "unveil(%s, \"rwc\")", ckoutdir); /* rwc /tmp for tmpfile() in help(), create_diff(), and run_blame(). */ if (unveil(P_tmpdir, "rwc") == -1) return RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS), "unveil(%s, \"rwc\")", P_tmpdir); @@ -10973,11 +11143,35 @@ "%s", "unveil"); #endif /* __OpenBSD__ */ return FSL_RC_OK; } -#ifdef HAVE_LANDLOCK +#else /* HAVE_LANDLOCK */ +static const char * +gettzfile(void) +{ + static char ret[PATH_MAX]; + const char *tzdir, *tz; + size_t n; + + if ((tz = getenv("TZ"))) { + if ((tzdir = getenv("TZDIR"))) { + n = fsl_strlcpy(ret, tzdir, sizeof(ret)); + if (ret[n - 1] != '/') + fsl_strlcat(ret, "/", sizeof(ret)); + } else + fsl_strlcpy(ret, "/usr/share/zoneinfo/", sizeof(ret)); + n = fsl_strlcat(ret, tz, sizeof(ret)); + } else + n = fsl_strlcpy(ret, "/etc/localtime", sizeof(ret)); + + if (n >= sizeof(ret) || fsl_file_size(ret) == -1) + return NULL; /* bogus $TZ{DIR} or file doesn't exist */ + + return ret; +} + /* * Sans libc wrappers, use the following shims provided by Landlock authors. * https://www.kernel.org/doc/html/latest/userspace-api/landlock.html */ #ifndef landlock_create_ruleset @@ -11003,38 +11197,33 @@ landlock_restrict_self(const int rfd, const __u32 flags) { return syscall(__NR_landlock_restrict_self, rfd, flags); } #endif -#endif /* HAVE_LANDLOCK */ /* * Similar to unveil(), grant read and write permissisions to the repo and * ckout files, and create permissions to the ckout, repo, and tmp dirs. */ static int init_landlock(const char **paths, const int n) { - int rc = FSL_RC_OK; -#ifdef HAVE_LANDLOCK +#define LANDLOCK_ACCESS_DIR (LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_WRITE_FILE | \ + LANDLOCK_ACCESS_FS_REMOVE_FILE | \ + LANDLOCK_ACCESS_FS_READ_DIR | \ + LANDLOCK_ACCESS_FS_MAKE_REG) /* * Define default block list of _all_ possible operations. - * XXX Due to the fail-open design, set all the bits to avoid following - * Landlock for new ops to be added to this deny-by-default list. + * XXX Due to landlock's fail-open design, set all the bits to avoid + * following Landlock for new ops to add to this deny-by-default list. */ struct landlock_ruleset_attr attr = { - .handled_access_fs = 0xffffffffffffffffULL + .handled_access_fs = ((LANDLOCK_ACCESS_FS_MAKE_SYM << 1) - 1) }; - /* Define allowed operations. */ - struct landlock_path_beneath_attr path_beneath { - .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | - LANDLOCK_ACCESS_FS_WRITE_FILE | - LANDLOCK_ACCESS_FS_REMOVE_FILE | - LANDLOCK_ACCESS_FS_READ_DIR | - LANDLOCK_ACCESS_FS_MAKE_REG - } - int rfd; + struct landlock_path_beneath_attr path_beneath; + int i, rfd, rc = FSL_RC_OK; rfd = landlock_create_ruleset(&attr, sizeof(attr), 0); if (rfd == -1) { /* Landlock is not supported or disabled by the kernel. */ if (errno == ENOSYS || errno == EOPNOTSUPP) @@ -11043,15 +11232,25 @@ "landlock: %s", "failed to create ruleset"); } /* Iterate paths to grant fs permissions. */ for (i = 0; !rc && i < n; ++i) { - path_beneath.parent_fd = open(paths[i], O_PATH | O_CLOEXEC); - if (path_beneath.parent_fd == -1) + struct stat sb; + if (paths[i] == NULL) + continue; + path_beneath.parent_fd = open(paths[i], O_RDONLY | O_CLOEXEC); + if (path_beneath.parent_fd == -1 || + (rc = fstat(path_beneath.parent_fd, &sb))) { + if (rc) + close(path_beneath.parent_fd); rc = RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS), - "landlock: failed to open dir '%s'", paths[i]); - else { + "landlock: failed to read '%s'", paths[i]); + } else { + path_beneath.allowed_access = LANDLOCK_ACCESS_DIR; + if (!S_ISDIR(sb.st_mode)) + path_beneath.allowed_access = + LANDLOCK_ACCESS_FS_READ_FILE; if (landlock_add_rule(rfd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) rc = RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS), "landlock: %s", "failed to update ruleset"); close(path_beneath.parent_fd); @@ -11066,8 +11265,8 @@ rc = RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS), "landlock: %s", "failed to enforce ruleset"); } close(rfd); -#endif /* HAVE_LANDLOCK */ return rc; } +#endif /* HAVE_LANDLOCK */