commithgraph / timeline (#428)
* Add model and tests for graph * Add route and router for graph * Add assets for graph * Add template for graphtokarchuk/v1.17
parent
35d9378e4e
commit
22e1bd31c6
@ -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); |
||||
}; |
@ -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" .}} |
Loading…
Reference in new issue