diff --git a/modules/git/grep.go b/modules/git/grep.go index 5572bd994f..1e34f0275c 100644 --- a/modules/git/grep.go +++ b/modules/git/grep.go @@ -27,12 +27,20 @@ type GrepResult struct { HighlightedRanges [][3]int } +type grepMode int + +const ( + FixedGrepMode grepMode = iota + FixedAnyGrepMode + RegExpGrepMode +) + type GrepOptions struct { RefName string MaxResultLimit int MatchesPerFile int ContextLineNumber int - IsFuzzy bool + Mode grepMode PathSpec []setting.Glob } @@ -75,11 +83,16 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO // -I skips binary files cmd := NewCommand(ctx, "grep", "-I", "--null", "--break", "--heading", "--column", - "--fixed-strings", "--line-number", "--ignore-case", "--full-name") + "--line-number", "--ignore-case", "--full-name") + if opts.Mode == RegExpGrepMode { + cmd.AddArguments("--perl-regexp") + } else { + cmd.AddArguments("--fixed-strings") + } cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber)) cmd.AddOptionValues("--max-count", fmt.Sprint(opts.MatchesPerFile)) words := []string{search} - if opts.IsFuzzy { + if opts.Mode == FixedAnyGrepMode { words = strings.Fields(search) } for _, word := range words { diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 3ba7a6efcb..835f441b19 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -201,3 +201,34 @@ func TestGrepRefs(t *testing.T) { assert.Len(t, res, 1) assert.Equal(t, "A", res[0].LineCodes[0]) } + +func TestGrepCanHazRegexOnDemand(t *testing.T) { + tmpDir := t.TempDir() + + err := InitRepository(DefaultContext, tmpDir, false, Sha1ObjectFormat.Name()) + require.NoError(t, err) + + gitRepo, err := openRepositoryWithDefaultContext(tmpDir) + require.NoError(t, err) + defer gitRepo.Close() + + require.NoError(t, os.WriteFile(path.Join(tmpDir, "matching"), []byte("It's a match!"), 0o666)) + require.NoError(t, os.WriteFile(path.Join(tmpDir, "not-matching"), []byte("Orisitamatch?"), 0o666)) + + err = AddChanges(tmpDir, true) + require.NoError(t, err) + + err = CommitChanges(tmpDir, CommitChangesOptions{Message: "Add fixtures for regexp test"}) + require.NoError(t, err) + + // should find nothing by default... + res, err := GrepSearch(context.Background(), gitRepo, "\\bmatch\\b", GrepOptions{}) + require.NoError(t, err) + assert.Empty(t, res) + + // ... unless configured explicitly + res, err = GrepSearch(context.Background(), gitRepo, "\\bmatch\\b", GrepOptions{Mode: RegExpGrepMode}) + require.NoError(t, err) + assert.Len(t, res, 1) + assert.Equal(t, "matching", res[0].Filename) +} diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index c4f9f9afd1..9a033c0f1b 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -17,16 +17,55 @@ import ( const tplSearch base.TplName = "repo/search" +type searchMode int + +const ( + ExactSearchMode searchMode = iota + FuzzySearchMode + RegExpSearchMode +) + +func searchModeFromString(s string) searchMode { + switch s { + case "fuzzy": + return FuzzySearchMode + case "regexp": + return RegExpSearchMode + default: + return ExactSearchMode + } +} + +func (m searchMode) String() string { + switch m { + case ExactSearchMode: + return "exact" + case FuzzySearchMode: + return "fuzzy" + case RegExpSearchMode: + return "regexp" + default: + panic("cannot happen") + } +} + // Search render repository search page func Search(ctx *context.Context) { language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") - isFuzzy := ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) + mode := ExactSearchMode + if modeStr := ctx.FormString("mode"); len(modeStr) > 0 { + mode = searchModeFromString(modeStr) + } else if ctx.FormOptionalBool("fuzzy").ValueOrDefault(true) { // for backward compatibility in links + mode = FuzzySearchMode + } ctx.Data["Keyword"] = keyword ctx.Data["Language"] = language - ctx.Data["IsFuzzy"] = isFuzzy + ctx.Data["IsFuzzy"] = mode == FuzzySearchMode + ctx.Data["IsRegExp"] = mode == RegExpSearchMode + ctx.Data["SearchMode"] = mode.String() ctx.Data["PageIsViewCode"] = true if keyword == "" { @@ -47,7 +86,7 @@ func Search(ctx *context.Context) { total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{ RepoIDs: []int64{ctx.Repo.Repository.ID}, Keyword: keyword, - IsKeywordFuzzy: isFuzzy, + IsKeywordFuzzy: mode == FuzzySearchMode, Language: language, Paginator: &db.ListOptions{ Page: page, @@ -64,11 +103,17 @@ func Search(ctx *context.Context) { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } } else { - res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ + grepOpt := git.GrepOptions{ ContextLineNumber: 1, - IsFuzzy: isFuzzy, RefName: ctx.Repo.RefName, - }) + } + switch mode { + case FuzzySearchMode: + grepOpt.Mode = git.FixedAnyGrepMode + case RegExpSearchMode: + grepOpt.Mode = git.RegExpGrepMode + } + res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, grepOpt) if err != nil { ctx.ServerError("GrepSearch", err) return diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 24779d41e0..e1b37d1e7f 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -417,7 +417,7 @@ func SearchWikiContents(ctx context.Context, repo *repo_model.Repository, keywor return git.GrepSearch(ctx, gitRepo, keyword, git.GrepOptions{ ContextLineNumber: 0, - IsFuzzy: true, + Mode: git.FixedAnyGrepMode, RefName: repo.GetWikiBranchName(), MaxResultLimit: 10, MatchesPerFile: 3,