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:
		
							parent
							
								
									35d9378e4e
								
							
						
					
					
						commit
						22e1bd31c6
					
				
					 10 changed files with 673 additions and 2 deletions
				
			
		|  | @ -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()) | ||||||
|  |  | ||||||
							
								
								
									
										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 ( | 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" .}} | 	  <div class="ui secondary menu"> | ||||||
| 		{{template "repo/commits_table" .}} | 	    {{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" .}} | ||||||
|  |  | ||||||
							
								
								
									
										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" .}} | ||||||
		Loading…
	
		Reference in a new issue