From e5b247ea8e77689bb14e5f304ada36f9326f0904 Mon Sep 17 00:00:00 2001
From: Cherrg <michael@gnehr.de>
Date: Mon, 8 Jul 2019 10:20:22 +0200
Subject: [PATCH] wiki - page revisions list  (#7369)

fix #7

* add wiki page revision list

* mobile improvements

* css improvements for long usernames

* split renderWikiPage into 3 functions

Signed-off-by: Michael Gnehr <michael@gnehr.de>
---
 options/locale/locale_en-US.ini   |   3 +
 public/css/index.css              |   5 +
 public/less/_markdown.less        |  32 ++++
 routers/repo/wiki.go              | 266 ++++++++++++++++++++++--------
 routers/routes/routes.go          |   1 +
 templates/repo/wiki/revision.tmpl | 104 ++++++++++++
 templates/repo/wiki/view.tmpl     |   1 +
 7 files changed, 341 insertions(+), 71 deletions(-)
 create mode 100644 templates/repo/wiki/revision.tmpl

diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 0c83a7aef..6ef1277c6 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1034,6 +1034,9 @@ wiki.save_page = Save Page
 wiki.last_commit_info = %s edited this page %s
 wiki.edit_page_button = Edit
 wiki.new_page_button = New Page
+wiki.file_revision = Page Revision
+wiki.wiki_page_revisions = Wiki Page Revisions
+wiki.back_to_wiki = Back to wiki page
 wiki.delete_page_button = Delete Page
 wiki.delete_page_notice_1 = Deleting the wiki page '%s' cannot be undone. Continue?
 wiki.page_already_exists = A wiki page with the same name already exists.
diff --git a/public/css/index.css b/public/css/index.css
index 9039409f1..b948766b4 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -290,6 +290,11 @@ footer .ui.left,footer .ui.right{line-height:40px}
 .markdown:not(code) .csv-data tr{border-top:0}
 .markdown:not(code) .csv-data th{font-weight:700;background:#f8f8f8;border-top:0}
 .markdown:not(code) .ui.list .list,.markdown:not(code) ol.ui.list ol,.markdown:not(code) ul.ui.list ul{padding-left:2em}
+.repository.wiki.revisions .ui.container>.ui.stackable.grid{flex-direction:row-reverse}
+.repository.wiki.revisions .ui.container>.ui.stackable.grid>.header{margin-top:0}
+.repository.wiki.revisions .ui.container>.ui.stackable.grid>.header .sub.header{padding-left:52px}
+.file-revisions-btn{display:block;float:left;margin-bottom:2px!important;padding:11px!important;margin-right:10px!important}
+.file-revisions-btn i{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
 .home .logo{max-width:220px}
 @media only screen and (max-width:767px){.home .hero h1{font-size:3.5em}
 .home .hero h2{font-size:2em}
diff --git a/public/less/_markdown.less b/public/less/_markdown.less
index e971248f6..1dcc2caf9 100644
--- a/public/less/_markdown.less
+++ b/public/less/_markdown.less
@@ -494,3 +494,35 @@
         padding-left: 2em;
     }
 }
+
+.repository.wiki.revisions {
+    .ui.container > .ui.stackable.grid {
+        -ms-flex-direction: row-reverse;
+        flex-direction: row-reverse;
+
+        > .header {
+            margin-top: 0;
+
+            .sub.header {
+                padding-left: 52px;
+            }
+        }
+    }
+}
+
+.file-revisions-btn {
+    display: block;
+    float: left;
+    margin-bottom: 2px !important;
+    padding: 11px !important;
+    margin-right: 10px !important;
+
+    i {
+        -webkit-touch-callout: none;
+        -webkit-user-select: none;
+        -khtml-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+    }
+}
diff --git a/routers/repo/wiki.go b/routers/repo/wiki.go
index 43149c034..0fdf85363 100644
--- a/routers/repo/wiki.go
+++ b/routers/repo/wiki.go
@@ -23,10 +23,11 @@ import (
 )
 
 const (
-	tplWikiStart base.TplName = "repo/wiki/start"
-	tplWikiView  base.TplName = "repo/wiki/view"
-	tplWikiNew   base.TplName = "repo/wiki/new"
-	tplWikiPages base.TplName = "repo/wiki/pages"
+	tplWikiStart    base.TplName = "repo/wiki/start"
+	tplWikiView     base.TplName = "repo/wiki/view"
+	tplWikiRevision base.TplName = "repo/wiki/revision"
+	tplWikiNew      base.TplName = "repo/wiki/new"
+	tplWikiPages    base.TplName = "repo/wiki/pages"
 )
 
 // MustEnableWiki check if wiki is enabled, if external then redirect
@@ -107,18 +108,20 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
 
 // wikiContentsByName returns the contents of a wiki page, along with a boolean
 // indicating whether the page exists. Writes to ctx if an error occurs.
-func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, bool) {
-	entry, err := findEntryForFile(commit, models.WikiNameToFilename(wikiName))
-	if err != nil {
+func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) {
+	var entry *git.TreeEntry
+	var err error
+	pageFilename := models.WikiNameToFilename(wikiName)
+	if entry, err = findEntryForFile(commit, pageFilename); err != nil {
 		ctx.ServerError("findEntryForFile", err)
-		return nil, false
+		return nil, nil, "", false
 	} else if entry == nil {
-		return nil, false
+		return nil, nil, "", true
 	}
-	return wikiContentsByEntry(ctx, entry), true
+	return wikiContentsByEntry(ctx, entry), entry, pageFilename, false
 }
 
-func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *git.TreeEntry) {
+func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
 	wikiRepo, commit, err := findWikiRepoCommit(ctx)
 	if err != nil {
 		if !git.IsErrNotExist(err) {
@@ -128,88 +131,176 @@ func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *gi
 	}
 
 	// Get page list.
-	if isViewPage {
-		entries, err := commit.ListEntries()
-		if err != nil {
-			ctx.ServerError("ListEntries", err)
-			return nil, nil
-		}
-		pages := make([]PageMeta, 0, len(entries))
-		for _, entry := range entries {
-			if !entry.IsRegular() {
-				continue
-			}
-			wikiName, err := models.WikiFilenameToName(entry.Name())
-			if err != nil {
-				if models.IsErrWikiInvalidFileName(err) {
-					continue
-				}
-				ctx.ServerError("WikiFilenameToName", err)
-				return nil, nil
-			} else if wikiName == "_Sidebar" || wikiName == "_Footer" {
-				continue
-			}
-			pages = append(pages, PageMeta{
-				Name:   wikiName,
-				SubURL: models.WikiNameToSubURL(wikiName),
-			})
-		}
-		ctx.Data["Pages"] = pages
+	entries, err := commit.ListEntries()
+	if err != nil {
+		ctx.ServerError("ListEntries", err)
+		return nil, nil
 	}
+	pages := make([]PageMeta, 0, len(entries))
+	for _, entry := range entries {
+		if !entry.IsRegular() {
+			continue
+		}
+		wikiName, err := models.WikiFilenameToName(entry.Name())
+		if err != nil {
+			if models.IsErrWikiInvalidFileName(err) {
+				continue
+			}
+			ctx.ServerError("WikiFilenameToName", err)
+			return nil, nil
+		} else if wikiName == "_Sidebar" || wikiName == "_Footer" {
+			continue
+		}
+		pages = append(pages, PageMeta{
+			Name:   wikiName,
+			SubURL: models.WikiNameToSubURL(wikiName),
+		})
+	}
+	ctx.Data["Pages"] = pages
 
+	// get requested pagename
 	pageName := models.NormalizeWikiName(ctx.Params(":page"))
 	if len(pageName) == 0 {
 		pageName = "Home"
 	}
 	ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
-
 	ctx.Data["old_title"] = pageName
 	ctx.Data["Title"] = pageName
 	ctx.Data["title"] = pageName
 	ctx.Data["RequireHighlightJS"] = true
 
-	pageFilename := models.WikiNameToFilename(pageName)
-	var entry *git.TreeEntry
-	if entry, err = findEntryForFile(commit, pageFilename); err != nil {
-		ctx.ServerError("findEntryForFile", err)
-		return nil, nil
-	} else if entry == nil {
+	//lookup filename in wiki - get filecontent, gitTree entry , real filename
+	data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
+	if noEntry {
 		ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
+	}
+	if entry == nil || ctx.Written() {
 		return nil, nil
 	}
-	data := wikiContentsByEntry(ctx, entry)
+
+	sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
 	if ctx.Written() {
 		return nil, nil
 	}
 
-	if isViewPage {
-		sidebarContent, sidebarPresent := wikiContentsByName(ctx, commit, "_Sidebar")
-		if ctx.Written() {
-			return nil, nil
-		}
-
-		footerContent, footerPresent := wikiContentsByName(ctx, commit, "_Footer")
-		if ctx.Written() {
-			return nil, nil
-		}
-
-		metas := ctx.Repo.Repository.ComposeMetas()
-		ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas)
-		ctx.Data["sidebarPresent"] = sidebarPresent
-		ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas)
-		ctx.Data["footerPresent"] = footerPresent
-		ctx.Data["footerContent"] = markdown.RenderWiki(footerContent, ctx.Repo.RepoLink, metas)
-	} else {
-		ctx.Data["content"] = string(data)
-		ctx.Data["sidebarPresent"] = false
-		ctx.Data["sidebarContent"] = ""
-		ctx.Data["footerPresent"] = false
-		ctx.Data["footerContent"] = ""
+	footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
+	if ctx.Written() {
+		return nil, nil
 	}
 
+	metas := ctx.Repo.Repository.ComposeMetas()
+	ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas)
+	ctx.Data["sidebarPresent"] = sidebarContent != nil
+	ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas)
+	ctx.Data["footerPresent"] = footerContent != nil
+	ctx.Data["footerContent"] = markdown.RenderWiki(footerContent, ctx.Repo.RepoLink, metas)
+
+	// get commit count - wiki revisions
+	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
+	ctx.Data["CommitCount"] = commitsCount
+
 	return wikiRepo, entry
 }
 
+func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
+	wikiRepo, commit, err := findWikiRepoCommit(ctx)
+	if err != nil {
+		if !git.IsErrNotExist(err) {
+			ctx.ServerError("GetBranchCommit", err)
+		}
+		return nil, nil
+	}
+
+	// get requested pagename
+	pageName := models.NormalizeWikiName(ctx.Params(":page"))
+	if len(pageName) == 0 {
+		pageName = "Home"
+	}
+	ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
+	ctx.Data["old_title"] = pageName
+	ctx.Data["Title"] = pageName
+	ctx.Data["title"] = pageName
+	ctx.Data["RequireHighlightJS"] = true
+
+	//lookup filename in wiki - get filecontent, gitTree entry , real filename
+	data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
+	if noEntry {
+		ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
+	}
+	if entry == nil || ctx.Written() {
+		return nil, nil
+	}
+
+	ctx.Data["content"] = string(data)
+	ctx.Data["sidebarPresent"] = false
+	ctx.Data["sidebarContent"] = ""
+	ctx.Data["footerPresent"] = false
+	ctx.Data["footerContent"] = ""
+
+	// get commit count - wiki revisions
+	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
+	ctx.Data["CommitCount"] = commitsCount
+
+	// get page
+	page := ctx.QueryInt("page")
+	if page <= 1 {
+		page = 1
+	}
+
+	// get Commit Count
+	commitsHistory, err := wikiRepo.CommitsByFileAndRange("master", pageFilename, page)
+	if err != nil {
+		ctx.ServerError("CommitsByFileAndRange", err)
+		return nil, nil
+	}
+	commitsHistory = models.ValidateCommitsWithEmails(commitsHistory)
+	commitsHistory = models.ParseCommitsWithSignature(commitsHistory)
+
+	ctx.Data["Commits"] = commitsHistory
+
+	pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
+	pager.SetDefaultParams(ctx)
+	ctx.Data["Page"] = pager
+
+	return wikiRepo, entry
+}
+
+func renderEditPage(ctx *context.Context) {
+	_, commit, err := findWikiRepoCommit(ctx)
+	if err != nil {
+		if !git.IsErrNotExist(err) {
+			ctx.ServerError("GetBranchCommit", err)
+		}
+		return
+	}
+
+	// get requested pagename
+	pageName := models.NormalizeWikiName(ctx.Params(":page"))
+	if len(pageName) == 0 {
+		pageName = "Home"
+	}
+	ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
+	ctx.Data["old_title"] = pageName
+	ctx.Data["Title"] = pageName
+	ctx.Data["title"] = pageName
+	ctx.Data["RequireHighlightJS"] = true
+
+	//lookup filename in wiki - get filecontent, gitTree entry , real filename
+	data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName)
+	if noEntry {
+		ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
+	}
+	if entry == nil || ctx.Written() {
+		return
+	}
+
+	ctx.Data["content"] = string(data)
+	ctx.Data["sidebarPresent"] = false
+	ctx.Data["sidebarContent"] = ""
+	ctx.Data["footerPresent"] = false
+	ctx.Data["footerContent"] = ""
+}
+
 // Wiki renders single wiki page
 func Wiki(ctx *context.Context) {
 	ctx.Data["PageIsWiki"] = true
@@ -221,7 +312,7 @@ func Wiki(ctx *context.Context) {
 		return
 	}
 
-	wikiRepo, entry := renderWikiPage(ctx, true)
+	wikiRepo, entry := renderViewPage(ctx)
 	if ctx.Written() {
 		return
 	}
@@ -247,6 +338,39 @@ func Wiki(ctx *context.Context) {
 	ctx.HTML(200, tplWikiView)
 }
 
+// WikiRevision renders file revision list of wiki page
+func WikiRevision(ctx *context.Context) {
+	ctx.Data["PageIsWiki"] = true
+	ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
+
+	if !ctx.Repo.Repository.HasWiki() {
+		ctx.Data["Title"] = ctx.Tr("repo.wiki")
+		ctx.HTML(200, tplWikiStart)
+		return
+	}
+
+	wikiRepo, entry := renderRevisionPage(ctx)
+	if ctx.Written() {
+		return
+	}
+	if entry == nil {
+		ctx.Data["Title"] = ctx.Tr("repo.wiki")
+		ctx.HTML(200, tplWikiStart)
+		return
+	}
+
+	// Get last change information.
+	wikiPath := entry.Name()
+	lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+	if err != nil {
+		ctx.ServerError("GetCommitByPath", err)
+		return
+	}
+	ctx.Data["Author"] = lastCommit.Author
+
+	ctx.HTML(200, tplWikiRevision)
+}
+
 // WikiPages render wiki pages list page
 func WikiPages(ctx *context.Context) {
 	if !ctx.Repo.Repository.HasWiki() {
@@ -399,7 +523,7 @@ func EditWiki(ctx *context.Context) {
 		return
 	}
 
-	renderWikiPage(ctx, false)
+	renderEditPage(ctx)
 	if ctx.Written() {
 		return
 	}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 744088a9d..ec57e8f5f 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -804,6 +804,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 		m.Group("/wiki", func() {
 			m.Get("/?:page", repo.Wiki)
 			m.Get("/_pages", repo.WikiPages)
+			m.Get("/:page/_revision", repo.WikiRevision)
 
 			m.Group("", func() {
 				m.Combo("/_new").Get(repo.NewWiki).
diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl
new file mode 100644
index 000000000..a64c386ed
--- /dev/null
+++ b/templates/repo/wiki/revision.tmpl
@@ -0,0 +1,104 @@
+{{template "base/head" .}}
+<div class="repository wiki revisions">
+	{{template "repo/header" .}}
+	{{ $title := .title}}
+	<div class="ui container">
+		<div class="ui stackable grid">
+			<div class="ui eight wide column text right">
+				<div class="ui action small input" id="clone-panel">
+					{{if not $.DisableHTTP}}
+						<button class="ui basic clone button" id="repo-clone-https" data-link="{{.WikiCloneLink.HTTPS}}">
+							{{if UseHTTPS}}HTTPS{{else}}HTTP{{end}}
+						</button>
+					{{end}}
+					{{if and (not $.DisableSSH) (or $.IsSigned $.ExposeAnonSSH)}}
+						<button class="ui basic clone button" id="repo-clone-ssh" data-link="{{.WikiCloneLink.SSH}}">
+							SSH
+						</button>
+					{{end}}
+					{{if not $.DisableHTTP}}
+						<input id="repo-clone-url" value="{{$.WikiCloneLink.HTTPS}}" readonly>
+					{{else if and (not $.DisableSSH) (or $.IsSigned $.ExposeAnonSSH)}}
+						<input id="repo-clone-url" value="{{$.WikiCloneLink.SSH}}" readonly>
+					{{end}}
+					{{if or ((not $.DisableHTTP) (and (not $.DisableSSH) (or $.IsSigned $.ExposeAnonSSH)))}}
+						<button class="ui basic icon button poping up clipboard" id="clipboard-btn" data-original="{{.i18n.Tr "repo.copy_link"}}" data-success="{{.i18n.Tr "repo.copy_link_success"}}" data-error="{{.i18n.Tr "repo.copy_link_error"}}" data-content="{{.i18n.Tr "repo.copy_link"}}" data-variation="inverted tiny" data-clipboard-target="#repo-clone-url">
+							<i class="octicon octicon-clippy"></i>
+						</button>
+					{{end}}
+				</div>
+			</div>
+			<div class="ui header eight wide column">
+				<a class="file-revisions-btn ui basic button" title="{{.i18n.Tr "repo.wiki.back_to_wiki"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}" ><span>{{.revision}}</span> <i class="fa fa-fw fa-file-text-o"></i></a>
+				{{$title}}
+				<div class="ui sub header">
+					{{$timeSince := TimeSince .Author.When $.Lang}}
+					{{.i18n.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince | Safe}}
+				</div>
+			</div>
+		</div>
+		<h2 class="ui top header">{{.i18n.Tr "repo.wiki.wiki_page_revisions"}}</h2>
+		<div class="ui" style="margin-top: 1rem;">
+			<h4 class="ui top attached header">
+				<div class="ui stackable grid">
+					<div class="sixteen wide column">
+						{{.CommitCount}} {{.i18n.Tr "repo.commits.commits"}}
+					</div>
+				</div>
+			</h4>
+
+			{{if and .Commits (gt .CommitCount 0)}}
+				<div class="ui attached table segment">
+					<table class="ui very basic striped fixed table single line" id="commits-table">
+						<thead>
+							<tr>
+								<th class="eight wide">{{.i18n.Tr "repo.commits.author"}}</th>
+								<th class="four wide sha">SHA1</th>
+								<th class="four wide">{{.i18n.Tr "repo.commits.date"}}</th>
+							</tr>
+						</thead>
+						<tbody class="commit-list">
+							{{ $r:= List .Commits}}
+							{{range $r}}
+								<tr>
+									<td class="author">
+										{{if .User}}
+										  {{if .User.FullName}}
+											<img class="ui avatar image" src="{{.User.RelAvatarLink}}" alt=""/>&nbsp;&nbsp;<a href="{{AppSubUrl}}/{{.User.Name}}">{{.User.FullName}}</a>
+										  {{else}}
+											<img class="ui avatar image" src="{{.User.RelAvatarLink}}" alt=""/>&nbsp;&nbsp;<a href="{{AppSubUrl}}/{{.User.Name}}">{{.Author.Name}}</a>
+										  {{end}}
+										{{else}}
+											<img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;{{.Author.Name}}
+										{{end}}
+									</td>
+									<td class="sha">
+										<label rel="nofollow" class="ui sha label {{if .Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
+											{{ShortSha .ID.String}}
+											{{if .Signature}}
+												<div class="ui detail icon button">
+													{{if .Verification.Verified}}
+														<i title="{{.Verification.Reason}}" class="lock green icon"></i>
+													{{else}}
+														<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
+													{{end}}
+												</div>
+											{{end}}
+										</label>
+									</td>
+									<td class="grey text">{{TimeSince .Author.When $.Lang}}</td>
+								</tr>
+							{{end}}
+						</tbody>
+					</table>
+				</div>
+			{{end}}
+
+			{{template "base/paginate" .}}
+
+		</div>
+	</div>
+</div>
+
+
+{{template "base/footer" .}}
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index dd2de2a04..f775ac942 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -56,6 +56,7 @@
 		<div class="ui dividing header">
 			<div class="ui stackable grid">
 				<div class="eight wide column">
+					<a class="file-revisions-btn ui basic button" title="{{.i18n.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}/_revision" ><span>{{.CommitCount}}</span> <i class="fa fa-fw fa-history"></i></a>
 					{{$title}}
 					<div class="ui sub header">
 						{{$timeSince := TimeSince .Author.When $.Lang}}