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}} + + {{/commits}}
    {{emptyMessage}}
    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 @@
    +