mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	GitHub like repo home page (#32213)
Move some components (description, license, release, language stats) to sidebar --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -145,6 +145,7 @@ confirm_delete_selected = Confirm to delete all selected items? | |||||||
|  |  | ||||||
| name = Name | name = Name | ||||||
| value = Value | value = Value | ||||||
|  | readme = Readme | ||||||
|  |  | ||||||
| filter = Filter | filter = Filter | ||||||
| filter.clear = Clear Filter | filter.clear = Clear Filter | ||||||
| @@ -1045,7 +1046,8 @@ generate_repo = Generate Repository | |||||||
| generate_from = Generate From | generate_from = Generate From | ||||||
| repo_desc = Description | repo_desc = Description | ||||||
| repo_desc_helper = Enter short description (optional) | repo_desc_helper = Enter short description (optional) | ||||||
| repo_lang = Language | repo_no_desc = No description provided | ||||||
|  | repo_lang = Languages | ||||||
| repo_gitignore_helper = Select .gitignore templates. | repo_gitignore_helper = Select .gitignore templates. | ||||||
| repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default. | repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default. | ||||||
| issue_labels = Issue Labels | issue_labels = Issue Labels | ||||||
|   | |||||||
| @@ -114,12 +114,6 @@ func RefBlame(ctx *context.Context) { | |||||||
| 	ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs | 	ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs | ||||||
| 	ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile | 	ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile | ||||||
|  |  | ||||||
| 	// Get Topics of this repo |  | ||||||
| 	renderRepoTopics(ctx) |  | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	commitNames := processBlameParts(ctx, result.Parts) | 	commitNames := processBlameParts(ctx, result.Parts) | ||||||
| 	if ctx.Written() { | 	if ctx.Written() { | ||||||
| 		return | 		return | ||||||
|   | |||||||
| @@ -89,7 +89,6 @@ func Branches(ctx *context.Context) { | |||||||
| 	pager := context.NewPagination(int(branchesCount), pageSize, page, 5) | 	pager := context.NewPagination(int(branchesCount), pageSize, page, 5) | ||||||
| 	pager.SetDefaultParams(ctx) | 	pager.SetDefaultParams(ctx) | ||||||
| 	ctx.Data["Page"] = pager | 	ctx.Data["Page"] = pager | ||||||
| 	ctx.Data["LicenseFileName"] = repo_service.LicenseFileName |  | ||||||
| 	ctx.HTML(http.StatusOK, tplBranch) | 	ctx.HTML(http.StatusOK, tplBranch) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -102,7 +102,6 @@ func Commits(ctx *context.Context) { | |||||||
| 	pager := context.NewPagination(int(commitsCount), pageSize, page, 5) | 	pager := context.NewPagination(int(commitsCount), pageSize, page, 5) | ||||||
| 	pager.SetDefaultParams(ctx) | 	pager.SetDefaultParams(ctx) | ||||||
| 	ctx.Data["Page"] = pager | 	ctx.Data["Page"] = pager | ||||||
| 	ctx.Data["LicenseFileName"] = repo_service.LicenseFileName |  | ||||||
| 	ctx.HTML(http.StatusOK, tplCommits) | 	ctx.HTML(http.StatusOK, tplCommits) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -219,8 +218,6 @@ func SearchCommits(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
| 	ctx.Data["Username"] = ctx.Repo.Owner.Name | 	ctx.Data["Username"] = ctx.Repo.Owner.Name | ||||||
| 	ctx.Data["Reponame"] = ctx.Repo.Repository.Name | 	ctx.Data["Reponame"] = ctx.Repo.Repository.Name | ||||||
| 	ctx.Data["RefName"] = ctx.Repo.RefName |  | ||||||
| 	ctx.Data["LicenseFileName"] = repo_service.LicenseFileName |  | ||||||
| 	ctx.HTML(http.StatusOK, tplCommits) | 	ctx.HTML(http.StatusOK, tplCommits) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -266,7 +263,6 @@ func FileHistory(ctx *context.Context) { | |||||||
| 	pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) | 	pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) | ||||||
| 	pager.SetDefaultParams(ctx) | 	pager.SetDefaultParams(ctx) | ||||||
| 	ctx.Data["Page"] = pager | 	ctx.Data["Page"] = pager | ||||||
| 	ctx.Data["LicenseFileName"] = repo_service.LicenseFileName |  | ||||||
| 	ctx.HTML(http.StatusOK, tplCommits) | 	ctx.HTML(http.StatusOK, tplCommits) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,7 +31,6 @@ import ( | |||||||
| 	"code.gitea.io/gitea/services/context/upload" | 	"code.gitea.io/gitea/services/context/upload" | ||||||
| 	"code.gitea.io/gitea/services/forms" | 	"code.gitea.io/gitea/services/forms" | ||||||
| 	releaseservice "code.gitea.io/gitea/services/release" | 	releaseservice "code.gitea.io/gitea/services/release" | ||||||
| 	repo_service "code.gitea.io/gitea/services/repository" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -153,9 +152,6 @@ func Releases(ctx *context.Context) { | |||||||
| 	ctx.Data["Title"] = ctx.Tr("repo.release.releases") | 	ctx.Data["Title"] = ctx.Tr("repo.release.releases") | ||||||
| 	ctx.Data["IsViewBranch"] = false | 	ctx.Data["IsViewBranch"] = false | ||||||
| 	ctx.Data["IsViewTag"] = true | 	ctx.Data["IsViewTag"] = true | ||||||
| 	// Disable the showCreateNewBranch form in the dropdown on this page. |  | ||||||
| 	ctx.Data["CanCreateBranch"] = false |  | ||||||
| 	ctx.Data["HideBranchesInDropdown"] = true |  | ||||||
|  |  | ||||||
| 	listOptions := db.ListOptions{ | 	listOptions := db.ListOptions{ | ||||||
| 		Page:     ctx.FormInt("page"), | 		Page:     ctx.FormInt("page"), | ||||||
| @@ -193,9 +189,6 @@ func Releases(ctx *context.Context) { | |||||||
| 	pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) | 	pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) | ||||||
| 	pager.SetDefaultParams(ctx) | 	pager.SetDefaultParams(ctx) | ||||||
| 	ctx.Data["Page"] = pager | 	ctx.Data["Page"] = pager | ||||||
|  |  | ||||||
| 	ctx.Data["LicenseFileName"] = repo_service.LicenseFileName |  | ||||||
|  |  | ||||||
| 	ctx.HTML(http.StatusOK, tplReleasesList) | 	ctx.HTML(http.StatusOK, tplReleasesList) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -205,9 +198,6 @@ func TagsList(ctx *context.Context) { | |||||||
| 	ctx.Data["Title"] = ctx.Tr("repo.release.tags") | 	ctx.Data["Title"] = ctx.Tr("repo.release.tags") | ||||||
| 	ctx.Data["IsViewBranch"] = false | 	ctx.Data["IsViewBranch"] = false | ||||||
| 	ctx.Data["IsViewTag"] = true | 	ctx.Data["IsViewTag"] = true | ||||||
| 	// Disable the showCreateNewBranch form in the dropdown on this page. |  | ||||||
| 	ctx.Data["CanCreateBranch"] = false |  | ||||||
| 	ctx.Data["HideBranchesInDropdown"] = true |  | ||||||
| 	ctx.Data["CanCreateRelease"] = ctx.Repo.CanWrite(unit.TypeReleases) && !ctx.Repo.Repository.IsArchived | 	ctx.Data["CanCreateRelease"] = ctx.Repo.CanWrite(unit.TypeReleases) && !ctx.Repo.Repository.IsArchived | ||||||
|  |  | ||||||
| 	namePattern := ctx.FormTrim("q") | 	namePattern := ctx.FormTrim("q") | ||||||
| @@ -254,8 +244,6 @@ func TagsList(ctx *context.Context) { | |||||||
| 	pager.SetDefaultParams(ctx) | 	pager.SetDefaultParams(ctx) | ||||||
| 	ctx.Data["Page"] = pager | 	ctx.Data["Page"] = pager | ||||||
| 	ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases) | 	ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases) | ||||||
| 	ctx.Data["LicenseFileName"] = repo_service.LicenseFileName |  | ||||||
|  |  | ||||||
| 	ctx.HTML(http.StatusOK, tplTagsList) | 	ctx.HTML(http.StatusOK, tplTagsList) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,18 +5,13 @@ | |||||||
| package repo | package repo | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	gocontext "context" | 	gocontext "context" | ||||||
| 	"encoding/base64" |  | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"image" |  | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path" |  | ||||||
| 	"slices" |  | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -29,33 +24,21 @@ import ( | |||||||
| 	asymkey_model "code.gitea.io/gitea/models/asymkey" | 	asymkey_model "code.gitea.io/gitea/models/asymkey" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	git_model "code.gitea.io/gitea/models/git" | 	git_model "code.gitea.io/gitea/models/git" | ||||||
| 	issue_model "code.gitea.io/gitea/models/issues" |  | ||||||
| 	access_model "code.gitea.io/gitea/models/perm/access" |  | ||||||
| 	"code.gitea.io/gitea/models/renderhelper" |  | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	unit_model "code.gitea.io/gitea/models/unit" | 	unit_model "code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/actions" |  | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/charset" | 	"code.gitea.io/gitea/modules/charset" | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/highlight" |  | ||||||
| 	"code.gitea.io/gitea/modules/lfs" | 	"code.gitea.io/gitea/modules/lfs" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/markup" | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	repo_module "code.gitea.io/gitea/modules/repository" |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/svg" |  | ||||||
| 	"code.gitea.io/gitea/modules/typesniffer" | 	"code.gitea.io/gitea/modules/typesniffer" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/routers/web/feed" |  | ||||||
| 	"code.gitea.io/gitea/services/context" | 	"code.gitea.io/gitea/services/context" | ||||||
| 	issue_service "code.gitea.io/gitea/services/issue" |  | ||||||
| 	repo_service "code.gitea.io/gitea/services/repository" | 	repo_service "code.gitea.io/gitea/services/repository" | ||||||
| 	files_service "code.gitea.io/gitea/services/repository/files" |  | ||||||
|  |  | ||||||
| 	"github.com/nektos/act/pkg/model" |  | ||||||
|  |  | ||||||
| 	_ "golang.org/x/image/bmp"  // for processing bmp images | 	_ "golang.org/x/image/bmp"  // for processing bmp images | ||||||
| 	_ "golang.org/x/image/webp" // for processing webp images | 	_ "golang.org/x/image/webp" // for processing webp images | ||||||
| @@ -70,140 +53,6 @@ const ( | |||||||
| 	tplMigrating    base.TplName = "repo/migrate/migrating" | 	tplMigrating    base.TplName = "repo/migrate/migrating" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // locate a README for a tree in one of the supported paths. |  | ||||||
| // |  | ||||||
| // entries is passed to reduce calls to ListEntries(), so |  | ||||||
| // this has precondition: |  | ||||||
| // |  | ||||||
| //	entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries() |  | ||||||
| // |  | ||||||
| // FIXME: There has to be a more efficient way of doing this |  | ||||||
| func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) { |  | ||||||
| 	// Create a list of extensions in priority order |  | ||||||
| 	// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md |  | ||||||
| 	// 2. Txt files - e.g. README.txt |  | ||||||
| 	// 3. No extension - e.g. README |  | ||||||
| 	exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority |  | ||||||
| 	extCount := len(exts) |  | ||||||
| 	readmeFiles := make([]*git.TreeEntry, extCount+1) |  | ||||||
|  |  | ||||||
| 	docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/) |  | ||||||
| 	for _, entry := range entries { |  | ||||||
| 		if tryWellKnownDirs && entry.IsDir() { |  | ||||||
| 			// as a special case for the top-level repo introduction README, |  | ||||||
| 			// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ... |  | ||||||
| 			// (note that docsEntries is ignored unless we are at the root) |  | ||||||
| 			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 |  | ||||||
| 		} |  | ||||||
| 		if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { |  | ||||||
| 			log.Debug("Potential readme file: %s", entry.Name()) |  | ||||||
| 			if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { |  | ||||||
| 				if entry.IsLink() { |  | ||||||
| 					target, err := entry.FollowLinks() |  | ||||||
| 					if err != nil && !git.IsErrBadLink(err) { |  | ||||||
| 						return "", nil, err |  | ||||||
| 					} else if target != nil && (target.IsExecutable() || target.IsRegular()) { |  | ||||||
| 						readmeFiles[i] = entry |  | ||||||
| 					} |  | ||||||
| 				} else { |  | ||||||
| 					readmeFiles[i] = entry |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	var readmeFile *git.TreeEntry |  | ||||||
| 	for _, f := range readmeFiles { |  | ||||||
| 		if f != nil { |  | ||||||
| 			readmeFile = f |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ctx.Repo.TreePath == "" && readmeFile == nil { |  | ||||||
| 		for _, subTreeEntry := range docsEntries { |  | ||||||
| 			if subTreeEntry == nil { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			subTree := subTreeEntry.Tree() |  | ||||||
| 			if subTree == nil { |  | ||||||
| 				// this should be impossible; if subTreeEntry exists so should this. |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			childEntries, err := subTree.ListEntries() |  | ||||||
| 			if err != nil { |  | ||||||
| 				return "", nil, err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false) |  | ||||||
| 			if err != nil && !git.IsErrNotExist(err) { |  | ||||||
| 				return "", nil, err |  | ||||||
| 			} |  | ||||||
| 			if readmeFile != nil { |  | ||||||
| 				return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return "", readmeFile, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func renderDirectory(ctx *context.Context) { |  | ||||||
| 	entries := renderDirectoryFiles(ctx, 1*time.Second) |  | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ctx.Repo.TreePath != "" { |  | ||||||
| 		ctx.Data["HideRepoInfo"] = true |  | ||||||
| 		ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("findReadmeFileInEntries", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	renderReadmeFile(ctx, subfolder, readmeFile) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // localizedExtensions prepends the provided language code with and without a |  | ||||||
| // regional identifier to the provided extension. |  | ||||||
| // Note: the language code will always be lower-cased, if a region is present it must be separated with a `-` |  | ||||||
| // Note: ext should be prefixed with a `.` |  | ||||||
| func localizedExtensions(ext, languageCode string) (localizedExts []string) { |  | ||||||
| 	if len(languageCode) < 1 { |  | ||||||
| 		return []string{ext} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	lowerLangCode := "." + strings.ToLower(languageCode) |  | ||||||
|  |  | ||||||
| 	if strings.Contains(lowerLangCode, "-") { |  | ||||||
| 		underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_") |  | ||||||
| 		indexOfDash := strings.Index(lowerLangCode, "-") |  | ||||||
| 		// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md] |  | ||||||
| 		return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// e.g. [.en.md, .md] |  | ||||||
| 	return []string{lowerLangCode + ext, ext} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type fileInfo struct { | type fileInfo struct { | ||||||
| 	isTextFile bool | 	isTextFile bool | ||||||
| 	isLFSFile  bool | 	isLFSFile  bool | ||||||
| @@ -261,85 +110,6 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, | |||||||
| 	return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil | 	return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) { |  | ||||||
| 	target := readmeFile |  | ||||||
| 	if readmeFile != nil && readmeFile.IsLink() { |  | ||||||
| 		target, _ = readmeFile.FollowLinks() |  | ||||||
| 	} |  | ||||||
| 	if target == nil { |  | ||||||
| 		// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't) |  | ||||||
| 		// simply skip rendering the README |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ctx.Data["RawFileLink"] = "" |  | ||||||
| 	ctx.Data["ReadmeInList"] = true |  | ||||||
| 	ctx.Data["ReadmeExist"] = true |  | ||||||
| 	ctx.Data["FileIsSymlink"] = readmeFile.IsLink() |  | ||||||
|  |  | ||||||
| 	buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("getFileReader", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	defer dataRc.Close() |  | ||||||
|  |  | ||||||
| 	ctx.Data["FileIsText"] = fInfo.isTextFile |  | ||||||
| 	ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name()) |  | ||||||
| 	ctx.Data["FileSize"] = fInfo.fileSize |  | ||||||
| 	ctx.Data["IsLFSFile"] = fInfo.isLFSFile |  | ||||||
|  |  | ||||||
| 	if fInfo.isLFSFile { |  | ||||||
| 		filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name())) |  | ||||||
| 		ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if !fInfo.isTextFile { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { |  | ||||||
| 		// Pretend that this is a normal text file to display 'This file is too large to be shown' |  | ||||||
| 		ctx.Data["IsFileTooLarge"] = true |  | ||||||
| 		ctx.Data["IsTextFile"] = true |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) |  | ||||||
|  |  | ||||||
| 	if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" { |  | ||||||
| 		ctx.Data["IsMarkup"] = true |  | ||||||
| 		ctx.Data["MarkupType"] = markupType |  | ||||||
|  |  | ||||||
| 		rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ |  | ||||||
| 			CurrentRefPath:  ctx.Repo.BranchNameSubURL(), |  | ||||||
| 			CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder), |  | ||||||
| 		}). |  | ||||||
| 			WithMarkupType(markupType). |  | ||||||
| 			WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). |  | ||||||
|  |  | ||||||
| 		ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) |  | ||||||
| 			delete(ctx.Data, "IsMarkup") |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ctx.Data["IsMarkup"] != true { |  | ||||||
| 		ctx.Data["IsPlainText"] = true |  | ||||||
| 		content, err := io.ReadAll(rd) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Error("Read readme content failed: %v", err) |  | ||||||
| 		} |  | ||||||
| 		contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content)) |  | ||||||
| 		ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { |  | ||||||
| 		ctx.Data["CanEditReadmeFile"] = true |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { | func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { | ||||||
| 	// Show latest commit info of repository in table header, | 	// Show latest commit info of repository in table header, | ||||||
| 	// or of directory if not in root directory. | 	// or of directory if not in root directory. | ||||||
| @@ -371,287 +141,6 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { | |||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderFile(ctx *context.Context, entry *git.TreeEntry) { |  | ||||||
| 	ctx.Data["IsViewFile"] = true |  | ||||||
| 	ctx.Data["HideRepoInfo"] = true |  | ||||||
| 	blob := entry.Blob() |  | ||||||
| 	buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("getFileReader", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	defer dataRc.Close() |  | ||||||
|  |  | ||||||
| 	ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) |  | ||||||
| 	ctx.Data["FileIsSymlink"] = entry.IsLink() |  | ||||||
| 	ctx.Data["FileName"] = blob.Name() |  | ||||||
| 	ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) |  | ||||||
|  |  | ||||||
| 	commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("GetCommitByPath", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if !loadLatestCommitData(ctx, commit) { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ctx.Repo.TreePath == ".editorconfig" { |  | ||||||
| 		_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) |  | ||||||
| 		if editorconfigWarning != nil { |  | ||||||
| 			ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error()) |  | ||||||
| 		} |  | ||||||
| 		if editorconfigErr != nil { |  | ||||||
| 			ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error()) |  | ||||||
| 		} |  | ||||||
| 	} else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) { |  | ||||||
| 		_, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit) |  | ||||||
| 		if issueConfigErr != nil { |  | ||||||
| 			ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error()) |  | ||||||
| 		} |  | ||||||
| 	} else if actions.IsWorkflow(ctx.Repo.TreePath) { |  | ||||||
| 		content, err := actions.GetContentFromEntry(entry) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Error("actions.GetContentFromEntry: %v", err) |  | ||||||
| 		} |  | ||||||
| 		_, workFlowErr := model.ReadWorkflow(bytes.NewReader(content)) |  | ||||||
| 		if workFlowErr != nil { |  | ||||||
| 			ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) |  | ||||||
| 		} |  | ||||||
| 	} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { |  | ||||||
| 		if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { |  | ||||||
| 			_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) |  | ||||||
| 			if len(warnings) > 0 { |  | ||||||
| 				ctx.Data["FileWarning"] = strings.Join(warnings, "\n") |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	isDisplayingSource := ctx.FormString("display") == "source" |  | ||||||
| 	isDisplayingRendered := !isDisplayingSource |  | ||||||
|  |  | ||||||
| 	if fInfo.isLFSFile { |  | ||||||
| 		ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	isRepresentableAsText := fInfo.st.IsRepresentableAsText() |  | ||||||
| 	if !isRepresentableAsText { |  | ||||||
| 		// If we can't show plain text, always try to render. |  | ||||||
| 		isDisplayingSource = false |  | ||||||
| 		isDisplayingRendered = true |  | ||||||
| 	} |  | ||||||
| 	ctx.Data["IsLFSFile"] = fInfo.isLFSFile |  | ||||||
| 	ctx.Data["FileSize"] = fInfo.fileSize |  | ||||||
| 	ctx.Data["IsTextFile"] = fInfo.isTextFile |  | ||||||
| 	ctx.Data["IsRepresentableAsText"] = isRepresentableAsText |  | ||||||
| 	ctx.Data["IsDisplayingSource"] = isDisplayingSource |  | ||||||
| 	ctx.Data["IsDisplayingRendered"] = isDisplayingRendered |  | ||||||
| 	ctx.Data["IsExecutable"] = entry.IsExecutable() |  | ||||||
|  |  | ||||||
| 	isTextSource := fInfo.isTextFile || isDisplayingSource |  | ||||||
| 	ctx.Data["IsTextSource"] = isTextSource |  | ||||||
| 	if isTextSource { |  | ||||||
| 		ctx.Data["CanCopyContent"] = true |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Check LFS Lock |  | ||||||
| 	lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) |  | ||||||
| 	ctx.Data["LFSLock"] = lfsLock |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("GetTreePathLock", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if lfsLock != nil { |  | ||||||
| 		u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) |  | ||||||
| 		if err != nil { |  | ||||||
| 			ctx.ServerError("GetTreePathLock", err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		ctx.Data["LFSLockOwner"] = u.Name |  | ||||||
| 		ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink() |  | ||||||
| 		ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Assume file is not editable first. |  | ||||||
| 	if fInfo.isLFSFile { |  | ||||||
| 		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") |  | ||||||
| 	} else if !isRepresentableAsText { |  | ||||||
| 		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	switch { |  | ||||||
| 	case isRepresentableAsText: |  | ||||||
| 		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { |  | ||||||
| 			ctx.Data["IsFileTooLarge"] = true |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if fInfo.st.IsSvgImage() { |  | ||||||
| 			ctx.Data["IsImageFile"] = true |  | ||||||
| 			ctx.Data["CanCopyContent"] = true |  | ||||||
| 			ctx.Data["HasSourceRenderedToggle"] = true |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) |  | ||||||
|  |  | ||||||
| 		shouldRenderSource := ctx.FormString("display") == "source" |  | ||||||
| 		readmeExist := util.IsReadmeFileName(blob.Name()) |  | ||||||
| 		ctx.Data["ReadmeExist"] = readmeExist |  | ||||||
|  |  | ||||||
| 		markupType := markup.DetectMarkupTypeByFileName(blob.Name()) |  | ||||||
| 		if markupType == "" { |  | ||||||
| 			markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf)) |  | ||||||
| 		} |  | ||||||
| 		if markupType != "" { |  | ||||||
| 			ctx.Data["HasSourceRenderedToggle"] = true |  | ||||||
| 		} |  | ||||||
| 		if markupType != "" && !shouldRenderSource { |  | ||||||
| 			ctx.Data["IsMarkup"] = true |  | ||||||
| 			ctx.Data["MarkupType"] = markupType |  | ||||||
| 			metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) |  | ||||||
| 			metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() |  | ||||||
| 			rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ |  | ||||||
| 				CurrentRefPath:  ctx.Repo.BranchNameSubURL(), |  | ||||||
| 				CurrentTreePath: path.Dir(ctx.Repo.TreePath), |  | ||||||
| 			}). |  | ||||||
| 				WithMarkupType(markupType). |  | ||||||
| 				WithRelativePath(ctx.Repo.TreePath). |  | ||||||
| 				WithMetas(metas) |  | ||||||
|  |  | ||||||
| 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) |  | ||||||
| 			if err != nil { |  | ||||||
| 				ctx.ServerError("Render", err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 			// to prevent iframe load third-party url |  | ||||||
| 			ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") |  | ||||||
| 		} else { |  | ||||||
| 			buf, _ := io.ReadAll(rd) |  | ||||||
|  |  | ||||||
| 			// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html |  | ||||||
| 			//   empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line; |  | ||||||
| 			// Gitea uses the definition (like most modern editors): |  | ||||||
| 			//   empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines; |  | ||||||
| 			//   When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL. |  | ||||||
| 			//   To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines. |  | ||||||
| 			// This NumLines is only used for the display on the UI: "xxx lines" |  | ||||||
| 			if len(buf) == 0 { |  | ||||||
| 				ctx.Data["NumLines"] = 0 |  | ||||||
| 			} else { |  | ||||||
| 				ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1 |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			fileContent, lexerName, err := highlight.File(blob.Name(), language, buf) |  | ||||||
| 			ctx.Data["LexerName"] = lexerName |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Error("highlight.File failed, fallback to plain text: %v", err) |  | ||||||
| 				fileContent = highlight.PlainText(buf) |  | ||||||
| 			} |  | ||||||
| 			status := &charset.EscapeStatus{} |  | ||||||
| 			statuses := make([]*charset.EscapeStatus, len(fileContent)) |  | ||||||
| 			for i, line := range fileContent { |  | ||||||
| 				statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) |  | ||||||
| 				status = status.Or(statuses[i]) |  | ||||||
| 			} |  | ||||||
| 			ctx.Data["EscapeStatus"] = status |  | ||||||
| 			ctx.Data["FileContent"] = fileContent |  | ||||||
| 			ctx.Data["LineEscapeStatus"] = statuses |  | ||||||
| 		} |  | ||||||
| 		if !fInfo.isLFSFile { |  | ||||||
| 			if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { |  | ||||||
| 				if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { |  | ||||||
| 					ctx.Data["CanEditFile"] = false |  | ||||||
| 					ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") |  | ||||||
| 				} else { |  | ||||||
| 					ctx.Data["CanEditFile"] = true |  | ||||||
| 					ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") |  | ||||||
| 				} |  | ||||||
| 			} else if !ctx.Repo.IsViewBranch { |  | ||||||
| 				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") |  | ||||||
| 			} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { |  | ||||||
| 				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	case fInfo.st.IsPDF(): |  | ||||||
| 		ctx.Data["IsPDFFile"] = true |  | ||||||
| 	case fInfo.st.IsVideo(): |  | ||||||
| 		ctx.Data["IsVideoFile"] = true |  | ||||||
| 	case fInfo.st.IsAudio(): |  | ||||||
| 		ctx.Data["IsAudioFile"] = true |  | ||||||
| 	case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): |  | ||||||
| 		ctx.Data["IsImageFile"] = true |  | ||||||
| 		ctx.Data["CanCopyContent"] = true |  | ||||||
| 	default: |  | ||||||
| 		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { |  | ||||||
| 			ctx.Data["IsFileTooLarge"] = true |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go" |  | ||||||
| 		// It is used by "external renders", markupRender will execute external programs to get rendered content. |  | ||||||
| 		if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" { |  | ||||||
| 			rd := io.MultiReader(bytes.NewReader(buf), dataRc) |  | ||||||
| 			ctx.Data["IsMarkup"] = true |  | ||||||
| 			ctx.Data["MarkupType"] = markupType |  | ||||||
|  |  | ||||||
| 			rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ |  | ||||||
| 				CurrentRefPath:  ctx.Repo.BranchNameSubURL(), |  | ||||||
| 				CurrentTreePath: path.Dir(ctx.Repo.TreePath), |  | ||||||
| 			}). |  | ||||||
| 				WithMarkupType(markupType). |  | ||||||
| 				WithRelativePath(ctx.Repo.TreePath) |  | ||||||
|  |  | ||||||
| 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) |  | ||||||
| 			if err != nil { |  | ||||||
| 				ctx.ServerError("Render", err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ctx.Repo.GitRepo != nil { |  | ||||||
| 		checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID) |  | ||||||
| 		if checker != nil { |  | ||||||
| 			defer deferable() |  | ||||||
| 			attrs, err := checker.CheckPath(ctx.Repo.TreePath) |  | ||||||
| 			if err == nil { |  | ||||||
| 				ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value() |  | ||||||
| 				ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value() |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() { |  | ||||||
| 		img, _, err := image.DecodeConfig(bytes.NewReader(buf)) |  | ||||||
| 		if err == nil { |  | ||||||
| 			// There are Image formats go can't decode |  | ||||||
| 			// Instead of throwing an error in that case, we show the size only when we can decode |  | ||||||
| 			ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { |  | ||||||
| 		if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { |  | ||||||
| 			ctx.Data["CanDeleteFile"] = false |  | ||||||
| 			ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") |  | ||||||
| 		} else { |  | ||||||
| 			ctx.Data["CanDeleteFile"] = true |  | ||||||
| 			ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") |  | ||||||
| 		} |  | ||||||
| 	} else if !ctx.Repo.IsViewBranch { |  | ||||||
| 		ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") |  | ||||||
| 	} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { |  | ||||||
| 		ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) { | func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) { | ||||||
| 	markupRd, markupWr := io.Pipe() | 	markupRd, markupWr := io.Pipe() | ||||||
| 	defer markupWr.Close() | 	defer markupWr.Close() | ||||||
| @@ -728,59 +217,6 @@ func checkHomeCodeViewable(ctx *context.Context) { | |||||||
| 	ctx.NotFound("Home", errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo"))) | 	ctx.NotFound("Home", errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo"))) | ||||||
| } | } | ||||||
|  |  | ||||||
| func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) { |  | ||||||
| 	if entry.Name() != "" { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		HandleGitError(ctx, "Repo.Commit.SubTree", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	allEntries, err := tree.ListEntries() |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("ListEntries", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	for _, entry := range allEntries { |  | ||||||
| 		if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" { |  | ||||||
| 			// Read Citation file contents |  | ||||||
| 			if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { |  | ||||||
| 				log.Error("checkCitationFile: GetBlobContent: %v", err) |  | ||||||
| 			} else { |  | ||||||
| 				ctx.Data["CitiationExist"] = true |  | ||||||
| 				ctx.PageData["citationFileContent"] = content |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Home render repository home page |  | ||||||
| func Home(ctx *context.Context) { |  | ||||||
| 	if setting.Other.EnableFeed { |  | ||||||
| 		isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req) |  | ||||||
| 		if isFeed { |  | ||||||
| 			switch { |  | ||||||
| 			case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): |  | ||||||
| 				feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) |  | ||||||
| 			case ctx.Repo.TreePath == "": |  | ||||||
| 				feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) |  | ||||||
| 			case ctx.Repo.TreePath != "": |  | ||||||
| 				feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) |  | ||||||
| 			} |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	checkHomeCodeViewable(ctx) |  | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	renderHomeCode(ctx) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body | // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body | ||||||
| func LastCommit(ctx *context.Context) { | func LastCommit(ctx *context.Context) { | ||||||
| 	checkHomeCodeViewable(ctx) | 	checkHomeCodeViewable(ctx) | ||||||
| @@ -877,220 +313,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri | |||||||
| 	return allEntries | 	return allEntries | ||||||
| } | } | ||||||
|  |  | ||||||
| func renderLanguageStats(ctx *context.Context) { |  | ||||||
| 	langs, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("Repo.GetTopLanguageStats", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ctx.Data["LanguageStats"] = langs |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func renderRepoTopics(ctx *context.Context) { |  | ||||||
| 	topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{ |  | ||||||
| 		RepoID: ctx.Repo.Repository.ID, |  | ||||||
| 	}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("models.FindTopics", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	ctx.Data["Topics"] = topics |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func prepareOpenWithEditorApps(ctx *context.Context) { |  | ||||||
| 	var tmplApps []map[string]any |  | ||||||
| 	apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx) |  | ||||||
| 	if len(apps) == 0 { |  | ||||||
| 		apps = setting.DefaultOpenWithEditorApps() |  | ||||||
| 	} |  | ||||||
| 	for _, app := range apps { |  | ||||||
| 		schema, _, _ := strings.Cut(app.OpenURL, ":") |  | ||||||
| 		var iconHTML template.HTML |  | ||||||
| 		if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" { |  | ||||||
| 			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2") |  | ||||||
| 		} else { |  | ||||||
| 			iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future |  | ||||||
| 		} |  | ||||||
| 		tmplApps = append(tmplApps, map[string]any{ |  | ||||||
| 			"DisplayName": app.DisplayName, |  | ||||||
| 			"OpenURL":     app.OpenURL, |  | ||||||
| 			"IconHTML":    iconHTML, |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| 	ctx.Data["OpenWithEditorApps"] = tmplApps |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func renderHomeCode(ctx *context.Context) { |  | ||||||
| 	ctx.Data["PageIsViewCode"] = true |  | ||||||
| 	ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled |  | ||||||
| 	prepareOpenWithEditorApps(ctx) |  | ||||||
|  |  | ||||||
| 	if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { |  | ||||||
| 		showEmpty := true |  | ||||||
| 		var err error |  | ||||||
| 		if ctx.Repo.GitRepo != nil { |  | ||||||
| 			showEmpty, err = ctx.Repo.GitRepo.IsEmpty() |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Error("GitRepo.IsEmpty: %v", err) |  | ||||||
| 				ctx.Repo.Repository.Status = repo_model.RepositoryBroken |  | ||||||
| 				showEmpty = true |  | ||||||
| 				ctx.Flash.Error(ctx.Tr("error.occurred"), true) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		if showEmpty { |  | ||||||
| 			ctx.HTML(http.StatusOK, tplRepoEMPTY) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// the repo is not really empty, so we should update the modal in database |  | ||||||
| 		// such problem may be caused by: |  | ||||||
| 		// 1) an error occurs during pushing/receiving.  2) the user replaces an empty git repo manually |  | ||||||
| 		// and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. |  | ||||||
| 		// it's possible for a repository to be non-empty by that flag but still 500 |  | ||||||
| 		// because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. |  | ||||||
| 		ctx.Repo.Repository.IsEmpty = false |  | ||||||
| 		if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { |  | ||||||
| 			ctx.ServerError("UpdateRepositoryCols", err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { |  | ||||||
| 			ctx.ServerError("UpdateRepoSize", err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values |  | ||||||
| 		link := ctx.Link |  | ||||||
| 		if ctx.Req.URL.RawQuery != "" { |  | ||||||
| 			link += "?" + ctx.Req.URL.RawQuery |  | ||||||
| 		} |  | ||||||
| 		ctx.Redirect(link) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name |  | ||||||
| 	if len(ctx.Repo.Repository.Description) > 0 { |  | ||||||
| 		title += ": " + ctx.Repo.Repository.Description |  | ||||||
| 	} |  | ||||||
| 	ctx.Data["Title"] = title |  | ||||||
|  |  | ||||||
| 	// Get Topics of this repo |  | ||||||
| 	renderRepoTopics(ctx) |  | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Get current entry user currently looking at. |  | ||||||
| 	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	checkOutdatedBranch(ctx) |  | ||||||
|  |  | ||||||
| 	checkCitationFile(ctx, entry) |  | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	renderLanguageStats(ctx) |  | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if entry.IsDir() { |  | ||||||
| 		renderDirectory(ctx) |  | ||||||
| 	} else { |  | ||||||
| 		renderFile(ctx, entry) |  | ||||||
| 	} |  | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ctx.Doer != nil { |  | ||||||
| 		if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil { |  | ||||||
| 			ctx.ServerError("GetBaseRepo", err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		opts := &git_model.FindRecentlyPushedNewBranchesOptions{ |  | ||||||
| 			Repo:     ctx.Repo.Repository, |  | ||||||
| 			BaseRepo: ctx.Repo.Repository, |  | ||||||
| 		} |  | ||||||
| 		if ctx.Repo.Repository.IsFork { |  | ||||||
| 			opts.BaseRepo = ctx.Repo.Repository.BaseRepo |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer) |  | ||||||
| 		if err != nil { |  | ||||||
| 			ctx.ServerError("GetUserRepoPermission", err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && |  | ||||||
| 			opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && |  | ||||||
| 			baseRepoPerm.CanRead(unit_model.TypePullRequests) { |  | ||||||
| 			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Error("FindRecentlyPushedNewBranches failed: %v", err) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var treeNames []string |  | ||||||
| 	paths := make([]string, 0, 5) |  | ||||||
| 	if len(ctx.Repo.TreePath) > 0 { |  | ||||||
| 		treeNames = strings.Split(ctx.Repo.TreePath, "/") |  | ||||||
| 		for i := range treeNames { |  | ||||||
| 			paths = append(paths, strings.Join(treeNames[:i+1], "/")) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		ctx.Data["HasParentPath"] = true |  | ||||||
| 		if len(paths)-2 >= 0 { |  | ||||||
| 			ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ctx.Data["Paths"] = paths |  | ||||||
|  |  | ||||||
| 	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() |  | ||||||
| 	treeLink := branchLink |  | ||||||
| 	if len(ctx.Repo.TreePath) > 0 { |  | ||||||
| 		treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) |  | ||||||
| 	} |  | ||||||
| 	ctx.Data["TreeLink"] = treeLink |  | ||||||
| 	ctx.Data["TreeNames"] = treeNames |  | ||||||
| 	ctx.Data["BranchLink"] = branchLink |  | ||||||
| 	ctx.Data["LicenseFileName"] = repo_service.LicenseFileName |  | ||||||
| 	ctx.HTML(http.StatusOK, tplRepoHome) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func checkOutdatedBranch(ctx *context.Context) { |  | ||||||
| 	if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName` |  | ||||||
| 	commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error("GetBranchCommitID: %v", err) |  | ||||||
| 		// Don't return an error page, as it can be rechecked the next time the user opens the page. |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error("GetBranch: %v", err) |  | ||||||
| 		// Don't return an error page, as it can be rechecked the next time the user opens the page. |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if dbBranch.CommitID != commit.ID.String() { |  | ||||||
| 		ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // RenderUserCards render a page show users according the input template | // RenderUserCards render a page show users according the input template | ||||||
| func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) { | func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) { | ||||||
| 	page := ctx.FormInt("page") | 	page := ctx.FormInt("page") | ||||||
|   | |||||||
							
								
								
									
										313
									
								
								routers/web/repo/view_file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								routers/web/repo/view_file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,313 @@ | |||||||
|  | // Copyright 2024 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package repo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"fmt" | ||||||
|  | 	"image" | ||||||
|  | 	"io" | ||||||
|  | 	"path" | ||||||
|  | 	"slices" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	git_model "code.gitea.io/gitea/models/git" | ||||||
|  | 	issue_model "code.gitea.io/gitea/models/issues" | ||||||
|  | 	"code.gitea.io/gitea/models/renderhelper" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/actions" | ||||||
|  | 	"code.gitea.io/gitea/modules/charset" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/highlight" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | 	"code.gitea.io/gitea/services/context" | ||||||
|  | 	issue_service "code.gitea.io/gitea/services/issue" | ||||||
|  | 	files_service "code.gitea.io/gitea/services/repository/files" | ||||||
|  |  | ||||||
|  | 	"github.com/nektos/act/pkg/model" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func renderFile(ctx *context.Context, entry *git.TreeEntry) { | ||||||
|  | 	ctx.Data["IsViewFile"] = true | ||||||
|  | 	ctx.Data["HideRepoInfo"] = true | ||||||
|  | 	blob := entry.Blob() | ||||||
|  | 	buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("getFileReader", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer dataRc.Close() | ||||||
|  |  | ||||||
|  | 	ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) | ||||||
|  | 	ctx.Data["FileIsSymlink"] = entry.IsLink() | ||||||
|  | 	ctx.Data["FileName"] = blob.Name() | ||||||
|  | 	ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) | ||||||
|  |  | ||||||
|  | 	commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("GetCommitByPath", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !loadLatestCommitData(ctx, commit) { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.TreePath == ".editorconfig" { | ||||||
|  | 		_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) | ||||||
|  | 		if editorconfigWarning != nil { | ||||||
|  | 			ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error()) | ||||||
|  | 		} | ||||||
|  | 		if editorconfigErr != nil { | ||||||
|  | 			ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error()) | ||||||
|  | 		} | ||||||
|  | 	} else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) { | ||||||
|  | 		_, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit) | ||||||
|  | 		if issueConfigErr != nil { | ||||||
|  | 			ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error()) | ||||||
|  | 		} | ||||||
|  | 	} else if actions.IsWorkflow(ctx.Repo.TreePath) { | ||||||
|  | 		content, err := actions.GetContentFromEntry(entry) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("actions.GetContentFromEntry: %v", err) | ||||||
|  | 		} | ||||||
|  | 		_, workFlowErr := model.ReadWorkflow(bytes.NewReader(content)) | ||||||
|  | 		if workFlowErr != nil { | ||||||
|  | 			ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) | ||||||
|  | 		} | ||||||
|  | 	} else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { | ||||||
|  | 		if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { | ||||||
|  | 			_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) | ||||||
|  | 			if len(warnings) > 0 { | ||||||
|  | 				ctx.Data["FileWarning"] = strings.Join(warnings, "\n") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	isDisplayingSource := ctx.FormString("display") == "source" | ||||||
|  | 	isDisplayingRendered := !isDisplayingSource | ||||||
|  |  | ||||||
|  | 	if fInfo.isLFSFile { | ||||||
|  | 		ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	isRepresentableAsText := fInfo.st.IsRepresentableAsText() | ||||||
|  | 	if !isRepresentableAsText { | ||||||
|  | 		// If we can't show plain text, always try to render. | ||||||
|  | 		isDisplayingSource = false | ||||||
|  | 		isDisplayingRendered = true | ||||||
|  | 	} | ||||||
|  | 	ctx.Data["IsLFSFile"] = fInfo.isLFSFile | ||||||
|  | 	ctx.Data["FileSize"] = fInfo.fileSize | ||||||
|  | 	ctx.Data["IsTextFile"] = fInfo.isTextFile | ||||||
|  | 	ctx.Data["IsRepresentableAsText"] = isRepresentableAsText | ||||||
|  | 	ctx.Data["IsDisplayingSource"] = isDisplayingSource | ||||||
|  | 	ctx.Data["IsDisplayingRendered"] = isDisplayingRendered | ||||||
|  | 	ctx.Data["IsExecutable"] = entry.IsExecutable() | ||||||
|  |  | ||||||
|  | 	isTextSource := fInfo.isTextFile || isDisplayingSource | ||||||
|  | 	ctx.Data["IsTextSource"] = isTextSource | ||||||
|  | 	if isTextSource { | ||||||
|  | 		ctx.Data["CanCopyContent"] = true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check LFS Lock | ||||||
|  | 	lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) | ||||||
|  | 	ctx.Data["LFSLock"] = lfsLock | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("GetTreePathLock", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if lfsLock != nil { | ||||||
|  | 		u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.ServerError("GetTreePathLock", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx.Data["LFSLockOwner"] = u.Name | ||||||
|  | 		ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink() | ||||||
|  | 		ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Assume file is not editable first. | ||||||
|  | 	if fInfo.isLFSFile { | ||||||
|  | 		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") | ||||||
|  | 	} else if !isRepresentableAsText { | ||||||
|  | 		ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	switch { | ||||||
|  | 	case isRepresentableAsText: | ||||||
|  | 		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { | ||||||
|  | 			ctx.Data["IsFileTooLarge"] = true | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if fInfo.st.IsSvgImage() { | ||||||
|  | 			ctx.Data["IsImageFile"] = true | ||||||
|  | 			ctx.Data["CanCopyContent"] = true | ||||||
|  | 			ctx.Data["HasSourceRenderedToggle"] = true | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) | ||||||
|  |  | ||||||
|  | 		shouldRenderSource := ctx.FormString("display") == "source" | ||||||
|  | 		readmeExist := util.IsReadmeFileName(blob.Name()) | ||||||
|  | 		ctx.Data["ReadmeExist"] = readmeExist | ||||||
|  |  | ||||||
|  | 		markupType := markup.DetectMarkupTypeByFileName(blob.Name()) | ||||||
|  | 		if markupType == "" { | ||||||
|  | 			markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf)) | ||||||
|  | 		} | ||||||
|  | 		if markupType != "" { | ||||||
|  | 			ctx.Data["HasSourceRenderedToggle"] = true | ||||||
|  | 		} | ||||||
|  | 		if markupType != "" && !shouldRenderSource { | ||||||
|  | 			ctx.Data["IsMarkup"] = true | ||||||
|  | 			ctx.Data["MarkupType"] = markupType | ||||||
|  | 			metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) | ||||||
|  | 			metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() | ||||||
|  | 			rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ | ||||||
|  | 				CurrentRefPath:  ctx.Repo.BranchNameSubURL(), | ||||||
|  | 				CurrentTreePath: path.Dir(ctx.Repo.TreePath), | ||||||
|  | 			}). | ||||||
|  | 				WithMarkupType(markupType). | ||||||
|  | 				WithRelativePath(ctx.Repo.TreePath). | ||||||
|  | 				WithMetas(metas) | ||||||
|  |  | ||||||
|  | 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("Render", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			// to prevent iframe load third-party url | ||||||
|  | 			ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") | ||||||
|  | 		} else { | ||||||
|  | 			buf, _ := io.ReadAll(rd) | ||||||
|  |  | ||||||
|  | 			// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html | ||||||
|  | 			//   empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line; | ||||||
|  | 			// Gitea uses the definition (like most modern editors): | ||||||
|  | 			//   empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines; | ||||||
|  | 			//   When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL. | ||||||
|  | 			//   To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines. | ||||||
|  | 			// This NumLines is only used for the display on the UI: "xxx lines" | ||||||
|  | 			if len(buf) == 0 { | ||||||
|  | 				ctx.Data["NumLines"] = 0 | ||||||
|  | 			} else { | ||||||
|  | 				ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1 | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			fileContent, lexerName, err := highlight.File(blob.Name(), language, buf) | ||||||
|  | 			ctx.Data["LexerName"] = lexerName | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("highlight.File failed, fallback to plain text: %v", err) | ||||||
|  | 				fileContent = highlight.PlainText(buf) | ||||||
|  | 			} | ||||||
|  | 			status := &charset.EscapeStatus{} | ||||||
|  | 			statuses := make([]*charset.EscapeStatus, len(fileContent)) | ||||||
|  | 			for i, line := range fileContent { | ||||||
|  | 				statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) | ||||||
|  | 				status = status.Or(statuses[i]) | ||||||
|  | 			} | ||||||
|  | 			ctx.Data["EscapeStatus"] = status | ||||||
|  | 			ctx.Data["FileContent"] = fileContent | ||||||
|  | 			ctx.Data["LineEscapeStatus"] = statuses | ||||||
|  | 		} | ||||||
|  | 		if !fInfo.isLFSFile { | ||||||
|  | 			if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { | ||||||
|  | 				if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { | ||||||
|  | 					ctx.Data["CanEditFile"] = false | ||||||
|  | 					ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") | ||||||
|  | 				} else { | ||||||
|  | 					ctx.Data["CanEditFile"] = true | ||||||
|  | 					ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") | ||||||
|  | 				} | ||||||
|  | 			} else if !ctx.Repo.IsViewBranch { | ||||||
|  | 				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") | ||||||
|  | 			} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { | ||||||
|  | 				ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	case fInfo.st.IsPDF(): | ||||||
|  | 		ctx.Data["IsPDFFile"] = true | ||||||
|  | 	case fInfo.st.IsVideo(): | ||||||
|  | 		ctx.Data["IsVideoFile"] = true | ||||||
|  | 	case fInfo.st.IsAudio(): | ||||||
|  | 		ctx.Data["IsAudioFile"] = true | ||||||
|  | 	case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): | ||||||
|  | 		ctx.Data["IsImageFile"] = true | ||||||
|  | 		ctx.Data["CanCopyContent"] = true | ||||||
|  | 	default: | ||||||
|  | 		if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { | ||||||
|  | 			ctx.Data["IsFileTooLarge"] = true | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go" | ||||||
|  | 		// It is used by "external renders", markupRender will execute external programs to get rendered content. | ||||||
|  | 		if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" { | ||||||
|  | 			rd := io.MultiReader(bytes.NewReader(buf), dataRc) | ||||||
|  | 			ctx.Data["IsMarkup"] = true | ||||||
|  | 			ctx.Data["MarkupType"] = markupType | ||||||
|  |  | ||||||
|  | 			rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ | ||||||
|  | 				CurrentRefPath:  ctx.Repo.BranchNameSubURL(), | ||||||
|  | 				CurrentTreePath: path.Dir(ctx.Repo.TreePath), | ||||||
|  | 			}). | ||||||
|  | 				WithMarkupType(markupType). | ||||||
|  | 				WithRelativePath(ctx.Repo.TreePath) | ||||||
|  |  | ||||||
|  | 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("Render", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.GitRepo != nil { | ||||||
|  | 		checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID) | ||||||
|  | 		if checker != nil { | ||||||
|  | 			defer deferable() | ||||||
|  | 			attrs, err := checker.CheckPath(ctx.Repo.TreePath) | ||||||
|  | 			if err == nil { | ||||||
|  | 				ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value() | ||||||
|  | 				ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value() | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() { | ||||||
|  | 		img, _, err := image.DecodeConfig(bytes.NewReader(buf)) | ||||||
|  | 		if err == nil { | ||||||
|  | 			// There are Image formats go can't decode | ||||||
|  | 			// Instead of throwing an error in that case, we show the size only when we can decode | ||||||
|  | 			ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { | ||||||
|  | 		if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { | ||||||
|  | 			ctx.Data["CanDeleteFile"] = false | ||||||
|  | 			ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Data["CanDeleteFile"] = true | ||||||
|  | 			ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") | ||||||
|  | 		} | ||||||
|  | 	} else if !ctx.Repo.IsViewBranch { | ||||||
|  | 		ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") | ||||||
|  | 	} else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { | ||||||
|  | 		ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										351
									
								
								routers/web/repo/view_home.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										351
									
								
								routers/web/repo/view_home.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,351 @@ | |||||||
|  | // Copyright 2024 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package repo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"html/template" | ||||||
|  | 	"net/http" | ||||||
|  | 	"path" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	git_model "code.gitea.io/gitea/models/git" | ||||||
|  | 	access_model "code.gitea.io/gitea/models/perm/access" | ||||||
|  | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
|  | 	unit_model "code.gitea.io/gitea/models/unit" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	repo_module "code.gitea.io/gitea/modules/repository" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/svg" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | 	"code.gitea.io/gitea/routers/web/feed" | ||||||
|  | 	"code.gitea.io/gitea/services/context" | ||||||
|  | 	repo_service "code.gitea.io/gitea/services/repository" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func checkOutdatedBranch(ctx *context.Context) { | ||||||
|  | 	if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName` | ||||||
|  | 	commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("GetBranchCommitID: %v", err) | ||||||
|  | 		// Don't return an error page, as it can be rechecked the next time the user opens the page. | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("GetBranch: %v", err) | ||||||
|  | 		// Don't return an error page, as it can be rechecked the next time the user opens the page. | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if dbBranch.CommitID != commit.ID.String() { | ||||||
|  | 		ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prepareHomeSidebarRepoTopics(ctx *context.Context) { | ||||||
|  | 	topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{ | ||||||
|  | 		RepoID: ctx.Repo.Repository.ID, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("models.FindTopics", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ctx.Data["Topics"] = topics | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prepareOpenWithEditorApps(ctx *context.Context) { | ||||||
|  | 	var tmplApps []map[string]any | ||||||
|  | 	apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx) | ||||||
|  | 	if len(apps) == 0 { | ||||||
|  | 		apps = setting.DefaultOpenWithEditorApps() | ||||||
|  | 	} | ||||||
|  | 	for _, app := range apps { | ||||||
|  | 		schema, _, _ := strings.Cut(app.OpenURL, ":") | ||||||
|  | 		var iconHTML template.HTML | ||||||
|  | 		if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" { | ||||||
|  | 			iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2") | ||||||
|  | 		} else { | ||||||
|  | 			iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future | ||||||
|  | 		} | ||||||
|  | 		tmplApps = append(tmplApps, map[string]any{ | ||||||
|  | 			"DisplayName": app.DisplayName, | ||||||
|  | 			"OpenURL":     app.OpenURL, | ||||||
|  | 			"IconHTML":    iconHTML, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 	ctx.Data["OpenWithEditorApps"] = tmplApps | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prepareHomeSidebarCitationFile(ctx *context.Context, entry *git.TreeEntry) { | ||||||
|  | 	if entry.Name() != "" { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		HandleGitError(ctx, "Repo.Commit.SubTree", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	allEntries, err := tree.ListEntries() | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("ListEntries", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	for _, entry := range allEntries { | ||||||
|  | 		if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" { | ||||||
|  | 			// Read Citation file contents | ||||||
|  | 			if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { | ||||||
|  | 				log.Error("checkCitationFile: GetBlobContent: %v", err) | ||||||
|  | 			} else { | ||||||
|  | 				ctx.Data["CitiationExist"] = true | ||||||
|  | 				ctx.PageData["citationFileContent"] = content | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prepareHomeSidebarLicenses(ctx *context.Context) { | ||||||
|  | 	repoLicenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("GetRepoLicenses", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ctx.Data["DetectedRepoLicenses"] = repoLicenses.StringList() | ||||||
|  | 	ctx.Data["LicenseFileName"] = repo_service.LicenseFileName | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prepareToRenderDirectory(ctx *context.Context) { | ||||||
|  | 	entries := renderDirectoryFiles(ctx, 1*time.Second) | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.TreePath != "" { | ||||||
|  | 		ctx.Data["HideRepoInfo"] = true | ||||||
|  | 		ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("findReadmeFileInEntries", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	prepareToRenderReadmeFile(ctx, subfolder, readmeFile) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prepareHomeSidebarLanguageStats(ctx *context.Context) { | ||||||
|  | 	langs, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("Repo.GetTopLanguageStats", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Data["LanguageStats"] = langs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prepareHomeSidebarLatestRelease(ctx *context.Context) { | ||||||
|  | 	if !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeReleases) { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID) | ||||||
|  | 	if err != nil && !repo_model.IsErrReleaseNotExist(err) { | ||||||
|  | 		ctx.ServerError("GetLatestReleaseByRepoID", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if release != nil { | ||||||
|  | 		if err = release.LoadAttributes(ctx); err != nil { | ||||||
|  | 			ctx.ServerError("release.LoadAttributes", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx.Data["LatestRelease"] = release | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func renderHomeCode(ctx *context.Context) { | ||||||
|  | 	ctx.Data["PageIsViewCode"] = true | ||||||
|  | 	ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled | ||||||
|  | 	prepareOpenWithEditorApps(ctx) | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { | ||||||
|  | 		showEmpty := true | ||||||
|  | 		var err error | ||||||
|  | 		if ctx.Repo.GitRepo != nil { | ||||||
|  | 			showEmpty, err = ctx.Repo.GitRepo.IsEmpty() | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("GitRepo.IsEmpty: %v", err) | ||||||
|  | 				ctx.Repo.Repository.Status = repo_model.RepositoryBroken | ||||||
|  | 				showEmpty = true | ||||||
|  | 				ctx.Flash.Error(ctx.Tr("error.occurred"), true) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if showEmpty { | ||||||
|  | 			ctx.HTML(http.StatusOK, tplRepoEMPTY) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// the repo is not really empty, so we should update the modal in database | ||||||
|  | 		// such problem may be caused by: | ||||||
|  | 		// 1) an error occurs during pushing/receiving.  2) the user replaces an empty git repo manually | ||||||
|  | 		// and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. | ||||||
|  | 		// it's possible for a repository to be non-empty by that flag but still 500 | ||||||
|  | 		// because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. | ||||||
|  | 		ctx.Repo.Repository.IsEmpty = false | ||||||
|  | 		if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { | ||||||
|  | 			ctx.ServerError("UpdateRepositoryCols", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { | ||||||
|  | 			ctx.ServerError("UpdateRepoSize", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values | ||||||
|  | 		link := ctx.Link | ||||||
|  | 		if ctx.Req.URL.RawQuery != "" { | ||||||
|  | 			link += "?" + ctx.Req.URL.RawQuery | ||||||
|  | 		} | ||||||
|  | 		ctx.Redirect(link) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name | ||||||
|  | 	if len(ctx.Repo.Repository.Description) > 0 { | ||||||
|  | 		title += ": " + ctx.Repo.Repository.Description | ||||||
|  | 	} | ||||||
|  | 	ctx.Data["Title"] = title | ||||||
|  |  | ||||||
|  | 	// Get Topics of this repo | ||||||
|  | 	prepareHomeSidebarRepoTopics(ctx) | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get current entry user currently looking at. | ||||||
|  | 	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	checkOutdatedBranch(ctx) | ||||||
|  |  | ||||||
|  | 	if entry.IsDir() { | ||||||
|  | 		prepareToRenderDirectory(ctx) | ||||||
|  | 	} else { | ||||||
|  | 		renderFile(ctx, entry) | ||||||
|  | 	} | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Doer != nil { | ||||||
|  | 		if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil { | ||||||
|  | 			ctx.ServerError("GetBaseRepo", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		opts := &git_model.FindRecentlyPushedNewBranchesOptions{ | ||||||
|  | 			Repo:     ctx.Repo.Repository, | ||||||
|  | 			BaseRepo: ctx.Repo.Repository, | ||||||
|  | 		} | ||||||
|  | 		if ctx.Repo.Repository.IsFork { | ||||||
|  | 			opts.BaseRepo = ctx.Repo.Repository.BaseRepo | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.ServerError("GetUserRepoPermission", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && | ||||||
|  | 			opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && | ||||||
|  | 			baseRepoPerm.CanRead(unit_model.TypePullRequests) { | ||||||
|  | 			ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Error("FindRecentlyPushedNewBranches failed: %v", err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var treeNames, paths []string | ||||||
|  | 	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() | ||||||
|  | 	treeLink := branchLink | ||||||
|  | 	if ctx.Repo.TreePath != "" { | ||||||
|  | 		treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) | ||||||
|  | 		treeNames = strings.Split(ctx.Repo.TreePath, "/") | ||||||
|  | 		for i := range treeNames { | ||||||
|  | 			paths = append(paths, strings.Join(treeNames[:i+1], "/")) | ||||||
|  | 		} | ||||||
|  | 		ctx.Data["HasParentPath"] = true | ||||||
|  | 		if len(paths)-2 >= 0 { | ||||||
|  | 			ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	isTreePathRoot := ctx.Repo.TreePath == "" | ||||||
|  | 	if isTreePathRoot { | ||||||
|  | 		prepareHomeSidebarLicenses(ctx) | ||||||
|  | 		if ctx.Written() { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		prepareHomeSidebarCitationFile(ctx, entry) | ||||||
|  | 		if ctx.Written() { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		prepareHomeSidebarLanguageStats(ctx) | ||||||
|  | 		if ctx.Written() { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		prepareHomeSidebarLatestRelease(ctx) | ||||||
|  | 		if ctx.Written() { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Data["Paths"] = paths | ||||||
|  | 	ctx.Data["TreeLink"] = treeLink | ||||||
|  | 	ctx.Data["TreeNames"] = treeNames | ||||||
|  | 	ctx.Data["BranchLink"] = branchLink | ||||||
|  | 	ctx.HTML(http.StatusOK, tplRepoHome) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Home render repository home page | ||||||
|  | func Home(ctx *context.Context) { | ||||||
|  | 	if setting.Other.EnableFeed { | ||||||
|  | 		isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req) | ||||||
|  | 		if isFeed { | ||||||
|  | 			switch { | ||||||
|  | 			case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): | ||||||
|  | 				feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) | ||||||
|  | 			case ctx.Repo.TreePath == "": | ||||||
|  | 				feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) | ||||||
|  | 			case ctx.Repo.TreePath != "": | ||||||
|  | 				feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) | ||||||
|  | 			} | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	checkHomeCodeViewable(ctx) | ||||||
|  | 	if ctx.Written() { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	renderHomeCode(ctx) | ||||||
|  | } | ||||||
							
								
								
									
										218
									
								
								routers/web/repo/view_readme.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								routers/web/repo/view_readme.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | |||||||
|  | // Copyright 2024 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package repo | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"fmt" | ||||||
|  | 	"html/template" | ||||||
|  | 	"io" | ||||||
|  | 	"net/url" | ||||||
|  | 	"path" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/renderhelper" | ||||||
|  | 	"code.gitea.io/gitea/modules/base" | ||||||
|  | 	"code.gitea.io/gitea/modules/charset" | ||||||
|  | 	"code.gitea.io/gitea/modules/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | 	"code.gitea.io/gitea/services/context" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // locate a README for a tree in one of the supported paths. | ||||||
|  | // | ||||||
|  | // entries is passed to reduce calls to ListEntries(), so | ||||||
|  | // this has precondition: | ||||||
|  | // | ||||||
|  | //	entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries() | ||||||
|  | // | ||||||
|  | // FIXME: There has to be a more efficient way of doing this | ||||||
|  | func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) { | ||||||
|  | 	// Create a list of extensions in priority order | ||||||
|  | 	// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md | ||||||
|  | 	// 2. Txt files - e.g. README.txt | ||||||
|  | 	// 3. No extension - e.g. README | ||||||
|  | 	exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority | ||||||
|  | 	extCount := len(exts) | ||||||
|  | 	readmeFiles := make([]*git.TreeEntry, extCount+1) | ||||||
|  |  | ||||||
|  | 	docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/) | ||||||
|  | 	for _, entry := range entries { | ||||||
|  | 		if tryWellKnownDirs && entry.IsDir() { | ||||||
|  | 			// as a special case for the top-level repo introduction README, | ||||||
|  | 			// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ... | ||||||
|  | 			// (note that docsEntries is ignored unless we are at the root) | ||||||
|  | 			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 | ||||||
|  | 		} | ||||||
|  | 		if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { | ||||||
|  | 			log.Debug("Potential readme file: %s", entry.Name()) | ||||||
|  | 			if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { | ||||||
|  | 				if entry.IsLink() { | ||||||
|  | 					target, err := entry.FollowLinks() | ||||||
|  | 					if err != nil && !git.IsErrBadLink(err) { | ||||||
|  | 						return "", nil, err | ||||||
|  | 					} else if target != nil && (target.IsExecutable() || target.IsRegular()) { | ||||||
|  | 						readmeFiles[i] = entry | ||||||
|  | 					} | ||||||
|  | 				} else { | ||||||
|  | 					readmeFiles[i] = entry | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	var readmeFile *git.TreeEntry | ||||||
|  | 	for _, f := range readmeFiles { | ||||||
|  | 		if f != nil { | ||||||
|  | 			readmeFile = f | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Repo.TreePath == "" && readmeFile == nil { | ||||||
|  | 		for _, subTreeEntry := range docsEntries { | ||||||
|  | 			if subTreeEntry == nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			subTree := subTreeEntry.Tree() | ||||||
|  | 			if subTree == nil { | ||||||
|  | 				// this should be impossible; if subTreeEntry exists so should this. | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			childEntries, err := subTree.ListEntries() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return "", nil, err | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false) | ||||||
|  | 			if err != nil && !git.IsErrNotExist(err) { | ||||||
|  | 				return "", nil, err | ||||||
|  | 			} | ||||||
|  | 			if readmeFile != nil { | ||||||
|  | 				return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "", readmeFile, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // localizedExtensions prepends the provided language code with and without a | ||||||
|  | // regional identifier to the provided extension. | ||||||
|  | // Note: the language code will always be lower-cased, if a region is present it must be separated with a `-` | ||||||
|  | // Note: ext should be prefixed with a `.` | ||||||
|  | func localizedExtensions(ext, languageCode string) (localizedExts []string) { | ||||||
|  | 	if len(languageCode) < 1 { | ||||||
|  | 		return []string{ext} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	lowerLangCode := "." + strings.ToLower(languageCode) | ||||||
|  |  | ||||||
|  | 	if strings.Contains(lowerLangCode, "-") { | ||||||
|  | 		underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_") | ||||||
|  | 		indexOfDash := strings.Index(lowerLangCode, "-") | ||||||
|  | 		// e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md] | ||||||
|  | 		return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// e.g. [.en.md, .md] | ||||||
|  | 	return []string{lowerLangCode + ext, ext} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) { | ||||||
|  | 	target := readmeFile | ||||||
|  | 	if readmeFile != nil && readmeFile.IsLink() { | ||||||
|  | 		target, _ = readmeFile.FollowLinks() | ||||||
|  | 	} | ||||||
|  | 	if target == nil { | ||||||
|  | 		// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't) | ||||||
|  | 		// simply skip rendering the README | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Data["RawFileLink"] = "" | ||||||
|  | 	ctx.Data["ReadmeInList"] = true | ||||||
|  | 	ctx.Data["ReadmeExist"] = true | ||||||
|  | 	ctx.Data["FileIsSymlink"] = readmeFile.IsLink() | ||||||
|  |  | ||||||
|  | 	buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob()) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("getFileReader", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer dataRc.Close() | ||||||
|  |  | ||||||
|  | 	ctx.Data["FileIsText"] = fInfo.isTextFile | ||||||
|  | 	ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name()) | ||||||
|  | 	ctx.Data["FileSize"] = fInfo.fileSize | ||||||
|  | 	ctx.Data["IsLFSFile"] = fInfo.isLFSFile | ||||||
|  |  | ||||||
|  | 	if fInfo.isLFSFile { | ||||||
|  | 		filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name())) | ||||||
|  | 		ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !fInfo.isTextFile { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { | ||||||
|  | 		// Pretend that this is a normal text file to display 'This file is too large to be shown' | ||||||
|  | 		ctx.Data["IsFileTooLarge"] = true | ||||||
|  | 		ctx.Data["IsTextFile"] = true | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) | ||||||
|  |  | ||||||
|  | 	if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" { | ||||||
|  | 		ctx.Data["IsMarkup"] = true | ||||||
|  | 		ctx.Data["MarkupType"] = markupType | ||||||
|  |  | ||||||
|  | 		rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ | ||||||
|  | 			CurrentRefPath:  ctx.Repo.BranchNameSubURL(), | ||||||
|  | 			CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder), | ||||||
|  | 		}). | ||||||
|  | 			WithMarkupType(markupType). | ||||||
|  | 			WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). | ||||||
|  |  | ||||||
|  | 		ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) | ||||||
|  | 			delete(ctx.Data, "IsMarkup") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ctx.Data["IsMarkup"] != true { | ||||||
|  | 		ctx.Data["IsPlainText"] = true | ||||||
|  | 		content, err := io.ReadAll(rd) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("Read readme content failed: %v", err) | ||||||
|  | 		} | ||||||
|  | 		contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content)) | ||||||
|  | 		ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { | ||||||
|  | 		ctx.Data["CanEditReadmeFile"] = true | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -396,13 +396,6 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { | |||||||
| 	ctx.Repo.Repository = repo | 	ctx.Repo.Repository = repo | ||||||
| 	ctx.Data["RepoName"] = ctx.Repo.Repository.Name | 	ctx.Data["RepoName"] = ctx.Repo.Repository.Name | ||||||
| 	ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty | 	ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty | ||||||
|  |  | ||||||
| 	repoLicenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.ServerError("GetRepoLicenses", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	ctx.Data["DetectedRepoLicenses"] = repoLicenses.StringList() |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // RepoAssignment returns a middleware to handle repository assignment | // RepoAssignment returns a middleware to handle repository assignment | ||||||
| @@ -1036,7 +1029,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func | |||||||
| 		ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch | 		ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch | ||||||
| 		ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag | 		ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag | ||||||
| 		ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit | 		ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit | ||||||
| 		ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() | 		ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef | ||||||
|  |  | ||||||
| 		ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() | 		ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
| @@ -3,35 +3,7 @@ | |||||||
| 	{{template "repo/header" .}} | 	{{template "repo/header" .}} | ||||||
| 	<div class="ui container {{if .IsBlame}}fluid padded{{end}}"> | 	<div class="ui container {{if .IsBlame}}fluid padded{{end}}"> | ||||||
| 		{{template "base/alert" .}} | 		{{template "base/alert" .}} | ||||||
| 		{{template "repo/code/recently_pushed_new_branches" .}} |  | ||||||
| 		{{if and (not .HideRepoInfo) (not .IsBlame)}} |  | ||||||
| 		<div class="repo-description tw-break-anywhere"> |  | ||||||
| 			{{- $description := .Repository.DescriptionHTML ctx -}} |  | ||||||
| 			{{if $description}}{{$description | RenderCodeBlock}}{{end}} |  | ||||||
| 			{{if .Repository.Website}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}} |  | ||||||
| 		</div> |  | ||||||
| 		<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics"> |  | ||||||
| 			{{/* it should match the code in issue-home.js */}} |  | ||||||
| 			{{range .Topics}}<a class="repo-topic ui large label" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}} |  | ||||||
| 			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}} |  | ||||||
| 		</div> |  | ||||||
| 		{{end}} |  | ||||||
| 		{{if and .Permission.IsAdmin (not .Repository.IsArchived)}} |  | ||||||
| 		<div class="ui form tw-hidden tw-flex tw-gap-2 tw-my-2" id="topic_edit"> |  | ||||||
| 			<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1"> |  | ||||||
| 				<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}"> |  | ||||||
| 				{{range .Topics}} |  | ||||||
| 					{{/* keep the same layout as Fomantic UI generated labels */}} |  | ||||||
| 					<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a> |  | ||||||
| 				{{end}} |  | ||||||
| 				<div class="text"></div> |  | ||||||
| 			</div> |  | ||||||
| 			<div> |  | ||||||
| 				<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button> |  | ||||||
| 				<button class="ui primary button" id="save_topic" data-link="{{.RepoLink}}/topics">{{ctx.Locale.Tr "save"}}</button> |  | ||||||
| 			</div> |  | ||||||
| 		</div> |  | ||||||
| 		{{end}} |  | ||||||
| 		{{if .Repository.IsArchived}} | 		{{if .Repository.IsArchived}} | ||||||
| 			<div class="ui warning message tw-text-center"> | 			<div class="ui warning message tw-text-center"> | ||||||
| 				{{if .Repository.ArchivedUnix.IsZero}} | 				{{if .Repository.ArchivedUnix.IsZero}} | ||||||
| @@ -41,11 +13,16 @@ | |||||||
| 				{{end}} | 				{{end}} | ||||||
| 			</div> | 			</div> | ||||||
| 		{{end}} | 		{{end}} | ||||||
|  |  | ||||||
|  | 		{{template "repo/code/recently_pushed_new_branches" .}} | ||||||
|  |  | ||||||
|  | 		{{$treeNamesLen := len .TreeNames}} | ||||||
|  | 		{{$isTreePathRoot := eq $treeNamesLen 0}} | ||||||
|  | 		{{$showSidebar := $isTreePathRoot}} | ||||||
|  | 		<div class="{{Iif $showSidebar "repo-grid-filelist-sidebar" "repo-grid-filelist-only"}}"> | ||||||
|  | 			<div class="repo-home-filelist"> | ||||||
| 				{{template "repo/sub_menu" .}} | 				{{template "repo/sub_menu" .}} | ||||||
| 		{{$n := len .TreeNames}} | 				<div class="repo-button-row"> | ||||||
| 		{{$l := Eval $n "-" 1}} |  | ||||||
| 		{{$isHomepage := (eq $n 0)}} |  | ||||||
| 		<div class="repo-button-row" data-is-homepage="{{$isHomepage}}"> |  | ||||||
| 					<div class="repo-button-row-left"> | 					<div class="repo-button-row-left"> | ||||||
| 						{{$branchDropdownCurrentRefType := "branch"}} | 						{{$branchDropdownCurrentRefType := "branch"}} | ||||||
| 						{{$branchDropdownCurrentRefShortName := .BranchName}} | 						{{$branchDropdownCurrentRefShortName := .BranchName}} | ||||||
| @@ -76,8 +53,9 @@ | |||||||
| 								{{svg "octicon-git-pull-request"}} | 								{{svg "octicon-git-pull-request"}} | ||||||
| 							</a> | 							</a> | ||||||
| 						{{end}} | 						{{end}} | ||||||
| 				<!-- Show go to file and breadcrumbs if not on home page --> |  | ||||||
| 				{{if $isHomepage}} | 						<!-- Show go to file if on home page --> | ||||||
|  | 						{{if $isTreePathRoot}} | ||||||
| 							<a href="{{.Repository.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> | 							<a href="{{.Repository.Link}}/find/{{.BranchNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> | ||||||
| 						{{end}} | 						{{end}} | ||||||
|  |  | ||||||
| @@ -101,26 +79,19 @@ | |||||||
| 							</button> | 							</button> | ||||||
| 						{{end}} | 						{{end}} | ||||||
|  |  | ||||||
| 				{{if and $isHomepage (.Repository.IsTemplate)}} | 						{{if and $isTreePathRoot .Repository.IsTemplate}} | ||||||
| 							<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}"> | 							<a role="button" class="ui primary compact button" href="{{AppSubUrl}}/repo/create?template_id={{.Repository.ID}}"> | ||||||
| 								{{ctx.Locale.Tr "repo.use_template"}} | 								{{ctx.Locale.Tr "repo.use_template"}} | ||||||
| 							</a> | 							</a> | ||||||
| 						{{end}} | 						{{end}} | ||||||
| 				{{if $isHomepage}} |  | ||||||
| 					{{/* only show the "code search" on the repo home page, it only does global search, | 						{{if not $isTreePathRoot}} | ||||||
| 						so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}} | 							{{$treeNameIdxLast := Eval $treeNamesLen "-" 1}} | ||||||
| 					<form class="ignore-dirty tw-flex tw-flex-1" action="{{.RepoLink}}/search" method="get"> |  | ||||||
| 						<div class="ui small action input tw-flex-1"> |  | ||||||
| 							<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> |  | ||||||
| 							{{template "shared/search/button"}} |  | ||||||
| 						</div> |  | ||||||
| 					</form> |  | ||||||
| 				{{else}} |  | ||||||
| 							<span class="breadcrumb repo-path tw-ml-1"> | 							<span class="breadcrumb repo-path tw-ml-1"> | ||||||
| 								<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> | 								<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> | ||||||
| 								{{- range $i, $v := .TreeNames -}} | 								{{- range $i, $v := .TreeNames -}} | ||||||
| 									<span class="breadcrumb-divider">/</span> | 									<span class="breadcrumb-divider">/</span> | ||||||
| 							{{- if eq $i $l -}} | 									{{- if eq $i $treeNameIdxLast -}} | ||||||
| 										<span class="active section" title="{{$v}}">{{$v}}</span> | 										<span class="active section" title="{{$v}}">{{$v}}</span> | ||||||
| 										<button class="btn interact-fg tw-mx-1" data-clipboard-text="{{$.TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "copy_path"}}">{{svg "octicon-copy" 14}}</button> | 										<button class="btn interact-fg tw-mx-1" data-clipboard-text="{{$.TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "copy_path"}}">{{svg "octicon-copy" 14}}</button> | ||||||
| 									{{- else -}} | 									{{- else -}} | ||||||
| @@ -130,9 +101,10 @@ | |||||||
| 							</span> | 							</span> | ||||||
| 						{{end}} | 						{{end}} | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
| 					<div class="repo-button-row-right"> | 					<div class="repo-button-row-right"> | ||||||
| 						<!-- Only show clone panel in repository home page --> | 						<!-- Only show clone panel in repository home page --> | ||||||
| 				{{if $isHomepage}} | 						{{if $isTreePathRoot}} | ||||||
| 							<div class="clone-panel ui action tiny input"> | 							<div class="clone-panel ui action tiny input"> | ||||||
| 								{{template "repo/clone_buttons" .}} | 								{{template "repo/clone_buttons" .}} | ||||||
| 								<button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}"> | 								<button class="ui small jump dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}"> | ||||||
| @@ -143,9 +115,6 @@ | |||||||
| 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a> | 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_tar"}}</a> | ||||||
| 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a> | 											<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.RefName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.download_bundle"}}</a> | ||||||
| 										{{end}} | 										{{end}} | ||||||
| 								{{if .CitiationExist}} |  | ||||||
| 									<a class="item" id="cite-repo-button">{{svg "octicon-cross-reference" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.cite_this_repo"}}</a> |  | ||||||
| 								{{end}} |  | ||||||
| 										{{range .OpenWithEditorApps}} | 										{{range .OpenWithEditorApps}} | ||||||
| 											<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a> | 											<a class="item js-clone-url-editor" data-href-template="{{.OpenURL}}">{{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}}</a> | ||||||
| 										{{end}} | 										{{end}} | ||||||
| @@ -155,7 +124,7 @@ | |||||||
| 							</div> | 							</div> | ||||||
| 							{{template "repo/cite/cite_modal" .}} | 							{{template "repo/cite/cite_modal" .}} | ||||||
| 						{{end}} | 						{{end}} | ||||||
| 				{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} | 						{{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} | ||||||
| 							<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}"> | 							<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}"> | ||||||
| 								{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} | 								{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} | ||||||
| 							</a> | 							</a> | ||||||
| @@ -170,5 +139,12 @@ | |||||||
| 					{{template "repo/view_list" .}} | 					{{template "repo/view_list" .}} | ||||||
| 				{{end}} | 				{{end}} | ||||||
| 			</div> | 			</div> | ||||||
|  |  | ||||||
|  | 			{{if $showSidebar}} | ||||||
|  | 				<div class="repo-home-sidebar-top">{{template "repo/home_sidebar_top" .}}</div> | ||||||
|  | 				<div class="repo-home-sidebar-bottom">{{template "repo/home_sidebar_bottom" .}}</div> | ||||||
|  | 			{{end}} | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
| </div> | </div> | ||||||
| {{template "base/footer" .}} | {{template "base/footer" .}} | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								templates/repo/home_sidebar_bottom.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								templates/repo/home_sidebar_bottom.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | <div class="flex-list"> | ||||||
|  | 	{{if .LatestRelease}} | ||||||
|  | 	<div class="flex-item"> | ||||||
|  | 		<div class="flex-item-main"> | ||||||
|  | 			<div class="flex-item-title"> | ||||||
|  | 				<a class="item muted" href="{{.Link}}/releases"> | ||||||
|  | 					{{ctx.Locale.Tr "repo.releases"}} | ||||||
|  | 					<span class="ui small label">{{.NumReleases}}</span> | ||||||
|  | 				</a> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="flex-item"> | ||||||
|  | 				<div class="flex-item-icon"> | ||||||
|  | 					{{svg "octicon-tag" 16}} | ||||||
|  | 				</div> | ||||||
|  | 				<div class="flex-item-main"> | ||||||
|  | 					<div class="flex-item-header"> | ||||||
|  | 						<div class="flex-item-title tw-gap-2"> | ||||||
|  | 							<a class="gt-ellipsis muted" href="{{.LatestRelease.Link}}" title="{{.LatestRelease.Title}}">{{.LatestRelease.Title}}</a> | ||||||
|  | 							<span class="ui basic green label tw-h-100">{{ctx.Locale.Tr "latest"}}</span> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 					<div class="flex-item-body"> | ||||||
|  | 						<span class="time">{{DateUtils.TimeSince .LatestRelease.CreatedUnix}}</span> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	{{end}} | ||||||
|  |  | ||||||
|  | 	{{if and (not .IsEmptyRepo) .LanguageStats}} | ||||||
|  | 	<div class="flex-item"> | ||||||
|  | 		<div class="flex-item-main"> | ||||||
|  | 			<div class="flex-item-title"> | ||||||
|  | 				{{ctx.Locale.Tr "repo.repo_lang"}} | ||||||
|  | 			</div> | ||||||
|  |  | ||||||
|  | 			<div class="flex-item-body"> | ||||||
|  | 				<div class="language-stats"> | ||||||
|  | 					{{range .LanguageStats}} | ||||||
|  | 						<div class="bar" style="width: {{.Percentage}}%; background-color: {{.Color}}" data-tooltip-placement="top" data-tooltip-content={{.Language}} data-tooltip-follow-cursor="horizontal"></div> | ||||||
|  | 					{{end}} | ||||||
|  | 				</div> | ||||||
|  | 				<div class="language-stats-details"> | ||||||
|  | 					{{range .LanguageStats}} | ||||||
|  | 						<div class="item"> | ||||||
|  | 							<i class="color-icon" style="background-color: {{.Color}}"></i> | ||||||
|  | 							<span class="tw-font-semibold"> | ||||||
|  | 								{{Iif (eq .Language "other") (ctx.Locale.Tr "repo.language_other") .Language}} | ||||||
|  | 							</span> | ||||||
|  | 							{{.Percentage}}% | ||||||
|  | 						</div> | ||||||
|  | 					{{end}} | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	{{end}} | ||||||
|  | </div> | ||||||
							
								
								
									
										67
									
								
								templates/repo/home_sidebar_top.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								templates/repo/home_sidebar_top.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | <form class="ignore-dirty tw-flex tw-flex-1 tw-mt-1" action="{{.RepoLink}}/search" method="get"> | ||||||
|  | 	<div class="ui small action input tw-flex-1"> | ||||||
|  | 		<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> | ||||||
|  | 		{{template "shared/search/button"}} | ||||||
|  | 	</div> | ||||||
|  | </form> | ||||||
|  |  | ||||||
|  | <div class="flex-list"> | ||||||
|  | 	<div class="flex-item"> | ||||||
|  | 		<div class="flex-item-main"> | ||||||
|  | 			<div class="flex-item-title"> | ||||||
|  | 				{{ctx.Locale.Tr "repo.repo_desc"}} | ||||||
|  | 			</div> | ||||||
|  | 			{{if and (not .HideRepoInfo) (not .IsBlame)}} | ||||||
|  | 				<div class="flex-item-body repo-description tw-break-anywhere tw-gap-2 tw-mt-2"> | ||||||
|  | 					{{- $description := .Repository.DescriptionHTML ctx -}} | ||||||
|  | 					{{if $description}}{{$description | RenderCodeBlock}}{{else}}{{ctx.Locale.Tr "repo.repo_no_desc"}}{{end}} | ||||||
|  | 					{{if .Repository.Website}}{{svg "octicon-link"}}<a href="{{.Repository.Website}}">{{.Repository.Website}}</a>{{end}} | ||||||
|  | 				</div> | ||||||
|  | 				<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics"> | ||||||
|  | 					{{/* !!!! it SHOULD and MUST match the code in issue-home.js */}} | ||||||
|  | 					{{range .Topics}}<a class="repo-topic ui large label gt-ellipsis" title={{.Name}} href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}} | ||||||
|  | 				</div> | ||||||
|  | 				{{if and .Permission.IsAdmin (not .Repository.IsArchived)}} | ||||||
|  | 					<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button> | ||||||
|  | 				{{end}} | ||||||
|  | 			{{end}} | ||||||
|  | 			{{if and .Permission.IsAdmin (not .Repository.IsArchived)}} | ||||||
|  | 				<div class="ui form tw-hidden flex-item-body tw-gap-2 tw-my-2" id="topic_edit"> | ||||||
|  | 					<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1"> | ||||||
|  | 						<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}"> | ||||||
|  | 						{{range .Topics}} | ||||||
|  | 							{{/* keep the same layout as Fomantic UI generated labels */}} | ||||||
|  | 							<a class="ui label transition visible tw-cursor-default tw-inline-block repo-topic" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a> | ||||||
|  | 						{{end}} | ||||||
|  | 						<div class="text"></div> | ||||||
|  | 					</div> | ||||||
|  | 					<div> | ||||||
|  | 						<button class="ui primary button" id="save_topic" data-link="{{.RepoLink}}/topics">{{ctx.Locale.Tr "save"}}</button> | ||||||
|  | 						<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			{{end}} | ||||||
|  | 			{{if .ReadmeExist}} | ||||||
|  | 				<div class="flex-item-body tw-mt-2"> | ||||||
|  | 					<a class="tw-flex tw-items-center tw-gap-2 muted" href="{{.TreeLink}}/{{.FileName}}"> | ||||||
|  | 						{{svg "octicon-book"}}{{ctx.Locale.Tr "readme"}} | ||||||
|  | 					</a> | ||||||
|  | 				</div> | ||||||
|  | 			{{end}} | ||||||
|  | 			{{if .DetectedRepoLicenses}} | ||||||
|  | 				<div class="flex-item-body"> | ||||||
|  | 					<a class="tw-flex tw-items-center tw-gap-2 muted" href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/{{PathEscapeSegments .LicenseFileName}}" title="{{StringUtils.Join .DetectedRepoLicenses ", "}}"> | ||||||
|  | 						{{svg "octicon-law"}}{{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}} | ||||||
|  | 					</a> | ||||||
|  | 				</div> | ||||||
|  | 			{{end}} | ||||||
|  | 			{{if .CitiationExist}} | ||||||
|  | 				<div class="flex-item-body"> | ||||||
|  | 					<a class="tw-flex tw-items-center tw-gap-2 muted" id="cite-repo-button"> | ||||||
|  | 						{{svg "octicon-cross-reference"}}{{ctx.Locale.Tr "repo.cite_this_repo"}} | ||||||
|  | 					</a> | ||||||
|  | 				</div> | ||||||
|  | 			{{end}} | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
| @@ -13,11 +13,6 @@ | |||||||
| 					{{svg "octicon-tag"}} <b>{{ctx.Locale.PrettyNumber .NumTags}}</b> {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}} | 					{{svg "octicon-tag"}} <b>{{ctx.Locale.PrettyNumber .NumTags}}</b> {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}} | ||||||
| 				</a> | 				</a> | ||||||
| 			{{end}} | 			{{end}} | ||||||
| 			{{if .DetectedRepoLicenses}} |  | ||||||
| 				<a class="item muted" href="{{.RepoLink}}/src/{{.Repository.DefaultBranch}}/{{PathEscapeSegments .LicenseFileName}}" data-tooltip-placement="top" data-tooltip-content="{{StringUtils.Join .DetectedRepoLicenses ", "}}"> |  | ||||||
| 					{{svg "octicon-law"}} <b>{{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}}</b> |  | ||||||
| 				</a> |  | ||||||
| 			{{end}} |  | ||||||
| 			<span class="item not-mobile" {{if not (eq .Repository.Size 0)}}data-tooltip-placement="top" data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}> | 			<span class="item not-mobile" {{if not (eq .Repository.Size 0)}}data-tooltip-placement="top" data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}> | ||||||
| 				{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}} | 				{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}} | ||||||
| 				{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}} | 				{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}} | ||||||
| @@ -25,27 +20,5 @@ | |||||||
| 			</span> | 			</span> | ||||||
| 		{{end}} | 		{{end}} | ||||||
| 	</div> | 	</div> | ||||||
| 	{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo) .LanguageStats}} |  | ||||||
| 	<div class="ui segment sub-menu language-stats-details tw-hidden"> |  | ||||||
| 		{{range .LanguageStats}} |  | ||||||
| 		<div class="item"> |  | ||||||
| 			<i class="color-icon" style="background-color: {{.Color}}"></i> |  | ||||||
| 			<span class="tw-font-semibold"> |  | ||||||
| 				{{if eq .Language "other"}} |  | ||||||
| 					{{ctx.Locale.Tr "repo.language_other"}} |  | ||||||
| 				{{else}} |  | ||||||
| 					{{.Language}} |  | ||||||
| 				{{end}} |  | ||||||
| 			</span> |  | ||||||
| 			{{.Percentage}}% |  | ||||||
| 		</div> |  | ||||||
| 		{{end}} |  | ||||||
| 	</div> |  | ||||||
| 	<a class="ui segment language-stats show-panel toggle" data-panel=".repository-summary > .sub-menu"> |  | ||||||
| 		{{range .LanguageStats}} |  | ||||||
| 		<div class="bar" style="width: {{.Percentage}}%; background-color: {{.Color}}" data-tooltip-placement="top" data-tooltip-content={{.Language}} data-tooltip-follow-cursor="horizontal"></div> |  | ||||||
| 		{{end}} |  | ||||||
| 	</a> |  | ||||||
| 	{{end}} |  | ||||||
| </div> | </div> | ||||||
| {{end}} | {{end}} | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <table id="repo-files-table" class="ui single line table tw-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}> | <table id="repo-files-table" class="ui single line fixed table tw-mt-0" {{if .HasFilesWithoutLatestCommit}}hx-indicator="tr.notready td.message span" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}> | ||||||
| 	<thead> | 	<thead> | ||||||
| 		<tr class="commit-list"> | 		<tr class="commit-list"> | ||||||
| 			<th class="tw-overflow-hidden" colspan="2"> | 			<th class="tw-overflow-hidden" colspan="2"> | ||||||
|   | |||||||
| @@ -65,6 +65,7 @@ | |||||||
| @import "./repo/linebutton.css"; | @import "./repo/linebutton.css"; | ||||||
| @import "./repo/wiki.css"; | @import "./repo/wiki.css"; | ||||||
| @import "./repo/header.css"; | @import "./repo/header.css"; | ||||||
|  | @import "./repo/home.css"; | ||||||
| @import "./repo/reactions.css"; | @import "./repo/reactions.css"; | ||||||
|  |  | ||||||
| @import "./editor/fileeditor.css"; | @import "./editor/fileeditor.css"; | ||||||
|   | |||||||
| @@ -422,14 +422,6 @@ td .commit-summary { | |||||||
|   border-radius: 0 0 var(--border-radius) var(--border-radius); |   border-radius: 0 0 var(--border-radius) var(--border-radius); | ||||||
| } | } | ||||||
|  |  | ||||||
| .repository.file.list .sidebar { |  | ||||||
|   padding-left: 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .repository.file.list .sidebar .svg { |  | ||||||
|   width: 16px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .repo-editor-header { | .repo-editor-header { | ||||||
|   width: 100%; |   width: 100%; | ||||||
| } | } | ||||||
| @@ -1822,16 +1814,6 @@ td .commit-summary { | |||||||
|   background: var(--color-secondary); |   background: var(--color-secondary); | ||||||
| } | } | ||||||
|  |  | ||||||
| .repository .repository-summary .segment.language-stats { |  | ||||||
|   display: flex; |  | ||||||
|   gap: 2px; |  | ||||||
|   padding: 0; |  | ||||||
|   height: 10px; |  | ||||||
|   white-space: nowrap; |  | ||||||
|   border-radius: 0 0 3px 3px !important; |  | ||||||
|   overflow: hidden; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #cite-repo-modal #citation-panel { | #cite-repo-modal #citation-panel { | ||||||
|   display: flex; |   display: flex; | ||||||
|   width: 100%; |   width: 100%; | ||||||
| @@ -2172,11 +2154,7 @@ td .commit-summary { | |||||||
|   justify-content: flex-end; |   justify-content: flex-end; | ||||||
| } | } | ||||||
|  |  | ||||||
| .repo-button-row[data-is-homepage="false"] .repo-button-row-right { | @media (max-width: 1200px) { | ||||||
|   flex-grow: 0; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (max-width: 991px) { |  | ||||||
|   .repository:not(.wiki) .repo-button-row { |   .repository:not(.wiki) .repo-button-row { | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     align-items: stretch; |     align-items: stretch; | ||||||
| @@ -2302,6 +2280,7 @@ tbody.commit-list { | |||||||
|   font-weight: var(--font-weight-normal); |   font-weight: var(--font-weight-normal); | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|  |   display: inline-block !important; | ||||||
| } | } | ||||||
|  |  | ||||||
| #new-dependency-drop-list.ui.selection.dropdown { | #new-dependency-drop-list.ui.selection.dropdown { | ||||||
| @@ -2820,9 +2799,9 @@ tbody.commit-list { | |||||||
| /* FIXME: These media selectors are not ideal (just keep them from old code). | /* FIXME: These media selectors are not ideal (just keep them from old code). | ||||||
|     There are many different pages, some need the max-width while some others don't, |     There are many different pages, some need the max-width while some others don't, | ||||||
|     they should be tested and improved in the future. */ |     they should be tested and improved in the future. */ | ||||||
| @media (min-width: 768px) and (max-width: 991.98px) { | @media (min-width: 768px) and (max-width: 1235px) { | ||||||
|   .branch-selector-dropdown .branch-dropdown-button { |   .branch-selector-dropdown .branch-dropdown-button { | ||||||
|     max-width: 185px; |     max-width: 301px; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										77
									
								
								web_src/css/repo/home.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								web_src/css/repo/home.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | |||||||
|  | .repo-grid-filelist-sidebar { | ||||||
|  |   display: grid; | ||||||
|  |   grid-template-columns: auto 300px; | ||||||
|  |   grid-template-rows: auto auto 1fr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .repo-grid-filelist-sidebar .repo-home-filelist { | ||||||
|  |   min-width: 0; | ||||||
|  |   grid-column: 1; | ||||||
|  |   grid-row: 1 / 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .repo-grid-filelist-sidebar .repo-home-sidebar-top { | ||||||
|  |   grid-column: 2; | ||||||
|  |   grid-row: 1; | ||||||
|  |   padding-left: 1em; | ||||||
|  | } | ||||||
|  | .repo-grid-filelist-sidebar .repo-home-sidebar-bottom { | ||||||
|  |   grid-column: 2; | ||||||
|  |   grid-row: 2; | ||||||
|  |   padding-left: 1em; | ||||||
|  | } | ||||||
|  | .repo-home-sidebar-bottom > :first-child { | ||||||
|  |   border-top: 1px solid var(--color-secondary); /* same to .flex-list > .flex-item + .flex-item */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 767.98px) { | ||||||
|  |   .repo-grid-filelist-sidebar { | ||||||
|  |     grid-template-columns: 100%; | ||||||
|  |     grid-template-rows: auto auto auto; | ||||||
|  |   } | ||||||
|  |   .repo-grid-filelist-sidebar .repo-home-filelist { | ||||||
|  |     grid-column: 1; | ||||||
|  |     grid-row: 2; | ||||||
|  |   } | ||||||
|  |   .repo-grid-filelist-sidebar .repo-home-sidebar-top { | ||||||
|  |     grid-column: 1; | ||||||
|  |     grid-row: 1; | ||||||
|  |     padding-left: 0; | ||||||
|  |   } | ||||||
|  |   .repo-grid-filelist-sidebar .repo-home-sidebar-bottom { | ||||||
|  |     grid-column: 1; | ||||||
|  |     grid-row: 3; | ||||||
|  |     padding-left: 0; | ||||||
|  |   } | ||||||
|  |   .repo-home-sidebar-bottom > :first-child { | ||||||
|  |     border-top: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .language-stats { | ||||||
|  |   display: flex; | ||||||
|  |   gap: 2px; | ||||||
|  |   padding: 0; | ||||||
|  |   height: 10px; | ||||||
|  |   white-space: nowrap; | ||||||
|  |   border-radius: 5px; | ||||||
|  |   overflow: hidden; | ||||||
|  |   width: 100%; | ||||||
|  |   margin-top: 1rem; | ||||||
|  |   margin-bottom: 5px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .language-stats-details { | ||||||
|  |   display: flex; | ||||||
|  |   flex-wrap: wrap; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .language-stats-details .item { | ||||||
|  |   height: 30px; | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: center; | ||||||
|  |   gap: 0.25em; | ||||||
|  |   padding: 0 0.5em; /* make the UI look better for narrow (mobile) view */ | ||||||
|  |   text-decoration: none; | ||||||
|  | } | ||||||
| @@ -41,11 +41,7 @@ export async function initCitationFileCopyContent() { | |||||||
|     citationCopyApa.classList.toggle('primary', !isBibtex); |     citationCopyApa.classList.toggle('primary', !isBibtex); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   document.querySelector('#cite-repo-button')?.addEventListener('click', async (e: MouseEvent & {target: HTMLAnchorElement}) => { |   document.querySelector('#cite-repo-button')?.addEventListener('click', async () => { | ||||||
|     const dropdownBtn = e.target.closest('.ui.dropdown.button'); |  | ||||||
|     dropdownBtn.classList.add('is-loading'); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|     try { |     try { | ||||||
|       await initInputCitationValue(citationCopyApa, citationCopyBibtex); |       await initInputCitationValue(citationCopyApa, citationCopyBibtex); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @@ -67,9 +63,6 @@ export async function initCitationFileCopyContent() { | |||||||
|     inputContent.addEventListener('click', () => { |     inputContent.addEventListener('click', () => { | ||||||
|       inputContent.select(); |       inputContent.select(); | ||||||
|     }); |     }); | ||||||
|     } finally { |  | ||||||
|       dropdownBtn.classList.remove('is-loading'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fomanticQuery('#cite-repo-modal').modal('show'); |     fomanticQuery('#cite-repo-modal').modal('show'); | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts'; | |||||||
| const {appSubUrl} = window.config; | const {appSubUrl} = window.config; | ||||||
|  |  | ||||||
| export function initRepoTopicBar() { | export function initRepoTopicBar() { | ||||||
|   const mgrBtn = document.querySelector('#manage_topic'); |   const mgrBtn = document.querySelector<HTMLButtonElement>('#manage_topic'); | ||||||
|   if (!mgrBtn) return; |   if (!mgrBtn) return; | ||||||
|  |  | ||||||
|   const editDiv = document.querySelector('#topic_edit'); |   const editDiv = document.querySelector('#topic_edit'); | ||||||
| @@ -18,7 +18,7 @@ export function initRepoTopicBar() { | |||||||
|   mgrBtn.addEventListener('click', () => { |   mgrBtn.addEventListener('click', () => { | ||||||
|     hideElem(viewDiv); |     hideElem(viewDiv); | ||||||
|     showElem(editDiv); |     showElem(editDiv); | ||||||
|     topicDropdown.querySelector('input.search').focus(); |     topicDropdown.querySelector<HTMLInputElement>('input.search').focus(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   document.querySelector('#cancel_topic_edit').addEventListener('click', () => { |   document.querySelector('#cancel_topic_edit').addEventListener('click', () => { | ||||||
| @@ -28,9 +28,9 @@ export function initRepoTopicBar() { | |||||||
|     mgrBtn.focus(); |     mgrBtn.focus(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   document.querySelector('#save_topic').addEventListener('click', async (e) => { |   document.querySelector('#save_topic').addEventListener('click', async (e: MouseEvent & {target: HTMLButtonElement}) => { | ||||||
|     lastErrorToast?.hideToast(); |     lastErrorToast?.hideToast(); | ||||||
|     const topics = editDiv.querySelector('input[name=topics]').value; |     const topics = editDiv.querySelector<HTMLInputElement>('input[name=topics]').value; | ||||||
|  |  | ||||||
|     const data = new FormData(); |     const data = new FormData(); | ||||||
|     data.append('topics', topics); |     data.append('topics', topics); | ||||||
| @@ -45,12 +45,13 @@ export function initRepoTopicBar() { | |||||||
|           const topicArray = topics.split(','); |           const topicArray = topics.split(','); | ||||||
|           topicArray.sort(); |           topicArray.sort(); | ||||||
|           for (const topic of topicArray) { |           for (const topic of topicArray) { | ||||||
|             // it should match the code in repo/home.tmpl |             // TODO: sort items in topicDropdown, or items in edit div will have different order to the items in view div | ||||||
|  |             // !!!! it SHOULD and MUST match the code in "home_sidebar_top.tmpl" !!!! | ||||||
|             const link = document.createElement('a'); |             const link = document.createElement('a'); | ||||||
|             link.classList.add('repo-topic', 'ui', 'large', 'label'); |             link.classList.add('repo-topic', 'ui', 'large', 'label', 'gt-ellipsis'); | ||||||
|             link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`; |             link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`; | ||||||
|             link.textContent = topic; |             link.textContent = topic; | ||||||
|             mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button |             viewDiv.append(link); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         hideElem(editDiv); |         hideElem(editDiv); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user