mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Render READMEs in docs/ .gitea or .github from root (#10361)
* Render READMEs in docs/ .gitea or .github from root
This commit is contained in:
		| @@ -80,4 +80,3 @@ INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTI3OTU5ODN9.O | |||||||
|  |  | ||||||
| [oauth2] | [oauth2] | ||||||
| JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko | JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko | ||||||
|  |  | ||||||
|   | |||||||
| @@ -50,6 +50,12 @@ func (err ErrBadLink) Error() string { | |||||||
| 	return fmt.Sprintf("%s: %s", err.Name, err.Message) | 	return fmt.Sprintf("%s: %s", err.Name, err.Message) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // IsErrBadLink if some error is ErrBadLink | ||||||
|  | func IsErrBadLink(err error) bool { | ||||||
|  | 	_, ok := err.(ErrBadLink) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  |  | ||||||
| // ErrUnsupportedVersion error when required git version not matched | // ErrUnsupportedVersion error when required git version not matched | ||||||
| type ErrUnsupportedVersion struct { | type ErrUnsupportedVersion struct { | ||||||
| 	Required string | 	Required string | ||||||
|   | |||||||
| @@ -167,6 +167,38 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) { | |||||||
| 	return target, nil | 	return target, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // FollowLinks returns the entry ultimately pointed to by a symlink | ||||||
|  | func (te *TreeEntry) FollowLinks() (*TreeEntry, error) { | ||||||
|  | 	if !te.IsLink() { | ||||||
|  | 		return nil, ErrBadLink{te.Name(), "not a symlink"} | ||||||
|  | 	} | ||||||
|  | 	entry := te | ||||||
|  | 	for i := 0; i < 999; i++ { | ||||||
|  | 		if entry.IsLink() { | ||||||
|  | 			next, err := entry.FollowLink() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			if next.ID == entry.ID { | ||||||
|  | 				return nil, ErrBadLink{ | ||||||
|  | 					entry.Name(), | ||||||
|  | 					"recursive link", | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			entry = next | ||||||
|  | 		} else { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if entry.IsLink() { | ||||||
|  | 		return nil, ErrBadLink{ | ||||||
|  | 			te.Name(), | ||||||
|  | 			"too many levels of symbolic links", | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return entry, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory ) | // GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory ) | ||||||
| func (te *TreeEntry) GetSubJumpablePathName() string { | func (te *TreeEntry) GetSubJumpablePathName() string { | ||||||
| 	if te.IsSubModule() || !te.IsDir() { | 	if te.IsSubModule() || !te.IsDir() { | ||||||
|   | |||||||
| @@ -732,6 +732,7 @@ file_too_large = The file is too large to be shown. | |||||||
| video_not_supported_in_browser = Your browser does not support the HTML5 'video' tag. | video_not_supported_in_browser = Your browser does not support the HTML5 'video' tag. | ||||||
| audio_not_supported_in_browser = Your browser does not support the HTML5 'audio' tag. | audio_not_supported_in_browser = Your browser does not support the HTML5 'audio' tag. | ||||||
| stored_lfs = Stored with Git LFS | stored_lfs = Stored with Git LFS | ||||||
|  | symbolic_link = Symbolic link | ||||||
| commit_graph = Commit Graph | commit_graph = Commit Graph | ||||||
| blame = Blame | blame = Blame | ||||||
| normal_view = Normal View | normal_view = Normal View | ||||||
|   | |||||||
| @@ -36,6 +36,83 @@ const ( | |||||||
| 	tplMigrating base.TplName = "repo/migrating" | 	tplMigrating base.TplName = "repo/migrating" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | type namedBlob struct { | ||||||
|  | 	name      string | ||||||
|  | 	isSymlink bool | ||||||
|  | 	blob      *git.Blob | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // FIXME: There has to be a more efficient way of doing this | ||||||
|  | func getReadmeFileFromPath(commit *git.Commit, treePath string) (*namedBlob, error) { | ||||||
|  | 	tree, err := commit.SubTree(treePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	entries, err := tree.ListEntries() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var readmeFiles [4]*namedBlob | ||||||
|  | 	var exts = []string{".md", ".txt", ""} // sorted by priority | ||||||
|  | 	for _, entry := range entries { | ||||||
|  | 		if entry.IsDir() { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		for i, ext := range exts { | ||||||
|  | 			if markup.IsReadmeFile(entry.Name(), ext) { | ||||||
|  | 				if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) { | ||||||
|  | 					name := entry.Name() | ||||||
|  | 					isSymlink := entry.IsLink() | ||||||
|  | 					target := entry | ||||||
|  | 					if isSymlink { | ||||||
|  | 						target, err = entry.FollowLinks() | ||||||
|  | 						if err != nil && !git.IsErrBadLink(err) { | ||||||
|  | 							return nil, err | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					if target != nil && (target.IsExecutable() || target.IsRegular()) { | ||||||
|  | 						readmeFiles[i] = &namedBlob{ | ||||||
|  | 							name, | ||||||
|  | 							isSymlink, | ||||||
|  | 							target.Blob(), | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if markup.IsReadmeFile(entry.Name()) { | ||||||
|  | 			if readmeFiles[3] == nil || base.NaturalSortLess(readmeFiles[3].name, entry.Blob().Name()) { | ||||||
|  | 				name := entry.Name() | ||||||
|  | 				isSymlink := entry.IsLink() | ||||||
|  | 				if isSymlink { | ||||||
|  | 					entry, err = entry.FollowLinks() | ||||||
|  | 					if err != nil && !git.IsErrBadLink(err) { | ||||||
|  | 						return nil, err | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				if entry != nil && (entry.IsExecutable() || entry.IsRegular()) { | ||||||
|  | 					readmeFiles[3] = &namedBlob{ | ||||||
|  | 						name, | ||||||
|  | 						isSymlink, | ||||||
|  | 						entry.Blob(), | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	var readmeFile *namedBlob | ||||||
|  | 	for _, f := range readmeFiles { | ||||||
|  | 		if f != nil { | ||||||
|  | 			readmeFile = f | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return readmeFile, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func renderDirectory(ctx *context.Context, treeLink string) { | func renderDirectory(ctx *context.Context, treeLink string) { | ||||||
| 	tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) | 	tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -65,25 +142,75 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||||||
| 	// 3 for the extensions in exts[] in order | 	// 3 for the extensions in exts[] in order | ||||||
| 	// the last one is for a readme that doesn't | 	// the last one is for a readme that doesn't | ||||||
| 	// strictly match an extension | 	// strictly match an extension | ||||||
| 	var readmeFiles [4]*git.Blob | 	var readmeFiles [4]*namedBlob | ||||||
|  | 	var docsEntries [3]*git.TreeEntry | ||||||
| 	var exts = []string{".md", ".txt", ""} // sorted by priority | 	var exts = []string{".md", ".txt", ""} // sorted by priority | ||||||
| 	for _, entry := range entries { | 	for _, entry := range entries { | ||||||
| 		if entry.IsDir() { | 		if entry.IsDir() { | ||||||
|  | 			lowerName := strings.ToLower(entry.Name()) | ||||||
|  | 			switch lowerName { | ||||||
|  | 			case "docs": | ||||||
|  | 				if entry.Name() == "docs" || docsEntries[0] == nil { | ||||||
|  | 					docsEntries[0] = entry | ||||||
|  | 				} | ||||||
|  | 			case ".gitea": | ||||||
|  | 				if entry.Name() == ".gitea" || docsEntries[1] == nil { | ||||||
|  | 					docsEntries[1] = entry | ||||||
|  | 				} | ||||||
|  | 			case ".github": | ||||||
|  | 				if entry.Name() == ".github" || docsEntries[2] == nil { | ||||||
|  | 					docsEntries[2] = entry | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		for i, ext := range exts { | 		for i, ext := range exts { | ||||||
| 			if markup.IsReadmeFile(entry.Name(), ext) { | 			if markup.IsReadmeFile(entry.Name(), ext) { | ||||||
| 				readmeFiles[i] = entry.Blob() | 				log.Debug("%s", entry.Name()) | ||||||
|  | 				name := entry.Name() | ||||||
|  | 				isSymlink := entry.IsLink() | ||||||
|  | 				target := entry | ||||||
|  | 				if isSymlink { | ||||||
|  | 					target, err = entry.FollowLinks() | ||||||
|  | 					if err != nil && !git.IsErrBadLink(err) { | ||||||
|  | 						ctx.ServerError("FollowLinks", err) | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				log.Debug("%t", target == nil) | ||||||
|  | 				if target != nil && (target.IsExecutable() || target.IsRegular()) { | ||||||
|  | 					readmeFiles[i] = &namedBlob{ | ||||||
|  | 						name, | ||||||
|  | 						isSymlink, | ||||||
|  | 						target.Blob(), | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if markup.IsReadmeFile(entry.Name()) { | 		if markup.IsReadmeFile(entry.Name()) { | ||||||
| 			readmeFiles[3] = entry.Blob() | 			name := entry.Name() | ||||||
|  | 			isSymlink := entry.IsLink() | ||||||
|  | 			if isSymlink { | ||||||
|  | 				entry, err = entry.FollowLinks() | ||||||
|  | 				if err != nil && !git.IsErrBadLink(err) { | ||||||
|  | 					ctx.ServerError("FollowLinks", err) | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if entry != nil && (entry.IsExecutable() || entry.IsRegular()) { | ||||||
|  | 				readmeFiles[3] = &namedBlob{ | ||||||
|  | 					name, | ||||||
|  | 					isSymlink, | ||||||
|  | 					entry.Blob(), | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var readmeFile *git.Blob | 	var readmeFile *namedBlob | ||||||
|  | 	readmeTreelink := treeLink | ||||||
| 	for _, f := range readmeFiles { | 	for _, f := range readmeFiles { | ||||||
| 		if f != nil { | 		if f != nil { | ||||||
| 			readmeFile = f | 			readmeFile = f | ||||||
| @@ -91,12 +218,31 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.TreePath == "" && readmeFile == nil { | ||||||
|  | 		for _, entry := range docsEntries { | ||||||
|  | 			if entry == nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			readmeFile, err = getReadmeFileFromPath(ctx.Repo.Commit, entry.GetSubJumpablePathName()) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("getReadmeFileFromPath", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			if readmeFile != nil { | ||||||
|  | 				readmeFile.name = entry.Name() + "/" + readmeFile.name | ||||||
|  | 				readmeTreelink = treeLink + "/" + entry.GetSubJumpablePathName() | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if readmeFile != nil { | 	if readmeFile != nil { | ||||||
| 		ctx.Data["RawFileLink"] = "" | 		ctx.Data["RawFileLink"] = "" | ||||||
| 		ctx.Data["ReadmeInList"] = true | 		ctx.Data["ReadmeInList"] = true | ||||||
| 		ctx.Data["ReadmeExist"] = true | 		ctx.Data["ReadmeExist"] = true | ||||||
|  | 		ctx.Data["FileIsSymlink"] = readmeFile.isSymlink | ||||||
|  |  | ||||||
| 		dataRc, err := readmeFile.DataAsync() | 		dataRc, err := readmeFile.blob.DataAsync() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			ctx.ServerError("Data", err) | 			ctx.ServerError("Data", err) | ||||||
| 			return | 			return | ||||||
| @@ -109,7 +255,7 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||||||
|  |  | ||||||
| 		isTextFile := base.IsTextFile(buf) | 		isTextFile := base.IsTextFile(buf) | ||||||
| 		ctx.Data["FileIsText"] = isTextFile | 		ctx.Data["FileIsText"] = isTextFile | ||||||
| 		ctx.Data["FileName"] = readmeFile.Name() | 		ctx.Data["FileName"] = readmeFile.name | ||||||
| 		fileSize := int64(0) | 		fileSize := int64(0) | ||||||
| 		isLFSFile := false | 		isLFSFile := false | ||||||
| 		ctx.Data["IsLFSFile"] = false | 		ctx.Data["IsLFSFile"] = false | ||||||
| @@ -151,13 +297,13 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||||||
|  |  | ||||||
| 				fileSize = meta.Size | 				fileSize = meta.Size | ||||||
| 				ctx.Data["FileSize"] = meta.Size | 				ctx.Data["FileSize"] = meta.Size | ||||||
| 				filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name())) | 				filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name)) | ||||||
| 				ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, filenameBase64) | 				ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, filenameBase64) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !isLFSFile { | 		if !isLFSFile { | ||||||
| 			fileSize = readmeFile.Size() | 			fileSize = readmeFile.blob.Size() | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if isTextFile { | 		if isTextFile { | ||||||
| @@ -170,10 +316,10 @@ func renderDirectory(ctx *context.Context, treeLink string) { | |||||||
| 				d, _ := ioutil.ReadAll(dataRc) | 				d, _ := ioutil.ReadAll(dataRc) | ||||||
| 				buf = charset.ToUTF8WithFallback(append(buf, d...)) | 				buf = charset.ToUTF8WithFallback(append(buf, d...)) | ||||||
|  |  | ||||||
| 				if markupType := markup.Type(readmeFile.Name()); markupType != "" { | 				if markupType := markup.Type(readmeFile.name); markupType != "" { | ||||||
| 					ctx.Data["IsMarkup"] = true | 					ctx.Data["IsMarkup"] = true | ||||||
| 					ctx.Data["MarkupType"] = string(markupType) | 					ctx.Data["MarkupType"] = string(markupType) | ||||||
| 					ctx.Data["FileContent"] = string(markup.Render(readmeFile.Name(), buf, treeLink, ctx.Repo.Repository.ComposeMetas())) | 					ctx.Data["FileContent"] = string(markup.Render(readmeFile.name, buf, readmeTreelink, ctx.Repo.Repository.ComposeMetas())) | ||||||
| 				} else { | 				} else { | ||||||
| 					ctx.Data["IsRenderedHTML"] = true | 					ctx.Data["IsRenderedHTML"] = true | ||||||
| 					ctx.Data["FileContent"] = strings.Replace( | 					ctx.Data["FileContent"] = strings.Replace( | ||||||
| @@ -218,6 +364,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | |||||||
| 	ctx.Data["Title"] = ctx.Data["Title"].(string) + " - " + ctx.Repo.TreePath + " at " + ctx.Repo.BranchName | 	ctx.Data["Title"] = ctx.Data["Title"].(string) + " - " + ctx.Repo.TreePath + " at " + ctx.Repo.BranchName | ||||||
|  |  | ||||||
| 	fileSize := blob.Size() | 	fileSize := blob.Size() | ||||||
|  | 	ctx.Data["FileIsSymlink"] = entry.IsLink() | ||||||
| 	ctx.Data["FileSize"] = fileSize | 	ctx.Data["FileSize"] = fileSize | ||||||
| 	ctx.Data["FileName"] = blob.Name() | 	ctx.Data["FileName"] = blob.Name() | ||||||
| 	ctx.Data["HighlightClass"] = highlight.FileNameToHighlightClass(blob.Name()) | 	ctx.Data["HighlightClass"] = highlight.FileNameToHighlightClass(blob.Name()) | ||||||
|   | |||||||
| @@ -2,10 +2,19 @@ | |||||||
| 	<h4 class="file-header ui top attached header"> | 	<h4 class="file-header ui top attached header"> | ||||||
| 		<div class="file-header-left"> | 		<div class="file-header-left"> | ||||||
| 			{{if .ReadmeInList}} | 			{{if .ReadmeInList}} | ||||||
| 				<i class="book icon"></i> | 				{{if .FileIsSymlink}} | ||||||
|  | 					<i class="icons"><i class="book icon"></i><i class="bottom left corner tiny inverted share icon"></i></i> | ||||||
|  | 				{{else}} | ||||||
|  | 					<i class="book icon"></i> | ||||||
|  | 				{{end}} | ||||||
| 				<strong>{{.FileName}}</strong> | 				<strong>{{.FileName}}</strong> | ||||||
| 			{{else}} | 			{{else}} | ||||||
| 				<div class="file-info text grey normal mono"> | 				<div class="file-info text grey normal mono"> | ||||||
|  | 					{{if .FileIsSymlink}} | ||||||
|  | 						<div class="file-info-entry"> | ||||||
|  | 							{{.i18n.Tr "repo.symbolic_link"}} | ||||||
|  | 						</div> | ||||||
|  | 					{{end}} | ||||||
| 					{{if .NumLinesSet}} | 					{{if .NumLinesSet}} | ||||||
| 						<div class="file-info-entry"> | 						<div class="file-info-entry"> | ||||||
| 							{{.NumLines}} {{.i18n.Tr (TrN .i18n.Lang .NumLines "repo.line" "repo.lines") }} | 							{{.NumLines}} {{.i18n.Tr (TrN .i18n.Lang .NumLines "repo.line" "repo.lines") }} | ||||||
|   | |||||||
| @@ -381,6 +381,14 @@ | |||||||
|                     font-size: 1em; |                     font-size: 1em; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 .small.icon { | ||||||
|  |                     font-size: 0.75em; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 .tiny.icon { | ||||||
|  |                     font-size: 0.5em; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 .file-actions { |                 .file-actions { | ||||||
|                     margin-bottom: -5px; |                     margin-bottom: -5px; | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user