Monorepo for Tangled tangled.org
9

Configure Feed

Select the types of activity you want to include in your feed.

appview/pages/markup: fix readme detection for directories and unsupported formats

- IsReadmeFile now takes (name, mode) and only matches regular file
blobs (filemode.Regular/Executable). A directory or symlink named
"readme" was being picked up by tree handlers, which then tried to
fetch its blob and 503'd.
- ReadmePattern matches by convention (^readme(?:[._-].+)?$) rather
than a fixed extension set. README.rst, README.org, README-old, etc.
now surface on index/tree pages and fall through to FormatText in
GetFormat for plaintext rendering.
- pages.RepoIndex and pages.RepoTree route through markup.GetFormat
instead of hardcoded extension switches, so the markdown extension
list lives in one place (FileTypePatterns[FormatMarkdown]).
- Added format_test.go covering IsReadmeFile (mode/symlink/dir
rejection, the directory regression), FileTypePatterns, and GetFormat.

Signed-off-by: Evan Jarrett <evan@evanjarrett.com>

+125 -17
+16 -8
appview/pages/markup/format.go
··· 2 2 3 3 import ( 4 4 "regexp" 5 + 6 + "github.com/go-git/go-git/v5/plumbing/filemode" 5 7 ) 6 8 7 9 type Format string ··· 10 12 FormatMarkdown Format = "markdown" 11 13 FormatText Format = "text" 12 14 ) 13 - 14 - var FileTypes map[Format][]string = map[Format][]string{ 15 - FormatMarkdown: {".md", ".markdown", ".mdown", ".mkdn", ".mkd"}, 16 - } 17 15 18 16 var FileTypePatterns = map[Format]*regexp.Regexp{ 19 17 FormatMarkdown: regexp.MustCompile(`(?i)\.(md|markdown|mdown|mkdn|mkd)$`), 20 18 } 21 19 22 - var ReadmePattern = regexp.MustCompile(`(?i)^readme(\.(md|markdown|txt))?$`) 20 + var ReadmePattern = regexp.MustCompile(`(?i)^readme(?:[._-].+)?$`) 23 21 24 - func IsReadmeFile(filename string) bool { 25 - return ReadmePattern.MatchString(filename) 22 + // IsReadmeFile reports whether name/mode identifies a readme blob. The git 23 + // mode is checked so directories or symlinks named "readme" are filtered out. 24 + func IsReadmeFile(name, mode string) bool { 25 + if !ReadmePattern.MatchString(name) { 26 + return false 27 + } 28 + m, err := filemode.New(mode) 29 + if err != nil { 30 + return false 31 + } 32 + return m == filemode.Regular || m == filemode.Executable 26 33 } 27 34 35 + // GetFormat returns the Format whose extension list matches filename, 36 + // falling back to FormatText. 28 37 func GetFormat(filename string) Format { 29 38 for format, pattern := range FileTypePatterns { 30 39 if pattern.MatchString(filename) { 31 40 return format 32 41 } 33 42 } 34 - // default format 35 43 return FormatText 36 44 }
+102
appview/pages/markup/format_test.go
··· 1 + package markup 2 + 3 + import "testing" 4 + 5 + func TestIsReadmeFile(t *testing.T) { 6 + const ( 7 + fileMode = "100644" 8 + execMode = "100755" 9 + dirMode = "040000" 10 + ) 11 + 12 + cases := []struct { 13 + name string 14 + mode string 15 + want bool 16 + }{ 17 + {"README.md", fileMode, true}, 18 + {"readme.md", fileMode, true}, 19 + {"ReadMe.MD", fileMode, true}, 20 + {"README.markdown", fileMode, true}, 21 + {"README.mdown", fileMode, true}, 22 + {"README.mkdn", fileMode, true}, 23 + {"README.mkd", fileMode, true}, 24 + {"README.txt", fileMode, true}, 25 + {"readme", fileMode, true}, 26 + {"README", execMode, true}, 27 + 28 + // regression: a directory named "readme" must not be picked up 29 + // as the README blob; tree handlers used to fetch it and 503. 30 + {"readme", dirMode, false}, 31 + {"README.md", dirMode, false}, 32 + 33 + // readme is matched by convention, not by renderable format — 34 + // unsupported markup falls through to plaintext in GetFormat. 35 + {"README.rst", fileMode, true}, 36 + {"README.org", fileMode, true}, 37 + {"readme-old", fileMode, true}, 38 + {"readme_legacy", fileMode, true}, 39 + 40 + {"notreadme.md", fileMode, false}, 41 + {"READMEISH", fileMode, false}, 42 + {"README.md", "", false}, 43 + {"README.md", "120000", false}, // symlink 44 + } 45 + 46 + for _, c := range cases { 47 + t.Run(c.name+"/"+c.mode, func(t *testing.T) { 48 + if got := IsReadmeFile(c.name, c.mode); got != c.want { 49 + t.Errorf("IsReadmeFile(%q, %q) = %v, want %v", c.name, c.mode, got, c.want) 50 + } 51 + }) 52 + } 53 + } 54 + 55 + func TestFileTypePatterns(t *testing.T) { 56 + cases := []struct { 57 + format Format 58 + filename string 59 + want bool 60 + }{ 61 + {FormatMarkdown, "x.md", true}, 62 + {FormatMarkdown, "x.MARKDOWN", true}, 63 + {FormatMarkdown, "x.mkdn", true}, 64 + {FormatMarkdown, "x.mkd", true}, 65 + {FormatMarkdown, "x.mdown", true}, 66 + {FormatMarkdown, "x.txt", false}, 67 + {FormatMarkdown, "x.rst", false}, 68 + } 69 + 70 + for _, c := range cases { 71 + t.Run(string(c.format)+"/"+c.filename, func(t *testing.T) { 72 + p, ok := FileTypePatterns[c.format] 73 + if !ok { 74 + t.Fatalf("FileTypePatterns[%q] missing", c.format) 75 + } 76 + if got := p.MatchString(c.filename); got != c.want { 77 + t.Errorf("FileTypePatterns[%q].MatchString(%q) = %v, want %v", c.format, c.filename, got, c.want) 78 + } 79 + }) 80 + } 81 + } 82 + 83 + func TestGetFormat(t *testing.T) { 84 + cases := []struct { 85 + filename string 86 + want Format 87 + }{ 88 + {"x.md", FormatMarkdown}, 89 + {"x.MARKDOWN", FormatMarkdown}, 90 + {"x.txt", FormatText}, 91 + {"x.rs", FormatText}, // unknown -> default 92 + {"noext", FormatText}, 93 + } 94 + 95 + for _, c := range cases { 96 + t.Run(c.filename, func(t *testing.T) { 97 + if got := GetFormat(c.filename); got != c.want { 98 + t.Errorf("GetFormat(%q) = %q, want %q", c.filename, got, c.want) 99 + } 100 + }) 101 + } 102 + }
+4 -6
appview/pages/pages.go
··· 867 867 rctx.RendererType = markup.RendererTypeRepoMarkdown 868 868 869 869 if params.ReadmeFileName != "" { 870 - ext := strings.ToLower(filepath.Ext(params.ReadmeFileName)) 871 - switch ext { 872 - case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 870 + switch markup.GetFormat(params.ReadmeFileName) { 871 + case markup.FormatMarkdown: 873 872 params.Raw = false 874 873 htmlString := rctx.RenderMarkdown(params.Readme) 875 874 sanitized := rctx.SanitizeDefault(htmlString) ··· 961 960 rctx.RendererType = markup.RendererTypeRepoMarkdown 962 961 963 962 if params.ReadmeFileName != "" { 964 - ext := strings.ToLower(filepath.Ext(params.ReadmeFileName)) 965 - switch ext { 966 - case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 963 + switch markup.GetFormat(params.ReadmeFileName) { 964 + case markup.FormatMarkdown: 967 965 params.Raw = false 968 966 htmlString := rctx.RenderMarkdown(params.Readme) 969 967 sanitized := rctx.SanitizeDefault(htmlString)
+1 -1
appview/repo/index.go
··· 272 272 treeResp = resp 273 273 274 274 for _, file := range resp.Files { 275 - if markup.IsReadmeFile(file.Name) { 275 + if markup.IsReadmeFile(file.Name, file.Mode) { 276 276 readmeFileName = file.Name 277 277 break 278 278 }
+1 -1
appview/repo/tree.go
··· 62 62 } 63 63 } 64 64 files[i] = file 65 - if markup.IsReadmeFile(xrpcFile.Name) { 65 + if markup.IsReadmeFile(xrpcFile.Name, xrpcFile.Mode) { 66 66 readmeFile = xrpcFile 67 67 } 68 68 }
+1 -1
knotserver/xrpc/repo_tree.go
··· 50 50 var readmeFileName string 51 51 var readmeContents string 52 52 for _, file := range files { 53 - if markup.IsReadmeFile(file.Name) { 53 + if markup.IsReadmeFile(file.Name, file.Mode) { 54 54 contents, err := gr.RawContent(filepath.Join(path, file.Name)) 55 55 if err != nil { 56 56 x.Logger.Error("failed to read contents of file", "path", path, "file", file.Name)