diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md
index b5ae5e4f04..6f175ffdd4 100644
--- a/docs/API-Reference/command/Commands.md
+++ b/docs/API-Reference/command/Commands.md
@@ -980,6 +980,12 @@ Opens git settings
## CMD\_GIT\_CLOSE\_UNMODIFIED
Closes unmodified files
+**Kind**: global variable
+
+
+## CMD\_GIT\_OPEN\_CHANGED\_FILES
+Opens all modified/untracked files
+
**Kind**: global variable
diff --git a/src/command/Commands.js b/src/command/Commands.js
index caa7c4bd14..edf3bd1fd4 100644
--- a/src/command/Commands.js
+++ b/src/command/Commands.js
@@ -534,6 +534,9 @@ define(function (require, exports, module) {
/** Closes unmodified files */
exports.CMD_GIT_CLOSE_UNMODIFIED = "git-close-unmodified-files";
+ /** Opens all modified/untracked files */
+ exports.CMD_GIT_OPEN_CHANGED_FILES = "git-open-changed-files";
+
/** Checks out a branch or commit */
exports.CMD_GIT_CHECKOUT = "git-checkout";
diff --git a/src/extensions/default/Git/main.js b/src/extensions/default/Git/main.js
index aaf38e8458..d80730bb68 100644
--- a/src/extensions/default/Git/main.js
+++ b/src/extensions/default/Git/main.js
@@ -40,10 +40,19 @@ define(function (require, exports, module) {
}
AppInit.appReady(function () {
- Main.init().then((enabled)=>{
- if(!enabled) {
- BracketsEvents.disableAll();
- }
+ function initMain() {
+ Main.init().then((enabled)=>{
+ if(!enabled) {
+ BracketsEvents.disableAll();
+ }
+ });
+ }
+ // Main.init emits events like GIT_ENABLED that the modules above listen
+ // for. They load asynchronously, so init must wait for them or events
+ // fired before they finish loading are lost (Eg. empty remotes picker).
+ require(modules, initMain, function (err) {
+ console.error("Git modules failed to load, initializing git anyway", err);
+ initMain();
});
});
diff --git a/src/extensions/default/Git/src/Branch.js b/src/extensions/default/Git/src/Branch.js
index 51f3236c98..a19b110294 100644
--- a/src/extensions/default/Git/src/Branch.js
+++ b/src/extensions/default/Git/src/Branch.js
@@ -32,6 +32,9 @@ define(function (require, exports) {
currentEditor,
$dropdown;
+ let lastRenderedBranchName = null;
+ let $dropdownAnchor = null;
+
function renderList(branches) {
branches = branches.map(function (name) {
return {
@@ -303,7 +306,9 @@ define(function (require, exports) {
function attachCloseEvents() {
$("html").on("click", closeDropdown);
$("#project-files-container").on("scroll", closeDropdown);
+ $("#git-panel .table-container").on("scroll", closeDropdown);
$("#titlebar .nav").on("click", closeDropdown);
+ $("#git-panel .git-remotes").on("click", closeDropdown);
currentEditor = EditorManager.getCurrentFullEditor();
if (currentEditor) {
@@ -316,7 +321,9 @@ define(function (require, exports) {
function detachCloseEvents() {
$("html").off("click", closeDropdown);
$("#project-files-container").off("scroll", closeDropdown);
+ $("#git-panel .table-container").off("scroll", closeDropdown);
$("#titlebar .nav").off("click", closeDropdown);
+ $("#git-panel .git-remotes").off("click", closeDropdown);
if (currentEditor) {
currentEditor._codeMirror.off("focus", closeDropdown);
@@ -325,15 +332,75 @@ define(function (require, exports) {
// $(window).off("keydown", keydownHook);
$dropdown = null;
+ $dropdownAnchor = null;
+ }
+
+ function _positionDropdownBelow($toggle) {
+ // two margins to account for the preceding project dropdown as well
+ const marginLeft = (parseInt($toggle.css("margin-left"), 10) * 2) || 0;
+
+ const toggleOffset = $toggle.offset();
+
+ $dropdown
+ .css({
+ left: toggleOffset.left - marginLeft + 3,
+ top: toggleOffset.top + $toggle.outerHeight() - 3
+ })
+ .appendTo($("body"));
+
+ // fix so it doesn't overflow the screen
+ const maxHeight = $dropdown.parent().height(),
+ height = $dropdown.height(),
+ topOffset = $dropdown.position().top;
+ if (height + topOffset >= maxHeight - 10) {
+ $dropdown.css("bottom", "10px");
+ }
+ }
+
+ function _positionDropdownAbove($anchor) {
+ const anchorOffset = $anchor.offset();
+
+ $dropdown
+ .css({
+ left: anchorOffset.left,
+ // #git-branch-dropdown carries a negative margin-left meant for the
+ // sidebar toggle alignment, neutralize it so left matches the anchor
+ "margin-left": 0,
+ // the .dropdown-menu class positions with "top: 100%", it has to be
+ // explicitly overridden or the bottom positioning below is over-constrained
+ top: "auto",
+ bottom: $(window).height() - anchorOffset.top + 3,
+ // grow upwards from the anchor instead of the default top-left origin
+ "transform-origin": "0 100%"
+ })
+ .appendTo($("body"));
+
+ // fix so it doesn't overflow the screen
+ if ($dropdown.height() >= anchorOffset.top - 10) {
+ $dropdown.css("top", "10px");
+ }
+
+ const rightOverflow = $dropdown.offset().left + $dropdown.outerWidth() - ($(window).width() - 10);
+ if (rightOverflow > 0) {
+ $dropdown.css("left", anchorOffset.left - rightOverflow);
+ }
}
function toggleDropdown(e) {
e.stopPropagation();
+ $("#git-panel .btn-group.open").removeClass("open");
+ // currentTarget is only valid while the event is being dispatched,
+ // so it has to be captured before the async branch listing below
+ const $anchor = $(e.currentTarget);
- // If the dropdown is already visible, close it
+ // clicking the anchor that opened the dropdown closes it, clicking the
+ // other anchor moves the dropdown there
if ($dropdown) {
+ const sameAnchor = $dropdownAnchor && $dropdownAnchor[0] === $anchor[0];
closeDropdown();
- return;
+ if (sameAnchor) {
+ return;
+ }
}
Menus.closeAll();
@@ -341,6 +408,9 @@ define(function (require, exports) {
Git.getBranches().catch(function (err) {
ErrorHandler.showError(err, Strings.ERROR_GETTING_BRANCH_LIST);
}).then(function (branches = []) {
+ if ($dropdown) {
+ return;
+ }
branches = branches.reduce(function (arr, branch) {
if (!branch.currentBranch && !branch.remote) {
arr.push(branch.name);
@@ -349,25 +419,12 @@ define(function (require, exports) {
}, []);
$dropdown = $(renderList(branches));
- const $toggle = $("#git-branch-dropdown-toggle");
- // two margins to account for the preceding project dropdown as well
- const marginLeft = (parseInt($toggle.css("margin-left"), 10) * 2) || 0;
-
- const toggleOffset = $toggle.offset();
-
- $dropdown
- .css({
- left: toggleOffset.left - marginLeft + 3,
- top: toggleOffset.top + $toggle.outerHeight() - 3
- })
- .appendTo($("body"));
-
- // fix so it doesn't overflow the screen
- var maxHeight = $dropdown.parent().height(),
- height = $dropdown.height(),
- topOffset = $dropdown.position().top;
- if (height + topOffset >= maxHeight - 10) {
- $dropdown.css("bottom", "10px");
+ $dropdownAnchor = $anchor;
+ if ($anchor.closest("#git-panel").length) {
+ // the git panel sits at the bottom of the screen, open upwards from there
+ _positionDropdownAbove($anchor);
+ } else {
+ _positionDropdownBelow($("#git-branch-dropdown-toggle"));
}
PopUpManager.addPopUp($dropdown, detachCloseEvents, true, {closeCurrentPopups: true});
@@ -409,10 +466,9 @@ define(function (require, exports) {
return;
}
- var branchInHead = m[1],
- branchInUi = $gitBranchName.text();
+ const branchInHead = m[1];
- if (branchInHead !== branchInUi) {
+ if (branchInHead !== lastRenderedBranchName) {
refresh();
}
});
@@ -421,15 +477,21 @@ define(function (require, exports) {
function refresh() {
if ($gitBranchName.length === 0) { return; }
+ const projectRoot = Utils.getProjectRoot();
+ function isStale() {
+ return Utils.getProjectRoot() !== projectRoot;
+ }
+
// show info that branch is refreshing currently
$gitBranchName
.text("\u2026")
.parent()
.show();
+ Panel.setBranchName("\u2026", "");
return Git.getGitRoot().then(function (gitRoot) {
- var projectRoot = Utils.getProjectRoot(),
- isRepositoryRootOrChild = gitRoot && projectRoot.indexOf(gitRoot) === 0;
+ if (isStale()) { return; }
+ var isRepositoryRootOrChild = gitRoot && projectRoot.indexOf(gitRoot) === 0;
$gitBranchName.parent().toggle(isRepositoryRootOrChild);
@@ -437,9 +499,12 @@ define(function (require, exports) {
Preferences.set("currentGitRoot", projectRoot);
Preferences.set("currentGitSubfolder", "");
+ lastRenderedBranchName = null;
$gitBranchName
.off("click")
- .text("not a git repo");
+ .text(Strings.GIT_NOT_A_REPO);
+ Panel.setBranchName(Strings.GIT_NOT_A_REPO, "");
+ $("#git-panel .git-panel-branch").removeClass("clickable").off("click");
Panel.disable("not-repo");
return;
@@ -454,6 +519,7 @@ define(function (require, exports) {
return Git.getCurrentBranchName().then(function (branchName) {
Git.getMergeInfo().then(function (mergeInfo) {
+ if (isStale()) { return; }
if (mergeInfo.mergeMode) {
branchName += "|MERGING";
@@ -471,17 +537,26 @@ define(function (require, exports) {
EventEmitter.emit(Events.REBASE_MERGE_MODE, mergeInfo.rebaseMode, mergeInfo.mergeMode);
- var MAX_LEN = 18;
+ const MAX_LEN = 18;
+ lastRenderedBranchName = branchName;
const tooltip = StringUtils.format(Strings.ON_BRANCH, branchName);
- const html = ` ${
- branchName.length > MAX_LEN ? branchName.substring(0, MAX_LEN) + "\u2026" : branchName
- }`;
+ const displayName = branchName.length > MAX_LEN
+ ? branchName.substring(0, MAX_LEN) + "\u2026"
+ : branchName;
+ // branch names may contain characters like "<", so set them
+ // as text and never as html
$gitBranchName
- .html(html)
+ .text(" " + displayName)
+ .prepend('')
.attr("title", tooltip)
.off("click")
.on("click", toggleDropdown);
+ Panel.setBranchName(displayName, tooltip);
+ $("#git-panel .git-panel-branch")
+ .addClass("clickable")
+ .off("click")
+ .on("click", toggleDropdown);
Panel.enable();
}).catch(function (err) {
@@ -489,10 +564,14 @@ define(function (require, exports) {
});
}).catch(function (ex) {
+ if (isStale()) { return; }
if (ErrorHandler.contains(ex, "unknown revision")) {
+ lastRenderedBranchName = null;
$gitBranchName
.off("click")
- .text("no branch");
+ .text(Strings.GIT_NO_BRANCH);
+ Panel.setBranchName(Strings.GIT_NO_BRANCH, "");
+ $("#git-panel .git-panel-branch").removeClass("clickable").off("click");
Panel.enable();
} else {
throw ex;
diff --git a/src/extensions/default/Git/src/Constants.js b/src/extensions/default/Git/src/Constants.js
index 2f851d7a52..5f7699d7e0 100644
--- a/src/extensions/default/Git/src/Constants.js
+++ b/src/extensions/default/Git/src/Constants.js
@@ -36,6 +36,7 @@ define(function (require, exports) {
exports.CMD_GIT_CLONE_WITH_URL = Commands.CMD_GIT_CLONE_WITH_URL;
exports.CMD_GIT_SETTINGS_COMMAND_ID = Commands.CMD_GIT_SETTINGS_COMMAND_ID;
exports.CMD_GIT_CLOSE_UNMODIFIED = Commands.CMD_GIT_CLOSE_UNMODIFIED;
+ exports.CMD_GIT_OPEN_CHANGED_FILES = Commands.CMD_GIT_OPEN_CHANGED_FILES;
exports.CMD_GIT_CHECKOUT = Commands.CMD_GIT_CHECKOUT;
exports.CMD_GIT_RESET_HARD = Commands.CMD_GIT_RESET_HARD;
exports.CMD_GIT_RESET_SOFT = Commands.CMD_GIT_RESET_SOFT;
diff --git a/src/extensions/default/Git/src/History.js b/src/extensions/default/Git/src/History.js
index bb1dd6d61c..52139655c6 100644
--- a/src/extensions/default/Git/src/History.js
+++ b/src/extensions/default/Git/src/History.js
@@ -7,6 +7,7 @@ define(function (require) {
LocalizationUtils = brackets.getModule("utils/LocalizationUtils"),
Strings = brackets.getModule("strings"),
Metrics = brackets.getModule("utils/Metrics"),
+ NotificationUI = brackets.getModule("widgets/NotificationUI"),
Mustache = brackets.getModule("thirdparty/mustache/mustache");
// Local modules
@@ -28,6 +29,13 @@ define(function (require) {
commitCache = [],
lastDocumentSeen = null;
+ // must match the page size git log is invoked with in GitCli.getHistory
+ const HISTORY_PAGE_SIZE = 100;
+
+ // guards against an older async render/load overwriting a newer one
+ let historyRenderId = 0,
+ loadingMoreHistory = false;
+
// Implementation
function initVariables() {
@@ -89,8 +97,40 @@ define(function (require) {
return author + email;
});
- // Render history list the first time
+ function _renderHistoryTable(commits, file) {
+ // calculate some missing stuff like avatars
+ commits = addAdditionalCommitInfo(commits);
+ 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
+ };
+
+ $tableContainer.find("#git-history-list").remove();
+ $tableContainer.append(Mustache.render(gitPanelHistoryTemplate, templateData, {
+ commits: gitPanelHistoryCommitsTemplate
+ }));
+
+ $historyList = $tableContainer.find("#git-history-list")
+ .data("file", file ? file.absolute : null)
+ .data("file-relative", file ? file.relative : null);
+
+ 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");
+ }
+ }
+
+ // Render history list the first time. resolves to false when rendering failed.
function renderHistory(file) {
+ const renderId = ++historyRenderId;
+
// clear cache
commitCache = [];
@@ -98,40 +138,37 @@ define(function (require) {
// Get the history commits of the current branch
var p = file ? Git.getFileHistory(file.relative, branchName) : Git.getHistory(branchName);
return p.then(function (commits) {
-
- // calculate some missing stuff like avatars
- commits = addAdditionalCommitInfo(commits);
- commitCache = commitCache.concat(commits);
-
- var templateData = {
- commits: commits,
- Strings: Strings
- };
-
- $tableContainer.append(Mustache.render(gitPanelHistoryTemplate, templateData, {
- commits: gitPanelHistoryCommitsTemplate
- }));
-
- $historyList = $tableContainer.find("#git-history-list")
- .data("file", file ? file.absolute : null)
- .data("file-relative", file ? file.relative : null);
-
- $historyList
- .find("tr.history-commit:last-child")
- .attr("x-initial-commit", "true");
+ if (renderId === historyRenderId) {
+ _renderHistoryTable(commits, file);
+ }
+ return true;
});
}).catch(function (err) {
+ if (renderId !== historyRenderId) {
+ return true;
+ }
+ // "bad revision"/"unknown revision" mean the branch has no commit
+ // yet (freshly initialized repository), so there is just no history
+ // to show and that is not an error
+ if (ErrorHandler.contains(err, "bad revision") || ErrorHandler.contains(err, "unknown revision")) {
+ _renderHistoryTable([], file);
+ return true;
+ }
ErrorHandler.showError(err, Strings.ERROR_GET_HISTORY);
+ return false;
});
}
// Load more rows in the history list on scroll
function loadMoreHistory() {
if ($historyList.is(":visible")) {
- if (($tableContainer.prop("scrollHeight") - $tableContainer.scrollTop()) === $tableContainer.height()) {
- if ($historyList.attr("x-finished") === "true") {
+ // 2px tolerance as scroll positions can be fractional on scaled displays
+ if (($tableContainer.prop("scrollHeight") - $tableContainer.scrollTop()) <= $tableContainer.height() + 2) {
+ if (loadingMoreHistory || $historyList.attr("x-finished") === "true") {
return;
}
+ loadingMoreHistory = true;
+ const renderId = historyRenderId;
return Git.getCurrentBranchName().then(function (branchName) {
var p,
file = $historyList.data("file-relative"),
@@ -142,6 +179,10 @@ define(function (require) {
p = Git.getHistory(branchName, skipCount);
}
return p.then(function (commits) {
+ if (renderId !== historyRenderId) {
+ // the list was re-rendered while this page was loading
+ return;
+ }
if (commits.length === 0) {
$historyList.attr("x-finished", "true");
// marks initial commit as first
@@ -167,6 +208,9 @@ define(function (require) {
})
.catch(function (err) {
ErrorHandler.showError(err, Strings.ERROR_GET_CURRENT_BRANCH);
+ })
+ .finally(function () {
+ loadingMoreHistory = false;
});
}
}
@@ -205,7 +249,18 @@ define(function (require) {
if (doc) {
lastDocumentSeen = doc;
}
- return doc || lastDocumentSeen;
+ // no fallback to lastDocumentSeen here: when no document is open, file
+ // history must not stick to a file the user has already closed
+ return doc;
+ }
+
+ function _showFileHistoryToast(message) {
+ NotificationUI.createToastFromTemplate(Strings.GIT_SHOW_FILE_HISTORY,
+ "
" + _.escape(message) + "
", {
+ toastStyle: NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.INFO,
+ autoCloseTimeS: 15,
+ instantOpen: true
+ });
}
function handleFileChange() {
@@ -251,10 +306,15 @@ define(function (require) {
}
if (historyEnabled && newHistoryMode === "FILE") {
- if (doc) {
+ if (doc && doc.file) {
file = {};
file.absolute = doc.file.fullPath;
file.relative = FileUtils.getRelativeFilename(Preferences.get("currentGitRoot"), file.absolute);
+ if (!file.relative) {
+ // the file is not inside the repository, so it has no history
+ historyEnabled = false;
+ file = null;
+ }
} else {
// we want a file history but no file was found
historyEnabled = false;
@@ -269,8 +329,19 @@ define(function (require) {
$historyList.remove();
}
var $spinner = $("").appendTo($gitPanel);
- renderHistory(file).finally(function () {
+ renderHistory(file).then(function (rendered) {
$spinner.remove();
+ if (!rendered) {
+ // rendering failed, go back to the changes view instead of
+ // leaving an empty table container behind
+ $tableContainer.find(".git-edited-list").show();
+ $gitPanel.find(".git-history-toggle").removeClass("active")
+ .attr("title", Strings.TOOLTIP_SHOW_HISTORY);
+ $gitPanel.find(".git-file-history").removeClass("active")
+ .attr("title", Strings.TOOLTIP_SHOW_FILE_HISTORY);
+ Git.status();
+ return;
+ }
if (isRefresh) {
// After rendering, we need to fetch the newly created #git-history-list
let $newHistoryList = $tableContainer.find("#git-history-list");
@@ -310,13 +381,24 @@ define(function (require) {
initVariables();
});
EventEmitter.on(Events.GIT_DISABLED, function () {
+ // invalidate any render still in flight so it can't repopulate the panel
+ historyRenderId++;
lastDocumentSeen = null;
$historyList.remove();
$historyList = $();
});
EventEmitter.on(Events.HISTORY_SHOW_FILE, function () {
- handleToggleHistory("FILE");
Metrics.countEvent(Metrics.EVENT_TYPE.GIT, 'panel', "fileHistory");
+ const doc = getCurrentDocument();
+ if (!doc || !doc.file) {
+ _showFileHistoryToast(Strings.GIT_FILE_HISTORY_OPEN_A_FILE);
+ return;
+ }
+ if (!FileUtils.getRelativeFilename(Preferences.get("currentGitRoot"), doc.file.fullPath)) {
+ _showFileHistoryToast(Strings.GIT_FILE_HISTORY_NOT_IN_REPO);
+ return;
+ }
+ handleToggleHistory("FILE");
});
EventEmitter.on(Events.HISTORY_SHOW_GLOBAL, function () {
handleToggleHistory("GLOBAL");
diff --git a/src/extensions/default/Git/src/Main.js b/src/extensions/default/Git/src/Main.js
index 16249c4ca4..15e8aedb14 100644
--- a/src/extensions/default/Git/src/Main.js
+++ b/src/extensions/default/Git/src/Main.js
@@ -241,6 +241,7 @@ define(function (require, exports) {
Constants.CMD_GIT_RESET_SOFT,
// "More options" context menu commands
+ Constants.CMD_GIT_OPEN_CHANGED_FILES,
Constants.CMD_GIT_DISCARD_ALL_CHANGES,
Constants.CMD_GIT_UNDO_LAST_COMMIT,
Constants.CMD_GIT_TOGGLE_UNTRACKED,
@@ -322,6 +323,8 @@ define(function (require, exports) {
// create context menu for git more options
const optionsCmenu = Menus.registerContextMenu(Constants.GIT_PANEL_OPTIONS_CMENU);
Menus.ContextMenu.assignContextMenuToSelector(".git-more-options-btn", optionsCmenu);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_OPEN_CHANGED_FILES);
+ optionsCmenu.addMenuDivider();
optionsCmenu.addMenuItem(Constants.CMD_GIT_DISCARD_ALL_CHANGES);
optionsCmenu.addMenuItem(Constants.CMD_GIT_UNDO_LAST_COMMIT);
optionsCmenu.addMenuDivider();
@@ -398,6 +401,7 @@ define(function (require, exports) {
Utils.enableCommand(Constants.CMD_GIT_GOTO_NEXT_CHANGE, enabled);
Utils.enableCommand(Constants.CMD_GIT_GOTO_PREVIOUS_CHANGE, enabled);
Utils.enableCommand(Constants.CMD_GIT_CLOSE_UNMODIFIED, enabled);
+ Utils.enableCommand(Constants.CMD_GIT_OPEN_CHANGED_FILES, enabled);
Utils.enableCommand(Constants.CMD_GIT_AUTHORS_OF_SELECTION, enabled);
Utils.enableCommand(Constants.CMD_GIT_AUTHORS_OF_FILE, enabled);
diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js
index 33a5c3b547..8ee71c6b22 100644
--- a/src/extensions/default/Git/src/Panel.js
+++ b/src/extensions/default/Git/src/Panel.js
@@ -15,6 +15,7 @@ define(function (require, exports) {
Menus = brackets.getModule("command/Menus"),
Mustache = brackets.getModule("thirdparty/mustache/mustache"),
FindInFiles = brackets.getModule("search/FindInFiles"),
+ MainViewManager = brackets.getModule("view/MainViewManager"),
WorkspaceManager = brackets.getModule("view/WorkspaceManager"),
ProjectManager = brackets.getModule("project/ProjectManager"),
StringUtils = brackets.getModule("utils/StringUtils"),
@@ -851,6 +852,14 @@ define(function (require, exports) {
}
}
+ // called by Branch.refresh() with the already truncated display name so the panel
+ // indicator always mirrors the sidebar one, including decorations like "main|MERGING"
+ function setBranchName(branchName, tooltip) {
+ const $branch = $gitPanel.find(".git-panel-branch");
+ $branch.attr("title", tooltip || "");
+ $branch.find(".git-branch-name").text(branchName);
+ }
+
function refreshCommitCounts() {
// Find Push and Pull buttons
var $pullBtn = $gitPanel.find(".git-pull");
@@ -895,7 +904,8 @@ define(function (require, exports) {
$gitPanel.find(".git-file-history").removeClass("active").attr("title", Strings.TOOLTIP_SHOW_FILE_HISTORY);
if (gitPanelMode === "not-repo") {
- $tableContainer.empty();
+ $tableContainer.html($("")
+ .text(StringUtils.format(Strings.GIT_NOT_REPO_MESSAGE, Strings.GIT_INIT, Strings.GIT_CLONE)));
return Promise.resolve();
}
@@ -1007,6 +1017,64 @@ define(function (require, exports) {
});
}
+ const OPEN_ALL_CONFIRM_THRESHOLD = 50;
+
+ function _openChangedFiles(fileList) {
+ MainViewManager.addListToWorkingSet(MainViewManager.ACTIVE_PANE, fileList);
+ if (!MainViewManager.getCurrentlyViewedPath(MainViewManager.ACTIVE_PANE)) {
+ CommandManager.execute(Commands.FILE_OPEN, { fullPath: fileList[0].fullPath });
+ }
+ }
+
+ function openAllChangedFiles() {
+ return Git.status().then(function (files) {
+ files = _.filter(files, function (file) {
+ if (!shouldShow(file)) {
+ return false;
+ }
+ // deleted files no longer exist on disk, so there is nothing to open
+ if (file.status.indexOf(Git.FILE_STATUS.DELETED) !== -1) {
+ return false;
+ }
+ // respect the "show untracked files" panel toggle
+ if (!showingUntracked && file.status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1) {
+ return false;
+ }
+ return true;
+ });
+
+ if (files.length === 0) {
+ return;
+ }
+
+ const currentGitRoot = Preferences.get("currentGitRoot");
+ const fileList = files.map(function (file) {
+ return FileSystem.getFileForPath(currentGitRoot + file.file);
+ });
+
+ // count only the files the command would actually add, so re-running it
+ // when most of them are already open doesn't ask for confirmation again
+ const newFileCount = _.filter(fileList, function (file) {
+ return MainViewManager.findInAllWorkingSets(file.fullPath).length === 0;
+ }).length;
+
+ if (newFileCount > OPEN_ALL_CONFIRM_THRESHOLD) {
+ return Utils.askQuestion(Strings.CMD_OPEN_CHANGED_FILES,
+ StringUtils.format(Strings.OPEN_CHANGED_FILES_CONFIRM, newFileCount),
+ { booleanResponse: true })
+ .then(function (response) {
+ if (response === true) {
+ _openChangedFiles(fileList);
+ }
+ });
+ }
+
+ _openChangedFiles(fileList);
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_OPENING_CHANGED_FILES);
+ });
+ }
+
var lastCheckOneClicked = null;
function attachDefaultTableHandlers() {
@@ -1214,6 +1282,7 @@ define(function (require, exports) {
const mainToolbarWidth = $mainToolbar.width();
let overFlowWidth = 540;
const breakpoints = [
+ { width: 600, className: "hide-when-medium" },
{ width: overFlowWidth, className: "hide-when-small" },
{ width: 400, className: "hide-when-x-small" }
];
@@ -1345,6 +1414,8 @@ define(function (require, exports) {
CommandManager.register(Strings.VIEW_AUTHORS_SELECTION, Constants.CMD_GIT_AUTHORS_OF_SELECTION, handleAuthorsSelection);
CommandManager.register(Strings.VIEW_AUTHORS_FILE, Constants.CMD_GIT_AUTHORS_OF_FILE, handleAuthorsFile);
CommandManager.register(Strings.HIDE_UNTRACKED, Constants.CMD_GIT_TOGGLE_UNTRACKED, handleToggleUntracked);
+ CommandManager.register(Strings.CMD_OPEN_CHANGED_FILES,
+ Constants.CMD_GIT_OPEN_CHANGED_FILES, openAllChangedFiles);
CommandManager.register(Strings.GIT_INIT, Constants.CMD_GIT_INIT, EventEmitter.getEmitter(Events.HANDLE_GIT_INIT));
CommandManager.register(Strings.GIT_CLONE, Constants.CMD_GIT_CLONE, EventEmitter.getEmitter(Events.HANDLE_GIT_CLONE));
CommandManager.register(Strings.GIT_SHOW_HISTORY, Constants.CMD_GIT_HISTORY_GLOBAL, ()=>{
@@ -1555,6 +1626,7 @@ define(function (require, exports) {
exports.toggle = toggle;
exports.enable = enable;
exports.disable = disable;
+ exports.setBranchName = setBranchName;
exports.getSelectedHistoryCommit = getSelectedHistoryCommit;
exports.getPanel = function () { return $gitPanel; };
diff --git a/src/extensions/default/Git/src/git/GitCli.js b/src/extensions/default/Git/src/git/GitCli.js
index 16d269776e..e62768219e 100644
--- a/src/extensions/default/Git/src/git/GitCli.js
+++ b/src/extensions/default/Git/src/git/GitCli.js
@@ -385,25 +385,46 @@ define(function (require, exports) {
return doPushWithArgs(args, progressTracker);
}
+ const PUSH_PORCELAIN_TO_LINE = new RegExp("^To\\s");
+ const PUSH_PORCELAIN_REF_LINE = new RegExp("^[ +\\-*!=]\\t[^\\t]*:[^\\t]*\\t");
+
function doPushWithArgs(args, progressTracker) {
return git(args, {progressTracker})
.catch(repositoryNotFoundHandler)
.then(function (stdout) {
- // this should clear lines from push hooks
- var lines = stdout.split("\n");
- while (lines.length > 0 && lines[0].match(/^To/) === null) {
- lines.shift();
+ // pre-push hooks print to stdout before the porcelain block and may
+ // even print lines starting with "To" (Eg. "Total size..."), so the
+ // "To " line only counts when a ref status line follows it.
+ // the block is at the end of the output, so scan backwards.
+ const lines = stdout.split("\n");
+ let headerIndex = -1;
+ for (let i = lines.length - 2; i >= 0; i--) {
+ if (PUSH_PORCELAIN_TO_LINE.test(lines[i]) && PUSH_PORCELAIN_REF_LINE.test(lines[i + 1])) {
+ headerIndex = i;
+ break;
+ }
+ }
+
+ const retObj = {};
+ if (headerIndex === -1) {
+ // the push itself succeeded as git exited with 0, we just can't
+ // parse the output. never report a successful push as an error.
+ retObj.flagDescription = Strings.GIT_PUSH_SUCCESS_MSG;
+ retObj.from = "";
+ retObj.to = "";
+ retObj.summary = "";
+ retObj.status = "";
+ return retObj;
}
- var retObj = {},
- lineTwo = lines[1].split("\t");
+ const lineTwo = lines[headerIndex + 1].split("\t");
- retObj.remoteUrl = lines[0].trim().split(" ")[1];
+ retObj.remoteUrl = lines[headerIndex].trim().split(" ")[1];
retObj.flag = lineTwo[0];
retObj.from = lineTwo[1].split(":")[0];
retObj.to = lineTwo[1].split(":")[1];
retObj.summary = lineTwo[2];
- retObj.status = lines[2];
+ retObj.status = lines[headerIndex + 2] || "";
switch (retObj.flag) {
case " ":
diff --git a/src/extensions/default/Git/styles/git-styles.less b/src/extensions/default/Git/styles/git-styles.less
index 0cf885384b..afdc6b31cf 100644
--- a/src/extensions/default/Git/styles/git-styles.less
+++ b/src/extensions/default/Git/styles/git-styles.less
@@ -1,4 +1,5 @@
@import "../../../../styles/brackets_core_ui_variables.less";
+@import (reference) "../../../../styles/brackets_variables.less";
// common
@git-moreDarkGrey: #868888;
@@ -451,6 +452,16 @@
width: 50px;
}
}
+ tbody tr.history-empty-message td {
+ width: auto;
+ padding: 15px;
+ white-space: normal;
+ text-align: left;
+ background: none;
+ color: inherit;
+ border: none;
+ cursor: default;
+ }
}
#history-viewer {
@@ -930,6 +941,7 @@
margin-left: -12px;
position: absolute;
display: block;
+ min-width: 200px;
max-width: none;
z-index: 100;
overflow-y: auto;
@@ -950,6 +962,7 @@
}
.git-branch-link {
position: relative;
+ padding-right: 62px;
.switch-branch {
display: inline-block;
width: 100%;
@@ -959,30 +972,39 @@
.trash-icon, .merge-branch {
position: absolute;
opacity: 0;
- top: 27%;
+ top: 50%;
+ transform: translateY(-50%);
background-image: none !important;
- width: 16px;
- height: 16px;
- font-size: 20px;
- color: rgba(0, 0, 0, 0.5);
- line-height: 15px;
+ width: 20px;
+ height: 20px;
+ font-size: 13px;
+ color: fade(@bc-menu-text, 55%);
+ line-height: 20px;
text-align: center;
- &:hover {
- color: rgba(0, 0, 0, 1);
+ .dark & {
+ color: fade(@dark-bc-menu-text, 55%);
}
- }
- .trash-icon, .merge-branch {
&:hover {
- color: rgba(0, 0, 0, 1);
+ color: @bc-menu-text;
+ .dark & {
+ color: @dark-bc-menu-text;
+ }
}
}
- &:hover {
+ &:hover, &.selected {
.trash-icon, .merge-branch {
opacity: 1;
}
}
.merge-branch {
- right: 5px;
+ right: 34px;
+ .octicon {
+ font-size: 15px;
+ }
+ }
+ .trash-icon {
+ left: auto;
+ right: 8px;
}
}
a {
@@ -1088,6 +1110,28 @@
right: 32px;
top: 5px;
}
+ .git-panel-branch {
+ display: inline-block;
+ vertical-align: middle;
+ box-sizing: border-box;
+ height: 22px;
+ margin-top: 2px;
+ margin-right: 8px;
+ padding: 2px 10px;
+ font-size: 12px;
+ line-height: 16px;
+ white-space: nowrap;
+ cursor: default;
+ i {
+ margin-right: 4px;
+ }
+ &:not(.clickable) {
+ opacity: .5;
+ }
+ &.clickable {
+ cursor: pointer;
+ }
+ }
.octicon:not(:only-child) {
margin-right: 5px;
vertical-align: -1px;
@@ -1103,21 +1147,42 @@
color: @dark-bc-text;
}
}
+ .mainToolbar:has(.btn-group.open) {
+ z-index: (@z-index-brackets-panel-resizer + 1);
+ }
.git-remotes {
border-radius: 4px 0 0 4px;
- padding-bottom: 5px;
+ display: inline-flex;
+ align-items: center;
+ &:focus {
+ border: 1px solid @bc-btn-border;
+ box-shadow: inset 0 1px @bc-highlight-hard;
+
+ .dark & {
+ border: 1px solid @dark-bc-btn-border;
+ box-shadow: inset 0 1px @dark-bc-highlight;
+ }
+ }
.caret {
border-bottom-color: @bc-text;
- margin: 7px 5px auto 0px;
+ margin: 0 5px 0 0;
.dark & {
border-bottom-color: @dark-bc-text;
}
}
+ .git-selected-remote {
+ position: relative;
+ top: -1px;
+ }
}
.git-remotes-dropdown {
// don't mess with this, the dropdown menu is at the top so it should grow from bottom left to top right.
-webkit-transform-origin: 0 100%;
+ font-size: 14px;
+ .divider {
+ margin: 5px 1px;
+ }
}
.git-remotes-dropdown a {
.change-remote {
@@ -1140,9 +1205,6 @@
&:hover .hover-icon {
opacity: 1;
}
- &[class$="-remote-new"] {
- font-style: italic;
- }
}
.dropdown-menu();
diff --git a/src/extensions/default/Git/templates/git-branches-menu.html b/src/extensions/default/Git/templates/git-branches-menu.html
index e9763993f3..11ca741d94 100644
--- a/src/extensions/default/Git/templates/git-branches-menu.html
+++ b/src/extensions/default/Git/templates/git-branches-menu.html
@@ -12,11 +12,11 @@
{{#branchList}}
+ {{name}}
+
{{#canDelete}}
- ×
+
{{/canDelete}}
-
- {{name}}
{{/branchList}}
diff --git a/src/extensions/default/Git/templates/git-panel-history.html b/src/extensions/default/Git/templates/git-panel-history.html
index c4ee9bbd3f..763af2c7da 100644
--- a/src/extensions/default/Git/templates/git-panel-history.html
+++ b/src/extensions/default/Git/templates/git-panel-history.html
@@ -1,5 +1,8 @@
{{> commits}}
+ {{^commits}}
+ | {{emptyMessage}} |
+ {{/commits}}
diff --git a/src/extensions/default/Git/templates/git-panel.html b/src/extensions/default/Git/templates/git-panel.html
index b0133c2c87..132d7cce07 100644
--- a/src/extensions/default/Git/templates/git-panel.html
+++ b/src/extensions/default/Git/templates/git-panel.html
@@ -57,6 +57,9 @@
+