Browse Source

Add a Language Statistics Bar to repo/view

With the usage of a port of github's linguist functionality to Go,
which I have made as a separate library and is hosted here:

https://github.com/generaltso/linguist

And a quick design I made, I have hacked a language statistics bar
into gogs

I wasn't sure where to put everything so it's sitting directly
on the view router and the CSS is inlined into a new template file

Based on the structure of this project I would fully expect this
feature to belong in its own sub-package

Also, even though determining language stats on-the-fly is pretty
fast, caching the results in the database for large codebases
would probably be a much better strategy, especially if the top
language were to be displayed in the "Explore" view like GitHub has

I also had difficulty trying to figure out how to do:

if len(something) == 1 ? '' : 's'

with go templates for plurals (1 Commit vs 2 Commits), and I kinda
gave up there...
pull/2134/head
tso 9 years ago
parent
commit
fa32faf1a4
  1. 173
      routers/repo/view.go
  2. 3
      templates/repo/home.tmpl
  3. 168
      templates/repo/language_statistics.tmpl

173
routers/repo/view.go

@ -6,11 +6,15 @@ package repo
import (
"bytes"
"fmt"
"io/ioutil"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/Unknwon/com"
"github.com/Unknwon/paginater"
"github.com/gogits/gogs/models"
@ -19,6 +23,8 @@ import (
"github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/middleware"
"github.com/gogits/gogs/modules/template"
"github.com/generaltso/linguist"
)
const (
@ -218,6 +224,14 @@ func Home(ctx *middleware.Context) {
}
ctx.Data["LastCommit"] = lastCommit
ctx.Data["LastCommitUser"] = models.ValidateCommitWithEmail(lastCommit)
branchId, err := ctx.Repo.GitRepo.GetCommitIdOfBranch(branchName)
if err != nil || branchId != lastCommit.ID.String() {
branchId = lastCommit.ID.String()
}
Langs := getLanguageStats(ctx, branchId)
ctx.Data["LanguageStats"] = Langs
}
ctx.Data["Username"] = userName
@ -295,3 +309,162 @@ func Forks(ctx *middleware.Context) {
ctx.HTML(200, FORKS)
}
func getLanguageStats(ctx *middleware.Context, branchId string) interface{} {
all_files := linguistlstree(ctx, branchId)
languages := map[string]float64{}
var total_size float64
for _, f := range all_files {
languages[f.Language] += f.Size
total_size += f.Size
}
percent := []float64{}
results := map[float64]string{}
for lang, size := range languages {
p := size / total_size * 100.0
percent = append(percent, p)
results[p] = lang
}
sort.Sort(sort.Reverse(sort.Float64Slice(percent)))
ret := []*LanguageStat{}
for i, p := range percent {
// limit result set
if i > 10 {
break
}
lang := results[p]
color := linguist.GetColor(lang)
if color == "" {
color = "#ccc" //grey
}
ret = append(ret, &LanguageStat{Name: lang,
Percent: fmt.Sprintf("%.2f%%", p),
Color: color})
}
return ret
}
type LanguageStat struct {
Name string
Percent string
Color string
}
// see below
type file struct {
Name string
Size float64
Language string
}
// just some utilities...
func gitcmd(ctx *middleware.Context, args ...string) string {
stdout, _, err := com.ExecCmdDir(ctx.Repo.GitRepo.Path, "git", args...)
tsoErr(ctx, err)
return stdout
}
func gitcmdbytes(ctx *middleware.Context, args ...string) []byte {
stdout, _, err := com.ExecCmdDirBytes(ctx.Repo.GitRepo.Path, "git", args...)
tsoErr(ctx, err)
return stdout
}
func tsoErr(ctx *middleware.Context, err error) {
if err != nil {
ctx.Handle(500, "*blames tso*", err)
}
}
// returns every file in a tree
// additionally detecting programming language
func linguistlstree(ctx *middleware.Context, treeish string) (files []*file) {
files = []*file{}
lstext := gitcmd(ctx, "ls-tree", treeish)
for _, ln := range strings.Split(lstext, "\n") {
fields := strings.Split(ln, " ")
if len(fields) != 3 {
continue
}
//fmode := fields[0]
ftype := fields[1]
fields = strings.Split(fields[2], "\t")
if len(fields) != 2 {
continue
}
fhash := fields[0]
fname := fields[1]
switch ftype {
// broken, don't know why
// case "tree":
// subdir := linguistlstree(ctx, fhash)
// files = append(files, subdir...)
case "blob":
// if it's vendored, don't even look at it
// (vendored means files like README.md, .gitignore, etc...)
if linguist.IsVendored(fname) {
continue
}
ssize := gitcmd(ctx, "cat-file", "-s", fhash)
fsize, err := strconv.ParseFloat(strings.TrimSpace(ssize), 64)
tsoErr(ctx, err)
// if it's an empty file don't even waste time
if fsize == 0 {
continue
}
f := &file{}
f.Name = fname
f.Size = fsize
//
// language detection
//
// by file extension
by_ext := linguist.DetectFromFilename(fname)
if by_ext != "" {
f.Language = by_ext
files = append(files, f)
continue
}
// by mimetype
// if we can't guess type by extension, then before jumping into
// lexing and parsing things like image files or cat videos
// ...or other binary formats which will give erroneous results...
// ...or other binary formats which will give erroneous results...
// with the linguist.DetectFromContents method, I posit looking
// at mimetype with linguist.DetectMimeFromFilename
//
// ...however, this is not what github does at all, instead ignoring
// binary files altogether. However, there is no law that states
// git must be used for code only.
by_mime, shouldIgnore, _ := linguist.DetectMimeFromFilename(fname)
if by_mime != "" && shouldIgnore {
f.Language = by_mime
files = append(files, f)
continue
}
// by contents
// see also: github.com/github/linguist
// see also: github.com/generaltso/linguist
contents := gitcmdbytes(ctx, "cat-file", "blob", fhash)
by_contents := linguist.DetectFromContents(contents)
if by_contents != "" {
f.Language = by_contents
} else {
f.Language = "(undetermined)"
}
files = append(files, f)
}
}
return files
}

3
templates/repo/home.tmpl

@ -6,6 +6,9 @@
{{if .Repository.DescriptionHtml}}<span class="description">{{.Repository.DescriptionHtml}}</span>{{else}}<span class="no-description text-italic">{{.i18n.Tr "repo.no_desc"}}</span>{{end}}
<a class="link" href="{{.Repository.Website}}">{{.Repository.Website}}</a>
</p>
{{if .LanguageStats}}
{{template "repo/language_statistics" .}}
{{end}}
<div class="ui secondary menu">
{{if .CanPullRequest}}
<div class="fitted item">

168
templates/repo/language_statistics.tmpl

@ -0,0 +1,168 @@
<div class="statistics">
<div class="statistics-box">
<div class="statistics-box__toppanel statistics-box__repo">
<ul class="statistics-nav">
<li class="statistics-nav__item">
<i class="octicon octicon-history"></i>
<a href="{{.RepoLink}}/commits/{{EscapePound .BranchName}}">{{.CommitsCount}} Commit(s)</a>
</li>
<li class="statistics-nav__item">
<i class="octicon octicon-git-branch"></i>
{{len .Branches}} Branch(es)
</li>
<li class="statistics-nav__item">
<i class="octicon octicon-tag"></i>
<a href="{{.RepoLink}}/releases">{{.Repository.NumTags}} Release(s)</a>
</li>
<li class="statistics-nav__item">
<i class="octicon octicon-organization"></i>
1337 Contributors
</li>
</ul>
<div class="statistics-box__bottompanel statistics-box__lang">
<ul class="statistics-nav">
{{range $Lang := .LanguageStats}}
<li class="statistics-nav__item"
style="color:{{$Lang.Color}}"
data-percent="{{$Lang.Percent}}">
<a href="#">{{$Lang.Name}}</a>
</li>
{{end}}
</ul>
</div>
</div>
</div>
<!--
would prefer not to use inline styles for this
in favor of data-* attributes + attr() in css
however browsers do not support this yet
and JavaScript cannot target ::before, ::after
-->
<div class="statistics-underbar"
title="Language Statistics"
onclick="document.querySelector('.statistics-box').classList.toggle('statistics-box__focusstate')">
{{range $Lang := .LanguageStats}}
<span style="color:{{$Lang.Color}};flex-basis:{{$Lang.Percent}}">{{$Lang.Name}}</span>
{{end}}
</div>
</div>
<style>
.statistics {
position: relative;
height: 2.6667em;
border: 1px solid #dedede;
border-radius: 5px;
background: #fff;
}
/**
* 3D box effect technique
*
* http://codepen.io/rachsmith/pen/cojza
*/
.statistics-box {
perspective: 1000px;
overflow: hidden;
border-radius: 5px 5px 0 0;
padding: 0 1.5em;
}
.statistics-box__toppanel, .statistics-box__bottompanel {
transform-origin: 50% 0;
transition-delay: 125ms;
/**
* this timing function from
* http://codepen.io/sbchewitt/pen/KpPZMx
*/
transition: all 0.5s cubic-bezier(.57,-0.42,.46,1.4);
}
.statistics-box__toppanel {
position: relative;
transform-style: preserve-3d;
background: #fff;
}
.statistics-box__bottompanel {
transform: rotateX(-90deg) translateZ(0);
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 100%;
background: #999;
}
.statistics-box__focusstate .statistics-box__toppanel {
transform: rotateX(90deg) translateY(-22px);
transition-delay: 0s;
}
.statistics-box__focusstate .statistics-box__bottompanel {
background: #fff;
transition-delay: 0s;
}
/*ul*/.statistics-nav {
list-style: none;
margin: 0;
padding: 0;
display: flex;
justify-content: space-around;
padding: 0.5em;
}
/*li*/.statistics-nav__item {
flex: 0 1 auto;
}
/*li*/.statistics-nav__item a {
color: #222;
text-decoration: none;
}
.statistics-box__repo /*li*/.statistics-nav__item:hover a {
color: #4183c4;
}
.statistics-box__lang /*li*/.statistics-nav__item::before {
background-color: currentColor;
content: '';
display: inline-block;
width: 0.75em;
height: 0.75em;
border-radius: 50%;
vertical-align: middle;
margin: 0 0.25em 2px 0;
cursor: pointer;
}
/*li*/.statistics-nav__item::after {
content: attr(data-percent);
margin-left: 0.5em;
color: #555;
}
.statistics-underbar {
position: absolute;
left: 0;
bottom: 0;
padding-top: 5px;
height: 8px;
width: 100%;
bottom: 0;
display: flex;
cursor: pointer;
border-radius: 0 0 5px 5px;
overflow: hidden;
opacity: 0.6;
transition: all 0.5s cubic-bezier(.57,-0.42,.46,1.4);
}
.statistics-box__focusstate ~ .statistics-underbar,
.statistics-underbar:hover {
opacity: 1;
padding: 0;
padding-top: 3px;
}
.statistics-underbar:hover {
opacity: 0.8;
}
.statistics-underbar span {
font-size: 0;
background-color: currentColor;
}
</style>
Loading…
Cancel
Save