Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26ff936df0 | ||
|
|
b412a45053 | ||
|
|
ac28b8c3ba | ||
|
|
2adb741c33 | ||
|
|
a38ab117c5 | ||
|
|
903a4da7ab |
No files matched your search
@@ -26,6 +26,10 @@ CHANGELOG
|
||||
```
|
||||
- nushell
|
||||
- The completion integration now requires nushell 0.116.1 or above, where an interactive completer can take the terminal through the `@interactive` attribute (#4934) (@sim590)
|
||||
- Fixed `--no-sort` being ignored in filter mode with `--sync`, `--tac`, or `--bench` (#4940) (@NavpreetSSidhu)
|
||||
- Vim plugin: Fixed garbage text on screen in Neovim with `set title` when the fzf command contains ANSI escape sequences (#4941)
|
||||
- Fixed `--walker=follow` skipping a symlink in one `--walker-root` when its target is an ancestor of another root (#4944) (@mabunemeh)
|
||||
- Fixed O(n^2) action parsing in `--listen`. A single ~1 MB request could block the server for ~30 s (@zachariah-mithani, AploSec)
|
||||
|
||||
0.74.5
|
||||
------
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
|
||||
<kbd align="center">
|
||||
<br/>
|
||||
<a href="https://commitgoods.com/collections/fzf"><img src="https://junegunn.github.io/fzf/images/fzf-mugs.jpg" width="80%" alt="fzf merch"></a>
|
||||
<a href="https://commitgoods.com/collections/fzf?utm_source=github&utm_medium=referral"><img src="https://junegunn.github.io/fzf/images/fzf-mugs.jpg" width="80%" alt="fzf merch"></a>
|
||||
<br/>
|
||||
<br/>
|
||||
Show your love for fzf -- T-shirts, mugs, and stickers now available!
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="https://commitgoods.com/collections/fzf">commitgoods.com/collections/fzf</a>
|
||||
<a href="https://commitgoods.com/collections/fzf?utm_source=github&utm_medium=referral"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cdn.shopify.com/s/files/1/0101/8752/files/commitgoods-badge-stacked-navy-dark.svg"><img src="https://cdn.shopify.com/s/files/1/0101/8752/files/commitgoods-badge-stacked-navy.svg" height="28" alt="Official merch on commitgoods.com"></picture></a>
|
||||
<br/>
|
||||
<br/>
|
||||
</kbd>
|
||||
@@ -1150,7 +1150,7 @@ Copyright (c) 2013-2026 Junegunn Choi
|
||||
Goods
|
||||
-----
|
||||
|
||||
Grab fzf T-shirts, mugs, and stickers here: https://commitgoods.com/collections/fzf
|
||||
Grab fzf T-shirts, mugs, and stickers here: [commitgoods.com/collections/fzf](https://commitgoods.com/collections/fzf?utm_source=github&utm_medium=referral)
|
||||
|
||||
Sponsors :heart:
|
||||
----------------
|
||||
|
||||
@@ -1104,6 +1104,12 @@ function! s:execute_term(dict, command, temps) abort
|
||||
let fzf.temps.batchfile = s:fzf_tempname().'.bat'
|
||||
call s:writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
|
||||
let command = fzf.temps.batchfile
|
||||
elseif has('nvim') && &title && a:command =~ '[[:cntrl:]]'
|
||||
" Neovim puts the command in the buffer name, which can end up raw in
|
||||
" the terminal title (#4941)
|
||||
let fzf.temps.shellscript = s:fzf_tempname()
|
||||
call s:writefile([a:command], fzf.temps.shellscript)
|
||||
let command = 'sh '.fzf#shellescape(fzf.temps.shellscript)
|
||||
else
|
||||
let command = a:command
|
||||
endif
|
||||
|
||||
@@ -264,7 +264,6 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
|
||||
pattern := patternBuilder([]rune(*opts.Filter))
|
||||
matcher.sort = pattern.sortable
|
||||
|
||||
transformer := buildItemTransformer(opts)
|
||||
|
||||
|
||||
+7
-6
@@ -1678,15 +1678,15 @@ func init() {
|
||||
}
|
||||
|
||||
func maskActionContents(action string) string {
|
||||
masked := ""
|
||||
var sb strings.Builder
|
||||
Loop:
|
||||
for len(action) > 0 {
|
||||
loc := argActionRegexp.FindStringIndex(action)
|
||||
if loc == nil {
|
||||
masked += action
|
||||
sb.WriteString(action)
|
||||
break
|
||||
}
|
||||
masked += action[:loc[1]]
|
||||
sb.WriteString(action[:loc[1]])
|
||||
action = action[loc[1]:]
|
||||
if len(action) == 0 {
|
||||
break
|
||||
@@ -1695,7 +1695,7 @@ Loop:
|
||||
var ce string
|
||||
switch action[0] {
|
||||
case ':':
|
||||
masked += strings.Repeat(" ", len(action))
|
||||
sb.WriteString(strings.Repeat(" ", len(action)))
|
||||
break Loop
|
||||
case '(':
|
||||
ce = ")"
|
||||
@@ -1716,7 +1716,7 @@ Loop:
|
||||
// @$ or @+
|
||||
loc = regexp.MustCompile(fmt.Sprintf(`(?s)^%s.*?(%s[+,]|%s$)`, cs, ce, ce)).FindStringIndex(action)
|
||||
if loc == nil {
|
||||
masked += action
|
||||
sb.WriteString(action)
|
||||
break
|
||||
}
|
||||
// Keep + or , at the end
|
||||
@@ -1724,9 +1724,10 @@ Loop:
|
||||
if lastChar == '+' || lastChar == ',' {
|
||||
loc[1]--
|
||||
}
|
||||
masked += strings.Repeat(" ", loc[1])
|
||||
sb.WriteString(strings.Repeat(" ", loc[1]))
|
||||
action = action[loc[1]:]
|
||||
}
|
||||
masked := sb.String()
|
||||
masked = strings.ReplaceAll(masked, ",,,", string([]rune{',', escapedComma, ','}))
|
||||
masked = strings.ReplaceAll(masked, ",:,", string([]rune{',', escapedColon, ','}))
|
||||
masked = strings.ReplaceAll(masked, "::", string([]rune{escapedColon, ':'}))
|
||||
|
||||
+20
-22
@@ -275,22 +275,12 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
Sort: fastwalk.SortFilesFirst,
|
||||
}
|
||||
|
||||
// When following symlinks, precompute the absolute real paths of walker
|
||||
// roots so we can skip symlinks that point to an ancestor. fastwalk's
|
||||
// built-in loop detection (shouldTraverse) catches loops on the second
|
||||
// pass, but a single pass through a symlink like z: -> / already
|
||||
// traverses the entire root filesystem, causing severe resource
|
||||
// exhaustion. Skipping ancestor symlinks prevents this entirely.
|
||||
var absRoots []string
|
||||
if opts.follow {
|
||||
for _, root := range roots {
|
||||
if real, err := filepath.EvalSymlinks(root); err == nil {
|
||||
if abs, err := filepath.Abs(real); err == nil {
|
||||
absRoots = append(absRoots, filepath.Clean(abs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// When following symlinks, skip targets that point to an ancestor of the
|
||||
// current walker root. fastwalk's built-in loop detection (shouldTraverse)
|
||||
// catches loops on the second pass, but a single pass through a symlink
|
||||
// like z: -> / already traverses the entire root filesystem, causing
|
||||
// severe resource exhaustion. Skipping ancestor symlinks prevents this.
|
||||
var absRoot string
|
||||
|
||||
ignoresBase := []string{}
|
||||
ignoresFull := []string{}
|
||||
@@ -326,19 +316,17 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
return filepath.SkipDir
|
||||
}
|
||||
// Skip symlinks whose target is an ancestor of (or equal to)
|
||||
// any walker root. Following such symlinks would traverse a
|
||||
// the current walker root. Following such symlinks would traverse a
|
||||
// superset of the tree we're already walking.
|
||||
if isDirSymlink && len(absRoots) > 0 {
|
||||
if isDirSymlink && absRoot != "" {
|
||||
if target, err := filepath.EvalSymlinks(path); err == nil {
|
||||
if abs, err := filepath.Abs(target); err == nil {
|
||||
abs = filepath.Clean(abs)
|
||||
if abs == string(os.PathSeparator) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, absRoot := range absRoots {
|
||||
if absRoot == abs || strings.HasPrefix(absRoot, abs+string(os.PathSeparator)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if absRoot == abs || strings.HasPrefix(absRoot, abs+string(os.PathSeparator)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,6 +365,16 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
}
|
||||
noerr := true
|
||||
for _, root := range roots {
|
||||
// Compare against this root only. Safe to update here because
|
||||
// fastwalk.Walk returns after all its workers finish.
|
||||
absRoot = ""
|
||||
if opts.follow {
|
||||
if real, err := filepath.EvalSymlinks(root); err == nil {
|
||||
if abs, err := filepath.Abs(real); err == nil {
|
||||
absRoot = filepath.Clean(abs)
|
||||
}
|
||||
}
|
||||
}
|
||||
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
|
||||
}
|
||||
return noerr
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -66,3 +70,70 @@ func TestReadFromCommand(t *testing.T) {
|
||||
t.Error("EvtReadFin should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func walkFiles(t *testing.T, roots ...string) []string {
|
||||
t.Helper()
|
||||
var mu sync.Mutex
|
||||
files := []string{}
|
||||
reader := NewReader(func(data []byte) bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
files = append(files, filepath.FromSlash(string(data)))
|
||||
return true
|
||||
}, util.NewEventBox(), nil, false, false)
|
||||
if !reader.readFiles(roots, walkerOpts{file: true, follow: true}, nil) {
|
||||
t.Fatal("file walk failed")
|
||||
}
|
||||
slices.Sort(files)
|
||||
return files
|
||||
}
|
||||
|
||||
func TestReadFilesSymlinkAcrossRoots(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
app := filepath.Join(base, "app")
|
||||
shared := filepath.Join(base, "shared")
|
||||
frontend := filepath.Join(shared, "frontend")
|
||||
os.MkdirAll(app, 0755)
|
||||
os.MkdirAll(frontend, 0755)
|
||||
os.WriteFile(filepath.Join(shared, "docs.txt"), nil, 0644)
|
||||
os.WriteFile(filepath.Join(frontend, "index.js"), nil, 0644)
|
||||
if err := os.Symlink(filepath.Join("..", "shared"), filepath.Join(app, "shared")); err != nil {
|
||||
t.Skipf("cannot create symlink: %v", err)
|
||||
}
|
||||
|
||||
viaApp := []string{
|
||||
filepath.Join(app, "shared", "docs.txt"),
|
||||
filepath.Join(app, "shared", "frontend", "index.js"),
|
||||
}
|
||||
both := append(slices.Clone(viaApp), filepath.Join(frontend, "index.js"))
|
||||
slices.Sort(both)
|
||||
for _, tc := range []struct {
|
||||
roots []string
|
||||
want []string
|
||||
}{
|
||||
{[]string{app}, viaApp},
|
||||
{[]string{app, frontend}, both},
|
||||
{[]string{frontend, app}, both},
|
||||
} {
|
||||
if files := walkFiles(t, tc.roots...); !slices.Equal(files, tc.want) {
|
||||
t.Errorf("roots = %v, files = %v, want %v", tc.roots, files, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFilesSkipsAncestorSymlinks(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
project := filepath.Join(base, "project")
|
||||
os.Mkdir(project, 0755)
|
||||
file := filepath.Join(project, "file.txt")
|
||||
os.WriteFile(file, nil, 0644)
|
||||
os.WriteFile(filepath.Join(base, "outside.txt"), nil, 0644)
|
||||
for name, target := range map[string]string{"parent": "..", "self": "."} {
|
||||
if err := os.Symlink(target, filepath.Join(project, name)); err != nil {
|
||||
t.Skipf("cannot create symlink: %v", err)
|
||||
}
|
||||
}
|
||||
if files := walkFiles(t, project); !slices.Equal(files, []string{file}) {
|
||||
t.Errorf("files = %v, want [%s]", files, file)
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,15 @@ class TestFilter < TestBase
|
||||
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
|
||||
end
|
||||
|
||||
def test_filter_no_sort
|
||||
input = "printf 'abz\\nab\\nzab\\n'"
|
||||
['', '--sync'].each do |opts|
|
||||
assert_equal %w[ab abz zab], `#{input} | #{FZF} -f ab #{opts}`.lines(chomp: true)
|
||||
assert_equal %w[abz ab zab], `#{input} | #{FZF} -f ab --no-sort #{opts}`.lines(chomp: true)
|
||||
assert_equal %w[zab ab abz], `#{input} | #{FZF} -f ab --no-sort --tac #{opts}`.lines(chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_filter_exitstatus
|
||||
# filter / streaming filter
|
||||
['', '--no-sort'].each do |opts|
|
||||
|
||||
Reference in new issue
Block a user