From 9c4d5cf9bdf6a2bcd19f4dbf60304ba22a63e0f3 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Tue, 4 Feb 2020 21:14:26 -0800 Subject: [PATCH 01/19] 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 247282549..ce05bc983 100644 --- a/components/graph/git-node.js +++ b/components/graph/git-node.js @@ -141,7 +141,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 e524f7586bd01172489ae2aa9a800240500b3b8b Mon Sep 17 00:00:00 2001 From: JK Kim Date: Tue, 4 Feb 2020 21:42:06 -0800 Subject: [PATCH 02/19] Prevent multi execution for `loadNodesFromApi` --- components/graph/graph.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index 39ff81d07..7ca8189e3 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -76,12 +76,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) { @@ -109,8 +109,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) => { @@ -150,6 +152,7 @@ class GraphViewModel { if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { this.scrolledToEnd(); } + this.isLoadNodesRunning = false; }); } From 1f73ad0167b8170c416fb8490d5d2ccbb44d0c65 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 5 Feb 2020 08:02:38 -0800 Subject: [PATCH 03/19] Making git node to be incremental --- components/graph/graph-graphics.html | 11 +------ components/graph/graph.js | 43 +++++++++++----------------- components/graph/graph.less | 5 ---- source/config.js | 4 +-- source/git-api.js | 14 +++++---- source/git-promise.js | 31 ++++++++------------ 6 files changed, 40 insertions(+), 68 deletions(-) diff --git a/components/graph/graph-graphics.html b/components/graph/graph-graphics.html index 4b823e15b..d8249873a 100644 --- a/components/graph/graph-graphics.html +++ b/components/graph/graph-graphics.html @@ -34,7 +34,7 @@ - - - diff --git a/components/graph/graph.js b/components/graph/graph.js index 7ca8189e3..dc0a6b310 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -14,7 +14,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(); @@ -39,16 +38,6 @@ class GraphViewModel { 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, @@ -114,34 +103,32 @@ class GraphViewModel { const nodeSize = this.nodes().length; return this.server - .getPromise('/gitlog', { path: this.repoPath(), limit: this.limit(), skip: this.skip() }) + .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( + .then((nodes) => { + // create and/or calculate nodes + return this.computeNode( nodes.map((logEntry) => { - return this.getNode(logEntry.sha1, logEntry); // convert to node object + return this.getNode(logEntry.sha1, logEntry); }) - ) - ) + ); + }) .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); } @@ -201,16 +188,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 42e78c372..0b4477453 100644 --- a/components/graph/graph.less +++ b/components/graph/graph.less @@ -7,11 +7,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 7c763599e..43e0952ec 100644 --- a/source/config.js +++ b/source/config.js @@ -125,8 +125,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 ad175d873..c77a84525 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -388,19 +388,23 @@ 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) + .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 e7ad51a89..b451806af 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -591,7 +591,7 @@ git.revParse = (repoPath) => { .catch((err) => ({ type: 'uninited', gitRootPath: path.normalize(repoPath) })); }; -git.log = (path, limit, skip, maxActiveBranchSearchIteration) => { +git.log = (path, skip, lookForHead, maxActiveBranchSearchIteration) => { return git( [ 'log', @@ -608,7 +608,7 @@ git.log = (path, limit, skip, maxActiveBranchSearchIteration) => { '--no-notes', '--numstat', '--date-order', - `--max-count=${limit}`, + `--max-count=${config.numberOfNodesPerLoad}`, `--skip=${skip}`, ], path @@ -616,24 +616,17 @@ git.log = (path, limit, skip, maxActiveBranchSearchIteration) => { .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 - ) - .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, - }; - }); + 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, + }; + }); } else { - return { limit: limit, skip: skip, nodes: log, isHeadExist: log.isHeadExist }; + return { skip: skip, nodes: log, isHeadExist: log.isHeadExist }; } }); }; From 62f12977a35cefd7b95cd71c1a115fd8775cf87f Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 5 Feb 2020 22:28:55 -0800 Subject: [PATCH 04/19] Fix branch ordering persistence issue --- components/graph/graph.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index dc0a6b310..f0221db3d 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -44,7 +44,7 @@ class GraphViewModel { true ); this.commitOpacity = ko.observable(1.0); - this.heighstBranchOrder = 0; + this.highestBranchOrder = 0; this.hoverGraphActionGraphic = ko.observable(); this.hoverGraphActionGraphic.subscribe( (value) => { @@ -115,11 +115,8 @@ class GraphViewModel { }) .then((nodes) => { // create and/or calculate nodes - return this.computeNode( - nodes.map((logEntry) => { - return this.getNode(logEntry.sha1, logEntry); - }) - ); + const nodeVMs = nodes.map((logEntry) => this.getNode(logEntry.sha1, logEntry)); + return this.computeNode(nodeVMs); }) .then((nodes) => { // create edges @@ -132,7 +129,8 @@ 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(() => { @@ -158,36 +156,36 @@ 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( + const nodesWithRefs = 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 = 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 da29ae5e01cf26a009865ccf4153106ea7a25f73 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 5 Feb 2020 22:49:37 -0800 Subject: [PATCH 05/19] Add debounce to node rendering --- components/graph/git-node.js | 61 +++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/components/graph/git-node.js b/components/graph/git-node.js index ce05bc983..3d09f853f 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 @@ -113,6 +114,38 @@ 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() { @@ -124,34 +157,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 23b8f7bf3c17295974781b974b289554957bc98c Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 5 Feb 2020 22:50:07 -0800 Subject: [PATCH 06/19] Allow setting skip and limit dynamically --- components/graph/graph.js | 37 +++++++++++++++++++------------------ source/git-api.js | 18 ++++++++---------- source/git-promise.js | 16 ++++++---------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index f0221db3d..9312f1df0 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -14,7 +14,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(); @@ -97,32 +97,34 @@ class GraphViewModel { return refViewModel; } - loadNodesFromApi() { + loadNodesFromApi(skip, limit) { if (this.isLoadNodesRunning) return; 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 || []; - }) + .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); }); }); @@ -131,6 +133,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(() => { @@ -186,13 +190,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); }); @@ -205,6 +205,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 c77a84525..e4086802b 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -389,22 +389,20 @@ exports.registerApi = (env) => { app.get(`${exports.pathPrefix}/gitlog`, ensureAuthenticated, ensurePathExists, (req, res) => { 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, - req.query.lookForHead === 'true', - config.maxActiveBranchSearchIteration - ) + .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) + /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 b451806af..39843a668 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -591,7 +591,7 @@ git.revParse = (repoPath) => { .catch((err) => ({ type: 'uninited', gitRootPath: path.normalize(repoPath) })); }; -git.log = (path, skip, lookForHead, maxActiveBranchSearchIteration) => { +git.log = (path, skip, limit, lookForHead, maxActiveBranchSearchIteration) => { return git( [ 'log', @@ -608,7 +608,7 @@ git.log = (path, skip, lookForHead, maxActiveBranchSearchIteration) => { '--no-notes', '--numstat', '--date-order', - `--max-count=${config.numberOfNodesPerLoad}`, + `--max-count=${limit}`, `--skip=${skip}`, ], path @@ -618,15 +618,11 @@ git.log = (path, skip, lookForHead, maxActiveBranchSearchIteration) => { 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) => { - return { - skip: skip, - nodes: log.concat(innerLog.nodes), - isHeadExist: innerLog.isHeadExist, - }; - }); + return git + .log(path, skip, maxActiveBranchSearchIteration - 1) + .then((innerLog) => log.concat(innerLog.nodes)); } else { - return { skip: skip, nodes: log, isHeadExist: log.isHeadExist }; + return log; } }); }; From a25654b3909ff9513c054ccaafdcc9c682dd11db Mon Sep 17 00:00:00 2001 From: JK Kim Date: Thu, 6 Feb 2020 21:06:35 -0800 Subject: [PATCH 07/19] 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 e4086802b..856319b68 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -70,9 +70,8 @@ exports.registerApi = (env) => { return mkdirp(pathToWatch); }) .then(() => { - const watcher = 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)) { @@ -81,9 +80,6 @@ exports.registerApi = (env) => { emitWorkingTreeChanged(socket.watcherPath); } }); - watcher.on('error', (err) => { - winston.warn(`Error watching ${pathToWatch}: `, JSON.stringify(err)); - }); socket.watcher.push(watcher); }); }; From 71b904e0613a2e6b39269c4ca8025925bd002f3e Mon Sep 17 00:00:00 2001 From: JK Kim Date: Thu, 6 Feb 2020 21:23:11 -0800 Subject: [PATCH 08/19] Setting up for node interweaving --- components/graph/graph.html | 2 +- components/graph/graph.js | 27 +++++++++------------------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/components/graph/graph.html b/components/graph/graph.html index f8416fad2..b8f601720 100644 --- a/components/graph/graph.html +++ b/components/graph/graph.html @@ -1,4 +1,4 @@ -
+
diff --git a/components/graph/graph.js b/components/graph/graph.js index 9312f1df0..f1c9290a1 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -36,13 +36,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(); @@ -70,7 +63,6 @@ class GraphViewModel { this.searchIcon = octicons.search.toSVG({ height: 18 }); this.plusIcon = octicons.plus.toSVG({ height: 18 }); this.isLoadNodesRunning = false; - this.loadNodesFromApi(); } updateNode(parentElement) { @@ -97,12 +89,12 @@ 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 @@ -139,9 +131,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; }); } @@ -265,10 +256,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 fe7bc2bd389ddfd01f1331b42547c339934c7811 Mon Sep 17 00:00:00 2001 From: Jung Kim Date: Fri, 7 Feb 2020 21:51:05 -0800 Subject: [PATCH 09/19] Making it work --- components/graph/git-graph-actions.js | 18 +++----- components/graph/git-node.js | 53 ++++++++++++----------- components/graph/git-ref.js | 5 ++- components/graph/graph.js | 61 +++++++++++++++++---------- source/git-parser.js | 1 + 5 files changed, 80 insertions(+), 58 deletions(-) diff --git a/components/graph/git-graph-actions.js b/components/graph/git-graph-actions.js index 99d303b81..71b7693b0 100644 --- a/components/graph/git-graph-actions.js +++ b/components/graph/git-graph-actions.js @@ -81,7 +81,7 @@ class Move extends ActionBase { class Reset extends ActionBase { constructor(graph, node) { - super(graph, 'Reset', 'reset', octicons.trashcan.toSVG({ height: 18 })); + super(graph, 'Reset', 'reset', octicons.trashcan.toSVG({ 'height': 18 })); this.node = node; this.visible = ko.computed(() => { if (this.isRunning()) return true; @@ -95,8 +95,7 @@ class Reset extends ActionBase { context && context.node() && remoteRef.node() != context.node() && - remoteRef.node().date < context.node().date - ); + remoteRef.node().timestamp < context.node().timestamp; }); } @@ -223,14 +222,11 @@ class Push extends ActionBase { if (remoteRef) { 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' })); + return ref.createRemoteRef().then(() => { + 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 3d09f853f..0aff73f73 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(); @@ -141,6 +142,12 @@ 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, @@ -157,11 +164,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); @@ -180,21 +192,12 @@ class GitNodeViewModel extends Animateable { showRefSearchForm(obj, event) { this.refSearchFormVisible(true); - const textBox = event.currentTarget.parentElement.querySelector('input[type="search"]'); - const $textBox = $(textBox); - - if (!$textBox.autocomplete('instance')) { - const renderItem = (ul, item) => $(`
  • ${item.displayHtml()}
  • `).appendTo(ul); - $textBox.autocomplete({ - classes: { - 'ui-autocomplete': 'dropdown-menu', - }, + const textBox = event.currentTarget.nextElementSibling.firstElementChild; // this may not be the best idea... + $(textBox) + .autocomplete({ source: this.refs().filter((ref) => !ref.isHEAD), minLength: 0, - create: (event) => { - $(event.target).data('ui-autocomplete')._renderItem = renderItem; - }, - select: (_event, ui) => { + select: (event, ui) => { const ref = ui.item; const ray = ref.isTag ? this.tagsToDisplay : this.branchesToDisplay; @@ -202,16 +205,18 @@ class GitNodeViewModel extends Animateable { ray.splice(ray.indexOf(ref), 1); ray.unshift(ref); this.refSearchFormVisible(false); - - // Clear search input on selection - return false; }, - }); - $textBox.focus((event) => { - $(event.target).autocomplete('search', event.target.value); - }); - $textBox.autocomplete('search', ''); - } + messages: { + noResults: '', + results: () => {}, + }, + }) + .focus(() => { + $(this).autocomplete('search', $(this).val()); + }) + .data('ui-autocomplete')._renderItem = (ul, item) => + $('
  • ').append(`${item.dom}`).appendTo(ul); + $(textBox).autocomplete('search', ''); } createBranch() { diff --git a/components/graph/git-ref.js b/components/graph/git-ref.js index 8530800b0..6ef42e6fd 100644 --- a/components/graph/git-ref.js +++ b/components/graph/git-ref.js @@ -68,6 +68,9 @@ 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.displayHtml = (largeCurrent) => { const size = largeCurrent && this.current() ? 26 : 18; @@ -120,7 +123,7 @@ class RefViewModel extends Selectable { operation = '/branches'; } - if (!rewindWarnOverride && this.node().date > toNode.date) { + if (!rewindWarnOverride && this.node().timestamp > toNode.timestamp) { promise = components .create('yesnodialog', { title: 'Are you sure?', diff --git a/components/graph/graph.js b/components/graph/graph.js index f1c9290a1..9ab8fa9c2 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -55,14 +55,18 @@ 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) { @@ -99,18 +103,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 @@ -126,7 +143,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(() => { @@ -173,8 +192,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++; } @@ -185,7 +203,6 @@ class GraphViewModel { node.ancestorOfHEAD(node.ancestorOfHEADTimeStamp == updateTimeStamp); if (node.ancestorOfHEAD()) node.branchOrder(0); node.render(); - this.nodes.push(node); }); return this.nodes(); @@ -201,8 +218,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; @@ -212,8 +229,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 d4f49f8eb..717d1a192 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]; From a7e064fe6341e3f16bd41754dfd7d14e47734853 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Sat, 8 Feb 2020 08:45:13 -0800 Subject: [PATCH 10/19] 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 9ab8fa9c2..887dba797 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -219,8 +219,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 a5d838f92832bc2ac4b618e8c219adc7380d328a Mon Sep 17 00:00:00 2001 From: JK Kim Date: Sat, 8 Feb 2020 09:01:54 -0800 Subject: [PATCH 11/19] 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 39843a668..4c2c3da00 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -620,7 +620,7 @@ git.log = (path, skip, limit, lookForHead, maxActiveBranchSearchIteration) => { 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 a148162ba1e6c6947d044e938afa1a2c8c4de95d Mon Sep 17 00:00:00 2001 From: JK Kim Date: Sat, 8 Feb 2020 09:08:54 -0800 Subject: [PATCH 12/19] Fix graph height issue and making it more snappier --- components/graph/graph.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index 887dba797..042ac33ac 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -56,10 +56,12 @@ class GraphViewModel { } }); this.loadNodesFromApiThrottled = _.throttle(this.loadNodesFromApi.bind(this), 1000, { - leading: false, + leading: true, + trailing: false, }); this.updateBranchesThrottled = _.throttle(this.updateBranches.bind(this), 1000, { - leading: false, + leading: true, + trailing: false, }); this.graphWidth = ko.observable(); this.graphHeight = ko.observable(800); @@ -127,19 +129,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 5a2fc22b3bd5e135cf2ea7f930445e7d015a360e Mon Sep 17 00:00:00 2001 From: JK Kim Date: Sun, 9 Feb 2020 11:37:09 -0800 Subject: [PATCH 13/19] update change log and package.json --- CHANGELOG.md | 6 ++++++ package.json | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea036b517..7a64991db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ We are following the [Keep a Changelog](https://keepachangelog.com/) format. ## [Unreleased](https://github.com/FredrikNoren/ungit/compare/v1.5.15...master) +### 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.15](https://github.com/FredrikNoren/ungit/compare/v1.5.14...v1.5.15) ### Changed diff --git a/package.json b/package.json index d97d8b293..0607068b6 100644 --- a/package.json +++ b/package.json @@ -97,5 +97,17 @@ "node": "^10.18.0 || >=11.14.0" }, "license": "MIT", - "main": "public/main.js" + "main": "public/main.js", + "window": { + "title": "Ungit", + "icon": "icon.png", + "toolbar": false, + "frame": false, + "show": false, + "width": 1000, + "height": 600, + "position": "center", + "min_width": 400, + "min_height": 200 + } } From cb718d4bec02857ca47dac1a16385cc3accac13c Mon Sep 17 00:00:00 2001 From: JK Kim Date: Mon, 10 Feb 2020 21:58:05 -0800 Subject: [PATCH 14/19] \#1091 prevent redundant ref calculations and overactive fs.watch() --- CHANGELOG.md | 1 - components/graph/graph.js | 124 ++++++++++++++++++-------------------- source/git-api.js | 15 ++--- source/git-promise.js | 23 +++++-- 4 files changed, 85 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a64991db..91aec610a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,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.15](https://github.com/FredrikNoren/ungit/compare/v1.5.14...v1.5.15) diff --git a/components/graph/graph.js b/components/graph/graph.js index 042ac33ac..85991ad9a 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -14,7 +14,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(); @@ -36,6 +37,15 @@ 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(); @@ -67,8 +77,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) { @@ -95,65 +105,48 @@ 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; - }); - - // 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 + 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) => { - if (node.cy() > maxHeight) { - maxHeight = node.cy(); - } node.parents().forEach((parentSha1) => { - this.getEdge(node.sha1, parentSha1); + edges.push(this.getEdge(node.sha1, parentSha1)); }); + node.render(); }); - this.graphHeight(maxHeight + 80); + this.edges(edges); + this.nodes(nodes); + if (nodes.length > 0) { + this.graphHeight(nodes[nodes.length - 1].cy() + 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)) .finally(() => { if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) { - this.loadNodesFromApiThrottled(); + this.scrolledToEnd(); } }); } @@ -169,46 +162,50 @@ 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( + 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) { @@ -216,7 +213,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; } @@ -238,8 +234,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++; @@ -282,10 +278,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') { diff --git a/source/git-api.js b/source/git-api.js index 856319b68..edb063806 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -80,6 +80,9 @@ exports.registerApi = (env) => { emitWorkingTreeChanged(socket.watcherPath); } }); + watcher.on('error', (err) => { + winston.warn(`Error watching ${pathToWatch}: `, JSON.stringify(err)); + }); socket.watcher.push(watcher); }); }; @@ -384,21 +387,19 @@ 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) + .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; } diff --git a/source/git-promise.js b/source/git-promise.js index 4c2c3da00..e7ad51a89 100644 --- a/source/git-promise.js +++ b/source/git-promise.js @@ -591,7 +591,7 @@ 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', @@ -616,13 +616,24 @@ git.log = (path, skip, limit, lookForHead, maxActiveBranchSearchIteration) => { .then(gitParser.parseGitLog) .then((log) => { log = log ? log : []; - skip = skip + log.length; - if (lookForHead && maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) { + if (maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) { return git - .log(path, skip, maxActiveBranchSearchIteration - 1) - .then((innerLog) => log.concat(innerLog)); + .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 }; } }); }; From 899bf5446bd16dc1a2a2d5ee7bc88e36a59d9bdc Mon Sep 17 00:00:00 2001 From: JK Kim Date: Mon, 10 Feb 2020 22:09:29 -0800 Subject: [PATCH 15/19] Missed few --- components/graph/graph-graphics.html | 11 ++++++++++- components/graph/graph.js | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/components/graph/graph-graphics.html b/components/graph/graph-graphics.html index d8249873a..4b823e15b 100644 --- a/components/graph/graph-graphics.html +++ b/components/graph/graph-graphics.html @@ -34,7 +34,7 @@ + + + diff --git a/components/graph/graph.js b/components/graph/graph.js index 85991ad9a..0a827a1eb 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -234,8 +234,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 179ec461109f0df90681fd9e65639996e3ac2c03 Mon Sep 17 00:00:00 2001 From: JK Kim Date: Wed, 12 Feb 2020 20:49:46 -0800 Subject: [PATCH 16/19] fix tests --- test/spec.git-parser.js | 79 ++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/test/spec.git-parser.js b/test/spec.git-parser.js index e7ee1a273..f5c08e61c 100644 --- a/test/spec.git-parser.js +++ b/test/spec.git-parser.js @@ -227,10 +227,13 @@ 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', - additions: 176, - deletions: 1, + total: { + additions: 176, + deletions: 1, + }, fileLineDiffs: [ { additions: 1, @@ -260,10 +263,13 @@ 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', - additions: 32, - deletions: 0, + total: { + additions: 32, + deletions: 0, + }, fileLineDiffs: [ { additions: 32, @@ -286,10 +292,13 @@ 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', - additions: 0, - deletions: 0, + total: { + additions: 0, + deletions: 0, + }, fileLineDiffs: [], isHead: false, message: 'empty commit', @@ -303,10 +312,13 @@ 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', - additions: 14, - deletions: 9, + total: { + additions: 14, + deletions: 9, + }, fileLineDiffs: [ { additions: 4, @@ -370,10 +382,13 @@ 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', - additions: 32, - deletions: 0, + total: { + additions: 32, + deletions: 0, + }, fileLineDiffs: [ { additions: 32, @@ -413,10 +428,13 @@ 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', - additions: 32, - deletions: 0, + total: { + additions: 32, + deletions: 0, + }, fileLineDiffs: [ { additions: 32, @@ -500,8 +518,10 @@ describe('git-parser parseGitLog', () => { expect(gitParser.parseGitLog(gitLog)[0]).to.eql({ refs: ['HEAD', 'refs/heads/git-parser-specs'], - additions: 32, - deletions: 0, + total: { + additions: 32, + deletions: 0, + }, fileLineDiffs: [ { additions: 32, @@ -521,6 +541,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', }); }); @@ -752,21 +773,21 @@ 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'; + `## 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`; expect(gitParser.parseGitStatus(gitStatus)).to.eql({ branch: 'git-parser-specs', From ea143ab025b1ba511658ef1a60a6909684c2055c Mon Sep 17 00:00:00 2001 From: JK Kim Date: Thu, 13 Feb 2020 21:30:32 -0800 Subject: [PATCH 17/19] fix scrolling issue --- components/graph/graph.html | 2 +- components/graph/graph.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/components/graph/graph.html b/components/graph/graph.html index b8f601720..f8416fad2 100644 --- a/components/graph/graph.html +++ b/components/graph/graph.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/components/graph/graph.js b/components/graph/graph.js index 0a827a1eb..672fb66fb 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -37,6 +37,14 @@ 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; From 65b672010a36f24d3cf2761a60faa7441ce80dad Mon Sep 17 00:00:00 2001 From: jk-kim Date: Tue, 25 Feb 2020 14:32:13 -0800 Subject: [PATCH 18/19] Update components/graph/graph.js Co-Authored-By: campersau --- components/graph/graph.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/graph/graph.js b/components/graph/graph.js index 672fb66fb..d9f190b85 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -49,7 +49,7 @@ class GraphViewModel { () => { if (this.skip() <= 0) return; this.skip(Math.max(this.skip() - numberOfNodesPerLoad, 0)); - this.loadNodesFromApi(); + this.loadNodesFromApiThrottled(); }, 500, true From 33a1ef6530f8ee1e73b47e7d5bb87886e5258bdd Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Mon, 1 Feb 2021 13:48:36 +0100 Subject: [PATCH 19/19] fixup --- components/graph/git-graph-actions.js | 18 ++++--- components/graph/git-node.js | 73 ++++++++++++--------------- components/graph/graph.js | 1 + source/git-api.js | 5 +- test/spec.git-parser.js | 72 +++++++++++--------------- 5 files changed, 77 insertions(+), 92 deletions(-) diff --git a/components/graph/git-graph-actions.js b/components/graph/git-graph-actions.js index 71b7693b0..7b9f946a6 100644 --- a/components/graph/git-graph-actions.js +++ b/components/graph/git-graph-actions.js @@ -81,7 +81,7 @@ class Move extends ActionBase { class Reset extends ActionBase { constructor(graph, node) { - super(graph, 'Reset', 'reset', octicons.trashcan.toSVG({ 'height': 18 })); + super(graph, 'Reset', 'reset', octicons.trashcan.toSVG({ height: 18 })); this.node = node; this.visible = ko.computed(() => { if (this.isRunning()) return true; @@ -95,7 +95,8 @@ class Reset extends ActionBase { context && context.node() && remoteRef.node() != context.node() && - remoteRef.node().timestamp < context.node().timestamp; + remoteRef.node().timestamp < context.node().timestamp + ); }); } @@ -222,11 +223,14 @@ class Push extends ActionBase { if (remoteRef) { 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' })); + return ref + .createRemoteRef() + .then(() => { + 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 0aff73f73..54915d4a4 100644 --- a/components/graph/git-node.js +++ b/components/graph/git-node.js @@ -116,43 +116,7 @@ class GitNodeViewModel extends Animateable { 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'); - 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 } - ); + this.render = _.debounce(this.render.bind(this), 50, { trailing: true }); } getGraphAttr() { @@ -164,9 +128,38 @@ class GitNodeViewModel extends Animateable { this.element().setAttribute('y', val[1] - 30); } - setParent(parent) { - this.aboveNode = parent; - if (parent) parent.belowNode = this; + 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'); + 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; + this.graph.nodes.push(this); + } + this.animate(); } setData(logEntry) { diff --git a/components/graph/graph.js b/components/graph/graph.js index d9f190b85..bc2d316b5 100644 --- a/components/graph/graph.js +++ b/components/graph/graph.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const moment = require('moment'); const octicons = require('octicons'); const components = require('ungit-components'); +const programEvents = require('ungit-program-events'); const GitNodeViewModel = require('./git-node'); const GitRefViewModel = require('./git-ref'); const EdgeViewModel = require('./edge'); diff --git a/source/git-api.js b/source/git-api.js index edb063806..53d75916e 100644 --- a/source/git-api.js +++ b/source/git-api.js @@ -70,7 +70,8 @@ exports.registerApi = (env) => { return mkdirp(pathToWatch); }) .then(() => { - const watcher = fs.watch(pathToWatch, options || {}, (event, filename) => { + const watcher = watch(pathToWatch, options || {}); + watcher.on('change', (event, filename) => { if (event === 'rename' || !filename) return; const filePath = path.join(subfolderPath, filename); winston.debug(`File change: ${filePath}`); @@ -395,7 +396,7 @@ exports.registerApi = (env) => { if (err.stderr && err.stderr.indexOf("fatal: bad default revision 'HEAD'") == 0) { return { limit: limit, skip: skip, nodes: [] }; } else if ( - /fatal: your current branch \'.+\' does not have any commits yet.*/.test(err.stderr) + /fatal: your current branch '.+' does not have any commits yet.*/.test(err.stderr) ) { return { limit: limit, skip: skip, nodes: [] }; } else if (err.stderr && err.stderr.indexOf('fatal: Not a git repository') == 0) { diff --git a/test/spec.git-parser.js b/test/spec.git-parser.js index f5c08e61c..b8f4a64e3 100644 --- a/test/spec.git-parser.js +++ b/test/spec.git-parser.js @@ -230,10 +230,8 @@ describe('git-parser parseGitLog', () => { timestamp: 1546610046000, committerEmail: 'test@example.com', committerName: 'Test ungit', - total: { - additions: 176, - deletions: 1, - }, + additions: 176, + deletions: 1, fileLineDiffs: [ { additions: 1, @@ -266,10 +264,8 @@ describe('git-parser parseGitLog', () => { timestamp: 1546607036000, committerEmail: 'test@example.com', committerName: 'Test ungit', - total: { - additions: 32, - deletions: 0, - }, + additions: 32, + deletions: 0, fileLineDiffs: [ { additions: 32, @@ -295,10 +291,8 @@ describe('git-parser parseGitLog', () => { timestamp: 1546606976000, committerEmail: 'test@example.com', committerName: 'Test ungit', - total: { - additions: 0, - deletions: 0, - }, + additions: 0, + deletions: 0, fileLineDiffs: [], isHead: false, message: 'empty commit', @@ -315,10 +309,8 @@ describe('git-parser parseGitLog', () => { timestamp: 1546606916000, committerEmail: 'test@example.com', committerName: 'Test ungit', - total: { - additions: 14, - deletions: 9, - }, + additions: 14, + deletions: 9, fileLineDiffs: [ { additions: 4, @@ -385,10 +377,8 @@ describe('git-parser parseGitLog', () => { timestamp: 1546607036000, committerEmail: 'test@example.com', committerName: 'Test ungit', - total: { - additions: 32, - deletions: 0, - }, + additions: 32, + deletions: 0, fileLineDiffs: [ { additions: 32, @@ -431,10 +421,8 @@ describe('git-parser parseGitLog', () => { timestamp: 1546607036000, committerEmail: 'test@example.com', committerName: 'Test ungit', - total: { - additions: 32, - deletions: 0, - }, + additions: 32, + deletions: 0, fileLineDiffs: [ { additions: 32, @@ -518,10 +506,8 @@ describe('git-parser parseGitLog', () => { expect(gitParser.parseGitLog(gitLog)[0]).to.eql({ refs: ['HEAD', 'refs/heads/git-parser-specs'], - total: { - additions: 32, - deletions: 0, - }, + additions: 32, + deletions: 0, fileLineDiffs: [ { additions: 32, @@ -773,21 +759,21 @@ 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`; + '## 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'; expect(gitParser.parseGitStatus(gitStatus)).to.eql({ branch: 'git-parser-specs',