feat: grep search

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main
jolheiser 2024-03-01 11:58:05 -06:00
parent a56081be17
commit 1f8b18f963
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
14 changed files with 407 additions and 21 deletions

View File

@ -0,0 +1,150 @@
package git
import (
"regexp"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
// GrepResult is the result of a search
type GrepResult struct {
File string
StartLine int
Line int
Content string
}
// Grep performs a naive "code search" via git grep
func (r Repo) Grep(search string) ([]GrepResult, error) {
// Plain-text search only
re, err := regexp.Compile(regexp.QuoteMeta(search))
if err != nil {
return nil, err
}
repo, err := r.Git()
if err != nil {
return nil, err
}
// Loosely modifed from
// https://github.com/go-git/go-git/blob/fb04aa392c8d4c259cb5b21c1cb4c6f8076e600b/options.go#L736-L740
// https://github.com/go-git/go-git/blob/fb04aa392c8d4c259cb5b21c1cb4c6f8076e600b/worktree.go#L753-L760
ref, err := repo.Head()
if err != nil {
return nil, err
}
commit, err := repo.CommitObject(ref.Hash())
if err != nil {
return nil, err
}
tree, err := commit.Tree()
if err != nil {
return nil, err
}
return findMatchInFiles(tree.Files(), ref.Hash().String(), &git.GrepOptions{
Patterns: []*regexp.Regexp{re},
})
}
// Lines below are copied and modifed from https://github.com/go-git/go-git/blob/fb04aa392c8d4c259cb5b21c1cb4c6f8076e600b/worktree.go#L961-L1045
// findMatchInFiles takes a FileIter, worktree name and GrepOptions, and
// returns a slice of GrepResult containing the result of regex pattern matching
// in content of all the files.
func findMatchInFiles(fileiter *object.FileIter, treeName string, opts *git.GrepOptions) ([]GrepResult, error) {
var results []GrepResult
err := fileiter.ForEach(func(file *object.File) error {
var fileInPathSpec bool
// When no pathspecs are provided, search all the files.
if len(opts.PathSpecs) == 0 {
fileInPathSpec = true
}
// Check if the file name matches with the pathspec. Break out of the
// loop once a match is found.
for _, pathSpec := range opts.PathSpecs {
if pathSpec != nil && pathSpec.MatchString(file.Name) {
fileInPathSpec = true
break
}
}
// If the file does not match with any of the pathspec, skip it.
if !fileInPathSpec {
return nil
}
grepResults, err := findMatchInFile(file, treeName, opts)
if err != nil {
return err
}
results = append(results, grepResults...)
return nil
})
return results, err
}
// findMatchInFile takes a single File, worktree name and GrepOptions,
// and returns a slice of GrepResult containing the result of regex pattern
// matching in the given file.
func findMatchInFile(file *object.File, treeName string, opts *git.GrepOptions) ([]GrepResult, error) {
var grepResults []GrepResult
content, err := file.Contents()
if err != nil {
return grepResults, err
}
// Split the file content and parse line-by-line.
contentByLine := strings.Split(content, "\n")
for lineNum, cnt := range contentByLine {
addToResult := false
// Match the patterns and content. Break out of the loop once a
// match is found.
for _, pattern := range opts.Patterns {
if pattern != nil && pattern.MatchString(cnt) {
// Add to result only if invert match is not enabled.
if !opts.InvertMatch {
addToResult = true
break
}
} else if opts.InvertMatch {
// If matching fails, and invert match is enabled, add to
// results.
addToResult = true
break
}
}
if addToResult {
startLine := lineNum + 1
ctx := []string{cnt}
if lineNum != 0 {
startLine -= 1
ctx = append([]string{contentByLine[lineNum-1]}, ctx...)
}
if lineNum != len(contentByLine)-1 {
ctx = append(ctx, contentByLine[lineNum+1])
}
grepResults = append(grepResults, GrepResult{
File: file.Name,
StartLine: startLine,
Line: lineNum + 1,
Content: strings.Join(ctx, "\n"),
})
}
}
return grepResults, nil
}

View File

@ -43,7 +43,7 @@
background: rgb(var(--ctp-surface0)) !important;
}
.commit .chroma {
.code>.chroma {
border-radius: .25rem;
padding: 1em;
}

View File

@ -26,7 +26,7 @@ var (
type code struct{}
func (c code) setup(source []byte, fileName string) (chroma.Iterator, *chroma.Style, error) {
func setup(source []byte, fileName string) (chroma.Iterator, *chroma.Style, error) {
lexer := lexers.Match(fileName)
if lexer == nil {
lexer = lexers.Fallback
@ -48,7 +48,7 @@ func (c code) setup(source []byte, fileName string) (chroma.Iterator, *chroma.St
// Basic formats code without any extras
func (c code) Basic(source []byte, fileName string, writer io.Writer) error {
iter, style, err := c.setup(source, fileName)
iter, style, err := setup(source, fileName)
if err != nil {
return err
}
@ -57,9 +57,24 @@ func (c code) Basic(source []byte, fileName string, writer io.Writer) error {
// Convert formats code with line numbers, links, etc.
func (c code) Convert(source []byte, fileName string, writer io.Writer) error {
iter, style, err := c.setup(source, fileName)
iter, style, err := setup(source, fileName)
if err != nil {
return err
}
return Formatter.Format(writer, style, iter)
}
// Snippet formats code with line numbers starting at a specific line
func Snippet(source []byte, fileName string, line int, writer io.Writer) error {
iter, style, err := setup(source, fileName)
if err != nil {
return err
}
formatter := html.New(
html.WithLineNumbers(true),
html.WithClasses(true),
html.LineNumbersInTable(true),
html.BaseLineNumber(line),
)
return formatter.Format(writer, style, iter)
}

View File

@ -21,6 +21,8 @@ templ repoHeaderComponent(rhcc RepoHeaderComponentContext) {
{ " - " }
<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/log/%s", rhcc.Name, rhcc.Ref)) }>log</a>
{ " - " }
<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/search", rhcc.Name)) }>search</a>
{ " - " }
<pre class="text-text inline select-all bg-base dark:bg-base/50 p-1 rounded">{ fmt.Sprintf("%s/%s.git", rhcc.CloneURL, rhcc.Name) }</pre>
</div>
<div class="text-text/80 mb-1">{ rhcc.Description }</div>

View File

@ -34,7 +34,7 @@ templ RepoCommit(rcc RepoCommitContext) {
<a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rcc.RepoHeaderComponentContext.Name, file.To.Commit, file.To.Path)) }>{ file.To.Path }</a>
}
</div>
<div class="whitespace-pre commit">@templ.Raw(file.Patch)</div>
<div class="whitespace-pre code">@templ.Raw(file.Patch)</div>
}
}
}

View File

@ -324,7 +324,7 @@ func RepoCommit(rcc RepoCommitContext) templ.Component {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"whitespace-pre commit\">")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"whitespace-pre code\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@ -10,7 +10,11 @@ type RepoFileContext struct {
templ RepoFile(rfc RepoFileContext) {
@base(rfc.BaseContext) {
@repoHeaderComponent(rfc.RepoHeaderComponentContext)
<div class="mt-2 text-text"><a class="text-text underline decoration-text/50 decoration-dashed hover:decoration-solid" href="?raw">Raw</a><span>{ " - " }{ rfc.Path }</span>@templ.Raw(rfc.Code)</div>
<div class="mt-2 text-text">
<a class="text-text underline decoration-text/50 decoration-dashed hover:decoration-solid" href="?raw">Raw</a>
<span>{ " - " }{ rfc.Path }</span>
<div class="code">@templ.Raw(rfc.Code)</div>
</div>
}
<script>
const lineRe = /#L(\d+)(?:-L(\d+))?/g

View File

@ -49,14 +49,14 @@ func RepoFile(rfc RepoFileContext) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><span>")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(" - ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_file.templ`, Line: 12, Col: 153}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_file.templ`, Line: 14, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@ -65,13 +65,13 @@ func RepoFile(rfc RepoFileContext) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(rfc.Path)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_file.templ`, Line: 12, Col: 165}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_file.templ`, Line: 14, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span>")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span><div class=\"code\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -79,7 +79,7 @@ func RepoFile(rfc RepoFileContext) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@ -0,0 +1,26 @@
package html
import "fmt"
import "go.jolheiser.com/ugit/internal/git"
type SearchContext struct {
BaseContext
RepoHeaderComponentContext
Results []git.GrepResult
}
templ RepoSearch(sc SearchContext) {
@base(sc.BaseContext) {
@repoHeaderComponent(sc.RepoHeaderComponentContext)
<form method="get"><label class="text-text">Search <input class="rounded p-1 mt-2 bg-mantle focus:border-lavender" id="search" type="text" name="q" placeholder="search"/></label></form>
for _, result := range sc.Results {
<div class="text-text mt-5"><a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s#L%d", sc.RepoHeaderComponentContext.Name, sc.RepoHeaderComponentContext.Ref, result.File, result.Line)) }>{ result.File }</a></div>
<div class="code">@templ.Raw(result.Content)</div>
}
}
<script>
const search = new URLSearchParams(window.location.search).get("q");
if (search !== "") document.querySelector("#search").value = search;
</script>
}

View File

@ -0,0 +1,124 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.501
package html
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "fmt"
import "go.jolheiser.com/ugit/internal/git"
type SearchContext struct {
BaseContext
RepoHeaderComponentContext
Results []git.GrepResult
}
func RepoSearch(sc SearchContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
templ_7745c5c3_Err = repoHeaderComponent(sc.RepoHeaderComponentContext).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <form method=\"get\"><label class=\"text-text\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var3 := `Search `
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<input class=\"rounded p-1 mt-2 bg-mantle focus:border-lavender\" id=\"search\" type=\"text\" name=\"q\" placeholder=\"search\"></label></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, result := range sc.Results {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-text mt-5\"><a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s#L%d", sc.RepoHeaderComponentContext.Name, sc.RepoHeaderComponentContext.Ref, result.File, result.Line))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(result.File)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_search.templ`, Line: 16, Col: 280}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div><div class=\"code\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(result.Content).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base(sc.BaseContext).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var6 := `
const search = new URLSearchParams(window.location.search).get("q");
if (search !== "") document.querySelector("#search").value = search;
`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -166,16 +166,47 @@ func repoHeaderComponent(rhcc RepoHeaderComponentContext) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/search", rhcc.Name))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var15 := `search`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(" - ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 24, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<pre class=\"text-text inline select-all bg-base dark:bg-base/50 p-1 rounded\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s/%s.git", rhcc.CloneURL, rhcc.Name))
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s/%s.git", rhcc.CloneURL, rhcc.Name))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 23, Col: 131}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 25, Col: 131}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -183,12 +214,12 @@ func repoHeaderComponent(rhcc RepoHeaderComponentContext) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(rhcc.Description)
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(rhcc.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 25, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 27, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

File diff suppressed because one or more lines are too long

View File

@ -85,6 +85,7 @@ func New(settings Settings) Server {
r.Get("/log/{ref}", httperr.Handler(rh.repoLog))
r.Get("/commit/{commit}", httperr.Handler(rh.repoCommit))
r.Get("/commit/{commit}.patch", httperr.Handler(rh.repoPatch))
r.Get("/search", httperr.Handler(rh.repoSearch))
// Protocol
r.Get("/info/refs", httperr.Handler(rh.infoRefs))

View File

@ -6,6 +6,7 @@ import (
"mime"
"net/http"
"path/filepath"
"strings"
"go.jolheiser.com/ugit/internal/html/markup"
@ -182,3 +183,35 @@ func (rh repoHandler) repoPatch(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (rh repoHandler) repoSearch(w http.ResponseWriter, r *http.Request) error {
repo := r.Context().Value(repoCtxKey).(*git.Repo)
var results []git.GrepResult
search := r.URL.Query().Get("q")
if q := strings.TrimSpace(search); q != "" {
var err error
results, err = repo.Grep(q)
if err != nil {
return httperr.Error(err)
}
for idx, result := range results {
var buf bytes.Buffer
if err := markup.Snippet([]byte(result.Content), filepath.Base(result.File), result.StartLine, &buf); err != nil {
return httperr.Error(err)
}
results[idx].Content = buf.String()
}
}
if err := html.RepoSearch(html.SearchContext{
BaseContext: rh.baseContext(),
RepoHeaderComponentContext: rh.repoHeaderContext(repo, r),
Results: results,
}).Render(r.Context(), w); err != nil {
return httperr.Error(err)
}
return nil
}