Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 109 additions & 18 deletions src/extensions/default/Git/src/History.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
define(function (require) {

// Brackets modules
var _ = brackets.getModule("thirdparty/lodash"),

Check failure on line 4 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEPz5PIa1mbvUcf&open=AaDkKLEPz5PIa1mbvUcf&pullRequest=3227
DocumentManager = brackets.getModule("document/DocumentManager"),
FileUtils = brackets.getModule("file/FileUtils"),
LocalizationUtils = brackets.getModule("utils/LocalizationUtils"),
Strings = brackets.getModule("strings"),
StringUtils = brackets.getModule("utils/StringUtils"),
Metrics = brackets.getModule("utils/Metrics"),
NotificationUI = brackets.getModule("widgets/NotificationUI"),
Mustache = brackets.getModule("thirdparty/mustache/mustache");
Expand All @@ -19,8 +20,9 @@
Preferences = require("src/Preferences");

// Templates
var gitPanelHistoryTemplate = require("text!templates/git-panel-history.html"),

Check failure on line 23 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcg&open=AaDkKLEQz5PIa1mbvUcg&pullRequest=3227
gitPanelHistoryCommitsTemplate = require("text!templates/git-panel-history-commits.html");
gitPanelHistoryCommitsTemplate = require("text!templates/git-panel-history-commits.html"),
gitPanelHistoryEndTemplate = require("text!templates/git-panel-history-end.html");

// Module variables
let $gitPanel = $(null),
Expand All @@ -29,6 +31,14 @@
commitCache = [],
lastDocumentSeen = null;

// Where the branch stands against its remote, for the push waterline.
// `remoteHead` is null when the branch has no counterpart on the remote at
// all; `remoteHeadSeen` latches once the waterline has been placed, so the
// pages loaded after it are known to be pushed without re-testing them.
let remoteHead = null,
remoteHeadSeen = false,
unpushedCount = 0;

// must match the page size git log is invoked with in GitCli.getHistory
const HISTORY_PAGE_SIZE = 100;

Expand All @@ -52,8 +62,8 @@
})
.on("click.history", ".history-commit", function () {
const $tr = $(this);
var hash = $tr.attr("x-hash");

Check failure on line 65 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUch&open=AaDkKLEQz5PIa1mbvUch&pullRequest=3227
var commit = _.find(commitCache, function (commit) { return commit.hash === hash; });

Check failure on line 66 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUci&open=AaDkKLEQz5PIa1mbvUci&pullRequest=3227
const historyShown = HistoryViewer.toggle(commit, getCurrentDocument(), {
isInitial: $(this).attr("x-initial-commit") === "true"
});
Expand All @@ -64,21 +74,21 @@
});
}

