From 482bfebec910ef023d9202ac8e7f7366fd8b7a72 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Tue, 4 Feb 2020 21:14:26 -0800 Subject: [PATCH 01/18] Fix issues when some none "HEAD" nodes are not rendered correctly in big repo --- components/graph/git-node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/graph/git-node.js b/components/graph/git-node.js index 0941c9d5a..abfa7e44b 100644 --- a/components/graph/git-node.js +++ b/components/graph/git-node.js @@ -134,7 +134,7 @@ class GitNodeViewModel extends Animateable { } else { this.r(15); this.cx(610 + (90 * this.branchOrder())); - this.cy(this.aboveNode ? this.aboveNode.cy() + 60 : 120); + this.cy(this.aboveNode && !isNaN(this.aboveNode.cy()) ? this.aboveNode.cy() + 60 : 120); } if (this.aboveNode && this.aboveNode.selected()) { From 33b25018cd59bd75a1ce65fc21231dc1d92ed59a Mon Sep 17 00:00:00 2001 From: JK Kim Date: Tue, 4 Feb 2020 21:42:06 -0800 Subject: [PATCH 02/18] Prevent multi execution for `loadNodesFromApi` --- components/graph/graph.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index e0618a93d..f9fa7d71f 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -64,12 +64,12 @@ class GraphViewModel { this.loadNodesFromApiThrottled = _.throttle(this.loadNodesFromApi.bind(this), 1000); this.updateBranchesThrottled = _.throttle(this.updateBranches.bind(this), 1000); - this.loadNodesFromApi(); - this.updateBranches(); this.graphWidth = ko.observable(); this.graphHeight = ko.observable(800); this.searchIcon = octicons.search.toSVG({ 'height': 18 }); this.plusIcon = octicons.plus.toSVG({ 'height': 18 }); + this.isLoadNodesRunning = false; + this.loadNodesFromApi(); } updateNode(parentElement) { @@ -97,8 +97,10 @@ class GraphViewModel { } loadNodesFromApi() { - const nodeSize = this.nodes().length; + if (this.isLoadNodesRunning) return; + this.isLoadNodesRunning = true + const nodeSize = this.nodes().length; return this.server.getPromise('/gitlog', { path: this.repoPath(), limit: this.limit(), skip: this.skip() }) .then(log => { // set new limit and skip @@ -106,9 +108,10 @@ class GraphViewModel { this.skip(parseInt(log.skip)); return log.nodes || []; }).then(nodes => // create and/or calculate nodes - this.computeNode(nodes.map((logEntry) => { - return this.getNode(logEntry.sha1, logEntry); // convert to node object - }))).then(nodes => { + this.computeNode(nodes.map((logEntry) => { + return this.getNode(logEntry.sha1, logEntry); // convert to node object + })) + ).then(nodes => { // create edges const edges = []; nodes.forEach(node => { @@ -130,6 +133,7 @@ class GraphViewModel { if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { this.scrolledToEnd(); } + this.isLoadNodesRunning = false }); } From 8faf41ec05059f96e181e64bab54fce082d3087c Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 5 Feb 2020 08:02:38 -0800 Subject: [PATCH 03/18] Making git node to be incremental --- components/graph/graph-graphics.html | 5 +---- components/graph/graph.js | 30 ++++++++++------------------ components/graph/graph.less | 4 ---- source/config.js | 4 ++-- source/git-api.js | 9 ++++----- source/git-promise.js | 14 ++++++------- 6 files changed, 24 insertions(+), 42 deletions(-) diff --git a/components/graph/graph-graphics.html b/components/graph/graph-graphics.html index dd996bdf7..f85ee95a1 100644 --- a/components/graph/graph-graphics.html +++ b/components/graph/graph-graphics.html @@ -18,12 +18,9 @@ - + - - - diff --git a/components/graph/graph.js b/components/graph/graph.js index f9fa7d71f..fa8726f61 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -15,7 +15,6 @@ class GraphViewModel { constructor(server, repoPath) { this._markIdeologicalStamp = 0; this.repoPath = repoPath; - this.limit = ko.observable(numberOfNodesPerLoad); this.skip = ko.observable(0); this.server = server; this.currentRemote = ko.observable(); @@ -37,12 +36,6 @@ class GraphViewModel { this.currentActionContext = ko.observable(); this.edgesById = {}; this.scrolledToEnd = _.debounce(() => { - this.limit(numberOfNodesPerLoad + this.limit()); - this.loadNodesFromApi(); - }, 500, true); - this.loadAhead = _.debounce(() => { - if (this.skip() <= 0) return; - this.skip(Math.max(this.skip() - numberOfNodesPerLoad, 0)); this.loadNodesFromApi(); }, 500, true); this.commitOpacity = ko.observable(1.0); @@ -101,28 +94,23 @@ class GraphViewModel { this.isLoadNodesRunning = true const nodeSize = this.nodes().length; - return this.server.getPromise('/gitlog', { path: this.repoPath(), limit: this.limit(), skip: this.skip() }) + return this.server.getPromise('/gitlog', { path: this.repoPath(), skip: this.skip(), lookForHead: this.HEAD() ? "false" : "true" }) .then(log => { // set new limit and skip - this.limit(parseInt(log.limit)); this.skip(parseInt(log.skip)); return log.nodes || []; - }).then(nodes => // create and/or calculate nodes - this.computeNode(nodes.map((logEntry) => { - return this.getNode(logEntry.sha1, logEntry); // convert to node object + }).then(nodes => {// create and/or calculate nodes + return this.computeNode(nodes.map((logEntry) => { + return this.getNode(logEntry.sha1, logEntry) })) - ).then(nodes => { + }).then(nodes => { // create edges - const edges = []; nodes.forEach(node => { node.parents().forEach(parentSha1 => { - edges.push(this.getEdge(node.sha1, parentSha1)); + this.edges.push(this.getEdge(node.sha1, parentSha1)); }); - node.render(); }); - this.edges(edges); - this.nodes(nodes); if (nodes.length > 0) { this.graphHeight(nodes[nodes.length - 1].cy() + 80); } @@ -178,16 +166,18 @@ class GraphViewModel { } this.heighstBranchOrder = branchSlotCounter - 1; - let prevNode; + let prevNode = this.nodes() ? this.nodes()[this.nodes().length - 1] : null; nodes.forEach(node => { node.ancestorOfHEAD(node.ancestorOfHEADTimeStamp == updateTimeStamp); if (node.ancestorOfHEAD()) node.branchOrder(0); node.aboveNode = prevNode; if (prevNode) prevNode.belowNode = node; prevNode = node; + node.render(); + this.nodes.push(node); }); - return nodes; + return this.nodes(); } getEdge(nodeAsha1, nodeBsha1) { diff --git a/components/graph/graph.less b/components/graph/graph.less index f00c14ec2..2fe913ddf 100644 --- a/components/graph/graph.less +++ b/components/graph/graph.less @@ -8,10 +8,6 @@ .graphLog { left: 575px; - .loadAhead { - cursor: pointer; - animation: throb 1s ease alternate infinite; - } } @keyframes throb { diff --git a/source/config.js b/source/config.js index 4689541d5..213257490 100644 --- a/source/config.js +++ b/source/config.js @@ -126,8 +126,8 @@ const defaultConfig = { // Always load with active checkout branch (deprecated: use `maxActiveBranchSearchIteration`) alwaysLoadActiveBranch: false, - // Max search iterations for active branch. ( value means not searching for active branch) - maxActiveBranchSearchIteration: -1, + // Max search iterations for active branch. + maxActiveBranchSearchIteration: 25, // number of nodes to load for each git.log call numberOfNodesPerLoad: 25, diff --git a/source/git-api.js b/source/git-api.js index 5b90761ab..a0b4c2bff 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -309,16 +309,15 @@ exports.registerApi = (env) => { }); app.get(`${exports.pathPrefix}/gitlog`, ensureAuthenticated, ensurePathExists, (req, res) => { - const limit = getNumber(req.query.limit, config.numberOfNodesPerLoad || 25); const skip = getNumber(req.query.skip, 0); - const task = gitPromise.log(req.query.path, limit, skip, config.maxActiveBranchSearchIteration) + const task = gitPromise.log(req.query.path, skip, req.query.lookForHead === "true", config.maxActiveBranchSearchIteration) .catch((err) => { if (err.stderr && err.stderr.indexOf('fatal: bad default revision \'HEAD\'') == 0) { - return { "limit": limit, "skip": skip, "nodes": []}; + return { "skip": skip, "nodes": []}; } else if (/fatal: your current branch \'.+\' does not have any commits yet.*/.test(err.stderr)) { - return { "limit": limit, "skip": skip, "nodes": []}; + return { "skip": skip, "nodes": []}; } else if (err.stderr && err.stderr.indexOf('fatal: Not a git repository') == 0) { - return { "limit": limit, "skip": skip, "nodes": []}; + return { "skip": skip, "nodes": []}; } else { throw err; } diff --git a/source/git-promise.js b/source/git-promise.js index 96cf49499..ebb2e6e36 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -442,23 +442,23 @@ git.revParse = (repoPath) => { }).catch((err) => ({ type: 'uninited', gitRootPath: path.normalize(repoPath) })); } -git.log = (path, limit, skip, maxActiveBranchSearchIteration) => { - return git(['log', '--cc', '--decorate=full', '--show-signature', '--date=default', '--pretty=fuller', '-z', '--branches', '--tags', '--remotes', '--parents', '--no-notes', '--numstat', '--date-order', `--max-count=${limit}`, `--skip=${skip}`], path) +git.log = (path, skip, lookForHead, maxActiveBranchSearchIteration) => { + return git(['log', '--cc', '--decorate=full', '--show-signature', '--date=default', '--pretty=fuller', '-z', '--branches', '--tags', '--remotes', '--parents', '--no-notes', '--numstat', '--date-order', `--max-count=${config.numberOfNodesPerLoad}`, `--skip=${skip}`], path) .then(gitParser.parseGitLog) .then((log) => { log = log ? log : []; - if (maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) { - return git.log(path, config.numberOfNodesPerLoad + limit, config.numberOfNodesPerLoad + skip, maxActiveBranchSearchIteration - 1) + skip = skip + log.length + if (lookForHead && maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) { + return git.log(path, skip, maxActiveBranchSearchIteration - 1) .then(innerLog => { return { - "limit": limit + (innerLog.isHeadExist ? 0 : config.numberOfNodesPerLoad), - "skip": skip + (innerLog.isHeadExist ? 0 : config.numberOfNodesPerLoad), + "skip": skip, "nodes": log.concat(innerLog.nodes), "isHeadExist": innerLog.isHeadExist } }); } else { - return { "limit": limit, "skip": skip, "nodes": log, "isHeadExist": log.isHeadExist }; + return { "skip": skip, "nodes": log, "isHeadExist": log.isHeadExist }; } }); } From 837212141a0056d3d73be9832cd388feeec80a98 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 5 Feb 2020 22:28:55 -0800 Subject: [PATCH 04/18] Fix branch ordering persistence issue --- components/graph/graph.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index fa8726f61..8457b2c33 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -39,7 +39,7 @@ class GraphViewModel { this.loadNodesFromApi(); }, 500, true); this.commitOpacity = ko.observable(1.0); - this.heighstBranchOrder = 0; + this.highestBranchOrder = 0; this.hoverGraphActionGraphic = ko.observable(); this.hoverGraphActionGraphic.subscribe(value => { if (value && value.destroy) @@ -99,10 +99,10 @@ class GraphViewModel { // set new limit and skip this.skip(parseInt(log.skip)); return log.nodes || []; - }).then(nodes => {// create and/or calculate nodes - return this.computeNode(nodes.map((logEntry) => { - return this.getNode(logEntry.sha1, logEntry) - })) + }).then(nodes => { + // create and/or calculate nodes + const nodeVMs = nodes.map((logEntry) => this.getNode(logEntry.sha1, logEntry)); + return this.computeNode(nodeVMs); }).then(nodes => { // create edges nodes.forEach(node => { @@ -114,7 +114,7 @@ class GraphViewModel { if (nodes.length > 0) { this.graphHeight(nodes[nodes.length - 1].cy() + 80); } - this.graphWidth(1000 + (this.heighstBranchOrder * 90)); + this.graphWidth(1000 + (this.highestBranchOrder * 90)); programEvents.dispatch({ event: 'init-tooltip' }); }).catch((e) => this.server.unhandledRejection(e)) .finally(() => { @@ -140,32 +140,32 @@ class GraphViewModel { const updateTimeStamp = moment().valueOf(); if (this.HEAD()) { + if (this.highestBranchOrder == 0) { + this.highestBranchOrder = 1; + } this.traverseNodeLeftParents(this.HEAD(), node => { node.ancestorOfHEADTimeStamp = updateTimeStamp; }); } // Filter out nodes which doesn't have a branch (staging and orphaned nodes) - nodes = nodes.filter(node => (node.ideologicalBranch() && !node.ideologicalBranch().isStash) || node.ancestorOfHEADTimeStamp == updateTimeStamp); - - let branchSlotCounter = this.HEAD() ? 1 : 0; + const nodesWithRefs = nodes.filter(node => (node.ideologicalBranch() && !node.ideologicalBranch().isStash) || node.ancestorOfHEADTimeStamp == updateTimeStamp); // Then iterate from the bottom to fix the orders of the branches - for (let i = nodes.length - 1; i >= 0; i--) { - const node = nodes[i]; + for (let i = nodesWithRefs.length - 1; i >= 0; i--) { + const node = nodesWithRefs[i]; if (node.ancestorOfHEADTimeStamp == updateTimeStamp) continue; const ideologicalBranch = node.ideologicalBranch(); // First occurrence of the branch, find an empty slot for the branch if (ideologicalBranch.lastSlottedTimeStamp != updateTimeStamp) { ideologicalBranch.lastSlottedTimeStamp = updateTimeStamp; - ideologicalBranch.branchOrder = branchSlotCounter++; + ideologicalBranch.branchOrder = this.highestBranchOrder++; } node.branchOrder(ideologicalBranch.branchOrder); } - this.heighstBranchOrder = branchSlotCounter - 1; let prevNode = this.nodes() ? this.nodes()[this.nodes().length - 1] : null; nodes.forEach(node => { node.ancestorOfHEAD(node.ancestorOfHEADTimeStamp == updateTimeStamp); From b33a28fd0c75d5d9a7dc1f5bb06c1d216d053ce3 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 5 Feb 2020 22:49:37 -0800 Subject: [PATCH 05/18] Add debounce to node rednering --- components/graph/git-node.js | 57 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/components/graph/git-node.js b/components/graph/git-node.js index abfa7e44b..d9c514ba6 100644 --- a/components/graph/git-node.js +++ b/components/graph/git-node.js @@ -4,6 +4,7 @@ const components = require('ungit-components'); const programEvents = require('ungit-program-events'); const Animateable = require('./animateable'); const GraphActions = require('./git-graph-actions'); +const _ = require('lodash'); const maxBranchesToDisplay = parseInt(ungit.config.numRefsToShow / 5 * 3); // 3/5 of refs to show to branches const maxTagsToDisplay = ungit.config.numRefsToShow - maxBranchesToDisplay; // 2/5 of refs to show to tags @@ -106,6 +107,34 @@ class GitNodeViewModel extends Animateable { new GraphActions.Revert(this.graph, this), new GraphActions.Squash(this.graph, this) ]; + + this.render = _.debounce(() => { + this.refSearchFormVisible(false); + if (!this.isInited) return; + if (this.ancestorOfHEAD()) { + this.r(30); + this.cx(610); + + if (!this.aboveNode) { + this.cy(120); + } else if (this.aboveNode.ancestorOfHEAD()) { + this.cy(this.aboveNode.cy() + 120); + } else { + this.cy(this.aboveNode.cy() + 60); + } + } else { + this.r(15); + this.cx(610 + (90 * this.branchOrder())); + this.cy(this.aboveNode && !isNaN(this.aboveNode.cy()) ? this.aboveNode.cy() + 60 : 120); + } + + if (this.aboveNode && this.aboveNode.selected()) { + this.cy(this.aboveNode.cy() + this.aboveNode.commitComponent.element().offsetHeight + 30); + } + + this.color(this.ideologicalBranch() ? this.ideologicalBranch().color : '#666'); + this.animate(); + }, 500, {leading: true}) } getGraphAttr() { @@ -117,34 +146,6 @@ class GitNodeViewModel extends Animateable { this.element().setAttribute('y', val[1] - 30); } - render() { - this.refSearchFormVisible(false); - if (!this.isInited) return; - if (this.ancestorOfHEAD()) { - this.r(30); - this.cx(610); - - if (!this.aboveNode) { - this.cy(120); - } else if (this.aboveNode.ancestorOfHEAD()) { - this.cy(this.aboveNode.cy() + 120); - } else { - this.cy(this.aboveNode.cy() + 60); - } - } else { - this.r(15); - this.cx(610 + (90 * this.branchOrder())); - this.cy(this.aboveNode && !isNaN(this.aboveNode.cy()) ? this.aboveNode.cy() + 60 : 120); - } - - if (this.aboveNode && this.aboveNode.selected()) { - this.cy(this.aboveNode.cy() + this.aboveNode.commitComponent.element().offsetHeight + 30); - } - - this.color(this.ideologicalBranch() ? this.ideologicalBranch().color : '#666'); - this.animate(); - } - setData(logEntry) { this.title(logEntry.message.split('\n')[0]); this.parents(logEntry.parents || []); From 99d6e7ccb20d3d53e57250b68de9ce9cfe084e26 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 5 Feb 2020 22:50:07 -0800 Subject: [PATCH 06/18] Allow setting skip and limit dynamically --- components/graph/graph.js | 36 +++++++++++++++++++++--------------- source/git-api.js | 11 +++++++---- source/git-promise.js | 16 +++++----------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index 8457b2c33..a4b70c333 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -15,7 +15,7 @@ class GraphViewModel { constructor(server, repoPath) { this._markIdeologicalStamp = 0; this.repoPath = repoPath; - this.skip = ko.observable(0); + this.graphSkip = 0; this.server = server; this.currentRemote = ko.observable(); this.nodes = ko.observableArray(); @@ -89,25 +89,32 @@ class GraphViewModel { return refViewModel; } - loadNodesFromApi() { + loadNodesFromApi(skip, limit) { if (this.isLoadNodesRunning) return; - this.isLoadNodesRunning = true + this.isLoadNodesRunning = true; + + skip = skip ? skip : this.graphSkip; + limit = limit ? limit : parseInt(ungit.config.numberOfNodesPerLoad); const nodeSize = this.nodes().length; - return this.server.getPromise('/gitlog', { path: this.repoPath(), skip: this.skip(), lookForHead: this.HEAD() ? "false" : "true" }) - .then(log => { - // set new limit and skip - this.skip(parseInt(log.skip)); - return log.nodes || []; - }).then(nodes => { + return this.server.getPromise('/gitlog', { path: this.repoPath(), skip: skip, limit: limit }) + .then(log => log || []) + .then(nodes => { // create and/or calculate nodes - const nodeVMs = nodes.map((logEntry) => this.getNode(logEntry.sha1, logEntry)); + let prevNode = this.nodes() ? this.nodes()[this.nodes().length - 1] : null; + const nodeVMs = nodes.map((logEntry) => { + const nodeVM = this.getNode(logEntry.sha1, logEntry); + nodeVM.aboveNode = prevNode; + if (prevNode) prevNode.belowNode = nodeVM; + prevNode = nodeVM; + return nodeVM; + }); return this.computeNode(nodeVMs); }).then(nodes => { // create edges nodes.forEach(node => { node.parents().forEach(parentSha1 => { - this.edges.push(this.getEdge(node.sha1, parentSha1)); + this.getEdge(node.sha1, parentSha1); }); }); @@ -116,6 +123,8 @@ class GraphViewModel { } this.graphWidth(1000 + (this.highestBranchOrder * 90)); programEvents.dispatch({ event: 'init-tooltip' }); + + this.graphSkip += parseInt(ungit.config.numberOfNodesPerLoad) }).catch((e) => this.server.unhandledRejection(e)) .finally(() => { if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { @@ -166,13 +175,9 @@ class GraphViewModel { node.branchOrder(ideologicalBranch.branchOrder); } - let prevNode = this.nodes() ? this.nodes()[this.nodes().length - 1] : null; nodes.forEach(node => { node.ancestorOfHEAD(node.ancestorOfHEADTimeStamp == updateTimeStamp); if (node.ancestorOfHEAD()) node.branchOrder(0); - node.aboveNode = prevNode; - if (prevNode) prevNode.belowNode = node; - prevNode = node; node.render(); this.nodes.push(node); }); @@ -185,6 +190,7 @@ class GraphViewModel { let edge = this.edgesById[id]; if (!edge) { edge = this.edgesById[id] = new EdgeViewModel(this, nodeAsha1, nodeBsha1); + this.edges.push(edge); } return edge; } diff --git a/source/git-api.js b/source/git-api.js index a0b4c2bff..0ed863fb5 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -310,14 +310,17 @@ exports.registerApi = (env) => { app.get(`${exports.pathPrefix}/gitlog`, ensureAuthenticated, ensurePathExists, (req, res) => { const skip = getNumber(req.query.skip, 0); - const task = gitPromise.log(req.query.path, skip, req.query.lookForHead === "true", config.maxActiveBranchSearchIteration) + const limit = getNumber(req.query.limit, parseInt(config.numberOfNodesPerLoad)); + const isLookForHead = skip === 0 && limit === config.numberOfNodesPerLoad; + + const task = gitPromise.log(req.query.path, skip, limit, isLookForHead, config.maxActiveBranchSearchIteration) .catch((err) => { if (err.stderr && err.stderr.indexOf('fatal: bad default revision \'HEAD\'') == 0) { - return { "skip": skip, "nodes": []}; + return []; } else if (/fatal: your current branch \'.+\' does not have any commits yet.*/.test(err.stderr)) { - return { "skip": skip, "nodes": []}; + return []; } else if (err.stderr && err.stderr.indexOf('fatal: Not a git repository') == 0) { - return { "skip": skip, "nodes": []}; + return []; } else { throw err; } diff --git a/source/git-promise.js b/source/git-promise.js index ebb2e6e36..70a1c5d80 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -442,23 +442,17 @@ git.revParse = (repoPath) => { }).catch((err) => ({ type: 'uninited', gitRootPath: path.normalize(repoPath) })); } -git.log = (path, skip, lookForHead, maxActiveBranchSearchIteration) => { - return git(['log', '--cc', '--decorate=full', '--show-signature', '--date=default', '--pretty=fuller', '-z', '--branches', '--tags', '--remotes', '--parents', '--no-notes', '--numstat', '--date-order', `--max-count=${config.numberOfNodesPerLoad}`, `--skip=${skip}`], path) +git.log = (path, skip, limit, lookForHead, maxActiveBranchSearchIteration) => { + return git(['log', '--cc', '--decorate=full', '--show-signature', '--date=default', '--pretty=fuller', '-z', '--branches', '--tags', '--remotes', '--parents', '--no-notes', '--numstat', '--date-order', `--max-count=${limit}`, `--skip=${skip}`], path) .then(gitParser.parseGitLog) .then((log) => { log = log ? log : []; - skip = skip + log.length + skip = skip + log.length; if (lookForHead && maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) { return git.log(path, skip, maxActiveBranchSearchIteration - 1) - .then(innerLog => { - return { - "skip": skip, - "nodes": log.concat(innerLog.nodes), - "isHeadExist": innerLog.isHeadExist - } - }); + .then(innerLog => log.concat(innerLog.nodes)); } else { - return { "skip": skip, "nodes": log, "isHeadExist": log.isHeadExist }; + return log; } }); } From 95720f19bf3f75c920aa188b8460e51d9cc8c884 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Thu, 6 Feb 2020 21:06:35 -0800 Subject: [PATCH 07/18] In some OS, node, and git version combinations, 'rename' file watches are too aggressive --- source/git-api.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/source/git-api.js b/source/git-api.js index 0ed863fb5..2bad8042c 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -71,9 +71,8 @@ exports.registerApi = (env) => { }); } }).then(() => { - const watcher = fs.watch(pathToWatch, options || {}); - watcher.on('change', (event, filename) => { - if (!filename) return; + const watcher = fs.watch(pathToWatch, options || {}, (event, filename) => { + if (event === 'rename' || !filename) return; const filePath = path.join(subfolderPath, filename); winston.debug(`File change: ${filePath}`); if (isFileWatched(filePath, socket.ignore)) { @@ -82,9 +81,6 @@ exports.registerApi = (env) => { emitWorkingTreeChanged(socket.watcherPath); } }); - watcher.on('error', (err) => { - winston.warn(`Error watching ${pathToWatch}: `, JSON.stringify(err)); - }); socket.watcher.push(watcher); }); }; From 28ecc9fd9d40b11e234a0bb2bf7c443cb26fa3a1 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Thu, 6 Feb 2020 21:23:11 -0800 Subject: [PATCH 08/18] Setting up for node interweaving --- components/graph/graph.html | 2 +- components/graph/graph.js | 20 ++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/components/graph/graph.html b/components/graph/graph.html index 513db91f6..4f374feaf 100644 --- a/components/graph/graph.html +++ b/components/graph/graph.html @@ -1,5 +1,5 @@ -
+
diff --git a/components/graph/graph.js b/components/graph/graph.js index a4b70c333..46e506411 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -35,9 +35,6 @@ class GraphViewModel { this.showCommitNode = ko.observable(false); this.currentActionContext = ko.observable(); this.edgesById = {}; - this.scrolledToEnd = _.debounce(() => { - this.loadNodesFromApi(); - }, 500, true); this.commitOpacity = ko.observable(1.0); this.highestBranchOrder = 0; this.hoverGraphActionGraphic = ko.observable(); @@ -62,7 +59,6 @@ class GraphViewModel { this.searchIcon = octicons.search.toSVG({ 'height': 18 }); this.plusIcon = octicons.plus.toSVG({ 'height': 18 }); this.isLoadNodesRunning = false; - this.loadNodesFromApi(); } updateNode(parentElement) { @@ -89,12 +85,9 @@ class GraphViewModel { return refViewModel; } - loadNodesFromApi(skip, limit) { - if (this.isLoadNodesRunning) return; - this.isLoadNodesRunning = true; - - skip = skip ? skip : this.graphSkip; - limit = limit ? limit : parseInt(ungit.config.numberOfNodesPerLoad); + loadNodesFromApi(isRefresh) { + const skip = isRefresh ? 0 : this.graphSkip; + const limit = isRefresh && this.graphSkip > 0 ? this.graphSkip : parseInt(ungit.config.numberOfNodesPerLoad); const nodeSize = this.nodes().length; return this.server.getPromise('/gitlog', { path: this.repoPath(), skip: skip, limit: limit }) @@ -128,9 +121,8 @@ class GraphViewModel { }).catch((e) => this.server.unhandledRejection(e)) .finally(() => { if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { - this.scrolledToEnd(); + this.loadNodesFromApiThrottled(); } - this.isLoadNodesRunning = false }); } @@ -250,10 +242,10 @@ class GraphViewModel { onProgramEvent(event) { if (event.event == 'git-directory-changed') { - this.loadNodesFromApiThrottled(); + this.loadNodesFromApiThrottled(true); this.updateBranchesThrottled(); } else if (event.event == 'request-app-content-refresh') { - this.loadNodesFromApiThrottled(); + this.loadNodesFromApiThrottled(true); } else if (event.event == 'remote-tags-update') { this.setRemoteTags(event.tags); } else if (event.event == 'current-remote-changed') { From 85b418319912dcd5cb6a61c703e8b5e73c46662b Mon Sep 17 00:00:00 2001 From: Jung Kim Date: Fri, 7 Feb 2020 21:51:05 -0800 Subject: [PATCH 09/18] Making it work --- components/graph/git-graph-actions.js | 14 +++--- components/graph/git-node.js | 30 +++++++++---- components/graph/git-ref.js | 24 +++++----- components/graph/graph.js | 57 ++++++++++++++---------- source/git-parser.js | 63 ++++++++++++++------------- source/git-promise.js | 8 ++-- 6 files changed, 111 insertions(+), 85 deletions(-) diff --git a/components/graph/git-graph-actions.js b/components/graph/git-graph-actions.js index d690420a3..71b395407 100644 --- a/components/graph/git-graph-actions.js +++ b/components/graph/git-graph-actions.js @@ -76,7 +76,7 @@ class Move extends ActionBase { class Reset extends ActionBase { - constructor (graph, node) { + constructor(graph, node) { super(graph, 'Reset', 'reset', octicons.trashcan.toSVG({ 'height': 18 })); this.node = node; this.visible = ko.computed(() => { @@ -88,7 +88,7 @@ class Reset extends ActionBase { return remoteRef && remoteRef.node() && context && context.node() && remoteRef.node() != context.node() && - remoteRef.node().date < context.node().date; + remoteRef.node().timestamp < context.node().timestamp; }); } @@ -103,7 +103,7 @@ class Reset extends ActionBase { perform() { const context = this.graph.currentActionContext(); const remoteRef = context.getRemoteRef(this.graph.currentRemote()); - return components.create('yesnodialog', { title: 'Are you sure?', details: 'Resetting to ref: ' + remoteRef.name + ' cannot be undone with ungit.'}) + return components.create('yesnodialog', { title: 'Are you sure?', details: 'Resetting to ref: ' + remoteRef.name + ' cannot be undone with ungit.' }) .show() .closeThen((diag) => { if (!diag.result()) return; @@ -197,10 +197,10 @@ class Push extends ActionBase { return remoteRef.moveTo(ref.node().sha1); } else { return ref.createRemoteRef().then(() => { - if (this.graph.HEAD().name == ref.name) { - this.grah.HEADref().node(ref.node()); - } - }).finally(() => programEvents.dispatch({ event: 'request-fetch-tags' })); + if (this.graph.HEAD().name == ref.name) { + this.grah.HEADref().node(ref.node()); + } + }).finally(() => programEvents.dispatch({ event: 'request-fetch-tags' })); } } } diff --git a/components/graph/git-node.js b/components/graph/git-node.js index d9c514ba6..f4ae1a0bd 100644 --- a/components/graph/git-node.js +++ b/components/graph/git-node.js @@ -12,13 +12,14 @@ const maxTagsToDisplay = ungit.config.numRefsToShow - maxBranchesToDisplay; // 2 class GitNodeViewModel extends Animateable { constructor(graph, sha1) { super(graph); + this.hasBeenRenderedBefore = false; this.graph = graph; this.sha1 = sha1; this.isInited = false; this.title = ko.observable(); this.parents = ko.observableArray(); this.commitTime = undefined; // commit time in string - this.date = undefined; // commit time in numeric format for sort + this.timestamp = undefined; // commit time in numeric format for sort this.color = ko.observable(); this.ideologicalBranch = ko.observable(); this.remoteTags = ko.observableArray(); @@ -133,8 +134,14 @@ class GitNodeViewModel extends Animateable { } this.color(this.ideologicalBranch() ? this.ideologicalBranch().color : '#666'); + if (!this.hasBeenRenderedBefore) { + // push this nodes into the graph's node list to be rendered if first time. + // if been pushed before, no need to add to nodes. + this.hasBeenRenderedBefore = true; + graph.nodes.push(this); + } this.animate(); - }, 500, {leading: true}) + }, 500, { leading: true }) } getGraphAttr() { @@ -146,11 +153,16 @@ class GitNodeViewModel extends Animateable { this.element().setAttribute('y', val[1] - 30); } + setParent(parent) { + this.aboveNode = parent; + if (parent) parent.belowNode = this; + } + setData(logEntry) { this.title(logEntry.message.split('\n')[0]); this.parents(logEntry.parents || []); this.commitTime = logEntry.commitDate; - this.date = Date.parse(this.commitTime); + this.timestamp = logEntry.timestamp || Date.parse(this.commitTime); this.commitComponent.setData(logEntry); this.signatureMade(logEntry.signatureMade); this.signatureDate(logEntry.signatureDate); @@ -184,7 +196,7 @@ class GitNodeViewModel extends Animateable { }, messages: { noResults: '', - results: () => {} + results: () => { } } }).focus(() => { $(this).autocomplete('search', $(this).val()); @@ -213,7 +225,7 @@ class GitNodeViewModel extends Animateable { createTag() { if (!this.canCreateRef()) return; this.graph.server.postPromise('/tags', { path: this.graph.repoPath(), name: this.newBranchName(), sha1: this.sha1 }) - .then(() => this.graph.getRef(`refs/tags/${this.newBranchName()}`).node(this) ) + .then(() => this.graph.getRef(`refs/tags/${this.newBranchName()}`).node(this)) .catch((e) => this.graph.server.unhandledRejection(e)) .finally(() => { this.branchingFormVisible(false); @@ -228,7 +240,7 @@ class GitNodeViewModel extends Animateable { beforeBelowCR = this.belowNode.commitComponent.element().getBoundingClientRect(); } - let prevSelected = this.graph.currentActionContext(); + let prevSelected = this.graph.currentActionContext(); if (!(prevSelected instanceof GitNodeViewModel)) prevSelected = null; const prevSelectedCR = prevSelected ? prevSelected.commitComponent.element().getBoundingClientRect() : null; this.selected(!this.selected()); @@ -240,12 +252,12 @@ class GitNodeViewModel extends Animateable { // If the next node is showing, try to keep it in the screen (no jumping) if (beforeBelowCR.top < window.innerHeight) { window.scrollBy(0, afterBelowCR.top - beforeBelowCR.top); - // Otherwise just try to bring them to the middle of the screen + // Otherwise just try to bring them to the middle of the screen } else { window.scrollBy(0, afterBelowCR.top - window.innerHeight / 2); } } - // If we are selecting + // If we are selecting } else { const afterThisCR = this.commitComponent.element().getBoundingClientRect(); if ((prevSelectedCR && (prevSelectedCR.top < 0 || prevSelectedCR.top > window.innerHeight)) && @@ -268,7 +280,7 @@ class GitNodeViewModel extends Animateable { pushRef(ref) { if (ref.isRemoteTag && !this.remoteTags().includes(ref)) { this.remoteTags.push(ref); - } else if(!this.branchesAndLocalTags().includes(ref)) { + } else if (!this.branchesAndLocalTags().includes(ref)) { this.branchesAndLocalTags.push(ref); } } diff --git a/components/graph/git-ref.js b/components/graph/git-ref.js index ebc0edabb..fd236163a 100644 --- a/components/graph/git-ref.js +++ b/components/graph/git-ref.js @@ -63,7 +63,7 @@ class RefViewModel extends Selectable { // This optimization is for autocomplete display this.value = splitedName[splitedName.length - 1]; this.label = this.localRefName; - this.dom = `${this.localRefName}${octicons[(this.isTag ? 'tag': 'git-branch')].toSVG({ 'height': 18 })}`; + this.dom = `${this.localRefName}${octicons[(this.isTag ? 'tag' : 'git-branch')].toSVG({ 'height': 18 })}`; this.displayHtml = (largeCurrent) => { const size = (largeCurrent && this.current()) ? 26 : 18; @@ -109,8 +109,8 @@ class RefViewModel extends Selectable { operation = '/branches'; } - if (!rewindWarnOverride && this.node().date > toNode.date) { - promise = components.create('yesnodialog', { title: 'Are you sure?', details: 'This operation potentially going back in history.'}) + if (!rewindWarnOverride && this.node().timestamp > toNode.timestamp) { + promise = components.create('yesnodialog', { title: 'Are you sure?', details: 'This operation potentially going back in history.' }) .show() .closeThen(diag => { if (diag.result()) { @@ -208,15 +208,15 @@ class RefViewModel extends Selectable { const isLocalCurrent = this.getLocalRef() && this.getLocalRef().current(); return promise.resolve().then(() => { - if (isRemote && !isLocalCurrent) { - return this.server.postPromise('/branches', { - path: this.graph.repoPath(), - name: this.refName, - sha1: this.name, - force: true - }); - } - }).then(() => this.server.postPromise('/checkout', { path: this.graph.repoPath(), name: this.refName })) + if (isRemote && !isLocalCurrent) { + return this.server.postPromise('/branches', { + path: this.graph.repoPath(), + name: this.refName, + sha1: this.name, + force: true + }); + } + }).then(() => this.server.postPromise('/checkout', { path: this.graph.repoPath(), name: this.refName })) .then(() => { if (isRemote && isLocalCurrent) { return this.server.postPromise('/reset', { path: this.graph.repoPath(), to: this.name, mode: 'hard' }); diff --git a/components/graph/graph.js b/components/graph/graph.js index 46e506411..c70ea8266 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -51,14 +51,14 @@ class GraphViewModel { this.hoverGraphActionGraphic(null); } }); - - this.loadNodesFromApiThrottled = _.throttle(this.loadNodesFromApi.bind(this), 1000); - this.updateBranchesThrottled = _.throttle(this.updateBranches.bind(this), 1000); + this.loadNodesFromApiThrottled = _.throttle(this.loadNodesFromApi.bind(this), 1000, { leading: false }); + this.updateBranchesThrottled = _.throttle(this.updateBranches.bind(this), 1000, { leading: false }); this.graphWidth = ko.observable(); this.graphHeight = ko.observable(800); this.searchIcon = octicons.search.toSVG({ 'height': 18 }); this.plusIcon = octicons.plus.toSVG({ 'height': 18 }); this.isLoadNodesRunning = false; + this.loadNodesFromApiThrottled(); } updateNode(parentElement) { @@ -91,18 +91,31 @@ class GraphViewModel { const nodeSize = this.nodes().length; return this.server.getPromise('/gitlog', { path: this.repoPath(), skip: skip, limit: limit }) - .then(log => log || []) - .then(nodes => { - // create and/or calculate nodes - let prevNode = this.nodes() ? this.nodes()[this.nodes().length - 1] : null; - const nodeVMs = nodes.map((logEntry) => { - const nodeVM = this.getNode(logEntry.sha1, logEntry); - nodeVM.aboveNode = prevNode; - if (prevNode) prevNode.belowNode = nodeVM; - prevNode = nodeVM; - return nodeVM; + .then(logs => { + logs = logs || []; + // get or update each commit nodes. + logs.forEach(log => this.getNode(log.sha1, log)); + + // sort in commit order + const allNodes = Object.values(this.nodesById) + .filter(node => node.timestamp) // some nodes are created by ref without info + .sort((a, b) => { + if (a.timestamp < b.timestamp) { + return 1; + } else if (a.timestamp > b.timestamp) { + return -1; + } + return 0; + }); + + // reset parent child relationship for each + let prevNode = null; + allNodes.forEach(node => { + node.setParent(prevNode); + prevNode = node; }); - return this.computeNode(nodeVMs); + + return this.computeNode(allNodes); }).then(nodes => { // create edges nodes.forEach(node => { @@ -117,7 +130,9 @@ class GraphViewModel { this.graphWidth(1000 + (this.highestBranchOrder * 90)); programEvents.dispatch({ event: 'init-tooltip' }); - this.graphSkip += parseInt(ungit.config.numberOfNodesPerLoad) + if (!isRefresh) { + this.graphSkip += parseInt(ungit.config.numberOfNodesPerLoad) + } }).catch((e) => this.server.unhandledRejection(e)) .finally(() => { if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { @@ -159,8 +174,7 @@ class GraphViewModel { const ideologicalBranch = node.ideologicalBranch(); // First occurrence of the branch, find an empty slot for the branch - if (ideologicalBranch.lastSlottedTimeStamp != updateTimeStamp) { - ideologicalBranch.lastSlottedTimeStamp = updateTimeStamp; + if (!ideologicalBranch.branchOrder) { ideologicalBranch.branchOrder = this.highestBranchOrder++; } @@ -171,7 +185,6 @@ class GraphViewModel { node.ancestorOfHEAD(node.ancestorOfHEADTimeStamp == updateTimeStamp); if (node.ancestorOfHEAD()) node.branchOrder(0); node.render(); - this.nodes.push(node); }); return this.nodes(); @@ -187,8 +200,8 @@ class GraphViewModel { return edge; } - markNodesIdeologicalBranches(refs, nodes, nodesById) { - refs = refs.filter(r => !!r.node()); + markNodesIdeologicalBranches(refs) { + refs = refs.filter(r => !!r.node().timestamp); refs = refs.sort((a, b) => { if (a.isLocal && !b.isLocal) return -1; if (b.isLocal && !a.isLocal) return 1; @@ -198,8 +211,8 @@ class GraphViewModel { if (!a.isHEAD && b.isHEAD) return -1; if (a.isStash && !b.isStash) return 1; if (b.isStash && !a.isStash) return -1; - if (a.node() && a.node().date && b.node() && b.node().date) - return a.node().date - b.node().date; + if (a.node() && a.node().timestamp && b.node() && b.node().timestamp) + return a.node().timestamp - b.node().timestamp; return a.refName < b.refName ? -1 : 1; }); const stamp = this._markIdeologicalStamp++; diff --git a/source/git-parser.js b/source/git-parser.js index ec586851e..6ad2ec34d 100644 --- a/source/git-parser.js +++ b/source/git-parser.js @@ -85,6 +85,7 @@ const gitLogHeaders = { }, 'CommitDate': (currentCommmit, date) => { currentCommmit.commitDate = date; + currentCommmit.timestamp = Date.parse(date); }, 'Reflog': (currentCommmit, data) => { currentCommmit.reflogId = /\{(.*?)\}/.exec(data)[1]; @@ -160,7 +161,7 @@ exports.parseGitLog = (data) => { const parseFileChanges = (row, index) => { // git log is using -z so all the file changes are on one line // merge commits start the file changes with a null - if (row[0] === '\x00'){ + if (row[0] === '\x00') { row = row.slice(1); } fileChangeRegex.lastIndex = 0; @@ -169,7 +170,7 @@ exports.parseGitLog = (data) => { let fileName = match.groups.fileName || match.groups.newFileName; let oldFileName = match.groups.oldFileName || match.groups.fileName; let displayName; - if(match.groups.oldFileName) { + if (match.groups.oldFileName) { displayName = `${match.groups.oldFileName} → ${match.groups.newFileName}`; } else { displayName = fileName; @@ -223,7 +224,7 @@ exports.parseGitBranches = (text) => { text.split('\n').forEach((row) => { if (row.trim() == '') return; const branch = { name: row.slice(2) }; - if(row[0] == '*') branch.current = true; + if (row[0] == '*') branch.current = true; branches.push(branch); }); return branches; @@ -250,7 +251,7 @@ exports.parseGitLsRemote = (text) => { } exports.parseGitStashShow = (text) => { - const lines = text.split('\n').filter((item) => item ); + const lines = text.split('\n').filter((item) => item); return lines.slice(0, lines.length - 1).map((line) => { return { filename: line.substring(0, line.indexOf('|')).trim() } }); @@ -265,38 +266,38 @@ exports.parseGitSubmodule = (text, args) => { const submodules = []; text.trim().split('\n').filter((line) => line) - .forEach((line) => { - if (line.indexOf("[submodule") === 0) { - submodule = { name: line.match(/"(.*?)"/)[1] }; - submodules.push(submodule); - } else { - const parts = line.split("="); - const key = parts[0].trim(); - let value = parts.slice(1).join("=").trim(); - - if (key == "path") { - value = path.normalize(value); - } else if (key == "url") { - // keep a reference to the raw url - let url = submodule.rawUrl = value; - - // When a repo is checkout with ssh or git instead of an url - if (url.indexOf('http') != 0) { - if (url.indexOf('git:') == 0) { // git - url = `http${url.substr(url.indexOf(':'))}`; - } else { // ssh - url = `http://${url.substr(url.indexOf('@') + 1).replace(':', '/')}`; + .forEach((line) => { + if (line.indexOf("[submodule") === 0) { + submodule = { name: line.match(/"(.*?)"/)[1] }; + submodules.push(submodule); + } else { + const parts = line.split("="); + const key = parts[0].trim(); + let value = parts.slice(1).join("=").trim(); + + if (key == "path") { + value = path.normalize(value); + } else if (key == "url") { + // keep a reference to the raw url + let url = submodule.rawUrl = value; + + // When a repo is checkout with ssh or git instead of an url + if (url.indexOf('http') != 0) { + if (url.indexOf('git:') == 0) { // git + url = `http${url.substr(url.indexOf(':'))}`; + } else { // ssh + url = `http://${url.substr(url.indexOf('@') + 1).replace(':', '/')}`; + } } + + value = url; } - value = url; + submodule[key] = value; } + }); - submodule[key] = value; - } - }); - - let sorted_submodules = submodules.sort((a,b) => a.name.localeCompare(b.name)); + let sorted_submodules = submodules.sort((a, b) => a.name.localeCompare(b.name)); return sorted_submodules; } diff --git a/source/git-promise.js b/source/git-promise.js index 70a1c5d80..02eba8b52 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -254,8 +254,8 @@ git.resolveConflicts = (repoPath, files) => { } }); })).then(() => { - const addExec = toAdd.length > 0 ? git(['add', toAdd ], repoPath) : null; - const removeExec = toRemove.length > 0 ? git(['rm', toRemove ], repoPath) : null; + const addExec = toAdd.length > 0 ? git(['add', toAdd], repoPath) : null; + const removeExec = toRemove.length > 0 ? git(['rm', toRemove], repoPath) : null; return Bluebird.join(addExec, removeExec); }); } @@ -414,7 +414,7 @@ git.commit = (repoPath, amend, emptyCommit, message, files) => { return Bluebird.join(commitPromiseChain, Bluebird.all(diffPatchPromises)); }).then(() => { const ammendFlag = (amend ? '--amend' : ''); - const allowedEmptyFlag = ((emptyCommit ||amend) ? '--allow-empty' : ''); + const allowedEmptyFlag = ((emptyCommit || amend) ? '--allow-empty' : ''); const isGPGSign = (config.isForceGPGSign ? '-S' : ''); return git(['commit', ammendFlag, allowedEmptyFlag, isGPGSign, '--file=-'], repoPath, null, null, message); }).catch((err) => { @@ -435,7 +435,7 @@ git.revParse = (repoPath) => { return git(['rev-parse', '--show-toplevel'], repoPath).then((topLevel) => { const rootPath = path.normalize(topLevel.trim() ? topLevel.trim() : repoPath); if (resultLines[0].indexOf('true') > -1) { - return { type: 'inited', gitRootPath: rootPath }; + return { type: 'inited', gitRootPath: rootPath }; } return { type: 'uninited', gitRootPath: rootPath }; }); From 401946caf13b047eda91d24888702c9856818c13 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Sat, 8 Feb 2020 08:45:13 -0800 Subject: [PATCH 10/18] Calculate only for valid nodes --- components/graph/graph.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index c70ea8266..3cbd0cc4f 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -201,8 +201,14 @@ class GraphViewModel { } markNodesIdeologicalBranches(refs) { - refs = refs.filter(r => !!r.node().timestamp); - refs = refs.sort((a, b) => { + const refNodeMap = {}; + refs.forEach(r => { + if (!r.node()) return; + if (!r.node().timestamp) return; + if (refNodeMap[r.node().sha1]) return; + refNodeMap[r.node().sha1] = r; + }); + refs = Object.values(refNodeMap).sort((a, b) => { if (a.isLocal && !b.isLocal) return -1; if (b.isLocal && !a.isLocal) return 1; if (a.isBranch && !b.isBranch) return -1; From ecfde1db9b915ec6616fe45b6503dbd0f95262c0 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Sat, 8 Feb 2020 09:01:54 -0800 Subject: [PATCH 11/18] fix parsing bug --- source/git-promise.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/git-promise.js b/source/git-promise.js index 02eba8b52..9b4c1ec45 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -450,7 +450,7 @@ git.log = (path, skip, limit, lookForHead, maxActiveBranchSearchIteration) => { skip = skip + log.length; if (lookForHead && maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) { return git.log(path, skip, maxActiveBranchSearchIteration - 1) - .then(innerLog => log.concat(innerLog.nodes)); + .then(innerLog => log.concat(innerLog)); } else { return log; } From cf108015d285acb451fbf5e4b4e548c0bb4f2b63 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Sat, 8 Feb 2020 09:08:54 -0800 Subject: [PATCH 12/18] Fix graph height issue and making it more snappier --- components/graph/graph.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index 3cbd0cc4f..9fd730151 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -51,8 +51,8 @@ class GraphViewModel { this.hoverGraphActionGraphic(null); } }); - this.loadNodesFromApiThrottled = _.throttle(this.loadNodesFromApi.bind(this), 1000, { leading: false }); - this.updateBranchesThrottled = _.throttle(this.updateBranches.bind(this), 1000, { leading: false }); + this.loadNodesFromApiThrottled = _.throttle(this.loadNodesFromApi.bind(this), 1000, { leading: true, trailing: false }); + this.updateBranchesThrottled = _.throttle(this.updateBranches.bind(this), 1000, { leading: true, trailing: false }); this.graphWidth = ko.observable(); this.graphHeight = ko.observable(800); this.searchIcon = octicons.search.toSVG({ 'height': 18 }); @@ -115,18 +115,20 @@ class GraphViewModel { prevNode = node; }); - return this.computeNode(allNodes); - }).then(nodes => { - // create edges + const nodes = this.computeNode(allNodes); + let maxHeight = 0; + + // create edges and calculate max height nodes.forEach(node => { + if (node.cy() > maxHeight) { + maxHeight = node.cy() + } node.parents().forEach(parentSha1 => { this.getEdge(node.sha1, parentSha1); }); }); - if (nodes.length > 0) { - this.graphHeight(nodes[nodes.length - 1].cy() + 80); - } + this.graphHeight(maxHeight + 80); this.graphWidth(1000 + (this.highestBranchOrder * 90)); programEvents.dispatch({ event: 'init-tooltip' }); From e595e0cec01d49264a443b458bd4dd4ae24c8047 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Sun, 9 Feb 2020 11:37:09 -0800 Subject: [PATCH 13/18] update change log and package.json --- CHANGELOG.md | 10 +++++++++- package.json | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3375cdfb..7e84072d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,15 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). We are following the [Keep a Changelog](https://keepachangelog.com/) format. -## [Unreleased](https://github.com/FredrikNoren/ungit/compare/v1.5.3...master) +## [Unreleased](https://github.com/FredrikNoren/ungit/compare/v1.5.4...master) + +## [1.5.4](https://github.com/FredrikNoren/ungit/compare/v1.5.3...v1.5.4) + +### Fixed +- Performance optimizations for the big org [#1091](https://github.com/FredrikNoren/ungit/issues/1091) + - ignore 'rename' filewatch event as it can cause constant update and refresh loop + - Prevent full gitlog history from server to client and load only what is needed + - Prevent redundant ref and node calculations per each `/gitlog` api call ## [1.5.3](https://github.com/FredrikNoren/ungit/compare/v1.5.2...v1.5.3) diff --git a/package.json b/package.json index a74320ea7..7569e97c5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "ungit", "author": "Fredrik Norén ", "description": "Git made easy", - "version": "1.5.3", + "version": "1.5.4", "ungitPluginApiVersion": "0.2.0", "scripts": { "start": "node ./bin/ungit", @@ -110,4 +110,4 @@ "min_width": 400, "min_height": 200 } -} +} \ No newline at end of file From 9d3d4e374e924cb3ed955f67d76eaefd700eff9d Mon Sep 17 00:00:00 2001 From: JK Kim Date: Mon, 10 Feb 2020 21:58:05 -0800 Subject: [PATCH 14/18] #1091 prevent redundant ref calculations and overactive fs.watch() --- CHANGELOG.md | 1 - components/graph/graph.js | 120 +++++++++++++++++--------------------- package-lock.json | 2 +- package.json | 2 +- source/git-api.js | 105 ++++++++++++++++----------------- source/git-promise.js | 20 ++++--- 6 files changed, 122 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e84072d9..2a8074259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ We are following the [Keep a Changelog](https://keepachangelog.com/) format. ### Fixed - Performance optimizations for the big org [#1091](https://github.com/FredrikNoren/ungit/issues/1091) - ignore 'rename' filewatch event as it can cause constant update and refresh loop - - Prevent full gitlog history from server to client and load only what is needed - Prevent redundant ref and node calculations per each `/gitlog` api call ## [1.5.3](https://github.com/FredrikNoren/ungit/compare/v1.5.2...v1.5.3) diff --git a/components/graph/graph.js b/components/graph/graph.js index 9fd730151..00b59e8e3 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -15,7 +15,8 @@ class GraphViewModel { constructor(server, repoPath) { this._markIdeologicalStamp = 0; this.repoPath = repoPath; - this.graphSkip = 0; + this.limit = ko.observable(numberOfNodesPerLoad); + this.skip = ko.observable(0); this.server = server; this.currentRemote = ko.observable(); this.nodes = ko.observableArray(); @@ -35,6 +36,11 @@ class GraphViewModel { this.showCommitNode = ko.observable(false); this.currentActionContext = ko.observable(); this.edgesById = {}; + this.loadAhead = _.debounce(() => { + if (this.skip() <= 0) return; + this.skip(Math.max(this.skip() - numberOfNodesPerLoad, 0)); + this.loadNodesFromApi(); + }, 500, true); this.commitOpacity = ko.observable(1.0); this.highestBranchOrder = 0; this.hoverGraphActionGraphic = ko.observable(); @@ -57,8 +63,8 @@ class GraphViewModel { this.graphHeight = ko.observable(800); this.searchIcon = octicons.search.toSVG({ 'height': 18 }); this.plusIcon = octicons.plus.toSVG({ 'height': 18 }); - this.isLoadNodesRunning = false; this.loadNodesFromApiThrottled(); + this.updateBranchesThrottled(); } updateNode(parentElement) { @@ -85,60 +91,39 @@ class GraphViewModel { return refViewModel; } - loadNodesFromApi(isRefresh) { - const skip = isRefresh ? 0 : this.graphSkip; - const limit = isRefresh && this.graphSkip > 0 ? this.graphSkip : parseInt(ungit.config.numberOfNodesPerLoad); - + loadNodesFromApi() { const nodeSize = this.nodes().length; - return this.server.getPromise('/gitlog', { path: this.repoPath(), skip: skip, limit: limit }) - .then(logs => { - logs = logs || []; - // get or update each commit nodes. - logs.forEach(log => this.getNode(log.sha1, log)); - // sort in commit order - const allNodes = Object.values(this.nodesById) - .filter(node => node.timestamp) // some nodes are created by ref without info - .sort((a, b) => { - if (a.timestamp < b.timestamp) { - return 1; - } else if (a.timestamp > b.timestamp) { - return -1; - } - return 0; + return this.server.getPromise('/gitlog', { path: this.repoPath(), limit: this.limit(), skip: this.skip() }) + .then(log => { + // set new limit and skip + this.limit(parseInt(log.limit)); + this.skip(parseInt(log.skip)); + return log.nodes || []; + }).then(nodes => // create and/or calculate nodes + this.computeNode(nodes.map((logEntry) => { + return this.getNode(logEntry.sha1, logEntry); // convert to node object + }))).then(nodes => { + // create edges + const edges = []; + nodes.forEach(node => { + node.parents().forEach(parentSha1 => { + edges.push(this.getEdge(node.sha1, parentSha1)); + }); + node.render(); }); - // reset parent child relationship for each - let prevNode = null; - allNodes.forEach(node => { - node.setParent(prevNode); - prevNode = node; - }); - - const nodes = this.computeNode(allNodes); - let maxHeight = 0; - - // create edges and calculate max height - nodes.forEach(node => { - if (node.cy() > maxHeight) { - maxHeight = node.cy() + this.edges(edges); + this.nodes(nodes); + if (nodes.length > 0) { + this.graphHeight(nodes[nodes.length - 1].cy() + 80); } - node.parents().forEach(parentSha1 => { - this.getEdge(node.sha1, parentSha1); - }); - }); - - this.graphHeight(maxHeight + 80); - this.graphWidth(1000 + (this.highestBranchOrder * 90)); - programEvents.dispatch({ event: 'init-tooltip' }); - - if (!isRefresh) { - this.graphSkip += parseInt(ungit.config.numberOfNodesPerLoad) - } - }).catch((e) => this.server.unhandledRejection(e)) + this.graphWidth(1000 + (this.highestBranchOrder * 90)); + programEvents.dispatch({ event: 'init-tooltip' }); + }).catch((e) => this.server.unhandledRejection(e)) .finally(() => { if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { - this.loadNodesFromApiThrottled(); + this.scrolledToEnd(); } }); } @@ -154,42 +139,46 @@ class GraphViewModel { computeNode(nodes) { nodes = nodes || this.nodes(); - this.markNodesIdeologicalBranches(this.refs(), nodes, this.nodesById); + this.markNodesIdeologicalBranches(this.refs()); const updateTimeStamp = moment().valueOf(); if (this.HEAD()) { - if (this.highestBranchOrder == 0) { - this.highestBranchOrder = 1; - } this.traverseNodeLeftParents(this.HEAD(), node => { node.ancestorOfHEADTimeStamp = updateTimeStamp; }); } // Filter out nodes which doesn't have a branch (staging and orphaned nodes) - const nodesWithRefs = nodes.filter(node => (node.ideologicalBranch() && !node.ideologicalBranch().isStash) || node.ancestorOfHEADTimeStamp == updateTimeStamp); + nodes = nodes.filter(node => (node.ideologicalBranch() && !node.ideologicalBranch().isStash) || node.ancestorOfHEADTimeStamp == updateTimeStamp); + + let branchSlotCounter = this.HEAD() ? 1 : 0; // Then iterate from the bottom to fix the orders of the branches - for (let i = nodesWithRefs.length - 1; i >= 0; i--) { - const node = nodesWithRefs[i]; + for (let i = nodes.length - 1; i >= 0; i--) { + const node = nodes[i]; if (node.ancestorOfHEADTimeStamp == updateTimeStamp) continue; const ideologicalBranch = node.ideologicalBranch(); // First occurrence of the branch, find an empty slot for the branch - if (!ideologicalBranch.branchOrder) { - ideologicalBranch.branchOrder = this.highestBranchOrder++; + if (ideologicalBranch.lastSlottedTimeStamp != updateTimeStamp) { + ideologicalBranch.lastSlottedTimeStamp = updateTimeStamp; + ideologicalBranch.branchOrder = branchSlotCounter++; } node.branchOrder(ideologicalBranch.branchOrder); } + this.highestBranchOrder = branchSlotCounter - 1; + let prevNode; nodes.forEach(node => { node.ancestorOfHEAD(node.ancestorOfHEADTimeStamp == updateTimeStamp); if (node.ancestorOfHEAD()) node.branchOrder(0); - node.render(); + node.aboveNode = prevNode; + if (prevNode) prevNode.belowNode = node; + prevNode = node; }); - return this.nodes(); + return nodes; } getEdge(nodeAsha1, nodeBsha1) { @@ -197,7 +186,6 @@ class GraphViewModel { let edge = this.edgesById[id]; if (!edge) { edge = this.edgesById[id] = new EdgeViewModel(this, nodeAsha1, nodeBsha1); - this.edges.push(edge); } return edge; } @@ -219,8 +207,8 @@ class GraphViewModel { if (!a.isHEAD && b.isHEAD) return -1; if (a.isStash && !b.isStash) return 1; if (b.isStash && !a.isStash) return -1; - if (a.node() && a.node().timestamp && b.node() && b.node().timestamp) - return a.node().timestamp - b.node().timestamp; + if (a.node() && a.node().date && b.node() && b.node().date) + return a.node().date - b.node().date; return a.refName < b.refName ? -1 : 1; }); const stamp = this._markIdeologicalStamp++; @@ -263,10 +251,10 @@ class GraphViewModel { onProgramEvent(event) { if (event.event == 'git-directory-changed') { - this.loadNodesFromApiThrottled(true); + this.loadNodesFromApiThrottled(); this.updateBranchesThrottled(); } else if (event.event == 'request-app-content-refresh') { - this.loadNodesFromApiThrottled(true); + this.loadNodesFromApiThrottled(); } else if (event.event == 'remote-tags-update') { this.setRemoteTags(event.tags); } else if (event.event == 'current-remote-changed') { @@ -321,4 +309,4 @@ class GraphViewModel { this.HEADref.node(toNode); } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7656ec9b1..6e8a304f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ungit", - "version": "1.5.3", + "version": "1.5.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7569e97c5..9a8af0f8a 100644 --- a/package.json +++ b/package.json @@ -110,4 +110,4 @@ "min_width": 400, "min_height": 200 } -} \ No newline at end of file +} diff --git a/source/git-api.js b/source/git-api.js index fe76538e0..4ef97945d 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -37,10 +37,10 @@ exports.registerApi = (env) => { fs.readFileAsync(path.join(socket.watcherPath, ".gitignore")) .then((ignoreContent) => socket.ignore = ignore().add(ignoreContent.toString())) - .catch(() => {}) + .catch(() => { }) .then(() => { socket.watcher = []; - return watchPath(socket, '.', {'recursive': true}); + return watchPath(socket, '.', { 'recursive': true }); }).then(() => { if (!isMac && !isWindows) { // recursive fs.watch only works on mac and windows @@ -60,29 +60,32 @@ exports.registerApi = (env) => { const pathToWatch = path.join(socket.watcherPath, subfolderPath); winston.info(`Start watching ${pathToWatch}`); return fs.isExists(pathToWatch).then((isExists) => { - // Sometimes necessary folders, '.../.git/refs/heads' and etc, are not created on git init - if (!isExists) { - winston.debug(`intended folder to watch doesn't exists, creating: ${pathToWatch}`); - return new Bluebird((resolve, reject) => { - mkdirp(pathToWatch, (err) => { - if (err) reject(err); - else resolve(); - }); + // Sometimes necessary folders, '.../.git/refs/heads' and etc, are not created on git init + if (!isExists) { + winston.debug(`intended folder to watch doesn't exists, creating: ${pathToWatch}`); + return new Bluebird((resolve, reject) => { + mkdirp(pathToWatch, (err) => { + if (err) reject(err); + else resolve(); }); - } - }).then(() => { - const watcher = fs.watch(pathToWatch, options || {}, (event, filename) => { - if (event === 'rename' || !filename) return; - const filePath = path.join(subfolderPath, filename); - winston.debug(`File change: ${filePath}`); - if (isFileWatched(filePath, socket.ignore)) { - winston.info(`${filePath} triggered refresh for ${socket.watcherPath}`); - emitGitDirectoryChanged(socket.watcherPath); - emitWorkingTreeChanged(socket.watcherPath); - } }); - socket.watcher.push(watcher); + } + }).then(() => { + const watcher = fs.watch(pathToWatch, options || {}, (event, filename) => { + if (event === 'rename' || !filename) return; + const filePath = path.join(subfolderPath, filename); + winston.debug(`File change: ${filePath}`); + if (isFileWatched(filePath, socket.ignore)) { + winston.info(`${filePath} triggered refresh for ${socket.watcherPath}`); + emitGitDirectoryChanged(socket.watcherPath); + emitWorkingTreeChanged(socket.watcherPath); + } + }); + watcher.on('error', (err) => { + winston.warn(`Error watching ${pathToWatch}: `, JSON.stringify(err)); }); + socket.watcher.push(watcher); + }); }; const stopDirectoryWatch = (socket) => { @@ -155,11 +158,11 @@ exports.registerApi = (env) => { const jsonResultOrFailProm = (res, promise) => { return promise.then((result) => { - res.json(result || {}); - }).catch((err) => { - winston.warn('Responding with ERROR: ', JSON.stringify(err)); - res.status(400).json(err); - }); + res.json(result || {}); + }).catch((err) => { + winston.warn('Responding with ERROR: ', JSON.stringify(err)); + res.status(400).json(err); + }); } const credentialsOption = (socketId, remote) => { @@ -176,7 +179,7 @@ exports.registerApi = (env) => { if (finalValue || finalValue === 0) { return finalValue; } else { - throw { error: "invalid number"}; + throw { error: "invalid number" }; } } @@ -219,10 +222,10 @@ exports.registerApi = (env) => { const task = gitPromise({ commands: credentialsOption(req.body.socketId, req.body.remote).concat([ - 'fetch', - req.body.remote, - req.body.ref ? req.body.ref : '', - config.autoPruneOnFetch ? '--prune' : '']), + 'fetch', + req.body.remote, + req.body.ref ? req.body.ref : '', + config.autoPruneOnFetch ? '--prune' : '']), repoPath: req.body.path, timeout: tenMinTimeoutMs }); @@ -236,10 +239,10 @@ exports.registerApi = (env) => { if (res.setTimeout) res.setTimeout(tenMinTimeoutMs); const task = gitPromise({ commands: credentialsOption(req.body.socketId, req.body.remote).concat([ - 'push', - req.body.remote, - (req.body.refSpec ? req.body.refSpec : 'HEAD') + (req.body.remoteBranch ? `:${req.body.remoteBranch}` : ''), - (req.body.force ? '-f' : '')]), + 'push', + req.body.remote, + (req.body.refSpec ? req.body.refSpec : 'HEAD') + (req.body.remoteBranch ? `:${req.body.remoteBranch}` : ''), + (req.body.force ? '-f' : '')]), repoPath: req.body.path, timeout: tenMinTimeoutMs }); @@ -278,7 +281,7 @@ exports.registerApi = (env) => { const gitIgnoreFile = `${currentPath}/.gitignore`; const ignoreFile = req.body.file.trim(); const task = fs.appendFileAsync(gitIgnoreFile, os.EOL + ignoreFile) - .catch((err) => { throw { errorCode: 'error-appending-ignore', error: 'Error while appending to .gitignore file.' }}); + .catch((err) => { throw { errorCode: 'error-appending-ignore', error: 'Error while appending to .gitignore file.' } }); jsonResultOrFailProm(res, task) .finally(emitWorkingTreeChanged.bind(null, req.body.path)); @@ -305,18 +308,16 @@ exports.registerApi = (env) => { }); app.get(`${exports.pathPrefix}/gitlog`, ensureAuthenticated, ensurePathExists, (req, res) => { + const limit = getNumber(req.query.limit, config.numberOfNodesPerLoad || 25); const skip = getNumber(req.query.skip, 0); - const limit = getNumber(req.query.limit, parseInt(config.numberOfNodesPerLoad)); - const isLookForHead = skip === 0 && limit === config.numberOfNodesPerLoad; - - const task = gitPromise.log(req.query.path, skip, limit, isLookForHead, config.maxActiveBranchSearchIteration) + const task = gitPromise.log(req.query.path, limit, skip, config.maxActiveBranchSearchIteration) .catch((err) => { if (err.stderr && err.stderr.indexOf('fatal: bad default revision \'HEAD\'') == 0) { - return []; + return { "limit": limit, "skip": skip, "nodes": [] }; } else if (/fatal: your current branch \'.+\' does not have any commits yet.*/.test(err.stderr)) { - return []; + return { "limit": limit, "skip": skip, "nodes": [] }; } else if (err.stderr && err.stderr.indexOf('fatal: Not a git repository') == 0) { - return []; + return { "limit": limit, "skip": skip, "nodes": [] }; } else { throw err; } @@ -449,7 +450,7 @@ exports.registerApi = (env) => { app.delete(`${exports.pathPrefix}/remote/tags`, ensureAuthenticated, ensurePathExists, (req, res) => { const commands = credentialsOption(req.query.socketId, req.query.remote).concat(['push', req.query.remote, `:refs/tags/${req.query.name.trim()}`]); const task = gitPromise(['tag', '-d', req.query.name.trim()], req.query.path) - .catch(() => {}) // might have already deleted, so ignoring error + .catch(() => { }) // might have already deleted, so ignoring error .then(() => gitPromise(commands, req.query.path)) jsonResultOrFailProm(res, task) @@ -545,7 +546,7 @@ exports.registerApi = (env) => { }); app.post(`${exports.pathPrefix}/launchmergetool`, ensureAuthenticated, ensurePathExists, (req, res) => { - const commands = ['mergetool', ...(typeof req.body.tool === 'string'? ['--tool ', req.body.tool]: []), '--no-prompt', req.body.file]; + const commands = ['mergetool', ...(typeof req.body.tool === 'string' ? ['--tool ', req.body.tool] : []), '--no-prompt', req.body.file]; gitPromise(commands, req.body.path); // Send immediate response, this is because merging may take a long time // and there is no need to wait for it to finish. @@ -570,7 +571,7 @@ exports.registerApi = (env) => { const task = fs.isExists(filename).then((exists) => { if (exists) { - return fs.readFileAsync(filename, {encoding: 'utf8'}) + return fs.readFileAsync(filename, { encoding: 'utf8' }) .catch(() => { return {} }) .then(gitParser.parseGitSubmodule); } else { @@ -618,7 +619,7 @@ exports.registerApi = (env) => { }); app.post(`${exports.pathPrefix}/stashes`, ensureAuthenticated, ensurePathExists, (req, res) => { - jsonResultOrFailProm(res, gitPromise(['stash', 'save', '--include-untracked', req.body.message || '' ], req.body.path)) + jsonResultOrFailProm(res, gitPromise(['stash', 'save', '--include-untracked', req.body.message || ''], req.body.path)) .finally(emitGitDirectoryChanged.bind(null, req.body.path)) .finally(emitWorkingTreeChanged.bind(null, req.body.path)); }); @@ -653,7 +654,7 @@ exports.registerApi = (env) => { res.status(400).json({ errorCode: 'socket-unavailable' }); } else { socket.once('credentials', (data) => res.json(data)); - socket.emit('request-credentials', {remote: remote}); + socket.emit('request-credentials', { remote: remote }); } }); @@ -682,7 +683,7 @@ exports.registerApi = (env) => { }); app.put(`${exports.pathPrefix}/gitignore`, ensureAuthenticated, ensurePathExists, (req, res) => { if (!req.body.data && req.body.data !== '') { - return res.status(400).json({ message: 'Invalid .gitignore content'}); + return res.status(400).json({ message: 'Invalid .gitignore content' }); } fs.writeFileAsync(path.join(req.body.path, '.gitignore'), req.body.data) .then(() => res.status(200).json({})) @@ -702,7 +703,7 @@ exports.registerApi = (env) => { const content = req.body.content ? req.body.content : (`test content\n${Math.random()}\n`); fs.writeFile(req.body.file, content, () => res.json({})); }); - app.post(`${exports.pathPrefix}/testing/createimagefile`, ensureAuthenticated, (req, res) => { + app.post(`${exports.pathPrefix}/testing/createimagefile`, ensureAuthenticated, (req, res) => { fs.writeFile(req.body.file, 'png', { encoding: 'binary' }, () => res.json({})); }); app.post(`${exports.pathPrefix}/testing/changeimagefile`, ensureAuthenticated, (req, res) => { @@ -721,4 +722,4 @@ exports.registerApi = (env) => { }); }); } -}; +}; \ No newline at end of file diff --git a/source/git-promise.js b/source/git-promise.js index 9b4c1ec45..49d88a61f 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -442,19 +442,25 @@ git.revParse = (repoPath) => { }).catch((err) => ({ type: 'uninited', gitRootPath: path.normalize(repoPath) })); } -git.log = (path, skip, limit, lookForHead, maxActiveBranchSearchIteration) => { +git.log = (path, limit, skip, maxActiveBranchSearchIteration) => { return git(['log', '--cc', '--decorate=full', '--show-signature', '--date=default', '--pretty=fuller', '-z', '--branches', '--tags', '--remotes', '--parents', '--no-notes', '--numstat', '--date-order', `--max-count=${limit}`, `--skip=${skip}`], path) .then(gitParser.parseGitLog) .then((log) => { log = log ? log : []; - skip = skip + log.length; - if (lookForHead && maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) { - return git.log(path, skip, maxActiveBranchSearchIteration - 1) - .then(innerLog => log.concat(innerLog)); + if (maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) { + return git.log(path, config.numberOfNodesPerLoad + limit, config.numberOfNodesPerLoad + skip, maxActiveBranchSearchIteration - 1) + .then(innerLog => { + return { + "limit": limit + (innerLog.isHeadExist ? 0 : config.numberOfNodesPerLoad), + "skip": skip + (innerLog.isHeadExist ? 0 : config.numberOfNodesPerLoad), + "nodes": log.concat(innerLog.nodes), + "isHeadExist": innerLog.isHeadExist + } + }); } else { - return log; + return { "limit": limit, "skip": skip, "nodes": log, "isHeadExist": log.isHeadExist }; } }); } -module.exports = git; +module.exports = git; \ No newline at end of file From f7bc29e79a8190121a978c817de453c90f2ffb32 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Mon, 10 Feb 2020 22:09:29 -0800 Subject: [PATCH 15/18] Missed few --- components/graph/graph-graphics.html | 5 ++++- components/graph/graph.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/components/graph/graph-graphics.html b/components/graph/graph-graphics.html index f85ee95a1..e99d4b963 100644 --- a/components/graph/graph-graphics.html +++ b/components/graph/graph-graphics.html @@ -18,9 +18,12 @@ - + + + + diff --git a/components/graph/graph.js b/components/graph/graph.js index 00b59e8e3..70247c96e 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -207,8 +207,8 @@ class GraphViewModel { if (!a.isHEAD && b.isHEAD) return -1; if (a.isStash && !b.isStash) return 1; if (b.isStash && !a.isStash) return -1; - if (a.node() && a.node().date && b.node() && b.node().date) - return a.node().date - b.node().date; + if (a.node() && a.node().timestamp && b.node() && b.node().timestamp) + return a.node().timestamp - b.node().timestamp; return a.refName < b.refName ? -1 : 1; }); const stamp = this._markIdeologicalStamp++; From a86006c721fcd94a95289dbdc5f079b9376b6acf Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 12 Feb 2020 20:49:46 -0800 Subject: [PATCH 16/18] fix tests --- test/spec.git-parser.js | 139 +++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 66 deletions(-) diff --git a/test/spec.git-parser.js b/test/spec.git-parser.js index 6d843d397..cbf9ff099 100644 --- a/test/spec.git-parser.js +++ b/test/spec.git-parser.js @@ -230,6 +230,7 @@ describe('git-parser parseGitLog', () => { authorEmail: "test@example.com", authorName: "Test ungit", commitDate: "Fri Jan 4 14:54:06 2019 +0100", + timestamp: 1546610046000, committerEmail: "test@example.com", committerName: "Test ungit", total: { @@ -270,6 +271,7 @@ describe('git-parser parseGitLog', () => { authorEmail: "test@example.com", authorName: "Test ungit", commitDate: "Fri Jan 4 14:03:56 2019 +0100", + timestamp: 1546607036000, committerEmail: "test@example.com", committerName: "Test ungit", total: { @@ -300,6 +302,7 @@ describe('git-parser parseGitLog', () => { authorEmail: "test@example.com", authorName: "Test ungit", commitDate: "Fri Jan 4 14:02:56 2019 +0100", + timestamp: 1546606976000, committerEmail: "test@example.com", committerName: "Test ungit", total: { @@ -321,6 +324,7 @@ describe('git-parser parseGitLog', () => { authorEmail: "test@example.com", authorName: "Test ungit", commitDate: "Fri Jan 4 14:01:56 2019 +0100", + timestamp: 1546606916000, committerEmail: "test@example.com", committerName: "Test ungit", total: { @@ -390,6 +394,7 @@ describe('git-parser parseGitLog', () => { authorEmail: "test@example.com", authorName: "Test ungit", commitDate: "Fri Jan 4 14:03:56 2019 +0100", + timestamp: 1546607036000, committerEmail: "test@example.com", committerName: "Test ungit", total: { @@ -440,6 +445,7 @@ describe('git-parser parseGitLog', () => { authorEmail: "test@example.com", authorName: "Test ungit", commitDate: "Fri Jan 4 14:03:56 2019 +0100", + timestamp: 1546607036000, committerEmail: "test@example.com", committerName: "Test ungit", total: { @@ -548,7 +554,7 @@ describe('git-parser parseGitLog', () => { expect(gitParser.parseGitLog(gitLog)[0]).to.eql( { - refs: [ 'HEAD', 'refs/heads/git-parser-specs' ], + refs: ['HEAD', 'refs/heads/git-parser-specs'], total: { "additions": 32, "deletions": 0 @@ -564,7 +570,7 @@ describe('git-parser parseGitLog', () => { } ], sha1: '37d1154434b70854ed243967e0d7e37aa3564551', - parents: [ 'd58c8e117fc257520d90b099fd2c6acd7c1e8861' ], + parents: ['d58c8e117fc257520d90b099fd2c6acd7c1e8861'], isHead: true, authorName: 'Test ungit', authorEmail: 'test@example.com', @@ -572,6 +578,7 @@ describe('git-parser parseGitLog', () => { committerName: 'Test ungit', committerEmail: 'test@example.com', commitDate: 'Fri Jan 4 14:03:56 2019 +0100', + timestamp: 1546607036000, message: 'submodules parser', } ); @@ -726,9 +733,9 @@ describe('parseGitBranches', () => { ` expect(gitParser.parseGitBranches(gitBranches)).to.eql([ - {"name":"dev", "current": true}, - {"name":"master"}, - {"name":"testbuild"} + { "name": "dev", "current": true }, + { "name": "master" }, + { "name": "testbuild" } ]); }); }); @@ -774,9 +781,9 @@ describe('parseGitLsRemote', () => { expect(gitParser.parseGitLsRemote(gitLsRemote)).to.eql([ { sha1: "86bec6415fa7ec0d7550a62389de86adb493d546", name: "refs/tags/0.1.0" }, - { sha1: "668ab7beae996c5a7b36da0be64b98e45ba2aa0b", name: "refs/tags/0.1.0^{}"}, - { sha1: "d3ec9678acf285637ef11c7cba897d697820de07", name: "refs/tags/0.1.1"}, - { sha1: "ad00b6c8b7b0cbdd0bd92d44dece559b874a4ae6", name: "refs/tags/0.1.1^{}"} + { sha1: "668ab7beae996c5a7b36da0be64b98e45ba2aa0b", name: "refs/tags/0.1.0^{}" }, + { sha1: "d3ec9678acf285637ef11c7cba897d697820de07", name: "refs/tags/0.1.1" }, + { sha1: "ad00b6c8b7b0cbdd0bd92d44dece559b874a4ae6", name: "refs/tags/0.1.1^{}" } ]); }); }); @@ -810,67 +817,67 @@ describe('parseGitStatusNumstat', () => { describe('parseGitStatus', () => { it('parses git status', () => { const gitStatus = `## git-parser-specs\x00` + - `A file1.js\x00` + - `M file2.js\x00` + - `D file3.js\x00` + - ` D file4.js\x00` + - ` U file5.js\x00` + - `U file6.js\x00` + - `AA file7.js\x00` + - `? file8.js\x00` + - `A file9.js\x00` + - `?D file10.js\x00` + - `AD file11.js\x00` + - ` M file12.js\x00` + - `?? file13.js\x00` + - `R ../source/sys.js\x00../source/sysinfo.js\x00` + `A file1.js\x00` + + `M file2.js\x00` + + `D file3.js\x00` + + ` D file4.js\x00` + + ` U file5.js\x00` + + `U file6.js\x00` + + `AA file7.js\x00` + + `? file8.js\x00` + + `A file9.js\x00` + + `?D file10.js\x00` + + `AD file11.js\x00` + + ` M file12.js\x00` + + `?? file13.js\x00` + + `R ../source/sys.js\x00../source/sysinfo.js\x00` expect(gitParser.parseGitStatus(gitStatus)).to.eql({ - branch: "git-parser-specs", - files: { - "../source/sys.js": { - conflict: false, displayName: "../source/sysinfo.js → ../source/sys.js", fileName: "../source/sys.js", oldFileName: "../source/sysinfo.js", isNew: false, removed: false, renamed: true, staged: false, type: "text" - }, - "file1.js": { - conflict: false, displayName: "file1.js", fileName: "file1.js", oldFileName: "file1.js", isNew: true, removed: false, renamed: false, staged: true, type: "text" - }, - "file2.js": { - conflict: false, displayName: "file2.js", fileName: "file2.js", oldFileName: "file2.js", isNew: false, removed: false, renamed: false, staged: true, type: "text" - }, - "file3.js": { - conflict: false, displayName: "file3.js", fileName: "file3.js", oldFileName: "file3.js", isNew: false, removed: true, renamed: false, staged: false, type: "text" - }, - "file4.js": { - conflict: false, displayName: "file4.js", fileName: "file4.js", oldFileName: "file4.js", isNew: false, removed: true, renamed: false, staged: false, type: "text" - }, - "file5.js": { - conflict: true, displayName: "file5.js", fileName: "file5.js", oldFileName: "file5.js", isNew: false, removed: false, renamed: false, staged: false, type: "text" - }, - "file6.js": { - conflict: true, displayName: "file6.js", fileName: "file6.js", oldFileName: "file6.js", isNew: false, removed: false, renamed: false, staged: false, type: "text" - }, - "file7.js": { - conflict: true, displayName: "file7.js", fileName: "file7.js", oldFileName: "file7.js", isNew: true, removed: false, renamed: false, staged: true, type: "text" - }, - "file8.js": { - conflict: false, displayName: "file8.js", fileName: "file8.js", oldFileName: "file8.js", isNew: true, removed: false, renamed: false, staged: false, type: "text" - }, - "file9.js": { - conflict: false, displayName: "file9.js", fileName: "file9.js", oldFileName: "file9.js", isNew: true, removed: false, renamed: false, staged: true, type: "text" - }, - "file10.js": { - conflict: false, displayName: "file10.js", fileName: "file10.js", oldFileName: "file10.js", isNew: false, removed: true, renamed: false, staged: false, type: "text" - }, - "file11.js": { - conflict: false, displayName: "file11.js", fileName: "file11.js", oldFileName: "file11.js", isNew: false, removed: true, renamed: false, staged: true, type: "text" - }, - "file12.js": { - conflict: false, displayName: "file12.js", fileName: "file12.js", oldFileName: "file12.js", isNew: false, removed: false, renamed: false, staged: false, type: "text" - }, - "file13.js": { - conflict: false, displayName: "file13.js", fileName: "file13.js", oldFileName: "file13.js", isNew: true, removed: false, renamed: false, staged: false, type: "text" - } + branch: "git-parser-specs", + files: { + "../source/sys.js": { + conflict: false, displayName: "../source/sysinfo.js → ../source/sys.js", fileName: "../source/sys.js", oldFileName: "../source/sysinfo.js", isNew: false, removed: false, renamed: true, staged: false, type: "text" + }, + "file1.js": { + conflict: false, displayName: "file1.js", fileName: "file1.js", oldFileName: "file1.js", isNew: true, removed: false, renamed: false, staged: true, type: "text" + }, + "file2.js": { + conflict: false, displayName: "file2.js", fileName: "file2.js", oldFileName: "file2.js", isNew: false, removed: false, renamed: false, staged: true, type: "text" + }, + "file3.js": { + conflict: false, displayName: "file3.js", fileName: "file3.js", oldFileName: "file3.js", isNew: false, removed: true, renamed: false, staged: false, type: "text" + }, + "file4.js": { + conflict: false, displayName: "file4.js", fileName: "file4.js", oldFileName: "file4.js", isNew: false, removed: true, renamed: false, staged: false, type: "text" + }, + "file5.js": { + conflict: true, displayName: "file5.js", fileName: "file5.js", oldFileName: "file5.js", isNew: false, removed: false, renamed: false, staged: false, type: "text" + }, + "file6.js": { + conflict: true, displayName: "file6.js", fileName: "file6.js", oldFileName: "file6.js", isNew: false, removed: false, renamed: false, staged: false, type: "text" + }, + "file7.js": { + conflict: true, displayName: "file7.js", fileName: "file7.js", oldFileName: "file7.js", isNew: true, removed: false, renamed: false, staged: true, type: "text" }, + "file8.js": { + conflict: false, displayName: "file8.js", fileName: "file8.js", oldFileName: "file8.js", isNew: true, removed: false, renamed: false, staged: false, type: "text" + }, + "file9.js": { + conflict: false, displayName: "file9.js", fileName: "file9.js", oldFileName: "file9.js", isNew: true, removed: false, renamed: false, staged: true, type: "text" + }, + "file10.js": { + conflict: false, displayName: "file10.js", fileName: "file10.js", oldFileName: "file10.js", isNew: false, removed: true, renamed: false, staged: false, type: "text" + }, + "file11.js": { + conflict: false, displayName: "file11.js", fileName: "file11.js", oldFileName: "file11.js", isNew: false, removed: true, renamed: false, staged: true, type: "text" + }, + "file12.js": { + conflict: false, displayName: "file12.js", fileName: "file12.js", oldFileName: "file12.js", isNew: false, removed: false, renamed: false, staged: false, type: "text" + }, + "file13.js": { + conflict: false, displayName: "file13.js", fileName: "file13.js", oldFileName: "file13.js", isNew: true, removed: false, renamed: false, staged: false, type: "text" + } + }, inited: true, isMoreToLoad: false }) From daa4b1a2358b74a68926205fbeeb96f54e54395e Mon Sep 17 00:00:00 2001 From: JK Kim Date: Thu, 13 Feb 2020 21:30:32 -0800 Subject: [PATCH 17/18] fix scrolling issue --- components/graph/graph.html | 2 +- components/graph/graph.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/components/graph/graph.html b/components/graph/graph.html index 4f374feaf..513db91f6 100644 --- a/components/graph/graph.html +++ b/components/graph/graph.html @@ -1,5 +1,5 @@ -
+
diff --git a/components/graph/graph.js b/components/graph/graph.js index 70247c96e..b8395085c 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -36,6 +36,10 @@ class GraphViewModel { this.showCommitNode = ko.observable(false); this.currentActionContext = ko.observable(); this.edgesById = {}; + this.scrolledToEnd = _.debounce(() => { + this.limit(numberOfNodesPerLoad + this.limit()); + this.loadNodesFromApiThrottled(); + }, 500, true); this.loadAhead = _.debounce(() => { if (this.skip() <= 0) return; this.skip(Math.max(this.skip() - numberOfNodesPerLoad, 0)); From 71c080cf48f18c946d3f720cc40bb97bb12dfb5a Mon Sep 17 00:00:00 2001 From: jk-kim Date: Tue, 25 Feb 2020 14:32:13 -0800 Subject: [PATCH 18/18] Update components/graph/graph.js Co-Authored-By: campersau --- components/graph/graph.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index b8395085c..0dfb60ec7 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -43,7 +43,7 @@ class GraphViewModel { this.loadAhead = _.debounce(() => { if (this.skip() <= 0) return; this.skip(Math.max(this.skip() - numberOfNodesPerLoad, 0)); - this.loadNodesFromApi(); + this.loadNodesFromApiThrottled(); }, 500, true); this.commitOpacity = ko.observable(1.0); this.highestBranchOrder = 0; @@ -313,4 +313,4 @@ class GraphViewModel { this.HEADref.node(toNode); } } -} \ No newline at end of file +}