Index: CHANGES.md ================================================================== --- CHANGES.md +++ CHANGES.md @@ -1,51 +1,11 @@ -**fnc 0.13** 2022-11-26 [[history][0.13a] / [diff][0.13b]] - -- improve reporting of invalid diff command arguments (reported by Dan Shearer) -- accept libfossil global `-V|--verbose` option for all commands -- plug memleak when using the `p` keymap in blame view -- fix `fnc blame -r` and make it behave like `fossil blame -o` for familiarity -- improve `fnc timeline ` lookup of repository paths not in the work tree -- make tree navigation more intuitive when opening repository subtrees -- implement `[` & `]` keymaps to navigate to the previous/next hunk in the diff -- allow `fnc stash get` to take an optional `` argument -- document fnc(1) missing `W` diff view keymap to ignore end-of-line whitespace -- document optional `` argument of `fnc stash pop [stash_id]` -- fix typo producing gcc 12.1.0 compiler warning (comparison always true) -- improve scope matching for function prototype in diff hunk headers -- ifdef out `A_BOLD` attr on OpenBSD to fix diff view line highlight in xterm(1) -- display checkout hash and build date with `fnc -v` like fossil(1) -- miscellaneous diff API implementation improvements -- protect against out-of-bound reads now line_type array is optionally populated -- add `-b|--brief` option to `fnc diff` and `b` diff view keymap -- general fnc(1) manual page markup, syntax, and documentation improvements -- rename FNC_DIFF_COLOUR_CHUNK to FNC_DIFF_COLOUR_HUNK to match internal naming - (*breaking change: existing FNC_DIFF_COLOUR_CHUNK will be silently ignored*) -- ensure diff parser does not internally mark modified files as renamed -- improve const correctness with fsl_uuid_cstr where possible -- fix contrived empty diff case when work tree root is passed to `fnc diff` -- improve path handling of files on disk vs. files in the repository database -- produce less verbose output on error by not presumptuously dumping help -- zap unnecessary allocation and minor refactor in the path parser -- don't display empty diff in contrived modified checkout edge case -- catch missed fsl_content_get() fsl_unlink_file() & fsl_mprintf() return codes -- miscellaneous style(9) fixes -- fix missed format specifier argument in blame path error message -- ensure `fnc stash (get|pop)` doesn't report updated files as renames -- use "(checkout)" label in diff header when diffing or stashing the work tree -- internal stash refactor in preparation to move code into libfossil -- catched missed SIGINT and SIGTERM signals -- use CHAR_BIT for bitstring manipulation macros -- fix assumption of invalid command argument as a path to pass to `fnc timeline` -- merge upstream libfossil with various changes - -**fnc 0.12** 2022-05-09 [[history][0.12a] / [diff][0.12b]] - -- replace \\s regexp with portable [[:space:]] character class (patch by Ashish) +**fnc 0.12** 2022-05-09 + +- replace \s regexp with portable [[:space:]] character class (patch by Ashish) - fix blame->diff child split view regression from 0.9 introduced in [d05828fbb] -**fnc 0.11** 2022-05-08 [[history][0.11a] / [diff][0.11b]] +**fnc 0.11** 2022-05-08 - handle diff of non-versioned files outside the work tree (reported by Dan) - replace getpagesize() with portable sysconf(_SC_PAGESIZE) (patch by mgagnon) - improve robustness by guarding against piped input abuse (reported by Dan) - improve documentation regarding UTF-8 character encoding and fonts @@ -68,11 +28,11 @@ - update in-tree SQLite lib to 3.38.5 with multiple bug fixes - add `apply` alias for `fnc stash get` to facilitate Fossiler muscle memory - simplify error, and tailor usage, reporting when handling invalid input - remove redundant fcli_has_unused_args() call in main() -**fnc 0.10** 2022-03-24 [[history][0.10a] / [diff][0.10b]] +**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) @@ -93,11 +53,11 @@ - 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 [[history][0.9a] / [diff][0.9b]] +**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 - refactor diff implementation to comport with code style @@ -117,16 +77,16 @@ - significant diff driver refactoring for finer granularity in view manipulation - implement `--sbs` (`S` keymap) to display side-by-side formatted diffs - add FNC_COLOUR_DIFF_SBS_EDIT option to set colour of edited lines in SBS diff - plug small memory leak when interactively changing diff format -**fnc 0.8** 2022-01-10 [[history][0.8a] / [diff][0.8b]] +**fnc 0.8** 2022-01-10 - fix vertical split view init regression from 0.7 when terminal < 120 cols wide - fix DB lock when opening horizontal split that signals the timeline thread -**fnc 0.7** 2022-01-09 [[history][0.7a] / [diff][0.7b]] +**fnc 0.7** 2022-01-09 - factor out common make(1) and gmake build bits - make build depend on make file - make all commands compatible with `-R|--repository` (i.e., no checkout needed) - implement Vim-like smartcase for commands that filter repository results @@ -165,11 +125,11 @@ - implement (F)ile keymap to navigate directly to a file in the diff - apply upstream diff v2 fix for rendering untouched lines as changed - implement Vim-like `C-d/C-u` keymaps to scroll the view down/up half a page - enhance search behaviour in all views -**fnc 0.6** 2021-11-22 [[history][0.6a] / [diff][0.6b]] +**fnc 0.6** 2021-11-22 - implement support for user-defined colours in tl, diff, tree and blame views - make branch view accessible from all other views - optimise colour scheme initialisation - implement new 'fnc config' interface to configure per-project settings @@ -190,18 +150,18 @@ - enhance timeline --type option with support for tag control artifacts - general documentation improvements - assorted updates from upstream libfossil including compiler warning fix - expand support for user-defined colours to include the branch view -**fnc 0.5** 2021-11-03 [[history][0.5a] / [diff][0.5b]] +**fnc 0.5** 2021-11-03 - simplify the build system to make-only and remove autosetup (patch from sdk) - remove invalid fsl_errno_to_rc() call and redundant alloc error check - reduce noise on app exit by removing redundant error output - relocate fnc_strlcpy() and fnc_strlcat() routines to the library -**fnc 0.4** 2021-10-31 [[history][0.4a] / [diff][0.4b]] +**fnc 0.4** 2021-10-31 - resolve database bug in the commit builder logic - prune dead code leftover from the initial single-threaded blame implementation - improve error handling of commands that return no records - update in-app help with blame key bindings @@ -229,11 +189,11 @@ - expand available sort options to the branch command - simplify usage output on error or `-h|--help` with usage callback - general man page and documentation improvements - fix incorrect configure `--prefix` install path in the docs (reported by sdk) -**fnc 0.3** 2021-10-17 [[history][0.3a] / [diff][0.3b]] +**fnc 0.3** 2021-10-17 - add in-app help with H|?|F1 key binding - improvements to the build system - decompose build_commits() to make reusable commit_builder() method - dynamically size header so metadata is not truncated in timeline view @@ -253,11 +213,11 @@ - enable handling of master branches not named 'trunk' in 'fnc blame' - enable accessing the tree interface from the timeline with new 't' key binding - add '-C|--no-colour' and 'c' key binding to 'fnc blame' - enhance and standardise parsing of path arguments for all commands -**fnc 0.2** 2021-09-04 [[history][0.2a] / [diff][0.2b]] +**fnc 0.2** 2021-09-04 - fix iconv lib linking in macOS builds - use pkg-config for detecting ncurses libs - prune dead auto.def code - bypass pkg-config in configure script on macOS systems @@ -275,11 +235,11 @@ - plug small memory leak in cmd_diff() routine - fix screen rendering bug due to late deallocation of resources - improve multibyte to wide character conversion error handling/reporting - fix bug involving renamed files in diff view -**fnc 0.1** 2021-08-28 [[history][0.1a] / [diff][0.1b]] +**fnc 0.1** 2021-08-28 - initial commit of infrastructure for the timeline command - add branch names and tags to search criteria - enhance parsing of malformed RFC822 email addresses in commit usernames - display wiki page content of initial wiki commits @@ -294,31 +254,5 @@ - tailor help/usage output to the specified command - fix invalid memory read in diff routine - add support for repository fingerprint version 1 - fix line wrap bug that truncated lines in fullscreen mode -[0.13a]: https://fnc.bsdbox.org/timeline?p=0.13&bt=0.12 -[0.13b]: https://fnc.bsdbox.org/vdiff?from=0.12&to=0.13 -[0.12a]: https://fnc.bsdbox.org/timeline?p=0.12&bt=0.11 -[0.12b]: https://fnc.bsdbox.org/vdiff?from=0.11&to=0.12 -[0.11a]: https://fnc.bsdbox.org/timeline?p=0.11&bt=0.10 -[0.11b]: https://fnc.bsdbox.org/vdiff?from=0.10&to=0.11 -[0.10a]: https://fnc.bsdbox.org/timeline?p=0.10&bt=0.9 -[0.10b]: https://fnc.bsdbox.org/vdiff?from=0.9&to=0.10 -[0.9a]: https://fnc.bsdbox.org/timeline?p=0.9&bt=0.8 -[0.9b]: https://fnc.bsdbox.org/vdiff?from=0.8&to=0.9 -[0.8a]: https://fnc.bsdbox.org/timeline?p=0.8&bt=0.7 -[0.8b]: https://fnc.bsdbox.org/vdiff?from=0.7&to=0.8 -[0.7a]: https://fnc.bsdbox.org/timeline?p=0.7&bt=0.6 -[0.7b]: https://fnc.bsdbox.org/vdiff?from=0.6&to=0.7 -[0.6a]: https://fnc.bsdbox.org/timeline?p=0.6&bt=0.5 -[0.6b]: https://fnc.bsdbox.org/vdiff?from=0.5&to=0.6 -[0.5a]: https://fnc.bsdbox.org/timeline?p=0.5&bt=0.4 -[0.5b]: https://fnc.bsdbox.org/vdiff?from=0.4&to=0.5 -[0.4a]: https://fnc.bsdbox.org/timeline?p=0.4&bt=0.3 -[0.4b]: https://fnc.bsdbox.org/vdiff?from=0.3&to=0.4 -[0.3a]: https://fnc.bsdbox.org/timeline?p=0.3&bt=0.2 -[0.3b]: https://fnc.bsdbox.org/vdiff?from=689182448e&to=0.3 -[0.2a]: https://fnc.bsdbox.org/timeline?p=0.2&bt=96e142ed8e -[0.2b]: https://fnc.bsdbox.org/vdiff?from=96e142ed8e&to=689182448e -[0.1a]: https://fnc.bsdbox.org/timeline?p=689182448e&bt=6eaea2465f -[0.1b]: https://fnc.bsdbox.org/vdiff?from=6eaea2465f&to=96e142ed8e Index: README.md ================================================================== --- README.md +++ README.md @@ -1,26 +1,27 @@ # README -# fnc: an interactive text-based user interface for [Fossil] - -`fnc` uses [ncurses] and [libfossil] to create a [`fossil ui`][fui] experience -in the terminal, and parse local changes at the hunk level to prepare atomic -commits. - -Tested and confirmed to run on the following amd64 systems (additional -platforms noted inline): - -1. OpenBSD 6.8-, 6.9-, 7.0-, 7.1-, and 7.2-{current,release} -2. macOS Catalina 10.15.7, Big Sur 11.5.2, and Ventura 13.0.1 +# fnc 0.12 + +## An interactive ncurses browser for [Fossil][0] repositories. + +`fnc` uses [libfossil][1] to create a [`fossil ui`][2] experience in the +terminal. + +Tested and confirmed to run on the following amd64 systems (additional platforms +noted inline): + +1. OpenBSD 6.8-, 6.9-, and 7.0-release +2. macOS 10.15.7 (Catalina) and 11.5.2 (Big Sur) 3. Linux Mint 20.2 (32- and 64-bit ARM) -4. Ubuntu 18.04, 21.04, 21.10, and 22.04 running Linux 5.1{1,3} (32-bit ARM) +4. Ubuntu 18.04 running Linux kernel 5.11 (32-bit ARM) 5. Debian GNU/Linux 8, 9, and 10 6. CentOS 6.5 (32-bit) -Alpha development notwithstanding, the `timeline`, `diff`, `tree`, `blame`, -`branch`, and `stash` commands are relatively stable; however, there is no -commitment to refrain from breaking changes. +Alpha development notwithstanding, the `timeline`, `diff`, `tree`, `blame`, and +`branch` commands are relatively stable; however, there is no commitment to +refrain from breaking changes. # Install * **OpenBSD** - `doas pkg_add fnc` @@ -27,18 +28,12 @@ * **macOS** - `sudo port install fnc` * **FreeBSD** - package: `pkg install fnc` - port: `cd /usr/ports/devel/fnc/ && make install clean` -* **NixOS** - - `nix-env -iA nixos.fnc` -* **Alpine Linux** - - `apk add fnc` - n.b. Ensure _testing_ repository is enabled (see ) - -Check [repology] to find if a package is provided for your operating system. -If no package exists, [download] and install the binary on your path. +* **Linux** + - [Download](/uv/download.html) and install the binary on your path # Build 1. clone the repository - `fossil clone https://fnc.bsdbox.org` @@ -52,98 +47,95 @@ - `cd ~/museum/repo && fossil open ../repo.fossil && fnc` This will install the `fnc` executable and man page into `/usr/local/bin` and `/usr/local/share/man/man1`, respectively. Alternatively, cryptographically signed tarballs of the source code and binaries for some of the abovementioned -platforms are available to [download]. +platforms are available to [download][3]. # Doc Commands available in **fnc**: -1. [**stash**](https://fnc.bsdbox.org/uv/doc/fnc.1.html#stash) +1. [**stash**](/uv/resources/doc/fnc.1.html#stash) - interactively select hunks to stash from the diff of local changes on disk -2. [**timeline**](https://fnc.bsdbox.org/uv/doc/fnc.1.html#timeline) +2. [**timeline**](/uv/resources/doc/fnc.1.html#timeline) - hyperlinked chronological commit history of the repository -3. [**diff**](https://fnc.bsdbox.org/uv/doc/fnc.1.html#diff) +3. [**diff**](/uv/resources/doc/fnc.1.html#diff) - diff of all changes between commits or blobs -4. [**blame**](https://fnc.bsdbox.org/uv/doc/fnc.1.html#blame) +4. [**blame**](/uv/resources/doc/fnc.1.html#blame) - annotated file displaying commit attribution history of each line -5. [**tree**](https://fnc.bsdbox.org/uv/doc/fnc.1.html#tree) +5. [**tree**](/uv/resources/doc/fnc.1.html#tree) - navigable file hierarchy of the repository tree -6. [**branch**](https://fnc.bsdbox.org/uv/doc/fnc.1.html#branch) +6. [**branch**](/uv/resources/doc/fnc.1.html#branch) - hyperlinked list of all public and private branches -7. [**config**](https://fnc.bsdbox.org/uv/doc/fnc.1.html#config) +7. [**config**](/uv/resources/doc/fnc.1.html#config) - configure or view fnc settings -See `fnc --help` for a quick reference, and the [fnc(1)] manual page for more -comprehensive documentation. Runtime help can also be accessed with the `?`, +See `fnc --help` for a quick reference, and the [fnc(1)][4] manual page for more +comprehensive documentation. In-app help can also be accessed with the `?`, `F1`, or `H` key binding. The following video briefly demonstrates some of the key bindings in use. -[![fnc demo][demoimg]][demo] +[![fnc demo][5]][6] # Why -`fnc` is heavily inspired by [`tog`], which I missed when I left [Got] behind -and started using Fossil. The objective is to provide an alternative to -`fossil ui` without leaving the terminal. +`fnc` is heavily inspired by [`tog`][7], which I missed when I left [Got][8] +(Git) behind and started using Fossil. The objective is to provide an +alternative to `fossil ui` without leaving the terminal. # Problems & Patches -Please submit bug reports via [email], the [forum], or by creating a new -[ticket]. As a rule, all reports should include a bug reproduction recipe; that -is, either (1) the series of steps beginning with `fossil init` to create a new -repository through to the `fnc` command that triggers the unexpected behaviour; -or, if possible, (2) a shell script that contains all necessary ingredients to -reproduce the problem. - -Patches are thoughtfully considered and can be sent to the [mailing list]. -While `diff -up` patches (or `fnc` diffs saved with the [`P` keymap]) are -preferred, `fossil patch create` and `fossil diff` patches are also welcomed. -Please ensure code conforms to the C99 standard, and complies with OpenBSD's -KNF [style(9)]. Any patch containing user-visible code addition, modification, -or deletion (i.e., code that impacts user interfaces) should concomitantly -include updating documentation affected by the change. +Please submit bug reports via [email][9], the [forum][10], or by creating a new +[ticket][11]. As a rule, all reports should include a bug reproduction recipe; +that is, either (1) the series of steps beginning with `fossil init` to create a +new repository through to the `fnc` command that triggers the unexpected +behaviour; or, if possible, (2) a shell script that contains all necessary +ingredients to reproduce the problem. + +Patches are thoughtfully considered and can be sent to the [mailing list][12]. +While `diff -up` patches are preferred, `fossil patch create` and `fossil diff` +patches are also welcomed. Please ensure code conforms to the C99 standard, +and complies with OpenBSD's KNF [style(9)][13]. Any patch containing +user-visible code addition, modification, or deletion (i.e., code that impacts +user interfaces) should concomitantly include updating documentation affected +by the change. # Screenshots -![stash](https://fnc.bsdbox.org/uv/img/fnc-stash.png "fnc stash") -![stash more](https://fnc.bsdbox.org/uv/img/fnc-stash-more.png "stash more") -![stash help](https://fnc.bsdbox.org/uv/img/fnc-stash-help.png "stash help") -![diff vsplit](https://fnc.bsdbox.org/uv/img/fnc-diff-vsplit.png "diff vertical split") -![diff hsplit renamed](https://fnc.bsdbox.org/uv/img/fnc-diff-hsplit-renamed.png "diff horizontal split file renamed") -![diff vsplit added](https://fnc.bsdbox.org/uv/img/fnc-diff-vsplit-added.png "diff vertical split file added") -![diff vsplit removed](https://fnc.bsdbox.org/uv/img/fnc-diff-vsplit-removed.png "diff vertical split file removed") -![blame vsplit](https://fnc.bsdbox.org/uv/img/fnc-blame-vsplit.png "blame vertical split") -![tree vsplit](https://fnc.bsdbox.org/uv/img/fnc-tree-vsplit.png "tree vertical split") -![branch hsplit](https://fnc.bsdbox.org/uv/img/fnc-branch-hsplit.png "branch horizontal split") -![runtime help](https://fnc.bsdbox.org/uv/img/fnc-runtime-help.png "fnc runtime help") -![timeline help](https://fnc.bsdbox.org/uv/img/fnc-timeline-help.png "fnc timeline help") +![stash](https://fnc.bsdbox.org/uv/resources/img/fnc-stash.png "fnc stash") +![stash more](https://fnc.bsdbox.org/uv/resources/img/fnc-stash-more.png "stash more") +![stash help](https://fnc.bsdbox.org/uv/resources/img/fnc-stash-help.png "stash help") +![diff vsplit](https://fnc.bsdbox.org/uv/resources/img/fnc-diff-vsplit.png "diff vertical split") +![diff hsplit renamed](https://fnc.bsdbox.org/uv/resources/img/fnc-diff-hsplit-renamed.png "diff horizontal split file renamed") +![diff vsplit added](https://fnc.bsdbox.org/uv/resources/img/fnc-diff-vsplit-added.png "diff vertical split file added") +![diff vsplit removed](https://fnc.bsdbox.org/uv/resources/img/fnc-diff-vsplit-removed.png "diff vertical split file removed") +![blame vsplit](https://fnc.bsdbox.org/uv/resources/img/fnc-blame-vsplit.png "blame vertical split") +![tree vsplit](https://fnc.bsdbox.org/uv/resources/img/fnc-tree-vsplit.png "tree vertical split") +![branch hsplit](https://fnc.bsdbox.org/uv/resources/img/fnc-branch-hsplit.png "branch horizontal split") +![in-app help](https://fnc.bsdbox.org/uv/resources/img/fnc-inapp_help.png "fnc in-app help") +![timeline help](https://fnc.bsdbox.org/uv/resources/img/fnc-timeline-help.png "fnc timeline help") # Trivia **fnc** [fɪŋk] *noun* (n.) -1. an interactive ncurses browser for [Fossil] repositories +1. an interactive ncurses browser for [Fossil][0] repositories *verb* (v.) 2. to inform etymology From the German word *Fink*, meaning "finch", a type of bird. -[demoimg]: https://fnc.bsdbox.org/uv/img/fnc-timeline-fullscreen.png -[demo]: https://itac.bsdbox.org/fnc-demo.mp4 -[download]: https://fnc.bsdbox.org/uv/download.html -[email]: mailto:fnc@bsdbox.org -[fnc(1)]: https://fnc.bsdbox.org/uv/doc/fnc.1.html -[forum]: https://fnc.bsdbox.org/forum -[Fossil]: https://fossil-scm.org -[fui]: https://fossil-scm.org/home/help?cmd=ui -[Got]: https://gameoftrees.org -[libfossil]: https://fossil.wanderinghorse.net/r/libfossil -[mailing list]: https://itac.bsdbox.org/listinfo/fnc -[ncurses]: https://invisible-island.net/ncurses/ncurses.html -[repology]: https://repology.org/project/fnc/versions -[style(9)]: https://man.openbsd.org/style.9 -[ticket]: https://fnc.bsdbox.org/ticket -[`tog`]: https://gameoftrees.org/tog.1.html -[`P` keymap]: https://fnc.bsdbox.org/uv/doc/fnc.1.html#P~3 +[0]: https://fossil-scm.org +[1]: https://fossil.wanderinghorse.net/r/libfossil +[2]: https://fossil-scm.org/home/help?cmd=ui +[3]: https://fnc.bsdbox.org/uv/download.html +[4]: https://fnc.bsdbox.org/uv/resources/doc/fnc.1.html +[5]: https://fnc.bsdbox.org/uv/resources/img/fnc-timeline-fullscreen.png +[6]: https://itac.bsdbox.org/fnc-demo.mp4 +[7]: https://gameoftrees.org/tog.1.html +[8]: https://gameoftrees.org +[9]: mailto:fnc@bsdbox.org +[10]: https://fnc.bsdbox.org/forum +[11]: https://fnc.bsdbox.org/ticket +[12]: https://itac.bsdbox.org/listinfo/fnc +[13]: https://man.openbsd.org/style.9 Index: fnc.bld.mk ================================================================== --- fnc.bld.mk +++ fnc.bld.mk @@ -4,13 +4,11 @@ # CONFIGURATION CC ?= cc PREFIX ?= /usr/local MANDIR ?= /share/man -VERSION ?= 0.13 -HASH != cut -f 1 manifest.uuid -DATE != sed '2q;d' manifest | cut -d ' ' -f 2 | tr T ' ' +VERSION ?= 0.12 # FLAGS NEEDED TO BUILD SQLITE3 SQLITE_CFLAGS = ${CFLAGS} -Wall -Werror -Wno-sign-compare -pedantic -std=c99 \ -DNDEBUG=1 \ -DSQLITE_DQS=0 \ @@ -42,12 +40,11 @@ # On SOME Linux (e.g., Ubuntu 18.04.6), we have to include wchar curses from # I/.../ncursesw, but linking to -lncursesw (w/ no special -L path) works fine. # FLAGS NEEDED TO BUILD FNC FNC_CFLAGS = ${CFLAGS} -Wall -Werror -Wsign-compare -pedantic -std=c99 \ -I./lib -I./include -I/usr/include/ncursesw \ - -D_XOPEN_SOURCE_EXTENDED -DVERSION=${VERSION} -DHASH=${HASH} \ - -DDATE="${DATE}" + -D_XOPEN_SOURCE_EXTENDED -DVERSION=${VERSION} FNC_LDFLAGS = ${LDFLAGS} -lm -lutil -lz -lpthread -fPIC all: bin @@ -79,11 +76,8 @@ clean: rm -f lib/*.o src/*.o src/fnc release: clean - mkdir /tmp/fnc-${VERSION} - pax -rw * /tmp/fnc-${VERSION} - tar czvf ../fnc-${VERSION}.tgz -C /tmp fnc-${VERSION} - rm -rf /tmp/fnc-${VERSION} + tar czvf ../fnc-${VERSION}.tgz -C .. fnc-${VERSION} .PHONY: clean release Index: include/diff.h ================================================================== --- include/diff.h +++ include/diff.h @@ -15,84 +15,104 @@ */ #include "libfossil.h" #include "settings.h" -/* - * Flags set by callers of the below diff APIs to determine diff output. - */ enum fnc_diff_flag { FNC_DIFF_IGNORE_EOLWS = 0x01, FNC_DIFF_IGNORE_ALLWS = 0x03, - FNC_DIFF_SIDEBYSIDE = 1 << 2, /* output side-by-side diff */ - FNC_DIFF_VERBOSE = 1 << 3, /* show added/rm'd file content */ - FNC_DIFF_BRIEF = 1 << 4, - FNC_DIFF_HTML = 1 << 5, - FNC_DIFF_LINENO = 1 << 6, /* output diff with line numbers */ - FNC_DIFF_NOOPT = 1 << 7, /* og. 0x0100 */ - FNC_DIFF_INVERT = 1 << 8, /* og. 0x0200 */ - FNC_DIFF_NOTTOOBIG = 1 << 9, /* og. 0x0800 */ - FNC_DIFF_STRIP_EOLCR = 1 << 10, /* og. 0x1000 */ - FNC_DIFF_ANSI_COLOR = 1 << 11, /* og. 0x2000 */ - FNC_DIFF_PROTOTYPE = 1 << 12 /* show func sig in hunk header */ + FNC_DIFF_SIDEBYSIDE = 1 << 2, + FNC_DIFF_VERBOSE = 1 << 3, + FNC_DIFF_BRIEF = 1 << 4, + FNC_DIFF_HTML = 1 << 5, + FNC_DIFF_LINENO = 1 << 6, + FNC_DIFF_NOOPT = 1 << 7, /* og. 0x0100 */ + FNC_DIFF_INVERT = 1 << 8, /* og. 0x0200 */ + FNC_DIFF_NOTTOOBIG = 1 << 9, /* og. 0x0800 */ + FNC_DIFF_STRIP_EOLCR = 1 << 10, /* og. 0x1000 */ + FNC_DIFF_ANSI_COLOR = 1 << 11, /* og. 0x2000 */ + FNC_DIFF_PROTOTYPE = 1 << 12 #define FNC_DIFF_CONTEXT_EX (((uint64_t)0x04) << 32) /* Allow 0 context */ #define FNC_DIFF_CONTEXT_MASK ((uint64_t)0x0000ffff) /* Default context */ -#define FNC_DIFF_WIDTH_MASK ((uint64_t)0x00ff0000) /* SBS column width */ +#define FNC_DIFF_WIDTH_MASK ((uint64_t)0x0fff0000) /* SBS column width */ +}; + +struct diff_out_state { + fsl_output_f out; /* Output callback */ + void *state; /* State for this->out() */ + enum line_type *lines; /* Diff line type (e.g., minus, plus) */ + uint32_t nlines; /* Index into this->lines */ + int rc; /* Error reporting */ + char ansi; /* ANSI colour code */ + struct { + const fsl_buffer *file; /* Diffed file */ + char *signature; /* Matching function */ + uint32_t lastmatch; /* Match line index */ + uint32_t lastline; /* Last line scanned */ + fsl_size_t offset; /* Match byte offset */ + } proto; +}; +static const struct diff_out_state diff_out_state_empty = + { NULL, NULL, NULL, 0, 0, 0, { NULL, NULL, 0, 0, 0 } }; + +struct sbsline { + struct diff_out_state *output; + fsl_buffer *cols[5]; /* Pointers to output columns */ + const char *tag; /* tag */ + const char *tag2; /* tag */ + int idx; /* Write tag before idx */ + int end; /* Close tag before end */ + int idx2; /* Write tag2 before idx2 */ + int end2; /* Close tag2 before end2 */ + int width; /* Max column width in diff */ + bool esc; /* Escape html characters */ + void *regex; /* Colour matching lines */ }; -/* - * Compute the diff of changes to convert the file in fsl_buffer parameter 1 - * to the file in fsl_buffer parameter 2 and save the result to the provided - * output fsl_buffer in parameter 3. - * - * A unified diff is output by default. This, along with other diff options - * (detailed in the above fnc_diff_flag enum), can be changed by setting the - * corresponding flags passed in int parameter 8. - * - * If a unified diff, parameter 7 is ignored, and the number of context lines - * is specified in short parameter 6. Negative values fallback to default. If - * a side-by-side diff, parameter 6 is ignored, and the column width of each - * side is specified in short parameter 7; only values larger than the longest - * line are honoured, otherwise the column width of each side will - * automatically grow to accommodate the longest line in the diff. - - * If not NULL, the enum array pointer in paramater 4 and uint32_t pointer in - * parameter 5 will be populated with each line_type and the total number of - * lines in the diff, respectively. Both pointers and the output buffer can - * be prepopulated and must be disposed of by the caller. - */ -int fnc_diff_text_to_buffer(const fsl_buffer *, const fsl_buffer *, - fsl_buffer *, enum line_type **, uint32_t *, short, short, int); - -/* - * Compute the diff of changes to convert the file in fsl_buffer parameter 1 - * to the file in fsl_buffer parameter 2 and invoke the fsl_output_f callback - * in parameter 3 for each computed line. The callback receives the provided - * void parameter 4 as its output state and a char pointer of the diffed line - * or diff metadata (e.g., hunk header, index). Remaining parameters are the - * same as the above fnc_diff_text_to_buffer() routine. - */ -int fnc_diff_text(const fsl_buffer *, const fsl_buffer *, fsl_output_f, void *, - enum line_type **, uint32_t *, short, short, int); - -/* - * Compute and save to the int array pointer in parameter 4 the array of - * copy/delete/insert triples that describes the sequence of changes to convert - * the file in fsl_buffer parameter 1 to the file in fsl_buffer parameter 2. - * Diff format and related options are set via flags passed in int parameter 3. - */ -int fnc_diff_text_raw(const fsl_buffer *, const fsl_buffer *, int, int **); - -/* - * Return the number of columns required to draw to the screen the char pointer - * in parameter 1 by expanding any tabs and accounting for unicode continuation - * bytes. Short parameter 2 may either be the byte size of the string or - * a negative value. - */ -unsigned short etcount(const char *, unsigned short); - -/* - * Save the line_type specified in parameter 3 to the nth index denoted by the - * uint32_t pointer in parameter 2 of the line_type array in parameter 1. The - * uint32_t value pointed to by the 2nd parameter will be incremented. - */ -int add_line_type(enum line_type **, uint32_t *, enum line_type); +int fnc_diff_text_raw(fsl_buffer const *, fsl_buffer const *, + int, int **); +int fnc_diff_text_to_buffer(fsl_buffer const *, fsl_buffer const *, + fsl_buffer *, enum line_type **, uint32_t *, short, short, + int ); +int fnc_diff_text(fsl_buffer const *, fsl_buffer const *, + fsl_output_f, void *, short, short, int ); +int fnc_diff_blobs(fsl_buffer const *, fsl_buffer const *, + fsl_output_f, void *, enum line_type **, uint32_t *, + uint16_t, short, int, int **); +int fnc_output_f_diff_out(void *, void const *, fsl_size_t); +int diff_outf(struct diff_out_state *, char const *, ... ); +int diff_out(struct diff_out_state * const, void const *, + fsl_int_t); +char *match_chunk_function(struct diff_out_state *const, uint32_t); +int buffer_copy_lines_from(fsl_buffer *const, + const fsl_buffer *const, fsl_size_t *, fsl_size_t, + fsl_size_t); +uint64_t fnc_diff_flags_convert(int); +int diff_context_lines(uint64_t); +int match_dline(fsl_dline *, fsl_dline *); +bool longest_common_subsequence(const char *z, int, const char *, + int, int *); +int unidiff(fsl__diff_cx *, struct diff_out_state *, void *, + uint16_t, uint64_t); +int unidiff_lineno(struct diff_out_state *, int, int, bool); +int unidiff_txt( struct diff_out_state *const, char, fsl_dline *, + int, void *); +int sbsdiff(fsl__diff_cx *, struct diff_out_state *, void *, + uint16_t, uint64_t); +int max_sbs_width(fsl__diff_cx *, int *, uint16_t); +unsigned short etcount(const char *str, unsigned short n); +int sbsdiff_width(uint64_t); +int sbsdiff_separator(struct sbsline *, int, int); +int sbsdiff_lineno(struct sbsline *, int, int); +void sbsdiff_shift_left(struct sbsline *, const char *); +void sbsdiff_simplify_line(struct sbsline *, const char *); +int sbsdiff_column(struct diff_out_state *, + fsl_buffer const *, int); +int sbsdiff_txt(struct sbsline *, fsl_dline *, int); +int sbsdiff_newline(struct sbsline *); +int sbsdiff_space(struct sbsline *, int, int); +int sbsdiff_marker(struct sbsline *, const char *, const char *); +int sbsdiff_close_gap(int *); +unsigned char *sbsdiff_align(fsl_dline *, int, fsl_dline *, int); +int sbsdiff_write_change(struct sbsline *, fsl_dline *, int, + fsl_dline *, int); +int add_line_type(enum line_type **, uint32_t *, enum line_type); Index: include/settings.h ================================================================== --- include/settings.h +++ include/settings.h @@ -32,11 +32,11 @@ _(pfx, COLOUR_USER), \ _(pfx, COLOUR_DATE), \ _(pfx, COLOUR_DIFF_META), \ _(pfx, COLOUR_DIFF_MINUS), \ _(pfx, COLOUR_DIFF_PLUS), \ - _(pfx, COLOUR_DIFF_HUNK), \ + _(pfx, COLOUR_DIFF_CHUNK), \ _(pfx, COLOUR_DIFF_TAGS), \ _(pfx, COLOUR_DIFF_SBS_EDIT), \ _(pfx, COLOUR_TREE_LINK), \ _(pfx, COLOUR_TREE_DIR), \ _(pfx, COLOUR_TREE_EXEC), \ @@ -76,11 +76,11 @@ _(pfx, DIFF_META), \ _(pfx, DIFF_MINUS), \ _(pfx, DIFF_PLUS), \ _(pfx, DIFF_EDIT), \ _(pfx, DIFF_CONTEXT), \ - _(pfx, DIFF_HUNK), \ + _(pfx, DIFF_CHUNK), \ _(pfx, DIFF_SEPARATOR) #define VIEW_MODE_ENUM(pfx, _) \ _(pfx, NONE), \ _(pfx, VERT), \ Index: lib/libfossil-config.h ================================================================== --- lib/libfossil-config.h +++ lib/libfossil-config.h @@ -81,13 +81,13 @@ #endif #endif /* _WIN32 */ -#define FSL_LIB_VERSION_HASH "46008704a620cc770bce96cc4313b0bf96498390" -#define FSL_LIB_VERSION_TIMESTAMP "2022-11-24 13:51:38.504 UTC" -#define FSL_LIB_CONFIG_TIME "2022-11-24 14:03 GMT" +#define FSL_LIB_VERSION_HASH "2a405470c0d32a84f8644de1b5c509635a7e78cb" +#define FSL_LIB_VERSION_TIMESTAMP "2021-11-18 17:40:56.695 UTC" +#define FSL_LIB_CONFIG_TIME "2021-11-18 18:02 GMT" #if defined(_MSC_VER) #define FSL_PLATFORM_OS "windows" #define FSL_PLATFORM_IS_WINDOWS 1 #define FSL_PLATFORM_IS_UNIX 0 #define FSL_PLATFORM_PLATFORM "windows" Index: lib/libfossil.c ================================================================== --- lib/libfossil.c +++ lib/libfossil.c @@ -1,860 +1,6 @@ #include "libfossil.h" -/* start of file ./src/xdirent.h */ -/** - Origin: https://gist.github.com/isakbosman/758eb668938806aabb04830736f4ac41 - - Modified only very slightly for use in the libfossil project: a - couple of #if's were added to allow us to include this file without - having to check which platform we're building on. In non-Windows builds - it uses the corresponding POSIX APIs. -*/ -/* - * dirent.h - dirent API for Microsoft Visual Studio - * - * Copyright (C) 2006-2012 Toni Ronkko - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * ``Software''), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL TONI RONKKO BE LIABLE FOR ANY CLAIM, DAMAGES OR - * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * $Id: dirent.h,v 1.20 2014/03/19 17:52:23 tronkko Exp $ - */ -#ifndef DIRENT_H -#define DIRENT_H - -#if FSL_PLATFORM_IS_WINDOWS -/* - * Define architecture flags so we don't need to include windows.h. - * Avoiding windows.h makes it simpler to use windows sockets in conjunction - * with dirent.h. - */ -#if !defined(_68K_) && !defined(_MPPC_) && !defined(_X86_) && !defined(_IA64_) && !defined(_AMD64_) && defined(_M_IX86) -# define _X86_ -#endif -#if !defined(_68K_) && !defined(_MPPC_) && !defined(_X86_) && !defined(_IA64_) && !defined(_AMD64_) && defined(_M_AMD64) -#define _AMD64_ -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Indicates that d_type field is available in dirent structure */ -#define _DIRENT_HAVE_D_TYPE - -/* Indicates that d_namlen field is available in dirent structure */ -#define _DIRENT_HAVE_D_NAMLEN - -/* Entries missing from MSVC 6.0 */ -#if !defined(FILE_ATTRIBUTE_DEVICE) -# define FILE_ATTRIBUTE_DEVICE 0x40 -#endif - -/* File type and permission flags for stat() */ -#if !defined(S_IFMT) -# define S_IFMT _S_IFMT /* File type mask */ -#endif -#if !defined(S_IFDIR) -# define S_IFDIR _S_IFDIR /* Directory */ -#endif -#if !defined(S_IFCHR) -# define S_IFCHR _S_IFCHR /* Character device */ -#endif -#if !defined(S_IFFIFO) -# define S_IFFIFO _S_IFFIFO /* Pipe */ -#endif -#if !defined(S_IFREG) -# define S_IFREG _S_IFREG /* Regular file */ -#endif -#if !defined(S_IREAD) -# define S_IREAD _S_IREAD /* Read permission */ -#endif -#if !defined(S_IWRITE) -# define S_IWRITE _S_IWRITE /* Write permission */ -#endif -#if !defined(S_IEXEC) -# define S_IEXEC _S_IEXEC /* Execute permission */ -#endif -#if !defined(S_IFIFO) -# define S_IFIFO _S_IFIFO /* Pipe */ -#endif -#if !defined(S_IFBLK) -# define S_IFBLK 0 /* Block device */ -#endif -#if !defined(S_IFLNK) -# define S_IFLNK 0 /* Link */ -#endif -#if !defined(S_IFSOCK) -# define S_IFSOCK 0 /* Socket */ -#endif - -#if defined(_MSC_VER) -# define S_IRUSR S_IREAD /* Read user */ -# define S_IWUSR S_IWRITE /* Write user */ -# define S_IXUSR 0 /* Execute user */ -# define S_IRGRP 0 /* Read group */ -# define S_IWGRP 0 /* Write group */ -# define S_IXGRP 0 /* Execute group */ -# define S_IROTH 0 /* Read others */ -# define S_IWOTH 0 /* Write others */ -# define S_IXOTH 0 /* Execute others */ -#endif - -/* Maximum length of file name */ -#if !defined(PATH_MAX) -# define PATH_MAX MAX_PATH -#endif -#if !defined(FILENAME_MAX) -# define FILENAME_MAX MAX_PATH -#endif -#if !defined(NAME_MAX) -# define NAME_MAX FILENAME_MAX -#endif - -/* File type flags for d_type */ -#define DT_UNKNOWN 0 -#define DT_REG S_IFREG -#define DT_DIR S_IFDIR -#define DT_FIFO S_IFIFO -#define DT_SOCK S_IFSOCK -#define DT_CHR S_IFCHR -#define DT_BLK S_IFBLK -#define DT_LNK S_IFLNK - -/* Macros for converting between st_mode and d_type */ -#define IFTODT(mode) ((mode) & S_IFMT) -#define DTTOIF(type) (type) - -/* - * File type macros. Note that block devices, sockets and links cannot be - * distinguished on Windows and the macros S_ISBLK, S_ISSOCK and S_ISLNK are - * only defined for compatibility. These macros should always return false - * on Windows. - */ -#define S_ISFIFO(mode) (((mode) & S_IFMT) == S_IFIFO) -#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) -#define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG) -#define S_ISLNK(mode) (((mode) & S_IFMT) == S_IFLNK) -#define S_ISSOCK(mode) (((mode) & S_IFMT) == S_IFSOCK) -#define S_ISCHR(mode) (((mode) & S_IFMT) == S_IFCHR) -#define S_ISBLK(mode) (((mode) & S_IFMT) == S_IFBLK) - -/* Return the exact length of d_namlen without zero terminator */ -#define _D_EXACT_NAMLEN(p) ((p)->d_namlen) - -/* Return number of bytes needed to store d_namlen */ -#define _D_ALLOC_NAMLEN(p) (PATH_MAX) - - -#ifdef __cplusplus -extern "C" { -#endif - - -/* Wide-character version */ -struct _wdirent { - long d_ino; /* Always zero */ - unsigned short d_reclen; /* Structure size */ - size_t d_namlen; /* Length of name without \0 */ - int d_type; /* File type */ - wchar_t d_name[PATH_MAX]; /* File name */ -}; -typedef struct _wdirent _wdirent; - -struct _WDIR { - struct _wdirent ent; /* Current directory entry */ - WIN32_FIND_DATAW data; /* Private file data */ - int cached; /* True if data is valid */ - HANDLE handle; /* Win32 search handle */ - wchar_t *patt; /* Initial directory name */ -}; -typedef struct _WDIR _WDIR; - -static _WDIR *_wopendir (const wchar_t *dirname); -static struct _wdirent *_wreaddir (_WDIR *dirp); -static int _wclosedir (_WDIR *dirp); -static void _wrewinddir (_WDIR* dirp); - - -/* For compatibility with Symbian */ -#define wdirent _wdirent -#define WDIR _WDIR -#define wopendir _wopendir -#define wreaddir _wreaddir -#define wclosedir _wclosedir -#define wrewinddir _wrewinddir - - -/* Multi-byte character versions */ -struct dirent { - long d_ino; /* Always zero */ - unsigned short d_reclen; /* Structure size */ - size_t d_namlen; /* Length of name without \0 */ - int d_type; /* File type */ - char d_name[PATH_MAX]; /* File name */ -}; -typedef struct dirent dirent; - -struct DIR { - struct dirent ent; - struct _WDIR *wdirp; -}; -typedef struct DIR DIR; - -static DIR *opendir (const char *dirname); -static struct dirent *readdir (DIR *dirp); -static int closedir (DIR *dirp); -static void rewinddir (DIR* dirp); - - -/* Internal utility functions */ -static WIN32_FIND_DATAW *dirent_first (_WDIR *dirp); -static WIN32_FIND_DATAW *dirent_next (_WDIR *dirp); - -static int dirent_mbstowcs_s( - size_t *pReturnValue, - wchar_t *wcstr, - size_t sizeInWords, - const char *mbstr, - size_t count); - -static int dirent_wcstombs_s( - size_t *pReturnValue, - char *mbstr, - size_t sizeInBytes, - const wchar_t *wcstr, - size_t count); - -static void dirent_set_errno (int error); - -/* - * Open directory stream DIRNAME for read and return a pointer to the - * internal working area that is used to retrieve individual directory - * entries. - */ -static _WDIR* -_wopendir( - const wchar_t *dirname) -{ - _WDIR *dirp = NULL; - int error; - - /* Must have directory name */ - if (dirname == NULL || dirname[0] == '\0') { - dirent_set_errno (ENOENT); - return NULL; - } - - /* Allocate new _WDIR structure */ - dirp = (_WDIR*) malloc (sizeof (struct _WDIR)); - if (dirp != NULL) { - DWORD n; - - /* Reset _WDIR structure */ - dirp->handle = INVALID_HANDLE_VALUE; - dirp->patt = NULL; - dirp->cached = 0; - - /* Compute the length of full path plus zero terminator */ - n = GetFullPathNameW (dirname, 0, NULL, NULL); - - /* Allocate room for absolute directory name and search pattern */ - dirp->patt = (wchar_t*) malloc (sizeof (wchar_t) * n + 16); - if (dirp->patt) { - - /* - * Convert relative directory name to an absolute one. This - * allows rewinddir() to function correctly even when current - * working directory is changed between opendir() and rewinddir(). - */ - n = GetFullPathNameW (dirname, n, dirp->patt, NULL); - if (n > 0) { - wchar_t *p; - - /* Append search pattern \* to the directory name */ - p = dirp->patt + n; - if (dirp->patt < p) { - switch (p[-1]) { - case '\\': - case '/': - case ':': - /* Directory ends in path separator, e.g. c:\temp\ */ - /*NOP*/; - break; - - default: - /* Directory name doesn't end in path separator */ - *p++ = '\\'; - } - } - *p++ = '*'; - *p = '\0'; - - /* Open directory stream and retrieve the first entry */ - if (dirent_first (dirp)) { - /* Directory stream opened successfully */ - error = 0; - } else { - /* Cannot retrieve first entry */ - error = 1; - dirent_set_errno (ENOENT); - } - - } else { - /* Cannot retrieve full path name */ - dirent_set_errno (ENOENT); - error = 1; - } - - } else { - /* Cannot allocate memory for search pattern */ - error = 1; - } - - } else { - /* Cannot allocate _WDIR structure */ - error = 1; - } - - /* Clean up in case of error */ - if (error && dirp) { - _wclosedir (dirp); - dirp = NULL; - } - - return dirp; -} - -/* - * Read next directory entry. The directory entry is returned in dirent - * structure in the d_name field. Individual directory entries returned by - * this function include regular files, sub-directories, pseudo-directories - * "." and ".." as well as volume labels, hidden files and system files. - */ -static struct _wdirent* -_wreaddir( - _WDIR *dirp) -{ - WIN32_FIND_DATAW *datap; - struct _wdirent *entp; - - /* Read next directory entry */ - datap = dirent_next (dirp); - if (datap) { - size_t n; - DWORD attr; - - /* Pointer to directory entry to return */ - entp = &dirp->ent; - - /* - * Copy file name as wide-character string. If the file name is too - * long to fit in to the destination buffer, then truncate file name - * to PATH_MAX characters and zero-terminate the buffer. - */ - n = 0; - while (n + 1 < PATH_MAX && datap->cFileName[n] != 0) { - entp->d_name[n] = datap->cFileName[n]; - n++; - } - dirp->ent.d_name[n] = 0; - - /* Length of file name excluding zero terminator */ - entp->d_namlen = n; - - /* File type */ - attr = datap->dwFileAttributes; - if ((attr & FILE_ATTRIBUTE_DEVICE) != 0) { - entp->d_type = DT_CHR; - } else if ((attr & FILE_ATTRIBUTE_DIRECTORY) != 0) { - entp->d_type = DT_DIR; - } else { - entp->d_type = DT_REG; - } - - /* Reset dummy fields */ - entp->d_ino = 0; - entp->d_reclen = sizeof (struct _wdirent); - - } else { - - /* Last directory entry read */ - entp = NULL; - - } - - return entp; -} - -/* - * Close directory stream opened by opendir() function. This invalidates the - * DIR structure as well as any directory entry read previously by - * _wreaddir(). - */ -static int -_wclosedir( - _WDIR *dirp) -{ - int ok; - if (dirp) { - - /* Release search handle */ - if (dirp->handle != INVALID_HANDLE_VALUE) { - FindClose (dirp->handle); - dirp->handle = INVALID_HANDLE_VALUE; - } - - /* Release search pattern */ - if (dirp->patt) { - free (dirp->patt); - dirp->patt = NULL; - } - - /* Release directory structure */ - free (dirp); - ok = /*success*/0; - - } else { - /* Invalid directory stream */ - dirent_set_errno (EBADF); - ok = /*failure*/-1; - } - return ok; -} - -/* - * Rewind directory stream such that _wreaddir() returns the very first - * file name again. - */ -static void -_wrewinddir( - _WDIR* dirp) -{ - if (dirp) { - /* Release existing search handle */ - if (dirp->handle != INVALID_HANDLE_VALUE) { - FindClose (dirp->handle); - } - - /* Open new search handle */ - dirent_first (dirp); - } -} - -/* Get first directory entry (internal) */ -static WIN32_FIND_DATAW* -dirent_first( - _WDIR *dirp) -{ - WIN32_FIND_DATAW *datap; - - /* Open directory and retrieve the first entry */ - dirp->handle = FindFirstFileW (dirp->patt, &dirp->data); - if (dirp->handle != INVALID_HANDLE_VALUE) { - - /* a directory entry is now waiting in memory */ - datap = &dirp->data; - dirp->cached = 1; - - } else { - - /* Failed to re-open directory: no directory entry in memory */ - dirp->cached = 0; - datap = NULL; - - } - return datap; -} - -/* Get next directory entry (internal) */ -static WIN32_FIND_DATAW* -dirent_next( - _WDIR *dirp) -{ - WIN32_FIND_DATAW *p; - - /* Get next directory entry */ - if (dirp->cached != 0) { - - /* A valid directory entry already in memory */ - p = &dirp->data; - dirp->cached = 0; - - } else if (dirp->handle != INVALID_HANDLE_VALUE) { - - /* Get the next directory entry from stream */ - if (FindNextFileW (dirp->handle, &dirp->data) != FALSE) { - /* Got a file */ - p = &dirp->data; - } else { - /* The very last entry has been processed or an error occured */ - FindClose (dirp->handle); - dirp->handle = INVALID_HANDLE_VALUE; - p = NULL; - } - - } else { - - /* End of directory stream reached */ - p = NULL; - - } - - return p; -} - -/* - * Open directory stream using plain old C-string. - */ -static DIR* -opendir( - const char *dirname) -{ - struct DIR *dirp; - int error; - - /* Must have directory name */ - if (dirname == NULL || dirname[0] == '\0') { - dirent_set_errno (ENOENT); - return NULL; - } - - /* Allocate memory for DIR structure */ - dirp = (DIR*) malloc (sizeof (struct DIR)); - if (dirp) { - wchar_t wname[PATH_MAX]; - size_t n; - - /* Convert directory name to wide-character string */ - error = dirent_mbstowcs_s (&n, wname, PATH_MAX, dirname, PATH_MAX); - if (!error) { - - /* Open directory stream using wide-character name */ - dirp->wdirp = _wopendir (wname); - if (dirp->wdirp) { - /* Directory stream opened */ - error = 0; - } else { - /* Failed to open directory stream */ - error = 1; - } - - } else { - /* - * Cannot convert file name to wide-character string. This - * occurs if the string contains invalid multi-byte sequences or - * the output buffer is too small to contain the resulting - * string. - */ - error = 1; - } - - } else { - /* Cannot allocate DIR structure */ - error = 1; - } - - /* Clean up in case of error */ - if (error && dirp) { - free (dirp); - dirp = NULL; - } - - return dirp; -} - -/* - * Read next directory entry. - * - * When working with text consoles, please note that file names returned by - * readdir() are represented in the default ANSI code page while any output to - * console is typically formatted on another code page. Thus, non-ASCII - * characters in file names will not usually display correctly on console. The - * problem can be fixed in two ways: (1) change the character set of console - * to 1252 using chcp utility and use Lucida Console font, or (2) use - * _cprintf function when writing to console. The _cprinf() will re-encode - * ANSI strings to the console code page so many non-ASCII characters will - * display correcly. - */ -static struct dirent* -readdir( - DIR *dirp) -{ - WIN32_FIND_DATAW *datap; - struct dirent *entp; - - /* Read next directory entry */ - datap = dirent_next (dirp->wdirp); - if (datap) { - size_t n; - int error; - - /* Attempt to convert file name to multi-byte string */ - error = dirent_wcstombs_s( - &n, dirp->ent.d_name, PATH_MAX, datap->cFileName, PATH_MAX); - - /* - * If the file name cannot be represented by a multi-byte string, - * then attempt to use old 8+3 file name. This allows traditional - * Unix-code to access some file names despite of unicode - * characters, although file names may seem unfamiliar to the user. - * - * Be ware that the code below cannot come up with a short file - * name unless the file system provides one. At least - * VirtualBox shared folders fail to do this. - */ - if (error && datap->cAlternateFileName[0] != '\0') { - error = dirent_wcstombs_s( - &n, dirp->ent.d_name, PATH_MAX, - datap->cAlternateFileName, PATH_MAX); - } - - if (!error) { - DWORD attr; - - /* Initialize directory entry for return */ - entp = &dirp->ent; - - /* Length of file name excluding zero terminator */ - entp->d_namlen = n - 1; - - /* File attributes */ - attr = datap->dwFileAttributes; - if ((attr & FILE_ATTRIBUTE_DEVICE) != 0) { - entp->d_type = DT_CHR; - } else if ((attr & FILE_ATTRIBUTE_DIRECTORY) != 0) { - entp->d_type = DT_DIR; - } else { - entp->d_type = DT_REG; - } - - /* Reset dummy fields */ - entp->d_ino = 0; - entp->d_reclen = sizeof (struct dirent); - - } else { - /* - * Cannot convert file name to multi-byte string so construct - * an errornous directory entry and return that. Note that - * we cannot return NULL as that would stop the processing - * of directory entries completely. - */ - entp = &dirp->ent; - entp->d_name[0] = '?'; - entp->d_name[1] = '\0'; - entp->d_namlen = 1; - entp->d_type = DT_UNKNOWN; - entp->d_ino = 0; - entp->d_reclen = 0; - } - - } else { - /* No more directory entries */ - entp = NULL; - } - - return entp; -} - -/* - * Close directory stream. - */ -static int -closedir( - DIR *dirp) -{ - int ok; - if (dirp) { - - /* Close wide-character directory stream */ - ok = _wclosedir (dirp->wdirp); - dirp->wdirp = NULL; - - /* Release multi-byte character version */ - free (dirp); - - } else { - - /* Invalid directory stream */ - dirent_set_errno (EBADF); - ok = /*failure*/-1; - - } - return ok; -} - -/* - * Rewind directory stream to beginning. - */ -static void -rewinddir( - DIR* dirp) -{ - /* Rewind wide-character string directory stream */ - _wrewinddir (dirp->wdirp); -} - -/* Convert multi-byte string to wide character string */ -static int -dirent_mbstowcs_s( - size_t *pReturnValue, - wchar_t *wcstr, - size_t sizeInWords, - const char *mbstr, - size_t count) -{ - int error; - -#if defined(_MSC_VER) && _MSC_VER >= 1400 - - /* Microsoft Visual Studio 2005 or later */ - error = mbstowcs_s (pReturnValue, wcstr, sizeInWords, mbstr, count); - -#else - - /* Older Visual Studio or non-Microsoft compiler */ - size_t n; - - /* Convert to wide-character string (or count characters) */ - n = mbstowcs (wcstr, mbstr, sizeInWords); - if (!wcstr || n < count) { - - /* Zero-terminate output buffer */ - if (wcstr && sizeInWords) { - if (n >= sizeInWords) { - n = sizeInWords - 1; - } - wcstr[n] = 0; - } - - /* Length of resuting multi-byte string WITH zero terminator */ - if (pReturnValue) { - *pReturnValue = n + 1; - } - - /* Success */ - error = 0; - - } else { - - /* Could not convert string */ - error = 1; - - } - -#endif - - return error; -} - -/* Convert wide-character string to multi-byte string */ -static int -dirent_wcstombs_s( - size_t *pReturnValue, - char *mbstr, - size_t sizeInBytes, /* max size of mbstr */ - const wchar_t *wcstr, - size_t count) -{ - int error; - -#if defined(_MSC_VER) && _MSC_VER >= 1400 - - /* Microsoft Visual Studio 2005 or later */ - error = wcstombs_s (pReturnValue, mbstr, sizeInBytes, wcstr, count); - -#else - - /* Older Visual Studio or non-Microsoft compiler */ - size_t n; - - /* Convert to multi-byte string (or count the number of bytes needed) */ - n = wcstombs (mbstr, wcstr, sizeInBytes); - if (!mbstr || n < count) { - - /* Zero-terminate output buffer */ - if (mbstr && sizeInBytes) { - if (n >= sizeInBytes) { - n = sizeInBytes - 1; - } - mbstr[n] = '\0'; - } - - /* Lenght of resulting multi-bytes string WITH zero-terminator */ - if (pReturnValue) { - *pReturnValue = n + 1; - } - - /* Success */ - error = 0; - - } else { - - /* Cannot convert string */ - error = 1; - - } - -#endif - - return error; -} - -/* Set errno variable */ -static void -dirent_set_errno( - int error) -{ -#if defined(_MSC_VER) && _MSC_VER >= 1400 - - /* Microsoft Visual Studio 2005 and later */ - _set_errno (error); - -#else - - /* Non-Microsoft compiler or older Microsoft compiler */ - errno = error; - -#endif -} - - -#ifdef __cplusplus -} -#endif - -#else /* !FSL_PLATFORM_IS_WINDOWS */ -#include -#include -#endif /* !FSL_PLATFORM_IS_WINDOWS */ - -#endif /*DIRENT_H*/ -/* end of file ./src/xdirent.h */ /* start of file ./src/fsl.c */ /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* Copyright 2013-2021 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt @@ -904,12 +50,10 @@ const fsl_checkin_opt fsl_checkin_opt_empty = fsl_checkin_opt_empty_m; const fsl_cidiff_opt fsl_cidiff_opt_empty = fsl_cidiff_opt_empty_m; const fsl_cidiff_state fsl_cidiff_state_empty = fsl_cidiff_state_empty_m; const fsl_ckout_manage_opt fsl_ckout_manage_opt_empty = fsl_ckout_manage_opt_empty_m; -const fsl_ckout_rename_opt fsl_ckout_rename_opt_empty = - fsl_ckout_rename_opt_empty_m; const fsl_ckout_unmanage_opt fsl_ckout_unmanage_opt_empty = fsl_ckout_unmanage_opt_empty_m; const fsl_ckup_opt fsl_ckup_opt_empty = fsl_ckup_opt_m; const fsl_confirmer fsl_confirmer_empty = fsl_confirmer_empty_m; const fsl_cx fsl_cx_empty = fsl_cx_empty_m; @@ -987,11 +131,11 @@ return FLCA.f(FLCA.state, mem, n); } #undef FLCA } -void * fsl_realloc_f_stdalloc(void * state __unused, void * mem, fsl_size_t n){ +void * fsl_realloc_f_stdalloc(void * state, void * mem, fsl_size_t n){ if(!mem){ return malloc(n); }else if(!n){ free(mem); return NULL; @@ -1024,18 +168,19 @@ *err = fsl_error_empty; } void fsl_error_reset( fsl_error * const err ){ err->code = 0; - fsl_buffer_reuse(&err->msg); + err->msg.used = err->msg.cursor = 0; + if(err->msg.mem) err->msg.mem[0] = 0; } int fsl_error_copy( fsl_error const * const src, fsl_error * const dest ){ if(src==dest) return FSL_RC_MISUSE; else { int rc = 0; - fsl_buffer_reuse(&dest->msg); + dest->msg.used = dest->msg.cursor = 0; dest->code = src->code; if(FSL_RC_OOM!=src->code){ rc = fsl_buffer_append( &dest->msg, src->msg.mem, src->msg.used ); } return rc; @@ -1050,26 +195,29 @@ *higher = err; } int fsl_error_setv( fsl_error * const err, int code, char const * fmt, va_list args ){ - fsl_buffer_reuse(&err->msg); - if(code){ + if(!code){ /* clear error state */ + err->code = 0; + err->msg.used = err->msg.cursor = 0; + if(err->msg.mem){ + err->msg.mem[0] = 0; + } + return 0; + }else{ int rc = 0; + err->msg.used = err->msg.cursor = 0; err->code = code; if(FSL_RC_OOM!=code){ - if(fmt) rc = fsl_buffer_appendfv(&err->msg, fmt, args); - else rc = fsl_buffer_appendf(&err->msg, "fsl_rc_e #%d: %s", - code, fsl_rc_cstr(code)); + rc = fmt + ? fsl_buffer_appendfv(&err->msg, fmt, args) + : fsl_buffer_append(&err->msg, fsl_rc_cstr(code), -1); if(rc) err->code = rc; } return rc ? rc : code; - }else{ /* clear error state */ - err->code = 0; - return 0; } - } int fsl_error_set( fsl_error * const err, int code, char const * fmt, ... ){ int rc; @@ -1090,20 +238,21 @@ return err->code; } char const * fsl_rc_cstr(int rc){ - switch((fsl_rc_e)rc){ - /* we cast ^^^^ so that gcc will warn if the switch() below is - missing any fsl_rc_e entries. */; + fsl_rc_e const RC = (fsl_rc_e)rc + /* we do this so that gcc will warn if the switch() below is + missing any fsl_rc_e entries. */ + ; + switch(RC){ #define STR(T) case FSL_RC_##T: return "FSL_RC_" #T STR(ACCESS); STR(ALREADY_EXISTS); STR(AMBIGUOUS); STR(BREAK); STR(SYNTAX); - STR(CANNOT_HAPPEN); STR(CHECKSUM_MISMATCH); STR(CONFLICT); STR(CONSISTENCY); STR(DB); STR(DELTA_INVALID_OPERATOR); @@ -1114,11 +263,10 @@ STR(DIFF_WS_ONLY); STR(end); STR(ERROR); STR(INTERRUPTED); STR(IO); - STR(LOCKED); STR(MISSING_INFO); STR(MISUSE); STR(NOOP); STR(NOT_A_CKOUT); STR(NOT_A_REPO); @@ -1136,14 +284,13 @@ STR(STEP_ERROR); STR(STEP_ROW); STR(TYPE); STR(UNKNOWN_RESOURCE); STR(UNSUPPORTED); - STR(WOULD_FORK); #undef STR } - return NULL; + return "Unknown result code"; } char const * fsl_library_version(){ return FSL_LIBRARY_VERSION; } @@ -1254,18 +401,20 @@ } fsl_size_t fsl_strlcpy(char * dst, const char * src, fsl_size_t dstsz){ fsl_size_t offset = 0; - if(dstsz){ - while((*(dst+offset) = *(src+offset))!='\0'){ - if(++offset == dstsz){ - --offset; - break; - } + if(dstsz<1){ + goto end; + } + while((*(dst+offset) = *(src+offset))!='\0'){ + if(++offset == dstsz){ + --offset; + break; } } +end: *(dst+offset) = '\0'; while(*(src+offset)!='\0'){ ++offset; /* Return src length. */ } return offset; @@ -1275,12 +424,10 @@ fsl_size_t offset; int dstlen, srclen, idx = 0; offset = dstlen = fsl_strlen(dst); srclen = fsl_strlen(src); - if( offset>=dstsz-1 ) - return dstlen+srclen; while((*(dst+offset++) = *(src+idx++))!='\0'){ if(offset==dstsz-1){ break; } @@ -1878,48 +1025,10 @@ } void fsl_randomness(unsigned int n, void *tgt){ sqlite3_randomness((int)n, tgt); } - -int fsl_system(const char *zOrigCmd){ - int rc; - /* The following was ported over from fossil(1). As of this writing, - the Windows version is completely untested even for - compilability. */ -#if defined(_WIN32) - /* On windows, we have to put double-quotes around the entire command. - ** Who knows why - this is just the way windows works. - */ - char *zNewCmd = fsl_mprintf("\"%s\"", zOrigCmd); - if(!zNewCmd){FSL__WARN_OOM; return FSL_RC_OOM;} - wchar_t *zUnicode = (wchar_t *)fsl_utf8_to_unicode(zNewCmd); - if(!zUnicode){ - fsl_free(zNewCmd); - FSL__WARN_OOM; - return FSL_RC_OOM; - } - //fossil_assert_safe_command_string(zOrigCmd); - rc = _wsystem(zUnicode); - fsl_unicode_free(zUnicode); - free(zNewCmd); -#else - /* On unix, evaluate the command directly. - */ - //fossil_assert_safe_command_string(zOrigCmd); - /* The regular system() call works to get a shell on unix */ - rc = system(zOrigCmd); - if(rc) { - if(-1==rc) rc = errno; - else if(rc>0){ - rc = FSL_RC_ERROR; - } - } -#endif - return rc ? fsl_errno_to_rc(rc, FSL_RC_ERROR) : 0; -} - #undef MARKER #if defined(_WIN32) || defined(WIN32) #undef isatty #endif @@ -2330,15 +1439,12 @@ int rc; Annotator ann = Annotator_empty; unsigned int i; fsl_buffer * const scratch = fsl__cx_scratchpad(f); fsl_annotate_step aStep; + assert(opt->out); - if(!opt->out){ - return fsl_cx_err_set(f, FSL_RC_MISUSE, - "fsl_annotate_opt is missing its output function."); - } if(opt->limitMs>0) fsl_timer_start(&ann.timer); rc = fsl__annotate_file(f, &ann, opt); if(rc) goto end; memset(&aStep,0,sizeof(fsl_annotate_step)); @@ -2476,26 +1582,19 @@ #define FSLPRINTF_ENABLE_JSON 1 /* Most C compilers handle variable-sized arrays, so we enable that by default. Some (e.g. tcc) do not, so we provide a way - to disable it: set FSLPRINTF_HAVE_VARARRAY to 0. + to disable it: set FSLPRINTF_HAVE_VARARRAY to 0 One approach would be to look at: defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) but some compilers support variable-sized arrays even when not explicitly running in c99 mode. */ -/* - 2022-05-17: apparently VLAs were made OPTIONAL in C11 and MSVC - decided not to support them. So we'll go ahead and remove the VLA - usage altogether. -*/ -#define FSLPRINTF_HAVE_VARARRAY 0 -#if 0 #if !defined(FSLPRINTF_HAVE_VARARRAY) # if defined(__TINYC__) # define FSLPRINTF_HAVE_VARARRAY 0 # else # if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) @@ -2502,11 +1601,10 @@ # define FSLPRINTF_HAVE_VARARRAY 1 /*use 1 in C99 mode */ # else # define FSLPRINTF_HAVE_VARARRAY 0 # endif # endif -#endif #endif /* Conversion types fall into various categories as defined by the following enumeration. @@ -2893,11 +1991,11 @@ converted to %XX, where XX is their hex value) and passes the encoded string to pf(). It returns the total length of the output string. */ static int spech_urlencode( fsl_output_f pf, void * pfArg, - unsigned int pfLen __unused, void * varg ){ + unsigned int pfLen, void * varg ){ char const * str = (char const *) varg; int rc = 0; char ch = 0; char const * hex = "0123456789ABCDEF"; #define xbufsz 10 @@ -3403,20 +2501,20 @@ char * V; \ if((int)(N)<((int)sizeof(V##2))){ \ V = V##2; \ }else{ \ V = (char *)fsl_malloc(N+1); \ - if(!V) {FSLPRINTF_RETURN(FSL_RC_OOM);} \ + if(!V) {FSLPRINTF_RETURN;} \ } # define FSLPRINTF_CHARARRAY_FREE(V) if(V!=V##2) fsl_free(V) #endif /* FSLPRINTF_RETURN, FSLPRINTF_CHECKERR, and FSLPRINTF_SPACES are internal helpers. */ -#define FSLPRINTF_RETURN(RC) if( zExtra ) fsl_free(zExtra); return RC -#define FSLPRINTF_CHECKERR if( 0!=pfrc ) { FSLPRINTF_RETURN(pfrc); } (void)0 +#define FSLPRINTF_RETURN if( zExtra ) fsl_free(zExtra); return pfrc +#define FSLPRINTF_CHECKERR if( 0!=pfrc ) { FSLPRINTF_RETURN; } (void)0 #define FSLPRINTF_SPACES(N) \ { \ FSLPRINTF_CHARARRAY(zSpaces,N); \ memset( zSpaces,' ',N); \ pfrc = pfAppend(pfAppendArg, zSpaces, N); \ @@ -3524,11 +2622,11 @@ if( infop ) xtype = infop->type; #undef FMTINFO #undef FMTNDX zExtra = 0; if( (!infop) || (!infop->type) ){ - FSLPRINTF_RETURN(FSL_RC_RANGE); + FSLPRINTF_RETURN; } /* Limit the precision to prevent overflowing buf[] during conversion */ if( precision>FSLPRINTF_BUF_SIZE-40 && (infop->flags & FLAG_STRING)==0 ){ precision = FSLPRINTF_BUF_SIZE-40; @@ -3557,11 +2655,11 @@ */ switch( xtype ){ case etPOINTER: flag_longlong = sizeof(char*)==sizeof(int64_t); flag_long = sizeof(char*)==sizeof(long int); - FSL_SWITCH_FALL_THROUGH; + /* Fall through into the next case */ case etORDINAL: case etRADIX: if( infop->flags & FLAG_SIGNED ){ int64_t v; if( flag_longlong ) v = va_arg(ap,int64_t); @@ -3819,11 +2917,11 @@ int limit = flag_alternateform ? va_arg(ap,int) : -1; char const *e = va_arg(ap,char const*); if( e && *e ){ length = StrNLen32(e, limit); zExtra = bufpt = fsl_malloc(length+1); - if(!zExtra) return FSL_RC_OOM; + if(!zExtra) return -1; for( i=0; i=0) && (length>limit)) length = limit; check = fsl_bytes_fossilize((unsigned char const *)bufpt, length, &fb); if(check){ fsl_buffer_reserve(&fb,0); - FSLPRINTF_RETURN(check); + FSLPRINTF_RETURN; } zExtra = bufpt = (char*)fb.mem /*transfer ownership*/; length = (int)fb.used; if( precision>=0 && precision=0); if(n==0 || (st->pos >= st->len)) return 0; else if((n + st->pos) > st->len){ n = st->len - st->pos; } memcpy(st->dest + st->pos, data, n); @@ -4481,70 +3580,57 @@ #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) -#define buf__is_external(b) (b->mem && 0==b->capacity) +#define buffer_is_external(b) (b->mem && 0==b->capacity) -#define buff__errcheck(B) if((B)->errCode) return (B)->errCode /** Materializes external buffer b by allocating b->used+extra+1 bytes, copying b->used bytes from b->mem to the new block, NUL-terminating the block, and replacing b->mem with the new block. Returns 0 on success, else FSL_RC_OOM. Asserts that b is an external buffer. */ static int fsl__buffer_materialize( fsl_buffer * const b, fsl_size_t extra ){ - assert(buf__is_external(b)); - buff__errcheck(b); + assert(buffer_is_external(b)); fsl_size_t const n = b->used + extra + 1; unsigned char * x = (unsigned char *)fsl_malloc(n); - if(!x) return b->errCode = FSL_RC_OOM; + if(!x) return FSL_RC_OOM; memcpy(x, b->mem, b->used); b->capacity = n; x[b->used] = 0; b->mem = x; return 0; } -int fsl_buffer_err( fsl_buffer const * b ){ - return b->errCode; -} - -void fsl_buffer_err_clear(fsl_buffer * const b){ - b->errCode = 0; -} - int fsl_buffer_materialize( fsl_buffer * const b ){ - buff__errcheck(b); - return buf__is_external(b) ? fsl__buffer_materialize(b, 0) : 0; + return buffer_is_external(b) ? fsl__buffer_materialize(b, 0) : 0; } -#define buf__materialize(B,N) (buf__is_external(B) ? fsl__buffer_materialize((B),(N)) : 0) +#define buffer_materialize(B,N) (buffer_is_external(B) ? fsl__buffer_materialize((B),(N)) : 0) void fsl_buffer_external( fsl_buffer * const b, void const * mem, fsl_int_t n ){ if(b->mem) fsl_buffer_clear(b); if(n<0) n =(fsl_int_t)fsl_strlen((char const *)mem); b->used = n; b->cursor = 0; - b->errCode = 0; b->mem = (unsigned char *)mem; b->capacity = 0; } fsl_buffer * fsl_buffer_reuse( fsl_buffer * const b ){ - if(buf__is_external(b)){ + if(buffer_is_external(b)){ *b = fsl_buffer_empty; }else{ if(b->capacity){ assert(b->mem); b->mem[0] = 0; b->used = 0; } b->cursor = 0; - b->errCode = 0; } return b; } void fsl_buffer_clear( fsl_buffer * const buf ){ @@ -4551,32 +3637,31 @@ if(buf->capacity) fsl_free(buf->mem); *buf = fsl_buffer_empty; } int fsl_buffer_reserve( fsl_buffer * const buf, fsl_size_t n ){ - if( 0 == n ){ - if(!buf__is_external(buf)){ + if( ! buf ) return FSL_RC_MISUSE; + else if( 0 == n ){ + if(!buffer_is_external(buf)){ fsl_free(buf->mem); }/* else if it has memory, it's owned elsewhere */ *buf = fsl_buffer_empty; return 0; - } - else buff__errcheck(buf); - else if( !buf__is_external(buf) && buf->capacity >= n ){ + }else if( !buffer_is_external(buf) && buf->capacity >= n ){ assert(buf->mem); return 0; }else{ unsigned char * x; - bool const isExt = buf__is_external(buf); + bool const isExt = buffer_is_external(buf); assert((buf->used < n) && "Buffer in-use greater than capacity!"); if(isExt && n<=buf->used){ /*For external buffers, always keep at least the initially-pointed-to size. */ n = buf->used + 1; } x = (unsigned char *)fsl_realloc( isExt ? NULL : buf->mem, n ); - if( !x ) return buf->errCode = FSL_RC_OOM; + if( !x ) return FSL_RC_OOM; else if(isExt){ memcpy( x, buf->mem, buf->used ); x[buf->used] = 0; }else{ memset( x + buf->used, 0, n - buf->used ); @@ -4586,20 +3671,19 @@ return 0; } } int fsl_buffer_resize( fsl_buffer * const b, fsl_size_t n ){ - buff__errcheck(b); - else if(buf__is_external(b)){ + if(buffer_is_external(b)){ if(n==b->used) return 0; else if(n==0){ b->capacity = 0; fsl_buffer_external(b, "", 0); return 0; } unsigned char * x = (unsigned char *)fsl_malloc( n+1/*NUL*/ ); - if( !x ) return b->errCode = FSL_RC_OOM; + if( !x ) return FSL_RC_OOM; memcpy(x, b->mem, n < b->used ? n : b->used); x[n] = 0; b->mem = x; b->capacity = n+1; b->used = n; @@ -4609,11 +3693,11 @@ b->mem[n] = 0; return 0; }else{ unsigned char * x = (unsigned char *)fsl_realloc( b->mem, n+1/*NUL*/ ); - if( ! x ) return b->errCode = FSL_RC_OOM; + if( ! x ) return FSL_RC_OOM; if(n > b->capacity){ /* zero-fill new parts */ memset( x + b->capacity, 0, n - b->capacity +1/*NUL*/ ); } b->capacity = n + 1 /*NUL*/; @@ -4635,18 +3719,10 @@ : ((szLerrCode){ - fsl_size_t sz = b->used; - if(len<0) len = (fsl_int_t)fsl_strlen((char const *)data); - if(buf__materialize(b, (fsl_size_t)len + 1)) return b->errCode; - assert(b->capacity ? !!b->mem : !b->mem); - assert(b->used <= b->capacity); - sz += len + 1/*NUL*/; - if(b->capacityerrCode){ - assert(b->capacity >= sz); - if(len>0) memcpy(b->mem + b->used, data, (size_t)len); - b->used += len; - b->mem[b->used] = 0; - } - } - return b->errCode; + fsl_size_t sz = b->used; + if(len<0) len = (fsl_int_t)fsl_strlen((char const *)data); + if(buffer_materialize(b, (fsl_size_t)len + 1)) return FSL_RC_OOM; + assert(b->capacity ? !!b->mem : !b->mem); + assert(b->used <= b->capacity); + sz += len + 1/*NUL*/; + int const rc = b->capacitycapacity >= sz); + if(len>0) memcpy(b->mem + b->used, data, (size_t)len); + b->used += len; + b->mem[b->used] = 0; + } + return rc; } int fsl_buffer_appendfv( fsl_buffer * const b, char const * fmt, va_list args){ return fsl_appendfv( fsl_output_f_buffer, b, fmt, args ); @@ -4694,35 +3768,36 @@ } int fsl_buffer_appendf( fsl_buffer * const b, char const * fmt, ... ){ - buff__errcheck(b); + if(!b || !fmt) return FSL_RC_MISUSE; else{ + int rc; va_list args; va_start(args,fmt); - fsl_buffer_appendfv( b, fmt, args ); + rc = fsl_buffer_appendfv( b, fmt, args ); va_end(args); - return b->errCode; + return rc; } } char const * fsl_buffer_cstr(fsl_buffer const * const b){ - return b->errCode ? NULL : (char const *)b->mem; + return b ? (char const *)b->mem : NULL; } char const * fsl_buffer_cstr2(fsl_buffer const * const b, fsl_size_t * const len){ char const * rc = NULL; - if(0==b->errCode){ + if(b){ rc = (char const *)b->mem; if(len) *len = b->used; } return rc; } char * fsl_buffer_str(fsl_buffer const * const b){ - return b->errCode ? NULL : (char *)b->mem; + return (char *)b->mem; } #if 0 fsl_size_t fsl_buffer_size(fsl_buffer const * const b){ @@ -4821,13 +3896,11 @@ fsl_int_t fsl_buffer_uncompressed_size(fsl_buffer const * b){ return fsl_data_uncompressed_size(b->mem, b->used); } -int fsl_buffer_compress(fsl_buffer const *pIn, fsl_buffer * const pOut){ - buff__errcheck(pIn); - else buff__errcheck(pOut); +int fsl_buffer_compress(fsl_buffer const *pIn, fsl_buffer *pOut){ unsigned int nIn = pIn->used; unsigned int nOut = 13 + nIn + (nIn+999)/1000; fsl_buffer temp = fsl_buffer_empty; int rc = fsl_buffer_resize(&temp, nOut+4); if(rc) return rc; @@ -4859,14 +3932,11 @@ return rc; } } int fsl_buffer_compress2(fsl_buffer const *pIn1, - fsl_buffer const *pIn2, fsl_buffer * const pOut){ - buff__errcheck(pIn1); - else buff__errcheck(pIn2); - else buff__errcheck(pOut); + fsl_buffer const *pIn2, fsl_buffer *pOut){ unsigned int nIn = pIn1->used + pIn2->used; unsigned int nOut = 13 + nIn + (nIn+999)/1000; fsl_buffer temp = fsl_buffer_empty; int rc; rc = fsl_buffer_resize(&temp, nOut+4); @@ -4906,12 +3976,10 @@ return rc; } } int fsl_buffer_uncompress(fsl_buffer const * const pIn, fsl_buffer * const pOut){ - buff__errcheck(pIn); - else buff__errcheck(pOut); unsigned int nOut; unsigned char *inBuf; unsigned int const nIn = pIn->used; fsl_buffer temp = fsl_buffer_empty; int rc; @@ -4920,11 +3988,10 @@ if(pIn==pOut || !pIn->mem) rc = 0; else{ fsl_buffer_reuse(pOut); rc = fsl_buffer_append(pOut, pIn->mem, pIn->used); } - assert(pOut->errCode == rc); return rc; } inBuf = pIn->mem; nOut = (inBuf[0]<<24) + (inBuf[1]<<16) + (inBuf[2]<<8) + inBuf[3]; /* MARKER(("decompress size: %u\n", nOut)); */ @@ -4942,11 +4009,11 @@ #else fsl_buffer_swap(&temp, pOut); #endif }else{ rc = fsl_buffer_reserve(&temp, nOut+1); - if(rc) return pOut->errCode = rc; + if(rc) return rc; temp.mem[nOut] = 0; } nOut2 = (long int)nOut; rc = uncompress(temp.mem, &nOut2, &inBuf[4], nIn - 4) @@ -4987,22 +4054,23 @@ assert(!"Cannot happen!"); rc = FSL_RC_RANGE; break; default: rc = FSL_RC_ERROR; break; } if(temp.mem!=pOut->mem) fsl_buffer_clear(&temp); - return pOut->errCode = rc; + return rc; } int fsl_buffer_fill_from( fsl_buffer * const dest, fsl_input_f src, - void * const state ){ - buff__errcheck(dest); + void * const state ) +{ int rc; enum { BufSize = 512 * 8 }; char rbuf[BufSize]; fsl_size_t total = 0; fsl_size_t rlen = 0; + if( !dest || ! src ) return FSL_RC_MISUSE; fsl_buffer_reuse(dest); while(1){ rlen = BufSize; rc = src( state, rbuf, &rlen ); if( rc ) break; @@ -5035,11 +4103,10 @@ } int fsl_buffer_fill_from_filename( fsl_buffer * const dest, char const * filename ){ - buff__errcheck(dest); int rc; FILE * src; fsl_fstat st = fsl_fstat_empty; /* This stat() is only an optimization to reserve all needed memory up front. @@ -5056,17 +4123,17 @@ fsl_fclose(src); } return rc; } -void fsl_buffer_swap( fsl_buffer * const left, fsl_buffer * const right ){ +void fsl_buffer_swap( fsl_buffer * left, fsl_buffer * right ){ fsl_buffer const tmp = *left; *left = *right; *right = tmp; } -void fsl_buffer_swap_free( fsl_buffer * const left, fsl_buffer * const right, int clearWhich ){ +void fsl_buffer_swap_free( fsl_buffer * left, fsl_buffer * right, int clearWhich ){ fsl_buffer_swap(left, right); if(0 != clearWhich) fsl_buffer_reserve((clearWhich<0) ? left : right, 0); } int fsl_buffer_copy( fsl_buffer * const dest, @@ -5079,13 +4146,10 @@ int fsl_buffer_delta_apply2( fsl_buffer const * const orig, fsl_buffer const * const pDelta, fsl_buffer * const pTarget, fsl_error * const pErr){ - buff__errcheck(orig); - else buff__errcheck(pDelta); - else buff__errcheck(pTarget); int rc; fsl_size_t n = 0; fsl_buffer out = fsl_buffer_empty; rc = fsl_delta_applied_size( pDelta->mem, pDelta->used, &n); if(rc){ @@ -5092,17 +4156,17 @@ if(pErr){ fsl_error_set(pErr, rc, "fsl_delta_applied_size() failed."); } return rc; } - assert(n>0); rc = fsl_buffer_resize( &out, n ); + if(rc) return rc; + rc = fsl_delta_apply2( orig->mem, orig->used, + pDelta->mem, pDelta->used, + out.mem, pErr); if(0==rc){ - rc = fsl_delta_apply2( orig->mem, orig->used, - pDelta->mem, pDelta->used, - out.mem, pErr); - if(0==rc) fsl_buffer_swap(&out, pTarget); + fsl_buffer_swap(&out, pTarget); } fsl_buffer_clear(&out); return rc; } @@ -5115,11 +4179,10 @@ void fsl_buffer_defossilize( fsl_buffer * const b ){ fsl_bytes_defossilize( b->mem, &b->used ); } int fsl_buffer_to_filename( fsl_buffer const * const b, char const * fname ){ - buff__errcheck(b); FILE * f; int rc = 0; if(!b || !fname) return FSL_RC_MISUSE; f = fsl_fopen(fname, "wb"); if(!f) rc = fsl_errno_to_rc(errno, FSL_RC_IO); @@ -5149,14 +4212,16 @@ return rc; } int fsl_output_f_buffer( void * state, void const * src, fsl_size_t n ){ - return fsl_buffer_append((fsl_buffer*)state, src, n); + return (!state || !src) + ? FSL_RC_MISUSE + : fsl_buffer_append((fsl_buffer*)state, src, n); } -int fsl_finalizer_f_buffer( void * state __unused, void * mem ){ +int fsl_finalizer_f_buffer( void * state, void * mem ){ fsl_buffer * b = (fsl_buffer*)mem; fsl_buffer_reserve(b, 0); *b = fsl_buffer_empty; return 0; } @@ -5172,11 +4237,10 @@ } } int fsl_buffer_stream_lines(fsl_output_f fTo, void * const toState, fsl_buffer * const pFrom, fsl_size_t N){ - buff__errcheck(pFrom); char *z = (char *)pFrom->mem; fsl_size_t i = pFrom->cursor; fsl_size_t n = pFrom->used; fsl_size_t cnt = 0; int rc = 0; @@ -5203,11 +4267,10 @@ int fsl_buffer_copy_lines(fsl_buffer * const pTo, fsl_buffer * const pFrom, fsl_size_t N){ #if 1 - if(pTo) buff__errcheck(pTo); return fsl_buffer_stream_lines( pTo ? fsl_output_f_buffer : NULL, pTo, pFrom, N ); #else char *z = (char *)pFrom->mem; fsl_size_t i = pFrom->cursor; @@ -5234,12 +4297,11 @@ return rc; #endif } int fsl_input_f_buffer( void * state, void * dest, fsl_size_t * n ){ - fsl_buffer * const b = (fsl_buffer*)state; - buff__errcheck(b); + fsl_buffer * b = (fsl_buffer*)state; fsl_size_t const from = b->cursor; fsl_size_t to; fsl_size_t c; if(from >= b->used){ *n = 0; @@ -5291,11 +4353,11 @@ } } char * fsl_buffer_take(fsl_buffer * const b){ char * z = NULL; - if(0==buf__materialize(b,0)){ + if(0==buffer_materialize(b,0)){ z = (char *)b->mem; *b = fsl_buffer_empty; } return z; } @@ -5303,11 +4365,10 @@ fsl_size_t fsl_buffer_seek(fsl_buffer * const b, fsl_int_t offset, fsl_buffer_seek_e whence){ int64_t c = (int64_t)b->cursor; switch(whence){ case FSL_BUFFER_SEEK_SET: c = offset; - __attribute__ ((fallthrough)); case FSL_BUFFER_SEEK_CUR: c = (int64_t)b->cursor + offset; break; case FSL_BUFFER_SEEK_END: c = (int64_t)b->used + offset; /* ^^^^^ fossil(1) uses (used + offset - 1) but @@ -5336,32 +4397,31 @@ void fsl_buffer_rewind(fsl_buffer * const b){ b->cursor = 0; } -int fsl_id_bag_to_buffer(fsl_id_bag const * bag, fsl_buffer * const b, +int fsl_id_bag_to_buffer(fsl_id_bag const * bag, fsl_buffer * b, char const * separator){ int i = 0; fsl_int_t const sepLen = (fsl_id_t)fsl_strlen(separator); - fsl_buffer_reserve(b, b->used + (bag->entryCount * 7) - + (bag->entryCount * sepLen)); + int rc = fsl_buffer_reserve(b, b->used + (bag->entryCount * 7) + + (bag->entryCount * sepLen)); for(fsl_id_t e = fsl_id_bag_first(bag); - !b->errCode && e; e = fsl_id_bag_next(bag, e)){ - if(i++) fsl_buffer_append(b, separator, sepLen); - fsl_buffer_appendf(b, "%" FSL_ID_T_PFMT, e); + !rc && e; e = fsl_id_bag_next(bag, e)){ + if(i++) rc = fsl_buffer_append(b, separator, sepLen); + if(!rc) rc = fsl_buffer_appendf(b, "%" FSL_ID_T_PFMT, e); } - return b->errCode; + return rc; } int fsl_buffer_append_tcl_literal(fsl_buffer * const b, bool escapeSquigglies, char const * z, fsl_int_t n){ - buff__errcheck(b); int rc; if(n<0) n = fsl_strlen(z); - fsl_buffer_append(b, "\"", 1); - for(fsl_int_t i=0; 0==b->errCode && ierrCode; + if(0==rc) rc = fsl_buffer_append(b, "\"", 1); + return rc; } #undef MARKER -#undef buf__is_external -#undef buf__errcheck -#undef buf__materialize +#undef buffer_is_external +#undef buffer_materialize /* end of file ./src/buffer.c */ /* start of file ./src/cache.c */ /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* @@ -5664,42 +4723,44 @@ if(!db) return FSL_RC_NOT_A_CKOUT; sql = fsl__cx_scratchpad(f); if(0>=vid) vid = f->ckout.rid; if(zName && *zName && !('.'==*zName && !zName[1])){ - fsl_buffer_appendf(sql, - "SELECT id FROM vfile WHERE vid=%" - FSL_ID_T_PFMT - " AND fsl_match_vfile_or_dir(pathname,%Q)", - vid, zName); + rc = fsl_buffer_appendf(sql, + "SELECT id FROM vfile WHERE vid=%" + FSL_ID_T_PFMT + " AND fsl_match_vfile_or_dir(pathname,%Q)", + vid, zName); }else{ - fsl_buffer_appendf(sql, - "SELECT id FROM vfile WHERE vid=%" FSL_ID_T_PFMT, - vid); - } - if(changedOnly){ - fsl_buffer_append(sql, " AND (chnged OR deleted OR rid=0 " - "OR (origname IS NOT NULL AND " - " origname<>pathname))", -1); + rc = fsl_buffer_appendf(sql, + "SELECT id FROM vfile WHERE vid=%" FSL_ID_T_PFMT, + vid); + } + if(rc) goto end; + else if(changedOnly){ + rc = fsl_buffer_append(sql, " AND (chnged OR deleted OR rid=0 " + "OR (origname IS NOT NULL AND " + " origname<>pathname))", -1); + if(rc) goto end; } rc = fsl_buffer_appendf(sql, " /* %s() */", __func__); - if(0==rc) rc = fsl_db_prepare(db, &st, "%b", sql); + if(rc) goto end; + rc = fsl_db_prepare(db, &st, "%b", sql); while(!rc && (FSL_RC_STEP_ROW == (rc=fsl_stmt_step(&st)))){ rc = fsl_id_bag_insert( dest, fsl_stmt_g_id(&st, 0) ); } if(FSL_RC_STEP_DONE==rc) rc = 0; + end: fsl__cx_scratchpad_yield(f, sql); fsl_stmt_finalize(&st); if(rc && !f->error.code && db->error.code){ fsl_cx_uplift_db_error(f, db); } return rc; } -int fsl_filename_to_vfile_id( fsl_cx * const f, fsl_id_t vid, - char const * zName, - fsl_id_t * const vfid ){ +int fsl_filename_to_vfile_id( fsl_cx * f, fsl_id_t vid, char const * zName, fsl_id_t * vfid ){ fsl_db * db = fsl_needs_ckout(f); int rc; fsl_stmt st = fsl_stmt_empty; assert(db); if(!db) return FSL_RC_NOT_A_CKOUT; @@ -9712,367 +8773,10 @@ if(fsl_file_size(z)>=1024) return dbName; } return NULL; } -/** - Internal helper for fsl_ckout_rename(). Performs the vfile update - for renaming zFrom to zTo, taking into account certain - vfile-semantics error conditions. -*/ -static int fsl__mv_one_file(fsl_cx * const f, char const * zFrom, - char const * zTo, bool dryRun){ - fsl_db * const db = fsl_cx_db_ckout(f); - int deleted = fsl_db_g_int32(db, -1, - "SELECT deleted FROM vfile WHERE vid=%"FSL_ID_T_PFMT - " AND pathname=%Q %s", - f->ckout.rid, zTo, fsl_cx_filename_collation(f)); - if(deleted>=0){ - if(0==deleted){ - if( !fsl_cx_is_case_sensitive(f,false) && - 0==fsl_stricmp(zFrom, zTo) ){ - /* Case change only */ - }else{ - return fsl_cx_err_set(f, FSL_RC_ALREADY_EXISTS, - "Cannot rename '%s' to '%s' because " - "another file named '%s' is already " - "under management.", zFrom, zTo, zTo); - } - }else{ - return fsl_cx_err_set(f, FSL_RC_CONSISTENCY, - "Cannot rename '%s' to '%s' because " - "a pending deletion of '%s' has not " - "yet been checked in.", zFrom, zTo, zTo); - } - } - int rc = 0; - if( !dryRun ){ - rc = fsl_cx_exec(f, "UPDATE vfile SET pathname=%Q WHERE " - "pathname=%Q %s AND vid=%"FSL_ID_T_PFMT, - zTo, zFrom, fsl_cx_filename_collation(f), - f->ckout.rid); - } - return rc; -} - -/** - Internal helper for fsl_ckout_rename(). Performs the filesystem-level - moving of all files in the TEMP.ckout_mv table. All fs-level errors - _are ignored_. -*/ -static int fsl__rename_process_fmove(fsl_cx * const f){ - int rc = 0; - fsl_stmt q = fsl_stmt_empty; - bool const allowSymlinks = fsl_cx_allows_symlinks(f, false); - rc = fsl_cx_prepare(f, &q, "SELECT fsl_ckout_dir()||f, " - "fsl_ckout_dir()||t " - "FROM ckout_mv ORDER BY 1"); - while(0==rc && FSL_RC_STEP_ROW==fsl_stmt_step(&q)){ - char const * zFrom = fsl_stmt_g_text(&q, 0, NULL); - char const * zTo = fsl_stmt_g_text(&q, 1, NULL); - if(!zFrom || !zTo){FSL__WARN_OOM; rc = FSL_RC_OOM; break;} - //MARKER(("MOVING: %s ==> %s\n", zFrom, zTo)); - int const fromDirCheck = fsl_dir_check(zFrom); - int fsrc; - if(fromDirCheck>0){ - /* This case is "impossible." Unless... perhaps... a user - somehow moves things around in the filesystem during the - fsl_ckout_rename(), such that a to-be-renamed entry which was - formerly a file is not a directory. */ -#if 0 - assert(!"This case cannot possibly happen."); - fsl__fatal(FSL_RC_CANNOT_HAPPEN, - "Input name for a file-rename is a directory: %s", - zFrom)/*does not return*/; -#endif - int const toDirCheck = fsl_dir_check(zTo); - if(0==toDirCheck){ - fsl_file_rename(zFrom, zTo); - } - }else if(fromDirCheck<0){ - if(fsl_is_symlink(zFrom)){ - fsrc = fsl_symlink_copy(zFrom, zTo, allowSymlinks); - }else{ - fsrc = fsl_file_copy(zFrom, zTo); - } - if(0==fsrc){ - /* fossil(1) unconditionally unlinks zFrom if zFrom is not a - directory. Maybe we should too? */ - fsl_file_unlink(zFrom); - } - } - } - fsl_stmt_finalize(&q); - return rc; -} - -int fsl_ckout_rename(fsl_cx * const f, fsl_ckout_rename_opt const * opt){ - int rc = 0; - bool inTrans = false; - fsl_buffer * const bDest = fsl__cx_scratchpad(f) - /* Destination directory */; - fsl_buffer * const bSrc = fsl__cx_scratchpad(f) - /* One source file/dir */; - fsl_stmt qName = fsl_stmt_empty; - fsl_stmt qMv = fsl_stmt_empty; - int origType = 0 - /* -1 == multiple input files, 0 == one file, 1 == directory */; - int destType = 0 - /* >0==directory, 0==does not exist, <0==non-dir */; - uint32_t srcCount = 0; - - if(!opt->src->used){ - rc = fsl_cx_err_set(f, FSL_RC_RANGE, - "Expecting 1 or more source files/directories."); - goto end; - } - rc = fsl_cx_transaction_begin(f); - if(rc) goto end; - inTrans = true; - rc = fsl_ckout_filename_check(f, opt->relativeToCwd, opt->dest, - bDest); - if(rc) goto end; - fsl_buffer_strip_slashes(bDest); - - rc = fsl_cx_exec_multi(f, "DROP TABLE IF EXISTS TEMP.ckout_mv; " - "CREATE TEMP TABLE ckout_mv(" - "f TEXT UNIQUE ON CONFLICT IGNORE, t TEXT)"); - if(rc) goto end; - rc = fsl_cx_exec(f, "UPDATE vfile SET origname=pathname " - "WHERE origname IS NULL"); - if(rc) goto end; - - if(opt->src->used > 1){ - origType = -1; - }else{ - /* Make opt->src->list[0] absolute and see if it resolves to an - existing dir. */ - char const * zSrc= (char const *)opt->src->list[0]; - rc = fsl_ckout_filename_check(f, opt->relativeToCwd, zSrc, bSrc); - if(rc) goto end; - fsl_buffer * const bCheck = fsl__cx_scratchpad(f); - int oCheck = 0; - rc = fsl_buffer_append(bCheck, f->ckout.dir, f->ckout.dirLen); - if(0==rc) rc = fsl_buffer_append(bCheck, bSrc->mem, bSrc->used); - if(0==rc) oCheck = fsl_dir_check(fsl_buffer_cstr(bCheck)); - fsl__cx_scratchpad_yield(f, bCheck); - if(rc){FSL__WARN_OOM; goto end;} - if(oCheck>0) origType = 1; - else if(oCheck<0) origType = 0; - } - { - /* Make bDest absolute and see if it resolves to an existing dir. */ - fsl_buffer * const bCheck = fsl__cx_scratchpad(f); - rc = fsl_buffer_append(bCheck, f->ckout.dir, f->ckout.dirLen); - if(0==rc) rc = fsl_buffer_append(bCheck, bDest->mem, bDest->used); - if(0==rc) destType = fsl_dir_check(fsl_buffer_cstr(bCheck)); - fsl__cx_scratchpad_yield(f, bCheck); - if(rc){FSL__WARN_OOM; goto end;} - } - if(-1==origType && destType<=0){ - rc = fsl_cx_err_set(f, FSL_RC_MISUSE, - "Multiple source files provided, so " - "destination must be an existing directory."); - goto end; - }else if(1==origType && destType<0){ - rc = fsl_cx_err_set(f, FSL_RC_TYPE, - "Cannot rename '%s' to '%s' " - "because a non-directory named '%s' already exists.", - (char const *)opt->src->list[0], - opt->dest, opt->dest); - goto end; - }else if( 0==origType && destType<=0 ){ - /* Move single file to dest. */ - fsl_id_t vfidCheck = 0; - rc = fsl_filename_to_vfile_id(f, 0, fsl_buffer_cstr(bSrc), - &vfidCheck); - if(rc) goto end; - else if(!vfidCheck){ - rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND, - "File not under SCM management: %B", - bSrc); - goto end; - } - rc = fsl_cx_exec(f, "INSERT INTO ckout_mv(f,t) VALUES(%B,%B)", - bSrc, bDest); - if(rc) goto end; - else ++srcCount; - } else { - if(fsl_buffer_eq(bDest, ".", 1)){ - fsl_buffer_reuse(bDest); - }else{ - rc = fsl_buffer_append(bDest, "/", 1); - if(rc){FSL__WARN_OOM; goto end;} - } - rc = fsl_cx_prepare(f, &qName, "SELECT pathname FROM vfile" - " WHERE vid=%"FSL_ID_T_PFMT - " AND fsl_match_vfile_or_dir(pathname,?1)" - " ORDER BY 1", f->ckout.rid); - if(rc) goto end; - rc = fsl_cx_prepare(f, &qMv, "INSERT INTO ckout_mv(f,t) VALUES(" - "?1, ?2||?3)"); - if(rc) goto end; - for(fsl_size_t i = 0; i < opt->src->used; ++i){ - uint32_t nFound = 0; - char const * zSrc = (char const *)opt->src->list[i]; - fsl_buffer_reuse(bSrc); - rc = fsl_ckout_filename_check(f, opt->relativeToCwd, - zSrc, bSrc); - if(rc) goto end; - fsl_size_t nOrig = 0; - char const * const zOrig = fsl_buffer_cstr2(bSrc, &nOrig); - rc = fsl_stmt_bind_text(&qName, 1, zOrig, (fsl_int_t)nOrig, false); - if(rc) goto end; - while(FSL_RC_STEP_ROW==fsl_stmt_step(&qName)){ - fsl_size_t nPath = 0; - char const * zPath = NULL; - ++nFound; - rc = fsl_stmt_get_text(&qName, 0, &zPath, &nPath); - if(rc){fsl_cx_uplift_db_error(f, qName.db); goto end;} - else if(!zPath){FSL__WARN_OOM; rc = FSL_RC_OOM; goto end;} - char const * zTail; - if(nPath==nOrig){ - zTail = fsl_file_tail(zPath); - }else if(origType!=0 && destType>0 ){ - zTail = &zPath[nOrig-fsl_strlen(fsl_file_tail(zOrig))]; - }else{ - zTail = &zPath[nOrig+1]; - } - rc = fsl_stmt_bind_step(&qMv, "sbs", zPath, bDest, zTail); - if(0!=rc){ - fsl_cx_uplift_db_error(f, qMv.db); - goto end; - } - } - srcCount += nFound; - fsl_stmt_reset(&qName); - if(!nFound){ - rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND, - "Name does not resolve to any " - "SCM-managed files: %B", - bSrc); - goto end; - } - }/*for each opt->src*/ - } - assert(0==rc); - fsl_stmt_finalize(&qName); - fsl_stmt_finalize(&qMv); - if(0==srcCount){ - rc = fsl_cx_err_set(f, FSL_RC_NOT_FOUND, - "Source name(s) do not resolve to " - "any managed files."); - goto end; - } - rc = fsl_cx_prepare(f, &qName, "SELECT f, t FROM ckout_mv ORDER BY f"); - if(rc) goto end; - //rc = fsl_cx_prepare(f, &qMv, "INSERT - while(FSL_RC_STEP_ROW==fsl_stmt_step(&qName)){ - char const * zFrom = fsl_stmt_g_text(&qName, 0, NULL); - char const * zTo = fsl_stmt_g_text(&qName, 1, NULL); - rc = fsl__mv_one_file(f, zFrom, zTo, opt->dryRun); - if(rc) goto end; - if(opt->callback){ - rc = opt->callback(f, opt, zFrom, zTo); - if(rc) goto end; - } - } - end: - fsl_stmt_finalize(&qName); - fsl_stmt_finalize(&qMv); - fsl__cx_scratchpad_yield(f, bDest); - fsl__cx_scratchpad_yield(f, bSrc); - if(0==rc){ - assert(inTrans); - if(!opt->dryRun && opt->doFsMv){ - rc = fsl__rename_process_fmove(f); - } - fsl_cx_exec(f, "DROP TABLE TEMP.ckout_mv"); - } - if(inTrans){ - if(rc) fsl_cx_transaction_end(f, true); - else rc = fsl_cx_transaction_end(f, false); - } - return rc; -} - - -int fsl_ckout_rename_revert(fsl_cx * const f, char const *zNewName, - bool relativeToCwd, bool doFsMv, - bool *didSomething){ - fsl_buffer * bufFName = fsl__cx_scratchpad(f); - int rc = 0; - bool inTrans = false; - fsl_db * const dbC = fsl_needs_ckout(f); - fsl_stmt q = fsl_stmt_empty; - if(!dbC) return FSL_RC_NOT_A_CKOUT; - rc = fsl_ckout_filename_check(f, relativeToCwd, zNewName, bufFName); - if(rc) goto end; - rc = fsl_cx_transaction_begin(f); - if(rc) goto end; - inTrans = true; - rc = fsl_cx_prepare(f, &q, - "SELECT id FROM vfile " - "WHERE pathname=%Q AND origname<>pathname " - "and origname IS NOT NULL %s", - fsl_buffer_cstr(bufFName), - fsl_cx_filename_collation(f)); - if(rc) goto end; - switch(fsl_stmt_step(&q)){ - case FSL_RC_STEP_ROW: { - char const * zNameP = NULL; - char const * zNameO = NULL; - fsl_id_t const vfid = fsl_stmt_g_id(&q, 0); - assert(vfid>0); - fsl_stmt_finalize(&q); - if(doFsMv){ - rc = fsl_cx_prepare(f, &q, "SELECT fsl_ckout_dir()||pathname, " - "fsl_ckout_dir()||origname FROM vfile " - "WHERE id=%"FSL_ID_T_PFMT, vfid); - if(rc) goto end; - rc = fsl_stmt_step(&q); - assert(FSL_RC_STEP_ROW==rc && "We _just_ confirmed that these are there."); - zNameP = fsl_stmt_g_text(&q, 0, NULL); - zNameO = fsl_stmt_g_text(&q, 1, NULL); - if(!zNameO || !zNameO) {FSL__WARN_OOM; rc = FSL_RC_OOM; goto end;} - } - rc = fsl_cx_exec(f, "UPDATE vfile SET pathname=origname, origname=NULL " - "WHERE id=%"FSL_ID_T_PFMT, vfid); - if(rc) goto end; - if(didSomething) *didSomething = true; - if(doFsMv && fsl_is_file(zNameP)){ - assert(zNameO && zNameP); - fsl_file_unlink(zNameO); - rc = fsl_file_rename(zNameP, zNameO); - if(rc){ - rc = fsl_cx_err_set(f, rc, "File rename failed with code %s: " - "'%s' => '%s'", fsl_rc_cstr(rc), - zNameO + f->ckout.dirLen, - zNameP + f->ckout.dirLen); - } - } - break; - } - case FSL_RC_STEP_DONE: - if(didSomething) *didSomething = false; - goto end; - default: - rc = fsl_cx_uplift_db_error(f, dbC); - goto end; - } - - end: - fsl_stmt_finalize(&q); - fsl__cx_scratchpad_yield(f, bufFName); - if(inTrans){ - if(0==rc) rc = fsl_cx_transaction_end(f, false); - else fsl_cx_transaction_end(f, true); - } - return rc; - -} - #undef MARKER /* end of file ./src/checkout.c */ /* start of file ./src/cli.c */ /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ @@ -10085,16 +8789,14 @@ SPDX-FileType: Code Heavily indebted to the Fossil SCM project (https://fossil-scm.org). */ #include /* for strchr() */ -#include -#if !defined(FSL_AMALGAMATION_BUILD) +#if !defined(ORG_FOSSIL_SCM_FSL_CORE_H_INCLUDED) /* When not in the amalgamation build, force assert() to always work... */ # if defined(NDEBUG) # undef NDEBUG -# undef DEBUG # define DEBUG 1 # endif #endif #include /* for the benefit of test apps */ @@ -10126,11 +8828,10 @@ }, \ {/*config*/ \ -1/*traceSql*/, \ fsl_outputer_empty_m \ }, \ - {/*paths*/fsl_pathfinder_empty_m/*bins*/}, \ fsl_error_empty_m/*err*/ \ } const fcli_t fcli_empty = fcli_empty_m; fcli_t fcli = fcli_empty_m; @@ -10243,11 +8944,10 @@ fsl_cx * const f = fcli.f; int rc = 0; fsl_error_clear(&fcli.err); fsl_free(fcli.argv)/*contents are in the FCliFree list*/; - fsl_pathfinder_clear(&fcli.paths.bins); if(f){ while(fsl_cx_transaction_level(f)){ MARKER(("WARNING: open db transaction at shutdown-time. " "Rolling back.\n")); @@ -10304,11 +9004,11 @@ static const fcli_cliflag FCliFlagsGlobal[] = { FCLI_FLAG_BOOL_X("?","help",NULL, fcli_flag_f_help, "Show app help. Also triggered if the first non-flag is \"help\"."), FCLI_FLAG_BOOL(0,"lib-version", &fcli.transient.versionRequested, - "Show libfossil version number."), + "Show app version number."), FCLI_FLAG("R","repo","REPO-FILE",&fcli.transient.repoDbArg, "Selects a specific repository database, ignoring the one " "used by the current directory's checkout (if any)."), FCLI_FLAG(NULL,"user","username",&fcli.transient.userArg, "Sets the name of the fossil user name for this session."), @@ -10453,11 +9153,10 @@ simply doesn't fit the age-old muscle memory of: svn ci -m ... cvs ci -m ... fossil ci -m ... - f-ci -m ... */ for( f = defs; f->flagShort || f->flagLong; ++f ){ if(!f->flagValue && !f->callback){ /* We accept these for purposes of generating the --help text, but we can't otherwise do anything sensible with them and @@ -10491,10 +9190,11 @@ } //MARKER(("Got?=%d flag: %s/%s %s\n",gotIt, f->flagShort, f->flagLong, v ? v : "")); if(!gotIt){ continue; } + assert(f->flagValue || f->callback); if(f->flagValue) switch(f->flagType){ case FCLI_FLAG_TYPE_BOOL: *((bool*)f->flagValue) = true; break; case FCLI_FLAG_TYPE_BOOL_INVERT: @@ -10580,10 +9280,18 @@ continue; } FLAG("V") { fcli.clientFlags.verbose += 1; continue; + } + FLAG("VV") { + fcli.clientFlags.verbose += 2; + continue; + } + FLAG("VVV") { + fcli.clientFlags.verbose += 3; + continue; } FLAG("verbose") { fcli.clientFlags.verbose += 1; continue; } @@ -10712,11 +9420,11 @@ static fsl_allocator fslAllocOrig; /** Proxies fslAllocOrig.f() and abort()s on OOM conditions. */ -static void * fsl_realloc_f_failing(void * state __unused, void * mem, fsl_size_t n){ +static void * fsl_realloc_f_failing(void * state, void * mem, fsl_size_t n){ void * rv = fslAllocOrig.f(fslAllocOrig.state, mem, n); if(n && !rv){ fsl__fatal(FSL_RC_OOM, NULL)/*does not return*/; } return rv; @@ -11035,11 +9743,11 @@ former and doesn't have the fcli_command state, so can't do this. Maybe we can change that somehow. */ helpState = 1; helpPos = orig; arg = fcli_next_arg(1); // consume it - }else if(0==fsl_strcmp(arg,cmd->name) || 0==fcli_cmd_aliascmp(cmd,arg)){ + }else if(0==fsl_strcmp(arg,cmd->name) || fcli_cmd_aliascmp(cmd,arg)){ if(!cmd->f){ rc = fcli_err_set(FSL_RC_NYI, "Command [%s] has no " "callback function."); }else{ @@ -11063,19 +9771,18 @@ } } if(helpState){ f_out("\n"); fcli_command_help(helpPos, true, helpState>1); - fcli.transient.helpRequested++; }else if(!cmd->name){ fsl_buffer msg = fsl_buffer_empty; int rc2; if(!arg){ rc2 = FSL_RC_MISUSE; fsl_buffer_appendf(&msg, "No command provided."); }else{ - rc2 = FCLI_RC_NO_CMD; + rc2 = FSL_RC_NOT_FOUND; fsl_buffer_appendf(&msg, "Command not found: %s.",arg); } fsl_buffer_appendf(&msg, " Available commands: "); cmd = orig; for( ; cmd && cmd->name; ++cmd ){ @@ -11090,19 +9797,19 @@ fcli_err_report(0); } return rc; } -int fcli_cmd_aliascmp(fcli_command const * cmd, char const * arg){ +bool fcli_cmd_aliascmp(fcli_command const * cmd, char const * arg){ char const * alias = cmd->aliases; while ( alias && *alias!=0 ){ if( 0==fsl_strcmp(alias, arg) ){ - return 0; + return true; } alias = strchr(alias, 0) + 1; } - return 1; + return false; } void fcli_command_help(fcli_command const * cmd, bool showUsage, bool onlyOne){ fcli_command const * c = cmd; for( ; c->name; ++c ){ @@ -11426,118 +10133,10 @@ f->cache.blobContent.metrics.misses, f->cache.blobContent.used, f->cache.blobContent.szTotal); } -char const * fcli_fossil_binary(bool errIfNotFound, int reportPolicy){ - static bool once = false; - if(!once){ - int rc = 0; - char const * path = getenv("PATH"); - if(path && *path){ - fsl_path_splitter pt = fsl_path_splitter_empty; - fsl_size_t tLen = 0; - char const * t = 0; - fsl_path_splitter_init(&pt, path, -1); - while(0==rc && 0==fsl_path_splitter_next(&pt, &t, &tLen)){ - rc = fsl_pathfinder_dir_add2(&fcli.paths.bins, - t, (fsl_int_t)tLen); - } - } - if(0==rc){ - fsl_pathfinder_ext_add2(&fcli.paths.bins,".exe", 4); - } - once = true; - } - char const * z = NULL; - fsl_pathfinder_search(&fcli.paths.bins, "fossil", &z, NULL); - if(!z && errIfNotFound){ - fcli_err_set(FSL_RC_NOT_FOUND, - "Fossil binary not found in $PATH."); - if(reportPolicy){ - fcli_err_report(reportPolicy>0); - } - } - return z; -} - -static int fcli__transaction_check(void){ - if(fsl_cx_transaction_level(fcli.f)){ - return fcli_err_set(FSL_RC_LOCKED, - "Sync cannot succeed if a transaction " - "is opened. Close all transactions before " - "calling %s().", __func__); - } - return 0; -} - -static bool fcli__autosync_setting(void){ - return fsl_configs_get_bool(fcli.f, "crg", - fsl_configs_get_bool(fcli.f, "crg", - false, "autosync"), - "fcli.autosync"); -} - -int fcli_sync( int ops ){ - int rc = 0; - if((rc = fcli__transaction_check())) return rc; - - int doPush = -1; - int doPull = -1; - char const * zSuppressOut = ""; - fsl_db * const dbR = fsl_needs_repo(fcli.f); - if(!dbR){ - return FSL_RC_NOT_A_REPO; - }else if(!fsl_db_exists(dbR, "select 1 from config " - "where name like 'syncwith:%%'")){ - /* No remote, so nothing to do (and any attempt would fail). */ - return 0; - } - if(FCLI_SYNC_PULL & ops){ - doPull = 1; - } - if(FCLI_SYNC_PUSH & ops){ - doPush = 1; - } -#if !FSL_PLATFORM_IS_WINDOWS - if(FCLI_SYNC_NO_OUTPUT & ops){ - zSuppressOut = " >/dev/null 2>&1"; - }else if(FCLI_SYNC_NO_STDOUT & ops){ - zSuppressOut = " >/dev/null"; - } -#endif - bool const autosync = fcli__autosync_setting(); - if(!autosync && (FCLI_SYNC_AUTO & ops)){ - return 0; - } - if(doPull<=0 && doPush<=0){ - return 0; - } - char const * zCmd; - char const * fslBin; - if(doPull>0 && doPush>0) zCmd = "sync"; - else if(doPull>0) zCmd = "pull"; - else{ - assert(doPush>0); - zCmd = "push"; - } - fslBin = fcli_fossil_binary(true, 0); - if(!fslBin){ - assert(fcli__error->code); - return fcli__error->code; - } - ; - char * cmd = fsl_mprintf("%s %s%s", fslBin, zCmd, zSuppressOut); - rc = fsl_system(cmd); - if(rc){ - fsl_cx_caches_reset(fcli.f); - rc = fcli_err_set(rc, "Command exited with non-0 result: %s", cmd); - } - fsl_free(cmd); - return rc; -} - #undef FCLI_V3 #undef fcli_empty_m #undef fcli__error #undef MARKER #undef FCLI_USE_SIGACTION @@ -13054,11 +11653,11 @@ } } int fsl_config_versionable_filename(fsl_cx *f, char const * key, fsl_buffer *b){ - if(!fsl_needs_ckout(f)) return FSL_RC_NOT_A_CKOUT; + if(!f || !fsl_needs_ckout(f)) return FSL_RC_NOT_A_CKOUT; else if(!key || !*key || !fsl_is_simple_pathname(key, true)){ return FSL_RC_MISUSE; } fsl_buffer_reuse(b); return fsl_buffer_appendf(b, "%s.fossil-settings/%s", @@ -13065,11 +11664,11 @@ f->ckout.dir, key); } int fsl_config_unset( fsl_cx * const f, fsl_confdb_e mode, char const * key ){ - fsl_db * const db = fsl_config_for_role(f, mode); + fsl_db * db = fsl_config_for_role(f, mode); if(!db || !key || !*key) return FSL_RC_MISUSE; else if(mode==FSL_CONFDB_VERSIONABLE) return FSL_RC_UNSUPPORTED; else{ char const * table = fsl_config_table_for_role(mode); assert(table); @@ -13080,20 +11679,20 @@ int32_t fsl_config_get_int32( fsl_cx * const f, fsl_confdb_e mode, int32_t dflt, char const * key ){ int32_t rv = dflt; switch(mode){ case FSL_CONFDB_VERSIONABLE:{ - char * const val = fsl_config_get_text(f, mode, key, NULL); + char * val = fsl_config_get_text(f, mode, key, NULL); if(val){ rv = (int32_t)atoi(val); fsl_free(val); } break; } default: { fsl_db * const db = fsl_config_for_role(f, mode); - char const * const table = fsl_config_table_for_role(mode); + char const * table = fsl_config_table_for_role(mode); assert(table); if(db){ fsl_stmt * st = NULL; fsl_db_prepare_cached(db, &st, SELECT_FROM_CONFIG, table, __FILE__); @@ -13115,20 +11714,20 @@ int64_t fsl_config_get_int64( fsl_cx * const f, fsl_confdb_e mode, int64_t dflt, char const * key ){ int64_t rv = dflt; switch(mode){ case FSL_CONFDB_VERSIONABLE:{ - char * const val = fsl_config_get_text(f, mode, key, NULL); + char * val = fsl_config_get_text(f, mode, key, NULL); if(val){ rv = (int64_t)strtoll(val, NULL, 10); fsl_free(val); } break; } default: { - fsl_db * const db = fsl_config_for_role(f, mode); - char const * const table = fsl_config_table_for_role(mode); + fsl_db * db = fsl_config_for_role(f, mode); + char const * table = fsl_config_table_for_role(mode); assert(table); if(db){ fsl_stmt * st = NULL; fsl_db_prepare_cached(db, &st, SELECT_FROM_CONFIG, table, __FILE__); @@ -13157,22 +11756,22 @@ double fsl_config_get_double( fsl_cx * const f, fsl_confdb_e mode, double dflt, char const * key ){ double rv = dflt; switch(mode){ case FSL_CONFDB_VERSIONABLE:{ - char * const val = fsl_config_get_text(f, mode, key, NULL); + char * val = fsl_config_get_text(f, mode, key, NULL); if(val){ rv = strtod(val, NULL); fsl_free(val); } break; } default: { - fsl_db * const db = fsl_config_for_role(f, mode); + fsl_db * db = fsl_config_for_role(f, mode); if(!db) break/*e.g. global config is not opened*/; fsl_stmt * st = NULL; - char const * const table = fsl_config_table_for_role(mode); + char const * table = fsl_config_table_for_role(mode); assert(table); fsl_db_prepare_cached(db, &st, SELECT_FROM_CONFIG, table, __FILE__); if(st){ st->role = fsl__confdb_to_role(mode); @@ -13211,14 +11810,14 @@ case FSL_CONFDB_VERSIONABLE:{ if(!fsl_needs_ckout(f)){ rc = FSL_RC_NOT_A_CKOUT; break; } - fsl_buffer * const fname = fsl__cx_scratchpad(f); + fsl_buffer * fname = fsl__cx_scratchpad(f); rc = fsl_config_versionable_filename(f, key, fname); if(!rc){ - char const * const zFile = fsl_buffer_cstr(fname); + char const * zFile = fsl_buffer_cstr(fname); rc = fsl_stat(zFile, 0, false); if(rc){ rc = fsl_cx_err_set(f, rc, "Could not stat file: %s", zFile); }else{ @@ -13227,11 +11826,11 @@ } fsl__cx_scratchpad_yield(f,fname); break; } default: { - char const * const table = fsl_config_table_for_role(mode); + char const * table = fsl_config_table_for_role(mode); assert(table); fsl_db * const db = fsl_config_for_role(f, mode); if(!db) break; fsl_stmt * st = NULL; rc = fsl_db_prepare_cached(db, &st, SELECT_FROM_CONFIG, @@ -13242,11 +11841,11 @@ } st->role = fsl__confdb_to_role(mode); fsl_stmt_bind_text(st, 1, key, -1, 0); if(FSL_RC_STEP_ROW==fsl_stmt_step(st)){ fsl_size_t len = 0; - char const * const s = fsl_stmt_g_text(st, 0, &len); + char const * s = fsl_stmt_g_text(st, 0, &len); rc = s ? fsl_buffer_append(b, s, len) : 0; }else{ rc = FSL_RC_NOT_FOUND; } fsl_stmt_cached_yield(st); @@ -13257,35 +11856,35 @@ } bool fsl_config_get_bool( fsl_cx * const f, fsl_confdb_e mode, bool dflt, char const * key ){ bool rv = dflt; - switch((key && *key) ? mode : 999){ - case 999: break; + switch(mode){ case FSL_CONFDB_VERSIONABLE:{ - char * const val = fsl_config_get_text(f, mode, key, NULL); + char * val = fsl_config_get_text(f, mode, key, NULL); if(val){ rv = fsl_str_bool(val); fsl_free(val); } break; } default:{ int rc; fsl_stmt * st = NULL; - char const * const table = fsl_config_table_for_role(mode); + char const * table = fsl_config_table_for_role(mode); fsl_db * db; + if(!f || !key || !*key) break; db = fsl_config_for_role(f, mode); if(!db) break; assert(table); rc = fsl_db_prepare_cached(db, &st, SELECT_FROM_CONFIG, table, __FILE__); if(!rc){ st->role = fsl__confdb_to_role(mode); fsl_stmt_bind_text(st, 1, key, -1, 0); if(FSL_RC_STEP_ROW==fsl_stmt_step(st)){ - char const * const col = fsl_stmt_g_text(st, 0, NULL); + char const * col = fsl_stmt_g_text(st, 0, NULL); rv = col ? fsl_str_bool(col) : dflt /* 0? */; } fsl_stmt_cached_yield(st); } break; @@ -13303,11 +11902,11 @@ Returns non-0 on error. */ static int fsl_config_set_prepare( fsl_cx * const f, fsl_stmt **st, fsl_confdb_e mode, char const * key ){ char const * table = fsl_config_table_for_role(mode); - fsl_db * const db = fsl_config_for_role(f,mode); + fsl_db * db = fsl_config_for_role(f,mode); assert(table); if(!db || !key) return FSL_RC_MISUSE; else if(!*key) return FSL_RC_RANGE; else{ const char * sql = FSL_CONFDB_REPO==mode @@ -13322,10 +11921,11 @@ fsl_cx_uplift_db_error(f, db); } return rc; } } + /* TODO/FIXME: the fsl_config_set_xxx() routines all use the same basic structure, differing only in the concrete bind() op they call. They should be consolidated somehow. @@ -14298,25 +12898,18 @@ fsl_deck_finalize(&f->cache.mcache.decks[i]); } f->cache.mcache = fsl__mcache_empty; } -void fsl_cx_caches_reset(fsl_cx * const f){ +void fsl__cx_clear_repo_caches(fsl_cx * const f){ fsl__bccache_reset(&f->cache.blobContent); fsl__cx_mcache_clear(f); fsl__cx_clear_mf_seen(f, false); f->cache.allowSymlinks = f->cache.caseInsensitive = f->cache.seenDeltaManifest = f->cache.manifestSetting = -1; - if(fsl_cx_db_ckout(f)){ - fsl__ckout_version_fetch(f) - /* FIXME: this "really should" be fsl__cx_ckout_clear(), but that data is not fetched - on demand (maybe it should be?). */; - }else{ - fsl__cx_ckout_clear(f); - } } static void fsl__cx_finalize_cached_stmt(fsl_cx * const f){ #define STMT(X) fsl_stmt_finalize(&f->cache.stmt.X) STMT(deltaSrcId); @@ -14361,11 +12954,11 @@ fsl_buffer_clear(&f->cache.deltaContent); for(int i = 0; i < FSL_CX_NSCRATCH; ++i){ fsl_buffer_clear(&f->scratchpads.buf[i]); f->scratchpads.used[i] = false; } - fsl_cx_caches_reset(f); + fsl__cx_clear_repo_caches(f); fsl__bccache_clear(&f->cache.blobContent); fsl__cx_clear_mf_seen(f, true); fsl_id_bag_clear(&f->cache.leafCheck); fsl_id_bag_clear(&f->cache.toVerify); assert(NULL==f->cache.mfSeen.list); @@ -14429,11 +13022,12 @@ fsl_db_err_reset(&f->config.db); fsl_db_err_reset(&f->ckout.db); } int fsl_cx_err_set_e( fsl_cx * const f, fsl_error * const err ){ - if(!err){ + if(!f) return FSL_RC_MISUSE; + else if(!err){ return fsl_cx_err_set(f, 0, NULL); }else{ fsl_error_move(err, &f->error); fsl_error_clear(err); return f->error.code; @@ -14440,21 +13034,26 @@ } } int fsl_cx_err_setv( fsl_cx * const f, int code, char const * fmt, va_list args ){ - return fsl_error_setv( &f->error, code, fmt, args ); + return f + ? fsl_error_setv( &f->error, code, fmt, args ) + : FSL_RC_MISUSE; } int fsl_cx_err_set( fsl_cx * const f, int code, char const * fmt, ... ){ - int rc; - va_list args; - va_start(args,fmt); - rc = fsl_error_setv( &f->error, code, fmt, args ); - va_end(args); - return rc; + if(!f) return FSL_RC_MISUSE; + else{ + int rc; + va_list args; + va_start(args,fmt); + rc = fsl_error_setv( &f->error, code, fmt, args ); + va_end(args); + return rc; + } } int fsl_cx_err_get( fsl_cx * const f, char const ** str, fsl_size_t * len ){ #if 1 return fsl_error_get( &f->error, str, len ); @@ -14611,12 +13210,14 @@ }else{ fsl_db * const db = fsl__cx_db_for_role(f,r); int rc = 0; switch(r){ case FSL_DBROLE_REPO: + fsl__cx_clear_repo_caches(f); + break; case FSL_DBROLE_CKOUT: - fsl_cx_caches_reset(f); + fsl__cx_ckout_clear(f); break; default: fsl__fatal(FSL_RC_ERROR, "Cannot happen. Really."); } fsl__cx_finalize_cached_stmt(f); @@ -14916,44 +13517,21 @@ fsl_db_close(db); return rc; } int fsl_config_global_preferred_name(char ** zOut){ - char * zEnv = 0 /* from fsl_getenv(). Note the special-case free() - semantics!!! */; - char * zRc = 0 /* `*zOut` result, from fsl_mprintf() */; + char * zEnv = 0; + char * zRc = 0; int rc = 0; - -#if FSL_PLATFORM_IS_WINDOWS - zEnv = fsl_getenv("FOSSIL_HOME"); - if( zEnv==0 ){ - zEnv = fsl_getenv("LOCALAPPDATA"); - if( zEnv==0 ){ - zEnv = fsl_getenv("APPDATA"); - if( zEnv==0 ){ - zEnv = fsl_getenv("USERPROFILE"); - if( zEnv==0 ){ - char * const zDrive = fsl_getenv("HOMEDRIVE"); - char * const zPath = fsl_getenv("HOMEPATH"); - if( zDrive && zPath ){ - zRc = fsl_mprintf("%s%//_fossil", zDrive, zPath); - } - if(zDrive) fsl_filename_free(zDrive); - if(zPath) fsl_filename_free(zPath); - } - } - } - } - if(!zRc){ - if(!zEnv) rc = FSL_RC_NOT_FOUND; - else{ - zRc = fsl_mprintf("%//_fossil", zEnv); - if(!zRc) rc = FSL_RC_OOM; - } - } -#else fsl_buffer buf = fsl_buffer_empty; + +#if FSL_PLATFORM_IS_WINDOWS +# error "TODO: port in fossil(1) db.c:db_configdb_name() Windows bits" +#else + +#endif + /* Option 1: $FOSSIL_HOME/.fossil */ zEnv = fsl_getenv("FOSSIL_HOME"); if(zEnv){ zRc = fsl_mprintf("%s/.fossil", zEnv); if(!zRc) rc = FSL_RC_OOM; @@ -14990,18 +13568,18 @@ /* Option 5: fall back to $HOME/.fossil */ buf.used -= 8 /* "/.config" */; buf.mem[buf.used] = 0; rc = fsl_buffer_append(&buf, "/.fossil", 8); if(!rc) zRc = fsl_buffer_take(&buf); + end: - fsl_buffer_clear(&buf); -#endif if(zEnv) fsl_filename_free(zEnv); if(!rc){ - if(zRc) *zOut = zRc; - else rc = FSL_RC_OOM; + assert(zRc); + *zOut = zRc; } + fsl_buffer_clear(&buf); return rc; } int fsl_config_open( fsl_cx * const f, char const * openDbName ){ int rc = 0; @@ -15291,13 +13869,16 @@ int fsl__ckout_version_fetch( fsl_cx * const f ){ fsl_id_t rid = 0; int rc = 0; fsl_db * dbC = fsl_cx_db_ckout(f); fsl_db * dbR = dbC ? fsl_needs_repo(f) : NULL; + assert(!dbC || (dbC && dbR)); fsl__cx_ckout_clear(f); - if(!dbC) return 0; - else if(!dbR) return FSL_RC_NOT_A_REPO; + if(!dbC){ + return 0; + } + fsl_cx_err_reset(f); rid = fsl_config_get_id(f, FSL_CONFDB_CKOUT, -1, "checkout"); //MARKER(("rc=%s rid=%d\n",fsl_rc_cstr(f->error.code), (int)rid)); if(rid>0){ f->ckout.uuid = fsl_rid_to_uuid(f, rid); if(!f->ckout.uuid){ @@ -16379,11 +14960,11 @@ case SQLITE_NOLFS: case SQLITE_RANGE: rc = FSL_RC_RANGE; break; case SQLITE_NOTFOUND: rc = FSL_RC_NOT_FOUND; break; case SQLITE_PERM: case SQLITE_AUTH: - case SQLITE_LOCKED: rc = FSL_RC_LOCKED; break; + case SQLITE_LOCKED: case SQLITE_READONLY: rc = FSL_RC_ACCESS; break; case SQLITE_CORRUPT: rc = FSL_RC_CONSISTENCY; break; case SQLITE_CANTOPEN: case SQLITE_IOERR: rc = FSL_RC_IO; break; @@ -17110,12 +15691,11 @@ /* Don't reset() for ROW b/c that clears the column data! */ break; default: rc = fsl_error_set(&st->db->error, rc, - "Error stepping statement: %s", - sqlite3_errmsg(st->db->dbh)); + "Error stepping statement."); break; } } return rc; } @@ -17730,22 +16310,51 @@ is to flush all relevant caches. It is unfortunate that this bit is in the db class, as opposed to the fsl_cx class, but we currently have no hook which would allow us to trigger this from that class. */ - fsl_cx_caches_reset(db->f); +#if 1 + fsl__bccache_reset(&db->f->cache.blobContent); + fsl__cx_mcache_clear(db->f); +#else + /* This one resets all of ^^^ plus certain repo-side config + settings, but it's not yet clear whether that they will be + reloaded when needed. */ + fsl__cx_clear_repo_caches(d->f); +#endif } fsl_db_cleanup_beforeCommit(db); fsl_db_reset_change_count(db); rc = fsl_db_exec(db, db->doRollback ? "ROLLBACK" : "COMMIT"); - if(db->doRollback && db->f && changeCount>0 && db->f->ckout.rid>0){ - int const rc2 = fsl__ckout_version_fetch(db->f) - /*Else it might be out of sync, leading to chaos.*/; - if(0==rc && rc2!=0) rc = rc2; - } db->doRollback = 0; return rc; +#if 0 + /* original impl, for reference purposes during testing */ + if( g.db==0 ) return; + if( db.nBegin<=0 ) return; + if( rollbackFlag ) db.doRollback = 1; + db.nBegin--; + if( db.nBegin==0 ){ + int i; + if( db.doRollback==0 && db.nPriorChangesdbh || !b || !sql || !*sql) return FSL_RC_MISUSE; else{ fsl_stmt st = fsl_stmt_empty; int rc = 0; rc = fsl_db_preparev( db, &st, sql, args ); if(rc) return rc; @@ -18061,10 +16670,11 @@ int const len = sqlite3_column_bytes(st.stmt,0); if(len && !str){ rc = FSL_RC_OOM; }else{ rc = 0; + b->used = 0; rc = fsl_buffer_append( b, str, len ); } break; } case FSL_RC_STEP_DONE: @@ -18189,11 +16799,11 @@ if(!db || !db->dbh || !sql) return 0; else if(!*sql) return 0; else{ fsl_stmt st = fsl_stmt_empty; bool rv = false; - if(0==fsl_db_preparev(db, &st, sql, args)){ + if(!fsl_db_preparev(db, &st, sql, args)){ rv = FSL_RC_STEP_ROW==fsl_stmt_step(&st) ? true : false; } fsl_stmt_finalize(&st); return rv; } @@ -18717,21 +17327,21 @@ /** fsl_list_visitor_f() impl which requires that obj be-a (fsl_card_T*), which this function passes to fsl_card_T_free(). */ -static int fsl_list_v_card_T_free(void * obj, void * visitorState __unused){ +static int fsl_list_v_card_T_free(void * obj, void * visitorState ){ if(obj) fsl_card_T_free( (fsl_card_T*)obj ); return 0; } -static int fsl_list_v_card_Q_free(void * obj, void * visitorState __unused ){ +static int fsl_list_v_card_Q_free(void * obj, void * visitorState ){ if(obj) fsl_card_Q_free( (fsl_card_Q*)obj ); return 0; } -static int fsl_list_v_card_J_free(void * obj, void * visitorState __unused){ +static int fsl_list_v_card_J_free(void * obj, void * visitorState ){ if(obj) fsl_card_J_free( (fsl_card_J*)obj ); return 0; } fsl_deck * fsl_deck_malloc(){ @@ -21089,18 +19699,17 @@ assert(zName && *zName); if(!d->F.used) return NULL; else if(FSL_CARD_F_LIST_NEEDS_SORT & d->F.flags){ fsl_card_F_list_sort(&d->F); } - bool const caseSensitive = fsl_cx_is_case_sensitive(d->f, false); #define FCARD(NDX) F_at(&d->F, (NDX)) lwr = 0; upr = d->F.used-1; if( d->F.cursor>=lwr && d->F.cursorf && caseSensitive) - ? fsl_strcmp(FCARD(d->F.cursor+1)->name, zName) - : fsl_stricmp(FCARD(d->F.cursor+1)->name, zName); + c = (d->f && d->f->cache.caseInsensitive) + ? fsl_stricmp(FCARD(d->F.cursor+1)->name, zName) + : fsl_strcmp(FCARD(d->F.cursor+1)->name, zName); if( c==0 ){ if(atNdx) *atNdx = (uint32_t)d->F.cursor+1; return FCARD(++d->F.cursor); }else if( c>0 ){ upr = d->F.cursor; @@ -21108,13 +19717,13 @@ lwr = d->F.cursor+1; } } while( lwr<=upr ){ i = (lwr+upr)/2; - c = (d->f && caseSensitive) - ? fsl_strcmp(FCARD(i)->name, zName) - : fsl_stricmp(FCARD(i)->name, zName); + c = (d->f && d->f->cache.caseInsensitive) + ? fsl_stricmp(FCARD(i)->name, zName) + : fsl_strcmp(FCARD(i)->name, zName); if( c<0 ){ lwr = i+1; }else if( c>0 ){ upr = i-1; }else{ @@ -22002,11 +20611,11 @@ /** Overrideable crosslink listener which updates the timeline for attachment records. */ -static int fsl_deck_xlink_f_attachment(fsl_deck * const d, void * state __unused){ +static int fsl_deck_xlink_f_attachment(fsl_deck * const d, void * state){ if(FSL_SATYPE_ATTACHMENT!=d->type) return 0; int rc; fsl_db * const db = fsl_cx_db_repo(d->f); fsl_buffer * const comment = fsl__cx_scratchpad(d->f); const bool isAdd = (d->A.src && *d->A.src) ? 1 : 0; @@ -22080,11 +20689,11 @@ /** Overrideable crosslink listener which updates the timeline for checkin records. */ -static int fsl_deck_xlink_f_checkin(fsl_deck * const d, void * state __unused){ +static int fsl_deck_xlink_f_checkin(fsl_deck * const d, void * state){ if(FSL_SATYPE_CHECKIN!=d->type) return 0; int rc; fsl_db * db; db = fsl_cx_db_repo(d->f); assert(db); @@ -22128,11 +20737,11 @@ (int)FSL_TAGID_COMMENT, d->rid, d->D ); return fsl_cx_uplift_db_error2(d->f, db, rc); } -static int fsl_deck_xlink_f_control(fsl_deck * const d, void * state __unused){ +static int fsl_deck_xlink_f_control(fsl_deck * const d, void * state){ if(FSL_SATYPE_CONTROL!=d->type) return 0; /* Create timeline event entry for all tags in this control construct. Note that we are using a lot of historical code which hard-codes english-lanuage text and links which only work in @@ -22275,11 +20884,11 @@ fsl__cx_scratchpad_yield(d->f, comment); return rc; } -static int fsl_deck_xlink_f_forum(fsl_deck * const d, void * state __unused){ +static int fsl_deck_xlink_f_forum(fsl_deck * const d, void * state){ if(FSL_SATYPE_FORUMPOST!=d->type) return 0; int rc = 0; fsl_db * const db = fsl_cx_db_repo(d->f); assert(db); fsl_cx * const f = d->f; @@ -22361,11 +20970,11 @@ assert(db->error.code); return fsl_cx_uplift_db_error(f, db); } -static int fsl_deck_xlink_f_technote(fsl_deck * const d, void * state __unused){ +static int fsl_deck_xlink_f_technote(fsl_deck * const d, void * state){ if(FSL_SATYPE_TECHNOTE!=d->type) return 0; char buf[FSL_STRLEN_K256 + 7 /* event-UUID\0 */] = {0}; fsl_id_t tagid; char const * zTag; int rc = 0; @@ -22410,11 +21019,11 @@ (int)FSL_TAGID_BGCOLOR, d->rid); } return rc; } -static int fsl_deck_xlink_f_wiki(fsl_deck * const d, void * state __unused){ +static int fsl_deck_xlink_f_wiki(fsl_deck * const d, void * state){ if(FSL_SATYPE_WIKI!=d->type) return 0; int rc; char const * zWiki; fsl_size_t nWiki = 0; char cPrefix = 0; @@ -24809,15 +23418,12 @@ N -= 4; } sum3 += (sum2 << 8) + (sum1 << 16) + (sum0 << 24); switch(N){ case 3: sum3 += (z[2] << 8); - __attribute__ ((fallthrough)); case 2: sum3 += (z[1] << 16); - __attribute__ ((fallthrough)); case 1: sum3 += (z[0] << 24); - __attribute__ ((fallthrough)); default: ; } return sum3; } @@ -25042,17 +23648,18 @@ int fsl_delta_create( unsigned char const *zSrc, fsl_size_t lenSrc, unsigned char const *zOut, fsl_size_t lenOut, unsigned char *zDelta, fsl_size_t * deltaSize){ int rc; DeltaOutputString os; + if(!zSrc || !zOut || !zDelta || !deltaSize) return FSL_RC_MISUSE; os.mem = (unsigned char *)zDelta; os.cursor = 0; rc = fsl_delta_create2( zSrc, lenSrc, zOut, lenOut, fsl_output_f_ostring, &os ); if(!rc){ os.mem[os.cursor] = 0; - *deltaSize = os.cursor; + if(deltaSize) *deltaSize = os.cursor; } return rc; } /* @@ -25403,15 +24010,10 @@ if(1==b->passNumber){ DICOSTATE(sst); ++sst->displayLines; return 0; } - if(b->lnLHS+1==lnnoLHS && b->lnRHS+1==lnnoRHS){ - fdb__outf(b, "<<>>\n"); - } - //fdb__outf(b, "lnLHS=%d, lnRHS=%d\n", (int)b->lnLHS, (int)b->lnRHS); return fdb__outf(b, "@@ -%" PRIu32 ",%" PRIu32 " +%" PRIu32 ",%" PRIu32 " @@\n", lnnoLHS, linesLHS, lnnoRHS, linesRHS); #else return 0; @@ -25827,17 +24429,15 @@ static int fdb__utxt_chunkHeader(fsl_dibu* const b, uint32_t lnnoLHS, uint32_t linesLHS, uint32_t lnnoRHS, uint32_t linesRHS ){ /* - Ticket 746ebbe86c20b5c0f96cdadd19abd8284770de16: - - Annoying cosmetic bug: the libf impl of this diff will sometimes - render two directly-adjecent chunks with a separator, e.g.: + Annoying cosmetic bug: the libf impl of this diff will sometimes + render two directly-adjecent chunks with a separator, e.g.: */ - // $ f-vdiff --format u 072d63965188 a725befe5863 -l '*vdiff*' | head -30 + // $ f-vdiff --forat u 072d63965188 a725befe5863 -l '*vdiff*' | head -30 // Index: f-apps/f-vdiff.c // ================================================================== // --- f-apps/f-vdiff.c // +++ f-apps/f-vdiff.c // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -25863,11 +24463,11 @@ consecutive lines of code. In fossil(1) that case is accounted for in the higher-level diff engine, which can not only collapse adjacent blocks but also does the rendering of chunk headers in that main algorithm (something we cannot do in the library because we need the fsl_dibu to be able to output to arbitrary - destinations). We can only _partially_ account for it here, + distinations). We can only _partially_ account for it here, eliminating the extraneous ~~~ line when we're in line-number mode. In non-line-number mode we have to output the chunk header as-is. If we skip it then the _previous_ chunk header, if any, will contain incorrect numbers for the chunk, invaliding the diff for purposes of tools which import unified-format diffs. @@ -26162,11 +24762,11 @@ end: #undef RC return rc; } -static int fdb__tcl_finish(fsl_dibu * const b __unused){ +static int fdb__tcl_finish(fsl_dibu * const b){ int rc = 0; #if 0 BR_CLOSE; if(0==rc && FSL_DIBU_TCL_BRACES & b->implFlags){ rc = fdb__out(b, "\n", 1); @@ -26790,92 +25390,33 @@ ** ALGORITHM (subject to change and refinement): ** ** 1. If the subsequence is larger than 1/7th of the original span, ** then consider it valid. --> return 1 ** -** 2. If no lines of the subsequence contains more than one -** non-whitespace character, --> return 0 -** -** 3. If any line of the subsequence contains more than one non-whitespace -** character and is unique across the entire sequence after ignoring -** leading and trailing whitespace --> return 1 -** -** 4. Otherwise, it is potentially an artifact of an indentation +** 2. If the subsequence contains any charaters other than '}', '{", +** or whitespace, then consider it valid. --> return 1 +** +** 3. Otherwise, it is potentially an artifact of an indentation ** change. --> return 0 */ static bool likelyNotIndentChngArtifact( fsl__diff_cx const * const p, /* The complete diff context */ int iS1, /* Start of the main segment */ int iSX, /* Start of the subsequence */ int iEX, /* First row past the end of the subsequence */ int iE1 /* First row past the end of the main segment */ ){ - int i, j, n; - - /* Rule (1) */ - if( (iEX-iSX)*7 >= (iE1-iS1) ) return 1; - - /* Compute fsl_dline.indent and fsl_dline.nw for all lines of the subsequence. - ** If no lines contain more than one non-whitespace character return - ** 0 because the subsequence could be due to an indentation change. - ** Rule (2). - */ - n = 0; - for(i=iSX; iaFrom[i]; - if( pA->nw==0 && pA->n ){ - const char *zA = pA->z; - const int nn = pA->n; - int ii, jj; - for(ii=0; iiindent = ii; - for(jj=nn-1; jj>ii && diff_isspace(zA[jj]); jj--){} - pA->nw = jj - ii + 1; - } - if( pA->nw>1 ) n++; - } - if( n==0 ) return 0; - - /* Compute fsl_dline.indent and fsl_dline.nw for the entire sequence */ - for(i=iS1; i=iE1 ) break; - } - pA = &p->aFrom[i]; - if( pA->nw==0 && pA->n ){ - const char *zA = pA->z; - const int nn = pA->n; - int ii, jj; - for(ii=0; iiindent = ii; - for(jj=nn-1; jj>ii && diff_isspace(zA[jj]); jj--){} - pA->nw = jj - ii + 1; - } - } - - /* Check to see if any subsequence line that has more than one - ** non-whitespace character is unique across the entire sequence. - ** Rule (3) - */ - for(i=iSX; iaFrom[i].z + p->aFrom[i].indent; - const int nw = p->aFrom[i].nw; - if( nw<=1 ) continue; - for(j=iS1; jaFrom[j].nw!=nw ) continue; - if( memcmp(p->aFrom[j].z+p->aFrom[j].indent,z,nw)==0 ) break; - } - if( jaFrom[j].nw!=nw ) continue; - if( memcmp(p->aFrom[j].z+p->aFrom[j].indent,z,nw)==0 ) break; - } - if( j>=iE1 ) break; - } - return i= (iE1-iS1) ) return true; + for(i=iSX; iaFrom[i].z; + for(j=p->aFrom[i].n-1; j>=0; j--){ + char c = z[j]; + if( c!='}' && c!='{' && !diff_isspace(c) ) return true; + } + } + return false; } /** Do a single step in the difference. Compute a sequence of copy/delete/insert steps that will convert lines iS1 through iE1-1 @@ -27731,11 +26272,11 @@ unsigned char **pResult, unsigned *pNResult ); /* -** Make a copy of a list of nLine fsl_dline objects from one array to +** Make a copy of a list of nLine DLine objects from one array to ** another. Hash the new array to ignore whitespace. */ static void diffDLineXfer( fsl_dline *aTo, const fsl_dline *aFrom, @@ -28809,368 +27350,38 @@ This file implements technote (formerly known as event)-related parts of the library. */ #include - -/** - Fetches all "technote" (formerly "event") IDs from the repository - and appends each one to the given list in the form of a - (`char*`). This function relies on the `event-` tag prefix being - reserved for technotes and that the technote IDS are all exactly 40 - bytes long. - - Returns 0 on success, FSL_RC_NOT_A_REPO if f has no repository db - opened, FSL_RC_OOM if allocation of a new list entry fails, or - propagates db-related code on any other error. Results are - undefined if either argument is NULL. - - TODO? Reformulate this to be like fsl_tkt_id_to_rids(), returning - the list as RIDs? -*/ -/*FSL_EXPORT*/ int fsl_technote_ids_get(fsl_cx * const f, fsl_list * const tgt ); - -int fsl_technote_ids_get( fsl_cx * const f, fsl_list * const tgt ){ - fsl_db * const db = fsl_needs_repo(f); - if(!db) return FSL_RC_NOT_A_REPO; - int rc = fsl_db_select_slist( db, tgt, - "SELECT substr(tagname,7) AS n " - "FROM tag " - "WHERE tagname GLOB 'event-*' " - "AND length(tagname)=46 " - "ORDER BY n"); - if(rc && db->error.code && !f->error.code){ - fsl_cx_uplift_db_error(f, db); - } - return rc; -} -/* end of file ./src/event.c */ -/* start of file ./src/foci.c */ -/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=2 et sw=2 tw=80: */ -/* - * Copyright 2022 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt - * - * SPDX-License-Identifier: BSD-2-Clause-FreeBSD - * SPDX-FileCopyrightText: 2021 The Libfossil Authors - * SPDX-ArtifactOfProjectName: Libfossil - * SPDX-FileType: Code - * - * Heavily indebted to the Fossil SCM project (https://fossil-scm.org). - */ - -/* - * This file implements the files-of-checkin (foci) API used to construct a - * SQLite3 virtual table via a table-valued function to aggregate all files - * pertaining to a specific check-in. This table is used in repository - * queries such as listing all files belonging to a specific version. - * - * Usage (from fossil(1) /src/foci.c:24): - * - * SELECT * FROM fsl_foci('trunk'); - * - * temp.foci table schema: - * - * CREATE TABLE fsl_foci( - * checkinID INTEGER, -- RID for the check-in manifest - * filename TEXT, -- Name of a file - * uuid TEXT, -- hash of the file - * previousName TEXT, -- Name of the file in previous check-in - * perm TEXT, -- Permissions on the file - * symname TEXT HIDDEN -- Symbolic name of the check-in. - * ); - * - * The hidden symname column is (optionally) used as a query parameter to - * identify the particular check-in to parse. The checkinID parameter - * (such is a unique numeric RID rather than symbolic name) can also be used - * to identify the check-in. Example: - * - * SELECT * FROM fsl_foci - * WHERE checkinID=fsl_sym2rid('trunk'); - * - */ -#include /*memset()*/ -#include - /* Only for debugging */ +#include #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) -enum { -FOCI_CHECKINID = 0, -FOCI_FILENAME = 1, -FOCI_UUID = 2, -FOCI_PREVNAME = 3, -FOCI_PERM = 4, -FOCI_SYMNAME = 5 -}; - -typedef struct FociCursor FociCursor; -struct FociCursor { - sqlite3_vtab_cursor base; /* Base class - must be first */ - fsl_deck d; /* Current manifest */ - const fsl_card_F *cf; /* Current file */ - int idx; /* File index */ -}; - -typedef struct FociTable FociTable; -struct FociTable { - sqlite3_vtab base; /* Base class - must be first */ - fsl_cx * f; /* libfossil context */ -}; - -/* - * The schema for the virtual table: - */ -static const char zFociSchema[] = - " CREATE TABLE fsl_foci(" - " checkinID INTEGER, -- RID for the check-in manifest\n" - " filename TEXT, -- Name of a file\n" - " uuid TEXT, -- hash of the file\n" - " previousName TEXT, -- Name of the file in previous check-in\n" - " perm TEXT, -- Permissions on the file\n" - " symname TEXT HIDDEN -- Symbolic name of the check-in\n" - " );"; - -/* - * Connect to or create a foci virtual table. - */ -static int fociConnect( - sqlite3 *db, - void *pAux /*a (fsl_cx*) */, - int argc __unused, - const char * const * argv __unused, - sqlite3_vtab **ppVtab, - char **pzErr __unused -){ - FociTable *pTab; - int rc = SQLITE_OK; - - pTab = (FociTable *)sqlite3_malloc(sizeof(FociTable)); - if( !pTab ){ - return SQLITE_NOMEM; - } - memset(pTab, 0, sizeof(FociTable)); - rc = sqlite3_declare_vtab(db, zFociSchema); - if( rc==SQLITE_OK ){ - pTab->f = (fsl_cx*)pAux; - *ppVtab = &pTab->base; - } - return rc; -} - -/* - * Disconnect from or destroy a focivfs virtual table. - */ -static int fociDisconnect(sqlite3_vtab *pVtab){ - sqlite3_free(pVtab); - return SQLITE_OK; -} - -/* - * Available scan methods: - * - * (0) A full scan. Visit every manifest in the repo. (Slow) - * (1) checkinID=?. visit only the single manifest specified. - * (2) symName=? visit only the single manifest specified. - */ -static int fociBestIndex(sqlite3_vtab *tab __unused, sqlite3_index_info *pIdxInfo){ - int i; - pIdxInfo->estimatedCost = 1000000000.0; - for( i=0; inConstraint; i++ ){ - if( !pIdxInfo->aConstraint[i].usable ) continue; - if( pIdxInfo->aConstraint[i].op==SQLITE_INDEX_CONSTRAINT_EQ - && (pIdxInfo->aConstraint[i].iColumn==FOCI_CHECKINID - || pIdxInfo->aConstraint[i].iColumn==FOCI_SYMNAME) - ){ - if( pIdxInfo->aConstraint[i].iColumn==FOCI_CHECKINID ){ - pIdxInfo->idxNum = 1; - }else{ - pIdxInfo->idxNum = 2; - } - pIdxInfo->estimatedCost = 1.0; - pIdxInfo->aConstraintUsage[i].argvIndex = 1; - pIdxInfo->aConstraintUsage[i].omit = 1; - break; - } - } - return SQLITE_OK; -} - -/* - * Open a new focivfs cursor. - */ -static int fociOpen(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor){ - FociCursor *pCsr; - pCsr = (FociCursor *)sqlite3_malloc(sizeof(FociCursor)); - if( !pCsr ){ - return SQLITE_NOMEM; - } - memset(pCsr, 0, sizeof(FociCursor)); - pCsr->d = fsl_deck_empty; - pCsr->base.pVtab = pVTab; - *ppCursor = (sqlite3_vtab_cursor *)pCsr; - return SQLITE_OK; -} - -/* - * Close a focivfs cursor. - */ -static int fociClose(sqlite3_vtab_cursor *pCursor){ - FociCursor *pCsr = (FociCursor *)pCursor; - fsl_deck_finalize(&pCsr->d); - sqlite3_free(pCsr); - return SQLITE_OK; -} - -/* - * Move a focivfs cursor to the next F card entry in the deck. If this fails, - * pass the vtab cursor to fociClose and return the failing result code. - */ -static int fociNext(sqlite3_vtab_cursor *pCursor){ - int rc = SQLITE_OK; - - FociCursor *pCsr = (FociCursor *)pCursor; - rc = fsl_deck_F_next(&pCsr->d, &pCsr->cf); - if( !rc ){ - pCsr->idx++; - }else{ - fociClose(pCursor); - } - return rc; -} - -static int fociEof(sqlite3_vtab_cursor *pCursor){ - FociCursor *pCsr = (FociCursor *)pCursor; - return pCsr->cf==0; -} - -static int fociFilter( - sqlite3_vtab_cursor *pCursor, - int idxNum, const char *idxStr __unused, - int argc __unused, sqlite3_value **argv -){ - int rc = SQLITE_OK; - FociCursor *const pCur = (FociCursor *)pCursor; - fsl_cx * const f = ((FociTable*)pCur->base.pVtab)->f; - - fsl_deck_finalize(&pCur->d); - if( idxNum ){ - fsl_id_t rid; - if( idxNum==1 ){ - rid = sqlite3_value_int(argv[0]); - }else{ - rc = fsl_sym_to_rid(f, (const char *)sqlite3_value_text(argv[0]), - FSL_SATYPE_CHECKIN, &rid); - if( rc ){ - goto end; - } - } - rc = fsl_deck_load_rid(f, &pCur->d, rid, FSL_SATYPE_CHECKIN); - if( rc ){ - goto end; - } - if( pCur->d.rid ){ - rc = fsl_deck_F_rewind(&pCur->d); - if( !rc ){ - rc = fsl_deck_F_next(&pCur->d, &pCur->cf); - } - if( rc ){ - goto end; - } - } - } - pCur->idx = 0; -end: - if( rc ){ - fsl_deck_finalize(&pCur->d); - } - return rc; -} - -static int fociColumn( - sqlite3_vtab_cursor *pCursor, - sqlite3_context *ctx, - int i -){ - FociCursor *pCsr = (FociCursor *)pCursor; - switch( i ){ - case FOCI_CHECKINID: - sqlite3_result_int(ctx, pCsr->d.rid); - break; - case FOCI_FILENAME: - sqlite3_result_text(ctx, pCsr->cf->name, -1, SQLITE_TRANSIENT); - break; - case FOCI_UUID: - sqlite3_result_text(ctx, pCsr->cf->uuid, -1, SQLITE_TRANSIENT); - break; - case FOCI_PREVNAME: - sqlite3_result_text(ctx, pCsr->cf->priorName, -1, SQLITE_TRANSIENT); - break; - case FOCI_PERM: { - char *perm[3] = {"l", "w", "x"}; - int i = 1; - switch( pCsr->cf->perm ){ - case FSL_FILE_PERM_LINK: - i = 0; break; - case FSL_FILE_PERM_EXE: - i = 2; break; - default: - break; - } - sqlite3_result_text(ctx, perm[i], 1, SQLITE_TRANSIENT); - break; - } - case FOCI_SYMNAME: - break; - } - return SQLITE_OK; -} - -static int fociRowid(sqlite3_vtab_cursor *pCursor, sqlite_int64 *pRowid){ - FociCursor *pCsr = (FociCursor *)pCursor; - *pRowid = pCsr->idx; - return SQLITE_OK; -} - -int fsl__foci_register(fsl_db * const db){ - static sqlite3_module foci_module = { - 0, /* iVersion */ - fociConnect, /* xCreate */ - fociConnect, /* xConnect */ - fociBestIndex, /* xBestIndex */ - fociDisconnect, /* xDisconnect */ - fociDisconnect, /* xDestroy */ - fociOpen, /* xOpen - open a cursor */ - fociClose, /* xClose - close a cursor */ - fociFilter, /* xFilter - configure scan constraints */ - fociNext, /* xNext - advance a cursor */ - fociEof, /* xEof - check for end of scan */ - fociColumn, /* xColumn - read data */ - fociRowid, /* xRowid - read data */ - 0, /* xUpdate */ - 0, /* xBegin */ - 0, /* xSync */ - 0, /* xCommit */ - 0, /* xRollback */ - 0, /* xFindMethod */ - 0, /* xRename */ - 0, /* xSavepoint */ - 0, /* xRelease */ - 0, /* xRollbackTo */ - 0 /* xShadowName */ - }; - assert(db->f); - int rc = sqlite3_create_module(db->dbh, "fsl_foci", - &foci_module, db->f); - return fsl__db_errcode(db, rc); -} + +int fsl_event_ids_get( fsl_cx * f, fsl_list * tgt ){ + fsl_db * db = fsl_needs_repo(f); + if(!f || !tgt) return FSL_RC_MISUSE; + else if(!db) return FSL_RC_NOT_A_REPO; + else { + int rc = fsl_db_select_slist( db, tgt, + "SELECT substr(tagname,7) AS n " + "FROM tag " + "WHERE tagname GLOB 'event-*' " + "ORDER BY n"); + if(rc && db->error.code && !f->error.code){ + fsl_cx_uplift_db_error(f, db); + } + return rc; + } +} + #undef MARKER -/* end of file ./src/foci.c */ +/* end of file ./src/event.c */ /* start of file ./src/fs.c */ /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* Copyright 2013-2021 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt @@ -29189,11 +27400,20 @@ #include #include /* strlen() */ #include /* NULL on linux */ #include #include +#include #if FSL_PLATFORM_IS_WINDOWS +# define DIR _WDIR +# define dirent _wdirent +# define opendir _wopendir +# define readdir _wreaddir +# define closedir _wclosedir +# include +# include +# include # if !defined(ELOOP) # define ELOOP 114 /* Missing in MinGW */ # endif #else # include /* access(2), readlink(2) */ @@ -29200,18 +27420,17 @@ # include # include #endif #include -const fsl_path_splitter fsl_path_splitter_empty = fsl_path_splitter_empty_m; - /* Only for debugging */ #include #define MARKER(pfexp) \ do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ printf pfexp; \ } while(0) + FILE *fsl_fopen(const char *zName, const char *zMode){ FILE *f; if(zName && ('-'==*zName && !zName[1])){ f = (strchr(zMode, 'w') || strchr(zMode,'+')) @@ -29683,27 +27902,17 @@ : fsl_buffer_append(pOut, "", 0) /* ensure a NUL terminator */; }else{ return fsl_buffer_append(pOut, zFilename, z-zFilename + 1); } } + } -const char *fsl_file_tail(const char *z){ - const char *zTail = z; - if( !zTail ) return 0; - while( z[0] ){ - if( '/'==z[0] || '\\'==z[0] ) zTail = &z[1]; - z++; - } - return zTail; -} - - -int fsl_find_home_dir( fsl_buffer * const tgt, bool requireWriteAccess ){ +int fsl_find_home_dir( fsl_buffer * tgt, bool requireWriteAccess ){ char * zHome = NULL; int rc = 0; - fsl_buffer_reuse(tgt); + tgt->used = 0; #if defined(_WIN32) || defined(__CYGWIN__) zHome = fsl_getenv("LOCALAPPDATA"); if( zHome==0 ){ zHome = fsl_getenv("APPDATA"); if( zHome==0 ){ @@ -30109,10 +28318,13 @@ ? -1 : fst.size; } #endif +/* + Set the mtime for a file. +*/ int fsl_file_mtime_set(const char *zFilename, fsl_time_t newMTime){ if(!zFilename || !*zFilename) return FSL_RC_MISUSE; else{ int rc; void * zMbcs; @@ -30148,13 +28360,12 @@ fsl_buffer_clear(&pf->buf); *pf = fsl_pathfinder_empty; } } -static int fsl__pathfinder_add_impl(fsl_list * const li, char const * str, - fsl_int_t strLen){ - char * cp = fsl_strndup(str, strLen); +static int fsl_pathfinder_add(fsl_list * const li, char const * str){ + char * cp = fsl_strdup(str); int rc; if(!cp) rc = FSL_RC_OOM; else{ rc = fsl_list_append(li, cp); if(rc) fsl_free(cp); @@ -30161,58 +28372,44 @@ } return rc; } int fsl_pathfinder_dir_add(fsl_pathfinder * const pf, char const * const dir){ - return dir - ? fsl__pathfinder_add_impl(&pf->dirs, dir, -1) - : FSL_RC_MISUSE; -} - -int fsl_pathfinder_dir_add2(fsl_pathfinder * const pf, char const * const dir, - fsl_int_t strLen){ - return dir - ? fsl__pathfinder_add_impl(&pf->dirs, dir, strLen) + return (pf && dir) + ? fsl_pathfinder_add(&pf->dirs, dir) : FSL_RC_MISUSE; } int fsl_pathfinder_ext_add(fsl_pathfinder * const pf, char const * const ext){ return (pf && ext) - ? fsl__pathfinder_add_impl(&pf->ext, ext, -1) - : FSL_RC_MISUSE; -} - -int fsl_pathfinder_ext_add2(fsl_pathfinder * const pf, char const * const ext, - fsl_int_t strLen){ - return (pf && ext) - ? fsl__pathfinder_add_impl(&pf->ext, ext, strLen) + ? fsl_pathfinder_add(&pf->ext, ext) : FSL_RC_MISUSE; } int fsl_pathfinder_search(fsl_pathfinder * const pf, char const * const base, char const ** pOut, fsl_size_t * const outLen ){ - fsl_buffer * const buf = &pf->buf; + fsl_buffer * buf = pf ? &pf->buf : NULL; fsl_list * ext; fsl_list * dirs; int rc = 0; fsl_size_t d, x, nD, nX, resetLen = 0; fsl_size_t baseLen; static char const pathSep = #if defined(_WIN32) - '\\' /* TODO: confirm whether we can always use '/', and do so if - we can. */ + '\\' #else '/' #endif ; - if(!base || !*base) return FSL_RC_MISUSE; + if(!buf || !base || !*base) return FSL_RC_MISUSE; else if(!*base) return FSL_RC_RANGE; else if(0==fsl_file_access( base, 0 )){ /* Special case: if base is found as-is, without a path search, - use it. */ + use it. This is arguable behaviour, though. + */ if(pOut) *pOut = base; if(outLen) *outLen = fsl_strlen(base); return 0; } baseLen = fsl_strlen(base); @@ -30252,79 +28449,19 @@ if(0==fsl_file_access( (char const *)buf->mem, 0 )){ goto gotone; } } } + return FSL_RC_NOT_FOUND; + gotone: if(outLen) *outLen = buf->used; if(pOut) *pOut = (char const *)buf->mem; return 0; } -void fsl_path_splitter_init( fsl_path_splitter * pt, char const * path, fsl_int_t len ){ - *pt = fsl_path_splitter_empty; - pt->pos = pt->begin = path; - pt->end = pt->begin + ((len>=0) ? (fsl_size_t)len : fsl_strlen(path)); -} - -int fsl_path_splitter_next( fsl_path_splitter * const pt, char const ** token, - fsl_size_t * const len ){ - if(!pt->pos || pt->pos>=pt->end) return FSL_RC_RANGE; - else if(!pt->separators || !*pt->separators) return FSL_RC_MISUSE; - else{ - char const * pos = pt->pos; - char const * t; - char const * sep; - for( sep = pt->separators; *sep; ++sep){ - if(*sep & 0x80) return FSL_RC_MISUSE; - /* non-ASCII */ - } - for( ; posend; ){ - /*skip leading separators*/ - for( sep = pt->separators; - *sep && *pos!=*sep; ++sep ){ - } - if(*pos == *sep) ++pos; - else break; - } - t = pos; - for( ; posend; ){ - /*skip until the next separator*/ - for( sep = pt->separators; - *sep && *pos!=*sep; ++sep ){ - } - if(*pos == *sep) break; - else ++pos; - } - pt->pos = pos; - if(pos>t){ - *token = t; - *len = (fsl_size_t)(pos - t); - return 0; - } - return FSL_RC_NOT_FOUND; - } -} - -int fsl_pathfinder_split( fsl_pathfinder * const tgt, - bool isDirs, - char const * path, - fsl_int_t pathLen ){ - int rc = 0; - char const * t = 0; - fsl_size_t tLen = 0; - fsl_path_splitter pt = fsl_path_splitter_empty; - fsl_path_splitter_init(&pt, path, pathLen); - while(0==rc && 0==fsl_path_splitter_next(&pt, &t, &tLen)){ - rc = isDirs - ? fsl_pathfinder_dir_add2(tgt, t, (fsl_int_t)tLen) - : fsl_pathfinder_ext_add2(tgt, t, (fsl_int_t)tLen); - } - return rc; -} - char * fsl__file_without_drive_letter(char * zIn){ #ifdef _WIN32 if( zIn && fsl_isalpha(zIn[0]) && zIn[1]==':' ) zIn += 2; #endif return zIn; @@ -30483,11 +28620,11 @@ } } return rc; } -void fsl_buffer_strip_slashes(fsl_buffer * const b){ +void fsl_buffer_strip_slashes(fsl_buffer * b){ b->used -= fsl_strip_trailing_slashes((char *)b->mem, (fsl_int_t)b->used); } int fsl_file_rename(const char *zFrom, const char *zTo){ @@ -30708,32 +28845,20 @@ } } return rc; } -int fsl_symlink_copy(char const *zFrom, char const *zTo, bool realLink){ +int fsl__symlink_copy(char const *zFrom, char const *zTo, bool realLink){ int rc; fsl_buffer b = fsl_buffer_empty; rc = fsl_symlink_read(&b, zFrom); if(0==rc){ rc = fsl_symlink_create(fsl_buffer_cstr(&b), zTo, realLink); } fsl_buffer_clear(&b); return rc; } - -char const * fsl_last_path_sep(char const * str, fsl_int_t slen ){ - if(slen<0) slen = (fsl_int_t)fsl_strlen(str); - unsigned char const * pos = (unsigned char const *)str + slen; - while( --pos >= (unsigned char const *)str ){ - if('/'==*pos || '\\'==*pos){ - return (char const *)pos; - } - } - return NULL; -} - #if 0 int fsl_file_relative_name( char const * zRoot, char const * zPath, fsl_buffer * pOut, char retainSlash ){ int rc = FSL_RC_NYI; @@ -30788,10 +28913,11 @@ fsl_db * db = fsl_needs_repo(f); if(!db) return FSL_RC_NOT_A_REPO; if(fsl_db_table_exists(db, FSL_DBROLE_REPO, "forumpost")){ return 0; } + MARKER(("table not exists?\n")); rc = fsl_db_exec_multi(db, "%s",fsl_schema_forum()); if(rc){ rc = fsl_cx_uplift_db_error(f, db); } return rc; @@ -31052,11 +29178,11 @@ return *n ? 0 : (feof(f) ? 0 : FSL_RC_IO); } -void fsl_finalizer_f_FILE( void * state __unused, void * mem ){ +void fsl_finalizer_f_FILE( void * state, void * mem ){ if(mem){ fsl_fclose((FILE*)mem); } } @@ -31536,11 +29662,11 @@ self->list[self->used++] = cp; if(self->usedcapacity) self->list[self->used]=NULL; return 0; } -int fsl_list_v_fsl_free(void * obj, void * visitorState __unused){ +int fsl_list_v_fsl_free(void * obj, void * visitorState ){ if(obj) fsl_free( obj ); return 0; } int fsl_list_clear( fsl_list * const self, fsl_list_visitor_f childFinalizer, @@ -32867,13 +30993,12 @@ #define fsl_merge_state_empty_m { \ NULL/*f*/, \ NULL/*opt*/, \ NULL/*filename*/, \ - NULL/*priorName*/, \ - FSL_MERGE_FCHANGE_NONE/*fileChangeType*/,\ - FSL_CKUP_RM_NOT/* fileRmInfo */ \ + NULL/*prevName*/, \ + FSL_MERGE_FCHANGE_NONE/*fileChangeType*/ \ } /** Initialized-with-defaults fsl_merge_state instance, intended for use in non-const copy initialization. */ @@ -33362,20 +31487,20 @@ if(rc) goto merge_rename_end; rc = fsl_cx_exec(f, "INSERT INTO tmprn(fn,tmpfn) VALUES(%Q,%B)", zNewName, bTmp); if(rc) goto merge_rename_end; rc = fsl_is_symlink(zFullNewPath) - ? fsl_symlink_copy(zFullNewPath, fsl_buffer_cstr(bTmp), realSymlinks) + ? fsl__symlink_copy(zFullNewPath, fsl_buffer_cstr(bTmp), realSymlinks) : fsl_file_copy(zFullNewPath, fsl_buffer_cstr(bTmp)); if(rc){ rc = fsl_cx_err_set(f, rc, "Error copying file [%s].", zFullNewPath); } if(rc) goto merge_rename_end; } rc = fsl_is_symlink(zFullOldPath) - ? fsl_symlink_copy(zFullOldPath, zFullNewPath, realSymlinks) + ? fsl__symlink_copy(zFullOldPath, zFullNewPath, realSymlinks) : fsl_file_copy(zFullOldPath, zFullNewPath); if(0==rc){ fsl_file_exec_set(zFullNewPath, !!isExe); fsl_file_unlink(zFullOldPath); /* ^^^ Ignore errors: not critical here */ @@ -34458,11 +32583,11 @@ as Q1 - which searching the most recent EVENT table entries for the most recent with the tag. But if the tag is relatively scarce (anything other than "trunk", basically) then we want to do the indexed search show below as Q2. */ -static fsl_id_t fsl__morewt(fsl_cx * const f, const char *zTag, fsl_satype_e type){ +static fsl_id_t fsl_morewt(fsl_cx * const f, const char *zTag, fsl_satype_e type){ char const * zType = fsl_satype_event_cstr(type); return fsl_db_g_id(fsl_cx_db_repo(f), 0, "SELECT objid FROM (" /* Q1: Begin by looking for the tag in the 30 most recent events */ "SELECT objid" @@ -34490,13 +32615,10 @@ ); } /** Modes for fsl_start_of_branch(). - - These values are hard-coded and must retain these values, - else queries will break. */ enum fsl_stobr_type { /** The check-in of the parent branch off of which the branch containing RID originally diverged. @@ -34516,47 +32638,47 @@ /* ** Return the RID that is the "root" of the branch that contains ** check-in "rid". Details depending on eType. If not found, rid is ** returned. */ -static fsl_id_t fsl_start_of_branch(fsl_cx * const f, fsl_id_t rid, +static fsl_id_t fsl_start_of_branch(fsl_cx * f, fsl_id_t rid, enum fsl_stobr_type eType){ + fsl_db * db; fsl_stmt q = fsl_stmt_empty; int rc; fsl_id_t ans = rid; char * zBr = 0; rc = fsl_branch_of_rid(f, rid, true, &zBr); - if(rc) return -1; - rc = fsl_cx_prepare(f, &q, - "WITH RECURSIVE" - " par(pid, ex, cnt) as (" - " SELECT pid, EXISTS(SELECT 1 FROM tagxref" - " WHERE tagid=%d AND tagtype>0" - " AND value=%Q AND rid=plink.pid), 1" - " FROM plink WHERE cid=%"FSL_ID_T_PFMT" AND isprim" - " UNION ALL " - " SELECT plink.pid, EXISTS(SELECT 1 FROM tagxref " - " WHERE tagid=%d AND tagtype>0" - " AND value=%Q AND rid=plink.pid)," - " 1+par.cnt" - " FROM plink, par" - " WHERE cid=par.pid AND isprim AND par.ex " - " LIMIT 100000 " - " )" - " SELECT pid FROM par WHERE ex>=%d ORDER BY cnt DESC LIMIT 1", - FSL_TAGID_BRANCH, zBr, ans, FSL_TAGID_BRANCH, zBr, eType%2 + if(rc) return rc; + db = fsl_cx_db_repo(f); + assert(db); + rc = fsl_db_prepare(db, &q, + "SELECT pid, EXISTS(SELECT 1 FROM tagxref" + " WHERE tagid=%d AND tagtype>0" + " AND value=%Q AND rid=plink.pid)" + " FROM plink" + " WHERE cid=? AND isprim", + FSL_TAGID_BRANCH, zBr ); fsl_free(zBr); zBr = 0; if(rc){ ans = -2; + fsl_cx_uplift_db_error(f, db); MARKER(("Internal error: fsl_db_prepare() says: %s\n", fsl_rc_cstr(rc))); goto end; } - if( FSL_RC_STEP_ROW == fsl_stmt_step(&q) ) { + do{ + fsl_stmt_reset(&q); + fsl_stmt_bind_id(&q, 1, ans); + rc = fsl_stmt_step(&q); + if( rc!=FSL_RC_STEP_ROW ) break; + if( eType==FSL_STOBR_FIRST_CI && fsl_stmt_g_int32(&q,1)==0 ){ + break; + } ans = fsl_stmt_g_id(&q, 0); - } + }while( fsl_stmt_g_int32(&q, 1)==1 && ans>0 ); fsl_stmt_finalize(&q); end: if( ans>0 && eType==FSL_STOBR_YOAN ){ rc = fsl_branch_of_rid(f, ans, true, &zBr); if(rc) goto oom; @@ -34573,21 +32695,21 @@ side-effect of another failure. */ return -1; } int fsl_sym_to_rid( fsl_cx * const f, char const * sym, - fsl_satype_e type, fsl_id_t * const rv ){ + fsl_satype_e type, fsl_id_t * rv ){ fsl_id_t rid = 0; fsl_id_t vid; fsl_size_t symLen; /* fsl_int_t i; */ - fsl_db * const dbR = fsl_cx_db_repo(f); - fsl_db * const dbC = fsl_cx_db_ckout(f); + fsl_db * dbR = fsl_cx_db_repo(f); + fsl_db * dbC = fsl_cx_db_ckout(f); bool startOfBranch = 0; int rc = 0; - if(!sym || !*sym || !rv) return FSL_RC_MISUSE; + if(!f || !sym || !*sym || !rv) return FSL_RC_MISUSE; else if(!dbR) return FSL_RC_NOT_A_REPO; if(FSL_SATYPE_BRANCH_START==type){ /* The original implementation takes a (char const *) for the type, and treats "b" (branch?) as a special case of @@ -34659,11 +32781,11 @@ /* Deprecated time formats elided: local:..., utc:... */ /* "tag:" + symbolic-name */ if( memcmp(sym, "tag:", 4)==0 ){ - rid = fsl__morewt(f, sym+4, type); + rid = fsl_morewt(f, sym+4, type); if(rid>0 && startOfBranch){ rid = fsl_start_of_branch(f, rid, FSL_STOBR_FIRST_CI); } goto gotit; } @@ -34675,19 +32797,10 @@ rid = fsl_start_of_branch(f, rid, FSL_STOBR_ORIGIN); } goto gotit; } - /* start:TAG -> The first check-in on branch named TAG */ - if( strncmp(sym, "start:", 6)==0 ){ - rc = fsl_sym_to_rid(f, sym+6, type, &rid); - if(!rc && rid>0){ - rid = fsl_start_of_branch(f, rid, FSL_STOBR_FIRST_CI); - } - goto gotit; - } - /* merge-in:TAG -> Most recent merge-in for the branch */ if( memcmp(sym, "merge-in:", 9)==0 ){ rc = fsl_sym_to_rid(f, sym+9, type, &rid); if(!rc){ rid = fsl_start_of_branch(f, rid, FSL_STOBR_YOAN); @@ -34704,11 +32817,13 @@ char zUuid[FSL_STRLEN_K256+1]; memcpy(zUuid, sym, symLen); zUuid[symLen] = 0; fsl_canonical16(zUuid, symLen); rid = 0; - /* Reminder to self: caching these queries would be cool. */ + /* Reminder to self: caching these queries would be cool but it + can't work with the GLOBs. + */ if( FSL_SATYPE_ANY==type ){ fsl_db_prepare(dbR, &q, "SELECT rid FROM blob WHERE uuid GLOB '%s*'", zUuid); }else{ @@ -34748,21 +32863,21 @@ " AND event.objid=tagxref.rid " " AND event.type GLOB '%q'", sym, fsl_satype_event_cstr(type) ); }else{ - rid = fsl__morewt(f, sym, type); + rid = fsl_morewt(f, sym, type); //MARKER(("morewt(%s,%s) == %d\n", sym, fsl_satype_cstr(type), (int)rid)); } if( rid>0 ){ if(startOfBranch) rid = fsl_start_of_branch(f, rid, FSL_STOBR_FIRST_CI); goto gotit; } - /* Undocumented: rid:### ==> validate that ### is a known rid */ + /* Undocumented: rid:### ==> rid */ if(symLen>4 && 0==fsl_strncmp("rid:",sym,4)){ int i; char const * oldSym = sym; sym += 4; for(i=0; fsl_isdigit(sym[i]); i++){} @@ -34820,12 +32935,12 @@ } return rv; } } -int fsl_sym_to_uuid( fsl_cx * const f, char const * sym, fsl_satype_e type, - fsl_uuid_str * const rv, fsl_id_t * const rvId ){ +int fsl_sym_to_uuid( fsl_cx * f, char const * sym, fsl_satype_e type, + fsl_uuid_str * rv, fsl_id_t * rvId ){ fsl_id_t rid = 0; fsl_db * dbR = fsl_needs_repo(f); fsl_uuid_str rvv = NULL; int rc = dbR ? fsl_sym_to_rid(f, sym, type, &rid) @@ -35024,11 +33139,10 @@ *rv = fsl_stmt_g_id(q, 0); break; case 0: rc = 0; *rv = 0; - __attribute__ ((fallthrough)); default: fsl_cx_uplift_db_error(f, q->db); break; } fsl_stmt_reset(q); @@ -35313,11 +33427,11 @@ opt->filename); goto end2; } } rc = fsl__cx_attach_role(f, opt->filename, FSL_DBROLE_REPO, true); - //MARKER(("attach role rc=%s\n", fsl_rc_cstr(rc))); + MARKER(("attach role rc=%s\n", fsl_rc_cstr(rc))); if(rc){ goto end2; } db = fsl_cx_db(f); if(!f->repo.user){ @@ -36008,21 +34122,22 @@ * hardware is configured. For example, it gave different answers on * native Linux versus running under valgrind. */ if(0==version){ fsl_stmt_finalize(&q); - fsl_buffer_append(sql, - "SELECT rcvid, quote(uid), quote(mtime), " - "quote(nonce), quote(ipaddr) " - "FROM rcvfrom ", -1); + rc = fsl_buffer_append(sql, + "SELECT rcvid, quote(uid), quote(mtime), " + "quote(nonce), quote(ipaddr) " + "FROM rcvfrom ", -1); }else{ assert(1==version); - fsl_buffer_append(sql, - "SELECT rcvid, quote(uid), datetime(mtime), " - "quote(nonce), quote(ipaddr) " - "FROM rcvfrom ", -1); + rc = fsl_buffer_append(sql, + "SELECT rcvid, quote(uid), datetime(mtime), " + "quote(nonce), quote(ipaddr) " + "FROM rcvfrom ", -1); } + if(rc) goto end; rc = (rcvid>0) ? fsl_buffer_appendf(sql, "WHERE rcvid=%" FSL_ID_T_PFMT, rcvid) : fsl_buffer_append(sql, "ORDER BY rcvid DESC LIMIT 1", -1); if(rc) goto end; rc = fsl_db_prepare(db, &q, "%b", sql); @@ -36102,12 +34217,13 @@ ridHash = (char *)bHash->mem; }else{ ridHash = f->ckout.uuid; } assert(ridHash); - fsl_buffer_append(pHash, ridHash, -1); - rc = fsl_buffer_append(pHash, "\n", 1); + rc = fsl_buffer_append(pHash, ridHash, -1); + if(!rc) rc = fsl_buffer_append(pHash, "\n", 1); + if(rc) goto end; } if(pTags){ fsl_stmt q = fsl_stmt_empty; fsl_db * const db = fsl_cx_db_repo(f); assert(db && "We can't have a checkout w/o a repo."); @@ -36125,18 +34241,22 @@ " AND tagxref.tagtype>0" " AND tag.tagid=tagxref.tagid" " AND tag.tagname GLOB 'sym-*'" " /*%s()*/", f->ckout.rid, __func__); - while( 0==rc && FSL_RC_STEP_ROW==fsl_stmt_step(&q) ){ + if(rc) goto end; + while( FSL_RC_STEP_ROW==fsl_stmt_step(&q) ){ const char *zName = fsl_stmt_g_text(&q, 0, NULL); rc = fsl_buffer_appendf(pTags, "tag %s\n", zName); + if(rc) break; } fsl_stmt_finalize(&q); } end: - if(bHash) fsl__cx_scratchpad_yield(f, bHash); + if(bHash){ + fsl__cx_scratchpad_yield(f, bHash); + } return rc; } /** Internal state for the rebuild process. @@ -36519,18 +34639,11 @@ if(!rc) rc = fsl_buffer_reserve(sql, 1024 * 4); if(rc) goto end; fsl__cx_clear_mf_seen(f, false); /* DROP all tables which are not part of our One True Vision of the - repo db... - - 2022-07-31: we might want to stop doing this because: if fossil - adds new tables, there may be a lag in getting them into - libfossil and we don't necessarily want to nuke those. OTOH, all - such tables would be transient/rebuildable state, so if we nuke - them then a rebuild from fossil(1) would correct it. - */ + repo db... */ rc = fsl_cx_prepare(f, &q, "SELECT name FROM %!Q.sqlite_schema /*scan*/" " WHERE type='table'" " AND name NOT IN ('admin_log', 'blob','delta','rcvfrom','user','alias'," "'config','shun','private','reportfmt'," @@ -41327,23 +39440,23 @@ SQL function to return the number of seconds since 1970. This is the same as strftime('%s','now') but is more compact. */ static void fsl_db_now_udf( sqlite3_context *context, - int argc __unused, - sqlite3_value **argv __unused + int argc, + sqlite3_value **argv ){ sqlite3_result_int64(context, (sqlite3_int64)time(0)); } /** SQL function to convert a Julian Day to a Unix timestamp. */ static void fsl_db_j2u_udf( sqlite3_context *context, - int argc __unused, - sqlite3_value **argv __unused + int argc, + sqlite3_value **argv ){ double const jd = (double)sqlite3_value_double(argv[0]); sqlite3_result_int64(context, (sqlite3_int64)fsl_julian_to_unix(jd)); } @@ -41386,11 +39499,10 @@ fsl_cx * f = (fsl_cx*)sqlite3_user_data(context); fsl_time_t mtime = 0; int rc; fsl_id_t vid, fid; assert(f); - assert(2<=argc); vid = (fsl_id_t)sqlite3_value_int(argv[0]); fid = (fsl_id_t)sqlite3_value_int(argv[1]); rc = fsl_mtime_of_manifest_file(f, vid, fid, &mtime); if( rc==0 ){ sqlite3_result_int64(context, mtime); @@ -41552,12 +39664,12 @@ Implement the user() SQL function. user() takes no arguments and returns the user ID of the current user. */ static void fsl_db_user_udf( sqlite3_context *context, - int argc __unused, - sqlite3_value **argv __unused + int argc, + sqlite3_value **argv ){ fsl_cx * f = (fsl_cx*)sqlite3_user_data(context); assert(f); if(f->repo.user){ sqlite3_result_text(context, f->repo.user, -1, SQLITE_STATIC); @@ -41649,11 +39761,11 @@ if(!p1 || !p2){ sqlite3_result_null(context); return; } int (*cmp)(char const *, char const *) = - fsl_cx_is_case_sensitive(f, false) ? fsl_stricmp : fsl_strcmp; + f->cache.caseInsensitive ? fsl_stricmp : fsl_strcmp; if(0==cmp(p1, p2)){ sqlite3_result_int(context, 1); return; } b = fsl__cx_scratchpad(f); @@ -41694,11 +39806,10 @@ fsl_cx * const f = (fsl_cx*)sqlite3_user_data(context); fsl_list * li = NULL; fsl_glob_category_e globType; char const * p1; char const * p2; - assert(2<=argc); p2 = (const char*)sqlite3_value_text(argv[1])/*value to check*/; if(NULL==p2 || 0==p2[0]){ sqlite3_result_int(context, 0); return; } @@ -41796,11 +39907,10 @@ for a given statement execution IF no SQL triggers an effect which forces the globs to reload. That "shouldn't ever happen." */ f, fsl_db_cx_glob_udf, 0, 0 ); - rc = fsl__foci_register(db); #if 0 /* functions registered in v1 by db.c:db_open(). */ /* porting cgi() requires access to the HTTP/CGI layer. i.e. this belongs downstream. */ sqlite3_create_function(dbh, "cgi", 1, SQLITE_ANY, 0, db_sql_cgi, 0, 0); @@ -41939,11 +40049,10 @@ #if defined(_WIN32) fsl_free(pOld); #elif (defined(__APPLE__) && !defined(WITHOUT_ICONV)) || defined(__CYGWIN__) fsl_free(pOld); #else - (void)pOld; /* No-op on all other unix */ #endif } char *fsl_filename_to_utf8(const void *zFilename){ @@ -42016,12 +40125,12 @@ void *fossil_utf8_to_path(const char *zUtf8, int isDir) That isDir param is only for Windows and its only purpose is to ensure that the translated path is not within 12 bytes of MAX_PATH. That same effect can be had by simply always assuming - that bool is true and sacrificing those 12 bytes and that - far-edge (literally) case. + that bool is true and sacrificing those 12 bytes and that far-edge + case. Also, the newer code jumps through many hoops which seem unimportant for fossil, e.g. handling UNC-style paths. Porting that latter bit over requires someone who can at least @@ -42174,11 +40283,11 @@ if(!dbC) return FSL_RC_NOT_A_CKOUT; else if(!dbR) return FSL_RC_NOT_A_REPO; if(vid<=0) vid = f->ckout.rid; assert(vid>=0); - rc = fsl_cx_transaction_begin(f); + rc = fsl_db_transaction_begin(dbC); if(rc) return rc; alreadyHad = fsl_db_exists(dbC, "SELECT 1 FROM vfile " "WHERE vid=%" FSL_ID_T_PFMT, vid); @@ -42264,12 +40373,12 @@ if(!alreadyHad){ assert(d.rid>0); } } fsl_deck_finalize(&d); - if(rc) fsl_cx_transaction_end(f, true); - else rc = fsl_cx_transaction_end(f, false); + if(rc) fsl_db_transaction_rollback(dbC); + else rc = fsl_db_transaction_commit(dbC); if(rc && !f->error.code){ if(dbC->error.code) fsl_cx_uplift_db_error(f, dbC); else if(dbR->error.code) fsl_cx_uplift_db_error(f, dbR); } return rc; @@ -42566,11 +40675,11 @@ fsl_stmt_finalize(&q); return rc; } int fsl__vfile_to_ckout(fsl_cx * const f, fsl_id_t vfileId, - int * wasWritten){ + int * wasWritten){ int rc = 0; fsl_db * const db = fsl_needs_ckout(f); fsl_stmt q = fsl_stmt_empty; int counter = 0; fsl_buffer content = fsl_buffer_empty; @@ -42705,23 +40814,10 @@ fsl_buffer_clear(&content); fsl_stmt_finalize(&q); return rc; } -int fsl_vfile_pathname(fsl_cx * const f, fsl_id_t vfid, bool absolute, char **zOut){ - assert(f->ckout.dir); - fsl_db * const db = fsl_cx_db_ckout(f); - assert(db); - int const rc = fsl_db_get_text(db, zOut, NULL, - "SELECT %Q || pathname FROM vfile " - "WHERE id=%"FSL_ID_T_PFMT, - absolute ? f->ckout.dir : "", - vfid); - if(rc) fsl_cx_uplift_db_error(f, db); - return rc; -} - #undef MARKER /* end of file ./src/vfile.c */ /* start of file ./src/vpath.c */ /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ @@ -43391,29 +41487,35 @@ */ #include #include #include /* atoi() and friends */ #include /* memset() */ + +#define MARKER(pfexp) \ + do{ printf("MARKER: %s:%d:%s():\t",__FILE__,__LINE__,__func__); \ + printf pfexp; \ + } while(0) + /* Write a 16- or 32-bit integer as little-endian into the given buffer. */ -static void fzip__put16(char *z, int v){ +static void fzip_put16(char *z, int v){ z[0] = v & 0xff; z[1] = (v>>8) & 0xff; } -static void fzip__put32(char *z, int v){ +static void fzip_put32(char *z, int v){ z[0] = v & 0xff; z[1] = (v>>8) & 0xff; z[2] = (v>>16) & 0xff; z[3] = (v>>24) & 0xff; } /** Set the date and time values from an ISO8601 date string. */ -static void fzip__timestamp_from_str(fsl_zip_writer *z, const char *zDate){ +static void fzip_timestamp_from_str(fsl_zip_writer *z, const char *zDate){ int y, m, d; int H, M, S; y = atoi(zDate); m = atoi(&zDate[5]); @@ -43424,62 +41526,63 @@ z->dosTime = (H<<11) + (M<<5) + (S>>1); z->dosDate = ((y-1980)<<9) + (m<<5) + d; } fsl_buffer const * fsl_zip_body( fsl_zip_writer const * const z ){ - return &z->body; + return z ? &z->body : NULL; } void fsl_zip_timestamp_set_julian(fsl_zip_writer * const z, double rDate){ char buf[20] = {0}; fsl_julian_to_iso8601(rDate, buf, 0); - fzip__timestamp_from_str(z, buf); + fzip_timestamp_from_str(z, buf); z->unixTime = (fsl_time_t)((rDate - 2440587.5)*86400.0); } void fsl_zip_timestamp_set_unix(fsl_zip_writer * const z, fsl_time_t epochTime){ char buf[20] = {0}; fsl_julian_to_iso8601(fsl_unix_to_julian(epochTime), buf, 0); - fzip__timestamp_from_str(z, buf); + fzip_timestamp_from_str(z, buf); z->unixTime = epochTime; } + /** Adds all directories for the given file to the zip if they are not in there already. Returns 0 on success, non-0 on error (namely OOM). */ -static int fzip__mkdir(fsl_zip_writer * const z, char const *zName); +static int fzip_mkdir(fsl_zip_writer * const z, char const *zName); /** Adds a file entry to zw's zip output. zName is the virtual name of the file or directory. If pSrc is NULL then it is assumed that we are creating a directory, otherwise the zip's entry is populated from pSrc. mPerms specify the fossil-specific permission flags - from the fsl_fileperm_e enum. If doMkDirs is true then fzip__mkdir() + from the fsl_fileperm_e enum. If doMkDirs is true then fzip_mkdir() is called to create the directory entries for zName, otherwise they are not. */ -static int fzip__file_add(fsl_zip_writer * const zw, char const * zName, - fsl_buffer const * pSrc, int mPerm, - char doMkDirs){ +static int fzip_file_add(fsl_zip_writer * const zw, char const * zName, + fsl_buffer const * pSrc, int mPerm, + char doMkDirs){ int rc = 0; z_stream stream; fsl_size_t nameLen; int toOut = 0; int iStart; - unsigned int iCRC = 0; + int iCRC = 0; int nByte = 0; int nByteCompr = 0; int nBlob; /* Size of the blob */ int iMethod; /* Compression method. */ int iMode = 0644; /* Access permissions */ char *z; char zHdr[30]; char zExTime[13]; char zBuf[100]; - char zOutBuf[/*historical: 100000*/ 1024 * 32]; + char zOutBuf[/*historical: 100000*/ 1024 * 16]; /* Fill in as much of the header as we know. */ nBlob = pSrc ? (int)pSrc->used : 0; if( pSrc ){ /* a file entry */ @@ -43492,51 +41595,55 @@ }else{ /* a directory entry */ iMethod = 0; iMode = 040755; } if(doMkDirs){ - rc = fzip__mkdir(zw, zName) - /* This causes an extraneous run of fzip__mkdir(), + rc = fzip_mkdir(zw, zName) + /* This causes an extraneous run of fzip_mkdir(), but it is harmless other than the waste of search time */; if(rc) return rc; } if(zw->rootDir){ - fsl_buffer_reuse(&zw->scratch); + zw->scratch.used = 0; rc = fsl_buffer_appendf(&zw->scratch, "%s%s", zw->rootDir, zName); - if(rc) return rc; + if(rc){ + assert(FSL_RC_OOM==rc); + return rc; + } zName = fsl_buffer_cstr(&zw->scratch); } nameLen = fsl_strlen(zName); memset(zHdr, 0, sizeof(zHdr)); - fzip__put32(&zHdr[0], 0x04034b50); - fzip__put16(&zHdr[4], 0x000a); - fzip__put16(&zHdr[6], 0x0800); - fzip__put16(&zHdr[8], iMethod); - fzip__put16(&zHdr[10], zw->dosTime); - fzip__put16(&zHdr[12], zw->dosDate); - fzip__put16(&zHdr[26], nameLen); - fzip__put16(&zHdr[28], 13); - - fzip__put16(&zExTime[0], 0x5455); - fzip__put16(&zExTime[2], 9); + fzip_put32(&zHdr[0], 0x04034b50); + fzip_put16(&zHdr[4], 0x000a); + fzip_put16(&zHdr[6], 0x0800); + fzip_put16(&zHdr[8], iMethod); + fzip_put16(&zHdr[10], zw->dosTime); + fzip_put16(&zHdr[12], zw->dosDate); + fzip_put16(&zHdr[26], nameLen); + fzip_put16(&zHdr[28], 13); + + fzip_put16(&zExTime[0], 0x5455); + fzip_put16(&zExTime[2], 9); zExTime[4] = 3; - fzip__put32(&zExTime[5], zw->unixTime); - fzip__put32(&zExTime[9], zw->unixTime); + fzip_put32(&zExTime[5], zw->unixTime); + fzip_put32(&zExTime[9], zw->unixTime); - /* Write the header and filename. */ + /* Write the header and filename. + */ iStart = (int)zw->body.used; fsl_buffer_append(&zw->body, zHdr, 30); fsl_buffer_append(&zw->body, zName, nameLen); fsl_buffer_append(&zw->body, zExTime, 13); - if(zw->body.errCode) return zw->body.errCode; - else if( nBlob>0 ){ - /* Write the compressed file. Compute the CRC as we progress. */ + if( nBlob>0 ){ + /* Write the compressed file. Compute the CRC as we progress. + */ stream.zalloc = (alloc_func)0; stream.zfree = (free_func)0; stream.opaque = 0; stream.avail_in = pSrc->used; stream.next_in = /* (unsigned char*) */pSrc->mem; @@ -43555,52 +41662,53 @@ stream.avail_out = sizeof(zOutBuf); stream.next_out = (unsigned char*)zOutBuf; deflate(&stream, Z_FINISH); toOut = sizeof(zOutBuf) - stream.avail_out; fsl_buffer_append(&zw->body, zOutBuf, toOut); - }while( stream.avail_out==0 && 0==zw->body.errCode ); - if(zw->body.errCode) return zw->body.errCode; + }while( stream.avail_out==0 ); nByte = stream.total_in; nByteCompr = stream.total_out; deflateEnd(&stream); - /* Go back and write the header, now that we know the compressed - file size. */ - z = (char *)zw->body.mem + iStart; - fzip__put32(&z[14], iCRC); - fzip__put32(&z[18], nByteCompr); - fzip__put32(&z[22], nByte); + /* Go back and write the header, now that we know the compressed file size. + */ + z = (char *)zw->body.mem + iStart/* &blob_buffer(&body)[iStart] */; + fzip_put32(&z[14], iCRC); + fzip_put32(&z[18], nByteCompr); + fzip_put32(&z[22], nByte); } - /* Make an entry in the tables of contents */ + /* Make an entry in the tables of contents + */ memset(zBuf, 0, sizeof(zBuf)); - fzip__put32(&zBuf[0], 0x02014b50); - fzip__put16(&zBuf[4], 0x0317); - fzip__put16(&zBuf[6], 0x000a); - fzip__put16(&zBuf[8], 0x0800); - fzip__put16(&zBuf[10], iMethod); - fzip__put16(&zBuf[12], zw->dosTime); - fzip__put16(&zBuf[14], zw->dosDate); - fzip__put32(&zBuf[16], iCRC); - fzip__put32(&zBuf[20], nByteCompr); - fzip__put32(&zBuf[24], nByte); - fzip__put16(&zBuf[28], nameLen); - fzip__put16(&zBuf[30], 9); - fzip__put16(&zBuf[32], 0); - fzip__put16(&zBuf[34], 0); - fzip__put16(&zBuf[36], 0); - fzip__put32(&zBuf[38], ((unsigned)iMode)<<16); - fzip__put32(&zBuf[42], iStart); + fzip_put32(&zBuf[0], 0x02014b50); + fzip_put16(&zBuf[4], 0x0317); + fzip_put16(&zBuf[6], 0x000a); + fzip_put16(&zBuf[8], 0x0800); + fzip_put16(&zBuf[10], iMethod); + fzip_put16(&zBuf[12], zw->dosTime); + fzip_put16(&zBuf[14], zw->dosDate); + fzip_put32(&zBuf[16], iCRC); + fzip_put32(&zBuf[20], nByteCompr); + fzip_put32(&zBuf[24], nByte); + fzip_put16(&zBuf[28], nameLen); + fzip_put16(&zBuf[30], 9); + fzip_put16(&zBuf[32], 0); + fzip_put16(&zBuf[34], 0); + fzip_put16(&zBuf[36], 0); + fzip_put32(&zBuf[38], ((unsigned)iMode)<<16); + fzip_put32(&zBuf[42], iStart); fsl_buffer_append(&zw->toc, zBuf, 46); fsl_buffer_append(&zw->toc, zName, nameLen); - fzip__put16(&zExTime[2], 5); + fzip_put16(&zExTime[2], 5); fsl_buffer_append(&zw->toc, zExTime, 9); ++zw->entryCount; - return zw->toc.errCode; + + return rc; } -int fzip__mkdir(fsl_zip_writer * const z, char const *zName){ +int fzip_mkdir(fsl_zip_writer * const z, char const *zName){ fsl_size_t i; fsl_size_t j; int rc = 0; char const * dirName; fsl_size_t nDir = z->dirs.used; @@ -43616,21 +41724,21 @@ char * cp = fsl_strndup(zName, (fsl_int_t)i+1); rc = cp ? fsl_list_append(&z->dirs, cp) : FSL_RC_OOM; if(cp && rc){ fsl_free(cp); }else{ - rc = fzip__file_add(z, cp, NULL, 0, 0); + rc = fzip_file_add(z, cp, NULL, 0, 0); } } } } return rc; } int fsl_zip_file_add(fsl_zip_writer * const z, char const * zName, fsl_buffer const * pSrc, int mPerm){ - return fzip__file_add(z, zName, pSrc, mPerm, 1); + return fzip_file_add(z, zName, pSrc, mPerm, 1); } int fsl_zip_root_set(fsl_zip_writer * const z, char const * zRoot ){ if(!z) return FSL_RC_MISUSE; else if(zRoot && *zRoot && fsl_is_absolute_path(zRoot)){ @@ -43668,11 +41776,11 @@ assert('/'==cp[n-1]); cp[n-1] = 0; rc = fsl_is_simple_pathname(cp, 1); cp[n-1] = '/'; rc = rc - ? fzip__mkdir(z, cp) + ? fzip_mkdir(z, cp) : FSL_RC_RANGE; z->rootDir = cp /* transfer ownership on error as well and let normal downstream clean it up. */; return rc; } @@ -43679,11 +41787,11 @@ } return 0; } } -static void fsl__zip_finalize(fsl_zip_writer * const z, bool alsoBody){ +static void fsl_zip_finalize_impl(fsl_zip_writer * const z, bool alsoBody){ if(z){ fsl_buffer_clear(&z->toc); fsl_buffer_clear(&z->scratch); fsl_list_visit_free(&z->dirs, 1); assert(NULL==z->dirs.list); @@ -43698,12 +41806,13 @@ } } } void fsl_zip_finalize(fsl_zip_writer * const z){ - fsl__zip_finalize(z, 1); + fsl_zip_finalize_impl(z, 1); } + int fsl_zip_end( fsl_zip_writer * const z ){ int rc; fsl_int_t iTocStart; fsl_int_t iTocEnd; @@ -43714,21 +41823,21 @@ if(rc) return rc; fsl_buffer_clear(&z->toc); iTocEnd = (fsl_int_t)z->body.used; memset(zBuf, 0, sizeof(zBuf)); - fzip__put32(&zBuf[0], 0x06054b50); - fzip__put16(&zBuf[4], 0); - fzip__put16(&zBuf[6], 0); - fzip__put16(&zBuf[8], (int)z->entryCount); - fzip__put16(&zBuf[10], (int)z->entryCount); - fzip__put32(&zBuf[12], iTocEnd - iTocStart); - fzip__put32(&zBuf[16], iTocStart); - fzip__put16(&zBuf[20], 0); + fzip_put32(&zBuf[0], 0x06054b50); + fzip_put16(&zBuf[4], 0); + fzip_put16(&zBuf[6], 0); + fzip_put16(&zBuf[8], (int)z->entryCount); + fzip_put16(&zBuf[10], (int)z->entryCount); + fzip_put32(&zBuf[12], iTocEnd - iTocStart); + fzip_put32(&zBuf[16], iTocStart); + fzip_put16(&zBuf[20], 0); rc = fsl_buffer_append(&z->body, zBuf, 22); - fsl__zip_finalize(z, 0); + fsl_zip_finalize_impl(z, 0); assert(z->body.used); return rc; } int fsl_zip_end_take( fsl_zip_writer * const z, fsl_buffer * dest ){ @@ -43762,10 +41871,12 @@ } fsl_zip_finalize( z ); return rc; } } + + struct ZipState{ fsl_cx * f; fsl_id_t vid; fsl_card_F_visitor_f progress; @@ -43783,11 +41894,12 @@ static int fsl_card_F_visitor_zip(fsl_card_F const * fc, void * state){ ZipState * zs = (ZipState *)state; fsl_id_t frid; int rc = 0; - if(zs->progress){ + if(!fc->uuid) return 0 /* file was removed in this (delta) manifest */; + else if(zs->progress){ rc = (*zs->progress)(fc, zs->progressState); if(rc) return rc; }else if(FSL_FILE_PERM_LINK == fc->perm){ return fsl_cx_err_set(zs->f, FSL_RC_NYI, "Symlinks are not yet supported " @@ -43806,11 +41918,11 @@ fsl_zip_timestamp_set_unix(&zs->z, mTime); zs->cbuf.used = 0; rc = fsl_content_get(zs->f, frid, &zs->cbuf); if(!rc){ rc = fsl_zip_file_add(&zs->z, fc->name, &zs->cbuf, - fc->perm); + FSL_FILE_PERM_REGULAR); if(rc){ fsl_cx_err_set(zs->f, rc, "Error %s adding file [%s] " "to zip.", fsl_rc_cstr(rc), fc->name); @@ -43818,10 +41930,11 @@ } } } return rc; } + int fsl_repo_zip_sym_to_filename( fsl_cx * const f, char const * sym, char const * rootDir, char const * fileName, fsl_card_F_visitor_f progress, @@ -43901,10 +42014,14 @@ fsl_buffer_clear(&zs.cbuf); fsl_zip_finalize(&zs.z); fsl_deck_clean(&mf); return rc; } + + + +#undef MARKER /* end of file ./src/zip.c */ /* start of file ./src/difftk_cstr.c */ /** @page page_difftk_cstr difftk.tcl Binary form of file ./src/difftk.tcl. Index: lib/libfossil.h ================================================================== --- lib/libfossil.h +++ lib/libfossil.h @@ -245,39 +245,10 @@ If true, the fsl_timer_xxx() family of functions might do something useful, otherwise they do not. */ #define FSL_CONFIG_ENABLE_TIMER 1 -#if !defined(FSL_SWITCH_FALL_THROUGH) -# if defined(__GNUC__) && !defined(__clang__) && (__GNUC__ >= 7) -/* - #define FSL_USING_GCC - - gcc v7+ treats implicit 'switch' fallthrough as a warning - (i.e. error because we always build with -Wall -Werror -Wextra - -pedantic). Because now it's apparently considered modern to warn - for using perfectly valid features of the language. Holy cow, guys, - what the hell were you thinking!?!?!? - - Similarly braindead, clang #defines __GNUC__. - - _Sigh_. -*/ -# define FSL_SWITCH_FALL_THROUGH __attribute__ ((fallthrough)) -# else -# define FSL_SWITCH_FALL_THROUGH -# endif -#endif -/* /FSL_SWITCH_FALL_THROUGH - - TODO: add support for the C++ attributes for doing this. -*/ - -#if !defined(__unused) -#define __unused -#endif - #endif /* ORG_FOSSIL_SCM_FSL_CONFIG_H_INCLUDED */ /* end of file ./include/fossil-scm/config.h */ /* start of file ./include/fossil-scm/util.h */ @@ -767,17 +738,13 @@ at its starting point. Subsequent calls to this function will increment the cursor by the number of bytes returned via *n. The buffer's "used" member is used to determine the logical end of input. - If fsl_buffer_err() is true for the state argument, that code is - returned without other side effects. - - Returns 0 on success and has no error conditions except for invalid - arguments, which result in undefined beavhiour, or fsl_buffer_err() - being true for the state argument. Results are undefined if any - argument is NULL. + Returns 0 on success and has no error conditions except for + invalid arguments, which result in undefined beavhiour. Results + are undefined if any argument is NULL. Tip (and warning): sometimes a routine might have a const buffer handle which it would like to use in conjunction with this routine but cannot without violating constness. Here's a crude workaround: @@ -914,12 +881,10 @@ @see fsl_buffer_cstr() @see fsl_buffer_size() @see fsl_buffer_capacity() @see fsl_buffer_clear() @see fsl_buffer_reuse() - @see fsl_buffer_err() - @see fsl_buffer_err_clear() */ struct fsl_buffer { /** The raw memory pointed to by this buffer. There are two ways of using this member: @@ -971,35 +936,25 @@ trailing NUL byte which is not counted in the "used" length. */ fsl_size_t used; /** Used by some routines to keep a cursor into this->mem. + + TODO: factor this back out and let those cases keep their own + state. This is only used by fsl_input_f_buffer() (and that + function cannot be implemented unless we add the cursor here + or add another layer of state type specifically for it). + + TODO: No, don't do ^^^^. It turns out that the merge algo + wants this as well. */ fsl_size_t cursor; - - /** - When any buffer-related routines encounter an error, they set - this flag so that the error can be propagated. Many APIs also - become no-ops when this is set, the intention being to simplify - many common uses of this class, e.g.: - - ``` - fsl_buffer_append(b, ... ); - fsl_buffer_append(b, ... ); - fsl_buffer_append(b, ... ); - if(fsl_buffer_err(b)) ... - ``` - - The alternative (without this member) being to check for an OOM - error after every append operation. - */ - int errCode; }; /** Empty-initialized fsl_buffer instance, intended for const-copy initialization. */ -#define fsl_buffer_empty_m {NULL,0U,0U,0U,0} +#define fsl_buffer_empty_m {NULL,0U,0U,0U} /** Empty-initialized fsl_buffer instance, intended for copy initialization. */ FSL_EXPORT const fsl_buffer fsl_buffer_empty; @@ -1194,30 +1149,14 @@ the first invalid byte. Thus is can be used on, e.g., full ISO8601-format strings. If z is NULL, 0 is returned. */ FSL_EXPORT int fsl_str_is_date2(const char *z); - -/** - Returns the current error code of the given buffer. Many buffer - APIs become noops when this function returns non-0. -*/ -FSL_EXPORT int fsl_buffer_err(fsl_buffer const * b); - -/** - Resets the error code of the given buffer to 0. -*/ -FSL_EXPORT void fsl_buffer_err_clear(fsl_buffer * const b); - /** Reserves at least n bytes of capacity in buf. Returns 0 on success, FSL_RC_OOM if allocation fails, FSL_RC_MISUSE if !buf. - If fsl_buffer_err() is true and n is not 0 then this function - returns its value without other side effects, with one exception: - if n is 0 then the error state is ignored. - If b is an external buffer then: - If n is 0, this disassociates b->mem from b, effectively clearing the buffer's state. Else... @@ -1229,15 +1168,13 @@ allocated, b->used bytes are copied from b->mem (its external memory) to the new block, and b->mem is replaced with the new block. Afterwards, b->capacity will be non-0. This does not change b->used, nor will it shrink the buffer - (reduce buf->capacity) unless n is 0, in which case it immediately: - - - frees b->mem (if b is a managed buffer) - - sets b->capacity, buf->used, and b->cursor to 0 - - sets b->errCode to 0 + (reduce buf->capacity) unless n is 0, in which case it immediately + frees b->mem (if b is a managed buffer) and sets b->capacity + and buf->used to 0. @see fsl_buffer_resize() @see fsl_buffer_materialize() @see fsl_buffer_clear() */ @@ -1291,12 +1228,10 @@ Results are undefined if passed a completely uninitialized buffer object. _Always_ initialize new buffer objects by copying fsl_buffer_empty or (when appropriate) fsl_buffer_empty_m. - This function resets b->errCode. - @see fsl_buffer_materialize() */ FSL_EXPORT void fsl_buffer_external( fsl_buffer * const b, void const * mem, fsl_int_t n ); /** @@ -1312,12 +1247,10 @@ external buffer). This does not (de)allocate memory, only changes the logical "used" size of the buffer. Returns its argument. - This function resets b->errCode. - Returns b. Achtung for fossil(1) porters: this function's semantics are much different from the fossil's blob_reset(). To get those semantics, use fsl_buffer_reserve(buf, 0) or its convenience form @@ -1328,23 +1261,20 @@ FSL_EXPORT fsl_buffer * fsl_buffer_reuse( fsl_buffer * const b ); /** Similar to fsl_buffer_reserve() except that... - - If fsl_buffer_err() is true, that result is returned with no other - side effects. - For managed buffers: - It does not free all memory when n==0. Instead it essentially - makes the memory a length-0, NUL-terminated string. + makes the memory a length-0, NUL-terminated string. - It will try to shrink (realloc) buf's memory if (ncapacity). - It sets buf->capacity to (n+1) and buf->used to n. This routine - allocates one extra byte to ensure that buf is always - NUL-terminated. + allocates one extra byte to ensure that buf is always + NUL-terminated. - On success it always NUL-terminates the buffer at offset buf->used. For external buffers it behaves slightly differently: @@ -1367,15 +1297,15 @@ @see fsl_buffer_clear() */ FSL_EXPORT int fsl_buffer_resize( fsl_buffer * const buf, fsl_size_t n ); /** - Swaps the entire state of the left and right arguments. Results are + Swaps the contents of the left and right arguments. Results are undefined if either argument is NULL or points to uninitialized memory. */ -FSL_EXPORT void fsl_buffer_swap( fsl_buffer * const left, fsl_buffer * const right ); +FSL_EXPORT void fsl_buffer_swap( fsl_buffer * left, fsl_buffer * right ); /** Similar fsl_buffer_swap() but it also optionally frees one of the buffer's memories after swapping them. If clearWhich is negative then the left buffer (1st arg) is cleared _after_ @@ -1399,23 +1329,19 @@ ``` Swaps the contents of b1 and b2, then frees the contents of the right-side buffer (b2). */ -FSL_EXPORT void fsl_buffer_swap_free( fsl_buffer * const left, - fsl_buffer * const right, +FSL_EXPORT void fsl_buffer_swap_free( fsl_buffer * left, fsl_buffer * right, int clearWhich ); /** Appends the first n bytes of src, plus a NUL byte, to b, expanding b as necessary and incrementing b->used by n. If n is less than 0 then the equivalent of fsl_strlen((char const*)src) is used to calculate the length. - If fsl_buffer_err() is true, its result is returned without further - side effects. - If b is an external buffer, it is first transformed into a managed buffer. Results are undefined if b or src are NULL. @@ -1438,13 +1364,10 @@ Uses fsl_appendf() to append formatted output to the given buffer. Returns 0 on success and FSL_RC_OOM if an allocation fails while expanding dest. Results are undefined if either of the first two arguments are NULL. - If fsl_buffer_err() is true, its result is returned without further - side effects. - @see fsl_buffer_append() @see fsl_buffer_reserve() */ FSL_EXPORT int fsl_buffer_appendf( fsl_buffer * const dest, char const * fmt, ... ); @@ -1455,24 +1378,18 @@ /** Compresses the first pIn->used bytes of pIn to pOut. It is ok for pIn and pOut to be the same blob. - If fsl_buffer_err() is true for either buffer, its result is - returned without further side effects. The buffers are checked in - the order of their parameter declaration. - pOut must either be the same as pIn or else a properly initialized buffer. Any prior contents will be freed or their memory reused. Results are undefined if any argument is NULL. - Returns 0 on success, FSL_RC_OOM on allocation error, and - FSL_RC_ERROR if the lower-level compression routines fail. If this - function returns non-0, it does not update pOut's error state - because the error will have happened on a temporary buffer. + Returns 0 on success, FSL_RC_OOM on allocation error, and FSL_RC_ERROR + if the lower-level compression routines fail. Use fsl_buffer_uncompress() to uncompress the data. The data is encoded with a big-endian, unsigned 32-bit length as the first four bytes (holding its uncomressed size), and then the data as compressed by zlib. @@ -1481,24 +1398,20 @@ @see fsl_buffer_compress2() @see fsl_buffer_uncompress() @see fsl_buffer_is_compressed() */ -FSL_EXPORT int fsl_buffer_compress(fsl_buffer const *pIn, fsl_buffer * const pOut); +FSL_EXPORT int fsl_buffer_compress(fsl_buffer const *pIn, fsl_buffer *pOut); /** Compress the concatenation of a blobs pIn1 and pIn2 into pOut. pOut must be either empty (cleanly initialized or newly recycled) or must be the same as either pIn1 or pIn2. Results are undefined if any argument is NULL. - If fsl_buffer_err() is true for any buffer, its result is - returned without further side effects. The buffers are checked in - the order of their parameter declaration. - Returns 0 on success, FSL_RC_OOM on allocation error, and FSL_RC_ERROR if the lower-level compression routines fail. TODO: if pOut!=(pIn1 or pIn2) then re-use its memory, if it has any. @@ -1506,32 +1419,27 @@ @see fsl_buffer_uncompress() @see fsl_buffer_is_compressed() */ FSL_EXPORT int fsl_buffer_compress2(fsl_buffer const *pIn1, fsl_buffer const *pIn2, - fsl_buffer * const pOut); + fsl_buffer *pOut); /** Uncompress buffer pIn and store the result in pOut. It is ok for pIn and pOut to be the same buffer. Returns 0 on success. If pIn!=pOut then on error, depending on the type of error, pOut may have been partially written so the state of its contents are unspecified (but its state as a buffer object is still valid). - If fsl_buffer_err() is true for either buffer, its result is - returned without further side effects. The buffers are checked in - the order of their parameter declaration. - pOut must be either cleanly initialized/empty or the same object as pIn. If it has any current memory, it will be reused if it's large enough and it is not the same pointer as pIn. Results are undefined if any argument is NULL. - Returns 0 on success, FSL_RC_OOM on allocation error, and some - other code if the lower-level decompression routines fail. On - error, pOut->errCode is updated. + Returns 0 on success, FSL_RC_OOM on allocation error, and + some other code if the lower-level decompression routines fail. Note that the decompression process, though computationally costly, is a no-op if pIn is not actually compressed. As a special case, if pIn==pOut and fsl_buffer_is_compressed() returns @@ -1572,45 +1480,39 @@ The fsl_buffer counterpart of fsl_data_uncompressed_size(). */ FSL_EXPORT fsl_int_t fsl_buffer_uncompressed_size(fsl_buffer const * b); /** - Equivalent to ((char const *)b->mem) except that if b->errCode is - non-0, this returns NULL. The returned string is effectively - b->used bytes long unless the user decides to apply his own - conventions. Note that the buffer APIs generally assure that + Equivalent to ((char const *)b->mem). The returned string is + effectively b->used bytes long unless the user decides to apply his + own conventions. Note that the buffer APIs generally assure that buffers are NUL-terminated, meaning that strings returned from this function can (for the vast majority of cases) assume that the returned string is NUL-terminated (with a string length of b->used _bytes_). It is, however, possible for client code to violate that convention via direct manipulation of the buffer or using non-NUL-terminated extranal buffers. - Results are undefined if b is NULL. - @see fsl_buffer_str() @see fsl_buffer_cstr2() */ FSL_EXPORT char const * fsl_buffer_cstr(fsl_buffer const * const b); /** - If b has any memory allocated to it and b->errCode is non-0, that - memory is returned. If len is not NULL then *len is set to - b->used. If b has no memory or b->errCode is non-0 then NULL is - returned and *len (if len is not NULL) is set to 0. - - Results are undefined if b is NULL. + If b has any memory allocated to it, that memory is returned. If + len is not NULL then *len is set to b->used. If b has no memory + then NULL is returned and *len (if len is not NULL) is set to 0. @see fsl_buffer_str() @see fsl_buffer_cstr() */ FSL_EXPORT char const * fsl_buffer_cstr2(fsl_buffer const * const b, fsl_size_t * const len); /** - Equivalent to ((char *)b->mem) except that if b->errCode is non-0 - then NULL is returned. The returned memory is effectively b->used - bytes long unless the user decides to apply their own conventions. + Equivalent to ((char *)b->mem). The returned memory is effectively + b->used bytes long unless the user decides to apply their own + conventions. Care must be taken to only write to the returned pointer for memory owned or write-proxied by this buffer. More specifically, results are undefined if b is an external buffer proxying const bytes. When in doubt about whether b is external, use fsl_buffer_materialize() @@ -1660,19 +1562,10 @@ where N is the shorter of the two buffers' lengths, it treats the shorter buffer as being "less than" the longer one. */ FSL_EXPORT int fsl_buffer_compare(fsl_buffer const * const lhs, fsl_buffer const * const rhs); -/** - Compares b and the first nStr bytes of the given string. If nStr is - negative, fsl_strlen() is used to calculate it. Returns true if the - buffer and the string match, as per the rules of - fsl_buffer_compare(), else returns false. -*/ -FSL_EXPORT bool fsl_buffer_eq(fsl_buffer const * const b, char const * str, - fsl_int_t nStr); - /** Bitwise-compares the contents of b against the file named by zFile. Returns 0 if they have the same size and contents, else non-zero. This function has no way to report if zFile cannot be opened, and any error results in a non-0 return value. No @@ -1720,14 +1613,10 @@ to properly initialized memory. If pTarget==pOriginal then this is a destructive operation, replacing the original's content with its new form. - If fsl_buffer_err() is true for any buffer, its result is - returned without further side effects. The buffers are checked in - the order of their parameter declaration. - Return 0 on success. @see fsl_buffer_delta_apply() @see fsl_delta_apply() @see fsl_delta_apply2() @@ -1742,14 +1631,10 @@ pErr if it is not NULL. It is rare that delta application fails (only if the inputs are invalid, e.g. do not belong together or are corrupt), but when it does, having error information can be useful. - If fsl_buffer_err() is true for any buffer, its result is - returned without further side effects. The buffers are checked in - the order of their parameter declaration. - @see fsl_buffer_delta_apply() @see fsl_delta_apply() @see fsl_delta_apply2() */ FSL_EXPORT int fsl_buffer_delta_apply2( fsl_buffer const * const pOriginal, @@ -1763,13 +1648,10 @@ dest must be a non-NULL, initialized (though possibly empty) fsl_buffer object. Its contents, if any, will be overwritten by this function, and any memory it holds might be re-used. - If fsl_buffer_err() is true then this function returns its value - without other side effects. - The src function is called, and passed the state parameter, to fetch the input. If it returns non-0, this function returns that error code. src() is called, possibly repeatedly, until it reports that there is no more data. @@ -1779,29 +1661,30 @@ dest->mem might (and possibly will) be (re)allocated by this function, so any pointers to it held from before this call might be invalidated by this call. - On error non-0 is returned and dest may be partially populated. + On error non-0 is returned and dest may bge partially populated. Errors include: - - Allocation error (FSL_RC_OOM), in which case dest->errCode is - updated. + dest or src are NULL (FSL_RC_MISUSE) - - src() returns an error code, in which case dest-errCode is _not_ - modified. + Allocation error (FSL_RC_OOM) + + src() returns an error code Whether or not the state parameter may be NULL depends on the src implementation requirements. On success dest will contain the contents read from the input source. dest->used will be the length of the read-in data, and dest->mem will point to the memory. dest->mem is automatically NUL-terminated if this function succeeds, but dest->used does not count that terminator. On error the state of dest->mem must be - considered incomplete, and is not guaranteed to be NUL-terminated. + considered incomplete, and is not guaranteed to be + NUL-terminated. Example usage: ``` fsl_buffer buf = fsl_buffer_empty; @@ -1844,25 +1727,19 @@ /** A wrapper for fsl_buffer_fill_from_FILE() which gets its input from the given file name. - If fsl_buffer_err() is true then this function returns its value - without other side effects. - It uses fsl_fopen() to open the file, so it supports the name '-' as an alias for stdin. */ FSL_EXPORT int fsl_buffer_fill_from_filename( fsl_buffer * const dest, char const * filename ); /** Writes the given buffer to the given filename. Returns 0 on success, FSL_RC_MISUSE if !b or !fname, FSL_RC_IO if opening or writing fails. - - If fsl_buffer_err() is true, that result is returned with no other - side effects. Uses fsl_fopen() to open the file, so it supports the name '-' as an alias for stdout. */ FSL_EXPORT int fsl_buffer_to_filename( fsl_buffer const * const b, @@ -1872,13 +1749,10 @@ Copy N lines of text from pFrom into pTo. The copy begins at the current pFrom->cursor position. pFrom->cursor is left pointing at the first character past the last `\n` copied. (Modification of the cursor is why pFrom is not const.) - If fsl_buffer_err() is true for either buffer (checked in parameter - order), that result is returned with no other side effects. - If pTo==NULL then this routine simply skips over N lines. Returns 0 if it copies lines or does nothing (because N is 0 or pFrom's contents have been exhausted). Copying fewer lines than requested (because of EOF) is not an error. Returns non-0 only on @@ -1893,13 +1767,10 @@ /** Works identically to fsl_buffer_copy_lines() except that it sends its output to the fTo output function. If fTo is NULL then it simply skips over N lines. - If fsl_buffer_err() is true, that result is returned with no other - side effects. - @see fsl_buffer_copy_lines() */ FSL_EXPORT int fsl_buffer_stream_lines(fsl_output_f fTo, void * const toState, fsl_buffer * const pFrom, fsl_size_t N); @@ -2531,17 +2402,10 @@ given path - only string evaluation. */ FSL_EXPORT int fsl_file_dirpart(char const * zFilename, fsl_int_t nLen, fsl_buffer * const pOut, bool leaveSlash); -/** - Return the tail of a NUL-terminated file pathname. The tail is the - last component of the path. For example, the tail of "/a/b/c.d" is - "c.d". If the name ends in a slash, a pointer to its NUL terminator - is returned. -*/ -FSL_EXPORT const char *fsl_file_tail(const char *z); /** Writes the absolute path name of the current directory to zBuf, which must be at least nBuf bytes long (nBuf includes the space for a trailing NUL terminator). @@ -2897,14 +2761,14 @@ */ FSL_EXPORT void *fsl_utf8_to_filename(const char *zUtf8); /** - Deallocate pOld, which must be NULL or must have been allocated by + Deallocate pOld, which must have been allocated by fsl_filename_to_utf8(), fsl_utf8_to_filename(), fsl_getenv(), or - another routine which explicitly documents this function as being - the proper finalizer for its returned memory. + another routine which explicitly documents this function as + being the proper finalizer for its returned memory. */ FSL_EXPORT void fsl_filename_free(void *pOld); /** Returns a (possible) copy of the environment variable with the @@ -3228,34 +3092,21 @@ If realLink is false or this is a Windows platform, a file is created named zLinkFile containing the string zTargetFile as its contents. If a file or symlink named zLinkFile already exists, it is removed before writing the new contents. - In both cases the parent directories for zLinkFile are created, if - needed, but that process will fail with FSL_RC_TYPE if any - non-directory components with conflicting names are found in the - to-be-mkdir'd path. + In both cases, the parent directories for zLinkFile are created, if + needed but that process will fail if any non-directory components + with conflicting names are found in the to-be-mkdir'd path. Returns 0 on success or some lower-level result code if creation/writing of a directory, a symlink, or pseudo-symlink fails. */ FSL_EXPORT int fsl_symlink_create(char const *zTargetFile, char const * zLinkFile, bool realLink); -/** - Reads symlink zFrom, as per fsl_symlink_read(), then creates a copy - named zTo, as per fsl_symlink_create(). The first argument for the - latter call is the contents of the result of fsl_symlink_read(). - The 2nd and 3rd arguments to fsl_symlink_create() are the 2nd and - 3rd arguments to this function. - - Returns 0 on success, FSL_RC_OOM on OOM, or any of various - filesystem-related non-0 codes if reading or saving the link fails. -*/ -FSL_EXPORT int fsl_symlink_copy(char const *zFrom, char const *zTo, bool realLink); - /** Uses fsl_getenv() to look for the environment variables (FOSSIL_USER, (Windows: USERNAME), (Unix: USER, LOGNAME)). If it finds one it returns a copy of that value, which must eventually be passed to fsl_free() to free it (NOT @@ -3313,11 +3164,11 @@ - FSL_RC_TYPE if the home (as determined via inspection of the environment) is not a directory. - FSL_RC_OOM if a memory (re)allocation fails. */ -FSL_EXPORT int fsl_find_home_dir( fsl_buffer * const tgt, bool requireWriteAccess ); +FSL_EXPORT int fsl_find_home_dir( fsl_buffer * tgt, bool requireWriteAccess ); /** Values for use with the fsl_fstat::type field. */ enum fsl_fstat_type_e { @@ -3439,16 +3290,15 @@ On success this function returns 0 and the length of the delta string, in bytes, excluding the final NUL terminator character, is written to *deltaSize. - Returns FSL_RC_OOM if memory allocation fails during generation of + Returns FSL_RC_MISUSE if any of the pointer arguments are NULL + and FSL_RC_OOM if memory allocation fails during generation of the delta. Returns FSL_RC_RANGE if lenSrc or lenOut are "too big" (if they cause an overflow in the math). - Results are undefined if any pointer is NULL. - Output Format: The delta begins with a base64 number followed by a newline. This number is the number of bytes in the TARGET file. Thus, given a delta file z, a program can compute the size of the @@ -3541,23 +3391,20 @@ A fsl_delta_create() wrapper which uses the first two arguments as the original and "new" content versions to delta, and outputs the delta to the 3rd argument (overwriting any existing contents and re-using any memory it had allocated). - If any of the buffers have the same address, FSL_RC_MISUSE is - returned. If fsl_buffer_err() is true for the 3rd argument, that - value is returned without side effects. If allocation of memory for - the delta fails, FSL_RC_OOM is returned and delta's errCode is - updated with that value. - - Results are undefined if any argument is NULL. + If the output buffer (delta) is the same as src or newVers, + FSL_RC_MISUSE is returned, and results are undefined if delta + indirectly refers to the same buffer as either src or newVers + or if any argument is NULL. Returns 0 on success. */ FSL_EXPORT int fsl_buffer_delta_create( fsl_buffer const * const src, - fsl_buffer const * const newVers, - fsl_buffer * const delta); + fsl_buffer const * const newVers, + fsl_buffer * const delta); /** Apply a delta created using fsl_delta_create(). The output buffer must be big enough to hold the whole output @@ -4096,11 +3943,11 @@ Convenience typedef. */ typedef struct fsl_dline fsl_dline; /** Initialized-with-defaults fsl_dline structure, intended for const-copy initialization. */ -#define fsl_dline_empty_m {NULL,0U,0U,0U,0U,0U,0U} +#define fsl_dline_empty_m {NULL,0U,0U,0U,0U,0U} /** Initialized-with-defaults fsl_dline structure, intended for non-const copy initialization. */ extern const fsl_dline fsl_dline_empty; /** @@ -4509,11 +4356,11 @@ NULL/*insertion()*/,NULL/*deletion()*/, NULL/*replacement()*/, \ NULL/*edit()*/, NULL/*finish()*/, NULL/*finally()*/,NULL/*finalize()*/, \ false/*twoPass*/,0U/*passNumber*/, \ NULL/*pimpl*/, 0U/*pimplFlags*/,0U/*implFlags*/,0U/*fileCount*/, \ 0/*lnLHS*/,0/*lnRHS*/, \ - {/*metrics*/0,0,0,0} \ + {/*metric*/0,0,0} \ } /** Initialized-with-defaults fsl_dibu structure, intended for non-const copy initialization. */ extern const fsl_dibu fsl_dibu_empty; @@ -4962,34 +4809,21 @@ Is a no-op if pf is NULL. */ FSL_EXPORT void fsl_pathfinder_clear(fsl_pathfinder * const pf); /** - Equivalent to fsl_pathfinder_dir_add2() with -1 as a final - argument. + Adds the given directory to pf's search path. Returns 0 on + success, FSL_RC_MISUSE if !pf or !dir (dir _may_ be an empty + string), FSL_RC_OOM if copying the string or adding it to the + list fails. @see fsl_pathfinder_ext_add() @see fsl_pathfinder_search() */ FSL_EXPORT int fsl_pathfinder_dir_add(fsl_pathfinder * const pf, char const * const dir); -/** - Adds the given directory to pf's search path. Returns 0 on - success, FSL_RC_MISUSE if !pf or !dir (dir _may_ be an empty - string), FSL_RC_OOM if copying the string or adding it to the - list fails. - - If strLen is negative, fsl_strlen() is used to calculate the length - of the string. - - @see fsl_pathfinder_ext_add() - @see fsl_pathfinder_search() -*/ -FSL_EXPORT int fsl_pathfinder_dir_add2(fsl_pathfinder * const pf, char const * const dir, - fsl_int_t strLen); - /** Adds the given directory to pf's search extensions. Returns 0 on success, FSL_RC_MISUSE if !pf or !dir (dir _may_ be an empty string), FSL_RC_OOM if copying the string or adding it to the list fails. @@ -5000,40 +4834,13 @@ and "~" are both perfectly valid extensions for this purpose. @see fsl_pathfinder_dir_add() @see fsl_pathfinder_search() */ -FSL_EXPORT int fsl_pathfinder_ext_add2(fsl_pathfinder * const pf, char const * const ext, - fsl_int_t strLen); - -/** - Equivalent to fsl_pathfinder_ext_add2() with -1 as a final - argument. - - @see fsl_pathfinder_dir_add() - @see fsl_pathfinder_search() -*/ FSL_EXPORT int fsl_pathfinder_ext_add(fsl_pathfinder * const pf, char const * const ext); -/** - Splits a conventional path-separator-delimited string into tokens - and adds each as either a directory (if isDirs is true) or an - extension in the given fsl_pathfinder object. Returns 0 on success, - FSL_RC_OOM on allocation error. pathLen is the length of the path - string. If it's negative, fsl_strlen() is used to calculate it. - - See fsl_path_splitter for the semantics of the splitting. - - To change the delimiter characters, set tgt->separators before - calling this. -*/ -FSL_EXPORT int fsl_pathfinder_split( fsl_pathfinder * const tgt, - bool isDirs, - char const * path, - fsl_int_t pathLen ); - /** Searches for a file whose name can be constructed by some combination of pf's directory/suffix list and the given base name. @@ -5051,28 +4858,27 @@ On success (a readable filesystem entry is found): - It returns 0. - - If pOut is not NULL then `*pOut` is set to the path it found. The - bytes of the returned string are only valid until the next search - operation on pf, so copy them if you need them. Note that the - returned path is _not_ normalized via fsl_file_canonical_name() - or similar, and it may very well return a relative path (if base - or one of pf->dirs contains a relative path part). As a special - case, if `base` is found as-is, without a lookup, `*pOut` is set - to `base`, so has its lifetime. - - - If outLen is not NULL, *outLen will be set to the length of the - returned string. + - If pOut is not NULL then *pOut is set to the path it + found. The bytes of the returned string are only valid until the + next search operation on pf, so copy them if you need them. + Note that the returned path is _not_ normalized via + fsl_file_canonical_name() or similar, and it may very well + return a relative path (if base or one of pf->dirs contains a + relative path part). + + - If outLen is not NULL, *outLen will be set to the + length of the returned string. On error: - Returns FSL_RC_MISUSE if !pf, !base, !*base. - - Returns FSL_RC_OOM on allocation error (it uses a buffer to hold - its path combinations and return value). + - Returns FSL_RC_OOM on allocation error (it uses a buffer to + hold its path combinations and return value). - Returns FSL_RC_NOT_FOUND if it finds no entry. The host platform's customary path separator is used to separate directory/file parts ('\\' on Windows and '/' everywhere else). @@ -5081,15 +4887,16 @@ which case a return of 0 signals that an entry was found, but the client has no way of knowing what path it might be (unless, of course, he relies on internal details of the fsl_pathfinder API, which he most certainly should not do). - Tip: if the client wants to be certain that this function will not - allocate memory, simply use fsl_buffer_reserve() on pf->buf to - reserve the desired amount of space in advance. As long as the - search paths never surpass that length, this function will not need - to allocate. (Until/unless the following TODO is implemented...) + Tip: if the client wants to be certain that this function will + not allocate memory, simply use fsl_buffer_reserve() on pf->buf + to reserve the desired amount of space in advance. As long as + the search paths never extend that length, this function will + not need to allocate. (Until/unless the following TODO is + implemented...) Potential TODO: use fsl_file_canonical_name() so that the search dirs themselves do not need to be entered using platform-specific separators. The main reason it's not done now is that it requires another allocation. The secondary reason is @@ -5348,12 +5155,12 @@ FSL_EXPORT int fsl_zip_end_to_filename( fsl_zip_writer * const z, char const * filename ); /** - Returns a pointer to z's ZIP content buffer. The contents are ONLY - valid after fsl_zip_end() returns 0. + Returns a pointer to z's ZIP content buffer. The contents + are ONLY valid after fsl_zip_end() returns 0. @see fsl_zip_timestamp_set_julian() @see fsl_zip_timestamp_set_unix() @see fsl_zip_file_add() @see fsl_zip_end() @@ -5744,13 +5551,10 @@ /** Appends the first n bytes of string z to buffer b in the form of TCL-format string literal. If n<0 then fsl_strlen() is used to determine the length. Returns 0 on success, FSL_RC_OOM on error. - If fsl_buffer_err() is true for the given buffer, that code is - returned without other side effects. - If the 2nd argument is true, squiggly braces within the string are escaped, else they are not. Whether that's required or not depends on how the resulting TCL will be used. If it will be eval'd directly, it must be escaped. If it will be read as a file and tokenized, it needn't be. @@ -6134,21 +5938,17 @@ A convenience from of fsl_strip_trailing_slashes() which strips trailing slashes from the given buffer and changes its b->used value to account for any stripping. Results are undefined if b is not properly initialized. */ -FSL_EXPORT void fsl_buffer_strip_slashes(fsl_buffer * const b); +FSL_EXPORT void fsl_buffer_strip_slashes(fsl_buffer * b); /** Appends each ID from the given bag to the given buffer using the given separator string. Returns FSL_RC_OOM on allocation error. - - If fsl_buffer_err() is true for the given buffer, that code is - returned without other side effects. */ -FSL_EXPORT int fsl_id_bag_to_buffer(fsl_id_bag const * bag, - fsl_buffer * const b, +FSL_EXPORT int fsl_id_bag_to_buffer(fsl_id_bag const * bag, fsl_buffer * b, char const * separator); /** Flags for use with the fsl_looks family of functions. */ @@ -6257,132 +6057,10 @@ not guaranteed by the API, but the implementation currently uses sqlite3_randomness(). */ FSL_EXPORT void fsl_randomness(unsigned int n, void *tgt); - -/** - Given a filename and its length, this function returns a pointer to - the last instance of a path separator character in that string, - checking for both `/` and `\\`. If the length is negative, - fsl_strlen() is used to calculate it. If no separator is found, - NULL is returned. If it returns non-NULL, the pointer is guaranteed to - live somewhere between the half-open range [str,str+slen). -*/ -FSL_EXPORT char const * fsl_last_path_sep(char const * str, fsl_int_t slen ); - -/** - A helper type for tokenizing conventional PATH-style strings. - Initialize them with fsl_path_splitter_init() and iterate over them - with fsl_path_splitter_next(). -*/ -struct fsl_path_splitter { - /** Begining of the input range. */ - char const * begin; - /** One-after-the-end of the input range. */ - char const * end; - /** Position for the next token lookup. */ - char const * pos; - /** List of token separator characters (ASCII only). */ - char const * separators; -}; -typedef struct fsl_path_splitter fsl_path_splitter; -/** @def fsl_path_splitter_empty_m - - Default-initialized fsl_path_splitter instance, intended for const-copy - initialization. On Windows builds its separators member is set to - ";" and on other platforms it's set to ":;". -*/ -#if FSL_PLATFORM_IS_WINDOWS -# define fsl_path_splitter_empty_m {NULL,NULL,NULL,";"} -#else -# define fsl_path_splitter_empty_m {NULL,NULL,NULL,":;"} -#endif - -/** - Default-initialized fsl_path_splitter instance, intended for - copy initialization. - - @see fsl_path_splitter_empty_m -*/ -FSL_EXPORT const fsl_path_splitter fsl_path_splitter_empty; - -/** - Wipes out pt's current state by copying fsl_path_splitter_empty over it - and initializes pt to use the given path as its input. If len is 0 - or more then it must be the length of the string, in bytes. If len - is less than 0, fsl_strlen() is used to determine the path's - length. (When dealing with inputs which are not NUL-terminated, - it's critical that the user pass the correct non-negative length.) - - If the client wants to modify pt->separators, it must be done so - *after* calling this. - - Use fsl_path_splitter_next() to iterate over the path entries. -*/ -void fsl_path_splitter_init( fsl_path_splitter * const pt, char const * path, - fsl_int_t len ); - -/** - Given a fsl_path_splitter which was formerly initialized using - fsl_path_splitter_init(), this iterates over the next-available - path component in the input, skipping over empty entries (leading, - consecutive, or trailing separator characters). - - The separator characters are specified by pt->separators, which must - be a NUL-terminated string of 1 or more characters. - - If a non-empty entry is found then: - - - *token is set to the first byte of the entry. - - - *len is assigned to the byte length of the entry. - - If no entry is found then: - - - *token, and *len are not modified. - - - FSL_RC_NOT_FOUND is returned if the end of the path was found - while tokenizing. - - - FSL_RC_MISUSE is returned if pt->separators is NULL or empty or - contains any non-ASCII characters. - - - FSL_RC_RANGE is returned if called after the previous case, or - if the input object's path has a length of 0. - - In any non-0-return case, it's not a fatal error, it's simply - information about why tokenization cannot continue, and can - normally be ignored. After non-0 is returned, the tokenizer must be - re-initialized if it is to be used again. - - Example: - - @code - char const * t = 0; - fsl_size_t tLen = 0; - fsl_path_splitter pt = fsl_path_splitter_empty; - fsl_path_splitter_init(&pt, path, pathLen); - while(0==fsl_path_splitter_next(&pt, &t, &tLen)){ - // The next element is the tLen bytes of memory starting at t: - printf("Path element: %.*s\n", (int)tLen, t); - } - @endcode -*/ -int fsl_path_splitter_next( fsl_path_splitter * const pt, char const ** token, - fsl_size_t * const len ); - -/** - Implements a cross-platform system(3) interface. It passes its - argument, which must not be NULL or empty to the equivalent of - system(). Returns 0 if that call fails, else it will return a - FSL_RC code approximating the lower-level error code (noting that - this system's man pages are a bit vague on the exact return - semantics of system(3)). -*/ -int fsl_system(const char *zOrigCmd); - #if 0 /** The UTF16 counterpart of fsl_looks_like_utf8(), with the addition that the 2nd argument, if true, specifies that the 2nd argument is true then the contents of the buffer are byte-swapped for checking purposes. @@ -6445,19 +6123,10 @@ */ namespace fsl {} extern "C" { #endif -/** @internal - - An internal helper macro to help localize OOM error reports (which - are most often side effects of other issues, rather than being - real OOM cases). -*/ -#define FSL__WARN_OOM \ - fprintf(stderr,"OOM @ %s:%d\n", __FILE__, __LINE__) - /** @struct fsl_cx The main Fossil "context" type. This is the first argument to many Fossil library API routines, and holds all state related @@ -6695,28 +6364,27 @@ Maintenance ACHTUNG: this enum's values must align with those from fossil(1) because their integer values are used in the `repo.config` table. */ enum fsl_hashpolicy_e { -/** Use only SHA1 hashes. */ +/* Use only SHA1 hashes. */ FSL_HPOLICY_SHA1 = 0, -/** Accept SHA1 hashes but auto-promote to SHA3. */ +/* Accept SHA1 hashes but auto-promote to SHA3. */ FSL_HPOLICY_AUTO = 1, -/** Use SHA3 hashes. */ +/* Use SHA3 hashes. */ FSL_HPOLICY_SHA3 = 2, -/** Use SHA3 hashes exclusively. */ +/* Use SHA3 hashes exclusively. */ FSL_HPOLICY_SHA3_ONLY = 3, -/** With this policy, fsl_uuid_is_shunned() will always return true - for SHA1 hashes, making it "impossible" to get SHA1-hashed content - into the repository (for a given value of "impossible"). */ +/* With this policy, fsl_uuid_is_shunned() will always return true for + SHA1 hashes. */ FSL_HPOLICY_SHUN_SHA1 = 4 }; typedef enum fsl_hashpolicy_e fsl_hashpolicy_e; /** Most functions in this API which return an int type return error - codes from the fsl_rc_e enum. None of these entries are + codes from the fsl_rc_e enum. None of these entries are (currently) guaranteed to have a specific value across library versions except for FSL_RC_OK, which is guaranteed to always be 0 (and the API guarantees that no other code shall have a value of zero). @@ -6744,11 +6412,11 @@ /** Out of memory. Indicates that a resource allocation request failed. */ FSL_RC_OOM = 102, -/** +/* API misuse (invalid args) */ FSL_RC_MISUSE = 103, /** Some range was violated (function argument, UTF character, etc.). @@ -6853,16 +6521,10 @@ Indicates that a checksum comparison failed, possibly indicating that corrupted or unexpected data was just read. */ FSL_RC_CHECKSUM_MISMATCH = 121, -/** - Indicates a resource-locking error of some sort, normally a - db lock. -*/ -FSL_RC_LOCKED, - /** Indicates that a merge conflict, or some other context-dependent type of conflict, was detected. */ FSL_RC_CONFLICT, @@ -6973,24 +6635,10 @@ Intended to be used with fsl_cx_interrupt() by signal handlers and UI threads. */ FSL_RC_INTERRUPTED, -/** - Intended to be used by operations which would cause what is - presumably an unintended fork. Fossil does not have any issues with - forking, but practice suggests that most forks (that is, checking - in to a non-leaf version) are unintentional. -*/ -FSL_RC_WOULD_FORK, - -/** - This is intended only for internal use with fsl__fatal(), to - report conditions which "cannot possibly happen." -*/ -FSL_RC_CANNOT_HAPPEN, - /** Must be the final entry in the enum. Used for creating client-side result codes which are guaranteed to live outside of this one's range. */ @@ -7023,14 +6671,13 @@ /** Returns a "standard" string form for a fsl_rc_e code. The string is primarily intended for debugging purposes. The returned bytes are guaranteed to be static and NUL-terminated. They are not guaranteed to contain anything useful for any purposes other than - debugging and tracking down problems. If passed a code which is - not in the fsl_rc_e enum, it returns NULL. + debugging and tracking down problems. */ -FSL_EXPORT char const * fsl_rc_cstr(int rc); +FSL_EXPORT char const * fsl_rc_cstr(int); /** Returns the value of FSL_LIBRARY_VERSION used to compile the library. If this value differs from the value the caller was compiled with, Chaos might ensue. @@ -7084,11 +6731,11 @@ struct fsl_cx_config { /** If true, all SQL which goes through the fossil engine will be traced to stdout. - TODO: replace this with a FILE pointer or a fsl_outputer. + TODO: replace this with a FILE pointer. */ bool traceSql; /** If true, the fsl_print() SQL function will output its output to the fsl_output()-configured channel, else it is a no-op. @@ -7208,14 +6855,12 @@ FSL_EXPORT int fsl_cx_init( fsl_cx ** tgt, fsl_cx_init_opt const * param ); /** Frees all memory associated with f, which must have been allocated/initialized using fsl_cx_malloc(), fsl_cx_init(), or - equivalent, or created on the stack and properly initialized (via - fsl_cx_init() or copy-constructed from fsl_cx_empty). If it was - allocated using fsl_cx_malloc(), this function frees f, else the - memory is owned by someone else and it is not freed. + equivalent, or created on the stack and properly initialized + (via fsl_cx_init() or copy-constructed from fsl_cx_empty). This function triggers any finializers set for f's client state or output channel. This is a no-op if !f and is effectively a no-op if f has no @@ -8289,26 +7934,10 @@ fsl_config_close()). Returns FSL_RC_MISUSE if the opened SCM db(s) have an opened transaction, but that behaviour may change in the future to force a rollback and close the database(s). */ FSL_EXPORT int fsl_close_scm_dbs(fsl_cx * const f); - -/** - Clears various internal caches and resets various - internally-cached values related to repository and checkout - dbs. This is intended primarily to be used when a db transaction - is rolled back which might have introduced state into those caches - which would be stale after a rollback (and it is internally used - so - _do not_ call this after initiating a rollback!). It can also - be used when shelling out to an external app like fossil(1) which - might update the db or checkout. - - This "really should not" be called while a transaction is - underway. -*/ -FSL_EXPORT void fsl_cx_caches_reset(fsl_cx * const f); - #if 0 /** DO NOT USE - not yet tested and ready. @@ -9769,20 +9398,18 @@ FSL_EXPORT void * fsl_db_g_blob( fsl_db * const db, fsl_size_t * len, char const * sql, ... ); /** Similar to fsl_db_get_text() and fsl_db_get_blob(), but writes - its result to tgt, appending its results to the given buffer. + its result to tgt, overwriting (not appending to) any existing + memory it might hold. If asBlob is true then the underlying BLOB API is used to populate the buffer, else the underlying STRING/TEXT API is used. For many purposes there will be no difference, but if you know you might have binary data, be sure to pass a true value for asBlob to avoid any potential encoding-related problems. - - Results are undefined if any pointer argument is NULL. Returns - FSL_RC_MISUSE if the SQL is an empty string. */ FSL_EXPORT int fsl_db_get_buffer( fsl_db * const db, fsl_buffer * const tgt, bool asBlob, char const * sql, ... ); @@ -13833,11 +13460,11 @@ be looked up if it has an opened checkout (see the list below). Returns 0 and sets *rv to the id if it finds an unambiguous match. - Returns FSL_RC_MISUSE if !sym, !*sym, or !rv. + Returns FSL_RC_MISUSE if !f, !sym, !*sym, or !rv. Returns FSL_RC_NOT_A_REPO if f has no opened repository. Returns FSL_RC_AMBIGUOUS if sym is a partial UUID which matches multiple full UUIDs. @@ -13846,18 +13473,19 @@ Symbols supported by this function: - SHA1/3 hash - SHA1/3 hash prefix of at least 4 characters - - Symbolic Name, e.g. branch name + - Symbolic Name - "tag:" + symbolic name - Date or date-time - "date:" + Date or date-time - symbolic-name ":" date-time - - "tip" means the most recent checkin, regardless of its branch + - "tip" + - "rid:###" resolves to the hash of blob.rid ### if that RID is in - the database + the database The following additional forms are available in local checkouts: - "current" - "prev" or "previous" @@ -13869,37 +13497,33 @@ - "root:" prefix resolves to the checkin of the parent branch from which the record's branch divered. i.e. the version from which it was branched. In the trunk this will always resolve to the first checkin. - - "start:" prefix resolves to the first checkin of the branch to - which the given checkin belongs. This differs from "root:" by a - single checkin: the "root:" point is the parent checkin of the - "start:" point. - - "merge-in:" TODO - document this once its implications are understood. - If type is not FSL_SATYPE_ANY then it will only match artifacts of - the specified type. In order to resolve arbitrary UUIDs, e.g. - those of arbitrary blob content, type needs to be FSL_SATYPE_ANY. + If type is not FSL_SATYPE_ANY then it will only match artifacts + of the specified type. In order to resolve arbitrary UUIDs, e.g. + those of arbitrary blob content, type needs to be + FSL_SATYPE_ANY. + */ FSL_EXPORT int fsl_sym_to_rid( fsl_cx * const f, char const * sym, - fsl_satype_e type, fsl_id_t * const rv ); + fsl_satype_e type, fsl_id_t * rv ); /** Similar to fsl_sym_to_rid() but on success it returns a UUID string by assigning it to *rv (if rv is not NULL). If rid is not NULL then on success the db record ID corresponding to the returned UUID is assigned to *rid. The caller must eventually free the returned string memory by passing it to fsl_free(). Returns 0 if it finds a - match and one of any number of possible result codes on error, most - notably FSL_RC_NOT_FOUND if no match is found. + match and any number of result codes on error. */ -FSL_EXPORT int fsl_sym_to_uuid( fsl_cx * const f, char const * sym, - fsl_satype_e type, fsl_uuid_str * const rv, - fsl_id_t * const rid ); +FSL_EXPORT int fsl_sym_to_uuid( fsl_cx * f, char const * sym, + fsl_satype_e type, fsl_uuid_str * rv, + fsl_id_t * rid ); /** Searches f's repo database for the a blob with the given uuid (any unique UUID prefix). On success a positive record ID is @@ -14562,11 +14186,11 @@ /** Convenience typedef. */ typedef struct fsl_rebuild_step fsl_rebuild_step; /** Initialized-with-defaults fsl_rebuild_step structure, intended for const-copy initialization. */ -#define fsl_rebuild_step_empty_m {NULL,NULL,0,0,-1,false,FSL_SATYPE_INVALID} +#define fsl_rebuild_step_empty_m {NULL,NULL,0,0,-1,false} /** Initialized-with-defaults fsl_rebuild_step structure, intended for non-const copy initialization. */ FSL_EXPORT const fsl_rebuild_step fsl_rebuild_step_empty; @@ -14729,11 +14353,11 @@ /** Convenience typedef. */ typedef struct fsl_cidiff_opt fsl_cidiff_opt; /** Initialized-with-defaults fsl_cidiff_opt structure, intended for const-copy initialization. */ -#define fsl_cidiff_opt_empty_m {0,0,0,0} +#define fsl_cidiff_opt_empty_m {0,0} /** Initialized-with-defaults fsl_cidiff_opt structure, intended for non-const copy initialization. */ FSL_EXPORT const fsl_cidiff_opt fsl_cidiff_opt_empty; @@ -17383,13 +17007,13 @@ This function matches only vfile.pathname, not vfile.origname, because it is possible for a given name to be in both fields (in different records) at the same time. */ -FSL_EXPORT int fsl_filename_to_vfile_id( fsl_cx * const f, fsl_id_t vid, +FSL_EXPORT int fsl_filename_to_vfile_id( fsl_cx * f, fsl_id_t vid, char const * zName, - fsl_id_t * const vfid ); + fsl_id_t * vfid ); /** Searches the `vfile` table where `vfile.vid=vid` for a name which matches `zName` or all `vfile` entries found under a subdirectory named `zName` (with no trailing slash). `zName` must be relative to @@ -18019,227 +17643,10 @@ As of this writing, and for the foreseeable future, the list is comprised of only 3 elements, {".fslckout", "_FOSSIL_", NULL}, but the order of the non-NULL elements is unspecified by the interface. */ FSL_EXPORT char const ** fsl_ckout_dbnames(void); - -/** Convenience typedef. */ -typedef struct fsl_ckout_rename_opt fsl_ckout_rename_opt; -/** - Callback type for use with fsl_ckout_rename(). It gets called once per - iteration of a rename operation, along with the original options object - for the rename and: - - - zSrcName is the checkout-relative original name of the file. - - zDestname is the checkout-relative new name of the file. - - Both strings are owned by the calling operation and will be freed - soon after this call returns. If the client needs them, they must - make copies. - - It must returns 0 on success. If it returns non-0 then the current - renaming op is cancelled (rolled back) and the result code is - propagated back to the caller of fsl_ckout_rename(). - - This gets called immediately after the associated db record is - modified and before the filesystem move (if any) is performed. - - Design note: ideally the callback would be called after the - filesystem move, but the way the moves are currently processed (as - a batch after the db updates, noting that those updates may trigger - variou error conditions) precludes that. Alternately, we could - delay the callback until the file-move phase (noting that the - file-move step is optional), and that might be a sensible change to - make. -*/ -typedef int (*fsl_ckout_rename_f)(fsl_cx *, fsl_ckout_rename_opt const *, - char const * zSrcName, char const *zDestName); - -/** - Options object for use with fsl_ckout_rename(). -*/ -struct fsl_ckout_rename_opt { - /** - The source filename(s) or directory name(s) to rename. The - contents of this list _must not_ be modified while - fsl_ckout_rename() is running (e.g. via a callback function). - */ - fsl_list const * src; - /** - The target filename or directory name to rename to. - */ - char const * dest; - /** - If true, src and dest are resolved/normalized based on the current - working directory, else they must be relative to the top of the - checkout. - */ - bool relativeToCwd; - /** - If true fsl_ckout_rename() will attempt to move files within the - filesystem. If false, it will only do the db-side renaming. - */ - bool doFsMv; - /** - If true, fsl_ckout_rename() will not perform any lasting - operations but will "go through the motions" with the exception - of actually attempting to move files on disk (if doFsMv is - true). It will still report errors in this mode, with the - exception that filesystem-level moving is not attempted so - potential failures there cannot be detected. - - Achtung: in dry-run mode it will trigger a rollback of any - pending transaction which is opened before fsl_ckout_rename() is - called. - */ - bool dryRun; - /** Optional callback. May be NULL. */ - fsl_ckout_rename_f callback; - /** Optional state for the callback. */ - void * callbackState; -}; - -/** Initialized-with-defaults fsl_ckout_rename_opt structure, intended for - const-copy initialization. */ -#define fsl_ckout_rename_opt_empty_m {NULL,NULL,true,false,false,NULL,NULL} - -/** Initialized-with-defaults fsl_ckout_rename_opt structure, intended for - non-const copy initialization. */ -extern const fsl_ckout_rename_opt fsl_ckout_rename_opt_empty; - -/** - This routine renames one or more SCM-managed files matching all - entries in opt->src to opt->dest within the current checkout. This - updates the current checkout's vfile table with the changes, and - optionally attempts to move the files within the filesystem, but - does not commit anything, so the change may still be reverted later - on. - - If an entry in opt->src matches an existing directory name within - the SCM-managed file list, this function assumes that all files - under that directory are to be moved. - - If opt->relativeToCwd is true then the destination name and all - opt->src names are canonicalized based on the current working - directory (see fsl_getcwd()), otherwise they are assumed to be - relative to f's current checkout directory. - - If opt->dest refers to an _existing_ directory, when an opt->src - entry refers to a directory then that whole directory, including the - directory name part, is renamed. If opt->dest does not exist in the - filesystem and if opt->src resolves to multiple inputs, opt->dest is - assumed to be a new target directory but the renaming behavior of - the opt->src files differs. Examples: - - - Case 1: opt->dest refers to an existing directory: moving - directory foo/bar to baz will result in baz/bar. - - - Case 2: opt->dest does not refer to an existing directory: moving - directory foo/bar to baz results in baz with the previous contents - of foo/bar. - - Why? Because that's how fossil(1) does it. - - This function matches opt->src entries only against - `vfile.pathname`, not `vfile.origname`. - - If opt->doFsMv is true then the final operation this function - attempts is to rename source files to their new destination. It - will only attempt this in cases where (A) the original file exists - and (B) the destination file does _not_ exist. Any errors generated - during this filesystem-level rename/move step _are ignored_ for two - reasons: 1) fossil(1) does it that way and (2) after moving any - files, trying to roll back and undo the filesystem-level operations - would add a significant amount of complexity and new errror cases - (what happens when a try-to-undo-rename fails?). - - Returns 0 on succcess. Potential errors include: - - - FSL_RC_OOM on allocation error. - - - FSL_RC_NOT_A_CKOUT if f has no opened checkout. - - - FSL_RC_MISUSE: opt->src contains multiple entries but the - destination is not an _existing_ directory. - - - FSL_RC_ALREADY_EXISTS: cannot rename because the destination name - is already in use by another record. - - - FSL_RC_TYPE: opt->dest refers to a non-directory and an opt->src - entry refers to a directory. - - - Any potential DB-related errors. - - BUGS/Shortcomings: - - - If it fails moving files in the filesystem, it might have - successfully already moved one or more source files, and those - moves are not undone. - - - Depending on the case-sensitivity of the repo and the filesystem, - it might (UNTESTED) fail to rename a file if the new name differs - only in its case (e.g. `README.TXT` to `README.txt`). Those - relavant code renames such vfile entries but leaves the filesystem - entries intact because fossil(1) does it that way. - - - If it moves a whole directory full of files it leaves any - original directories intact. Improving this is on the TODO list. - Often, such directories have contents (e.g. compiled/generated/temp - files) and cannot be removed. -*/ -FSL_EXPORT int fsl_ckout_rename(fsl_cx * const f, - fsl_ckout_rename_opt const * opt); - -/** - Undoes a rename scheduled by fsl_ckout_rename(). zNewName is the - current (renamed) name of the file. - - If relativeToCwd is true then zNewName is canonicalized based on - the current working directory (see fsl_getcwd()), otherwise it is - assumed to be relative to f's current checkout directory. - - On success, or if no such pending rename is found, it returns 0. - If it actually performs a db-level rename then it sets - `*didSomething` to true if didSomething is not NULL. It should - arguably return FSL_RC_NOT_FOUND (or some such) if no matching - rename is pending, but that currently feels overly-pedantic. - - If doFsMv is true and a db entry is renamed and a file with the - new name exists in the filesystem then this function will - _delete_ any file which has the original name and rename the zNewName - file to its reverted name. If that fails, the operation as a - whole will fail and the db changes will be rolled back. Note, - however, that a file deleted by this operation cannot be recovered - and that the deletion must be done before the rename is attempted. - - On error, returns any of a wide array of non-0 result codes, only - two of which can come directly from this function: FSL_RC_OOM or - FSL_RC_NOT_A_CKOUT. Any other non-zero result codes are propagated - errors from lower-level code. -*/ -FSL_EXPORT int fsl_ckout_rename_revert(fsl_cx * const f, - char const * zNewName, - bool relativeToCwd, bool doFsMv, - bool *didSomething); - -/** - Fetches the vfile.pathname value for the given vfile.id - entry. Results are undefined if f does not have an opened - checkout. On success returns 0 and sets `*zOut` to the name, - transfering ownership to the caller (who must eventually pass it - to fsl_free()). If absolute is true then the returned name has the - checkout dir prepended to it, else it is relative to the top of - the repo. - - Potential TODO: this routine is in no way optimized for heavy use - within an app, expecting to be only rarely used, e.g. in - conjunction with fsl_ckout_rename()-like ops. If it sees any - significant use it should be refactored to use a cached statement. -*/ -FSL_EXPORT int fsl_vfile_pathname(fsl_cx * const f, fsl_id_t vfid, - bool absolute, char **zOut); - #if defined(__cplusplus) } /*extern "C"*/ #endif #endif @@ -18621,16 +18028,10 @@ user by assigning it to *zOut. Returns 0 on success, in which case *zOut is updated and non-0 on error, in which case *zOut is not modified. On success, ownership of *zOut is transferred to the caller, who must eventually free it using fsl_free(). - Errors include: - - - FSL_RC_OOM on allocation error - - FSL_RC_NOT_FOUND (Windows only) if no relevant environment - variables are set from which to form the db path. - The locations searched for the database file are platform-dependent... Unix-like systems are searched in the following order: @@ -18644,27 +18045,17 @@ 4) If $HOME/.config is a directory, use $HOME/.config/fossil.db 5) Fall back to $HOME/.fossil (historical name). + Except where listed above, this function does not check whether the + file already exists or is a database. + Windows: - 1) If $FOSSIL_HOME is set, use that directory. - 2) If $LOCALAPPDATA is set, use that directory. - 3) If $APPDATA is set, use that directory. - 4) If $USERPROFILE is set, use that directory. - 5) If $HOMEDRIVE and $HOMEPATH are set, use that directory. - - If none of the above are set, FSL_RC_NOT_FOUND is returned. - - The name `_fossil` is appended to the first of the above-listed - locations which is set in the environment. - - All platforms: - - Except where listed above, this function does not check whether the - file already exists or is a database. + - We need a Windows port of this routine. Currently it `#error`'s + out at compile-time on Windows. */ FSL_EXPORT int fsl_config_global_preferred_name(char ** zOut); /** A variant of fsl_config_get_int32() which can search through multiple @@ -19009,36 +18400,10 @@ #if defined(__cplusplus) extern "C" { #endif -/** @internal - - An internal helper macro to help localize OOM error reports (which - are most often side effects of other issues, rather than being - real OOM cases). It's intended to be used like: - - ``` - void * someAllocedMemory = fsl_malloc(...); // any allocating routine - if(!someAllocedMemory){ - FSL__WARN_OOM; rc = FSL_RC_OOM; goto end; - } - ``` - - or: - - ``` - if(FSL_RC_OOM==fs){ FSL__WARN_OOM; } - ``` - - Note that this macro _may_ expand to encompass multiple commands - so must never be used as the RHS of an `if` without squiggly - braces surrounding it. -*/ -#define FSL__WARN_OOM \ - fprintf(stderr,"OOM @ %s:%d\n", __FILE__, __LINE__) - typedef struct fsl__bccache fsl__bccache; typedef struct fsl__bccache_line fsl__bccache_line; typedef struct fsl__pq fsl__pq; typedef struct fsl__pq_entry fsl__pq_entry; @@ -21358,10 +20723,21 @@ state is updated with the current sqlite3_errmsg() string. */ int fsl__db_errcode(fsl_db * const db, int sqliteCode); /** @internal + + Clears various internal caches and resets various + internally-cached values related to a repository db, but the data + cleared here are not associated directly with a db handle. This is + intended primarily to be used when a db transaction is rolled back + which might have introduced state into those caches which would be + stale after a rollback. +*/ +void fsl__cx_clear_repo_caches(fsl_cx * const f); + +/** @internal Plug in fsl_cx-specific db functionality into the given db handle. This must only be passed the MAIN db handle for the context, immediately after opening that handle, before f->dbMain is assigned. @@ -21407,15 +20783,10 @@ Frees/clears the non-db state of f->ckout. */ void fsl__cx_ckout_clear(fsl_cx * const f); - -/** @internal - Register the "files of checkin" (fsl_foci) SQLite3 virtual table. -*/ -int fsl__foci_register(fsl_db * const db); /** @internal Maximum length of a line in a text file, in bytes. (2**15 = 32k) */ #define FSL__LINE_LENGTH_MASK_SZ 15 @@ -21425,17 +20796,10 @@ Bitmask which, when AND-ed with a number, will result in the bottom FSL__LINE_LENGTH_MASK_SZ bits of that number. */ #define FSL__LINE_LENGTH_MASK ((1<errCode - #if defined(__cplusplus) } /*extern "C"*/ #endif #endif /* ORG_FOSSIL_SCM_FSL_INTERNAL_H_INCLUDED */ @@ -22432,10 +21796,11 @@ #define FCLI_V(pfexp) FCLI_VN(1,pfexp) #if defined(__cplusplus) extern "C" { #endif + /** Result codes specific to the fcli API. */ enum fcli_rc_e { @@ -22443,15 +21808,10 @@ For use with fcli_flag_callback_f() implementations to indicate that the flag processor should check for that flag again. */ FCLI_RC_FLAG_AGAIN = FSL_RC_end + 1, /** - Returned from fcli_dispatch_commands() when the string returned - from fcli_next_arg() does not match any fcli_command in the list. -*/ -FCLI_RC_NO_CMD, -/** Returned from fcli_setup() if flag processing invokes the help system. This is an indication that the app should exit immediately with a 0 result code. */ FCLI_RC_HELP @@ -22848,17 +22208,10 @@ */ fsl_outputer outputer; } config; /** - Search paths. - */ - struct { - /** Search object for binaries. */ - fsl_pathfinder bins; - } paths; - /** For holding pre-this->f-init error state. Once this->f is initialized, all errors reported via fcli_err_set() are stored in that object's error state. */ fsl_error err; @@ -23111,15 +22464,13 @@ Convenience macro for using fcli_err_report2(). */ #define fcli_err_report(CLEAR) fcli_err_report2((CLEAR), __FILE__, __LINE__) /** - Peeks at or takes the next argument from the CLI args. If the - argument is true, it is removed from the args list. It is owned by + Peeks at or takes the next argument from the CLI args. If the + argument is true, it is removed from the args list. It is owned by fcli and will be freed when the app exits. - - Returns NULL if the args list is empty. */ FSL_EXPORT const char * fcli_next_arg(bool remove); /** If fcli.argv contains what looks like any flag arguments, this @@ -23178,19 +22529,15 @@ /** Expects an array of fcli_commands which contain a trailing sentry entry with a NULL name and callback. It searches the list for a command matching fcli_next_arg(). If found, it removes that argument from the list, calls the callback, and - returns its result. If no command is found FCLI_RC_NO_CMD is + returns its result. If no command is found FSL_RC_NOT_FOUND is returned, the argument list is not modified, and the error state is updated with a description of the problem and a list of all command names in cmdList. - The distinct FCLI return code aims to help clients distinguish - between fcli_dispatch_commands() failure and failures propagated - by successfully dispatched fcli_command fcli_command_f callbacks. - If reportErrors is true then on error this function outputs the error result but it keeps the error state in place for the downstream use. As a special case: when a command matches the first argument and @@ -23202,14 +22549,14 @@ FSL_EXPORT int fcli_dispatch_commands( fcli_command const * cmdList, bool reportErrors); /** Parse cmd->aliases for an alias (i.e., NUL-terminated string semantically - equivalent to cmd->name) matching arg. Return 0 if a match is found, else 1. - Note that cmd->aliases _must_ be double-NUL terminated. + equivalent to cmd->name) matching arg. Return true if found. Note that + cmd->aliases _must_ be double-NUL terminated. */ -FSL_EXPORT int fcli_cmd_aliascmp( fcli_command const * cmd, char const * arg ); +FSL_EXPORT bool fcli_cmd_aliascmp( fcli_command const * cmd, char const * arg ); /** A minor helper function intended to be passed the pending result code of the main() routine. This function outputs any pending error state in fcli. Returns one of EXIT_SUCCESS if mainRc is 0 @@ -23434,30 +22781,11 @@ Returns the "tail" part of the argv[0] string which was passed to fcli_setup(), or NULL if neither of those have yet been called. The "tail" part is the part immediately after the final '/' or '\\' character. */ -FSL_EXPORT char const * fcli_progname(void); - -/** - Searches the `$PATH` for a binary named fossil or fossil.exe. If - found, that string is returned. The bytes are guaranteed to be - alive until either the app ends or someone modifies - fcli.path.bins. If not found, it returns NULL. - - If errIfNotFound is true and no binary is found, it will update - fcli's error state with a description of the problem. - - If errIfNotFound is false, reportPolicy is ignored, else it is - interpreted as: - - - 0 = do not fcli_err_report() any errors and retain error state. - - <0 = report and retain the error state. - - >0 = report and clear error state. -*/ -FSL_EXPORT char const * fcli_fossil_binary(bool errIfNotFound, - int reportPolicy); +FSL_EXPORT char const * fcli_progname(); /** Color theme IDs for use with fcli_diff_colors. */ enum fcli_diff_colors_e{ @@ -23479,71 +22807,10 @@ dependend on the second argument. */ FSL_EXPORT void fcli_diff_colors(fsl_dibu_opt * const tgt, fcli_diff_colors_e theme); - -/** - Flags for use with fcli_sync(). -*/ -enum fcli_sync_e { -/** - Only perform one of the request operations if the fossil config - setting "fcli.autosync" or "autosync" is set, preferring the - former. "autosync" is fossil(1) standard flag for this, but clients - may use "fcli.autosync" to trump that without changing fossil's - behavior. -*/ -FCLI_SYNC_AUTO = 0x01, -/** Perform a pull (read-only) sync. */ -FCLI_SYNC_PULL = 0x02, -/** Perform a push (write-only) sync. */ -FCLI_SYNC_PUSH = 0x04, -/** Perform a full sync. */ -FCLI_SYNC_FULL = FCLI_SYNC_PULL | FCLI_SYNC_PUSH, -/** Perform a full sync, but only if autosync is enabled. */ -FCLI_SYNC_FULLAUTO = FCLI_SYNC_AUTO | FCLI_SYNC_FULL, -/** Redirect all stdout output from the fsl_system() to /dev/null. - This only works on non-Windows platforms. stderr output is - retained. */ -FCLI_SYNC_NO_STDOUT = 0x10, -/** - Works like FCLI_SYNC_NO_STDOUT, but also suppresses - stderr. - */ -FCLI_SYNC_NO_OUTPUT = 0x20 -}; - -/** - Uses fcli_fossil_binary() to find the fossil(1) binary and run a - sync operation, optionally either or both of push and pull - operations. - - Its argument may be a mask of any values from the fcli_sync_e - enum. - - Returns 0 if it does nothing or if fossil exits with a zero code. - Returns FSL_RC_NOT_A_REPO if no repo is opened. If the repo is - opened but has no "syncwith:" entries in its config table, no sync - is attempted and 0 is returned. - - If fossil cannot be found, returns FSL_RC_NOT_FOUND, else returns - as documented for fsl_system(). - - This function will fail with FSL_RC_LOCKED if a transaction is - currently open. Synching cannot succeed if a transaction is opened - because the repo is write-locked during that time. - - Though this routine does not generate any console output, - the proxied binary will, which may corrupt screen state of - any hypothetical curses-style libfossil clients. -*/ -//FSL_EXPORT int fcli_sync( bool push, bool pull ); - -FSL_EXPORT int fcli_sync( int ops ); - - /** @internal This function is intented for use in development of libfossil. It dumps the current state of cached SQL statements to fcli_printf(). @@ -23560,14 +22827,16 @@ primarily to keep fcli client code from having to access the related internal members directly. This is a no-op if fcli has not been initialized. */ FSL_EXPORT void fcli_dump_cache_metrics(void); + #if defined(__cplusplus) } /*extern "C"*/ #endif + #endif /* _ORG_FOSSIL_SCM_FCLI_H_INCLUDED_ */ /* end of file ./include/fossil-scm/cli.h */ /* start of file ./include/fossil-scm/deprecated.h */ ADDED signify/fnc-11-release.pub Index: signify/fnc-11-release.pub ================================================================== --- signify/fnc-11-release.pub +++ signify/fnc-11-release.pub @@ -0,0 +1,2 @@ +untrusted comment: fnc 0.11 public key +RWQIBA4rVoFFDlh8+FWU6Oeg79hcWqdRXv1cTOzrwTeSxKiIUhMd5rRE DELETED signify/fnc-13-release.pub Index: signify/fnc-13-release.pub ================================================================== --- signify/fnc-13-release.pub +++ signify/fnc-13-release.pub @@ -1,2 +0,0 @@ -untrusted comment: fnc 0.13 public key -RWRBkQNnoj1IBcgbH7YSJa2155xwSKh7+TGI3jW8KwraJ/GGgXtJPp0I Index: src/diff.c ================================================================== --- src/diff.c +++ src/diff.c @@ -57,17 +57,10 @@ #define SBS_LTEXT 1 /* Left text */ #define SBS_MID 2 /* Middle separator column */ #define SBS_RLINE 3 /* Right line number */ #define SBS_RTEXT 4 /* Right text */ -/* Return values for validate_scope_line() */ -#define VSL_FALSE 0 -#define VSL_TRUE 1 -#define VSL_PRIVATE 2 -#define VSL_PROTECT 3 -#define VSL_PUBLIC 4 - /* * ANSI escape codes: https://en.wikipedia.org/wiki/ANSI_escape_code */ #define ANSI_COLOR_BLACK(BOLD) ((BOLD) ? "\x1b[30m" : "\x1b[30m") #define ANSI_COLOR_RED(BOLD) ((BOLD) ? "\x1b[31;1m" : "\x1b[31m") @@ -91,113 +84,38 @@ #define ANSI_RESET_COLOR "\x1b[39;49m" #define ANSI_RESET_ALL "\x1b[0m" #define ANSI_RESET ANSI_RESET_ALL /* #define ANSI_BOLD ";1m" */ -struct diff_out_state { - fsl_output_f out; /* Output callback */ - void *state; /* State for this->out() */ - enum line_type *lines; /* Diff line type (e.g., minus, plus) */ - uint32_t nlines; /* Index into this->lines */ - int rc; /* Error reporting */ - char ansi; /* ANSI colour code */ - struct { - const fsl_buffer *file; /* Diffed file */ - char *sig; /* Matching function */ - char *spec; /* C++ specifier */ - uint32_t lastmatch; /* Match line index */ - uint32_t lastline; /* Last line scanned */ - fsl_size_t offset; /* Match byte offset */ - } proto; -}; -static const struct diff_out_state diff_out_state_ctor = - { NULL, NULL, NULL, 0U, 0, '\0', { NULL, NULL, NULL, 0U, 0U, 0U } }; - -struct sbsline { - struct diff_out_state *output; - fsl_buffer *cols[5]; /* Pointers to output columns */ - const char *tag; /* tag */ - const char *tag2; /* tag */ - int idx; /* Write tag before idx */ - int end; /* Close tag before end */ - int idx2; /* Write tag2 before idx2 */ - int end2; /* Close tag2 before end2 */ - int width; /* Max column width in diff */ - bool esc; /* Escape html characters */ - void *regex; /* Colour matching lines */ -}; - -static int diff_blobs(fsl_buffer const *, fsl_buffer const *, - fsl_output_f, void *, enum line_type **, uint32_t *, - /* void *regex, */ uint16_t, short, int, int **); -static int fnc_output_f_diff_out(void *, void const *, fsl_size_t); -static int diff_outf(struct diff_out_state *, char const *, ... ); -static int diff_out(struct diff_out_state * const, void const *, - fsl_int_t); -static int validate_scope_line(const char *); -static int match_hunk_function(struct diff_out_state *const, - uint32_t); -static int buffer_copy_lines_from(fsl_buffer *const, - const fsl_buffer *const, fsl_size_t *, fsl_size_t, - fsl_size_t); -/* static uint64_t fnc_diff_flags_convert(int); */ -/* static int diff_context_lines(uint64_t); */ -static int match_dline(fsl_dline *, fsl_dline *); -static bool find_lcs(const char *z, int, const char *, int, int *); -static int unidiff(fsl__diff_cx *, struct diff_out_state *, - void *, uint16_t, uint64_t); -static int unidiff_lineno(struct diff_out_state *, int, int, bool); -static int unidiff_txt( struct diff_out_state *const, char, - fsl_dline *, int, void *); -static int sbsdiff(fsl__diff_cx *, struct diff_out_state *, - void *, uint16_t, uint64_t); -static int max_sbs_width(fsl__diff_cx *, int *, unsigned short, - uint16_t); -static int sbsdiff_width(uint64_t); -static int sbsdiff_separator(struct sbsline *, int, int); -static int sbsdiff_lineno(struct sbsline *, int, int); -static void sbsdiff_shift_left(struct sbsline *, const char *); -static void sbsdiff_simplify_line(struct sbsline *, const char *); -static int sbsdiff_column(struct diff_out_state *, - fsl_buffer const *, int); -static int sbsdiff_txt(struct sbsline *, fsl_dline *, int); -static int sbsdiff_newline(struct sbsline *); -static int sbsdiff_space(struct sbsline *, int, int); -static int sbsdiff_marker(struct sbsline *, const char *, - const char *); -static int sbsdiff_close_gap(int *); -static unsigned char *sbsdiff_align(fsl_dline *, int, fsl_dline *, int); -static int sbsdiff_write_change(struct sbsline *, fsl_dline *, - int, fsl_dline *, int); int fnc_diff_text_raw(fsl_buffer const *blob1, fsl_buffer const *blob2, int flags, int **out) { - return diff_blobs(blob1, blob2, NULL, NULL, NULL, NULL, 0, 0, + return fnc_diff_blobs(blob1, blob2, NULL, NULL, NULL, NULL, 0, 0, flags, out); } int fnc_diff_text_to_buffer(fsl_buffer const *blob1, fsl_buffer const *blob2, fsl_buffer *out, enum line_type **lines, uint32_t *nlines, short context, short sbswidth, int flags) { return (blob1 && blob2 && out) ? - diff_blobs(blob1, blob2, fsl_output_f_buffer, out, lines, + fnc_diff_blobs(blob1, blob2, fsl_output_f_buffer, out, lines, nlines, context, sbswidth, flags, NULL) : FSL_RC_MISUSE; } int fnc_diff_text(fsl_buffer const *blob1, fsl_buffer const *blob2, - fsl_output_f out, void *state, enum line_type **lines, uint32_t *nlines, - short context, short sbswidth, int flags) + fsl_output_f out, void *state, short context, short sbswidth, int flags) { - return diff_blobs(blob1, blob2, out, state, lines, nlines, + return fnc_diff_blobs(blob1, blob2, out, state, NULL, NULL, context, sbswidth, flags, NULL); } /* + * * Diff two arbitrary blobs and either stream output to the third argument * or return an array of copy/delete/insert triples via the final argument. * The third XOR final argument must be set. * * If the third argument is not NULL: @@ -209,12 +127,12 @@ * array of copy/delete/insert triples. Ownership is transfered to the caller, * who must eventually dispose of it with fsl_free(). * * Return 0 on success, any number of other codes on error. */ -static int -diff_blobs(fsl_buffer const *blob1, fsl_buffer const *blob2, +int +fnc_diff_blobs(fsl_buffer const *blob1, fsl_buffer const *blob2, fsl_output_f out, void *state, enum line_type **lines, uint32_t *nlines, /* void *regex, */ uint16_t context, short sbswidth, int flags, int **rawdata) { fsl__diff_cx c = fsl__diff_cx_empty; @@ -274,43 +192,41 @@ /* fsl__dump_triples(&c, __FILE__, __LINE__); */ /* DEBUG */ if (!FLAG_CHK(flags, FNC_DIFF_NOOPT)) fsl__diff_optimize(&c); /* fsl__dump_triples(&c, __FILE__, __LINE__); */ /* DEBUG */ - if (FLAG_CHK(flags, FNC_DIFF_SIDEBYSIDE)) { - rc = max_sbs_width(&c, &flags, sbswidth, context); + /* Encode longest line length in flags to set max sbs diff width */ + if ((!sbswidth && FLAG_CHK(flags, FNC_DIFF_SIDEBYSIDE)) || + sbswidth < 0) { + flags |= ((int)(sbswidth & 0xFFFF)) << 8; + FLAG_SET(flags, FNC_DIFF_SIDEBYSIDE); + rc = max_sbs_width(&c, &flags, context); if (rc) goto end; } if (out) { /* - * Compute a unified or side-by-side diff. + * Compute a context or side-by-side diff. * XXX Missing regex support. */ - struct diff_out_state dos = diff_out_state_ctor; - + struct diff_out_state dos = diff_out_state_empty; + if (FLAG_CHK(flags, FNC_DIFF_PROTOTYPE)) { + memset(&dos.proto, 0, sizeof(dos.proto)); + dos.proto.file = blob1; + } dos.out = out; dos.state = state; - if (lines && nlines) { - dos.lines = *lines; - dos.nlines = *nlines; - } + dos.lines = *lines; + dos.nlines = *nlines; dos.ansi = !!(FLAG_CHK(flags, FNC_DIFF_ANSI_COLOR)); - if (FLAG_CHK(flags, FNC_DIFF_PROTOTYPE)) - dos.proto.file = blob1; - if (FLAG_CHK(flags, FNC_DIFF_SIDEBYSIDE)) rc = sbsdiff(&c, &dos, NULL /*regex*/, context, flags); else rc = unidiff(&c, &dos, NULL /*regex*/, context, flags); - - if (lines && nlines) { - *lines = dos.lines; /* realloc'd */ - *nlines = dos.nlines; - } - fsl_free(dos.proto.sig); + *lines = dos.lines; /* realloc'd */ + *nlines = dos.nlines; } else if (rawdata) { /* Return array of COPY/DELETE/INSERT triples. */ *rawdata = c.aEdit; c.aEdit = NULL; } @@ -320,25 +236,22 @@ fsl_free(c.aEdit); return rc; } /* - * Find the longest line in the file and, if longer than the - * currently set sbs width, encode the length in *flags. + * Find the longest line in the file and encode the length in *flags. */ -static int -max_sbs_width(fsl__diff_cx *c, int *flags, unsigned short width, uint16_t ctxt) +int +max_sbs_width(fsl__diff_cx *c, int *flags, uint16_t ctxt) { int i, j, k; - int endl, endr, start; /* Start and end of l+r hunks */ - unsigned short sz; /* Size of each line */ + int endl, endr, start; /* Start and end of l+r chunks */ + unsigned short sz, sbswidth; /* Size of each line and max width */ j = endl = endr = start = 0; k = c->nEdit; - - if (width < 0) - width = 80; + sbswidth = (*flags & FNC_DIFF_SIDEBYSIDE) ? sbsdiff_width(*flags) : 0; /* Find minimal copy/delete/insert triples. */ while (k > 2 && c->aEdit[k - 1] == 0 && c->aEdit[k - 2] == 0) k -= 3; @@ -349,18 +262,18 @@ */ do { int e, e2, s; /* - * Compute number of lines in both the left and right hunk. - * Both hunks will have the _same_ start line, but may have + * Compute number of lines in both the left and right chunk. + * Both chunks will have the _same_ start line, but may have * distinct end lines. */ - start += c->aEdit[j]; /* Hunk start line */ + start += c->aEdit[j]; /* Chunk start line */ endl = endr = start; - endl += MAX(c->aEdit[j + 1], 0); /* End of left side hunk */ - endr += MAX(c->aEdit[j + 2], 0); /* End of right side hunk */ + endl += MAX(c->aEdit[j + 1], 0); /* End of left side chunk */ + endr += MAX(c->aEdit[j + 2], 0); /* End of right side chunk */ s = MAX(start - ctxt, 0); /* Include leading ctxt */ e = MIN(endl + ctxt, c->nFrom); /* L include trailing ctxt */ e2 = MIN(endr + ctxt, c->nTo); /* R include trailing ctxt */ /* @@ -368,32 +281,32 @@ * columns the width of the longest line from either side. * XXX Consider keeping max width of both sides to make each * column only as wide as its longest line; we can set a min * default in diffs with added/removed files. */ - for (i = s; i < e || i < e2; ++i) { + for (i = s; sbswidth && (i < e || i < e2); ++i) { if (i < e) { /* Left side line length */ - sz = etcount(c->aFrom[i].z, c->aFrom[i].n); - if (sz > width) - width = sz; + sz = c->aFrom[i].n; + if (sz > sbswidth) + sbswidth = etcount(c->aFrom[i].z, sz); } if (i < e2) { /* Right side line length */ - sz = etcount(c->aTo[i].z, c->aTo[i].n); - if (sz > width) - width = sz; + sz = c->aTo[i].n; + if (sz > sbswidth) + sbswidth = etcount(c->aTo[i].z, sz); } } if (i >= c->nFrom && i >= c->nTo) break; - /* Account for insertions before adding lines to next hunk. */ + /* Account for insertions before adding lines to next chunk. */ start += c->aEdit[j + 2]; - } while ((j += 3) < k); + } while ((j += 3) < k - 3); - *flags |= ((int)(width & 0xFF)) << 16; /* Encode max width */ + *flags |= ((int)(sbswidth & 0xFFF)) << 16; /* Encode max width */ return FSL_RC_OK; } /* @@ -403,17 +316,14 @@ unsigned short etcount(const char *str, unsigned short n) { unsigned short c = 0; - if (n < 0) - n = fsl_strlen(str); - while (str && (str[c] != '\n' && str[c] != '\0')) { /* Expand tabs */ if (str[c] == '\t') - n += (8 - ((c - (c ? 1 : 0)) % 8)) - 1; + n += 8 - (c % 8); if (UTF_CONT(str[c])) --n; ++c; } @@ -425,12 +335,11 @@ * 64-bit bitmask used by the DIFF_xxx macros because fossil(1) uses the macro * approach and a low-level encoding of data in the bitmask (e.g., the context * lines count). The public API hides the lower-level flags and allows the * internal API to take care of the encoding. */ -#if 0 -static uint64_t +uint64_t fnc_diff_flags_convert(int mask) { uint64_t rc = 0U; #define DO(f) if ((mask & f) == f) rc |= (((uint64_t)f) << 24) @@ -446,11 +355,10 @@ DO(FNC_DIFF_NOOPT); DO(FNC_DIFF_INVERT); #undef DO return rc; } -#endif static inline int min(int a, int b) { return a < b ? a : b; @@ -464,11 +372,11 @@ * 1. Remove leading and trailing whitespace. * 2. Truncate both strings to at most 250 characters * 3. Find the length of the longest common subsequence * 4. Longer common subsequences yield lower scores. */ -static int +int match_dline(fsl_dline *lline, fsl_dline *rline) { const char *l, *r; /* Left and right strings */ int nleft, nright; /* Bytes in l and r */ int avg; /* Average length of l and r */ @@ -563,11 +471,11 @@ * lcs[3] = end of the common segment in right * * n.b. This computation is for display purposes only and does not have to be * optimal or exact. */ -static bool +bool find_lcs(const char *left, int nleft, const char *right, int nright, int *lcs) { const unsigned char *l, *r; /* Left and right strings */ unsigned int probe; /* Probe to compare target */ @@ -634,11 +542,11 @@ } /* * Send src to o->out(). If n is negative, use strlen() to determine length. */ -static int +int diff_out(struct diff_out_state *const o, void const *src, fsl_int_t n) { return o->rc = n ? o->out(o->state, src, n < 0 ? strlen((char const *)src) : (size_t)n) : 0; @@ -646,11 +554,11 @@ /* * Starting from seek lines, copy n lines from src to dst. The seek starts * from offset bytes into src->mem. This routine does _not_ modify src. */ -static int +int buffer_copy_lines_from(fsl_buffer *const dst, const fsl_buffer *const src, fsl_size_t *offset, fsl_size_t seek, fsl_size_t n) { const char *z = (const char *)src->mem; fsl_size_t idx = *offset, ln = 0, start = 0; @@ -676,98 +584,77 @@ *offset = start; return rc; } /* - * If str is a valid scope line (i.e., function prototype; class, namespace, - * enum, struct, or union declaration; or C++ class specifier) return the - * corresponding int value. GNU C and MSVC allow '$' in identifier names: - * https://gcc.gnu.org/onlinedocs/gcc/Dollar-Signs.html - * https://docs.microsoft.com/en-us/cpp/cpp/identifiers-cpp - */ -static int -validate_scope_line(const char *str) -{ - if (fsl_isalpha(*str) || *str == '_' || *str == '$') { - if (strchr(str, '(') || strchr(str, '{') || - strstr(str, "struct ") || strstr(str, "union ") || - strstr(str, "enum ") || strstr(str, "class ") || - strstr(str, "namespace ")) - return VSL_TRUE; - if (starts_with(str, "private")) - return VSL_PRIVATE; - if (starts_with(str, "protected")) - return VSL_PROTECT; - if (starts_with(str, "public")) - return VSL_PUBLIC; - } - - return VSL_FALSE; -} - -/* - * Back scan the diffed file dst->proto.file from line pos, which is the first - * changed line of the current hunk, for the enclosing function in which the - * hunk resides. Point dst->proto.sig to the heap-allocated matching line, - * which the caller must eventually free. Return FSL_RC_OK on success. - */ -static int -match_hunk_function(struct diff_out_state *const dst, uint32_t pos) + * Scan the diffed file dst->proto->file from line pos preceding the start of + * the current chunk for the enclosing function in which the change resides. + * Return first match. + */ +char * +match_chunk_function(struct diff_out_state *const dst, uint32_t pos) { fsl_buffer buf = fsl_buffer_empty; - char *line = NULL; + const char *line; + char *spec = NULL; fsl_size_t offset; uint32_t last = dst->proto.lastline; - int rc = FSL_RC_OK; dst->proto.lastline = pos; offset = dst->proto.offset; /* Begin seek from last match */ while (pos > 1 && pos > last) { - rc = buffer_copy_lines_from(&buf, dst->proto.file, &offset, + buffer_copy_lines_from(&buf, dst->proto.file, &offset, pos - dst->proto.lastmatch, 1); - if (rc) - return rc; - line = fsl_buffer_take(&buf); - if (line == NULL) - continue; - - switch (validate_scope_line(line)) { - case VSL_FALSE: - break; - case VSL_PRIVATE: - if (!dst->proto.spec) - dst->proto.spec = " (private)"; - break; - case VSL_PROTECT: - if (!dst->proto.spec) - dst->proto.spec = " (protected)"; - break; - case VSL_PUBLIC: - if (!dst->proto.spec) - dst->proto.spec = " (public)"; - break; - case VSL_TRUE: - fsl_free(dst->proto.sig); - dst->proto.sig = line; - /* - * It's expensive to seek from the start of the - * file for each hunk when diffing large files, - * so save offset and line index of this match. - */ - dst->proto.lastmatch = pos; - dst->proto.offset = offset; - return FSL_RC_OK; - } - + line = fsl_buffer_cstr(&buf); + /* + * GNU C and MSVC allow '$' in identifier names. + * https://gcc.gnu.org/onlinedocs/gcc/Dollar-Signs.html + * https://docs.microsoft.com/en-us/cpp/cpp/identifiers-cpp + */ + if (line) { + if (fsl_isalpha(line[0]) || line[0] == '_' || + line[0] == '$'){ + if (starts_with(line, "private:")) { + if (!spec) + spec = " (private)"; + } else if (starts_with(line, "protected:")) { + if (!spec) + spec = " (protected)"; + } else if (starts_with(line, "public:")) { + if (!spec) + spec = " (public)"; + } else { + /* + * Don't exceed 80 cols: chunk header + * consumes ~25, so cap signature at 55. + */ + char *sig = fsl_mprintf("%s%s", line, + spec ? spec : ""); + fsl_free(dst->proto.signature); + dst->proto.signature = + fsl_mprintf("%.55s", sig); + /* + * It's expensive to seek from the start + * of the file for each chunk when + * diffing large files, so save offset + * and line index of this match. + */ + dst->proto.lastmatch = pos; + dst->proto.offset = offset; + fsl_free(sig); + fsl_buffer_clear(&buf); + return dst->proto.signature; + } + } + } /* No match, revert to last offset. */ - fsl_free(line); offset = dst->proto.offset; + fsl_buffer_clear(&buf); --pos; } - - return FSL_RC_OK; + return dst->proto.lastmatch > 0 ? dst->proto.signature : NULL; } /* * Render the diff triples array in cx->aEdit as a side-by-side diff in out. * cx Raw diff data @@ -774,11 +661,11 @@ * out Side-by-side diff representation * regex Show changes matching this regex * context Number of context lines * flags Flags controlling the diff */ -static int +int sbsdiff(fsl__diff_cx *cx, struct diff_out_state *dst, void *regex, uint16_t context, uint64_t flags) { fsl_dline *l, *r; /* Left and right side of diff */ fsl_buffer sbscols[5] = { @@ -785,11 +672,11 @@ fsl_buffer_empty_m, fsl_buffer_empty_m, fsl_buffer_empty_m, fsl_buffer_empty_m, fsl_buffer_empty_m }; struct sbsline s; /* Output line buffer */ - static int hunks = 0; /* Number of hunks so far processed */ + static int chunks = 0; /* Number of chunks so far processed */ int li, ri; /* Index of next line in l[] and r[] */ int *c; /* copy/delete/insert triples */ int ci; /* Index into c[] */ int nc; /* number of c[] triples to process */ int max_ci; /* Maximum value for ci */ @@ -891,16 +778,13 @@ nright += c[ci + i * 3]; } /* Draw separator between blocks except the first. */ if (showsep) { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_SEPARATOR); - if (rc) - goto end; - } + rc = add_line_type(lines, nlines, LINE_DIFF_SEPARATOR); + if (rc) + goto end; if (s.esc) { char ln[10]; fsl_snprintf(ln, sizeof(ln), "%d", li + skip + 1); rc = sbsdiff_separator(&s, fsl_strlen(ln), @@ -924,44 +808,36 @@ s.width * 2 + 16, '.'); if (rc) goto end; } showsep = true; - ++hunks; + ++chunks; if (s.esc) rc = fsl_buffer_appendf(s.cols[SBS_LLINE], - "", - hunks); + "", + chunks); /* Show the initial common area */ li += skip; ri += skip; ntotal = c[ci] - skip; - for (j = 0; j < ntotal; j++) { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_CONTEXT); - if (rc) - goto end; - } + for (j = 0; !rc && j < ntotal; j++) { + rc = add_line_type(lines, nlines, LINE_DIFF_CONTEXT); + if (!rc) + rc = sbsdiff_lineno(&s, li + j, SBS_LLINE); s.idx = s.end = -1; - rc = sbsdiff_lineno(&s, li + j, SBS_LLINE); - if (rc) - goto end; - rc = sbsdiff_txt(&s, &l[li + j], SBS_LTEXT); - if (rc) - goto end; - rc = sbsdiff_marker(&s, " ", ""); - if (rc) - goto end; - rc = sbsdiff_lineno(&s, ri + j, SBS_RLINE); - if (rc) - goto end; - rc = sbsdiff_txt(&s, &r[ri + j], SBS_RTEXT); - if (rc) - goto end; - } + if (!rc) + rc = sbsdiff_txt(&s, &l[li + j], SBS_LTEXT); + if (!rc) + rc = sbsdiff_marker(&s, " ", ""); + if (!rc) + rc = sbsdiff_lineno(&s, ri + j, SBS_RLINE); + if (!rc) + rc = sbsdiff_txt(&s, &r[ri + j], SBS_RTEXT); + } + if (rc) + goto end; li += ntotal; ri += ntotal; /* Show the differences */ for (i = 0; i < nc; i++) { @@ -986,11 +862,11 @@ nright); if (!alignment) { rc = FSL_RC_OOM; goto end; } - for (j = 0; nleft + nright > 0; j++) { + for (j = 0; !rc && nleft + nright > 0; j++) { char tag[30] = "", sizeof(tag)); s.tag = tag; s.end = LENGTH(&l[li]); rc = sbsdiff_txt(&s, &l[li], SBS_LTEXT); - if (rc) - goto end_align; - if (lines && nlines) { + if (!rc) rc = add_line_type(lines, nlines, LINE_DIFF_MINUS); - if (rc) - goto end_align; - } + if (rc) + goto end_align; rc = sbsdiff_marker(&s, " <", "<"); if (rc) goto end_align; rc = sbsdiff_newline(&s); if (rc) @@ -1036,18 +909,15 @@ s.idx = 0; fsl_strlcat(tag, "add\">", sizeof(tag)); s.tag = tag; s.end = LENGTH(&r[ri]); rc = sbsdiff_txt(&s, &r[ri], SBS_RTEXT); - if (rc) - goto end_align; - if (lines && nlines) { + if (!rc) rc = add_line_type(lines, nlines, LINE_DIFF_PLUS); - if (rc) - goto end_align; - } + if (rc) + goto end_align; assert(nright > 0); nright--; ri++; break; case 3: @@ -1060,11 +930,11 @@ nleft--; nright--; li++; ri++; break; - default: + default: { /* Delete left and insert right */ rc = sbsdiff_lineno(&s, li, SBS_LLINE); if (rc) goto end_align; s.idx = 0; @@ -1073,18 +943,15 @@ s.end = LENGTH(&l[li]); rc = sbsdiff_txt(&s, &l[li], SBS_LTEXT); if (rc) goto end_align; rc = sbsdiff_marker(&s, " | ", "|"); - if (rc) - goto end_align; - if (lines && nlines) { + if (!rc) rc = add_line_type(lines, nlines, LINE_DIFF_EDIT); - if (rc) - goto end_align; - } + if (rc) + goto end_align; rc = sbsdiff_lineno(&s, ri, SBS_RLINE); if (rc) goto end_align; s.idx = 0; s.tag = ""; @@ -1096,31 +963,31 @@ nright--; li++; ri++; break; } + } } end_align: fsl_free(alignment); + if (rc) + goto end; if (i < nc - 1) { ntotal = c[ci + i * 3 + 3]; - for (j = 0; j < ntotal; j++) { + for (j = 0; !rc && j < ntotal; j++) { rc = sbsdiff_lineno(&s, li + j, SBS_LLINE); + s.idx = s.end = -1; if (rc) goto end; - s.idx = s.end = -1; rc = sbsdiff_txt(&s, &l[li + j], SBS_LTEXT); - if (rc) - goto end; - if (lines && nlines) { + if (!rc) rc = add_line_type(lines, nlines, LINE_DIFF_CONTEXT); - if (rc) - goto end; - } + if (rc) + goto end; rc = sbsdiff_marker(&s, " ", ""); if (rc) goto end; rc = sbsdiff_lineno(&s, ri + j, SBS_RLINE); @@ -1139,31 +1006,23 @@ /* Show the final common area */ assert(nc == i); ntotal = c[ci + nc * 3]; if (ntotal > context) ntotal = context; - for (j = 0; j < ntotal; j++) { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_CONTEXT); - if (rc) - goto end; - } + for (j = 0; !rc && j < ntotal; j++) { + rc = add_line_type(lines, nlines, LINE_DIFF_CONTEXT); + if (!rc) + rc = sbsdiff_lineno(&s, li + j, SBS_LLINE); s.idx = s.end = -1; - rc = sbsdiff_lineno(&s, li + j, SBS_LLINE); - if (rc) - goto end; - rc = sbsdiff_txt(&s, &l[li + j], SBS_LTEXT); - if (rc) - goto end; - rc = sbsdiff_marker(&s, " ", ""); - if (rc) - goto end; - rc = sbsdiff_lineno(&s, ri + j, SBS_RLINE); - if (rc) - goto end; - rc = sbsdiff_txt(&s, &r[ri + j], SBS_RTEXT); + if (!rc) + rc = sbsdiff_txt(&s, &l[li + j], SBS_LTEXT); + if (!rc) + rc = sbsdiff_marker(&s, " ", ""); + if (!rc) + rc = sbsdiff_lineno(&s, ri + j, SBS_RLINE); + if (!rc) + rc = sbsdiff_txt(&s, &r[ri + j], SBS_RTEXT); if (rc) goto end; } } /* diff triplet loop */ @@ -1170,18 +1029,14 @@ assert(!rc); if (s.esc && (s.cols[SBS_LLINE]->used > 0)) { rc = diff_out(dst, "\n", -1); - if (rc) - goto end; - for (i = SBS_LLINE; i <= SBS_RTEXT; i++) { + for (i = SBS_LLINE; !rc && i <= SBS_RTEXT; i++) rc = sbsdiff_column(dst, s.cols[i], i); - if (rc) - goto end; - } - rc = diff_out(dst, "
\n", -1); + if (!rc) + rc = diff_out(dst, "\n", -1); } end: for (i = 0; i < (int)nitems(sbscols); ++i) fsl_buffer_clear(&sbscols[i]); return rc; @@ -1193,16 +1048,16 @@ * out Side-by-side diff representation * regex Show changes matching this regex * context Number of context lines * flags Flags controlling the diff */ -static int +int unidiff(fsl__diff_cx *cx, struct diff_out_state *dst, void *regex, uint16_t context, uint64_t flags) { fsl_dline *l, *r; /* Left and right side of diff */ - static int hunks = 0; /* Number of hunks so far processed */ + static int chunks = 0; /* Number of chunks so far processed */ int li, ri; /* Index of next line in l[] and r[] */ int *c; /* copy/delete/insert triples */ int ci; /* Index into c[] */ int nc; /* number of c[] triples to process */ int max_ci; /* Maximum value for ci */ @@ -1293,49 +1148,35 @@ /* * Show the header for this block, or if we are doing a modified * unified diff that contains line numbers, show the separator * from the previous block. */ - ++hunks; + ++chunks; if (showln) { if (!showsep) showsep = 1; /* Don't show a top divider */ - else if (html) { + else if (html) rc = diff_outf(dst, "%.*c\n", 80, '.'); - if (rc) - return rc; - } else { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_SEPARATOR); - if (rc) - return rc; - } - rc = diff_outf(dst, "%.95c\n", '.'); - if (rc) - return rc; - } - if (html) { + else { + rc = add_line_type(lines, nlines, + LINE_DIFF_SEPARATOR); + if (!rc) + rc = diff_outf(dst, "%.95c\n", '.'); + } + if (!rc && html) rc = diff_outf(dst, - "", - hunks); - if (rc) - return rc; - } + "", + chunks); } else { - const char *ansi1 = ""; - const char *ansi2 = ""; - const char *ansi3 = ""; - - if (html) { + char const *ansi1 = ""; + char const *ansi2 = ""; + char const *ansi3 = ""; + if (html) rc = diff_outf(dst, ""); - if (rc) - return rc; - } #if 0 /* Turns out this just confuses the output */ else if (dst->ansi) { ansi1 = ANSI_DIFF_RM(0); ansi2 = ANSI_DIFF_ADD(0); @@ -1343,145 +1184,101 @@ } #endif /* * If the patch changes an empty file or results in * an empty file, the block header must use 0,0 as - * position indicator and not 1,0. Otherwise, patch(1) + * position indicator and not 1,0. Otherwise, patch * would be confused and may reject the diff. */ - rc = diff_outf(dst, "@@ %s-%d,%d %s+%d,%d%s @@", - ansi1, nleft ? li + skip + 1 : 0, nleft, - ansi2, nright ? ri + skip + 1 : 0, nright, ansi3); - if (rc) - return rc; - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_HUNK); - if (rc) - return rc; - } - - if (html) { - rc = diff_outf(dst, ""); - if (rc) - return rc; - } - - if (proto && li + skip > 1) { - int n = 55; - - rc = match_hunk_function(dst, - li + skip + context); - if (rc) - return rc; - - n -= fsl_strlen(dst->proto.spec); - if (dst->proto.sig) { - rc = diff_outf(dst, " %.*s", n, - dst->proto.sig); - if (rc) - return rc; - } - if (dst->proto.spec) { - rc = diff_outf(dst, " %s", - dst->proto.spec); - if (rc) - return rc; - } - } - rc = diff_out(dst, "\n", 1); - if (rc) - return rc; - } + if (!rc) { + rc = diff_outf(dst, "@@ %s-%d,%d %s+%d,%d%s @@", + ansi1, nleft ? li + skip + 1 : 0, nleft, + ansi2, nright ? ri + skip + 1 : 0, + nright, ansi3); + if (!rc) + rc = add_line_type(lines, nlines, + LINE_DIFF_CHUNK); + } + if (!rc) { + if (html) + rc = diff_outf(dst, "
"); + + if (proto && li + skip > 1) { + char *f = match_chunk_function(dst, + (li + skip) - 1); + if (f != NULL) + rc = diff_outf(dst, " %s", f); + } + if (!rc) + rc = diff_out(dst, "\n", 1); + } + } + if (rc) + return rc; /* Show the initial common area */ li += skip; ri += skip; ntotal = c[ci] - skip; - for (j = 0; j < ntotal; j++) { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_CONTEXT); - if (rc) - return rc; - } - if (showln) { + for (j = 0; !rc && j < ntotal; j++) { + rc = add_line_type(lines, nlines, LINE_DIFF_CONTEXT); + if (!rc && showln) rc = unidiff_lineno(dst, li + j + 1, ri + j + 1, html); - if (rc) - return rc; - } - rc = unidiff_txt(dst, ' ', &l[li + j], html, 0); - if (rc) - return rc; + if (!rc) + rc = unidiff_txt(dst, ' ', &l[li + j], html, 0); } + if (rc) + return rc; li += ntotal; ri += ntotal; /* Show the differences */ for (i = 0; i < nc; i++) { ntotal = c[ci + i * 3 + 1]; - for (j = 0; j < ntotal; j++) { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_MINUS); - if (rc) - return rc; - } - if (showln) { + for (j = 0; !rc && j < ntotal; j++) { + rc = add_line_type(lines, nlines, + LINE_DIFF_MINUS); + if (!rc && showln) rc = unidiff_lineno(dst, li + j + 1, 0, html); - if (rc) - return rc; - } - rc = unidiff_txt(dst, '-', &l[li + j], - html, regex); - if (rc) - return rc; - } + if (!rc) + rc = unidiff_txt(dst, '-', &l[li + j], + html, regex); + } + if (rc) + return rc; li += ntotal; ntotal = c[ci + i * 3 + 2]; - for (j = 0; j < ntotal; j++) { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_PLUS); - if (rc) - return rc; - } - if (showln) { + for (j = 0; !rc && j < ntotal; j++) { + rc = add_line_type(lines, nlines, + LINE_DIFF_PLUS); + if (!rc && showln) rc = unidiff_lineno(dst, 0, ri + j + 1, html); - if (rc) - return rc; - } - rc = unidiff_txt(dst, '+', &r[ri + j], - html, regex); - if (rc) - return rc; - } + if (!rc) + rc = unidiff_txt(dst, '+', &r[ri + j], + html, regex); + } + if (rc) + return rc; ri += ntotal; if (i < nc - 1) { ntotal = c[ci + i * 3 + 3]; - for (j = 0; j < ntotal; j++) { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_CONTEXT); - if (rc) - return rc; - } - if (showln) { + for (j = 0; !rc && j < ntotal; j++) { + rc = add_line_type(lines, nlines, + LINE_DIFF_CONTEXT); + if (!rc && showln) rc = unidiff_lineno(dst, li + j + 1, ri + j + 1, html); - if (rc) - return rc; - } - rc = unidiff_txt(dst, ' ', - &l[li + j], html, 0); - if (rc) - return rc; - } + if (!rc) + rc = unidiff_txt(dst, ' ', + &l[li + j], html, 0); + } + if (rc) + return rc; ri += ntotal; li += ntotal; } } @@ -1488,59 +1285,52 @@ /* Show the final common area */ assert(nc==i); ntotal = c[ci + nc * 3]; if (ntotal > context) ntotal = context; - for (j = 0; j < ntotal; j++) { - if (lines && nlines) { - rc = add_line_type(lines, nlines, - LINE_DIFF_CONTEXT); - if (rc) - return rc; - } - if (showln) { + for (j = 0; !rc && j < ntotal; j++) { + rc = add_line_type(lines, nlines, LINE_DIFF_CONTEXT); + if (!rc && showln) rc = unidiff_lineno(dst, li + j + 1, ri + j + 1, html); - if (rc) - return rc; - } - rc = unidiff_txt(dst, ' ', &l[li + j], html, 0); - if (rc) - return rc; + if (!rc) + rc = unidiff_txt(dst, ' ', &l[li + j], html, 0); } } /* _big_ for() loop */ + fsl_free(dst->proto.signature); return rc; } /* Extract the number of context lines from flags. */ -#if 0 -static int +int diff_context_lines(uint64_t flags) { - int n = flags >> 32; + int n = flags & FNC_DIFF_CONTEXT_MASK; if (!n && !FLAG_CHK(flags, FNC_DIFF_CONTEXT_EX)) n = 5; return n; } -#endif /* - * Extract column width from flags for side-by-side diff. - * Return appropriate default if no width is specified. + * Extract column width for side-by-side diff from flags. Return appropriate + * default if no width is specified. */ -static int +int sbsdiff_width(uint64_t flags) { - int w = (flags & FNC_DIFF_WIDTH_MASK) / (FNC_DIFF_CONTEXT_MASK + 1); + int w = (flags & (FNC_DIFF_WIDTH_MASK - 0xf)) / FNC_DIFF_CONTEXT_MASK; + + if (!w) + w = 80; - return w ? w : 80; + return w; } /* Append a separator line of length len to column col. */ -static int +int sbsdiff_separator(struct sbsline *dst, int len, int col) { char ch = '.'; if (len < 1) { @@ -1554,32 +1344,32 @@ /* * fsl_output_f() implementation for use with diff_outf(). State must be a * struct diff_out_state *. */ -static int -fnc_output_f_diff_out(void *state, void const *src, fsl_size_t n) +int +fsl_output_f_diff_out(void *state, void const *src, fsl_size_t n) { struct diff_out_state *const dst = (struct diff_out_state *)state; return dst->rc = dst->out(dst->state, src, n); } -static int +int diff_outf(struct diff_out_state *dst, char const *fmt, ...) { va_list va; va_start(va,fmt); - fsl_appendfv(fnc_output_f_diff_out, dst, fmt, va); + fsl_appendfv(fsl_output_f_diff_out, dst, fmt, va); va_end(va); return dst->rc; } /* Append a column to the final output blob. */ -static int +int sbsdiff_column(struct diff_out_state *dst, fsl_buffer const *content, int col) { return diff_outf(dst, "
\n" "
\n"
@@ -1597,11 +1387,11 @@
  * SBS_LTEXT and esc is false.
  *
  * This comment contains multibyte unicode characters (�, �, �) in order
  * to test the ability of the diff code to handle such characters.
  */
-static int
+int
 sbsdiff_txt(struct sbsline *dst, fsl_dline *dline, int col)
 {
 	fsl_buffer	*o = dst->cols[col];
 	const char	*str = dline->z;
 	int		 n = dline->n;
@@ -1679,11 +1469,11 @@
 /*
  * Append newlines to columns corresponding to sbs diff format.
  *   html: all columns
  *   text: right column only
  */
-static int
+int
 sbsdiff_newline(struct sbsline *dst)
 {
 	int i, rc = FSL_RC_OK;
 
 	for (i = dst->esc ? SBS_LLINE : SBS_RTEXT; !rc && i <= SBS_RTEXT; i++)
@@ -1691,25 +1481,25 @@
 
 	return rc;
 }
 
 /* Append n spaces to column col in the sbs diff. */
-static int
+int
 sbsdiff_space(struct sbsline *dst, int n, int col)
 {
 	return fsl_buffer_appendf(dst->cols[col], "%*s", n, "");
 }
 
 /* Append plaintext XOR html marker into the center column of the sbs diff. */
-static int
+int
 sbsdiff_marker(struct sbsline *dst, const char *str, const char *html)
 {
 	return fsl_buffer_append(dst->cols[SBS_MID], dst->esc ? html : str, -1);
 }
 
 /* Append file line number ln to column col in the sbs diff. */
-static int
+int
 sbsdiff_lineno(struct sbsline *dst, int ln, int col)
 {
 	int rc;
 
 	if (dst->esc)
@@ -1722,11 +1512,11 @@
 
 	return rc;
 }
 
 /* Try to shift dst->idx as far as possible to the left. */
-static void
+void
 sbsdiff_shift_left(struct sbsline *dst, const char *z)
 {
 	int i, j;
 
 	while ((i = dst->idx) > 0 && z[i - 1] == z[i]) {
@@ -1742,11 +1532,11 @@
  * Simplify line at idx and idx2 in SBS diff output:
  *    -  If idx is a null-change then move idx2 into idx
  *    -  Make sure any null-changes are in canonical form.
  *    -  Make sure all changes are at character boundaries for multibyte chars.
  */
-static void
+void
 sbsdiff_simplify_line(struct sbsline *dst, const char *z)
 {
 	if (dst->idx2 == dst->end2)
 		dst->idx2 = dst->end2 = 0;
 	else if (dst->idx2) {
@@ -1777,11 +1567,11 @@
 /*
  * c[] is an array of six integers: two copy/delete/insert triples for a
  * pair of adjacent differences. Return true if the gap between these two
  * differences is so small that they should be rendered as a single edit.
  */
-static int
+int
 sbsdiff_close_gap(int *c)
 {
 	return c[3] <= 2 || c[3] <= (c[1] + c[2] + c[4] + c[5]) / 8;
 }
 
@@ -1810,11 +1600,11 @@
  *   left	lines of text on the left
  *   nleft	number of lines on the left
  *   right	lines of text on the right
  *   nright	number of lines on the right
  */
-static unsigned char *
+unsigned char *
 sbsdiff_align(fsl_dline *left, int nleft, fsl_dline *right, int nright)
 {
 	int		 buf[100];	/* left[] stack if nright not too big */
 	int		*row;		/* One row of the Wagner matrix */
 	int		*ptr;		/* Space that needs to be freed */
@@ -1955,11 +1745,11 @@
  *   left	Left line of the change
  *   llnno	Line number for the left line
  *   right	Right line of the change
  *   rlnno	Line number of the right line
  */
-static int
+int
 sbsdiff_write_change(struct sbsline *dst, fsl_dline *left, int llnno,
     fsl_dline *right, int rlnno)
 {
 	static const char	  tag_rm[] = "";
 	static const char	  tag_add[] = "";
@@ -2008,84 +1798,69 @@
 			npfx = 0;
 	}
 	if (npfx + nsfx > shortest)
 		npfx = shortest - nsfx;
 
-	/* A single hunk of text inserted on the right */
+	/* A single chunk of text inserted on the right */
 	if (npfx + nsfx == leftsz) {
 		rc = sbsdiff_lineno(dst, llnno, SBS_LLINE);
 		if (rc)
 			return rc;
 		dst->idx2 = dst->end2 = 0;
 		dst->idx = dst->end = -1;
 		rc = sbsdiff_txt(dst, left, SBS_LTEXT);
-		if (rc)
-			return rc;
-		if (leftsz == rightsz && ltxt[leftsz] == rtxt[rightsz]) {
-			if (lines && nlines) {
-				rc = add_line_type(lines, nlines,
-				    LINE_DIFF_CONTEXT);
-				if (rc)
-					return rc;
-			}
-			rc = sbsdiff_marker(dst, "   ", "");
-			if (rc)
-				return rc;
-		} else {
-			if (lines && nlines) {
-				rc = add_line_type(lines, nlines,
-				    LINE_DIFF_EDIT);
-				if (rc)
-					return rc;
-			}
-			rc = sbsdiff_marker(dst, " | ", "|");
-			if (rc)
-				return rc;
-		}
-
-		rc = sbsdiff_lineno(dst, rlnno, SBS_RLINE);
-		if (rc)
-			return rc;
-		dst->idx = npfx;
-		dst->end = rightsz - nsfx;
-		dst->tag = tag_add;
-		rc = sbsdiff_txt(dst, right, SBS_RTEXT);
-		return rc;
-	}
-
-	/* A single hunk of text deleted from the left */
-	if (npfx + nsfx == rightsz) {
-		rc = sbsdiff_lineno(dst, llnno, SBS_LLINE);
-		if (rc)
-			return rc;
-
-		if (lines && nlines) {
-			rc = add_line_type(lines, nlines, LINE_DIFF_EDIT);
-			if (rc)
-				return rc;
-		}
-
+		if (!rc && leftsz == rightsz && ltxt[leftsz] == rtxt[rightsz]) {
+			rc = add_line_type(lines, nlines, LINE_DIFF_CONTEXT);
+			if (!rc)
+				rc = sbsdiff_marker(dst, "   ", "");
+		}
+		else {
+			rc = add_line_type(lines, nlines, LINE_DIFF_EDIT);
+			if (!rc)
+				rc = sbsdiff_marker(dst, " | ", "|");
+		}
+
+		if (!rc) {
+			rc = sbsdiff_lineno(dst, rlnno, SBS_RLINE);
+			if (!rc) {
+				dst->idx = npfx;
+				dst->end = rightsz - nsfx;
+				dst->tag = tag_add;
+				rc = sbsdiff_txt(dst, right, SBS_RTEXT);
+			}
+		}
+		return rc;
+	}
+
+	/* A single chunk of text deleted from the left */
+	if (npfx + nsfx == rightsz) {
+		/* Text deleted from the left */
+		rc = sbsdiff_lineno(dst, llnno, SBS_LLINE);
+		if (!rc)
+			rc = add_line_type(lines, nlines, LINE_DIFF_EDIT);
+		if (rc)
+			return rc;
 		dst->idx2 = dst->end2 = 0;
 		dst->idx = npfx;
 		dst->end = leftsz - nsfx;
 		dst->tag = tag_rm;
 		rc = sbsdiff_txt(dst, left, SBS_LTEXT);
-		if (rc)
-			return rc;
-		rc = sbsdiff_marker(dst, " | ", "|");
-		if (rc)
-			return rc;
-		rc = sbsdiff_lineno(dst, rlnno, SBS_RLINE);
-		if (rc)
-			return rc;
-		dst->idx = dst->end = -1;
-		rc = sbsdiff_txt(dst, right, SBS_RTEXT);
+		if (!rc) {
+			rc = sbsdiff_marker(dst, " | ", "|");
+			if (!rc) {
+				rc = sbsdiff_lineno(dst, rlnno, SBS_RLINE);
+				if (!rc) {
+					dst->idx = dst->end = -1;
+					sbsdiff_txt(dst, right, SBS_RTEXT);
+				}
+			}
+		}
 		return rc;
 	}
 
 	/*
-	 * At this point we know that there is a hunk of text that has
+	 * At this point we know that there is a chunk of text that has
 	 * changed between the left and the right. Check to see if there
 	 * is a large unchanged section in the middle of that changed block.
 	 */
 	nleft = leftsz - nsfx - npfx;
 	nright = rightsz - nsfx - npfx;
@@ -2104,26 +1879,18 @@
 		dst->idx2 = npfx + lcs[1];
 		dst->end2 = leftsz - nsfx;
 		dst->tag2 = lcs[3] == nright ? tag_rm : tag_chg;
 		sbsdiff_simplify_line(dst, ltxt + npfx);
 		rc = sbsdiff_txt(dst, left, SBS_LTEXT);
-		if (rc)
-			return rc;
-
-		if (lines && nlines) {
-			rc = add_line_type(lines, nlines, LINE_DIFF_EDIT);
-			if (rc)
-				return rc;
-		}
-
-		rc = sbsdiff_marker(dst, " | ", "|");
-		if (rc)
-			return rc;
-		rc = sbsdiff_lineno(dst, rlnno, SBS_RLINE);
-		if (rc)
-			return rc;
-
+		if (!rc)
+			rc = add_line_type(lines, nlines, LINE_DIFF_EDIT);
+		if (!rc)
+			rc = sbsdiff_marker(dst, " | ", "|");
+		if (!rc)
+			rc = sbsdiff_lineno(dst, rlnno, SBS_RLINE);
+		if (rc)
+			return rc;
 		dst->idx = npfx;
 		dst->end = npfx + lcs[2];
 		if (!lcs[0]) {
 			sbsdiff_shift_left(dst, right->z);
 			dst->tag = tag_add;
@@ -2137,34 +1904,28 @@
 		return rc;
 	}
 
 	/* If all else fails, show a single big change between left and right */
 	rc = sbsdiff_lineno(dst, llnno, SBS_LLINE);
-	if (rc)
-		return rc;
-	dst->idx2 = dst->end2 = 0;
-	dst->idx = npfx;
-	dst->end = leftsz - nsfx;
-	dst->tag = tag_chg;
-	rc = sbsdiff_txt(dst, left, SBS_LTEXT);
-	if (rc)
-		return rc;
-
-	if (lines && nlines) {
-		rc = add_line_type(lines, nlines, LINE_DIFF_EDIT);
-		if (rc)
-			return rc;
-	}
-
-	rc = sbsdiff_marker(dst, " | ", "|");
-	if (rc)
-		return rc;
-	rc = sbsdiff_lineno(dst, rlnno, SBS_RLINE);
-	if (rc)
-		return rc;
-	dst->end = rightsz - nsfx;
-	rc = sbsdiff_txt(dst, right, SBS_RTEXT);
+	if (!rc) {
+		dst->idx2 = dst->end2 = 0;
+		dst->idx = npfx;
+		dst->end = leftsz - nsfx;
+		dst->tag = tag_chg;
+		rc = sbsdiff_txt(dst, left, SBS_LTEXT);
+		if (!rc) {
+			rc = add_line_type(lines, nlines, LINE_DIFF_EDIT);
+			rc = sbsdiff_marker(dst, " | ", "|");
+			if (!rc) {
+				rc = sbsdiff_lineno(dst, rlnno, SBS_RLINE);
+				if (!rc) {
+					dst->end = rightsz - nsfx;
+					sbsdiff_txt(dst, right, SBS_RTEXT);
+				}
+			}
+		}
+	}
 	return rc;
 }
 
 /*
  * Add two line numbers to the beginning of a unified diff output line.
@@ -2172,11 +1933,11 @@
  *   lln	Line number corresponding to the line in the left (old) file
  *   rln	Line number corresponding to the line in the right (new) file
  *   html	Specify html formatted output
  * n.b. lln or rln can be zero to leave that number field blank.
  */
-static int
+int
 unidiff_lineno(struct diff_out_state *dst, int lln, int rln, bool html)
 {
 	int rc = FSL_RC_OK;
 
 	if (html) {
@@ -2208,11 +1969,11 @@
  *   sign	Either a " " (context), "+" (added),  or "-" (removed) line
  *   line	The line to be output
  *   html	True if generating HTML, false for plain text
  *   regex	colourise only if line matches this regex
  */
-static int
+int
 unidiff_txt(struct diff_out_state *const dst, char sign, fsl_dline *line,
     int html, void *regex)
 {
 	char const	*ansiccode;
 	int		 rc = FSL_RC_OK;

Index: src/fnc.1
==================================================================
--- src/fnc.1
+++ src/fnc.1
@@ -16,29 +16,36 @@
 .Dd $Mdocdate$
 .Dt FNC 1
 .Os
 .Sh NAME
 .Nm fnc
-.Nd An interactive text-based user interface for Fossil
+.Nd Read-only ncurses-based Fossil repository browser
 .Sh SYNOPSIS
 .Nm
 .Op Ar command
 .Op Fl h | -help
 .Nm
 .Op Fl h | -help
 .Op Fl v | -version
 .Nm
-.Cm stash Op Po Cm get Ns | Ns Cm pop Pc Oo Ar id Oc | Fl ChPx
+.Cm stash
+.Sm off
+.Oo
+.Cm get | pop
+.Li |
+.Fl ChPx
+.Sm on
+.Oc
 .Nm
 .Cm config
 .Op Fl hu
 .Op Fl -ls
 .Op Fl R Ar path
 .Op Ar setting Op Ar value
 .Nm
 .Cm timeline
-.Op Fl Chz
+.Op Fl Cz
 .Op Fl b Ar branch
 .Op Fl c Ar commit
 .Op Fl f Ar glob
 .Op Fl n Ar number
 .Op Fl R Ar path
@@ -46,47 +53,48 @@
 .Op Fl t Ar type
 .Op Fl u Ar user
 .Op Ar path
 .Nm
 .Cm diff
-.Op Fl bChilPqsWw
+.Op Fl CilPqsWw
 .Op Fl R Ar path
 .Op Fl x Ar number
 .Op Ar artifact1 Op Ar artifact2
 .Op Ar path ...
 .Nm
 .Cm tree
-.Op Fl Ch
+.Op Fl C
 .Op Fl c Ar commit
 .Op Fl R Ar path
 .Op Ar path
 .Nm
 .Cm blame
-.Op Fl Ch
+.Op Fl C
 .Op Fl c Ar commit Op Fl r
 .Op Fl n Ar number
 .Op Fl R Ar path
 .Ar path
 .Nm
 .Cm branch
-.Op Fl Cchopr
+.Op Fl Ccopr
 .Op Fl a Ar date | Fl b Ar date
 .Op Fl R Ar path
 .Op Fl s Ar order
 .Op Ar glob
 .Nm
 .Op Ar path
 .Sh DESCRIPTION
 .Nm
-is an interactive text-based user interface for
+is an interactive interface for
 .Xr fossil 1
 repositories,
 and supports multiple views to display repository data:
 .Bl -tag -width Ds
 .It Timeline view
 Display commits from the repository's history in chronologically
 descending order.
+.Br
 If no
 .Ar command
 or
 .Ar arg
 are specified, or just a
@@ -99,181 +107,174 @@
 artifacts.
 .It Tree view
 Display navigable tree reflecting the repository state as at the specified
 commit.
 .It Blame view
-Display and annotate each line in the given file with the hyperlinked
-historical commit that last modified the line.
+Display and annotate each line in the given file with the hyperlinked historical
+version that last modified the line.
 .It Branch view
 Display navigable list of all repository branches.
 .El
 .Pp
 .Nm
-provides both global and command-specific options and runtime key
-bindings.
-Global options are as follows:
+provides both global and command-specific options and in-app key
+bindings.  Global options are as follows:
 .Bl -tag -width 6v
 .It Fl h , -help
 Display program help and usage information then exit.
 .It Fl R , -repo Ar path
 Use the
 .Xr fossil 1
-repository at the specified
+repository database at the specified
 .Ar path
 for the current
 .Nm
-invocation.
-See command specific options for details.
+session.  See command specific options for details.
 .It Fl v , -version
 Display program version then exit.
 .El
 .Pp
+Note that any global options preceding a command name will be
+interpreted as the command-specific variant if such an option exists.
+.Pp
 Global key bindings are as follows:
 .Bl -tag -width Ds
-.It Ic H, \&?, F1
-Display runtime help.
-.It Ic Tab
+.It Cm H, ?, F1
+Display in-app help.
+.It Cm Tab
 Switch focus between open views.
-.It Ic f
-Toggle the active view between fullscreen and splitscreen mode.
-By default,
+.It Cm f
+Toggle the active view between fullscreen and splitscreen mode.  Note
+that
 .Nm
-will open nested views in a splitscreen if the terminal window is equal to or
-greater than 110 columns wide.
-.It Ic Q
-Quit
+will open nested views in splitscreen mode if the terminal window is
+equal to or greater than 110 columns wide.
+.It Cm Q
+Immediatey quit
 .Nm .
-.It Ic q
+.It Cm q
 Quit the active view.
 .El
 .Pp
 Commands available to
 .Nm
 are as follows:
 .Bl -tag -width 4v
 .Tg conf
 .It Cm config Oo Fl h | -help Oc Oo Fl -ls Oc Oo Fl R | -repo Ar path Oc \
-Oo Ar setting Oo Ar value | Fl -unset Oc Oc
-.Dl Pq aliases: Cm conf , Cm set
+Oo Ar setting Op Ar value | Fl -unset Oc
+.Dl Pq aliases: Cm conf , Cm cfg , Cm settings , Cm set
 Retrieve the current, or set a new,
 .Ar value
 for
 .Ar setting
-in the local repository.
-When specified,
+in the local repository.  When specified,
 .Ar value
 will become the new value for
 .Ar setting ,
 otherwise
 .Nm
 will display the current value of
 .Ar setting .
 With no arguments,
-.Nm Cm config
-will display a list of all configurable settings.
-Alternatively, see
+.Cm fnc config
+will display a list of all configurable settings.  Alternatively, see
 .Sx ENVIRONMENT
 for a detailed list of available settings
-used in the display or processing of data.
-When no value is defined for a given setting in the local repository,
-environment variables will be searched.
-If still not found,
-.Nm
-will fallback to default values.
-Unless the
+used in the display or processing of data.  When no value is found for a given
+setting in the local repository, environment variables will be searched.
+If still not found, default values will be used.  Unless the
 .Sy --repo
-option is used, this command must be invoked from within a work tree; that is,
+option is used, this command must be executed from within the repository
+hierarchy; that is,
 .Nm
 assumes a local checkout is open in or above the current working directory.
 Options for
-.Nm Cm config
+.Cm fnc config
 are as follows:
 .Bl -tag -width Ds
 .It Fl h , -help
 Display config command help and usage information then exit.
 .It Fl -ls
 List all currently defined settings.
 .It Fl R , -repo Ar path
 Use the
 .Xr fossil 1
-repository at the specified
+repository database at the specified
 .Ar path
 for the current
-.Nm Cm config
+.Cm fnc config
 invocation.
 .It Fl u , -unset
 Clear the specified
-.Ar setting .
+.Ar setting.
 .El
 .Tg stash
-.It Cm stash Oo Po Cm get Ns | Ns Cm pop Pc Oo Ar id Oc | \
-Oo Fl C | -no-colour Oc Oo Fl h | -help Oc Oo Fl P | -no-prototype Oc \
-Oo Fl x | -context Ar n Oc Oc
-.Dl Pq aliases: Cm save , Cm sta
+.It Cm stash Oo Cm get Ns | Ns Cm pop Ns | Ns Oo Fl C | -no-colour Oc \
+Oo Fl h | -help Oc Oo Fl P | -no-prototype Oc Oo Fl x | -context Ar n Oc Oc
+.Dl Pq aliases: Cm snapshot , Cm snap , Cm save , Cm sta
 When run with neither the
 .Cm get
 nor
 .Cm pop
 subcommands,
-.Nm Cm stash
+.Nm
+.Cm stash
 will present an interactive view of the local changes on disk and iterate
 each hunk in the diff, prompting the user to either stash or keep the current
-change in the checkout.
-Valid answers are as follows:
+change in the checkout. Valid answers are as follows:
 .Bl -column -offset 2s YXZ description
 .Sy b Ta scroll back Ns \(ha
 .Sy m Ta show more of the current hunk Ns \(ha
 .Sy y Ta yes, stash the current hunk
 .Sy n Ta no, do not stash the current hunk
 .Sy a Ta yes, stash this hunk and all remaining hunks in the file
 .Sy k Ta no, do not stash this hunk nor any remaining hunks in the file
 .Sy A Ta yes, stash this hunk and all remaining hunks in the diff
 .Sy K Ta no, do not stash this hunk nor any remaining hunks in the diff
-.Sy \&? Ta display help dialog
+.Sy ? Ta display help dialog
 .El
 .Pp
 \(haConditionally available when the current hunk occupies the previous
 and/or following page.
 .Pp
 When all hunks have been selected,
 .Nm
-will prompt the user to enter a stash message.
-If not provided, a default message of
+will prompt the user to enter a stash message.  If not provided, a default
+message of
 .Qo
 fnc stash HASH-PREFIX
 .Qc ,
 where
 .Qq HASH-PREFIX
-is an abbreviated SHA hash of the current checkout,
-will be used.
-At any time prior to the final hunk being selected
-.Pq i.e., before the stash message prompt ,
+is an abbreviated SHA UUID hash of the current checkout,
+will be used.  At any time prior to the final hunk being selected
+.Po
+i.e., before the stash message prompt
+.Pc ,
 the operation can be aborted by opening the help dialog and entering
-.Qq Q .
-This will discard all selections and leave the checkout state unchanged.
+.Qq Q ,
+which will discard all selections and leave the checkout state unchanged.
 .Pp
 Available subcommands for
-.Nm Cm stash
+.Nm
+.Cm stash
 are as follows:
-.Bl -tag -width 00
-.It Cm get Op Ar id
+.Bl -ohang -width Ds
+.It Cm get
 .Dl Pq aliases: Cm apply
-Retrieve stash
-.Ar id ,
-or if not provided the most recent stash entry, and apply it to the current
-checkout.
-.It Cm pop Op Ar id
-Remove stash
-.Ar id ,
-or if not provided the most recent stash entry, and apply it to the current
-checkout.
+Retrieve the most recent stash entry and apply it to the current checkout.
+.It Cm pop
+Remove the most recent stash entry and apply it to the current checkout.
 .El
 .Pp
 Options only apply to
 .Nm
 .Cm stash
-.Po i.e., neither the
+.Po
+i.e.,
+neither the
 .Cm get
 nor
 .Cm pop
 subcommands
 .Pc
@@ -285,22 +286,21 @@
 Display
 .Cm stash
 command help and usage information then exit.
 .It Fl P , -no-prototype
 Disable hunk 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
+which is enabled by default.  The heuristic will produce reliable results for
+all C-like languages
 .Pq e.g., C/C++, Java, Python, JavaScript, Rust ;
 however, Lisps and non-source code
 .Pq e.g., Markdown, reStructuredText
 will return meaningless results.
 .It Fl x , -context Ar n
 Set
 .Ar n
 context lines to be shown in the interactive stash diff display such that
-0 \*(Le n \*(Le 64.
-By default, 5 context lines are shown.
+0 \*(Le n \*(Le 64.  By default, 5 context lines are shown.
 Illegal values are a no-op.
 .El
 .Pp
 .Tg log
 .It Cm timeline Oo Fl C | -no-colour Oc Oo Fl T | -tag Ar tag Oc \
@@ -307,50 +307,46 @@
 Oo Fl b | -branch Ar branch Oc Oo Fl c | -commit Ar commit Oc \
 Oo Fl f | -filter Ar glob Oc Oo Fl h | -help Oc  Oo Fl n | -limit Ar n Oc \
 Oo Fl R | -repo Ar path Oc Oo Fl t | -type Ar type Oc \
 Oo Fl u | -username Ar user Oc Oo Fl z | -utc Oc \
 Op Ar path
-.Dl Pq aliases: Cm log , Cm tl
-Display commit history of a repository.
-If
+.Dl Pq aliases: Cm log , Cm tl , Cm time , Cm ti
+Display commit history of a repository.  If
 .Ar path
 is specified, only commits that modified the file(s) at this path will populate
-the timeline.
-The
+the timeline.  The
 .Ar path
 may be absolute, relative to the current working directory, or relative to the
-repository root.
-Unless the
+repository root.  Unless the
 .Sy --repo
-option is used, this command must be invoked from within a work tree; that is,
+option is used, this command must be executed from within the repository
+hierarchy; that is,
 .Nm
 assumes a local checkout is open in or above the current working directory.
 .Pp
 If no command is explicitly specified, this command will be executed by
 default.
 .Pp
 Options for
-.Nm Cm timeline
+.Cm fnc timeline
 are as follows:
 .Bl -tag -width Ds
 .It Fl b , -branch Ar branch
 Display commits that are members of the specified
 .Ar branch .
 The expected argument is a glob of the symbolic name of a branch, with the most
-recent branch to match being selected.
-Pattern matching is case-insensitive unless
+recent branch to match being selected.  Pattern matching is case-insensitive
+unless
 .Ar branch
 has at least one uppercase character, in which case the search will be
-case-sensitive.
-By default,
+case-sensitive.  By default,
 .Nm
 will display all commits irrespective of the branch on which they
 reside.
 .It Fl C , -no-colour
 Disable colourised timeline, which is enabled by default on supported
-terminals.
-If this option is not used, colour can be toggled with the
+terminals. If this option is not used, colour can be toggled with the
 .Sy c
 timeline view key binding as documented below.
 User-defined colours are also supported, see
 .Sx ENVIRONMENT
 for details.
@@ -357,26 +353,25 @@
 .It Fl c , -commit Ar commit
 Open the timeline from the check-in identified by
 .Ar commit .
 The expected argument is either the name of a branch, which will resolve
 to the latest commit on the given branch, or (a unique abbreviated
-prefix of) a valid commit SHA1 or SHA3 hash.
-When this option is not supplied,
+prefix of) a valid commit UUID SHA1 or SHA3 hash. When this option is
+not supplied,
 .Nm
-will open the timeline to the latest leaf on the repository tree.
-For a complete list of valid arguments this option accepts, see
+will open the timeline to the latest leaf on the repository tree.  For a
+complete list of valid arguments this option accepts, see
 .Lk https://fossil-scm.org/home/doc/trunk/www/checkin_names.wiki \
 "Fossil's Check-in Names".
 .It Fl f , -filter Ar glob
 Filter timeline by commits containing
 .Ar glob
-in any of the commit comment, user, or branch fields.
-Pattern matching is case-insensitive unless
+in any of the commit comment, user, or branch fields.  Pattern matching is
+case-insensitive unless
 .Ar glob
 has at least one uppercase character, in which case the search will be
-case-sensitive.
-Filtering can also be performed at runtime with the
+case-sensitive.  Filtering can also be performed in-session with the
 .Sy F
 timeline view key binding as documented below.
 .It Fl h , -help
 Display timeline command help and usage information then exit.
 .It Fl n , -limit Ar n
@@ -389,46 +384,43 @@
 will load the entire history of the repository's local checkout.
 Negative values are a no-op.
 .It Fl R , -repo Ar path
 Use the
 .Xr fossil 1
-repository at the specified
+repository database at the specified
 .Ar path
 for the current
-.Nm Cm timeline
-invocation.
-When this option is used, the checkout-related Fossil special tags
+.Cm fnc timeline
+invocation.  When this option is used, the checkout-related Fossil special
+tags
 .Qq current ,
 .Qq prev ,
 and
 .Qq next
 are invalid arguments to the
 .Sy --commit
-option.
-When
+option.  When
 .Sy --commit
 is not specified,
 .Nm
 will default to populating the timeline from the latest commit.
 .It Fl T , -tag Ar tag
 Only display commits with T cards containing
 .Ar tag .
 The expected argument is a glob of a commit manifest's T card argument, with the
-most recent tag to match being selected.
-Pattern matching is case-insensitive unless
+most recent tag to match being selected.  Pattern matching is case-insensitive
+unless
 .Ar tag
 has at least one uppercase character, in which case the search will be
-case-sensitive.
-By default,
+case-sensitive.  By default,
 .Nm
 will indiscriminately display all commits irrespective of which T cards
 are attached to the commit manifest.
 .It Fl t , -type Ar type
 Only display
 .Ar type
-commits.
-Valid
+commits. Valid
 .Ar type
 values are as follows:
 .Bl -column -offset 2s YXZ description
 .Sy ci Ta check-in
 .Sy w Ta wiki
@@ -440,12 +432,15 @@
 .Pp
 By default, when this option is not supplied,
 .Nm
 will indiscriminately load all commits irrespective of
 .Ar type .
-This is a repeatable flag
-.Pq e.g., Nm Cm timeline Cm -t e -t t .
+Note that this is a repeatable flag (e.g.,
+.Nm
+.Cm timeline
+.Cm -t e -t t Ns
+).
 .It Fl u , -username Ar user
 Only display commits authored by
 .Ar user .
 The search is case-insensitive by default unless
 .Ar user
@@ -455,86 +450,82 @@
 Use Coordinated Universal Time (UTC) rather than local time when
 displaying commit dates and timestamps.
 .El
 .Pp
 Key bindings for
-.Nm Cm timeline
+.Cm fnc timeline
 are as follows:
 .Bl -tag -width Ds
-.It Ic Arrow-down, j, >, \&.
+.It Cm Arrow-down, j, >, \&.
 Move selection cursor down the timeline.
-.It Ic Arrow-up, k, <, \&,
+.It Cm Arrow-up, k, <, \&,
 Move selection cursor up the timeline.
-.It Ic Arrow-right, l
-Scroll the view two columns to the right in the buffer.
-The comment field moves left on the screen.
-.It Ic Arrow-left, h
-Scroll the view two columns to the left in the buffer.
-The comment field moves right on the screen.
-.It Ic $
+.It Cm Arrow-right, l
+Scroll the view two columns to the right in the buffer.  The comment field
+moves left on the screen.
+.It Cm Arrow-left, h
+Scroll the view two columns to the left in the buffer.  The comment field
+moves right on the screen.
+.It Cm $
 Scroll the view right to the end of the longest comment summary line on the
 page.
-.It Ic 0
+.It Cm 0
 Scroll the view left to the beginning of the line.
-.It Ic C-f, Page-down
+.It Cm C-f, Page-down
 Scroll timeline view one page downwards in the buffer.
-.It Ic C-b, Page-up
+.It Cm C-b, Page-up
 Scroll timeline view one page upwards in the buffer.
-.It Ic C-d
+.It Cm C-d
 Scroll timeline view half a page downwards in the buffer.
-.It Ic C-u
+.It Cm C-u
 Scroll timeline view half a page upwards in the buffer.
-.It Ic G, End
+.It Cm G, End
 Move selection cursor to the last commit on the timeline (i.e., oldest commit
 in the repository).
-.It Ic gg, Home
+.It Cm gg, Home
 Move selection cursor to the first commit on the timeline (i.e., newest commit
 in the repository).
-.It Ic Enter
+.It Cm Enter
 Open a
 .Cm diff
 view displaying the changeset of the currently selected commit.
-.It Ic Space
+.It Cm Space
 (Un)tag the currently selected commit as the base commit for the next tagged
-commit to be diffed against.
-If another commit is already tagged, show the changes between it and the
-selected commit.
-.It Ic b
+commit to be diffed against.  If another commit is already tagged, show the
+changes between it and the selected commit.
+.It Cm b
 Open and populate branch view with all repository branches.
-.It Ic C
+.It Cm C
 Diff local changes on disk in the current checkout against the selected
 commit.
-.It Ic c
-Toggle colourised timeline.
-On supported terminals,
+.It Cm c
+Toggle colourised timeline. On supported terminals,
 .Nm
 will default to displaying the timeline in colour.
-.It Ic F
+.It Cm F
 Prompt to enter a search term to filter a new timeline view by commits with
-comment, user, or branch fields that match the entered pattern.
-If no commits match, a message is displayed on screen.
-.It Ic t
+comment, user, or branch fields that match the entered pattern.  If no commits
+match, a message is displayed on screen.
+.It Cm t
 Display the tree of the repository corresponding to the currently selected
 commit.
-.It Ic /
+.It Cm /
 Prompt to enter a search term to begin searching for commits matching
-the pattern provided.
-The search term is an extended regular expression, which is cross-referenced
-against a commit's comment, the username of its author, branch, and SHA1
-or SHA3 hash.
-See
+the pattern provided.  The search term is an extended regular expression,
+which is cross-referenced against a commit's comment, the username of
+its author, branch, and UUID SHA1 or SHA3 hash.  See
 .Xr re_format 7
 for regular expression syntax.
-.It Ic n
-Find the next commit that matches the current search term.
-The search will continue until either a match is found or the earliest commit
-on the timeline is consumed.
-.It Ic 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 Ic Backspace
+.It Cm n
+Find the next commit that matches the current search term.  The search
+will continue until either a match is found or the earliest commit on
+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 ,
@@ -541,66 +532,58 @@
 or
 .Sy End
 .Pc .
 .El
 .Tg di
-.It Cm diff Oo Fl b | -brief Oc 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-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 ...
+.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-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
+changes on disk and a given commit.  If neither
 .Ar artifact1
 nor
 .Ar artifact2
 are specified,
 .Nm
 will diff the local changes on disk against the version on which the current
-checkout is based.
-If only
+checkout is based.  If only
 .Ar artifact1
 is specified,
 .Nm
 will diff the current checkout, including any local changes on disk, against
-this version.
-When both arguments are specified, the changes between these two versions will
-be displayed.
-If supplied,
+this version.  When both arguments are specified, the changes between these two
+versions will be displayed.  If supplied,
 .Nm
 will filter diffs between commits by
 .Ar path
-so that only changes involving the file(s) identified are displayed.
-Paths may be absolute, relative to the current working directory, or relative
-to the repository root.
-Both
+so that only changes involving the file(s) identified are displayed.  Paths may
+be absolute, relative to the current working directory, or relative to the
+repository root.  Both
 .Ar artifact1
 and
 .Ar artifact2
 must be of the same type, which is expected to be either a symbolic check-in
-name, tag, (unique abbreviated prefix of) a commit or blob artifact SHA1
-or SHA3 hash, or an ISO 8601 formatted date.
-Both artifact arguments must be supplied when diffing blobs; any following
-non-option arguments are invalid and will be ignored.
-Unless the
+name, tag, (unique abbreviated prefix of) a commit or blob artifact UUID SHA1
+or SHA3 hash, or an ISO 8601 formatted date. Both artifact arguments must be
+supplied when diffing blobs; any following non-option arguments are invalid
+and will be ignored.  Unless the
 .Sy --repo
-option is used, this command must be invoked from within a work tree; that is,
+option is used, this command must be executed from within the repository
+hierarchy; that is,
 .Nm
 assumes a local checkout is open in or above the current working directory.
 .Pp
 Options for
-.Nm Cm diff
+.Cm fnc diff
 are as follows:
 .Bl -tag -width Ds
-.It Fl b , -brief
-Display file index and hash lines only.
 .It Fl C , -no-colour
 Disable coloured diff output, which is enabled by default on supported
-terminals.
-If this option is not used, colour can be toggled with the
+terminals. If this option is not used, colour can be toggled with the
 .Sy c
 diff view key binding as documented below.
 User-defined colours are also supported, see
 .Sx ENVIRONMENT
 for details.
@@ -607,45 +590,44 @@
 .It Fl h , -help
 Display diff command help and usage information then exit.
 .It Fl i , -invert
 Invert the difference between artifacts when displaying the diff.
 .It Fl l , -line-numbers
-Display file line numbers in diff output.
-As documented below, this option can be toggled with the
+Display file line numbers in diff output.  As documented below, this option can
+be toggled with the
 .Sy L
 diff view key binding.
 .It Fl P , -no-prototype
-Disable hunk 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
+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., C/C++, Java, Python, JavaScript, Rust ;
 however, Lisps and non-source code
 .Pq e.g., Markdown, reStructuredText
 will return meaningless results.
-Function prototype cannot be displayed in the hunk header with either
+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 at runtime with the
+This option can be toggled in-session with the
 .Sy p
 key binding as documented below.
 .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
+or deleted files, which are displayed by default.  Verbosity can also be
+toggled with the
 .Sy v
 key binding as documented below.
 .It Fl R , -repo Ar path
 Use the
 .Xr fossil 1
-repository at the specified
+repository database at the specified
 .Ar path
 for the current
-.Nm Cm diff
-invocation.
-When this option is used, both
+.Cm fnc diff
+invocation.  When this option is used, both
 .Ar artifact1
 and
 .Ar artifact2
 are required, and the checkout-related Fossil special tags
 .Qq current ,
@@ -654,13 +636,13 @@
 .Qq next
 are invalid
 .Sy artifact
 operands.
 .It Fl s , -sbs
-Display a side-by-side formatted diff.
-As documented below, this option can also be toggled at runtime with the
-.Sy S
+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
@@ -669,166 +651,148 @@
 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 such that 0 \*(Le n \*(Le 64.
-By default, 5 context lines are shown.
-Illegal values are a no-op.
+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
+can be made persistent as global or per-repo settings.  See
 .Sx ENVIRONMENT
 for details.
 .Pp
 Key bindings for
-.Nm Cm diff
+.Cm fnc diff
 are as follows:
 .Bl -tag -width Ds
-.It Ic Arrow-down, j
+.It Cm Arrow-down, j
 Move the selection cursor down one line.
-.It Ic Arrow-up, k
+.It Cm Arrow-up, k
 Move the selection cursor up one line.
-.It Ic Arrow-right, l
-Scroll the view two columns to the right in the buffer.
-Diff output moves left on the screen.
-.It Ic Arrow-left, h
-Scroll the view two columns to the left in the buffer.
-Diff output moves right on the screen.
-.It Ic $
+.It Cm Arrow-right, l
+Scroll the view two columns to the right in the buffer.  Diff output moves
+left on the screen.
+.It Cm Arrow-left, h
+Scroll the view two columns to the left in the buffer.  Diff output moves
+right on the screen.
+.It Cm $
 Scroll the view right to the end of the longest line in the diff.
-.It Ic 0
+.It Cm 0
 Scroll the view left to the beginning of the line.
-.It Ic C-e
-Scroll view one line downwards in the buffer.
-Diff output moves upwards on the screen.
-.It Ic C-y
-Scroll view one line upwards in the buffer.
-Diff output moves downwards on the screen.
-.It Ic C-f, Page-down, Space
+.It Cm C-e
+Scroll view one line downwards in the buffer.  Diff output moves upwards on the
+screen.
+.It Cm C-y
+Scroll view one line upwards in the buffer.  Diff output moves downwards on the
+screen.
+.It Cm C-n
+Navigate to next file in the diff.
+.It Cm C-p
+Navigate to previous file in the diff.
+.It Cm C-f, Page-down, Space
 Scroll diff view one page downwards in the buffer.
-.It Ic C-b, Page-up
+.It Cm C-b, Page-up
 Scroll diff view one page upwards in the buffer.
-.It Ic C-d
+.It Cm C-d
 Scroll diff view half a page downwards in the buffer.
-.It Ic C-u
+.It Cm C-u
 Scroll diff view half a page upwards in the buffer.
-.It Ic G, End
+.It Cm G, End
 Scroll to the end of the view (i.e., last line of diff output).
-.It Ic gg, Home
+.It Cm gg, Home
 Scroll to the top of the view (i.e., first line of diff output).
-.It Ic C-k, K, <, \&,
+.It Cm C-k, K, <, \&,
 If the diff is derived from a
 .Cm 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 Ic C-j, J, >, \&.
+.It Cm C-j, J, >, \&.
 If the diff is derived from a
 .Cm 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 Ic C-p
-Navigate to previous file in the diff.
-.It Ic C-n
-Navigate to next file in the diff.
-.It Ic \&[
-Navigate to previous hunk in the diff.
-.It Ic \&]
-Navigate to next hunk in the diff.
-.It Ic \&-, \&_
+.It Cm \&-, \&_
 Decrease the number of context lines shown in diff output.
-.It Ic \&=, \&+
+.It Cm \&=, \&+
 Increase the number of context lines shown in diff output.
-.It Ic #
+.It Cm #
 Toggle display of diff view line numbers.
-.It Ic @
+.It Cm @
 Open prompt to enter line number and navigate to that line in the view.
-.It Ic b
-Toggle brief diff mode by only displaying file index and hash lines.
-.It Ic B
+.It Cm b
 Open and populate branch view with all repository branches.
-.It Ic c
-Toggle coloured diff output.
-On supported terminals,
+.It Cm c
+Toggle coloured diff output. On supported terminals,
 .Nm
 will default to displaying changes and diff metadata in colour.
-.It Ic F
+.It Cm F
 Open prompt to enter file number and navigate to that file in the diff.
-.It Ic i
+.It Cm i
 Toggle inversion of diff output.
-.It Ic L
+.It Cm L
 Toggle display of file line numbers in the diff.
-.It Ic P
+.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
+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
-.Pq e.g., Qq Pa 2870235eef.patch .
-.It Ic p
-In the diff hunk header, toggle display of which function each change is in;
+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 Ic S
+.It Cm S
 Toggle display of a side-by-side formatted diff.
-.It Ic v
-Toggle verbosity of diff output.
-By default,
+.It Cm v
+Toggle verbosity of diff output. By default,
 .Nm
 will display the entire content of newly added or deleted files.
-.It Ic W
-Toggle whether end-of-line whitespace changes are ignored when comparing lines
-in the diff.
-.It Ic w
+.It Cm w
 Toggle whether whitespace-only changes are ignored when comparing lines in the
 diff.
-.It Ic /
+.It Cm /
 Prompt to enter a search term to begin searching the diff output for
-lines matching the pattern provided.
-The search term is an extended regular expression, which is documented in
+lines matching the pattern provided.  The search term is an extended
+regular expression, which is documented in
 .Xr re_format 7 .
-.It Ic n
+.It Cm n
 Find the next line that matches the current search term.
-.It Ic N
+.It Cm N
 Find the previous line that matches the current search term.
 .El
 .Tg dir
 .It Cm tree Oo Fl C | -no-colour Oc Oo Fl c | -commit Ar commit Oc \
 Oo Fl h | -help Oc Oo Fl R | -repo Ar path Oc Op Ar path
 .Dl Pq aliases: Cm dir , Cm tr
-Display navigable, hierarchical tree of a repository.
-If a
+Display navigable, hierarchical tree of a repository.  If a
 .Ar path
-is specified, display tree nodes of this path.
-The
+is specified, display tree nodes of this path.  The
 .Ar path
 may be absolute, relative to the current working directory, or relative to the
-repository root.
-With no options passed, the tree will reflect the state of the latest commit on
-trunk.
-Unless the
+repository root.  With no options passed, the tree will reflect the state of the
+latest commit on trunk.  Unless the
 .Sy --repo
-option is used, this command must be invoked from within a work tree; that is,
+option is used, this command must be executed from within the repository
+hierarchy; that is,
 .Nm
 assumes a local checkout is open in or above the current working directory.
 .Pp
 Tree nodes are lexicographically ordered and may be postfixed with an identifier
 corresponding to the mode of the file object on disk as returned by
@@ -841,11 +805,11 @@
 .Pp
 Nodes representing symbolic links are also annotated with the path of the
 source file.
 .Pp
 Options for
-.Nm Cm tree
+.Cm fnc tree
 are as follows:
 .Bl -tag -width Ds
 .It Fl C , -no-colour
 Disable coloured output, which is enabled by default on supported terminals.
 If this option is not used, colour can be toggled with the
@@ -858,25 +822,25 @@
 The displayed tree will reflect the state of the repository as at the check-in
 identified by
 .Ar commit .
 The expected argument is either the name of a branch, which will resolve
 to the latest commit on the given branch, or (a unique abbreviated
-prefix of) a valid commit SHA1 or SHA3 hash.
-For a complete list of valid arguments this option accepts, see
+prefix of) a valid commit UUID SHA1 or SHA3 hash.  For a complete list of valid
+arguments this option accepts, see
 .Lk https://fossil-scm.org/home/doc/trunk/www/checkin_names.wiki \
 "Fossil's Check-in Names".
 .It Fl h , -help
 Display tree command help and usage information then exit.
 .It Fl R , -repo Ar path
 Use the
 .Xr fossil 1
-repository at the specified
+repository database at the specified
 .Ar path
 for the current
-.Nm Cm tree
-invocation.
-When this option is used, the checkout-related Fossil special tags
+.Cm fnc tree
+invocation.  When this option is used, the checkout-related Fossil special
+tags
 .Qq current ,
 .Qq prev ,
 and
 .Qq next
 are invalid arguments to the
@@ -883,81 +847,79 @@
 .Sy --commit
 option.
 .El
 .Pp
 Key bindings for
-.Nm Cm tree
+.Cm fnc tree
 are as follows:
 .Bl -tag -width Ds
-.It Ic Enter, Arrow-right, l
+.It Cm Enter, Arrow-right, l
 Enter the currently selected directory, or open a
 .Cm blame
 view of the currently selected file.
-.It Ic Backspace, Arrow-left, h
-Move up a level to the parent directory.
-This is a no-op when in the root tree.
-.It Ic Arrow-down, j
+.It Cm Backspace, Arrow-left, h
+Move up a level to the parent directory.  This is a no-op when in the root tree.
+.It Cm Arrow-down, j
 Move selection cursor one node down the tree.
-.It Ic Arrow-up, k
+.It Cm Arrow-up, k
 Move selection cursor one node up the tree.
-.It Ic C-f, Page-down
+.It Cm C-f, Page-down
 Scroll tree view one page downwards in the buffer.
-.It Ic C-b, Page-up
+.It Cm C-b, Page-up
 Scroll tree view one page upwards in the buffer.
-.It Ic C-d
+.It Cm C-d
 Scroll tree view half a page downwards in the buffer.
-.It Ic C-u
+.It Cm C-u
 Scroll tree view half a page upwards in the buffer.
-.It Ic Home, gg
+.It Cm Home, gg
 Move selection cursor to the first node in the tree.
-.It Ic End, G
+.It Cm End, G
 Move selection cursor to the last node in the tree.
-.It Ic b
+.It Cm b
 Open and populate branch view with all repository branches.
-.It Ic c
-Toggle coloured output.
-On supported terminals,
+.It Cm c
+Toggle coloured output. On supported terminals,
 .Nm
 will default to displaying the tree in colour.
-.It Ic d
+.It Cm d
 Toggle ISO8601 modified timestamp display for each tree entry.
-.It Ic i
-Toggle SHA hash display for all file nodes displayed in the tree.
-.It Ic t
+.It Cm i
+Toggle SHA hash UUID display for all file nodes displayed in the tree.
+.It Cm t
 Open
 .Cm timeline
-view for the currently selected tree node.
-This will display the timeline of all commits that involve the versioned
-file(s) corresponding to the selected node.
-.It Ic /
+view for the currently selected tree node.  This will display the timeline of
+all commits that involve the versioned file(s) corresponding to the selected
+node.
+.It Cm /
 Prompt to enter a search term to begin searching the tree for nodes matching the
-entered pattern.
-The search term is an extended regular expression, as documented in
+entered pattern.  The search term is an extended regular expression, as
+documented in
 .Xr re_format 7 ,
 and is matched against the path of each tree node.
-.It Ic n
+.It Cm n
 Find the next tree node that matches the current search pattern.
-.It Ic N
+.It Cm N
 Find the previous tree node that matches the current search pattern.
 .El
 .Tg praise
 .It Cm blame Oo Fl C | -no-colour Oc \
 Oo Fl c | -commit Ar commit Oo Fl r | -reverse Oc Oc Oo Fl h | -help Oc \
 Oo Fl n | -limit Ar n Oc Oo Fl R | -repo Ar path Oc Ar path
-.Dl Pq aliases: Cm praise , Cm bl
+.Dl Pq aliases: Cm praise , Cm annotate , Cm bl , Cm pr , Cm an
 Show commit attribution history for each line of the file at the specified
 .Ar path ,
 which may be absolute, relative to the current working directory, or relative to
-the repository root.
-Unless the
+the repository root.  Unless the
 .Sy --repo
-option is used, this command must be invoked from within a work tree; that is,
+option is used, this command must be executed from within the repository
+hierarchy; that is,
 .Nm
 assumes a local checkout is open in or above the current working directory.
 .Pp
 Options for
-.Nm Cm blame
+.Cm fnc blame
 are as follows:
 .Bl -tag -width Ds
 .It Fl C , -no-colour
 Disable coloured output, which is enabled by default on supported terminals.
 If this option is not used, colour can be toggled with the
@@ -971,15 +933,15 @@
 .Ar path
 from the check-in identified by
 .Ar commit .
 The expected argument is either the name of a branch, which will resolve
 to the latest commit on the given branch, or (a unique abbreviated
-prefix of) a valid commit SHA1 or SHA3 hash.
-When this option is not supplied,
+prefix of) a valid commit UUID SHA1 or SHA3 hash. When this option is
+not supplied,
 .Nm
-will blame the version of the file from the current checkout.
-For a complete list of valid arguments this option accepts, see
+will blame the version of the file from the current checkout.  For a complete
+list of valid arguments this option accepts, see
 .Lk https://fossil-scm.org/home/doc/trunk/www/checkin_names.wiki \
 "Fossil's Check-in Names".
 .It Fl h , -help
 Display blame command help and usage information then exit.
 .It Fl l , -line Ar lineno
@@ -989,137 +951,131 @@
 .Sy @
 key binding can be used to navigate to an arbitrary line in the file.
 .It Fl n , -limit Ar n
 Limit depth of blame history to
 .Ar n
-commits or seconds.
-The latter is denoted by a postfixed 's' (e.g., 30s).
+commits or seconds.  The latter is denoted by a postfixed 's' (e.g., 30s).
 With this option,
 .Nm
 will traverse either as many commits as specified, or as possible in the
-specified time limit.
-By default,
+specified time limit.  By default,
 .Nm
 will traverse the entire historical record of the file, which can be expensive
-for large files that span many commits.
-Use this option for a faster, more targeted annotation.
+for large files that span many commits. Use this option for a faster, more
+targeted annotation.
 .It Fl R , -repo Ar path
 Use the
 .Xr fossil 1
-repository at the specified
+repository database at the specified
 .Ar path
 for the current
-.Nm Cm blame
-invocation.
-When this option is used, the checkout-related Fossil special tags
+.Cm fnc blame
+invocation.  When this option is used, the checkout-related Fossil special
+tags
 .Qq current ,
 .Qq prev ,
 and
 .Qq next
 are invalid arguments to the
 .Sy --commit
-option.
-When
+option.  When
 .Sy --commit
 is not specified,
 .Nm
 will default to blaming the version of the file found in the latest commit.
 .It Fl r , -reverse
 Reverse annotate the file starting from a historical commit and move forward in
-time.
-That is, rather than show the most recent change to each line, show the first
-time each line was modified by a subsequent commit after the specified
+time. That is, rather than show the most recent change to each line, show the
+first time each line was modified by a subsequent commit after the specified
 .Ar commit .
 (Requires \fB\-c\fP|\fB\-\-commit\fP.)
 .El
 .Pp
 Key bindings for
-.Nm Cm blame
+.Cm fnc blame
 are as follows:
 .Bl -tag -width Ds
-.It Ic Arrow-down, j
+.It Cm Arrow-down, j
 Move selection cursor down one line.
-.It Ic Arrow-up, k
+.It Cm Arrow-up, k
 Move selection cursor up one line.
-.It Ic Arrow-right, l
-Scroll the view two columns to the right in the buffer.
-File output moves left on the screen.
-.It Ic Arrow-left, h
-Scroll the view two columns to the left in the buffer.
-File output moves right on the screen.
-.It Ic $
+.It Cm Arrow-right, l
+Scroll the view two columns to the right in the buffer.  File output moves
+left on the screen.
+.It Cm Arrow-left, h
+Scroll the view two columns to the left in the buffer.  File output moves
+right on the screen.
+.It Cm $
 Scroll the view right to the end of the longest line in the file.
-.It Ic 0
+.It Cm 0
 Scroll the view left to the beginning of the line.
-.It Ic C-f, Page-down
+.It Cm C-f, Page-down
 Scroll blame view one page downwards in the buffer.
-.It Ic C-b, Page-up
+.It Cm C-b, Page-up
 Scroll blame view one page upwards in the buffer.
-.It Ic C-d
+.It Cm C-d
 Scroll blame view half a page downwards in the buffer.
-.It Ic C-u
+.It Cm C-u
 Scroll blame view half a page upwards in the buffer.
-.It Ic Home, gg
+.It Cm Home, gg
 Move selection cursor to the first line in the file.
-.It Ic End, G
+.It Cm End, G
 Move selection cursor to the last line in the file.
-.It Ic Enter
+.It Cm Enter
 Display the
 .Cm diff
 of the commit corresponding to the currently selected line.
-.It Ic #
+.It Cm #
 Toggle display of file line numbers.
-.It Ic @
+.It Cm @
 Open prompt to enter line number and navigate to that line in the file.
-.It Ic B, Backspace
+.It Cm B, Backspace
 Reload the previous blamed version of the file.
-.It Ic b
+.It Cm b
 Blame the version of the file corresponding to the commit in the currently
 selected line.
-.It Ic c
-Toggle coloured output.
-On supported terminals,
+.It Cm c
+Toggle coloured output. On supported terminals,
 .Nm
 will default to displaying the blamed file in colour.
-.It Ic p
+.It Cm p
 Blame the version of the file corresponding to the parent of the commit in
 the currently selected line.
-.It Ic T
+.It Cm T
 Open and populate branch view with all repository branches.
-.It Ic /
+.It Cm /
 Prompt to enter a search term to begin searching the file for tokens matching
-the entered pattern.
-The search term is an extended regular expression, as documented in
+the entered pattern.  The search term is an extended regular expression, as
+documented in
 .Xr re_format 7 .
-.It Ic N
+.It Cm N
 Find the previous token that matches the current search pattern.
-.It Ic n
+.It Cm n
 Find the next token that matches the current search pattern.
 .El
 .Tg tag
 .It Cm branch Oo Fl C | -no-colour Oc Oo Fl -after Ar date | \
 Fl -before Ar date Oc Oo Fl h | -help Oc Oo Fl -open | Fl -closed Oc \
 Oo Fl p | -no-private Oc Oo Fl R | -repo Ar path Oc Oo Fl r | -reverse Oc \
 Oo Fl s | -sort Ar order Oc Op Ar glob
 .Dl Pq aliases: Cm tag , Cm br
-Display navigable list of repository branches.
-If
+Display navigable list of repository branches. If
 .Ar glob
-is specified, only display branches matching the pattern provided.
-Pattern matching is case-insensitive unless
+is specified, only display branches matching the pattern provided.  Pattern
+matching is case-insensitive unless
 .Ar glob
 contains at least one uppercase character, in which case the search will be
-case-sensitive.
-Unless the
+case-sensitive.  Unless the
 .Sy --repo
-option is used, this command must be invoked from within a work tree; that is,
+option is used, this command must be executed from within the repository
+hierarchy; that is,
 .Nm
 assumes a local checkout is open in or above the current working directory.
 .Pp
 Branches are lexicographically ordered by default, and are prefixed with an
-identifier corresponding to the branch state (i.e., open/closed).
-The current and private branches are additionally annotated with a postfixed
+identifier corresponding to the branch state (i.e., open/closed). The
+current and private branches are additionally annotated with a postfixed
 identifier:
 .Bl -column -offset Ds ABCDEFGHIJ description
 .It +dev-foo Ta open
 .It -rm-bar Ta closed
 .It +trunk@ Ta current
@@ -1128,11 +1084,11 @@
 .Pp
 All branches, irrespective of state or privacy, are displayed by default, but
 can be filtered based on several characteristics.
 .Pp
 Options for
-.Nm Cm branch
+.Cm fnc branch
 are as follows:
 .Bl -tag -width Ds
 .It Fl a , -after Ar date
 Display only those branches with activity after the specified
 .Ar date ,
@@ -1175,14 +1131,14 @@
 Do not show private branches, which are included in the list of displayed
 branches by default.
 .It Fl R , -repo Ar path
 Use the
 .Xr fossil 1
-repository at the specified
+repository database at the specified
 .Ar path
 for the current
-.Nm Cm branch
+.Cm fnc branch
 invocation.
 .It Fl r , -reverse
 Reverse the order in which branches are displayed.
 .It Fl s , -sort Ar order
 Sort branches by
@@ -1197,109 +1153,105 @@
 .Pp
 Branches are sorted in lexicographical order by default.
 .El
 .Pp
 Key bindings for
-.Nm Cm branch
+.Cm fnc branch
 are as follows:
 .Bl -tag -width Ds
-.It Ic Arrow-down, j
+.It Cm Arrow-down, j
 Move selection cursor down one branch.
-.It Ic Arrow-up, k
+.It Cm Arrow-up, k
 Move selection cursor up one branch.
-.It Ic C-f, Page-down
+.It Cm C-f, Page-down
 Scroll branch view one page downwards in the buffer.
-.It Ic C-b, Page-up
+.It Cm C-b, Page-up
 Scroll branch view one page upwards in the buffer.
-.It Ic C-d
+.It Cm C-d
 Scroll branch view half a page downwards in the buffer.
-.It Ic C-u
+.It Cm C-u
 Scroll branch view half a page upwards in the buffer.
-.It Ic Home, gg
+.It Cm Home, gg
 Move selection cursor to the first branch in the list.
-.It Ic End, G
+.It Cm End, G
 Move selection cursor to the last branch in the list.
-.It Ic Enter, Space
+.It Cm Enter, Space
 Display the
 .Cm timeline
 of the currently selected branch.
-.It Ic c
-Toggle coloured output.
-On supported terminals,
+.It Cm c
+Toggle coloured output.  On supported terminals,
 .Nm
 will default to displaying the branch list in colour.
-.It Ic d
+.It Cm d
 Toggle display of the date on which the branch last received changes.
-.It Ic i
+.It Cm i
 Toggle display of the SHA{1,3} hash that identifies branch, which is the hash
 of the commit on the tip of said branch.
-.It Ic s
-Toggle sort order of currently displayed branches.
-If branches are ordered lexicographically,
+.It Cm s
+Toggle sort order of currently displayed branches.  If branches are ordered
+lexicographically,
 .Nm
 will sort branches in most recently used order, otherwise branches will be
 sorted by their open/closed state.
-.It Ic t
+.It Cm t
 Open the
 .Cm tree
 view of the currently selected branch.
-.It Ic R, C-l
+.It Cm R, C-l
 Reload the view with all repository branches, irrespective of which options
 were used in this
-.Nm Cm branch
+.Cm fnc branch
 invocation.
-.It Ic /
+.It Cm /
 Prompt to enter a search term to begin searching the list for branches matching
-the entered pattern.
-The search term is an extended regular expression, as documented in
+the entered pattern.  The search term is an extended regular expression, as
+documented in
 .Xr re_format 7 .
-.It Ic n
+.It Cm n
 Find the next branch that matches the current search pattern.
-.It Ic N
+.It Cm N
 Find the previous branch that matches the current search pattern.
 .El
 .El
 .Sh ENVIRONMENT
 Depending on the available screen estate determined by the LINES and COLUMNS
 environment variables,
 .Nm
-will display child views in either a horizontal or vertical split.
-By default, if COLUMNS is \(>= 120, a child view will open in a vertical split
-at least 80 columns wide.
-Otherwise, the child view will open in a horizontal split that is approximately
-60% of the terminal height.
-This behaviour can be customised by configuring the following options as either
-exported environment variables or with
+will display child views in either a horizontal or vertical split.  By default,
+if COLUMNS is \(>= 120, a child view will open in a vertical split at least 80
+columns wide.  Otherwise, the child view will open in a horizontal split that
+is approximately 60% of the terminal height.  This behaviour can be customised
+by configuring the following options as either exported environment variables
+or with
 .Nm Cm config
 as documented above.
 .Bl -tag -width FNC_VIEW_SPLIT_HEIGHT
 .It Ev FNC_VIEW_SPLIT_MODE
-Open child views in a horizontal or vertical split.
-Value can be one of
+Open child views in a horizontal or vertical split. Value can be one of
 .Sy auto ,
 .Sy horizontal ,
 or
 .Sy vertical .
 Default:
 .Qq auto .
 .It Ev FNC_VIEW_SPLIT_HEIGHT
-Height of the child view when opening in a horizontal split.
-Valid numeric values are 1 \(<=
+Height of the child view when opening in a horizontal split. Valid numeric
+values are 1 \(<=
 .Sy n
-< LINES.
-Percentage values denoted with a postfixed
+< LINES.  Percentage values denoted with a postfixed
 .Sq %
 .Po
 e.g.,
 .Sy 55%
 .Pc
 are also valid.
 Default:
 .Qq 60% .
 .It Ev FNC_VIEW_SPLIT_WIDTH
-Minimum width of the child view when opening in a vertical split.
-Currently a no-op.
+Minimum width of the child view when opening in a vertical split.  Currently a
+no-op.
 Default:
 .Qq 80 .
 .El
 .Pp
 Similarly,
@@ -1319,61 +1271,57 @@
 .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., bCilPqsWw
+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.
+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.
+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
+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 Ev FNC_COLOUR_COMMIT=red
+.Cm export FNC_COLOUR_COMMIT=red
 .Pc ,
 or with
 .Nm Cm config
-as documented above.
-At startup,
+as documented above.  At startup,
 .Nm
 will search for user-defined colours in the following order:
 .Bl -column " environment variables " description -offset Ds
 .It 1. repository settings Ta Pa repo.fossil
-.It 2. environment variables Ta Sy shell
+.It 3. environment variables Ta Sy shell
 .El
 .Pp
-If none are found, the default colour scheme will be displayed.
-This enables setting per-project colours to visually distinguish the current
-repository being viewed, and globally changing the colour scheme for all
-repositories with no local settings configured.
-Except where documented below, colours supported in
+If none are found, the default colour scheme will be displayed.  This enables
+setting per-project colours to visually distinguish the current repository
+being viewed, and globally changing the colour scheme for all repositories with
+no local settings configured.  Except where documented below, colours supported
+in
 .Nm
 are:
 .Bl -column "black" "yellow" "magenta" "default" -offset indent-two
 .It Qo black Qc Ta Qo green Qc Ta Qo blue Qc Ta Qo cyan Qc
 .It Qo red Qc Ta Qo yellow Qc Ta Qo magenta Qc Ta Qo default Qc
@@ -1389,113 +1337,105 @@
 .It Ev FNC_COLOUR_COMMIT
 The commit hash ID field displayed in the timeline, diff, tree, and blame views.
 If not defined, the default value is
 .Qq green .
 .It Ev FNC_COLOUR_USER
-The username field displayed in the timeline and diff views.
-If not defined, the default value is
+The username field displayed in the timeline and diff views.  If not defined,
+the default value is
 .Qq cyan .
 .It Ev FNC_COLOUR_DATE
-The date field displayed in the timeline and diff views.
-If not defined, the default value is
+The date field displayed in the timeline and diff views.  If not defined, the
+default value is
 .Qq yellow .
 .It Ev FNC_COLOUR_DIFF_MINUS
-Removed lines displayed in the diff view.
-If not defined, the default value is
+Removed lines displayed in the diff view.  If not defined, the default value is
 .Qq magenta .
 .It Ev FNC_COLOUR_DIFF_PLUS
-Added lines displayed in the diff view.
-If not defined, the default value is
+Added lines displayed in the diff view.  If not defined, the default value is
 .Qq cyan .
-.It Ev FNC_COLOUR_DIFF_HUNK
-Hunk header lines
+.It Ev FNC_COLOUR_DIFF_CHUNK
+Chunk header lines
 .Po e.g.,
 .Li @@ -732,34 +747,40 @@
 .Pc
-displayed in the diff view.
-If not defined, the default value is
+displayed in the diff view.  If not defined, the default value is
 .Qq yellow .
 .It Ev FNC_COLOUR_DIFF_META
-Metadata displayed in the diff view.
-If not defined, the default value is
+Metadata displayed in the diff view.  If not defined, the default value is
 .Qq green .
 .It Ev FNC_COLOUR_DIFF_TAGS
-The tag field displayed in the diff view.
-If not defined, the default value is
+The tag field displayed in the diff view.  If not defined, the default value is
 .Qq magenta .
 .It Ev FNC_COLOUR_DIFF_SBS_EDIT
 Changed
 .Pq i.e., not added or removed
-lines in the diff view when displaying side-by-side diffs.
-If not defined, the default value is
+lines in the diff view when displaying side-by-side diffs.  If not defined, the
+default value is
 .Qq red .
 .It Ev FNC_COLOUR_TREE_DIR
-Directory entries displayed in the tree view.
-If not defined, the default value is
+Directory entries displayed in the tree view.  If not defined, the default value
+is
 .Qq cyan .
 .It Ev FNC_COLOUR_TREE_EXEC
-Executable file entries displayed in the tree view.
-If not defined, the default value is
+Executable file entries displayed in the tree view.  If not defined, the default
+value is
 .Qq green .
 .It Ev FNC_COLOUR_TREE_LINK
-Symbolic link entries displayed in the tree view.
-If not defined, the default value is
+Symbolic link entries displayed in the tree view.  If not defined, the default
+value is
 .Qq magenta .
 .It Ev FNC_COLOUR_BRANCH_OPEN
-Open branches displayed in the branch view.
-If not defined, the default value is
+Open branches displayed in the branch view.  If not defined, the default value
+is
 .Qq cyan .
 .It Ev FNC_COLOUR_BRANCH_CLOSED
-Closed branches displayed in the branch view.
-If not defined, the default value is
+Closed branches displayed in the branch view.  If not defined, the default value
+is
 .Qq magenta .
 .It Ev FNC_COLOUR_BRANCH_CURRENT
 The branch corresponding to the current checkout displayed in the branch view.
 If not defined, the default value is
 .Qq green .
 .It Ev FNC_COLOUR_BRANCH_PRIVATE
-Private branches displayed in the branch view.
-If not defined, the default value is
+Private branches displayed in the branch view.  If not defined, the default
+value is
 .Qq yellow .
 .It Ev FNC_COLOUR_HL_LINE
-Selected line highlight in the diff view.
-Value can be one of
+Selected line highlight in the diff view.  Value can be one of
 .Sy auto
 or
 .Sy mono .
 The former will invert the foreground colour of the selected line, while the
-latter will use a monochromatic highlight.
-If not defined, the default value is
+latter will use a monochromatic highlight.  If not defined, the default value is
 .Qq auto .
 .It Ev FNC_COLOUR_HL_SEARCH
-Search term highlight in blame and diff view.
-If not defined, the default value is
+Search term highlight in blame and diff view.  If not defined, the default
+value is
 .Qq yellow .
 .El
 .Pp
 To clear environment variables, issue
 .Cm unset Ar ENVIRONMENT_VARIABLE
 in the shell.
 .Pp
 .Nm
 displays best with UTF-8, and will detect whether UTF-8 is enabled to determine
-which characters to draw in certain views.
-If UTF-8 is supported by your terminal but is currently disabled, it can be
-enabled with
+which characters to draw in certain views.  If UTF-8 is supported by your
+terminal but is currently disabled, it can be enabled with
 .Qq export LC_ALL=en_US.UTF-8 ;
 If not available,
 .Nm
-will revert to ASCII.
-Relatedly, some fonts may render certain characters poorly in the help screen;
+will revert to ASCII.  Relatedly, some fonts may render certain characters
+poorly in the help screen;
 .Li Monospace Regular ,
 .Li JetBrains Mono ,
 and
 .Li Menlo
 are known to render all characters well.
 .Sh EXIT STATUS
 .Ex -std fnc
 .Sh SEE ALSO
 .Xr fossil 1 ,
-.Xr sqlite3 1 ,
 .Xr re_format 7
-.Sh AUTHORS
+.Xr sqlite3 1
+.Sh AUTHOR
 .An Mark Jamsek Aq Mt mark@jamsek.com

Index: src/fnc.c
==================================================================
--- src/fnc.c
+++ src/fnc.c
@@ -80,12 +80,10 @@
 
 #include "libfossil.h"
 #include "diff.h"
 
 #define FNC_VERSION	VERSION  /* cf. Makefile */
-#define FNC_HASH	HASH
-#define FNC_DATE	DATE
 
 /* 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)
@@ -109,12 +107,10 @@
 #define FLAG_TOG(_f, _b)	((_f) ^= (_b))
 #define FLAG_CLR(_f, _b)	((_f) &= ~(_b))
 
 /* Application macros. */
 #define PRINT_VERSION	STRINGIFY(FNC_VERSION)
-#define PRINT_HASH	STRINGIFY(FNC_HASH)
-#define PRINT_DATE	STRINGIFY(FNC_DATE)
 #define DEF_DIFF_CTX	5		/* Default diff context lines. */
 #define MAX_DIFF_CTX	64		/* Max diff context lines. */
 #define HSPLIT_SCALE	0.4		/* Default horizontal split scale. */
 #define SPIN_INTERVAL	200		/* Status line progress indicator. */
 #define LINENO_WIDTH	6		/* View lineno max column width. */
@@ -146,38 +142,15 @@
 # endif /* HAVE_STRTONUM */
 #else
 #  define inrange(n, min, max) true
 #endif /* OpenBSD */
 
-/*
- * Bold intersects the colour space, which makes colours that use the intensity
- * bit (e.g., yellow) unavailable in VGA text mode; observed in xterm(1).
- */
-#ifdef __OpenBSD__
-# define FNC_HIGHLIGHT	A_REVERSE
-#else
-# define FNC_HIGHLIGHT	A_BOLD | A_REVERSE
-#endif
-
 #define PRINTFV(fmt, args) __attribute__((format (printf, fmt, args)))
 #ifndef __dead
 #define __dead	__attribute__((noreturn))
 #endif
 
-#ifndef __predict_true
-# ifdef __has_builtin
-#  if __has_builtin(__builtin_expect)
-#   define __predict_true(_e)	__builtin_expect(((_e) != 0), 1)
-#   define __predict_false(_e)	__builtin_expect(((_e) != 0), 0)
-#  endif  /* __builtin_expect */
-# endif  /* __has_builtin */
-#endif  /* __predict_true */
-#ifndef __predict_true
-# define __predict_true(_e)	((_e) != 0)
-# define __predict_false(_e)	((_e) != 0)
-#endif  /* __predict_true */
-
 #ifndef TAILQ_FOREACH_SAFE
 /* Rewrite of OpenBSD 6.9 sys/queue.h for Linux builds. */
 #define TAILQ_FOREACH_SAFE(var, head, field, tmp)			\
 	for ((var) = ((head)->tqh_first);				\
 		(var) != (NULL) && ((tmp) = TAILQ_NEXT(var, field), 1);	\
@@ -270,11 +243,10 @@
 	bool		 nocolour;	/* Disable colour in 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. */
-	bool		 brief;		/* Only display file index lines. */
 
 	/* 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. */
@@ -289,11 +261,11 @@
 	/* Command line flags and help. */
 	fcli_help_info	  fnc_help;			/* Global help. */
 	fcli_cliflag	  cliflags_global[3];		/* Global options. */
 	fcli_command	  cmd_args[8];			/* App commands. */
 	fcli_cliflag	  cliflags_timeline[13];	/* Timeline options. */
-	fcli_cliflag	  cliflags_diff[13];		/* 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. */
 	fcli_cliflag	  cliflags_stash[5];		/* Stash options. */
@@ -321,11 +293,10 @@
 	false,		/* nocolour defaults to off (i.e., use diff colours). */
 	true,		/* verbose defaults to on. */
 	false,		/* invert diff defaults to off. */
 	false,		/* showln in diff defaults to off. */
 	true,		/* proto in diff hunk header defaults to on. */
-	false,		/* brief diff defaults to off. */
 	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). */
@@ -345,29 +316,29 @@
 	    "Display program version number and exit."),
 	    fcli_cliflag_empty_m
 	},
 
 	{ /* cmd_args available app commands. */
-	    {"timeline", "tl\0log\0",
+	    {"timeline", "tl\0time\0ti\0log\0",
 	    "Show chronologically descending commit history of the repository.",
 	    cmd_timeline, usage_timeline, fnc_init.cliflags_timeline},
 	    {"diff", "di\0",
 	    "Show changes to versioned files introduced with a given commit.",
 	    cmd_diff, usage_diff, fnc_init.cliflags_diff},
 	    {"tree", "tr\0dir\0",
 	    "Show repository tree corresponding to a given commit",
 	    cmd_tree, usage_tree, fnc_init.cliflags_tree},
-	    {"blame", "bl\0praise\0",
+	    {"blame", "bl\0praise\0pr\0annotate\0an\0",
 	    "Show commit attribution history for each line of a file.",
 	    cmd_blame, usage_blame, fnc_init.cliflags_blame},
 	    {"branch", "br\0tag\0",
 	    "Show navigable list of repository branches.",
 	    cmd_branch, usage_branch, fnc_init.cliflags_branch},
-	    {"config", "conf\0set\0",
+	    {"config", "conf\0cfg\0settings\0set\0",
 	    "Configure or view currently available settings.",
 	    cmd_config, usage_config, fnc_init.cliflags_config},
-	    {"stash", "save\0sta\0",
+	    {"stash", "snapshot\0snap\0save\0sta\0",
 	    "Interactively select hunks to stash from the diff of local "
 	    "changes on\n  disk.",
 	    cmd_stash, usage_stash, fnc_init.cliflags_stash},
 	    {NULL, NULL, NULL, NULL, NULL}	/* Sentinel. */
 	},
@@ -420,13 +391,10 @@
 	    "Use UTC (instead of local) time."),
 	    fcli_cliflag_empty_m
 	}, /* End cliflags_timeline. */
 
 	{ /* cliflags_diff diff command related options. */
-	    FCLI_FLAG_BOOL("b", "brief", &fnc_init.brief,
-	    "Display file index and hash lines only. Toggle with the 'b' key\n"
-	    "    binding in diff view."),
 	    FCLI_FLAG_BOOL("C", "no-colour", &fnc_init.nocolour,
 	    "Disable coloured diff output, which is enabled by default on\n    "
 	    "supported terminals. Colour can also be toggled with the 'c' "
 	    "\n    key binding in diff view when this option is not used."),
 	    FCLI_FLAG_BOOL("h", "help", NULL,
@@ -695,11 +663,11 @@
 	enum fnc_diff_type	 diff_type;
 };
 
 struct fsl_file_artifact {
 	fsl_card_F		*fc;
-	fsl_ckout_change_e	 change;
+	enum fsl_ckout_change_e	 change;
 };
 
 TAILQ_HEAD(commit_tailhead, commit_entry);
 struct commit_entry {
 	TAILQ_ENTRY(commit_entry)	 entries;
@@ -882,16 +850,18 @@
 	bool				 report;
 };
 
 struct stash_cx {
 	struct patch_cx	 pcx;
+	struct index	 hunk;	   /* line indexes for each hunk in the diff */
 	char		 patch[2][PATH_MAX]; /* stash & ckout patch filepath */
-	unsigned char	*stash;	/* fnc_diff_view_state.hundex.lineno bitarray */
+	unsigned char	*stash;	   /* bit array into this.hunk->lineno */
+#define NBITS	(sizeof(unsigned char) * 8)
 #define nbytes(nbits)	(((nbits) + 7) >> 3)
-#define BIT_SET(_B, _i)	(_B[(_i / CHAR_BIT)] |=  (1 << (_i % CHAR_BIT)))
-#define BIT_CLR(_B, _i)	(_B[(_i / CHAR_BIT)] &= ~(1 << (_i % CHAR_BIT)))
-#define BIT_CHK(_B, _i)	(_B[(_i / CHAR_BIT)] &   (1 << (_i % CHAR_BIT)))
+#define BIT_SET(_B, _i)	(_B[(_i / NBITS)] |=  (1 << (_i % NBITS)))
+#define BIT_CLR(_B, _i)	(_B[(_i / NBITS)] &= ~(1 << (_i % NBITS)))
+#define BIT_CHK(_B, _i)	(_B[(_i / NBITS)] &   (1 << (_i % NBITS)))
 };
 
 struct fnc_diff_view_state {
 	struct fnc_view			*view;
 	struct fnc_view			*parent_view;
@@ -898,12 +868,11 @@
 	struct fnc_commit_artifact	*selected_entry;
 	struct fnc_pathlist_head	*paths;
 	struct stash_cx			 scx;
 	fsl_buffer			 buf;
 	struct fnc_colours		 colours;
-	struct index			 index;   /* line indexes for files */
-	struct index			 hundex;  /* line indexes for hunks */
+	struct index			 index;
 	FILE				*f;
 	fsl_uuid_str			 id1;
 	fsl_uuid_str			 id2;
 	int				 first_line_onscreen;
 	int				 last_line_onscreen;
@@ -957,11 +926,12 @@
 };
 
 struct fnc_blame_cb_cx {
 	struct fnc_view		*view;
 	struct fnc_blame_line	*lines;
-	fsl_uuid_cstr		 commit_id;
+	fsl_uuid_str		 commit_id;
+	fsl_uuid_str		 root_commit;
 	int			 nlines;
 	uint32_t		 maxlen;
 	bool			*quit;
 };
 
@@ -999,11 +969,11 @@
 	struct fnc_blame		 blame;
 	struct fnc_commit_id_queue	 blamed_commits;
 	struct fnc_commit_qid		*blamed_commit;
 	struct fnc_commit_artifact	*selected_entry;
 	struct fnc_colours		 colours;
-	fsl_uuid_cstr			 commit_id;
+	fsl_uuid_str			 commit_id;
 	const char			*lineno;
 	char				*path;
 	int				 first_line_onscreen;
 	int				 last_line_onscreen;
 	int				 selected_line;
@@ -1115,12 +1085,10 @@
 };
 
 static volatile sig_atomic_t rec_sigwinch;
 static volatile sig_atomic_t rec_sigpipe;
 static volatile sig_atomic_t rec_sigcont;
-static volatile sig_atomic_t rec_sigint;
-static volatile sig_atomic_t rec_sigterm;
 
 static void		 fnc_show_version(void);
 static int		 init_curses(void);
 static int		 fnc_set_signals(void);
 static struct fnc_view	*view_open(int, int, int, int, enum fnc_view_id);
@@ -1194,46 +1162,38 @@
 /* static int		 countlines(const char *); */
 static int		 wrapline(char *, fsl_size_t,
 			    struct fnc_diff_view_state *, off_t *);
 static int		 add_line_offset(off_t **, size_t *, off_t);
 static int		 diff_commit(struct fnc_diff_view_state *);
-static bool		 path_to_diff(const struct fnc_pathlist_head *,
-			    const fsl_card_F *, const fsl_card_F *);
 static int		 diff_checkout(struct fnc_diff_view_state *);
 static int		 write_diff_meta(struct fnc_diff_view_state *,
-			    const char *, fsl_uuid_cstr, const char *,
-			    fsl_uuid_cstr, const fsl_ckout_change_e);
+			    const char *, fsl_uuid_str, const char *,
+			    fsl_uuid_str, enum fsl_ckout_change_e);
 static int		 diff_file(struct fnc_diff_view_state *, fsl_buffer *,
-			    const char *, const char *, fsl_uuid_cstr,
-			    const char *, const fsl_ckout_change_e);
+			    const char *, const char *, fsl_uuid_str,
+			    const char *, enum fsl_ckout_change_e);
 static int		 diff_non_checkin(struct fnc_diff_view_state *);
 static int		 diff_file_artifact(struct fnc_diff_view_state *,
 			    fsl_id_t, const fsl_card_F *, const fsl_card_F *,
-			    const fsl_ckout_change_e);
+			    fsl_ckout_change_e);
 static int		 show_diff(struct fnc_view *);
-static int		 write_diff(struct fnc_view *, const char *,
-			    const char *);
+static int		 write_diff(struct fnc_view *, char *);
 static int		 match_line(const char *, regex_t *, size_t,
 			    regmatch_t *);
 static int		 draw_matched_line(struct fnc_view *, const char *,
 			    int *, int, int, regmatch_t *, attr_t);
 static void		 drawborder(struct fnc_view *);
 static int		 diff_input_handler(struct fnc_view **,
 			    struct fnc_view *, int);
-static int		 prev_file(struct fnc_diff_view_state *);
-static int		 next_file(struct fnc_diff_view_state *);
-static int		 prev_hunk(struct fnc_diff_view_state *);
-static int		 next_hunk(struct fnc_diff_view_state *);
 static int		 request_tl_commits(struct fnc_view *);
 static int		 reset_diff_view(struct fnc_view *, bool);
 static int		 stash_get_rm_cb(fsl_ckout_unmanage_state const *);
 static int		 stash_get_add_cb(fsl_ckout_manage_state const *,
 			    bool *);
 static int		 f__add_files_in_sfile(int *, int);
-static int		 f__stash_get(uint32_t, bool);
+static int		 f__stash_get(bool);
 static int		 fnc_stash(struct fnc_view *);
-static int		 fnc_stash_get(bool);
 static int		 select_hunks(struct fnc_view *);
 static int		 stash_input_handler(struct fnc_view *, bool *);
 static void		 set_choice(struct fnc_diff_view_state *, bool *,
 			    struct input *, struct index *, uint32_t *,
 			    size_t *, size_t *, bool *, enum stash_opt *);
@@ -1289,12 +1249,11 @@
 static int		 find_next_match(struct fnc_view *);
 static void		 grep_set_view(struct fnc_view *, FILE **, off_t **,
 			    size_t *, int **, int **, int **, int **,
 			    uint8_t *);
 static int		 view_close(struct fnc_view *);
-static int		 map_ckout_path(char **);
-static int		 valid_path(const char *, const char *);
+static int		 map_repo_path(char **);
 static int		 init_timeline_view(struct fnc_view **, int, int,
 			    fsl_id_t, const char *, const char *);
 static bool		 path_is_child(const char *, const char *, size_t);
 static int		 path_skip_common_ancestor(char **, const char *,
 			    size_t, const char *, size_t);
@@ -1308,11 +1267,12 @@
 static int		 browse_commit_tree(struct fnc_view **, int, int,
 			    struct commit_entry *, const char *);
 static int		 open_tree_view(struct fnc_view *, const char *,
 			    fsl_id_t);
 static int		 walk_tree_path(struct fnc_tree_view_state *,
-			    const char *, uint16_t);
+			    struct fnc_repository_tree *,
+			    struct fnc_tree_object **, const char *);
 static int		 create_repository_tree(struct fnc_repository_tree **,
 			    fsl_uuid_str *, fsl_id_t);
 static int		 tree_builder(struct fnc_repository_tree *,
 			    struct fnc_tree_object **, const char *);
 /* static void		 delete_tree_node(struct fnc_tree_entry **, */
@@ -1322,11 +1282,11 @@
 static int		 show_tree_view(struct fnc_view *);
 static int		 tree_input_handler(struct fnc_view **,
 			    struct fnc_view *, int);
 static int		 blame_tree_entry(struct fnc_view **, int, int,
 			    struct fnc_tree_entry *, struct fnc_parent_trees *,
-			    fsl_uuid_cstr);
+			    fsl_uuid_str);
 static void		 tree_grep_init(struct fnc_view *);
 static int		 tree_search_next(struct fnc_view *);
 static int		 tree_entry_path(char **, struct fnc_parent_trees *,
 			    struct fnc_tree_entry *);
 static int		 draw_tree(struct fnc_view *, const char *);
@@ -1342,11 +1302,11 @@
 			    struct fnc_tree_entry *);
 static int		 match_tree_entry(struct fnc_tree_entry *, regex_t *);
 static void		 fnc_object_tree_close(struct fnc_tree_object *);
 static void		 fnc_close_repo_tree(struct fnc_repository_tree *);
 static int		 open_blame_view(struct fnc_view *, char *,
-			    fsl_uuid_cstr, fsl_id_t, int, const char *);
+			    fsl_uuid_str, fsl_id_t, int, const char *);
 static int		 run_blame(struct fnc_view *);
 static int		 fnc_dump_buffer_to_file(off_t *, int *, off_t **,
 			    FILE *, fsl_buffer *);
 static int		 show_blame_view(struct fnc_view *);
 static void		*blame_thread(void *);
@@ -1390,11 +1350,11 @@
 static void		 view_set_child(struct fnc_view *, struct fnc_view *);
 static int		 view_close_child(struct fnc_view *);
 static int		 close_tree_view(struct fnc_view *);
 static int		 close_timeline_view(struct fnc_view *);
 static int		 close_diff_view(struct fnc_view *);
-static void		 free_index(struct index *, bool);
+static void		 free_index(struct index *);
 static void		 free_tags(struct fnc_tl_view_state *, bool);
 static int		 view_resize(struct fnc_view *, bool);
 static bool		 screen_is_split(struct fnc_view *);
 static bool		 screen_is_shared(struct fnc_view *);
 static void		 updatescreen(WINDOW *, bool, bool);
@@ -1404,13 +1364,10 @@
 static void		 fnc_commit_artifact_close(struct fnc_commit_artifact*);
 static int		 fsl_file_artifact_free(void *, void *);
 static void		 sigwinch_handler(int);
 static void		 sigpipe_handler(int);
 static void		 sigcont_handler(int);
-static void		 sigint_handler(int);
-static void		 sigterm_handler(int);
-static bool		 fatal_signal(void);
 static int		 draw_lineno(struct fnc_view *, int, int, attr_t);
 static bool		 gotoline(struct fnc_view *, int *, int *);
 static int		 strtonumcheck(long *, const char *, const int,
 			    const int);
 static int		 fnc_prompt_input(struct fnc_view *, struct input *);
@@ -1450,13 +1407,13 @@
 				    const char *, size_t);
 
 int
 main(int argc, const char **argv)
 {
-	const fcli_command	*cmd = NULL;
-	char			*path = NULL;
-	int			 rc = FSL_RC_OK;
+	fcli_command	*cmd = NULL;
+	char		*path = NULL;
+	int		 rc = FSL_RC_OK;
 
 	/*
 	 * XXX Guard against misuse. Will have to take another approach once
 	 * the test harness is finished as we pipe input for our tests cases.
 	 */
@@ -1482,53 +1439,54 @@
 		goto end;
 	} else if (fnc_init.hflag) {
 		rc = FCLI_RC_HELP;
 		goto end;
 	}
-	if (!fsl_cx_db_repo(fcli_cx())) {
-		rc = RC(FSL_RC_MISUSE, "repository database required");
-		goto end;
-	}
 #ifdef __OpenBSD__
 	/*
 	 * See pledge(2). This is the most restrictive set we can operate under.
 	 * Look for any adverse impact & revise when implementing new features.
 	 * stdio (close, sigaction); rpath (chdir getcwd lstat); wpath (getcwd);
 	 * cpath (symlink); flock (open); tty (TIOCGWINSZ); unveil (unveil).
 	 * XXX 'fnc stash' needs more perms, call pledge(2) from cmd_stash().
 	 */
 	if (!(!fsl_strcmp(fnc_init.cmdarg, "stash") ||
-	    !fcli_cmd_aliascmp(&fnc_init.cmd_args[6], fnc_init.cmdarg)) &&
+	    fcli_cmd_aliascmp(&fnc_init.cmd_args[6], fnc_init.cmdarg)) &&
 	    pledge("stdio rpath wpath cpath flock tty unveil", NULL) == -1) {
 		rc = RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS), "pledge");
 		goto end;
 	}
 #endif
 	rc = fcli_fingerprint_check(true);
-	if (rc)
-		goto end;
-
-	if (fcli.argc) {
+	if (argc == 1)
+		cmd = &fnc_init.cmd_args[FNC_VIEW_TIMELINE];
+	else if (!rc) {
 		rc = fcli_dispatch_commands(fnc_init.cmd_args, false);
-		if (rc == FCLI_RC_NO_CMD && fcli.argc == 1) {
+		if (rc == FSL_RC_NOT_FOUND && argc == 2) {
 			/*
 			 * Check if user entered fnc path/in/repo; if valid path
 			 * is found, assume fnc timeline path/in/repo was meant.
 			 */
-			rc = map_ckout_path(&path);
-			if (rc && rc != FSL_RC_NOT_FOUND)
-				goto end;
-			/* Path may have existed in the repo at some point. */
-			RC_RESET(rc); /* for fcli_process_flags */
-			cmd = &fnc_init.cmd_args[FNC_VIEW_TIMELINE];
-			fnc_init.path = path;
-		}
-	} else
-		cmd = &fnc_init.cmd_args[FNC_VIEW_TIMELINE];
-
-	if (fcli.transient.helpRequested)
-		goto end;
+			rc = map_repo_path(&path);
+			if (rc == FSL_RC_UNKNOWN_RESOURCE || !path) {
+				rc = RC(FSL_RC_NOT_FOUND,
+				    "'%s' is not a valid command or path",
+				    argv[1]);
+			} else if (!rc) {
+				cmd = &fnc_init.cmd_args[FNC_VIEW_TIMELINE];
+				fnc_init.path = path;
+				fcli_err_reset(); /* for fcli_process_flags */
+			}
+		}
+	}
+	if (rc)
+		goto end;
+
+	if (!fsl_cx_db_repo(fcli_cx())) {
+		rc = RC(FSL_RC_MISUSE, "repository database required");
+		goto end;
+	}
 
 	if (cmd != NULL)
 		rc = cmd->f(cmd);
 end:
 	fsl_free(path);
@@ -1535,16 +1493,19 @@
 	!isendwin() ? endwin() : 0;  /* may have been called in cmd_stash() */
 	if (rc) {
 		if (rc == FSL_RC_BREAK) {
 			const fsl_cx *const f = fcli_cx();
 			const char *errstr;
-
 			fsl_error_get(&f->error, &errstr, NULL);
 			fsl_fprintf(stdout, "%s", errstr);
 			RC_RESET(rc);  /* for fcli_end_of_main() */
-		} else if (rc == FCLI_RC_HELP) {
-			fnc_init.err = FCLI_RC_HELP;
+		} else if (rc == FSL_RC_UNKNOWN_RESOURCE) {
+			/* file not found by map_repo_path() */
+			fcli_err_set(FSL_RC_NOT_FOUND, "%s",
+			    fsl_buffer_cstr(&fcli_error()->msg));
+		} else {
+			fnc_init.err = rc == FCLI_RC_HELP ? FSL_RC_OK : rc;
 			usage();
 			/* NOT REACHED */
 		}
 	}
 	putchar('\n');
@@ -1580,17 +1541,13 @@
 	if (fnc_init.glob)
 		glob = fsl_strdup(fnc_init.glob);
 	if (fnc_init.path)
 		path = fsl_strdup(fnc_init.path);
 	else {
-		rc = map_ckout_path(&path);
-		if (rc) {
-			if (rc != FSL_RC_NOT_FOUND && !fnc_init.sym)
-				goto end;
-			/* Path may have existed in the repo at some point. */
-			RC_RESET(rc);
-		}
+		rc = map_repo_path(&path);
+		if (rc)
+			goto end;
 	}
 
 	rc = init_curses();
 	if (rc)
 		goto end;
@@ -1622,33 +1579,41 @@
 
 	return rc;
 }
 
 /*
- * Look for a work tree path in **argv. If found, canonicalise path as
- * relative to the root of the tree (e.g., ckoutdir/found/path), and assign to
- * a dynamically allocated string in *requested_path, which the caller must
- * free. As a special case, if path is the root of the tree, *requested_path
- * will be NULL. If path is not found, return FSL_RC_NOT_FOUND.
+ * Look for an in-repository path in **argv. If found, canonicalise it as an
+ * absolute path relative to the repository root (e.g., /ckoutdir/found/path),
+ * and assign to a dynamically allocated string in *requested_path, which the
+ * caller must dispose of with fsl_free or free(3).
  */
 static int
-map_ckout_path(char **requested_path)
+map_repo_path(char **requested_path)
 {
 	fsl_cx		*const f = fcli_cx();
 	fsl_buffer	 buf = fsl_buffer_empty;
-	char		*ckoutdir = NULL, *path = NULL;
-	const char	*canonpath = NULL, *ckoutdir0 = NULL;
+	char		*canonpath = NULL, *ckoutdir = NULL, *path = NULL;
+	const char	*ckoutdir0 = NULL;
 	fsl_size_t	 len;
 	int		 rc = 0;
 	bool		 root;
 
 	*requested_path = NULL;
 
 	/* If no path argument is supplied, default to repository root. */
-	canonpath = fcli_next_arg(true);
-	if (canonpath == NULL)
+	if (!fcli_next_arg(false)) {
+		*requested_path = fsl_strdup("/");
+		if (*requested_path == NULL)
+			return RC(FSL_RC_ERROR, "fsl_strdup");
 		return rc;
+	}
+
+	canonpath = fsl_strdup(fcli_next_arg(true));
+	if (canonpath == NULL) {
+		rc = RC(FSL_RC_ERROR, "fsl_strdup");
+		goto end;
+	}
 
 	/*
 	 * If no checkout (e.g., 'fnc timeline -R') copy the path verbatim to
 	 * check its validity against a deck of F cards in open_timeline_view().
 	 */
@@ -1664,89 +1629,123 @@
 		rc = fsl_file_canonical_name2(ckoutdir0, canonpath, &buf, NULL);
 		if (rc) {
 			rc = RC(rc, "fsl_file_canonical_name2");
 			goto end;
 		}
+		fsl_free(path);
 		path = realpath(fsl_buffer_cstr(&buf), NULL);
-		if (!path)
-			rc = RC(FSL_RC_NOT_FOUND,
-			    "path not found in checkout: %s", canonpath);
-		/* Confirmed path is relative to repository root. */
-		fsl_free(path);
-		path = fsl_strdup(canonpath);
-		if (!rc && path == NULL)
-			rc = RC(FSL_RC_ERROR, "fsl_strdup");
+		if (path) {
+			/* Confirmed path is relative to repository root. */
+			fsl_free(path);
+			path = fsl_strdup(canonpath);
+			if (path == NULL)
+				rc = RC(FSL_RC_ERROR, "fsl_strdup");
+		} else
+			rc = RC(FSL_RC_UNKNOWN_RESOURCE,
+			    "'%s' not found in tree", canonpath);
 		goto end;
 	}
-
 	/*
 	 * Use the cwd as the virtual root to canonicalise the supplied path if
 	 * it is either: (a) relative; or (b) the root of the current checkout.
 	 * Otherwise, use the root of the current checkout.
 	 */
 	rc = fsl_cx_getcwd(f, &buf);
 	if (rc)
 		goto end;
-
-	ckoutdir = fsl_mprintf("%.*s", len - 1, ckoutdir0);
-	root = !fsl_strcmp(ckoutdir, fsl_buffer_cstr(&buf));
-	fsl_buffer_reuse(&buf);
-	rc = fsl_ckout_filename_check(f, (*canonpath == '.' || !root) ?
-	    true : false, path, &buf);
-	if (rc)
-		goto end;
-
-	fsl_free(path);
-	path = fsl_buffer_take(&buf);
-	if (*path == '\0' || (*path == '.' && !path[1])) {
-		fsl_free(path);
-		path = NULL;
-		goto end;
-	}
-
-	fsl_buffer_reuse(&buf);
-	rc = fsl_file_canonical_name2(f->ckout.dir, path, &buf, false);
-	if (rc)
-		goto end;
-
-	fsl_free(path);
-	path = fsl_buffer_take(&buf);
-	if (access(path, F_OK) != 0) {
-		rc = RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS),
-		    "path does not exist or inaccessible [%s]", path);
-		goto end;
-	}
-
-	/*
-	 * Now we have an absolute path, check again if it's the ckout
-	 * dir; if so, clear it to signal an open_timeline_view() check.
-	 */
-	len = fsl_strlen(path);
-	if (!fsl_strcmp(path, f->ckout.dir)) {
-		fsl_free(path);
-		path = NULL;
-	} else if (len > f->ckout.dirLen && path_is_child(path,
-	    f->ckout.dir, f->ckout.dirLen)) {
-		char *child;
-
-		/*
-		 * Matched on-disk path within the repository; strip
-		 * common prefix with repository root path.
-		 */
-		rc = path_skip_common_ancestor(&child, f->ckout.dir,
-		    f->ckout.dirLen, path, len);
-		fsl_free(path);
-		if (!rc)
-			path = child;
-	}
-end:
-	/* Trim trailing slash if it exists. */
-	if (path && path[fsl_strlen(path) - 1] == '/')
-		path[fsl_strlen(path) - 1] = '\0';
-	*requested_path = path;
-
-	fsl_buffer_clear(&buf);
+	ckoutdir = fsl_mprintf("%.*s", len - 1, ckoutdir0);
+	root = fsl_strcmp(ckoutdir, fsl_buffer_cstr(&buf)) == 0;
+	fsl_buffer_reuse(&buf);
+	rc = fsl_ckout_filename_check(f, (canonpath[0] == '.' || !root) ?
+	    true : false, canonpath, &buf);
+	if (rc)
+		goto end;
+	fsl_free(path);
+	fsl_free(canonpath);
+	canonpath = fsl_strdup(fsl_buffer_str(&buf));
+
+	if (canonpath[0] == '\0') {
+		path = fsl_strdup(canonpath);
+		if (path == NULL) {
+			rc = RC(FSL_RC_ERROR, "fsl_strdup");
+			goto end;
+		}
+	} else {
+		fsl_buffer_reuse(&buf);
+		rc = fsl_file_canonical_name2(f->ckout.dir, canonpath, &buf,
+		    false);
+		if (rc)
+			goto end;
+		path = fsl_strdup(fsl_buffer_str(&buf));
+		if (path == NULL) {
+			rc = RC(FSL_RC_ERROR, "fsl_strdup");
+			goto end;
+		}
+		if (access(path, F_OK) != 0) {
+			rc = RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS),
+			    "path does not exist or inaccessible [%s]", path);
+			goto end;
+		}
+		/*
+		 * Now we have an absolute path, check again if it's the ckout
+		 * dir; if so, clear it to signal an open_timeline_view() check.
+		 */
+		len = fsl_strlen(path);
+		if (!fsl_strcmp(path, f->ckout.dir)) {
+			fsl_free(path);
+			path = fsl_strdup("");
+			if (path == NULL) {
+				rc = RC(FSL_RC_ERROR, "fsl_strdup");
+				goto end;
+			}
+		} else if (len > f->ckout.dirLen && path_is_child(path,
+		    f->ckout.dir, f->ckout.dirLen)) {
+			char *child;
+			/*
+			 * Matched on-disk path within the repository; strip
+			 * common prefix with repository root path.
+			 */
+			rc = path_skip_common_ancestor(&child, f->ckout.dir,
+			    f->ckout.dirLen, path, len);
+			if (rc)
+				goto end;
+			fsl_free(path);
+			path = child;
+		} else {
+			/*
+			 * Matched on-disk path outside the repository; treat
+			 * as relative to repo root. (Though this should fail.)
+			 */
+			fsl_free(path);
+			path = canonpath;
+			canonpath = NULL;
+		}
+	}
+
+	/* Trim trailing slash if it exists. */
+	if (path[fsl_strlen(path) - 1] == '/')
+		path[fsl_strlen(path) - 1] = '\0';
+
+end:
+	if (rc) {
+		*requested_path = fsl_strdup(canonpath);
+		fsl_free(path);
+	} else {
+		/* Make path absolute from repository root. */
+		if (path[0] != '/' && (path[0] != '.' && path[1] != '/')) {
+			char *abspath;
+			if ((abspath = fsl_mprintf("/%s", path)) == NULL) {
+				rc = RC(FSL_RC_ERROR, "fsl_mprintf");
+			}
+			fsl_free(path);
+			path = abspath;
+		}
+
+		*requested_path = path;
+	}
+	fsl_buffer_clear(&buf);
+	fsl_free(canonpath);
 	fsl_free(ckoutdir);
 	return rc;
 }
 
 static bool
@@ -1762,22 +1761,19 @@
 
 	return true;
 }
 
 /*
- * NULL, "/", and "." all resolve to the repository root. The latter being a
- * special case, due to fsl_ckout_filename_check() resolving the current
- * checkout root directory to ".". For this reason, when path is intended to be
- * the current working directory for any directory other than the repository
- * root, callers must ensure path is either absolute or relative to the
- * respository root--not ".".
+ * As a special case, due to fsl_ckout_filename_check() resolving the current
+ * checkout directory to ".", this function returns true for ".". For this
+ * reason, when path is intended to be the current working directory for any
+ * directory other than the repository root, callers must ensure path is either
+ * absolute or relative to the respository root--not ".".
  */
 static bool
 fnc_path_is_root_dir(const char *path)
 {
-	if (!path)
-		return true;
 	while (*path == '/' || *path == '.')
 		++path;
 	return (*path == '\0');
 }
 
@@ -1820,16 +1816,10 @@
 #endif
 
 static int
 init_curses(void)
 {
-	int rc;
-
-	rc = fnc_set_signals();
-	if (rc)
-		return rc;
-
 	initscr();
 	cbreak();
 	noecho();
 	nonl();
 	intrflush(stdscr, FALSE);
@@ -1844,11 +1834,11 @@
 	if (!fnc_init.nocolour && has_colors()) {
 		start_color();
 		use_default_colors();
 	}
 
-	return FSL_RC_OK;
+	return fnc_set_signals();
 }
 
 static int
 fnc_set_signals(void)
 {
@@ -1862,19 +1852,10 @@
 		    "sigaction(SIGWINCH)");
 	if (sigaction(SIGCONT, &(struct sigaction){{sigcont_handler}}, NULL)
 	    == -1)
 		return RC(fsl_errno_to_rc(errno, FSL_RC_ERROR),
 		    "sigaction(SIGCONT)");
-	if (sigaction(SIGINT, &(struct sigaction){{sigint_handler}}, NULL)
-	    == -1)
-		return RC(fsl_errno_to_rc(errno, FSL_RC_ERROR),
-		    "sigaction(SIGINT)");
-	if (sigaction(SIGTERM, &(struct sigaction){{sigterm_handler}}, NULL)
-	    == -1)
-		return RC(fsl_errno_to_rc(errno, FSL_RC_ERROR),
-		    "sigaction(SIGTERM)");
-
 
 	return FSL_RC_OK;
 }
 
 static struct fnc_view *
@@ -1949,24 +1930,41 @@
 		    "WHERE objid=%d)", rid);
 	else
 		fsl_ckout_version_info(f, NULL, &s->curr_ckout_uuid);
 
 	/*
-	 * In 'fnc tl -R  ' or 'fnc tl -c  ' case,
-	 * check that path is a valid path in the tree as at either the
-	 * latest checkin or the specified commit.
+	 * In 'fnc timeline -R repo.fossil path' case, check that path is a
+	 * valid repository path in the repository tree as at either the
+	 * latest check-in or the specified commit.
 	 */
-	if (s->curr_ckout_uuid == NULL && path) {
+	if (s->curr_ckout_uuid == NULL && path[1]) {
+		fsl_deck d = fsl_deck_empty;
 		fsl_uuid_str id = NULL;
-
+		bool ispath = false;
 		if (rid)
 			id = fsl_rid_to_uuid(f, rid);
-
-		rc = valid_path(path, id ? id : "tip");
+		rc = fsl_deck_load_sym(f, &d, fnc_init.sym ? fnc_init.sym :
+		    id ? id : "tip", FSL_SATYPE_CHECKIN);
+		fsl_deck_F_rewind(&d);
+		if (fsl_deck_F_search(&d, path + 1 /* Slash */) == NULL) {
+			const fsl_card_F *cf;
+			fsl_deck_F_next(&d, &cf);
+			do {
+				fsl_deck_F_next(&d, &cf);
+				if (cf && !fsl_strncmp(path + 1 /* Slash */,
+				    cf->name, fsl_strlen(path) - 1)) {
+					ispath = true;
+					break;
+				}
+			} while (cf);
+		} else
+			ispath = true;
+		fsl_deck_finalize(&d);
 		fsl_free(id);
-		if (rc)
-			return rc;
+		if (!ispath)
+			return RC(FSL_RC_NOT_FOUND, "'%s' invalid path in [%s]",
+			    path + 1, fnc_init.sym ? fnc_init.sym : "tip");
 	}
 
 	if ((rc = pthread_cond_init(&s->thread_cx.commit_consumer, NULL))) {
 		RC(fsl_errno_to_rc(rc, FSL_RC_ACCESS), "pthread_cond_init");
 		goto end;
@@ -2085,28 +2083,30 @@
 		fsl_buffer_appendf(&sql, " AND event.mtime <= %s", startdate);
 		fsl_free(startdate);
 	}
 
 	/*
-	 * If path is not root (NULL, /, .), a versioned path in the repository
-	 * has been requested, only retrieve commits involving path.
+	 * If path is not root ("/"), a versioned path in the repository has
+	 * been requested, only retrieve commits involving path.
 	 */
-	if (path && path[1]) {
+	if (path[1]) {
 		fsl_buffer_appendf(&sql,
 		    " AND EXISTS(SELECT 1 FROM mlink"
 		    " WHERE mlink.mid = event.objid"
 		    " AND mlink.fnid IN ");
-		if (fsl_cx_is_case_sensitive(f, false))
+		if (fsl_cx_is_case_sensitive(f,false)) {
 			fsl_buffer_appendf(&sql,
 			    "(SELECT fnid FROM filename"
 			    " WHERE name = %Q OR name GLOB '%q/*')",
-			    path, path);
-		else
+			    path + 1, path + 1);  /* Skip prepended slash. */
+		} else {
 			fsl_buffer_appendf(&sql,
 			    "(SELECT fnid FROM filename"
 			    " WHERE name = %Q COLLATE nocase"
-			    " OR lower(name) GLOB lower('%q/*'))", path, path);
+			    " OR lower(name) GLOB lower('%q/*'))",
+			    path + 1, path + 1);  /* Skip prepended slash. */
+		}
 		fsl_buffer_append(&sql, ")", 1);
 	}
 
 	fsl_buffer_appendf(&sql, " ORDER BY event.mtime DESC");
 
@@ -2132,21 +2132,11 @@
 		break;
 	case FSL_RC_STEP_ERROR:
 		rc = RC(rc, "fsl_stmt_step");
 		goto end;
 	case FSL_RC_STEP_DONE:
-		if (path == NULL)
-			rc = RC(FSL_RC_NOT_FOUND, "no matching records");
-		else {
-			fcli_command *c = &fnc_init.cmd_args[FNC_VIEW_TIMELINE];
-			const char *say = "invalid command or path: ";
-
-			if (!fsl_strcmp(c->name, fnc_init.cmdarg) ||
-			    !fcli_cmd_aliascmp(c, fnc_init.cmdarg))
-				say = "path not found in repository: ";
-			rc = RC(FSL_RC_NOT_FOUND, "%s%s", say, path);
-		}
+		rc = RC(FSL_RC_BREAK, "no matching records");
 		goto end;
 	}
 
 	s->colour = !fnc_init.nocolour && has_colors();
 	s->showmeta = true;
@@ -2201,11 +2191,11 @@
 	view->active = true;
 	rc = view->show(view);
 	if (rc)
 		return rc;
 
-	while (!TAILQ_EMPTY(&views) && !done && !fatal_signal()) {
+	while (!TAILQ_EMPTY(&views) && !done && !rec_sigpipe) {
 		rc = view_input(&new_view, &done, view, &views);
 		if (rc)
 			break;
 		if (view->egress) {
 			struct fnc_view *v, *prev = NULL;
@@ -2339,11 +2329,11 @@
 
 	rc = block_main_thread_signals();
 	if (rc)
 		return (void *)(intptr_t)rc;
 
-	while (!done && !rc && !fatal_signal()) {
+	while (!done && !rc && !rec_sigpipe) {
 		switch (rc = build_commits(cx)) {
 		case FSL_RC_STEP_DONE:
 			done = true;
 			/* FALL THROUGH */
 		case FSL_RC_STEP_ROW:
@@ -2403,18 +2393,14 @@
 	sigset_t set;
 
 	if (sigemptyset(&set) == -1)
 		return RC(fsl_errno_to_rc(errno, FSL_RC_MISUSE), "sigemptyset");
 
-	/* Bespoke handlers for SIGWINCH, SIGCONT, SIGINT, and SIGTERM. */
+	/* Bespoke signal handlers for SIGWINCH and SIGCONT. */
 	if (sigaddset(&set, SIGWINCH) == -1)
 		return RC(fsl_errno_to_rc(errno, FSL_RC_MISUSE), "sigaddset");
 	if (sigaddset(&set, SIGCONT) == -1)
-		return RC(fsl_errno_to_rc(errno, FSL_RC_MISUSE), "sigaddset");
-	if (sigaddset(&set, SIGINT) == -1)
-		return RC(fsl_errno_to_rc(errno, FSL_RC_MISUSE), "sigaddset");
-	if (sigaddset(&set, SIGTERM) == -1)
 		return RC(fsl_errno_to_rc(errno, FSL_RC_MISUSE), "sigaddset");
 
 	/* ncurses handles SIGTSTP. */
 	if (sigaddset(&set, SIGTSTP) == -1)
 		return RC(fsl_errno_to_rc(errno, FSL_RC_MISUSE), "sigaddset");
@@ -2677,11 +2663,11 @@
 	const char			*search_str = NULL;
 	char				*headln = NULL, *idxstr = NULL;
 	char				*branch = NULL, *type = NULL;
 	char				*uuid = NULL;
 	wchar_t				*wline;
-	attr_t				 rx = 0;
+	attr_t				 rx = A_BOLD;
 	int				 ncommits = 0, rc = FSL_RC_OK, wlen = 0;
 	int				 ncols_needed, maxlen = -1;
 
 	if (s->selected_entry && !(view->searching != SEARCH_DONE &&
 	    view->search_status == SEARCH_WAITING)) {
@@ -2734,12 +2720,12 @@
 	ncols_needed = fsl_strlen(type) + fsl_strlen(idxstr) + FSL_STRLEN_K256
 	    + (!search_str && (!fsl_strcmp(type, "wiki") ||
 	    !fsl_strcmp(type, "tag")  || !fsl_strcmp(type, "ticket") ||
 	    (!branch && !fsl_strcmp(type, "technote"))) ? 0 : 1);
 	/* If a path has been requested, display it in the headline. */
-	if (s->path && s->path[1]) {
-		if ((headln = fsl_mprintf("%s%c%.*s /%s%s", type ? type : "",
+	if (s->path[1]) {
+		if ((headln = fsl_mprintf("%s%c%.*s %s%s", type ? type : "",
 		    type ? ' ' : SPINNER[tcx->spin_idx], view->ncols <
 		    ncols_needed ? view->ncols - (ncols_needed -
 		    FSL_STRLEN_K256) : FSL_STRLEN_K256, uuid ? uuid :
 		    "........................................",
 		    s->path, idxstr)) == NULL) {
@@ -2763,11 +2749,11 @@
 		goto end;
 
 	werase(view->window);
 
 	if (screen_is_shared(view) || view->active)
-		rx = FNC_HIGHLIGHT;
+		rx |= A_REVERSE;
 	if (s->colour)
 		c = get_colour(&s->colours, FNC_COLOUR_COMMIT);
 	if (c)
 		rx |= COLOR_PAIR(c->scheme);
 	wattron(view->window, rx);
@@ -3455,15 +3441,12 @@
 	    {"  Space            ", "  ❬Space❭         "},
 	    {"  #                ", "  ❬#❭             "},
 	    {"  @                ", "  ❬@❭             "},
 	    {"  C-e              ", "  ❬C-e❭           "},
 	    {"  C-y              ", "  ❬C-y❭           "},
-	    {"  C-p              ", "  ❬C-p❭           "},
 	    {"  C-n              ", "  ❬C-n❭           "},
-	    {"  [                ", "  ❬[❭             "},
-	    {"  ]                ", "  ❬]❭             "},
-	    {"  B                ", "  ❬B❭             "},
+	    {"  C-p              ", "  ❬C-p❭           "},
 	    {"  b                ", "  ❬b❭             "},
 	    {"  F                ", "  ❬F❭             "},
 	    {"  i                ", "  ❬i❭             "},
 	    {"  L                ", "  ❬L❭             "},
 	    {"  P                ", "  ❬P❭             "},
@@ -3507,11 +3490,11 @@
 	    {0}
 	};
 	static const char *desc[] = {
 	    "",
 	    "Global",
-	    "Open runtime help",
+	    "Open in-app help",
 	    "Move selection cursor or page up one line",
 	    "Move selection cursor or page down one line",
 	    "Scroll view up one page",
 	    "Scroll view down one page",
 	    "Scroll view up one half page",
@@ -3548,16 +3531,13 @@
 	    "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 down in the buffer",
 	    "Scroll the view up in the buffer",
-	    "Navigate to previous file in the diff",
 	    "Navigate to next file in the diff",
-	    "Navigate to previous hunk in the diff",
-	    "Navigate to next hunk in the diff",
+	    "Navigate to previous file in the diff",
 	    "Open and populate branch view with all repository branches",
-	    "Toggle brief diff display of file indexes and hashes only",
 	    "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 hunk header",
@@ -3646,11 +3626,11 @@
 		if (keys[ln][1]) {
 			width = MAX((fsl_size_t)width,
 			    fsl_strlen(keys[ln][cs]) + fsl_strlen(desc[ln]));
 		}
 		fsl_fprintf(txt, "%s%s%c", keys[ln][cs], desc[ln],
-		    keys[ln + 1][0] ? '\n' : 0);
+		    keys[ln + 1] ? '\n' : 0);
 	}
 	++width;
 	rewind(txt);
 
 	x0 = 4;	 /* column number at which to start the help window */
@@ -3876,11 +3856,11 @@
 		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 at runtime. Clear it and notify user.
+		 * files are modified in-session. Clear it and notify user.
 		 */
 		rc = fsl_ckout_changes_scan(f);
 		if (rc == FSL_RC_DB) {
 			rc = sitrep(view, SR_ALL, "-- checkout db busy --");
 			break;
@@ -4747,15 +4727,13 @@
 
 	set_diff_opt(s);
 
 	s->index.n = 0;
 	s->index.idx = 0;
-	s->hundex.n = 0;
+	s->scx.hunk.n = 0;
 	s->paths = paths;
 	s->selected_entry = commit;
-	s->id1 = s->selected_entry->puuid;
-	s->id2 = s->selected_entry->uuid;
 	s->first_line_onscreen = 1;
 	s->last_line_onscreen = view->nlines;
 	s->selected_line = 1;
 	s->f = NULL;
 	s->view = view;
@@ -4798,11 +4776,11 @@
 /*
  * 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: bCilPqsWw (see: fnc diff -h for all boolean flags)
+ *      - 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.
@@ -4819,11 +4797,10 @@
 	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;
-	fnc_init.brief ? FLAG_SET(s->diff_flags, FNC_DIFF_BRIEF) : 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 */
@@ -4844,13 +4821,10 @@
 	while (opt && (ch = opt[i++])) {
 		switch (ch) {
 		case 'C':
 			s->colour = false;
 			break;
-		case 'b':
-			FLAG_SET(s->diff_flags, FNC_DIFF_BRIEF);
-			break;
 		case 'i':
 			FLAG_SET(s->diff_flags, FNC_DIFF_INVERT);
 			break;
 		case 'l':
 			if (FLAG_CHK(s->diff_flags, FNC_DIFF_SIDEBYSIDE))
@@ -4905,12 +4879,11 @@
 	uint32_t idx = 0;
 	int	 rc = 0;
 
 	s->maxx = 0;
 
-	free_index(&s->hundex, false);
-	free_index(&s->index, true);
+	free_index(&s->index);
 	free(s->dlines);
 	s->dlines = fsl_malloc(sizeof(enum line_type));
 	if (s->dlines == NULL)
 		return RC(FSL_RC_ERROR, "fsl_malloc");
 	s->ndlines = 0;
@@ -4950,10 +4923,33 @@
 	if (!rc && s->selected_entry->diff_type == FNC_DIFF_WIKI)
 		rc = diff_non_checkin(s);
 	if (rc)
 		goto end;
 
+	/*
+	 * Delay assigning diff headline labels (i.e., diff id1 id2) till now
+	 * because wiki parent commits are obtained in diff_non_checkin().
+	 */
+	if (s->selected_entry->puuid) {
+		fsl_free(s->id1);
+		s->id1 = fsl_strdup(s->selected_entry->puuid);
+		if (s->id1 == NULL) {
+			rc = RC(FSL_RC_ERROR, "fsl_strdup");
+			goto end;
+		}
+	} else
+		s->id1 = NULL;	/* Initial commit, tag, technote, etc. */
+	if (s->selected_entry->uuid) {
+		fsl_free(s->id2);
+		s->id2 = fsl_strdup(s->selected_entry->uuid);
+		if (s->id2 == NULL) {
+			rc = RC(FSL_RC_ERROR, "fsl_strdup");
+			goto end;
+		}
+	} else
+		s->id2 = NULL;	/* Local work tree. */
+
 	if (s->diff_mode != COMMIT_META) {
 		s->index.offset = fsl_realloc(s->index.offset,
 		    (s->index.n + 1) * sizeof(off_t));
 		s->index.offset[s->index.n++] = 0;
 	}
@@ -5013,20 +5009,21 @@
 		}
 		off += n;
 		rc = add_line_offset(&s->line_offsets, &s->nlines, off);
 		if (rc)
 			goto end;
-		/* Save line offsets for each hunk. */
-		if (s->nlines < s->ndlines - 1 &&
-		    s->dlines[s->nlines] == LINE_DIFF_HUNK) {
-			s->hundex.lineno = fsl_realloc(s->hundex.lineno,
-			    (s->hundex.n + 1) * sizeof(size_t));
-			if (s->hundex.lineno == NULL) {
+		/* If in stash mode, save line offsets for each hunk. */
+		if (s->diff_mode == STASH_INTERACTIVE &&
+		    s->nlines < s->ndlines - 1 &&
+		    s->dlines[s->nlines] == LINE_DIFF_CHUNK) {
+			s->scx.hunk.lineno = fsl_realloc(s->scx.hunk.lineno,
+			    (s->scx.hunk.n + 1) * sizeof(size_t));
+			if (s->scx.hunk.lineno == NULL) {
 				rc = RC(FSL_RC_ERROR, "realloc");
 				goto end;
 			}
-			s->hundex.lineno[s->hundex.n++] = s->nlines;
+			s->scx.hunk.lineno[s->scx.hunk.n++] = s->nlines;
 		}
 	}
 	--s->nlines;  /* Don't count EOF '\n' */
 end:
 	fsl_free(st0);
@@ -5085,11 +5082,11 @@
 		 * any selected hunks, otherwise we end up with orphaned hunks.
 		 */
 		if (s->dlines[lineno] == LINE_DIFF_INDEX ||
 		    s->dlines[lineno] == LINE_DIFF_META)
 			drop = false;
-		if (s->dlines[lineno++] == LINE_DIFF_HUNK) {
+		if (s->dlines[lineno++] == LINE_DIFF_CHUNK) {
 			/* Stash diff and hunk not marked for stash? Drop it. */
 			if (s->stash == HUNK_STASH)
 				drop = !BIT_CHK(s->scx.stash, idx) ?
 				    true : false;
 			else  /* ckout diff & hunk marked for stash? drop it */
@@ -5455,14 +5452,25 @@
 
 	fsl_deck_F_next(&d2, &fc2);
 	while (fc1 || fc2) {
 		const fsl_card_F	*a = NULL, *b = NULL;
 		fsl_ckout_change_e	 change = FSL_CKOUT_CHANGE_NONE;
-		bool			 diff;
+		bool			 diff = true;
 
-		diff = (!s->paths || TAILQ_EMPTY(s->paths)) ?
-		    true : path_to_diff(s->paths, fc1, fc2);
+		if (s->paths != NULL && !TAILQ_EMPTY(s->paths)) {
+			struct fnc_pathlist_entry *pe;
+			diff = false;
+			TAILQ_FOREACH(pe, s->paths, entry)
+				if (!fsl_strcmp(pe->path, fc1->name) ||
+				    !fsl_strcmp(pe->path, fc2->name) ||
+				    !fsl_strncmp(pe->path, fc1->name,
+				    pe->pathlen) || !fsl_strncmp(pe->path,
+				    fc2->name, pe->pathlen)) {
+					diff = true;
+					break;
+				}
+		}
 
 		if (!fc1)	/* File added. */
 			different = 1;
 		else if (!fc2)	/* File deleted. */
 			different = -1;
@@ -5511,40 +5519,10 @@
 	fsl_deck_finalize(&d1);
 	fsl_deck_finalize(&d2);
 	return rc;
 }
 
-/*
- * Iterate path list paths and return true if either:
- *   1. both fc1 and fc2 have the same filename and either their full path or
- *      parent dir(s) match a path in the list
- *   2. fc1 has been deleted and is a full or partial match as per (1)
- *   3. fc2 has been added and is a full or partial match as per (1)
- * Otherwise return false.
- */
-static bool
-path_to_diff(const struct fnc_pathlist_head *paths, const fsl_card_F *fc1,
-    const fsl_card_F *fc2)
-{
-	struct fnc_pathlist_entry *pe;
-
-	TAILQ_FOREACH(pe, paths, entry)
-		/* path matches modified file */
-		if ((!fsl_strcmp(fc1->name, fc2->name) &&
-		    !fsl_strcmp(pe->path, fc1->name)) ||
-		    /* path or parent dir matches deleted or modified fc1 */
-		    (fsl_strcmp(fc1->name, fc2->name) <= 0 &&
-		    !fsl_strncmp(pe->path, fc1->name, pe->pathlen)) ||
-		    /* path or parent dir matches added or modified fc2 */
-		    (fsl_strcmp(fc1->name, fc2->name) >= 0 &&
-		    !fsl_strncmp(pe->path, fc2->name, pe->pathlen)))
-			return true;
-
-	/* file not in requested paths to diff */
-	return false;
-}
-
 /*
  * Diff local changes on disk in the current checkout against either a previous
  * commit or, if no version has been supplied, the current checkout.
  *   buf  output buffer in which diff content is appended
  *   vid  repository database record id of the version to diff against
@@ -5609,14 +5587,14 @@
 		rc = RC(rc, "fsl_cx_prepare");
 		goto yield;
 	}
 
 	while ((rc = fsl_stmt_step(st)) == FSL_RC_STEP_ROW) {
-		const char		*path, *ogpath;
-		int			 deleted, changed, added, fid, symlink;
-		fsl_ckout_change_e	 change;
-		bool			 diff = true;
+		const char	*path, *ogpath;
+		int		 deleted, changed, added, fid, symlink;
+		enum		 fsl_ckout_change_e change;
+		bool		 diff = true;
 
 		path = fsl_stmt_g_text(st, 0, NULL);
 		ogpath = fsl_stmt_g_text(st, 1, NULL);
 		deleted = fsl_stmt_g_int32(st, 2);
 		changed = fsl_stmt_g_int32(st, 3);
@@ -5640,11 +5618,11 @@
 			fid = 0;
 			change = FSL_CKOUT_CHANGE_MERGE_ADD;
 		} else if (changed == 5) {
 			fid = 0;
 			change = FSL_CKOUT_CHANGE_INTEGRATE_ADD;
-		} else if (ogpath && fsl_strcmp(ogpath, path))
+		} else if (fsl_strcmp(ogpath, path))
 			change = FSL_CKOUT_CHANGE_RENAMED;
 		else
 			change = FSL_CKOUT_CHANGE_MOD;
 
 		/*
@@ -5762,35 +5740,39 @@
  *   diff_flags  bitwise flags to control the diff
  *   change      enum denoting the versioning change of the file
  */
 static int
 write_diff_meta(struct fnc_diff_view_state *s, const char *zminus,
-    fsl_uuid_cstr xminus, const char *zplus, fsl_uuid_cstr xplus,
-    const fsl_ckout_change_e change)
+    fsl_uuid_str xminus, const char *zplus, fsl_uuid_str xplus,
+    enum fsl_ckout_change_e change)
 {
 	const char	*index, *plus, *minus;
-	int		 c, rc = FSL_RC_OK;
-	enum line_type	 i;
+	int		 rc = FSL_RC_OK;
 
 	index = zplus ? zplus : (zminus ? zminus : NULL_DEVICE);
 
 	switch (change) {
 	case FSL_CKOUT_CHANGE_MERGE_ADD:
+		/* FALL THROUGH */
 	case FSL_CKOUT_CHANGE_INTEGRATE_ADD:
+		/* FALL THROUGH */
 	case FSL_CKOUT_CHANGE_ADDED:
 		minus = NULL_DEVICE;
 		plus = xplus;
 		zminus = NULL_DEVICE;
 		break;
 	case FSL_CKOUT_CHANGE_MISSING:
+		/* FALL THROUGH */
 	case FSL_CKOUT_CHANGE_REMOVED:
 		minus = xminus;
 		plus = NULL_DEVICE;
 		zplus = NULL_DEVICE;
 		break;
 	case FSL_CKOUT_CHANGE_RENAMED:
+		/* FALL THROUGH */
 	case FSL_CKOUT_CHANGE_MOD:
+		/* FALL THROUGH */
 	default:
 		minus = xminus;
 		plus = xplus;
 		break;
 	}
@@ -5797,41 +5779,45 @@
 
 	zminus = zminus ? zminus : zplus;
 
 	if FLAG_CHK(s->diff_flags, FNC_DIFF_INVERT) {
 		const char *tmp = minus;
-
 		minus = plus;
 		plus = tmp;
 		tmp = zminus;
 		zminus = zplus;
 		zplus = tmp;
 	}
 
-	if (s->buf.used) {
-		/*
-		 * There're previous files in the diff--I don't like
-		 * Git's contiguous lines between files--so add a new
-		 * line before this file's 'Index: file/path' line.
-		 */
-		rc = add_line_type(&s->dlines, &s->ndlines, LINE_BLANK);
+	if (!FLAG_CHK(s->diff_flags, FNC_DIFF_BRIEF)) {
+		int c;
+		enum line_type i;
+
+		if (s->buf.used) {
+			/*
+			 * There're previous files in the diff--I don't like
+			 * Git's contiguous lines between files--so add a new
+			 * line before this file's 'Index: file/path' line.
+			 */
+			rc = add_line_type(&s->dlines, &s->ndlines, LINE_BLANK);
+			if (!rc)
+				rc = fsl_buffer_append(&s->buf, "\n", 1);
+		}
+		for (c = 0, i = 10; !rc && i < 14; ++c) {
+			rc = add_line_type(&s->dlines, &s->ndlines, i);
+			i += (c > 3 || c % 2) ? 1 : 0;
+		}
+		if (!rc)
+			rc = fsl_buffer_appendf(&s->buf, "Index: %s\n%.71c\n",
+			    index, '=');
+		if (!rc)
+			rc = fsl_buffer_appendf(&s->buf,
+			    "hash - %s\nhash + %s\n", minus, plus);
 		if (!rc)
-			rc = fsl_buffer_append(&s->buf, "\n", 1);
-	}
-	for (c = 0, i = 10; i < 14; ++c) {
-		rc = add_line_type(&s->dlines, &s->ndlines, i);
-		if (rc)
-			return rc;
-		i += (c > 3 || c % 2) ? 1 : 0;
-	}
-	rc = fsl_buffer_appendf(&s->buf, "Index: %s\n%.71c\n", index, '=');
-	if (rc)
-		return rc;
-	rc = fsl_buffer_appendf(&s->buf, "hash - %s\nhash + %s\n", minus, plus);
-	if (rc)
-		return rc;
-	rc = fsl_buffer_appendf(&s->buf, "--- %s\n+++ %s\n", zminus, zplus);
+			rc = fsl_buffer_appendf(&s->buf, "--- %s\n+++ %s\n",
+			    zminus, zplus);
+	}
 
 	return rc;
 }
 
 /*
@@ -5844,13 +5830,13 @@
  *   abspath  absolute path to the file on disk being diffed
  *   change   enum denoting the versioning change of the file
  * diff_flags, context, and sbs are the same parameters as diff_file_artifact()
  */
 static int
-diff_file(struct fnc_diff_view_state *s, fsl_buffer *bminus,
-    const char *zminus, const char *zplus, fsl_uuid_cstr xminus,
-    const char *abspath, const fsl_ckout_change_e change)
+diff_file(struct fnc_diff_view_state *s, fsl_buffer *bminus, const char *zminus,
+    const char *zplus, fsl_uuid_str xminus, const char *abspath,
+    enum fsl_ckout_change_e change)
 {
 	fsl_cx		*const f = fcli_cx();
 	fsl_buffer	 bplus = fsl_buffer_empty;
 	fsl_buffer	 xplus = fsl_buffer_empty;
 	int		 rc = 0;
@@ -5857,11 +5843,11 @@
 
 	/*
 	 * If it exists, read content of abspath to diff EXCEPT for the content
 	 * of 'fossil rm FILE' files because they will either: (1) have the same
 	 * content as the versioned file's blob in bminus or (2) have changes.
-	 * As a result, the upcoming call to fnc_diff_text_to_buffer() _will_
+	 * As a result, the upcoming call to fsl_diff_text_to_buffer() _will_
 	 * (1) produce an empty diff or (2) show the differences; neither are
 	 * expected behaviour because the SCM has been instructed to remove the
 	 * file; therefore, the diff should display the versioned file content
 	 * as being entirely removed. With this check, fnc now contrasts the
 	 * behaviour of fossil(1), which produces the abovementioned unexpected
@@ -5895,11 +5881,13 @@
 		    FSL_HPOLICY_AUTO, "hash-policy")) {
 		case FSL_HPOLICY_SHA1:
 			rc = fsl_sha1sum_buffer(&bplus, &xplus);
 			break;
 		case FSL_HPOLICY_AUTO:
+			/* FALL THROUGH */
 		case FSL_HPOLICY_SHA3:
+			/* FALL THROUGH */
 		case FSL_HPOLICY_SHA3_ONLY:
 			rc = fsl_sha3sum_buffer(&bplus, &xplus);
 			break;
 		}
 		break;
@@ -5908,35 +5896,27 @@
 		goto end;
 	}
 	if (rc)
 		goto end;
 
-	/*
-	 * XXX Edge case where the current checkout has a changed file that is
-	 * now the same as it is in another version and the checkout is diffed
-	 * against said version. diff_checkout() declares the file 'changed'
-	 * because fsl_ckout_changes_scan() picks it up, but because it's the
-	 * same as the other version the diff will be empty. As such, don't
-	 * draw the index header UNLESS the file has been renamed.
-	 */
-	if (!fsl_uuidcmp(xminus, fsl_buffer_cstr(&xplus)) &&
-	    change != FSL_CKOUT_CHANGE_RENAMED)
-		goto end;
-
 	if (s->buf.used) {
 		s->index.offset = fsl_realloc(s->index.offset,
 		    (s->index.n + 1) * sizeof(off_t));
 		s->index.offset[s->index.n++] = s->buf.used;
 	}
-	rc = write_diff_meta(s, zminus, xminus, zplus, fsl_buffer_cstr(&xplus),
+	rc = write_diff_meta(s, zminus, xminus, zplus, fsl_buffer_str(&xplus),
 	    change);
 	if (rc)
 		goto end;
 
-	if (!FLAG_CHK(s->diff_flags, FNC_DIFF_BRIEF) &&
-	    (FLAG_CHK(s->diff_flags, FNC_DIFF_VERBOSE) ||
-	    (bminus->used && bplus.used)))
+	if FLAG_CHK(s->diff_flags, FNC_DIFF_BRIEF) {
+		rc = fsl_buffer_compare(bminus, &bplus);
+		if (!rc)
+			rc = fsl_buffer_appendf(&s->buf, "CHANGED -> %s\n",
+			    zminus);
+	} else if (FLAG_CHK(s->diff_flags, FNC_DIFF_VERBOSE) ||
+	    (bminus->used && bplus.used))
 		rc = fnc_diff_text_to_buffer(bminus, &bplus, &s->buf,
 		    &s->dlines, &s->ndlines, s->context, s->sbs,
 		    s->diff_flags);
 end:
 	fsl_buffer_clear(&bplus);
@@ -6067,11 +6047,11 @@
  *   context     the number of context lines to surround changes
  *   sbs	 number of columns in which to display each side-by-side diff
  */
 static int
 diff_file_artifact(struct fnc_diff_view_state *s, fsl_id_t vid1,
-    const fsl_card_F *a, const fsl_card_F *b, const fsl_ckout_change_e change)
+    const fsl_card_F *a, const fsl_card_F *b, enum fsl_ckout_change_e change)
 {
 	fsl_cx		*const f = fcli_cx();
 	fsl_stmt	 stmt = fsl_stmt_empty;
 	fsl_buffer	 fbuf1 = fsl_buffer_empty;
 	fsl_buffer	 fbuf2 = fsl_buffer_empty;
@@ -6114,15 +6094,11 @@
 			goto end;
 		}
 		xminus0 = fsl_rid_to_uuid(f, vid1);
 		xminus = xminus0;
 		fsl_stmt_finalize(&stmt);
-		rc = fsl_content_get(f, vid1, &fbuf1);
-		if (rc) {
-			rc = RC(rc, "fsl_context_get(%s)", vid1);
-			goto end;
-		}
+		fsl_content_get(f, vid1, &fbuf1);
 	}
 	if (b) {
 		rc = fsl_card_F_content(f, b, &fbuf2);
 		if (rc)
 			goto end;
@@ -6148,15 +6124,11 @@
 			goto end;
 		}
 		xplus0 = fsl_rid_to_uuid(f, vid2);
 		xplus = xplus0;
 		fsl_stmt_finalize(&stmt);
-		rc = fsl_content_get(f, vid2, &fbuf2);
-		if (rc) {
-			rc = RC(rc, "fsl_context_get(%s)", vid2);
-			goto end;
-		}
+		fsl_content_get(f, vid2, &fbuf2);
 	}
 
 	if (s->buf.used) {
 		s->index.offset = fsl_realloc(s->index.offset,
 		    (s->index.n + 1) * sizeof(off_t));
@@ -6165,12 +6137,11 @@
 	}
 	rc = write_diff_meta(s, zminus, xminus, zplus, xplus, change);
 	if (rc)
 		goto end;
 
-	if (!FLAG_CHK(s->diff_flags, FNC_DIFF_BRIEF) &&
-	    (FLAG_CHK(s->diff_flags, FNC_DIFF_VERBOSE) || (a && b)))
+	if (FLAG_CHK(s->diff_flags, FNC_DIFF_VERBOSE) || (a && b))
 		rc = fnc_diff_text_to_buffer(&fbuf1, &fbuf2, &s->buf,
 		    &s->dlines, &s->ndlines, s->context, s->sbs,
 		    s->diff_flags);
 	if (rc)
 		RC(rc, "%s: fnc_diff_text_to_buffer\n"
@@ -6189,37 +6160,50 @@
 
 static int
 show_diff(struct fnc_view *view)
 {
 	struct fnc_diff_view_state	*s = &view->state.diff;
-	char				*id2, *id1;
-	enum fnc_diff_type		 diff = s->selected_entry->diff_type;
+	char				*headln, *id2, *id1 = NULL;
 
 	/* Some diffs (e.g., technote, tag) have no parent hash to display. */
-	id1 = s->id1 ? s->id1 : "/dev/null";
+	id1 = fsl_strdup(s->id1 ? s->id1 : "/dev/null");
+	if (id1 == NULL)
+		return RC(FSL_RC_ERROR, "fsl_strdup");
 
-	/* If diffing the work tree, we have no hash to display for it. */
-	id2 = !s->id2 || diff == FNC_DIFF_CKOUT ? "(checkout)" : s->id2;
+	/*
+	 * If diffing the work tree, we have no hash to display for it.
+	 * XXX Display "work tree" or "checkout" or "/dev/null" for clarity?
+	 */
+	id2 = fsl_strdup(s->id2 ? s->id2 : "");
+	if (id2 == NULL) {
+		fsl_free(id1);
+		return RC(FSL_RC_ERROR, "fsl_strdup");
+	}
 
-	return write_diff(view, id1, id2);
+	if ((headln = fsl_mprintf("diff %.40s %.40s", id1, id2)) == NULL) {
+		fsl_free(id1);
+		fsl_free(id2);
+		return RC(FSL_RC_RANGE, "fsl_mprintf");
+	}
+
+	fsl_free(id1);
+	fsl_free(id2);
+	return write_diff(view, headln);
 }
 
 static int
-write_diff(struct fnc_view *view, const char *id1, const char *id2)
+write_diff(struct fnc_view *view, char *headln)
 {
 	struct fnc_diff_view_state	*s = &view->state.diff;
 	regmatch_t			*regmatch = &view->regmatch;
 	struct fnc_colour		*c = NULL;
 	wchar_t				*wcstr;
 	char				*line;
-	static char			 pct[MAX_PCT_LEN];
 	size_t				 lidx, linesz = 0;
 	ssize_t				 linelen;
 	off_t				 line_offset;
-	attr_t				 rx = 0;
-	double				 percent;
-	int				 ln, pctlen;
+	attr_t				 rx = A_BOLD;
 	int				 col, wstrlen, max_lines = view->nlines;
 	int				 nlines = s->nlines;
 	int				 nprinted = 0, rc = FSL_RC_OK;
 	bool				 selected;
 
@@ -6228,48 +6212,51 @@
 	if (fseeko(s->f, line_offset, SEEK_SET))
 		return RC(fsl_errno_to_rc(errno, FSL_RC_ERROR), "fseeko");
 
 	werase(view->window);
 
-	ln = s->gtl ? s->gtl : s->lineno + s->selected_line;
-	percent = 100.00 * ln / nlines;
-	pctlen = snprintf(pct, MAX_PCT_LEN, "%.*lf%%",
-	    percent > 99.99 ? 0 : 2, percent);
-	if (pctlen < 0 || pctlen >= MAX_PCT_LEN)
-		return RC(fsl_errno_to_rc(errno, FSL_RC_RANGE), "snprintf");
-
-	line = fsl_mprintf("[%d/%d] diff %.40s %.40s", ln, nlines, id1, id2);
-	if (line == NULL)
-		return RC(FSL_RC_RANGE, "fsl_mprintf");
-
-	rc = formatln(&wcstr, &wstrlen, line, view->ncols, 0, false);
-	fsl_free(line);
-	line = NULL;
-	if (rc)
-		return rc;
-
-	if (screen_is_shared(view) || view->active)
-		rx = FNC_HIGHLIGHT;
-
-	wattron(view->window, rx);
-
-	waddwstr(view->window, wcstr);
-	fsl_free(wcstr);
-	wcstr = NULL;
-
-	col = wstrlen;
-	while (col++ < view->ncols)
-		waddch(view->window, ' ');
-	if (wstrlen < view->ncols - pctlen)
-		mvwaddstr(view->window, 0, view->ncols - pctlen, pct);
-
-	wattroff(view->window, rx);
-
-	if (--max_lines < 1)
-		return rc;
+	if (headln) {
+		static char	pct[MAX_PCT_LEN];
+		double		percent;
+		int		ln, pctlen;
+
+		ln = s->gtl ? s->gtl : s->lineno + s->selected_line;
+		percent = 100.00 * ln / nlines;
+		pctlen = snprintf(pct, MAX_PCT_LEN, "%.*lf%%",
+		    percent > 99.99 ? 0 : 2, percent);
+		if (pctlen < 0 || pctlen >= MAX_PCT_LEN)
+			return RC(fsl_errno_to_rc(errno, FSL_RC_RANGE),
+			    "snprintf");
+
+		line = fsl_mprintf("[%d/%d] %s", ln, nlines, headln);
+		if (line == NULL)
+			return RC(FSL_RC_RANGE, "fsl_mprintf");
+		rc = formatln(&wcstr, &wstrlen, line, view->ncols, 0, false);
+		fsl_free(line);
+		fsl_free(headln);
+		if (rc)
+			return rc;
+
+		if (screen_is_shared(view) || view->active)
+			rx |= A_REVERSE;
+		wattron(view->window, rx);
+		waddwstr(view->window, wcstr);
+		fsl_free(wcstr);
+		wcstr = NULL;
+		col = wstrlen;
+		while (col++ < view->ncols)
+			waddch(view->window, ' ');
+		if (wstrlen < view->ncols - pctlen)
+			mvwaddstr(view->window, 0, view->ncols - pctlen, pct);
+		wattroff(view->window, rx);
+
+		if (--max_lines < 1)
+			return rc;
+	}
 
 	s->eof = false;
+	line = NULL;
 	while (max_lines > 0 && nprinted < max_lines) {
 		col = wstrlen = 0;
 		linelen = getline(&line, &linesz, s->f);
 		if (linelen == -1) {
 			if (feof(s->f)) {
@@ -6289,19 +6276,19 @@
 				continue;
 
 		rx = 0;
 		lidx = s->lineno - 1;
 		if ((selected = nprinted == s->selected_line - 1))
-			rx = FNC_HIGHLIGHT;
+			rx = A_BOLD | A_REVERSE;
 		if (s->showln)
 			col = draw_lineno(view, nlines, s->lineno, rx);
 
 		if (s->diff_mode == STASH_INTERACTIVE &&
-		    s->hundex.lineno[s->hundex.idx] == lidx)
+		    s->scx.hunk.lineno[s->scx.hunk.idx] == lidx)
 			rx = A_REVERSE;  /* highlight current hunk to stash */
 
-		if (s->colour && s->ndlines > 0)
+		if (s->colour)
 			c = get_colour(&s->colours,
 			    s->dlines[MIN(s->ndlines - 1, lidx)]);
 		if (c && !(selected && s->sline == SLINE_MONO))
 			rx |= COLOR_PAIR(c->scheme);
 		if (c || selected)
@@ -6397,11 +6384,11 @@
     int offset, regmatch_t *regmatch, attr_t rx)
 {
 	wchar_t		*wstr = NULL;
 	int		 rme, rms, skip = view->pos.col, wlen;
 	int		 n, rc = FSL_RC_OK;
-	attr_t		 hl = FNC_HIGHLIGHT;
+	attr_t		 hl = A_BOLD | A_REVERSE;
 
 	*col = n = 0;
 	rms = regmatch->rm_so;
 	rme = regmatch->rm_eo;
 
@@ -6521,23 +6508,33 @@
 	case KEY_LEFT:
 	case 'h':
 		view->pos.col -= MIN(view->pos.col, 2);
 		break;
 	case CTRL('p'):
-		s->first_line_onscreen = prev_file(s);
+		if (s->selected_entry->diff_type == FNC_DIFF_WIKI ||
+		    !s->index.lineno)
+			break;
+		if (!((size_t)s->lineno > s->index.lineno[s->index.n - 1])) {
+			if (s->index.idx == 0)
+				s->index.idx = s->index.n - 1;
+			else
+				--s->index.idx;
+		} else
+			s->index.idx = s->index.n - 1;
+		s->first_line_onscreen = s->index.lineno[s->index.idx];
 		s->selected_line = 1;
 		break;
 	case CTRL('n'):
-		s->first_line_onscreen = next_file(s);
-		s->selected_line = 1;
-		break;
-	case '[':
-		s->first_line_onscreen = prev_hunk(s);
-		s->selected_line = 1;
-		break;
-	case ']':
-		s->first_line_onscreen = next_hunk(s);
+		if (s->selected_entry->diff_type == FNC_DIFF_WIKI ||
+		    !s->index.lineno)
+			break;
+		if (!((size_t)s->lineno < s->index.lineno[0])) {
+			if (++s->index.idx == s->index.n)
+				s->index.idx = 0;
+		} else
+			s->index.idx = 0;
+		s->first_line_onscreen = s->index.lineno[s->index.idx];
 		s->selected_line = 1;
 		break;
 	case CTRL('e'):
 		if (!s->eof) {
 			++s->first_line_onscreen;
@@ -6674,11 +6671,11 @@
 		break;
 	}
 	case '#':
 		s->showln = !s->showln;
 		break;
-	case 'B': {
+	case 'b': {
 		int start_col = 0;
 		if (view_is_parent(view))
 			start_col = view_split_start_col(view->start_col);
 		branch_view = view_open(view->nlines, view->ncols,
 		    view->start_ln, start_col, FNC_VIEW_BRANCH);
@@ -6704,11 +6701,10 @@
 	}
 	case 'P':
 		s->patch = true;
 		rc = create_diff(s);
 		break;
-	case 'b':
 	case 'c':
 	case 'i':
 	case 'L':
 	case 'p':
 	case 'S':
@@ -6715,17 +6711,15 @@
 	case 'v':
 	case 'W':
 	case 'w':
 		if (ch == 'c')
 			s->colour = !s->colour;
-		/* biLpSvWw 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 == 'b')
-			FLAG_TOG(s->diff_flags, FNC_DIFF_BRIEF);
 		else if (ch == 'i')
 			FLAG_TOG(s->diff_flags, FNC_DIFF_INVERT);
 		else if (ch == 'L')
 			FLAG_TOG(s->diff_flags, FNC_DIFF_LINENO);
 		else if (ch == 'p')
@@ -6807,96 +6801,23 @@
 
 	return rc;
 }
 
 static int
-prev_file(struct fnc_diff_view_state *s)
-{
-	if (s->selected_entry->diff_type == FNC_DIFF_WIKI || !s->index.lineno)
-		return s->first_line_onscreen;
-
-	if (!((size_t)s->lineno > s->index.lineno[s->index.n - 1])) {
-		if (s->index.idx == 0)
-			s->index.idx = s->index.n - 1;
-		else
-			--s->index.idx;
-	} else
-		s->index.idx = s->index.n - 1;
-
-	return s->index.lineno[s->index.idx];
-}
-
-static int
-next_file(struct fnc_diff_view_state *s)
-{
-	if (s->selected_entry->diff_type == FNC_DIFF_WIKI || !s->index.lineno)
-		return s->first_line_onscreen;
-
-	if (!((size_t)s->lineno < s->index.lineno[0])) {
-		if (++s->index.idx == s->index.n)
-			s->index.idx = 0;
-	} else
-		s->index.idx = 0;
-
-	return s->index.lineno[s->index.idx];
-}
-
-static int
-prev_hunk(struct fnc_diff_view_state *s)
-{
-	struct index *h = &s->hundex;
-
-	if (!h->lineno)
-		return s->first_line_onscreen;
-
-	if (!((size_t)s->lineno - 1 > h->lineno[h->idx])) {
-		if (h->idx == 0 || (size_t)s->lineno < h->lineno[0])
-			h->idx = h->n - 1;
-		else
-			while (h->idx > 0 &&
-			    (size_t)s->lineno - 1 <= h->lineno[h->idx])
-				--h->idx;
-	} else
-		while (h->idx < h->n - 1 &&
-			    (size_t)s->lineno > h->lineno[h->idx + 1])
-			++h->idx;
-
-	return h->lineno[h->idx] + 1;
-}
-
-static int
-next_hunk(struct fnc_diff_view_state *s)
-{
-	struct index *h = &s->hundex;
-
-	if (!h->lineno)
-		return s->first_line_onscreen;
-
-	if (!((size_t)s->lineno - 1 < h->lineno[h->idx])) {
-		if (h->idx == h->n - 1 ||
-		    (size_t)s->lineno > h->lineno[h->n - 1])
-			h->idx = 0;
-		else
-			while (h->idx < h->n - 1 &&
-			    (size_t)s->lineno > h->lineno[h->idx])
-				++h->idx;
-	} else
-		while (h->idx > 0 &&
-		    (size_t)s->lineno - 1 < h->lineno[h->idx - 1])
-			--h->idx;
-
-	return h->lineno[h->idx] + 1;
-}
-
-static int
-f__stash_get(uint32_t stashid, bool pop)
+f__stash_get(bool pop)
 {
 	fsl_cx	*const f = fcli_cx();
 	fsl_db	*db = fsl_needs_ckout(f);
 	fsl_stmt q = fsl_stmt_empty;
-	int	 nadded, vid, rc0, rc = FSL_RC_OK;
+	int	 nadded, stashid, vid, rc0, rc = FSL_RC_OK;
 	uint32_t cc = 0;
+
+	stashid = fsl_db_g_int32(db, 0, "SELECT max(stashid) FROM stash");
+	if (!stashid) {
+		f_out(">> empty stash");
+		return rc;
+	}
 
 	vid = f->ckout.rid;
 
 	rc = fsl_db_prepare(db, &q, "SELECT blob.rid, isRemoved, isExec,"
 	    "  isLink, origname, newname, delta FROM stashfile, blob"
@@ -7055,42 +6976,40 @@
 			fsl_buffer_clear(&out);
 			fsl_buffer_clear(&disk);
 			if (rc)
 				goto clear_delta;
 		}
-		if (ogname && fsl_strcmp(ogname, name)) {
-			rc = fsl_file_unlink(ogpath);
-			if (rc) {
+clear_delta:
+		fsl_buffer_clear(&delta);
+		if (!rc && fsl_strcmp(ogname, name)) {
+			fsl_file_unlink(ogpath);
+			if (rc)
 				rc = RC(rc, "fsl_file_unlink(%s)", ogpath);
-				goto clear_delta;
-			}
-			rc = fsl_db_exec_multi(db,
+			rc = rc ? rc : fsl_db_exec_multi(db,
 			    "UPDATE vfile SET pathname='%q', origname='%q'"
 			    " WHERE pathname='%q' %s AND vid=%d", name, ogname,
 			    ogname, fsl_cx_filename_collation(f), vid);
 		}
-clear_delta:
-		fsl_buffer_clear(&delta);
 		fsl_free(path);
 		fsl_free(ogpath);
 		if (rc)
-			goto end;
+			break;
 	}
-	rc = f__add_files_in_sfile(&nadded, vid);
-	if (rc)
-		goto end;
+	if (!rc)
+		rc = f__add_files_in_sfile(&nadded, vid);
+	rc0 = fsl_stmt_finalize(&q);
+	rc = rc ? rc : rc0;
 
 	if (cc)
 		f_out("\n>> merge conflict(s): %u\n", cc);
+
 	if (pop) {
 		rc = fsl_db_exec_multi(db, "DELETE FROM stash WHERE stashid=%d;"
 		    "DELETE FROM stashfile WHERE stashid=%d;",
 		    stashid, stashid);
 	}
-end:
-	rc0 = fsl_stmt_finalize(&q);
-	return rc ? rc : rc0;
+	return rc;
 }
 
 static int
 f__add_files_in_sfile(int *nadded, int vid)
 {
@@ -7143,81 +7062,62 @@
 	f_out("[+] %s\n", cms->filename);
 	return FSL_RC_OK;
 }
 
 /*
- * Interactive stash is a simple algorithm:
- *   1. let user select hunks to stash
- *   2. make two patch(1) files:
- *      2.1 diff of hunks selected to stash
- *      2.2 diff of hunks to be kept in the checkout
- *   3. revert checkout
- *   4. apply patch of hunks selected to stash
- *   5. stash changes
- *   6. revert checkout
- *   7. apply patch of hunks that were not selected to stash
- * This produces a checkout with only the changes that were not selected
+ * Get hunks selected to stash from the user and make two patch(1) files:
+ * (1) diff of all hunks selected to stash; and (2) diff of all hunks to be
+ * kept in the checkout. Then revert the checkout and use patch files to:
+ *   1. apply patch of hunks selected to stash
+ *   2. stash (and revert) checkout
+ *   3. apply patch of hunks that were not selected to stash
+ * This produces a ckout with only those changes that were not selected
  * to stash, achieving the same function as 'git add -p'. The user can
  * now test the code, commit, then run 'fnc stash pop' and repeat.
  */
 static int
 fnc_stash(struct fnc_view *view)
 {
-	struct fnc_diff_view_state	*s = &view->state.diff;
-	struct stash_cx			*scx = &s->scx;
-	struct input			 in;
-	char				*msg = NULL, *prompt = NULL;
-	int				 rc = FSL_RC_OK;
+	struct fnc_diff_view_state	 *s = &view->state.diff;
+	struct stash_cx			 *scx = &s->scx;
+	struct input			  in;
+	char				 *msg = NULL, *prompt = NULL;
+	int				  rc = FSL_RC_OK;
 
-	scx->stash = alloc_bitstring(s->hundex.n);
+	scx->stash = alloc_bitstring(scx->hunk.n);
 	if (scx->stash == NULL)
-		return RC(FSL_RC_ERROR, "alloc_bitstring");
+		return RC(FSL_RC_ERROR, "calloc");
 
-	rc = select_hunks(view);  /* 1. get hunks to stash */
+	rc = select_hunks(view);  /* get hunks to stash */
 	if (rc)
 		goto end;
 
 	/* Use default stash msg of "fnc stash CKOUT-HASH" if not provided. */
 	msg = fsl_mprintf("fnc stash %.11s", s->id2);
-	if (msg == NULL) {
-		rc = RC(FSL_RC_ERROR, "fsl_mprintf");
-		goto end;
-	}
 	prompt = fsl_mprintf("stash message [%s]: ", msg);
-	if (prompt == NULL) {
-		rc = RC(FSL_RC_ERROR, "fsl_mprintf");
-		goto end;
-	}
-
 	in = (struct input){NULL, prompt, INPUT_ALPHA, SR_CLREOL};
 	rc = fnc_prompt_input(view, &in);
+	if (in.buf[0]) {
+		fsl_free(msg);
+		msg = fsl_mprintf("%s", in.buf);
+	}
 	if (rc) {
 		rc = RC(rc, "fnc_prompt_input");
 		goto end;
 	}
-	if (in.buf[0]) {
-		fsl_free(msg);
-		msg = fsl_mprintf("%s", in.buf);
-		if (msg == NULL) {
-			rc = RC(FSL_RC_ERROR, "fsl_mprintf");
-			goto end;
-		}
-	}
-
-	s->stash = HUNK_STASH;
-	rc = create_diff(s);  /* 2.1 make patch of hunks selected to stash */
-	if (rc)
-		goto end;
-
-	s->stash = HUNK_CKOUT;
-	rc = create_diff(s);  /* 2.2 make patch of hunks to keep in ckout */
+
+	s->stash = HUNK_STASH;  /* make patch of hunks selected to stash */
+	rc = create_diff(s);
+	s->stash = HUNK_CKOUT;  /* make patch of hunks to keep in ckout */
+	if (!rc)
+		rc = create_diff(s);
 	if (rc)
 		goto end;
 
 	endwin();  /* restore tty so we can report progress to stdout */
 
-	/* 3. revert ckout to apply patches; vfile scanned in cmd_stash() */
+	/* Clean ckout to apply patches; vfile already scanned in cmd_stash() */
 	rc = revert_ckout(true, false);
 	if (rc) {
 		rc = RC(rc, "revert_ckout");
 		goto end;
 	}
@@ -7228,34 +7128,25 @@
 	if (rc)
 		goto end;
 
 	scx->pcx.context = s->context;
 	scx->pcx.report = true;  /* report files with changes stashed */
-
-	/* 4. apply patch of hunks selected to stash */
-	rc = fnc_patch(&scx->pcx, scx->patch[0]);
-	if (rc)
-		goto end;
-
+	rc = fnc_patch(&scx->pcx, scx->patch[0]);  /* (1) apply stash patch */
 	scx->pcx.report = false; /* don't report changes kept in ckout */
-	/* fnc_execp((const char *const []) */
-	/*     {"fossil", "stash", "save", "-m", msg, (char *)NULL}, 10); */
-
-	/* 5. stash changes */
-	rc = f__stash_create(msg, s->selected_entry->rid);
-	if (rc)
-		goto end;
-
-	rc = revert_ckout(false, false);  /* 6. revert checkout */
-	if (rc)
-		goto end;
-
-	/* 7. apply patch of hunks that were not selected to stash */
-	rc = fnc_patch(&scx->pcx, scx->patch[1]);
+	if (!rc) {
+		/* fnc_execp((const char *const []) */
+		/*     {"fossil", "stash", "save", "-m", msg, (char *)NULL}, 10); */
+		rc = f__stash_create(msg, fcli_cx()->ckout.rid);  /* (2) */
+		if (!rc)
+			rc = revert_ckout(false, false);
+	}
+	if (!rc)
+		rc = fnc_patch(&scx->pcx, scx->patch[1]);  /* (3) ckout patch */
 	rc = (rc == NO_PATCH ? FSL_RC_OK : rc);
 end:
 	fsl_free(scx->stash);
+	fsl_free(scx->hunk.lineno);
 	fsl_free(msg);
 	fsl_free(prompt);
 	return rc;
 }
 
@@ -7319,11 +7210,11 @@
 	uint32_t			*nh;
 	int				 hl, rc = FSL_RC_OK;
 	enum stash_opt			 choice = NO_CHOICE;
 	bool				 lastfile = false;
 
-	hunks = &s->hundex;
+	hunks = &s->scx.hunk;
 	nh = &hunks->idx;
 	in = (struct input){NULL, NULL, INPUT_ALPHA, SR_CLREOL, "X"};
 
 	/* Iterate hunks and prompt user to stash or keep in ckout. */
 	while (!rc && *nh < hunks->n) {
@@ -9036,12 +8927,16 @@
 }
 
 static int
 set_selected_commit(struct fnc_diff_view_state *s, struct commit_entry *entry)
 {
-	s->id2 = entry->commit->uuid;
-	s->id1 = entry->commit->puuid;
+	fsl_free(s->id2);
+	s->id2 = fsl_strdup(entry->commit->uuid);
+	if (s->id2 == NULL)
+		return RC(FSL_RC_ERROR, "fsl_strdup");
+	fsl_free(s->id1);
+	s->id1 = entry->commit->puuid ? fsl_strdup(entry->commit->puuid) : NULL;
 	s->selected_entry = entry->commit;
 
 	return 0;
 }
 
@@ -9190,26 +9085,28 @@
 	struct fnc_diff_view_state	*s = &view->state.diff;
 	int				 rc = 0;
 
 	if (s->f && fclose(s->f) == EOF)
 		rc = RC(fsl_errno_to_rc(errno, FSL_RC_IO), "fclose");
+	fsl_free(s->id1);
+	s->id1 = NULL;
+	fsl_free(s->id2);
+	s->id2 = NULL;
 	fsl_free(s->line_offsets);
 	free_colours(&s->colours);
 	s->line_offsets = NULL;
 	s->nlines = 0;
-	free_index(&s->index, true);
-	free_index(&s->hundex, false);
+	free_index(&s->index);
 	fsl_free(s->dlines);
 	s->dlines = NULL;
 	return rc;
 }
 
 static void
-free_index(struct index *index, bool reset)
+free_index(struct index *index)
 {
-	if (reset)
-		index->idx = 0;
+	index->idx = 0;
 	index->n = 0;
 	fsl_free(index->lineno);
 	index->lineno = NULL;
 	fsl_free(index->offset);
 	index->offset = NULL;
@@ -9338,12 +9235,15 @@
 	struct artifact_types	*ft = &fnc_init.filter_types;
 	const char		*t = *((const char **)v->flagValue);
 
 	/* Valid types: ci, e, f, g, t, w */
 	if (strlen(t) > 2 || (t[1] && (*t != 'c' || t[1] != 'i')) || (!t[1] &&
-	    (*t != 'e' && *t != 'f' && *t != 'g' && *t != 't' && *t != 'w')))
-		return RC(FSL_RC_TYPE, "invalid type: %s", t);
+	    (*t != 'e' && *t != 'f' && *t != 'g' && *t != 't' && *t != 'w'))) {
+		fnc_init.err = RC(FSL_RC_TYPE, "invalid type: %s", t);
+		usage();
+		/* NOT REACHED */
+	}
 
 	ft->values = fsl_realloc(ft->values, (ft->nitems + 1) * sizeof(char *));
 	ft->values[ft->nitems++] = t;
 
 	return FCLI_RC_FLAG_AGAIN;
@@ -9379,28 +9279,10 @@
 sigcont_handler(int sig)
 {
 	rec_sigcont = 1;
 }
 
-static void
-sigint_handler(int signo)
-{
-	rec_sigint = 1;
-}
-
-static void
-sigterm_handler(int signo)
-{
-	rec_sigterm = 1;
-}
-
-static bool
-fatal_signal(void)
-{
-	return (rec_sigpipe || rec_sigint || rec_sigterm);
-}
-
 __dead static void
 usage(void)
 {
 	/*
 	 * It looks like the fsl_cx f member of the ::fcli singleton has
@@ -9420,11 +9302,11 @@
 	/* If a command was passed on the CLI, output its corresponding help. */
 	if (fnc_init.cmdarg)
 		for (idx = 0; idx < nitems(fnc_init.cmd_args); ++idx) {
 			fcli_command cmd = fnc_init.cmd_args[idx];
 			if (!fsl_strcmp(fnc_init.cmdarg, cmd.name) ||
-			    !fcli_cmd_aliascmp(&cmd, fnc_init.cmdarg)) {
+			    fcli_cmd_aliascmp(&cmd, fnc_init.cmdarg)) {
 				if (!fsl_strcmp(cmd.name, "stash")) {
 					help_stash(&cmd);
 				} else
 					fcli_command_help(&cmd, true, true);
 				exit(fcli_end_of_main(fnc_init.err));
@@ -9453,15 +9335,14 @@
 
 static void
 usage_diff(void)
 {
 	fsl_fprintf(fnc_init.err ? stderr : stdout,
-	    " usage: %s diff  [-b|--brief][-C|--no-colour] [-h|--help] "
+	    " usage: %s diff [-C|--no-colour] [-R path] [-h|--help] "
 	    "[-i|--invert] [-l|--line-numbers] [-P|--no-prototype] "
-	    "[-q|--quiet] [-R path] [-s|--sbs] [-W|--whitespace-eol] "
-	    "[-w|--whitespace] [-x|--context n] [artifact1 [artifact2]] "
-	    "[path ...]\n"
+	    "[-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
@@ -9507,87 +9388,34 @@
 
 static void
 usage_stash(void)
 {
 	fsl_fprintf(fnc_init.err ? stderr : stdout,
-	    " usage: %s stash [(get|pop) [] | [-C|--no-colour] [-h|--help] "
+	    " usage: %s stash [get|pop | [-C|--no-colour] [-h|--help] "
 	    "[-P|--no-prototype] [-x|--context n]]\n"
 	    "  e.g.: %s stash -x 10\n\n", fcli_progname(), fcli_progname());
 }
 
 static void
 help_stash(const fcli_command *cmd)
 {
 	fcli_command_help(cmd, false, true);
 	f_out("[stash] subcommands:\n\n");
-	f_out("  get []\n    "
-	    "Apply the latest stash or stash  to the checkout.\n\n");
-	f_out("  pop []\n    Remove the latest stash or stash  "
+	f_out("  get\n    "
+	    "Apply the most recent stash changeset to the checkout.\n\n");
+	f_out("  pop\n    Remove the most recent stash changeset, "
 	    "and apply to the checkout.\n\n");
 	usage_stash();
-}
-
-static int
-fnc_stash_get(bool pop)
-{
-	fsl_cx		*const f = fcli_cx();
-	fsl_db		*db = fsl_needs_ckout(f);
-	char		*comment, *date;
-	fsl_uuid_str	 hash;
-	long		 stashid;
-	int		 rc = FSL_RC_OK;
-
-	if (fcli_next_arg(false)) {
-		rc = strtonumcheck(&stashid, fcli_next_arg(true), 1, INT_MAX);
-		if (rc)
-			return rc;
-	} else {
-		stashid = fsl_db_g_int32(db, 0,
-		    "SELECT max(stashid) FROM stash");
-		if (!stashid) {
-			f_out("empty stash");
-			return FSL_RC_OK;
-		}
-	}
-
-	if (!fsl_db_exists(db, "SELECT 1 FROM stash WHERE stashid=%d",
-	    stashid)) {
-		f_out("no such stash: %d", stashid);
-		return FSL_RC_OK;
-	}
-
-	comment = fsl_db_g_text(db, NULL,
-	    "SELECT comment FROM stash WHERE stashid=%d", stashid);
-	date = fsl_db_g_text(db, NULL, "SELECT datetime(ctime) "
-	    "FROM stash WHERE stashid=%d", stashid);
-	hash = fsl_db_g_text(db, NULL,
-	    "SELECT hash FROM stash WHERE stashid=%d", stashid);
-
-	rc = f__stash_get(stashid, pop);
-	if (rc)
-		goto end;
-
-	f_out("\n%s stash:\n%5d: [%.14s] from %s\n",
-	    pop ? "Popped" : "Applied", stashid, hash, date);
-	if (comment && *comment)
-		f_out("        %s", comment);
-end:
-	fsl_free(comment);
-	fsl_free(date);
-	fsl_free(hash);
-	return rc;
-
 }
 
 static int
 cmd_stash(fcli_command const *argv)
 {
 	fsl_cx				*const f = fcli_cx();
 	struct fnc_view			*view = NULL;
 	struct fnc_commit_artifact	*commit = NULL;
-	const char			*cmd;
-	fsl_id_t			 rid;
+	fsl_id_t			 prid = -1, rid = -1;
 	int				 rc = FSL_RC_OK;
 	enum fnc_diff_type		 diff_type = FNC_DIFF_CKOUT;
 	enum fnc_diff_mode		 diff_mode = STASH_INTERACTIVE;
 
 #ifdef __OpenBSD__
@@ -9597,39 +9425,40 @@
 
 	rc = fcli_process_flags(argv->flags);
 	if (rc || (rc = fcli_has_unused_flags(false)))
 		return rc;
 
-	cmd = fcli_next_arg(true);
-	if (cmd) {
+	if (fcli_next_arg(false)) {
+		const char *cmd;
 		bool pop = false;
-
-		if (fsl_strcmp(cmd, "get") && fsl_strcmp(cmd, "apply") &&
-		    !(pop = !strcmp(cmd, "pop")))
+		if (fsl_strcmp((cmd = fcli_next_arg(true)), "get") &&
+		    fsl_strcmp(cmd, "apply") && !(pop = !fsl_strcmp(cmd, "pop")))
 			return RC(FSL_RC_NOT_FOUND,
 			    "invalid stash subcommand: %s", cmd);
+		return f__stash_get(pop);
+	}
 
-		return fnc_stash_get(pop);
-	}
+	rc = fsl_sym_to_rid(f, "current", FSL_SATYPE_CHECKIN, &prid);
+	if (rc || prid < 0)
+		return RC(rc, "fsl_sym_to_rid");
 
 	fsl_ckout_version_info(f, &rid, NULL);
-	rc = fsl_ckout_changes_scan(f);
-	if (rc)
+	if ((rc = fsl_ckout_changes_scan(f)))
 		return RC(rc, "fsl_ckout_changes_scan");
 	if (!fsl_ckout_has_changes(f)) {
 		fsl_fprintf(stdout, "No local changes.\n");
 		return rc;
 	}
 
 	commit = calloc(1, sizeof(*commit));
 	if (commit == NULL)
 		return RC(fsl_errno_to_rc(errno, FSL_RC_ERROR), "calloc");
-	commit->prid = rid;
+	commit->prid = prid;
 	commit->rid = rid;
-	commit->puuid = fsl_rid_to_uuid(f, rid);
-	commit->uuid = fsl_strdup(commit->puuid);
-	commit->type = fsl_strdup("checkin");
+	commit->puuid = fsl_rid_to_uuid(f, prid);
+	commit->uuid = fsl_rid_to_uuid(f, rid);
+	commit->type = fsl_strdup("blob");
 	commit->diff_type = diff_type;
 
 	rc = init_curses();
 	if (rc)
 		goto end;
@@ -9650,25 +9479,21 @@
 		rc = RC(FSL_RC_ERROR, "view_open");
 		goto end;
 	}
 
 	rc = open_diff_view(view, commit, NULL, NULL, diff_mode);
-	if (rc)
-		goto end;
-
-	rc = show_diff(view);
-	if (rc)
-		goto end;
-	rc = fnc_stash(view);
-	if (rc)
-		goto end;
-	rc = fsl_vfile_changes_scan(f, rid, FSL_VFILE_CKSIG_HASH);
+	if (!rc) {
+		rc = show_diff(view);
+		if (!rc)
+			rc = fnc_stash(view);
+	}
 end:
 	/*
 	 * We must check for changes based on file content--not mtime--else
 	 * the lib will report files as unchanged in some cases.
 	 */
+	fsl_vfile_changes_scan(f, f->ckout.rid, FSL_VFILE_CKSIG_HASH);
 	if (commit)
 		fnc_commit_artifact_close(commit);
 	if (view)
 		view_close(view);
 	return rc;
@@ -9683,10 +9508,11 @@
 	struct fnc_pathlist_head	 paths;
 	struct fnc_pathlist_entry	*pe;
 	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;
 	int				 rc = FSL_RC_OK;
 	unsigned short			 blob = 0;
 	enum fnc_diff_type		 diff_type = FNC_DIFF_CKOUT;
 	enum fnc_diff_mode		 diff_mode = DIFF_PLAIN;
@@ -9718,71 +9544,86 @@
 			artifact2 = fcli_next_arg(true);
 			diff_type = FNC_DIFF_COMMIT;
 			if (!fsl_rid_is_a_checkin(f, rid))
 				++blob;
 		}
-	} else if (fcli.argc > 0 &&
-	    !fsl_sym_to_rid(f, fcli.argv[1], FSL_SATYPE_ANY, &prid))
-		return RC(FSL_RC_NOT_FOUND, "invalid hash prefix [%s]",
-		    fcli.argv[0]);
+	}
 	if (fcli_error()->code == FSL_RC_NOT_FOUND)
 		RC_RESET(rc);  /* If args aren't symbols, treat as paths. */
 	if (blob == 2)
 		diff_type = FNC_DIFF_BLOB;
 	if (!artifact1 && diff_type != FNC_DIFF_BLOB) {
 		artifact1 = "current";
 		rc = fsl_sym_to_rid(f, artifact1, FSL_SATYPE_CHECKIN, &prid);
-		if (rc || prid < 0)
-			return RC(rc, "fsl_sym_to_rid");
+		if (rc || prid < 0) {
+			rc = RC(rc, "fsl_sym_to_rid");
+			goto end;
+		}
 	}
 	if (!artifact2 && diff_type != FNC_DIFF_BLOB) {
 		fsl_ckout_version_info(f, &rid, NULL);
-		rc = fsl_ckout_changes_scan(f);
-		if (rc)
+		if ((rc = fsl_ckout_changes_scan(f)))
 			return RC(rc, "fsl_ckout_changes_scan");
-		if (diff_type == FNC_DIFF_CKOUT && !fsl_ckout_has_changes(f)) {
+		if (!fsl_strcmp(artifact1, "current") &&
+		    !fsl_ckout_has_changes(f)) {
 			fsl_fprintf(stdout, "No local changes.\n");
 			return rc;
 		}
 	}
 	while (fcli_next_arg(false) && diff_type != FNC_DIFF_BLOB) {
-		struct fnc_pathlist_entry	*ins;
-		char				*path;
-
-		rc = map_ckout_path(&path);
+		struct fnc_pathlist_entry *ins;
+		char *path, *path_to_diff;
+		rc = map_repo_path(&path0);
+		path = path0;
 		if (rc) {
-			if (rc != FSL_RC_NOT_FOUND) {
+			if (rc != FSL_RC_UNKNOWN_RESOURCE) {
 				if (!fsl_strcmp(artifact1, "current") &&
 				    !artifact2) {
 					rc = RC(rc, "invalid artifact hash: %s",
 					    path);
 				}
 				goto end;
 			}
-
+			RC_RESET(rc);
 			/* Path may be valid in tree of specified commit(s). */
-			RC_RESET(rc);
-
-			rc = valid_path(path, artifact1);
-			if (rc) {
-				if (rc != FSL_RC_UNKNOWN_RESOURCE || !artifact2)
+			const fsl_card_F *cf = NULL;
+			rc = fsl_deck_load_sym(f, &d, artifact1,
+			    FSL_SATYPE_CHECKIN);
+			if (rc)
+				goto end;
+			cf = fsl_deck_F_search(&d, path);
+			if (cf == NULL) {
+				if (!artifact2) {
+					rc = RC(FSL_RC_UNKNOWN_RESOURCE,
+					    "'%s' not found in tree [%s]", path,
+					    artifact1);
 					goto end;
-
-				rc = valid_path(path, artifact2);
-				if (rc == FSL_RC_UNKNOWN_RESOURCE)
-					rc = RC(rc, "path not found in tree "
-					    "[%s] or [%s]: %s",
-					    artifact1, artifact2, path);
+				}
+				fsl_deck_finalize(&d);
+				rc = fsl_deck_load_sym(f, &d, artifact2,
+				    FSL_SATYPE_CHECKIN);
 				if (rc)
 					goto end;
+				cf = fsl_deck_F_search(&d, path);
+				if (cf == NULL) {
+					rc = RC(FSL_RC_NOT_FOUND,
+					    "'%s' not found in trees [%s] [%s]",
+					    path, artifact1, artifact2);
+					goto end;
+				}
 			}
+		} else
+			while (path[0] == '/')
+				++path;
+		path_to_diff = fsl_strdup(path);
+		if (path_to_diff == NULL) {
+			rc = RC(FSL_RC_ERROR, "fsl_strdup");
+			goto end;
 		}
-		if (path == NULL)
-			break;  /* work tree root */
-		rc = fnc_pathlist_insert(&ins, &paths, path, NULL);
+		rc = fnc_pathlist_insert(&ins, &paths, path_to_diff, NULL);
 		if (rc || ins == NULL /* Duplicate path. */)
-			fsl_free(path);
+			fsl_free(path_to_diff);
 		if (rc)
 			goto end;
 	}
 
 	if (diff_type != FNC_DIFF_BLOB && diff_type != FNC_DIFF_CKOUT) {
@@ -9828,10 +9669,11 @@
 
 	rc = open_diff_view(view, commit, &paths, NULL, diff_mode);
 	if (!rc)
 		rc = view_loop(view);
 end:
+	fsl_free(path0);
 	fsl_deck_finalize(&d);
 	fsl_stmt_finalize(q);
 	if (commit)
 		fnc_commit_artifact_close(commit);
 	TAILQ_FOREACH(pe, &paths, entry)
@@ -9838,52 +9680,10 @@
 		free((char *)pe->path);
 	fnc_pathlist_free(&paths);
 	return rc;
 }
 
-/*
- * Check that path is either a valid file or dir in the tree identified by sym.
- */
-static int
-valid_path(const char *path, const char *sym)
-{
-	fsl_cx			*const f = fcli_cx();
-	fsl_deck		 d = fsl_deck_empty;
-	const fsl_card_F	*cf;
-	int			 rc;
-	bool			 valid = false;
-
-	rc = fsl_deck_load_sym(f, &d, sym, FSL_SATYPE_CHECKIN);
-	if (rc)
-		goto end;
-
-	valid = !!(cf = fsl_deck_F_search(&d, path));
-	if (valid)
-		goto end;
-
-	rc = fsl_deck_F_rewind(&d);
-	if (rc)
-		goto end;
-
-	do {
-		rc = fsl_deck_F_next(&d, &cf);
-		if (rc)
-			goto end;
-
-		if (cf && !fsl_strncmp(path, cf->name, fsl_strlen(path) - 1))
-			valid = true;
-
-	} while (cf && !valid);
-
-end:
-	fsl_deck_finalize(&d);
-	if (!valid && !rc)
-		rc = RC(FSL_RC_UNKNOWN_RESOURCE, "path not found in [%s]: %s",
-		    sym, path);
-	return rc;
-}
-
 static int
 browse_commit_tree(struct fnc_view **new_view, int start_col, int start_ln,
     struct commit_entry *entry, const char *path)
 {
 	struct fnc_view	*tree_view;
@@ -9913,21 +9713,13 @@
 
 	rc = fcli_process_flags(argv->flags);
 	if (rc || (rc = fcli_has_unused_flags(false)))
 		goto end;
 
-	rc = map_ckout_path(&path);
-	if (rc) {
-		if (rc != FSL_RC_NOT_FOUND || !fnc_init.sym)
-			goto end;
-
-		/* Path may be valid in tree of specified commit. */
-		RC_RESET(rc);
-		rc = valid_path(path, fnc_init.sym);
-		if (rc)
-			goto end;
-	}
+	rc = map_repo_path(&path);
+	if (rc)
+		goto end;
 	if (fnc_init.sym)
 		rc = fsl_sym_to_rid(f, fnc_init.sym, FSL_SATYPE_ANY, &rid);
 	else
 		fsl_ckout_version_info(f, &rid, NULL);
 
@@ -9944,10 +9736,11 @@
 		case FSL_RC_NOT_FOUND:
 			RC(rc, "invalid symbolic checkin name [%s]",
 			    fnc_init.sym);
 			goto end;
 		case FSL_RC_MISUSE:
+			/* FALL THROUGH */
 		default:
 			goto end;
 		}
 	}
 
@@ -10009,21 +9802,21 @@
 
 	/*
 	 * Open the initial root level of the repository tree now. Subtrees
 	 * opened during traversal are built and destroyed on demand.
 	 */
-	rc = tree_builder(s->repo, &s->root, NULL);
+	rc = tree_builder(s->repo, &s->root, "/");
 	if (rc)
 		goto end;
 	s->tree = s->root;
 	/*
 	 * If user has supplied a path arg (i.e., fnc tree path/in/repo), or
 	 * has selected a commit from an 'fnc timeline path/in/repo' command,
 	 * walk the path and open corresponding (sub)tree objects now.
 	 */
 	if (!fnc_path_is_root_dir(path)) {
-		rc = walk_tree_path(s, path, view->nlines - 4);
+		rc = walk_tree_path(s, s->repo, &s->root, path);
 		if (rc)
 			goto end;
 	}
 
 
@@ -10030,12 +9823,12 @@
 	if ((s->tree_label = fsl_mprintf("checkin %s", s->commit_id)) == NULL) {
 		rc = RC(FSL_RC_RANGE, "fsl_mprintf");
 		goto end;
 	}
 
-	s->first_entry_onscreen = s->selected_entry = s->tree == s->root ?
-	    &s->tree->entries[0] : NULL;
+	s->first_entry_onscreen = &s->tree->entries[0];
+	s->selected_entry = &s->tree->entries[0];
 
 	if (s->colour) {
 		STAILQ_INIT(&s->colours);
 		rc = set_colours(&s->colours, FNC_VIEW_TREE);
 		if (rc)
@@ -10054,15 +9847,14 @@
 }
 
 /*
  * Decompose the supplied path into its constituent components, then build,
  * open and visit each subtree segment on the way to the requested entry.
- * Display max n entries per page, so if each visited dir is more than n
- * entries deep, scroll just enough to make it the last entry on screen.
  */
 static int
-walk_tree_path(struct fnc_tree_view_state *s, const char *path, uint16_t n)
+walk_tree_path(struct fnc_tree_view_state *s, struct fnc_repository_tree *repo,
+    struct fnc_tree_object **root, const char *path)
 {
 	struct fnc_tree_object	*tree = NULL;
 	const char		*p;
 	char			*slash, *subpath = NULL;
 	int			 rc = 0;
@@ -10093,21 +9885,11 @@
 			fsl_free(te_name);
 			break;
 		}
 		fsl_free(te_name);
 
-		s->selected_entry = te;  /* dir matching provided path */
-
-		/*
-		 * If not in the root tree and the matching dir fits on the
-		 * first page, the first displayed entry should be ".." (NULL),
-		 * else scroll so the matching dir is the last entry on screen.
-		 */
-		s->first_entry_onscreen = &s->tree->entries[MAX(te->idx - n, 0)];
-		if (__predict_true(!s->first_entry_onscreen->idx &&
-		    s->selected_entry->idx < n && s->tree != s->root))
-			s->first_entry_onscreen = NULL;
+		s->first_entry_onscreen = s->selected_entry = te;
 		if (!S_ISDIR(s->selected_entry->mode))
 			break;	/* If a file, jump to this entry. */
 
 		slash = strchr(p, '/');
 		if (slash)
@@ -10117,11 +9899,11 @@
 		if (subpath == NULL) {
 			rc = RC(FSL_RC_ERROR, "fsl_strdup");
 			break;
 		}
 
-		rc = tree_builder(s->repo, &tree, subpath);
+		rc = tree_builder(repo, &tree, subpath + 1 /* Leading slash */);
 		if (rc)
 			break;
 		rc = visit_subtree(s, tree);
 		if (rc) {
 			fnc_object_tree_close(tree);
@@ -10220,24 +10002,26 @@
 		return RC(FSL_RC_ERROR, "fsl_malloc");
 	memset(*tree, 0, sizeof(**tree));
 
 	/*
 	 * Count how many elements will comprise the tree to be allocated.
-	 * If dir is the root of the repository tree (i.e., NULL), only tree
+	 * 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 && !dir) ||
-		    (tn->parent_dir && !fsl_strcmp(dir, tn->parent_dir->path)))
-			++i;
+	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;
+	}
 	(*tree)->entries = calloc(i, sizeof(struct fnc_tree_entry));
 	if ((*tree)->entries == NULL)
 		return RC(fsl_errno_to_rc(errno, FSL_RC_ERROR), "calloc");
 	/* Construct the tree to be displayed. */
-	for (tn = repo->head, i = 0; tn; tn = tn->next) {
-		if ((!tn->parent_dir && dir) ||
+	for(tn = repo->head, i = 0; tn; tn = tn->next) {
+		if ((!tn->parent_dir && fsl_strcmp(dir, "/")) ||
 		    (tn->parent_dir && fsl_strcmp(dir, tn->parent_dir->path)))
 			continue;
 		te = &(*tree)->entries[i];
 		te->mode = tn->mode;
 		te->mtime = tn->mtime;
@@ -10252,11 +10036,11 @@
 			return RC(FSL_RC_ERROR, "fsl_strdup");
 		te->idx = i++;
 	}
 	(*tree)->nentries = i;
 
-	return FSL_RC_OK;
+	return 0;
 }
 
 #if 0
 static void
 delete_tree_node(struct fnc_tree_entry **head, struct fnc_tree_entry *del)
@@ -10429,11 +10213,11 @@
 static int
 tree_entry_path(char **path, struct fnc_parent_trees *parents,
     struct fnc_tree_entry *te)
 {
 	struct fnc_parent_tree	*pt;
-	size_t			 len = 1;  /* NUL */
+	size_t			 len = 2;  /* Leading slash and NUL. */
 	int			 rc = 0;
 
 	TAILQ_FOREACH(pt, parents, entry)
 		len += strlen(pt->selected_entry->basename) + 1 /* slash */;
 	if (te)
@@ -10441,10 +10225,11 @@
 
 	*path = calloc(1, len);
 	if (path == NULL)
 		return RC(fsl_errno_to_rc(errno, FSL_RC_ERROR), "calloc");
 
+	(*path)[0] = '/';  /* Make it absolute from the repository root. */
 	pt = TAILQ_LAST(parents, fnc_parent_trees);
 	while (pt) {
 		const char *name = pt->selected_entry->basename;
 		if (strlcat(*path, name, len) >= len) {
 			rc = RC(FSL_RC_RANGE, "strlcat(%s, %s, %lu)",
@@ -10530,44 +10315,43 @@
 	fsl_free(wcstr);
 	wcstr = NULL;
 	if (--limit <= 0)
 		return rc;
 
-	/* Draw (sub)tree absolute repo path (prepend '/') so -1 from limit. */
-	rc = formatln(&wcstr, &wstrlen, treepath, view->ncols - 1, 0, false);
+	/* Write this (sub)tree's absolute repository path subheader. */
+	rc = formatln(&wcstr, &wstrlen, treepath, view->ncols, 0, false);
 	if (rc)
 		return rc;
-	wprintw(view->window, "/%ls", wcstr);	/* prepend slash */
+	waddwstr(view->window, wcstr);
 	fsl_free(wcstr);
 	wcstr = NULL;
-	if (wstrlen < view->ncols)
+	if (wstrlen < view->ncols - 1)
 		waddch(view->window, '\n');
 	if (--limit <= 0)
 		return rc;
 	waddch(view->window, '\n');
 	if (--limit <= 0)
 		return rc;
 
 	/* Write parent dir entry (i.e., "..") if top of the tree is in view. */
-	n = 0;
 	if (s->first_entry_onscreen == NULL) {
 		te = &s->tree->entries[0];
 		if (s->selected == 0) {
 			wattr_on(view->window, A_REVERSE, NULL);
 			s->selected_entry = NULL;
 		}
-		if (fsl_strlen(treepath)) {
-			waddstr(view->window, "  ..\n");
-			n = 1;
-		}
+		waddstr(view->window, "  ..\n");
 		if (s->selected == 0)
 			wattr_off(view->window, A_REVERSE, NULL);
 		++s->ndisplayed;
 		if (--limit <= 0)
 			return rc;
-	} else
+		n = 1;
+	} else {
+		n = 0;
 		te = s->first_entry_onscreen;
+	}
 
 	nentries = s->tree->nentries;
 	for (idx = 0; idx < nentries; ++idx)	/* Find max hash length. */
 		hashlen = MAX(fsl_strlen(s->tree->entries[idx].uuid), hashlen);
 	/* Iterate and write tree nodes postfixed with path type identifier. */
@@ -11021,15 +10805,10 @@
 	}
 
 	return FSL_RC_OK;
 }
 
-/*
- * Visit subtree by assigning the current tree, selected and first displayed
- * entries, and selected line index to a new parent tree node to be inserted
- * into the parents linked list. Then make subtree the current tree.
- */
 static int
 visit_subtree(struct fnc_tree_view_state *s, struct fnc_tree_object *subtree)
 {
 	struct fnc_parent_tree	*parent;
 
@@ -11038,30 +10817,23 @@
 		return RC(fsl_errno_to_rc(errno, FSL_RC_ERROR), "calloc");
 
 	parent->tree = s->tree;
 	parent->first_entry_onscreen = s->first_entry_onscreen;
 	parent->selected_entry = s->selected_entry;
-	/*
-	 * If not the first page of entries (".." isn't visible), the line to
-	 * select is the difference betwixt selected & first displayed entries.
-	 */
-	parent->selected = s->first_entry_onscreen ?
-	    s->selected_entry->idx - s->first_entry_onscreen->idx :
-	    s->selected_entry->idx + 1;
-
+	parent->selected = s->selected;
 	TAILQ_INSERT_HEAD(&s->parents, parent, entry);
 	s->tree = subtree;
 	s->selected = 0;
 	s->first_entry_onscreen = NULL;
 
-	return FSL_RC_OK;
+	return 0;
 }
 
 static int
 blame_tree_entry(struct fnc_view **new_view, int start_col, int start_ln,
     struct fnc_tree_entry *te, struct fnc_parent_trees *parents,
-    fsl_uuid_cstr commit_id)
+    fsl_uuid_str commit_id)
 {
 	struct fnc_view	*blame_view;
 	char		*path;
 	int		 rc = 0;
 
@@ -11286,13 +11058,16 @@
 	if (rc || (rc = fcli_has_unused_flags(false)))
 		return rc;
 
 	opt = fcli_next_arg(true);
 	if (opt == NULL || fnc_init.lsconf) {
-		if (fnc_init.unset)
-			return RC(FSL_RC_MISSING_INFO,
+		if (fnc_init.unset) {
+			fnc_init.err = RC(FSL_RC_MISSING_INFO,
 			    "-u|--unset requires ");
+			usage();
+			/* NOT REACHED */
+		}
 		return fnc_conf_lsopt(fnc_init.lsconf ? false : true);
 	}
 
 	setid = fnc_conf_str2enum(opt);
 	if (!setid)
@@ -11427,11 +11202,11 @@
 		    {LINE_DIFF_USER, init_colour(FNC_COLOUR_USER)},
 		    {LINE_DIFF_DATE, init_colour(FNC_COLOUR_DATE)},
 		    {LINE_DIFF_TAGS, init_colour(FNC_COLOUR_DIFF_TAGS)},
 		    {LINE_DIFF_MINUS, init_colour(FNC_COLOUR_DIFF_MINUS)},
 		    {LINE_DIFF_PLUS, init_colour(FNC_COLOUR_DIFF_PLUS)},
-		    {LINE_DIFF_HUNK, init_colour(FNC_COLOUR_DIFF_HUNK)},
+		    {LINE_DIFF_CHUNK, init_colour(FNC_COLOUR_DIFF_CHUNK)},
 		    {LINE_DIFF_EDIT, init_colour(FNC_COLOUR_DIFF_SBS_EDIT)}
 		};
 		rc = set_colour_scheme(s, pairs_diff, regexp_diff,
 		    nitems(regexp_diff));
 		break;
@@ -11615,11 +11390,11 @@
 	case FNC_COLOUR_DIFF_PLUS:
 	case FNC_COLOUR_TREE_DIR:
 	case FNC_COLOUR_BRANCH_OPEN:
 		return COLOR_CYAN;
 	case FNC_COLOUR_DATE:
-	case FNC_COLOUR_DIFF_HUNK:
+	case FNC_COLOUR_DIFF_CHUNK:
 	case FNC_COLOUR_BRANCH_PRIVATE:
 		return COLOR_YELLOW;
 	case FNC_COLOUR_DIFF_MINUS:
 	case FNC_COLOUR_DIFF_TAGS:
 	case FNC_COLOUR_TREE_LINK:
@@ -11721,11 +11496,11 @@
 {
 	fsl_cx		*const f = fcli_cx();
 	struct fnc_view	*view;
 	char		*path = NULL;
 	fsl_uuid_str	 commit_id = NULL;
-	fsl_id_t	 oid = 0, rid = 0;
+	fsl_id_t	 tip = 0, rid = 0;
 	long		 nlimit = 0;
 	int		 rc = 0;
 
 	rc = fcli_process_flags(argv->flags);
 	if (rc || (rc = fcli_has_unused_flags(false)))
@@ -11755,34 +11530,34 @@
 				rc = RC(FSL_RC_MISSING_INFO,
 				    "%s blame --reverse requires --commit",
 				    fcli_progname());
 				goto end;
 			}
-			rc = fsl_sym_to_rid(f, fnc_init.sym,
-			    FSL_SATYPE_CHECKIN, &oid);
-		} else
-			rc = fsl_sym_to_rid(f, fnc_init.sym,
-			    FSL_SATYPE_CHECKIN, &rid);
+			rc = fsl_sym_to_rid(f, "tip", FSL_SATYPE_CHECKIN, &tip);
+			if (rc)
+				goto end;
+		}
+		rc = fsl_sym_to_rid(f, fnc_init.sym, FSL_SATYPE_CHECKIN, &rid);
 		if (rc)
 			goto end;
 	} else if (!fnc_init.sym) {
 		fsl_ckout_version_info(f, &rid, NULL);
 		if (!rid)  /* -R|--repo option used */
 			fsl_sym_to_rid(f, "tip", FSL_SATYPE_CHECKIN, &rid);
 	}
 
-	rc = map_ckout_path(&path);
+	rc = map_repo_path(&path);
 	if (rc) {
-		if (rc != FSL_RC_NOT_FOUND || !fnc_init.sym)
+		if (rc != FSL_RC_UNKNOWN_RESOURCE || !fnc_init.sym)
 			goto end;
 		/* Path may be valid in repository tree of specified commit. */
 		RC_RESET(rc);
 	}
 
-	commit_id = fsl_rid_to_uuid(f, rid ? rid : f->ckout.rid);
-	if (!path) {
-		rc = RC(FSL_RC_MISSING_INFO,
+	commit_id = fsl_rid_to_uuid(f, rid);
+	if (rc || (path[0] == '/' && path[1] == '\0')) {
+		rc = rc ? rc : RC(FSL_RC_MISSING_INFO,
 		    "%s blame requires versioned file path", fcli_progname());
 		goto end;
 	}
 
 	rc = init_curses();
@@ -11797,11 +11572,11 @@
 	if (view == NULL) {
 		rc = RC(FSL_RC_ERROR, "view_open");
 		goto end;
 	}
 
-	rc = open_blame_view(view, path, commit_id, oid, nlimit,
+	rc = open_blame_view(view, path, commit_id, tip, nlimit,
 	    fnc_init.lineno);
 	if (rc)
 		goto end;
 	rc = view_loop(view);
 end:
@@ -11809,12 +11584,12 @@
 	fsl_free(commit_id);
 	return rc;
 }
 
 static int
-open_blame_view(struct fnc_view *view, char *path, fsl_uuid_cstr commit_id,
-    fsl_id_t oid, int nlimit, const char *lineno)
+open_blame_view(struct fnc_view *view, char *path, fsl_uuid_str commit_id,
+    fsl_id_t tip, int nlimit, const char *lineno)
 {
 	struct fnc_blame_view_state	*s = &view->state.blame;
 	int				 rc = 0;
 
 	CONCAT(STAILQ, _INIT)(&s->blamed_commits);
@@ -11835,11 +11610,11 @@
 	s->first_line_onscreen = 1;
 	s->last_line_onscreen = view->nlines;
 	s->selected_line = 1;
 	s->blame_complete = false;
 	s->commit_id = commit_id;
-	s->blame.origin = oid;
+	s->blame.origin = tip;
 	s->blame.nlimit = nlimit;
 	s->spin_idx = 0;
 	s->colour = !fnc_init.nocolour && has_colors();
 	s->lineno = lineno;
 
@@ -11868,22 +11643,30 @@
 	fsl_deck			 d = fsl_deck_empty;
 	fsl_buffer			 buf = fsl_buffer_empty;
 	fsl_annotate_opt		*opt = NULL;
 	const fsl_card_F		*cf;
 	char				*filepath = NULL;
+	char				*master = NULL, *root = NULL;
 	int				 rc = 0;
 
-	filepath = s->path;
+	/*
+	 * Trim prefixed '/' if path has been processed by map_repo_path(),
+	 * which only occurs when the -c option has not been passed.
+	 * XXX This slash trimming is cumbersome; we should not prefix a slash
+	 * in map_repo_path() as we only want the slash for displaying an
+	 * absolute-repository-relative path, so we should prefix it only then.
+	 */
+	filepath = s->path[0] != '/' ? s->path : s->path + 1;
 
 	rc = fsl_deck_load_sym(f, &d, s->blamed_commit->id, FSL_SATYPE_CHECKIN);
 	if (rc)
 		goto end;
 
 	cf = fsl_deck_F_search(&d, filepath);
 	if (cf == NULL) {
-		rc = RC(FSL_RC_UNKNOWN_RESOURCE, "path not found in [%s]: %s",
-		    s->blamed_commit->id, filepath);
+		rc = RC(FSL_RC_NOT_FOUND, "'%s' not found in tree [%s]",
+		    filepath, s->blamed_commit->id);
 		goto end;
 	}
 	rc = fsl_card_F_content(f, cf, &buf);
 	if (rc)
 		goto end;
@@ -11907,11 +11690,11 @@
 	fcli_fax((char *)opt->filename);
 	rc = fsl_sym_to_rid(f, s->blamed_commit->id, FSL_SATYPE_CHECKIN,
 	    &opt->versionRid);
 	if (rc)
 		goto end;
-	opt->originRid = blame->origin;    /* -c version when -r is passed */
+	opt->originRid = blame->origin;    /* tip when -r is passed */
 	if (blame->nlimit < 0)
 		opt->limitMs = abs(blame->nlimit) * 1000;
 	else
 		opt->limitVersions = blame->nlimit;
 	opt->out = blame_cb;
@@ -11941,15 +11724,35 @@
 	blame->lines = calloc(blame->nlines, sizeof(*blame->lines));
 	if (blame->lines == NULL) {
 		rc = RC(fsl_errno_to_rc(errno, FSL_RC_ERROR), "calloc");
 		goto end;
 	}
+
+	master = fsl_config_get_text(f, FSL_CONFDB_REPO, "main-branch", NULL);
+	if (master == NULL) {
+		master = fsl_strdup("trunk");
+		if (master == NULL) {
+			rc = RC(FSL_RC_ERROR, "fsl_strdup");
+			goto end;
+		}
+	}
+	root = fsl_mprintf("root:%s", master);
+	rc = fsl_sym_to_uuid(f, root, FSL_SATYPE_CHECKIN,
+	    &blame->cb_cx.root_commit, NULL);
+	if (rc) {
+		rc = RC(rc, "fsl_sym_to_uuid");
+		goto end;
+	}
 
 	blame->cb_cx.view = view;
 	blame->cb_cx.lines = blame->lines;
 	blame->cb_cx.nlines = blame->nlines;
-	blame->cb_cx.commit_id = s->blamed_commit->id;
+	blame->cb_cx.commit_id = fsl_strdup(s->blamed_commit->id);
+	if (blame->cb_cx.commit_id == NULL) {
+		rc = RC(FSL_RC_ERROR, "fsl_strdup");
+		goto end;
+	}
 	blame->cb_cx.quit = &s->done;
 
 	blame->thread_cx.path = s->path;
 	blame->thread_cx.cb_cx = &blame->cb_cx;
 	blame->thread_cx.complete = &s->blame_complete;
@@ -11963,10 +11766,12 @@
 		s->selected_line = 1;
 	}
 	s->matched_line = 0;
 	s->maxx = &blame->thread_cx.cb_cx->maxlen;
 end:
+	fsl_free(master);
+	fsl_free(root);
 	fsl_deck_finalize(&d);
 	fsl_buffer_clear(&buf);
 	if (rc)
 		stop_blame(blame);
 	return rc;
@@ -12155,13 +11960,15 @@
 		}
 		line->annotated = true;
 	} else
 		line->id = NULL;
 
-	/* -r can return lines with no version (cf. fossil(1)). */
-	if (opt->originRid && !line->id)
-		line->annotated = false;
+	/* -r can return lines with no version, so use root check-in. */
+	if (opt->originRid && !line->id) {
+		line->id = fsl_strdup(cx->root_commit);
+		line->annotated = true;
+	}
 
 	line->lineno = step->lineNumber;
 	cx->maxlen = MAX(step->lineLength, cx->maxlen);
 	++cx->nlines;
 end:
@@ -12180,11 +11987,11 @@
 	struct fnc_blame_line		*blame_line;
 	regmatch_t			*regmatch = &view->regmatch;
 	struct fnc_colour		*c = NULL;
 	wchar_t				*wcstr;
 	char				*line = NULL;
-	fsl_uuid_cstr			 prev_id = NULL;
+	fsl_uuid_str			 prev_id = NULL;
 	ssize_t				 linelen;
 	size_t				 linesz = 0;
 	int				 col, width, lineno = 0, nprinted = 0;
 	int				 rc = FSL_RC_OK;
 	const int			 idfield = 11;  /* Prefix + space. */
@@ -12221,11 +12028,11 @@
 	fsl_free(wcstr);
 	wcstr = NULL;
 
 	line = fsl_mprintf("[%d/%d] %s%s %c", s->gtl ? s->gtl :
 	    MIN(blame->nlines, s->first_line_onscreen - 1 + s->selected_line),
-	    blame->nlines, s->blame_complete ? "/" : "annotating... /",
+	    blame->nlines, s->blame_complete ? "" : "annotating... ",
 	    s->path, s->blame_complete ? ' ' : SPINNER[s->spin_idx]);
 	if (SPINNER[++s->spin_idx] == '\0')
 		s->spin_idx = 0;
 	rc = formatln(&wcstr, &width, line, view->ncols, 0, false);
 	fsl_free(line);
@@ -12257,11 +12064,11 @@
 		if (s->gtl)
 			if (!gotoline(view, &lineno, &nprinted))
 				continue;
 
 		if ((selected = nprinted == s->selected_line - 1)) {
-			rx = FNC_HIGHLIGHT;
+			rx = A_BOLD | A_REVERSE;
 			wattron(view->window, rx);
 		}
 
 		if (blame->nlines > 0) {
 			blame_line = &blame->lines[lineno - 1];
@@ -12538,37 +12345,50 @@
 			    "AND blob.rid=plink.pid AND plink.isprim", rid);
 			if (pid == NULL)
 				break;
 			/* Check file exists in parent check-in. */
 			rc = fsl_deck_load_sym(f, &d, pid, FSL_SATYPE_CHECKIN);
-			if (!rc)
-				rc = fsl_deck_F_rewind(&d);
-			if (!rc && fsl_deck_F_search(&d, s->path) == NULL) {
+			if (rc) {
+				fsl_deck_finalize(&d);
+				fsl_free(pid);
+				return RC(rc, "fsl_deck_load_sym");
+			}
+			rc = fsl_deck_F_rewind(&d);
+			if (rc) {
+				fsl_deck_finalize(&d);
+				fsl_free(pid);
+				return RC(rc, "fsl_deck_F_rewind");
+			}
+			if (fsl_deck_F_search(&d, s->path +
+			    (fnc_init.sym ? 0 : 1)) == NULL) {
 				sitrep(view, SR_ALL ^ SR_RESET,
-				    "-- /%s not in [%.12s] --", s->path, pid);
-				rc = FSL_RC_BREAK;
-			}
-			if (!rc)
-				rc = fnc_commit_qid_alloc(&s->blamed_commit,
-				    pid);
-			fsl_deck_finalize(&d);
-			fsl_free(pid);
+				    "-- %s not in [%.12s] --",
+				    s->path + (fnc_init.sym ? 0 : 1), pid);
+				fsl_deck_finalize(&d);
+				fsl_free(pid);
+				break;
+			}
+			rc = fnc_commit_qid_alloc(&s->blamed_commit, pid);
+			if (rc)
+				return rc;
 		} else {
 			if (!fsl_uuidcmp(id, s->blamed_commit->id))
 				break;
 			rc = fnc_commit_qid_alloc(&s->blamed_commit, id);
 		}
 		if (rc)
-			return rc == FSL_RC_BREAK ? FSL_RC_OK : rc;
+			break;
 		s->done = true;
 		rc = stop_blame(&s->blame);
 		s->done = false;
-		if (!rc) {
-			CONCAT(STAILQ, _INSERT_HEAD)(&s->blamed_commits,
-			    s->blamed_commit, entry);
-			rc = run_blame(view);
-		}
+		if (rc)
+			break;
+		CONCAT(STAILQ, _INSERT_HEAD)(&s->blamed_commits,
+		    s->blamed_commit, entry);
+		rc = run_blame(view);
+		if (rc)
+			break;
 		break;
 	}
 	case KEY_BACKSPACE:
 	case 'B': {
 		struct fnc_commit_qid *first;
@@ -12582,10 +12402,12 @@
 			break;
 		CONCAT(STAILQ, _REMOVE_HEAD)(&s->blamed_commits, entry);
 		fnc_commit_qid_free(s->blamed_commit);
 		s->blamed_commit = CONCAT(STAILQ, _FIRST)(&s->blamed_commits);
 		rc = run_blame(view);
+		if (rc)
+			break;
 		break;
 	}
 	case 'T':
 		if (view_is_parent(view))
 			start_col = view_split_start_col(view->start_col);
@@ -12671,11 +12493,11 @@
 	case KEY_RESIZE:
 		if (s->selected_line > view->nlines - 2) {
 			s->selected_line = MIN(s->blame.nlines,
 			    view->nlines - 2);
 		}
-		/* FALL THROUGH */
+		break;
 	default:
 		break;
 	}
 	return rc;
 }
@@ -12777,10 +12599,14 @@
 			fsl_free(blame->lines[idx].id);
 		fsl_free(blame->lines);
 		blame->lines = NULL;
 	}
 
+	fsl_free(blame->cb_cx.root_commit);
+	blame->cb_cx.root_commit = NULL;
+	fsl_free(blame->cb_cx.commit_id);
+	blame->cb_cx.commit_id = NULL;
 	fsl_free(blame->line_offsets);
 
 	return rc;
 }
 
@@ -13153,11 +12979,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 = 0;
+	attr_t				 rx = A_BOLD;
 
 	werase(view->window);
 	s->ndisplayed = 0;
 
 	limit = view->nlines;
@@ -13174,11 +13000,11 @@
 	if (rc) {
 		fsl_free(line);
 		return rc;
 	}
 	if (screen_is_shared(view) || view->active)
-		rx = FNC_HIGHLIGHT;
+		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);
@@ -13409,11 +13235,11 @@
 
 	tree_view = view_open(0, 0, 0, start_col, FNC_VIEW_TREE);
 	if (tree_view == NULL)
 		return RC(FSL_RC_ERROR, "view_open");
 
-	rc = open_tree_view(tree_view, NULL, rid);
+	rc = open_tree_view(tree_view, "/", rid);
 	if (!rc)
 		*new_view = tree_view;
 	return rc;
 }
 
@@ -13691,12 +13517,11 @@
 }
 
 static void
 fnc_show_version(void)
 {
-	printf("%s %s [%.10s] %.19s UTC", fcli_progname(), PRINT_VERSION,
-	    PRINT_HASH, PRINT_DATE);
+	printf("%s %s", fcli_progname(), PRINT_VERSION);
 }
 
 static int
 strtonumcheck(long *ret, const char *nstr, const int min, const int max)
 {
@@ -13769,31 +13594,31 @@
 }
 
 static int PRINTFV(3, 4)
 sitrep(struct fnc_view *view, int flags, const char *msg, ...)
 {
-	va_list args;
-
-	va_start(args, msg);
-	/* vw_printw(view->window, msg, args); */
-	wattr_on(view->window, A_BOLD, NULL);
-	wmove(view->window, view->nlines - 1, 0);
-	vw_printw(view->window, msg, args);
-	if (FLAG_CHK(flags, SR_CLREOL))
-		wclrtoeol(view->window);
-	wattr_off(view->window, A_BOLD, NULL);
-	va_end(args);
-	if (FLAG_CHK(flags, SR_UPDATE)) {
-		update_panels();
-		doupdate();
-	}
-	if (FLAG_CHK(flags, SR_RESET))
-		fcli_err_reset();
-	if (FLAG_CHK(flags, SR_SLEEP))
-		sleep(1);
-
-	return FSL_RC_OK;
+       va_list args;
+
+       va_start(args, msg);
+       /* vw_printw(view->window, msg, args); */
+       wattr_on(view->window, A_BOLD, NULL);
+       wmove(view->window, view->nlines - 1, 0);
+       vw_printw(view->window, msg, args);
+       if (FLAG_CHK(flags, SR_CLREOL))
+               wclrtoeol(view->window);
+       wattr_off(view->window, A_BOLD, NULL);
+       va_end(args);
+       if (FLAG_CHK(flags, SR_UPDATE)) {
+               update_panels();
+               doupdate();
+       }
+       if (FLAG_CHK(flags, SR_RESET))
+               fcli_err_reset();
+       if (FLAG_CHK(flags, SR_SLEEP))
+               sleep(1);
+
+       return FSL_RC_OK;
  }
 
 /*
  * Attempt to parse string d, which must resemble either an ISO8601 formatted
  * date (e.g., 2021-10-10, 2020-01-01T10:10:10), disgregarding any trailing
@@ -13936,11 +13761,11 @@
 {
 #ifdef __OpenBSD__
 	int i;
 
 	for (i = 0; i < n; ++i) {
-		if (paths[i] && unveil(paths[i], perms[i]) == -1)
+		if (unveil(paths[i], perms[i]) == -1)
 			return RC(fsl_errno_to_rc(errno, FSL_RC_ACCESS),
 			    "unveil(%s, \"%s\")", paths[i], perms[i]);
 	}
 
 	if (disable)