mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	commithgraph / timeline (#428)
* Add model and tests for graph * Add route and router for graph * Add assets for graph * Add template for graph
This commit is contained in:
		@@ -547,6 +547,7 @@ func runWeb(ctx *cli.Context) error {
 | 
			
		||||
			m.Get("/src/*", repo.SetEditorconfigIfExists, repo.Home)
 | 
			
		||||
			m.Get("/raw/*", repo.SingleDownload)
 | 
			
		||||
			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("/forks", repo.Forks)
 | 
			
		||||
		}, context.RepoRef())
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										108
									
								
								models/graph.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								models/graph.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								models/graph_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								models/graph_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								public/css/gitgraph.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								public/css/gitgraph.css
									
									
									
									
									
										Normal file
									
								
							@@ -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;}
 | 
			
		||||
							
								
								
									
										17
									
								
								public/js/draw.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								public/js/draw.js
									
									
									
									
									
										Normal file
									
								
							@@ -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);
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										399
									
								
								public/js/libs/gitgraph.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								public/js/libs/gitgraph.js
									
									
									
									
									
										Normal file
									
								
							@@ -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 (
 | 
			
		||||
	tplCommits base.TplName = "repo/commits"
 | 
			
		||||
	tplGraph   base.TplName = "repo/graph"
 | 
			
		||||
	tplDiff    base.TplName = "repo/diff/page"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -75,6 +76,32 @@ func Commits(ctx *context.Context) {
 | 
			
		||||
	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
 | 
			
		||||
func SearchCommits(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["PageIsCommits"] = true
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,13 @@
 | 
			
		||||
		</script>
 | 
			
		||||
	{{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 -->
 | 
			
		||||
	<link rel="stylesheet" href="{{AppSubUrl}}/css/semantic-2.2.1.min.css">
 | 
			
		||||
	<link rel="stylesheet" href="{{AppSubUrl}}/css/index.css?v={{MD5 AppVer}}">
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,20 @@
 | 
			
		||||
<div class="repository commits">
 | 
			
		||||
	{{template "repo/header" .}}
 | 
			
		||||
	<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>
 | 
			
		||||
{{template "base/footer" .}}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								templates/repo/graph.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								templates/repo/graph.tmpl
									
									
									
									
									
										Normal file
									
								
							@@ -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" .}}
 | 
			
		||||
		Reference in New Issue
	
	Block a user