* Add model and tests for graph * Add route and router for graph * Add assets for graph * Add template for graphmaster
@@ -547,6 +547,7 @@ func runWeb(ctx *cli.Context) error { | |||||
m.Get("/src/*", repo.SetEditorconfigIfExists, repo.Home) | m.Get("/src/*", repo.SetEditorconfigIfExists, repo.Home) | ||||
m.Get("/raw/*", repo.SingleDownload) | m.Get("/raw/*", repo.SingleDownload) | ||||
m.Get("/commits/*", repo.RefCommits) | m.Get("/commits/*", repo.RefCommits) | ||||
m.Get("/graph", repo.Graph) | |||||
m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.Diff) | m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.Diff) | ||||
m.Get("/forks", repo.Forks) | m.Get("/forks", repo.Forks) | ||||
}, context.RepoRef()) | }, context.RepoRef()) | ||||
@@ -0,0 +1,108 @@ | |||||
// Copyright 2016 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package models | |||||
import ( | |||||
"fmt" | |||||
"strings" | |||||
"code.gitea.io/git" | |||||
) | |||||
// GraphItem represent one commit, or one relation in timeline | |||||
type GraphItem struct { | |||||
GraphAcii string | |||||
Relation string | |||||
Branch string | |||||
Rev string | |||||
Date string | |||||
Author string | |||||
AuthorEmail string | |||||
ShortRev string | |||||
Subject string | |||||
OnlyRelation bool | |||||
} | |||||
// GraphItems is a list of commits from all branches | |||||
type GraphItems []GraphItem | |||||
// GetCommitGraph return a list of commit (GraphItems) from all branches | |||||
func GetCommitGraph(r *git.Repository) (GraphItems, error) { | |||||
var Commitgraph []GraphItem | |||||
format := "DATA:|%d|%H|%ad|%an|%ae|%h|%s" | |||||
graphCmd := git.NewCommand("log") | |||||
graphCmd.AddArguments("--graph", | |||||
"--date-order", | |||||
"--all", | |||||
"-C", | |||||
"-M", | |||||
"-n 100", | |||||
"--date=iso", | |||||
fmt.Sprintf("--pretty=format:%s", format), | |||||
) | |||||
graph, err := graphCmd.RunInDir(r.Path) | |||||
if err != nil { | |||||
return Commitgraph, err | |||||
} | |||||
Commitgraph = make([]GraphItem, 0, 100) | |||||
for _, s := range strings.Split(graph, "\n") { | |||||
GraphItem, err := graphItemFromString(s, r) | |||||
if err != nil { | |||||
return Commitgraph, err | |||||
} | |||||
Commitgraph = append(Commitgraph, GraphItem) | |||||
} | |||||
return Commitgraph, nil | |||||
} | |||||
func graphItemFromString(s string, r *git.Repository) (GraphItem, error) { | |||||
var ascii string | |||||
var data = "|||||||" | |||||
lines := strings.Split(s, "DATA:") | |||||
switch len(lines) { | |||||
case 1: | |||||
ascii = lines[0] | |||||
case 2: | |||||
ascii = lines[0] | |||||
data = lines[1] | |||||
default: | |||||
return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s. Expect 1 or two fields", s) | |||||
} | |||||
rows := strings.Split(data, "|") | |||||
if len(rows) != 8 { | |||||
return GraphItem{}, fmt.Errorf("Failed parsing grap line:%s - Should containt 8 datafields", s) | |||||
} | |||||
/* // see format in getCommitGraph() | |||||
0 Relation string | |||||
1 Branch string | |||||
2 Rev string | |||||
3 Date string | |||||
4 Author string | |||||
5 AuthorEmail string | |||||
6 ShortRev string | |||||
7 Subject string | |||||
*/ | |||||
gi := GraphItem{ascii, | |||||
rows[0], | |||||
rows[1], | |||||
rows[2], | |||||
rows[3], | |||||
rows[4], | |||||
rows[5], | |||||
rows[6], | |||||
rows[7], | |||||
len(rows[2]) == 0, // no commits refered to, only relation in current line. | |||||
} | |||||
return gi, nil | |||||
} |
@@ -0,0 +1,41 @@ | |||||
// Copyright 2016 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package models | |||||
import ( | |||||
"testing" | |||||
"code.gitea.io/git" | |||||
) | |||||
func BenchmarkGetCommitGraph(b *testing.B) { | |||||
currentRepo, err := git.OpenRepository(".") | |||||
if err != nil { | |||||
b.Error("Could not open repository") | |||||
} | |||||
graph, err := GetCommitGraph(currentRepo) | |||||
if err != nil { | |||||
b.Error("Could get commit graph") | |||||
} | |||||
if len(graph) < 100 { | |||||
b.Error("Should get 100 log lines.") | |||||
} | |||||
} | |||||
func BenchmarkParseCommitString(b *testing.B) { | |||||
testString := "* DATA:||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Kjell Kvinge|kjell@kvinge.biz|4e61bac|Add route for graph" | |||||
graphItem, err := graphItemFromString(testString, nil) | |||||
if err != nil { | |||||
b.Error("could not parse teststring") | |||||
} | |||||
if graphItem.Author != "Kjell Kvinge" { | |||||
b.Error("Did not get expected data") | |||||
} | |||||
} |
@@ -0,0 +1,15 @@ | |||||
body {font:13.34px/1.4 helvetica,arial,freesans,clean,sans-serif;} | |||||
em {font-style:normal;} | |||||
#git-graph-container, #rel-container {float:left;} | |||||
#git-graph-container {} | |||||
#git-graph-container li {list-style-type:none;height:20px;line-height:20px;overflow:hidden;} | |||||
#git-graph-container li .node-relation {font-family:'Bitstream Vera Sans Mono', 'Courier', monospace;} | |||||
#git-graph-container li .author {color:#666666;} | |||||
#git-graph-container li .time {color:#999999;font-size:80%} | |||||
#git-graph-container li a {color:#000000;} | |||||
#git-graph-container li a:hover {text-decoration:underline;} | |||||
#git-graph-container li a em {color:#BB0000;border-bottom:1px dotted #BBBBBB;text-decoration:none;font-style:normal;} | |||||
#rev-container {width:80%} | |||||
#rev-list {margin:0;padding:0 5px 0 0;width:80%} | |||||
#graph-raw-list {margin:0px;} |
@@ -0,0 +1,17 @@ | |||||
$(document).ready(function () { | |||||
var graphList = []; | |||||
if (!document.getElementById('graph-canvas')) { | |||||
return; | |||||
} | |||||
$("#graph-raw-list li span.node-relation").each(function () { | |||||
graphList.push($(this).text()); | |||||
}) | |||||
gitGraph(document.getElementById('graph-canvas'), graphList); | |||||
if ($("#rev-container")) { | |||||
$("#rev-container").css("width", document.body.clientWidth - document.getElementById('graph-canvas').width); | |||||
} | |||||
}) |
@@ -0,0 +1,399 @@ | |||||
/* | |||||
* Copyright (c) 2011, Terrence Lee <kill889@gmail.com> | |||||
* All rights reserved. | |||||
* | |||||
* Redistribution and use in source and binary forms, with or without | |||||
* modification, are permitted provided that the following conditions are met: | |||||
* * Redistributions of source code must retain the above copyright | |||||
* notice, this list of conditions and the following disclaimer. | |||||
* * Redistributions in binary form must reproduce the above copyright | |||||
* notice, this list of conditions and the following disclaimer in the | |||||
* documentation and/or other materials provided with the distribution. | |||||
* * Neither the name of the <organization> nor the | |||||
* names of its contributors may be used to endorse or promote products | |||||
* derived from this software without specific prior written permission. | |||||
* | |||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |||||
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY | |||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||||
*/ | |||||
var gitGraph = function (canvas, rawGraphList, config) { | |||||
if (!canvas.getContext) { | |||||
return; | |||||
} | |||||
if (typeof config === "undefined") { | |||||
config = { | |||||
unitSize: 20, | |||||
lineWidth: 3, | |||||
nodeRadius: 4 | |||||
}; | |||||
} | |||||
var flows = []; | |||||
var graphList = []; | |||||
var ctx = canvas.getContext("2d"); | |||||
var init = function () { | |||||
var maxWidth = 0; | |||||
var i; | |||||
var l = rawGraphList.length; | |||||
var row; | |||||
var midStr; | |||||
for (i = 0; i < l; i++) { | |||||
midStr = rawGraphList[i].replace(/\s+/g, " ").replace(/^\s+|\s+$/g, ""); | |||||
maxWidth = Math.max(midStr.replace(/(\_|\s)/g, "").length, maxWidth); | |||||
row = midStr.split(""); | |||||
graphList.unshift(row); | |||||
} | |||||
canvas.width = maxWidth * config.unitSize; | |||||
canvas.height = graphList.length * config.unitSize; | |||||
ctx.lineWidth = config.lineWidth; | |||||
ctx.lineJoin = "round"; | |||||
ctx.lineCap = "round"; | |||||
}; | |||||
var genRandomStr = function () { | |||||
var chars = "0123456789ABCDEF"; | |||||
var stringLength = 6; | |||||
var randomString = '', rnum, i; | |||||
for (i = 0; i < stringLength; i++) { | |||||
rnum = Math.floor(Math.random() * chars.length); | |||||
randomString += chars.substring(rnum, rnum + 1); | |||||
} | |||||
return randomString; | |||||
}; | |||||
var findFlow = function (id) { | |||||
var i = flows.length; | |||||
while (i-- && flows[i].id !== id) {} | |||||
return i; | |||||
}; | |||||
var findColomn = function (symbol, row) { | |||||
var i = row.length; | |||||
while (i-- && row[i] !== symbol) {} | |||||
return i; | |||||
}; | |||||
var findBranchOut = function (row) { | |||||
if (!row) { | |||||
return -1 | |||||
} | |||||
var i = row.length; | |||||
while (i-- && | |||||
!(row[i - 1] && row[i] === "/" && row[i - 1] === "|") && | |||||
!(row[i - 2] && row[i] === "_" && row[i - 2] === "|")) {} | |||||
return i; | |||||
} | |||||
var genNewFlow = function () { | |||||
var newId; | |||||
do { | |||||
newId = genRandomStr(); | |||||
} while (findFlow(newId) !== -1); | |||||
return {id:newId, color:"#" + newId}; | |||||
}; | |||||
//draw method | |||||
var drawLineRight = function (x, y, color) { | |||||
ctx.strokeStyle = color; | |||||
ctx.beginPath(); | |||||
ctx.moveTo(x, y + config.unitSize / 2); | |||||
ctx.lineTo(x + config.unitSize, y + config.unitSize / 2); | |||||
ctx.stroke(); | |||||
}; | |||||
var drawLineUp = function (x, y, color) { | |||||
ctx.strokeStyle = color; | |||||
ctx.beginPath(); | |||||
ctx.moveTo(x, y + config.unitSize / 2); | |||||
ctx.lineTo(x, y - config.unitSize / 2); | |||||
ctx.stroke(); | |||||
}; | |||||
var drawNode = function (x, y, color) { | |||||
ctx.strokeStyle = color; | |||||
drawLineUp(x, y, color); | |||||
ctx.beginPath(); | |||||
ctx.arc(x, y, config.nodeRadius, 0, Math.PI * 2, true); | |||||
ctx.fill(); | |||||
}; | |||||
var drawLineIn = function (x, y, color) { | |||||
ctx.strokeStyle = color; | |||||
ctx.beginPath(); | |||||
ctx.moveTo(x + config.unitSize, y + config.unitSize / 2); | |||||
ctx.lineTo(x, y - config.unitSize / 2); | |||||
ctx.stroke(); | |||||
}; | |||||
var drawLineOut = function (x, y, color) { | |||||
ctx.strokeStyle = color; | |||||
ctx.beginPath(); | |||||
ctx.moveTo(x, y + config.unitSize / 2); | |||||
ctx.lineTo(x + config.unitSize, y - config.unitSize / 2); | |||||
ctx.stroke(); | |||||
}; | |||||
var draw = function (graphList) { | |||||
var colomn, colomnIndex, prevColomn, condenseIndex; | |||||
var x, y; | |||||
var color; | |||||
var nodePos, outPos; | |||||
var tempFlow; | |||||
var prevRowLength = 0; | |||||
var flowSwapPos = -1; | |||||
var lastLinePos; | |||||
var i, k, l; | |||||
var condenseCurrentLength, condensePrevLength = 0, condenseNextLength = 0; | |||||
var inlineIntersect = false; | |||||
//initiate for first row | |||||
for (i = 0, l = graphList[0].length; i < l; i++) { | |||||
if (graphList[0][i] !== "_" && graphList[0][i] !== " ") { | |||||
flows.push(genNewFlow()); | |||||
} | |||||
} | |||||
y = canvas.height - config.unitSize * 0.5; | |||||
//iterate | |||||
for (i = 0, l = graphList.length; i < l; i++) { | |||||
x = config.unitSize * 0.5; | |||||
currentRow = graphList[i]; | |||||
nextRow = graphList[i + 1]; | |||||
prevRow = graphList[i - 1]; | |||||
flowSwapPos = -1; | |||||
condenseCurrentLength = currentRow.filter(function (val) { | |||||
return (val !== " " && val !== "_") | |||||
}).length; | |||||
if (nextRow) { | |||||
condenseNextLength = nextRow.filter(function (val) { | |||||
return (val !== " " && val !== "_") | |||||
}).length; | |||||
} else { | |||||
condenseNextLength = 0; | |||||
} | |||||
//pre process begin | |||||
//use last row for analysing | |||||
if (prevRow) { | |||||
if (!inlineIntersect) { | |||||
//intersect might happen | |||||
for (colomnIndex = 0; colomnIndex < prevRowLength; colomnIndex++) { | |||||
if (prevRow[colomnIndex + 1] && | |||||
(prevRow[colomnIndex] === "/" && prevRow[colomnIndex + 1] === "|") || | |||||
((prevRow[colomnIndex] === "_" && prevRow[colomnIndex + 1] === "|") && | |||||
(prevRow[colomnIndex + 2] === "/"))) { | |||||
flowSwapPos = colomnIndex; | |||||
//swap two flow | |||||
tempFlow = {id:flows[flowSwapPos].id, color:flows[flowSwapPos].color}; | |||||
flows[flowSwapPos].id = flows[flowSwapPos + 1].id; | |||||
flows[flowSwapPos].color = flows[flowSwapPos + 1].color; | |||||
flows[flowSwapPos + 1].id = tempFlow.id; | |||||
flows[flowSwapPos + 1].color = tempFlow.color; | |||||
} | |||||
} | |||||
} | |||||
if (condensePrevLength < condenseCurrentLength && | |||||
((nodePos = findColomn("*", currentRow)) !== -1 && | |||||
(findColomn("_", currentRow) === -1))) { | |||||
flows.splice(nodePos - 1, 0, genNewFlow()); | |||||
} | |||||
if (prevRowLength > currentRow.length && | |||||
(nodePos = findColomn("*", prevRow)) !== -1) { | |||||
if (findColomn("_", currentRow) === -1 && | |||||
findColomn("/", currentRow) === -1 && | |||||
findColomn("\\", currentRow) === -1) { | |||||
flows.splice(nodePos + 1, 1); | |||||
} | |||||
} | |||||
} //done with the previous row | |||||
prevRowLength = currentRow.length; //store for next round | |||||
colomnIndex = 0; //reset index | |||||
condenseIndex = 0; | |||||
condensePrevLength = 0; | |||||
while (colomnIndex < currentRow.length) { | |||||
colomn = currentRow[colomnIndex]; | |||||
if (colomn !== " " && colomn !== "_") { | |||||
++condensePrevLength; | |||||
} | |||||
if (colomn === " " && | |||||
currentRow[colomnIndex + 1] && | |||||
currentRow[colomnIndex + 1] === "_" && | |||||
currentRow[colomnIndex - 1] && | |||||
currentRow[colomnIndex - 1] === "|") { | |||||
currentRow.splice(colomnIndex, 1); | |||||
currentRow[colomnIndex] = "/"; | |||||
colomn = "/"; | |||||
} | |||||
//create new flow only when no intersetc happened | |||||
if (flowSwapPos === -1 && | |||||
colomn === "/" && | |||||
currentRow[colomnIndex - 1] && | |||||
currentRow[colomnIndex - 1] === "|") { | |||||
flows.splice(condenseIndex, 0, genNewFlow()); | |||||
} | |||||
//change \ and / to | when it's in the last position of the whole row | |||||
if (colomn === "/" || colomn === "\\") { | |||||
if (!(colomn === "/" && findBranchOut(nextRow) === -1)) { | |||||
if ((lastLinePos = Math.max(findColomn("|", currentRow), | |||||
findColomn("*", currentRow))) !== -1 && | |||||
(lastLinePos < colomnIndex - 1)) { | |||||
while (currentRow[++lastLinePos] === " ") {} | |||||
if (lastLinePos === colomnIndex) { | |||||
currentRow[colomnIndex] = "|"; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
if (colomn === "*" && | |||||
prevRow && | |||||
prevRow[condenseIndex + 1] === "\\") { | |||||
flows.splice(condenseIndex + 1, 1); | |||||
} | |||||
if (colomn !== " ") { | |||||
++condenseIndex; | |||||
} | |||||
++colomnIndex; | |||||
} | |||||
condenseCurrentLength = currentRow.filter(function (val) { | |||||
return (val !== " " && val !== "_") | |||||
}).length; | |||||
//do some clean up | |||||
if (flows.length > condenseCurrentLength) { | |||||
flows.splice(condenseCurrentLength, flows.length - condenseCurrentLength); | |||||
} | |||||
colomnIndex = 0; | |||||
//a little inline analysis and draw process | |||||
while (colomnIndex < currentRow.length) { | |||||
colomn = currentRow[colomnIndex]; | |||||
prevColomn = currentRow[colomnIndex - 1]; | |||||
if (currentRow[colomnIndex] === " ") { | |||||
currentRow.splice(colomnIndex, 1); | |||||
x += config.unitSize; | |||||
continue; | |||||
} | |||||
//inline interset | |||||
if ((colomn === "_" || colomn === "/") && | |||||
currentRow[colomnIndex - 1] === "|" && | |||||
currentRow[colomnIndex - 2] === "_") { | |||||
inlineIntersect = true; | |||||
tempFlow = flows.splice(colomnIndex - 2, 1)[0]; | |||||
flows.splice(colomnIndex - 1, 0, tempFlow); | |||||
currentRow.splice(colomnIndex - 2, 1); | |||||
colomnIndex = colomnIndex - 1; | |||||
} else { | |||||
inlineIntersect = false; | |||||
} | |||||
color = flows[colomnIndex].color; | |||||
switch (colomn) { | |||||
case "_" : | |||||
drawLineRight(x, y, color); | |||||
x += config.unitSize; | |||||
break; | |||||
case "*" : | |||||
drawNode(x, y, color); | |||||
break; | |||||
case "|" : | |||||
drawLineUp(x, y, color); | |||||
break; | |||||
case "/" : | |||||
if (prevColomn && | |||||
(prevColomn === "/" || | |||||
prevColomn === " ")) { | |||||
x -= config.unitSize; | |||||
} | |||||
drawLineOut(x, y, color); | |||||
x += config.unitSize; | |||||
break; | |||||
case "\\" : | |||||
drawLineIn(x, y, color); | |||||
break; | |||||
} | |||||
++colomnIndex; | |||||
} | |||||
y -= config.unitSize; | |||||
} | |||||
}; | |||||
init(); | |||||
draw(graphList); | |||||
}; |
@@ -18,6 +18,7 @@ import ( | |||||
const ( | const ( | ||||
tplCommits base.TplName = "repo/commits" | tplCommits base.TplName = "repo/commits" | ||||
tplGraph base.TplName = "repo/graph" | |||||
tplDiff base.TplName = "repo/diff/page" | tplDiff base.TplName = "repo/diff/page" | ||||
) | ) | ||||
@@ -75,6 +76,32 @@ func Commits(ctx *context.Context) { | |||||
ctx.HTML(200, tplCommits) | ctx.HTML(200, tplCommits) | ||||
} | } | ||||
// Graph render commit graph - show commits from all branches. | |||||
func Graph(ctx *context.Context) { | |||||
ctx.Data["PageIsCommits"] = true | |||||
commitsCount, err := ctx.Repo.Commit.CommitsCount() | |||||
if err != nil { | |||||
ctx.Handle(500, "GetCommitsCount", err) | |||||
return | |||||
} | |||||
graph, err := models.GetCommitGraph(ctx.Repo.GitRepo) | |||||
if err != nil { | |||||
ctx.Handle(500, "GetCommitGraph", err) | |||||
return | |||||
} | |||||
ctx.Data["Graph"] = graph | |||||
ctx.Data["Username"] = ctx.Repo.Owner.Name | |||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name | |||||
ctx.Data["CommitCount"] = commitsCount | |||||
ctx.Data["Branch"] = ctx.Repo.BranchName | |||||
ctx.Data["RequireGitGraph"] = true | |||||
ctx.HTML(200, tplGraph) | |||||
} | |||||
// SearchCommits render commits filtered by keyword | // SearchCommits render commits filtered by keyword | ||||
func SearchCommits(ctx *context.Context) { | func SearchCommits(ctx *context.Context) { | ||||
ctx.Data["PageIsCommits"] = true | ctx.Data["PageIsCommits"] = true | ||||
@@ -31,6 +31,13 @@ | |||||
</script> | </script> | ||||
{{end}} | {{end}} | ||||
{{if .RequireGitGraph}} | |||||
<!-- graph --> | |||||
<script src="{{AppSubUrl}}/js/libs/gitgraph.js"></script> | |||||
<script src="{{AppSubUrl}}/js/draw.js"></script> | |||||
<link rel="stylesheet" href="{{AppSubUrl}}/css/gitgraph.css"> | |||||
{{end}} | |||||
<!-- Stylesheet --> | <!-- Stylesheet --> | ||||
<link rel="stylesheet" href="{{AppSubUrl}}/css/semantic-2.2.1.min.css"> | <link rel="stylesheet" href="{{AppSubUrl}}/css/semantic-2.2.1.min.css"> | ||||
<link rel="stylesheet" href="{{AppSubUrl}}/css/index.css?v={{MD5 AppVer}}"> | <link rel="stylesheet" href="{{AppSubUrl}}/css/index.css?v={{MD5 AppVer}}"> | ||||
@@ -2,8 +2,20 @@ | |||||
<div class="repository commits"> | <div class="repository commits"> | ||||
{{template "repo/header" .}} | {{template "repo/header" .}} | ||||
<div class="ui container"> | <div class="ui container"> | ||||
{{template "repo/branch_dropdown" .}} | |||||
{{template "repo/commits_table" .}} | |||||
<div class="ui secondary menu"> | |||||
{{template "repo/branch_dropdown" .}} | |||||
<div class="fitted item"> | |||||
<div class="ui breadcrumb"> | |||||
<a href="{{.RepoLink}}/graph"> | |||||
<span class="text"> | |||||
<i class="octicon octicon-git-branch"></i> | |||||
</span> | |||||
commit graph | |||||
</a> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
{{template "repo/commits_table" .}} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
{{template "base/footer" .}} | {{template "base/footer" .}} |
@@ -0,0 +1,44 @@ | |||||
{{template "base/head" .}} | |||||
<div class="repository commits"> | |||||
{{template "repo/header" .}} | |||||
<div class="ui container"> | |||||
<div id="git-graph-container"> | |||||
<div id="rel-container"> | |||||
<canvas id="graph-canvas"> | |||||
<ul id="graph-raw-list"> | |||||
{{ range .Graph }} | |||||
<li><span class="node-relation">{{ .GraphAcii -}}</span></li> | |||||
{{ end }} | |||||
</ul> | |||||
</canvas> | |||||
</div> | |||||
<div id="rev-container"> | |||||
<ul id="rev-list"> | |||||
{{ range .Graph }} | |||||
<li> | |||||
{{ if .OnlyRelation }} | |||||
<span /> | |||||
{{ else }} | |||||
<code id="{{.ShortRev}}"> | |||||
<a href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.Rev}}">{{ .ShortRev}}</a> | |||||
</code> | |||||
<strong> {{.Branch}}</strong> | |||||
<em>{{.Subject}}</em> by | |||||
<span class="author"> | |||||
{{.Author}} | |||||
</span> | |||||
<span class="time">{{.Date}}</span> | |||||
{{ end }} | |||||
</li> | |||||
{{ end }} | |||||
</ul> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
{{template "base/footer" .}} |