Compare commits

...

6 Commits

Author SHA1 Message Date
Junegunn Choi ce4bef7595 0.73.1 2026-05-25 14:35:03 +09:00
Junegunn Choi 25868a62f7 Address code scanning alerts 2026-05-25 14:31:51 +09:00
Junegunn Choi 7963a2c658 server: avoid O(n^2) body accumulation in HTTP listener
- handleHttpRequest used `body += text` per token, allocating a new
  backing array on every append (O(n^2) total copy work)
- a single ~390 KB POST monopolised the single-threaded server for
  ~8 s, blocking all other --listen clients
- switch to strings.Builder for amortised O(n)

Reported with fix by Michal Majchrowicz and Marcin Wyczechowski
(AFINE Team).
2026-05-25 14:10:52 +09:00
Junegunn Choi 4b23aa45a8 Skip FZF_CURRENT_ITEM export when item contains NUL
- exec(2) rejects env entries containing NUL, breaking preview and
  other child commands when the input has NUL bytes
- skip the export and document the limitation

Fix #2395
2026-05-25 14:06:50 +09:00
Junegunn Choi 3953d1c649 Add make tag for release tagging
- `prerelease`: version-consistency grep across CHANGELOG, install,
  install.ps1, and both man pages (extracted from `release` target)
- `tag`: depends on `prerelease`; signs and pushes the version tag
- RELEASE.md: replace manual tag/push steps with `make tag`
2026-05-24 01:19:01 +09:00
Junegunn Choi 5e137613d3 Add GitHub Actions release workflow
- Triggers on tag push (v*); fires real release
- workflow_dispatch for dry runs (--snapshot --skip=publish)
- Gated by `release` environment with required reviewer
- RELEASE.md documents tag-only push flow and dispatch testing
2026-05-24 00:59:41 +09:00
11 changed files with 172 additions and 11 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to validate (e.g. 0.73.0).'
type: string
required: true
permissions:
contents: write
jobs:
release:
runs-on: macos-latest
environment: release
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Determine version
id: ver
run: |
if [ "${{ github.event_name }}" = "push" ]; then
v=${GITHUB_REF_NAME#v}
else
v='${{ inputs.version }}'
fi
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "Resolved version: '$v'"
- name: Verify version consistency
run: |
set -e
V='${{ steps.ver.outputs.version }}'
R=$(echo "$V" | sed 's/\./\\./g')
grep -q "^${R}$" CHANGELOG.md
grep -qF "\"fzf ${V}\"" man/man1/fzf.1
grep -qF "\"fzf ${V}\"" man/man1/fzf-tmux.1
grep -qF "${V}" install
grep -qF "${V}" install.ps1
- name: Extract release notes
run: |
set -e
mkdir -p tmp
V='${{ steps.ver.outputs.version }}'
R=$(echo "$V" | sed 's/\./\\./g')
sed -n "/^${R}$/,/^[0-9]/p" CHANGELOG.md \
| tail -r | sed '1,/^ *$/d' | tail -r | sed '1,2d' \
| tee tmp/release-note
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: >-
${{ github.event_name == 'push'
&& 'release --clean --release-notes tmp/release-note'
|| 'release --snapshot --clean --skip=publish' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
+6
View File
@@ -1,6 +1,12 @@
CHANGELOG
=========
0.73.1
------
- Bug fixes
- Skip `$FZF_CURRENT_ITEM` export when the item contains a NUL byte; `exec(2)` rejects the env, breaking preview and other child commands (#2395)
- Fixed O(n^2) HTTP body accumulation in `--listen`; a single ~390 KB request could block the single-threaded server for ~8 s (Michal Majchrowicz, Marcin Wyczechowski, AFINE Team)
0.73.0
------
_Release highlights: https://junegunn.github.io/fzf/releases/0.73.0/_
+14 -1
View File
@@ -117,6 +117,19 @@ generate:
build:
goreleaser build --clean --snapshot --skip=post-hooks
prerelease:
# Check if version numbers are properly updated
grep -q ^$(VERSION_REGEX)$$ CHANGELOG.md
grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf.1
grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf-tmux.1
grep -qF $(VERSION) install
grep -qF $(VERSION) install.ps1
@echo "OK: all files consistent at $(VERSION)"
tag: prerelease
git tag -s v$(VERSION) -m v$(VERSION)
git push origin v$(VERSION)
release:
# Make sure that the tests pass and the build works
TAGS=tcell make test
@@ -206,4 +219,4 @@ update:
$(GO) get -u
$(GO) mod tidy
.PHONY: all generate build release test itest bench lint install clean docker docker-test update fmt
.PHONY: all generate build prerelease tag release test itest bench lint install clean docker docker-test update fmt
+54
View File
@@ -0,0 +1,54 @@
Release process
===============
Building, signing, notarizing, and publishing is handled by
[`.github/workflows/release.yml`](.github/workflows/release.yml),
triggered by a tag push.
## Steps
1. Update version in the following files and commit on `master`:
- `CHANGELOG.md`
- `main.go`
- `install`
- `install.ps1`
- `man/man1/fzf.1`
- `man/man1/fzf-tmux.1`
2. Verify file consistency, sign the tag, and push the tag.
```sh
make tag VERSION=0.73.1
```
`make tag` runs `prerelease` first (checks that the version
appears in CHANGELOG.md, both man pages, install, and install.ps1)
and only signs + pushes the tag if the checks pass.
Only the tag is pushed; `master` on origin still points to the
old version, so `/master/install` keeps resolving against existing
binaries during the publish window.
3. The workflow fires on the tag push and pauses on the `release`
environment gate. Approve it in the Actions tab to release.
4. After the GitHub release is published, fast-forward `master`:
```sh
git push origin master
```
## Testing the workflow
To exercise the workflow without firing a real release:
1. Actions tab -> **Release** -> **Run workflow**.
2. Pick a branch and enter the version currently on that branch
(the version-consistency check requires the input to match the
files in the checked-out tree).
3. Approve the `release` environment gate when prompted.
4. Goreleaser runs with `--snapshot --skip=publish`. Signing and
notarization run; only the GitHub release upload is skipped.
Use this to validate the workflow YAML, version-extraction logic,
the macOS runner setup, and the signing/notarization credentials.
+1 -1
View File
@@ -2,7 +2,7 @@
set -u
version=0.73.0
version=0.73.1
auto_completion=
key_bindings=
update_config=2
+1 -1
View File
@@ -1,4 +1,4 @@
$version="0.73.0"
$version="0.73.1"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
+1 -1
View File
@@ -21,7 +21,7 @@ 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.
..
.TH fzf\-tmux 1 "May 2026" "fzf 0.73.0" "fzf\-tmux - open fzf in tmux split pane"
.TH fzf\-tmux 1 "May 2026" "fzf 0.73.1" "fzf\-tmux - open fzf in tmux split pane"
.SH NAME
fzf\-tmux - open fzf in tmux split pane
+5 -1
View File
@@ -21,7 +21,7 @@ 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.
..
.TH fzf 1 "May 2026" "fzf 0.73.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "May 2026" "fzf 0.73.1" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -1531,6 +1531,10 @@ fzf exports the following environment variables to its child processes.
.br
.BR FZF_RAW " Only in raw mode. 1 if the current item matches, 0 otherwise"
.PP
.B FZF_CURRENT_ITEM
is omitted when the item contains a NUL byte, because exec(2) cannot pass it.
.SH EXTENDED SEARCH MODE
Unless specified otherwise, fzf will start in "extended\-search mode". In this
+5 -1
View File
@@ -4,6 +4,7 @@ package fzf
import (
"fmt"
"maps"
"math"
"os"
"sync"
"time"
@@ -241,6 +242,9 @@ func Run(opts *Options) (int, error) {
denylist = make(map[int32]struct{})
denyMutex.Unlock()
}
if opts.HeaderLines > math.MaxInt32 {
opts.HeaderLines = math.MaxInt32
}
headerLines := int32(opts.HeaderLines)
headerUpdated := false
patternBuilder := func(runes []rune) *Pattern {
@@ -467,7 +471,7 @@ func Run(opts *Options) (int, error) {
terminal.UpdateCount(max(0, total-int(headerLines)), !reading, value.(*string))
if headerLines > 0 && !headerUpdated {
terminal.UpdateHeader(GetItems(snapshot, int(headerLines)))
headerUpdated = int32(total) >= headerLines
headerUpdated = total >= int(headerLines)
}
if heightUnknown && !deferred {
determine(!reading)
+4 -3
View File
@@ -153,7 +153,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
contentLength := 0
apiKey := ""
body := ""
var bodyBuilder strings.Builder
answer := func(code string, message string) string {
message += "\n"
return code + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
@@ -175,7 +175,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
token := data[:found+len(crlf)]
return len(token), token, nil
}
if atEOF || len(body)+len(data) >= contentLength {
if atEOF || bodyBuilder.Len()+len(data) >= contentLength {
return 0, data, bufio.ErrFinalToken
}
return 0, nil, nil
@@ -218,7 +218,7 @@ Loop:
}
}
case 2: // Request body
body += text
bodyBuilder.WriteString(text)
}
}
@@ -234,6 +234,7 @@ Loop:
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
}
body := bodyBuilder.String()
if len(body) < contentLength {
return bad("incomplete request")
}
+5 -2
View File
@@ -1439,7 +1439,10 @@ func (t *Terminal) environImpl(forPreview bool) []string {
env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns))
env = append(env, fmt.Sprintf("FZF_POS=%d", min(t.merger.Length(), t.cy+1)))
if item := t.currentItem(); item != nil {
env = append(env, "FZF_CURRENT_ITEM="+item.AsString(t.ansi))
// Skip if the value contains a NUL byte; exec(2) would reject the env.
if s := item.AsString(t.ansi); !strings.ContainsRune(s, 0) {
env = append(env, "FZF_CURRENT_ITEM="+s)
}
}
env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_LINE=%d", t.clickHeaderLine))
env = append(env, fmt.Sprintf("FZF_CLICK_HEADER_COLUMN=%d", t.clickHeaderColumn))
@@ -6826,7 +6829,7 @@ func (t *Terminal) Loop() error {
changed = true
// Deselect items that are now part of the header
for idx := range t.selected {
if idx < int32(n) {
if int(idx) < n {
delete(t.selected, idx)
}
}