var generateCssAvatar = _.memoize(function (author, email) {

Check failure on line 77 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcj&open=AaDkKLEQz5PIa1mbvUcj&pullRequest=3227

// Original source: http://indiegamr.com/generate-repeatable-random-numbers-in-js/
var seededRandom = function (max, min, seed) {

Check failure on line 80 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUck&open=AaDkKLEQz5PIa1mbvUck&pullRequest=3227
max = max || 1;
min = min || 0;

seed = (seed * 9301 + 49297) % 233280;
var rnd = seed / 233280.0;

Check failure on line 85 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcl&open=AaDkKLEQz5PIa1mbvUcl&pullRequest=3227

return min + rnd * (max - min);
};

// Use `seededRandom()` to generate a pseudo-random number [0-16] to pick a color from the list
var seedBase = parseInt(author.charCodeAt(3).toString(), email.length),

Check failure on line 91 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcm&open=AaDkKLEQz5PIa1mbvUcm&pullRequest=3227
seed = parseInt(email.charCodeAt(seedBase.toString().substring(1, 2)).toString(), 16),
colors = [
"#ffb13b", "#dd5f7a", "#8dd43a", "#2f7e2f", "#4141b9", "#3dafea", "#7e3e3e", "#f2f26b",
Expand All @@ -100,12 +110,15 @@
function _renderHistoryTable(commits, file) {
// calculate some missing stuff like avatars
commits = addAdditionalCommitInfo(commits);
commits = markPushState(commits, file);
commitCache = commitCache.concat(commits);

const templateData = {
commits: commits,
emptyMessage: file ? Strings.GIT_FILE_HISTORY_NOTHING_TO_SHOW : Strings.GIT_HISTORY_NOTHING_TO_SHOW,
Strings: Strings
// the banner only makes sense for a branch, not one file's history
notPushed: !file && !remoteHead && commits.length > 0,
Strings: Strings,
emptyMessage: file ? Strings.GIT_FILE_HISTORY_NOTHING_TO_SHOW : Strings.GIT_HISTORY_NOTHING_TO_SHOW
};

$tableContainer.find("#git-history-list").remove();
Expand All @@ -120,10 +133,30 @@
if (commits.length < HISTORY_PAGE_SIZE) {
// the full history is already here, so the last commit is the initial
// one. with more pages the initial commit is marked by loadMoreHistory.
$historyList.attr("x-finished", "true");
$historyList
.find("tr.history-commit:last-child")
.attr("x-initial-commit", "true");
markHistoryFinished();
}
}

/**
* Note that the whole history is on screen.
*
* Marks the oldest commit so HistoryViewer knows it has nothing to diff
* against, and, for a branch with no counterpart on the remote, closes the
* unpushed run with a line — only here, because until the initial commit is
* reached a closing line would claim a bottom the list does not have yet.
*
* Matches the last commit row with `.last()` rather than `:last-child`: the
* waterline rows are siblings, so the oldest commit is not always the final
* child of the table body.
*/
function markHistoryFinished() {
$historyList.attr("x-finished", "true");
const $lastCommit = $historyList.find("tr.history-commit").last();
$lastCommit.attr("x-initial-commit", "true");
if (!remoteHead && $lastCommit.length && !$historyList.find("tr.history-push-end").length) {
$lastCommit.after(Mustache.render(gitPanelHistoryEndTemplate, {
allLocalText: StringUtils.format(Strings.GIT_HISTORY_ALL_LOCAL, unpushedCount)
}));
}
}

Expand All @@ -134,14 +167,25 @@
// clear cache
commitCache = [];

// the waterline is recomputed per render: the remote may have moved
remoteHead = null;
remoteHeadSeen = false;
unpushedCount = 0;

return Git.getCurrentBranchName().then(function (branchName) {
// Get the history commits of the current branch
var p = file ? Git.getFileHistory(file.relative, branchName) : Git.getHistory(branchName);
return p.then(function (commits) {
if (renderId === historyRenderId) {
_renderHistoryTable(commits, file);
}
return true;
return Git.getRemoteBranchHead(branchName).catch(function () {
// a repo without remotes is normal, not an error to surface
return null;
}).then(function (head) {
remoteHead = head;
// Get the history commits of the current branch
var p = file ? Git.getFileHistory(file.relative, branchName) : Git.getHistory(branchName);

Check failure on line 182 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcn&open=AaDkKLEQz5PIa1mbvUcn&pullRequest=3227
return p.then(function (commits) {
if (renderId === historyRenderId) {
_renderHistoryTable(commits, file);
}
return true;
});
});
}).catch(function (err) {
if (renderId !== historyRenderId) {
Expand Down Expand Up @@ -170,7 +214,7 @@
loadingMoreHistory = true;
const renderId = historyRenderId;
return Git.getCurrentBranchName().then(function (branchName) {
var p,

Check failure on line 217 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUco&open=AaDkKLEQz5PIa1mbvUco&pullRequest=3227
file = $historyList.data("file-relative"),
skipCount = $tableContainer.find("tr.history-commit").length;
if (file) {
Expand All @@ -184,22 +228,20 @@
return;
}
if (commits.length === 0) {
$historyList.attr("x-finished", "true");
// marks initial commit as first
$historyList
.find("tr.history-commit:last-child")
.attr("x-initial-commit", "true");
markHistoryFinished();
return;
}

commits = addAdditionalCommitInfo(commits);
commits = markPushState(commits, file);
commitCache = commitCache.concat(commits);

var templateData = {

Check failure on line 240 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcp&open=AaDkKLEQz5PIa1mbvUcp&pullRequest=3227
commits: commits,
Strings: Strings
};
var commitsHtml = Mustache.render(gitPanelHistoryCommitsTemplate, templateData);

Check failure on line 244 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcq&open=AaDkKLEQz5PIa1mbvUcq&pullRequest=3227
$historyList.children("tbody").append(commitsHtml);
})
.catch(function (err) {
Expand Down Expand Up @@ -241,11 +283,60 @@
return commits;
}

/**
* Flag each commit against the remote, for the waterline the table draws.
*
* Commits arrive newest first, so everything listed before the remote's
* head is still local: walk until that commit shows up, mark it, and leave
* the rest alone — they are older than the remote tip and therefore pushed.
* The latch carries the answer across pages, since every later page is
* older than the one before it. With no counterpart on the remote nothing
* has been pushed and every commit is flagged.
*
* File history is skipped entirely: it lists only the commits touching one
* file, so the remote's head usually is not among them and every row would
* be wrongly called local.
*
* @param {Array<Object>} commits - commits for this page, newest first
* @param {?(Object|string)} file - whatever the caller holds for the file on
* show; only its presence matters, the first page has the object and
* later pages the relative path
* @return {Array<Object>} the same commits, flagged in place
*/
function markPushState(commits, file) {
if (file) {
return commits;
}
_.forEach(commits, function (commit) {
if (!remoteHead) {
commit.unpushed = true;
return;
}
if (remoteHeadSeen) {
return;
}
if (commit.hash === remoteHead.hash) {
commit.isRemoteHead = true;
// the ref name is a git identifier, never translated
commit.remoteRef = remoteHead.ref;
commit.isAhead = unpushedCount > 0;
commit.pushStateText = commit.isAhead
? StringUtils.format(Strings.GIT_HISTORY_AHEAD, unpushedCount)
: Strings.GIT_HISTORY_UP_TO_DATE;
remoteHeadSeen = true;
return;
}
commit.unpushed = true;
unpushedCount++;
});
return commits;
}

function getCurrentDocument() {
if (HistoryViewer.isVisible()) {
return lastDocumentSeen;
}
var doc = DocumentManager.getCurrentDocument();

Check failure on line 339 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcr&open=AaDkKLEQz5PIa1mbvUcr&pullRequest=3227
if (doc) {
lastDocumentSeen = doc;
}
Expand All @@ -264,7 +355,7 @@
}

function handleFileChange() {
var currentDocument = getCurrentDocument();

Check failure on line 358 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcs&open=AaDkKLEQz5PIa1mbvUcs&pullRequest=3227

if ($historyList.is(":visible") && $historyList.data("file")) {
handleToggleHistory("FILE", currentDocument);
Expand Down Expand Up @@ -322,13 +413,13 @@
}

// Render #git-history-list if is not already generated or if the viewed file for file history has changed
var isEmpty = $historyList.find("tr").length === 0,

Check failure on line 416 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUct&open=AaDkKLEQz5PIa1mbvUct&pullRequest=3227
fileChanged = currentFile !== (file ? file.absolute : null);
if (historyEnabled && (isEmpty || fileChanged || isRefresh)) {
if ($historyList.length > 0) {
$historyList.remove();
}
var $spinner = $("<div class='spinner spin large'></div>").appendTo($gitPanel);

Check failure on line 422 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcu&open=AaDkKLEQz5PIa1mbvUcu&pullRequest=3227
renderHistory(file).then(function (rendered) {
$spinner.remove();
if (!rendered) {
Expand Down Expand Up @@ -368,7 +459,7 @@
if (!historyEnabled) { HistoryViewer.hide(); }

// Toggle history button
var globalButtonActive = historyEnabled && newHistoryMode === "GLOBAL",

Check failure on line 462 in src/extensions/default/Git/src/History.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected var, use let or const instead.

See more on https://sonarcloud.io/project/issues?id=phcode-dev_phoenix&issues=AaDkKLEQz5PIa1mbvUcv&open=AaDkKLEQz5PIa1mbvUcv&pullRequest=3227
fileButtonActive = historyEnabled && newHistoryMode === "FILE";
$gitPanel.find(".git-history-toggle").toggleClass("active", globalButtonActive)
.attr("title", globalButtonActive ? Strings.TOOLTIP_HIDE_HISTORY : Strings.TOOLTIP_SHOW_HISTORY);
Expand Down
10 changes: 10 additions & 0 deletions src/extensions/default/Git/src/Remotes.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ define(function (require) {
clearRemotePicker();
}
}).catch(function (err) {
// Opening a project without a repository still refreshes the panel,
// so `git remote -v` lands here reporting there is no repository.
// That is the project the user chose, not a fault, and a modal over
// the editor for it interrupts them for nothing — drop the picker
// and keep the reason in the log.
if (ErrorHandler.contains(err, "not a git repository")) {
clearRemotePicker();
ErrorHandler.logError(err);
return;
}
ErrorHandler.showError(err, Strings.ERROR_GETTING_REMOTES);
});
}
Expand Down
5 changes: 5 additions & 0 deletions src/extensions/default/Git/src/git/Git.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ define(function (require, exports) {
return GitCli.getHistory(branch, skip, file);
}

function getRemoteBranchHead(branch) {
return GitCli.getRemoteBranchHead(branch);
}

function resetIndex() {
return GitCli.reset();
}
Expand Down Expand Up @@ -180,6 +184,7 @@ define(function (require, exports) {
exports.getAllBranches = getAllBranches;
exports.getHistory = getHistory;
exports.getFileHistory = getFileHistory;
exports.getRemoteBranchHead = getRemoteBranchHead;
exports.resetIndex = resetIndex;
exports.discardAllChanges = discardAllChanges;
exports.getMergeInfo = getMergeInfo;
Expand Down
71 changes: 71 additions & 0 deletions src/extensions/default/Git/src/git/GitCli.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,76 @@ define(function (require, exports) {
});
}

/**
* Where the current branch's counterpart on the remote points right now.
*
* The configured upstream wins, but a branch pushed with an explicit refspec
* often never gets one, so the default remote's branch of the same name
* stands in — otherwise history would call a pushed branch local. Resolves
* to null when neither ref is there, which is what a branch that has never
* been pushed looks like.
*
* Usually one git call: a single `for-each-ref` reads the branch and that
* default-remote ref together, giving the upstream's name and the hash in
* the same pass. Only a branch tracking some other ref needs a second.
*
* @param {string} branch - name of the current branch
* @return {Promise<?{ref: string, hash: string}>} null when there is no counterpart
*/
function getRemoteBranchHead(branch) {
if (!branch) {
return Promise.resolve(null);
}
const remotes = Preferences.get("defaultRemotes") || {};
const remote = remotes[Preferences.get("currentGitRoot")];
const guess = remote ? remote + "/" + branch : null;
// The full refname disambiguates: a branch really can be called
// "origin/ai", and then both rows would answer to the same short name.
const localRef = "refs/heads/" + branch;
const guessRef = guess ? "refs/remotes/" + guess : null;
// "|" is legal in a ref name, so the fields are parted the way
// getHistory above does it rather than on a character refs may contain.
const separator = "_._";
const args = [
"for-each-ref",
"--format=%(refname)" + separator + "%(upstream:short)" + separator + "%(objectname)",
localRef
];
if (guessRef) { args.push(guessRef); }

// One pass over both refs: the branch row carries the upstream's name,
// the remote row carries the hash. A ref that is not there prints
// nothing rather than failing, so absence needs no special case.
return git(args).then(function (stdout) {
let upstream = null, guessHash = null;
(stdout || "").split("\n").forEach(function (line) {
const parts = line.trim().split(separator);
if (parts.length < 3) { return; }
if (parts[0] === localRef) {
upstream = parts[1] || null;
} else if (parts[0] === guessRef) {
guessHash = parts[2] || null;
}
});
if (upstream && upstream !== guess) {
// Tracking something other than <remote>/<branch>, so its hash
// was not in the pass above. rev-parse resolves it whatever it
// is — a remote-tracking ref, or a local branch for `remote = .`
return git(["rev-parse", "--verify", "--quiet", upstream])
.then(function (out) {
const hash = (out || "").trim();
return hash ? { ref: upstream, hash: hash } : null;
})
.catch(function () { return null; });
}
// The upstream is the ref just read, or there is no upstream and the
// branch was pushed to its own name; either way the hash is in hand.
return guessHash ? { ref: guess, hash: guessHash } : null;
}).catch(function () {
return null;
});
}

// Get list of deleted files between two branches
function getDeletedFiles(oldBranch, newBranch) {
return git(["diff", "--no-ext-diff", "--name-status", oldBranch + ".." + newBranch])
Expand Down Expand Up @@ -1166,6 +1236,7 @@ define(function (require, exports) {
exports.setUpstreamBranch = setUpstreamBranch;
exports.getCurrentBranchName = getCurrentBranchName;
exports.getCurrentUpstreamBranch = getCurrentUpstreamBranch;
exports.getRemoteBranchHead = getRemoteBranchHead;
exports.getConfig = getConfig;
exports.setConfig = setConfig;
exports.getBranches = getBranches;
Expand Down
Loading
Loading