From d704779388d091cf14f328d0a376322e34765553 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 14 Jan 2025 20:40:53 +0530 Subject: [PATCH 1/2] feat: adding git extension to core, but not loaded for now. working --- src/extensions/default/Git/LICENSE | 19 + src/extensions/default/Git/main.js | 52 + src/extensions/default/Git/package.json | 10 + .../default/Git/requirejs-config.json | 1 + .../default/Git/src/BracketsEvents.js | 81 + src/extensions/default/Git/src/Branch.js | 541 +++++++ src/extensions/default/Git/src/Cli.js | 240 +++ .../default/Git/src/CloseNotModified.js | 65 + src/extensions/default/Git/src/Constants.js | 58 + .../default/Git/src/ErrorHandler.js | 89 ++ .../default/Git/src/EventEmitter.js | 40 + src/extensions/default/Git/src/Events.js | 59 + .../default/Git/src/ExpectedError.js | 17 + .../default/Git/src/GutterManager.js | 379 +++++ src/extensions/default/Git/src/History.js | 329 ++++ .../default/Git/src/HistoryViewer.js | 293 ++++ src/extensions/default/Git/src/Main.js | 434 +++++ src/extensions/default/Git/src/NoRepo.js | 163 ++ src/extensions/default/Git/src/Panel.js | 1414 +++++++++++++++++ src/extensions/default/Git/src/Preferences.js | 97 ++ .../default/Git/src/ProjectTreeMarks.js | 204 +++ src/extensions/default/Git/src/Remotes.js | 390 +++++ .../default/Git/src/SettingsDialog.js | 124 ++ src/extensions/default/Git/src/Utils.js | 608 +++++++ .../default/Git/src/dialogs/Clone.js | 73 + .../default/Git/src/dialogs/Progress.js | 151 ++ .../default/Git/src/dialogs/Pull.js | 65 + .../default/Git/src/dialogs/Push.js | 63 + .../default/Git/src/dialogs/RemoteCommon.js | 143 ++ .../src/dialogs/templates/clone-dialog.html | 66 + .../dialogs/templates/progress-dialog.html | 14 + .../src/dialogs/templates/pull-dialog.html | 98 ++ .../src/dialogs/templates/push-dialog.html | 98 ++ .../dialogs/templates/remotes-template.html | 39 + src/extensions/default/Git/src/git/Git.js | 198 +++ src/extensions/default/Git/src/git/GitCli.js | 1162 ++++++++++++++ src/extensions/default/Git/src/utils/Setup.js | 126 ++ .../default/Git/styles/brackets-git.less | 673 ++++++++ .../brackets/brackets_core_ui_variables.less | 211 +++ .../default/Git/styles/code-mirror.less | 53 + src/extensions/default/Git/styles/colors.less | 36 + .../default/Git/styles/commit-diff.less | 168 ++ src/extensions/default/Git/styles/common.less | 124 ++ .../default/Git/styles/dialogs-all.less | 53 + .../default/Git/styles/fonts/octicon.less | 611 +++++++ .../styles/fonts/octicons-regular-webfont.eot | Bin 0 -> 35820 bytes .../styles/fonts/octicons-regular-webfont.svg | 215 +++ .../styles/fonts/octicons-regular-webfont.ttf | Bin 0 -> 35652 bytes .../fonts/octicons-regular-webfont.woff | Bin 0 -> 19016 bytes .../default/Git/styles/history.less | 299 ++++ .../default/Git/styles/icons/git-icon.svg | 19 + src/extensions/default/Git/styles/mixins.less | 72 + .../default/Git/templates/authors-dialog.html | 29 + .../Git/templates/branch-merge-dialog.html | 49 + .../Git/templates/branch-new-dialog.html | 32 + .../default/Git/templates/default-gitignore | 5 + .../default/Git/templates/format-diff.html | 17 + .../Git/templates/git-branches-menu.html | 23 + .../git-commit-dialog-lint-results.html | 18 + .../Git/templates/git-commit-dialog.html | 40 + .../Git/templates/git-diff-dialog.html | 11 + .../Git/templates/git-error-dialog.html | 12 + .../default/Git/templates/git-output.html | 19 + .../templates/git-panel-history-commits.html | 12 + .../Git/templates/git-panel-history.html | 5 + .../Git/templates/git-panel-results.html | 20 + .../default/Git/templates/git-panel.html | 76 + .../Git/templates/git-question-dialog.html | 20 + .../Git/templates/git-remotes-picker.html | 10 + .../Git/templates/git-settings-dialog.html | 109 ++ .../default/Git/templates/git-tag-dialog.html | 15 + .../Git/templates/history-viewer-files.html | 12 + .../default/Git/templates/history-viewer.html | 46 + src/nls/root/strings.js | 269 +++- 74 files changed, 11385 insertions(+), 1 deletion(-) create mode 100644 src/extensions/default/Git/LICENSE create mode 100644 src/extensions/default/Git/main.js create mode 100644 src/extensions/default/Git/package.json create mode 100644 src/extensions/default/Git/requirejs-config.json create mode 100644 src/extensions/default/Git/src/BracketsEvents.js create mode 100644 src/extensions/default/Git/src/Branch.js create mode 100644 src/extensions/default/Git/src/Cli.js create mode 100644 src/extensions/default/Git/src/CloseNotModified.js create mode 100644 src/extensions/default/Git/src/Constants.js create mode 100644 src/extensions/default/Git/src/ErrorHandler.js create mode 100644 src/extensions/default/Git/src/EventEmitter.js create mode 100644 src/extensions/default/Git/src/Events.js create mode 100644 src/extensions/default/Git/src/ExpectedError.js create mode 100644 src/extensions/default/Git/src/GutterManager.js create mode 100644 src/extensions/default/Git/src/History.js create mode 100644 src/extensions/default/Git/src/HistoryViewer.js create mode 100644 src/extensions/default/Git/src/Main.js create mode 100644 src/extensions/default/Git/src/NoRepo.js create mode 100644 src/extensions/default/Git/src/Panel.js create mode 100644 src/extensions/default/Git/src/Preferences.js create mode 100644 src/extensions/default/Git/src/ProjectTreeMarks.js create mode 100644 src/extensions/default/Git/src/Remotes.js create mode 100644 src/extensions/default/Git/src/SettingsDialog.js create mode 100644 src/extensions/default/Git/src/Utils.js create mode 100644 src/extensions/default/Git/src/dialogs/Clone.js create mode 100644 src/extensions/default/Git/src/dialogs/Progress.js create mode 100644 src/extensions/default/Git/src/dialogs/Pull.js create mode 100644 src/extensions/default/Git/src/dialogs/Push.js create mode 100644 src/extensions/default/Git/src/dialogs/RemoteCommon.js create mode 100644 src/extensions/default/Git/src/dialogs/templates/clone-dialog.html create mode 100644 src/extensions/default/Git/src/dialogs/templates/progress-dialog.html create mode 100644 src/extensions/default/Git/src/dialogs/templates/pull-dialog.html create mode 100644 src/extensions/default/Git/src/dialogs/templates/push-dialog.html create mode 100644 src/extensions/default/Git/src/dialogs/templates/remotes-template.html create mode 100644 src/extensions/default/Git/src/git/Git.js create mode 100644 src/extensions/default/Git/src/git/GitCli.js create mode 100644 src/extensions/default/Git/src/utils/Setup.js create mode 100644 src/extensions/default/Git/styles/brackets-git.less create mode 100644 src/extensions/default/Git/styles/brackets/brackets_core_ui_variables.less create mode 100644 src/extensions/default/Git/styles/code-mirror.less create mode 100644 src/extensions/default/Git/styles/colors.less create mode 100644 src/extensions/default/Git/styles/commit-diff.less create mode 100644 src/extensions/default/Git/styles/common.less create mode 100644 src/extensions/default/Git/styles/dialogs-all.less create mode 100644 src/extensions/default/Git/styles/fonts/octicon.less create mode 100644 src/extensions/default/Git/styles/fonts/octicons-regular-webfont.eot create mode 100644 src/extensions/default/Git/styles/fonts/octicons-regular-webfont.svg create mode 100644 src/extensions/default/Git/styles/fonts/octicons-regular-webfont.ttf create mode 100644 src/extensions/default/Git/styles/fonts/octicons-regular-webfont.woff create mode 100644 src/extensions/default/Git/styles/history.less create mode 100644 src/extensions/default/Git/styles/icons/git-icon.svg create mode 100644 src/extensions/default/Git/styles/mixins.less create mode 100644 src/extensions/default/Git/templates/authors-dialog.html create mode 100644 src/extensions/default/Git/templates/branch-merge-dialog.html create mode 100644 src/extensions/default/Git/templates/branch-new-dialog.html create mode 100644 src/extensions/default/Git/templates/default-gitignore create mode 100644 src/extensions/default/Git/templates/format-diff.html create mode 100644 src/extensions/default/Git/templates/git-branches-menu.html create mode 100644 src/extensions/default/Git/templates/git-commit-dialog-lint-results.html create mode 100644 src/extensions/default/Git/templates/git-commit-dialog.html create mode 100644 src/extensions/default/Git/templates/git-diff-dialog.html create mode 100644 src/extensions/default/Git/templates/git-error-dialog.html create mode 100644 src/extensions/default/Git/templates/git-output.html create mode 100644 src/extensions/default/Git/templates/git-panel-history-commits.html create mode 100644 src/extensions/default/Git/templates/git-panel-history.html create mode 100644 src/extensions/default/Git/templates/git-panel-results.html create mode 100644 src/extensions/default/Git/templates/git-panel.html create mode 100644 src/extensions/default/Git/templates/git-question-dialog.html create mode 100644 src/extensions/default/Git/templates/git-remotes-picker.html create mode 100644 src/extensions/default/Git/templates/git-settings-dialog.html create mode 100644 src/extensions/default/Git/templates/git-tag-dialog.html create mode 100644 src/extensions/default/Git/templates/history-viewer-files.html create mode 100644 src/extensions/default/Git/templates/history-viewer.html diff --git a/src/extensions/default/Git/LICENSE b/src/extensions/default/Git/LICENSE new file mode 100644 index 0000000000..df712b341d --- /dev/null +++ b/src/extensions/default/Git/LICENSE @@ -0,0 +1,19 @@ +GNU AGPL-3.0 License + +Copyright (c) 2021 - present core.ai . All rights reserved. +original work Copyright (c) 2013-2014 Martin Zagora and other contributors + +This program is free software: you can redistribute it and/or modify it +under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + + diff --git a/src/extensions/default/Git/main.js b/src/extensions/default/Git/main.js new file mode 100644 index 0000000000..5ec801328a --- /dev/null +++ b/src/extensions/default/Git/main.js @@ -0,0 +1,52 @@ +/*! + * Brackets Git Extension + * + * @author Martin Zagora + * @license http://opensource.org/licenses/MIT + */ + +define(function (require, exports, module) { + + // Brackets modules + const _ = brackets.getModule("thirdparty/lodash"), + AppInit = brackets.getModule("utils/AppInit"), + ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); + + // Local modules + require("src/SettingsDialog"); + const EventEmitter = require("src/EventEmitter"), + Events = require("src/Events"), + Main = require("src/Main"), + Preferences = require("src/Preferences"), + BracketsEvents = require("src/BracketsEvents"); + + // Load extension modules that are not included by core + var modules = [ + "src/GutterManager", + "src/History", + "src/NoRepo", + "src/ProjectTreeMarks", + "src/Remotes" + ]; + require(modules); + + // Load CSS + ExtensionUtils.loadStyleSheet(module, "styles/brackets-git.less"); + ExtensionUtils.loadStyleSheet(module, "styles/fonts/octicon.less"); + + AppInit.appReady(function () { + Main.init().then((enabled)=>{ + if(!enabled) { + BracketsEvents.disableAll(); + } + }); + }); + + // export API's for other extensions + if (typeof window === "object") { + window.phoenixGitEvents = { + EventEmitter: EventEmitter, + Events: Events + }; + } +}); diff --git a/src/extensions/default/Git/package.json b/src/extensions/default/Git/package.json new file mode 100644 index 0000000000..90b8c4feec --- /dev/null +++ b/src/extensions/default/Git/package.json @@ -0,0 +1,10 @@ +{ + "name": "phcode-git-core", + "title": "Phoenix Code Git", + "version": "1.0.0", + "engines": { + "brackets": ">=4.0.0" + }, + "description": "Integration of Git into Phoenix Code", + "dependencies": {} +} diff --git a/src/extensions/default/Git/requirejs-config.json b/src/extensions/default/Git/requirejs-config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/src/extensions/default/Git/requirejs-config.json @@ -0,0 +1 @@ +{} diff --git a/src/extensions/default/Git/src/BracketsEvents.js b/src/extensions/default/Git/src/BracketsEvents.js new file mode 100644 index 0000000000..7848accd37 --- /dev/null +++ b/src/extensions/default/Git/src/BracketsEvents.js @@ -0,0 +1,81 @@ +define(function (require, exports, module) { + + // Brackets modules + const _ = brackets.getModule("thirdparty/lodash"), + DocumentManager = brackets.getModule("document/DocumentManager"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + ProjectManager = brackets.getModule("project/ProjectManager"), + MainViewManager = brackets.getModule("view/MainViewManager"); + + // Local modules + const Events = require("src/Events"), + EventEmitter = require("src/EventEmitter"), + HistoryViewer = require("src/HistoryViewer"), + Preferences = require("src/Preferences"), + Utils = require("src/Utils"); + + // White-list for .git file watching + const watchedInsideGit = ["HEAD"]; + const GIT_EVENTS = "gitEvents"; + + FileSystem.on(`change.${GIT_EVENTS}`, function (evt, file) { + // we care only for files in current project + var currentGitRoot = Preferences.get("currentGitRoot"); + if (file && file.fullPath.indexOf(currentGitRoot) === 0) { + + if (file.fullPath.indexOf(currentGitRoot + ".git/") === 0) { + + var whitelisted = _.any(watchedInsideGit, function (entry) { + return file.fullPath === currentGitRoot + ".git/" + entry; + }); + if (!whitelisted) { + Utils.consoleDebug("Ignored FileSystem.change event: " + file.fullPath); + return; + } + + } + + EventEmitter.emit(Events.BRACKETS_FILE_CHANGED, file); + } + }); + + DocumentManager.on(`documentSaved.${GIT_EVENTS}`, function (evt, doc) { + // we care only for files in current project + if (doc.file.fullPath.indexOf(Preferences.get("currentGitRoot")) === 0) { + EventEmitter.emit(Events.BRACKETS_DOCUMENT_SAVED, doc); + } + }); + + MainViewManager.on(`currentFileChange.${GIT_EVENTS}`, function (evt, currentDocument, previousDocument) { + currentDocument = currentDocument || DocumentManager.getCurrentDocument(); + if (!HistoryViewer.isVisible()) { + EventEmitter.emit(Events.BRACKETS_CURRENT_DOCUMENT_CHANGE, currentDocument, previousDocument); + } else { + HistoryViewer.hide(); + } + }); + + ProjectManager.on(`projectOpen.${GIT_EVENTS}`, function () { + EventEmitter.emit(Events.BRACKETS_PROJECT_CHANGE); + }); + + ProjectManager.on(`projectRefresh.${GIT_EVENTS}`, function () { + EventEmitter.emit(Events.BRACKETS_PROJECT_REFRESH); + }); + + ProjectManager.on(`beforeProjectClose.${GIT_EVENTS}`, function () { + // Disable Git when closing a project so listeners won't fire before new is opened + EventEmitter.emit(Events.GIT_DISABLED); + }); + + function disableAll() { + FileSystem.off(`change.${GIT_EVENTS}`); + DocumentManager.off(`documentSaved.${GIT_EVENTS}`); + MainViewManager.off(`currentFileChange.${GIT_EVENTS}`); + ProjectManager.off(`projectOpen.${GIT_EVENTS}`); + ProjectManager.off(`projectRefresh.${GIT_EVENTS}`); + ProjectManager.off(`beforeProjectClose.${GIT_EVENTS}`); + } + + exports.disableAll = disableAll; +}); diff --git a/src/extensions/default/Git/src/Branch.js b/src/extensions/default/Git/src/Branch.js new file mode 100644 index 0000000000..e71bf763d1 --- /dev/null +++ b/src/extensions/default/Git/src/Branch.js @@ -0,0 +1,541 @@ +define(function (require, exports) { + + var _ = brackets.getModule("thirdparty/lodash"), + CommandManager = brackets.getModule("command/CommandManager"), + Dialogs = brackets.getModule("widgets/Dialogs"), + EditorManager = brackets.getModule("editor/EditorManager"), + FileSyncManager = brackets.getModule("project/FileSyncManager"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + Menus = brackets.getModule("command/Menus"), + Mustache = brackets.getModule("thirdparty/mustache/mustache"), + PopUpManager = brackets.getModule("widgets/PopUpManager"), + StringUtils = brackets.getModule("utils/StringUtils"), + DocumentManager = brackets.getModule("document/DocumentManager"), + Strings = brackets.getModule("strings"), + MainViewManager = brackets.getModule("view/MainViewManager"); + + var Git = require("src/git/Git"), + Events = require("src/Events"), + EventEmitter = require("src/EventEmitter"), + ErrorHandler = require("src/ErrorHandler"), + Panel = require("src/Panel"), + Setup = require("src/utils/Setup"), + Preferences = require("src/Preferences"), + ProgressDialog = require("src/dialogs/Progress"), + Utils = require("src/Utils"), + branchesMenuTemplate = require("text!templates/git-branches-menu.html"), + newBranchTemplate = require("text!templates/branch-new-dialog.html"), + mergeBranchTemplate = require("text!templates/branch-merge-dialog.html"); + + var $gitBranchName = $(null), + currentEditor, + $dropdown; + + function renderList(branches) { + branches = branches.map(function (name) { + return { + name: name, + currentBranch: name.indexOf("* ") === 0, + canDelete: name !== "master" + }; + }); + var templateVars = { + branchList: _.filter(branches, function (o) { return !o.currentBranch; }), + Strings: Strings + }; + return Mustache.render(branchesMenuTemplate, templateVars); + } + + function closeDropdown() { + if ($dropdown) { + PopUpManager.removePopUp($dropdown); + } + detachCloseEvents(); + } + + function doMerge(fromBranch) { + Git.getBranches().then(function (branches) { + + var compiledTemplate = Mustache.render(mergeBranchTemplate, { + fromBranch: fromBranch, + branches: branches, + Strings: Strings + }); + + var dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); + var $dialog = dialog.getElement(); + $dialog.find("input").focus(); + + var $toBranch = $dialog.find("[name='branch-target']"); + var $useRebase = $dialog.find("[name='use-rebase']"); + var $useNoff = $dialog.find("[name='use-noff']"); + + if (fromBranch === "master") { + $useRebase.prop("checked", true); + } + if ($toBranch.val() === "master") { + $useRebase.prop("checked", false).prop("disabled", true); + } + + // fill merge message if possible + var $mergeMessage = $dialog.find("[name='merge-message']"); + $mergeMessage.attr("placeholder", "Merge branch '" + fromBranch + "'"); + $dialog.find(".fill-pr").on("click", function () { + var prMsg = "Merge pull request #??? from " + fromBranch; + $mergeMessage.val(prMsg); + $mergeMessage[0].setSelectionRange(prMsg.indexOf("???"), prMsg.indexOf("???") + 3); + }); + + // can't use rebase and no-ff together so have a change handler for this + $useRebase.on("change", function () { + var useRebase = $useRebase.prop("checked"); + $useNoff.prop("disabled", useRebase); + if (useRebase) { $useNoff.prop("checked", false); } + }).trigger("change"); + + dialog.done(function (buttonId) { + // right now only merge to current branch without any configuration + // later delete merge branch and so ... + var useRebase = $useRebase.prop("checked"); + var useNoff = $useNoff.prop("checked"); + var mergeMsg = $mergeMessage.val(); + + if (buttonId === "ok") { + + if (useRebase) { + + Git.rebaseInit(fromBranch).catch(function (err) { + throw ErrorHandler.showError(err, Strings.ERROR_REBASE_FAILED); + }).then(function (stdout) { + Utils.showOutput(stdout || Strings.GIT_REBASE_SUCCESS, Strings.REBASE_RESULT).finally(function () { + EventEmitter.emit(Events.REFRESH_ALL); + }); + + }); + } else { + + Git.mergeBranch(fromBranch, mergeMsg, useNoff).catch(function (err) { + throw ErrorHandler.showError(err, Strings.ERROR_MERGE_FAILED); + }).then(function (stdout) { + Utils.showOutput(stdout || Strings.GIT_MERGE_SUCCESS, Strings.MERGE_RESULT).finally(function () { + EventEmitter.emit(Events.REFRESH_ALL); + }); + }); + } + } + }); + }); + } + + function _reloadBranchSelect($el, branches) { + var template = "{{#branches}}{{/branches}}"; + var html = Mustache.render(template, { branches: branches }); + $el.html(html); + } + + function closeNotExistingFiles(oldBranchName, newBranchName) { + return Git.getDeletedFiles(oldBranchName, newBranchName).then(function (deletedFiles) { + + var gitRoot = Preferences.get("currentGitRoot"), + openedFiles = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES); + + // Close files that does not exists anymore in the new selected branch + deletedFiles.forEach(function (dFile) { + var oFile = _.find(openedFiles, function (oFile) { + return oFile.fullPath === gitRoot + dFile; + }); + if (oFile) { + DocumentManager.closeFullEditor(oFile); + } + }); + + EventEmitter.emit(Events.REFRESH_ALL); + + }).catch(function (err) { + ErrorHandler.showError(err, Strings.ERROR_GETTING_DELETED_FILES); + }); + } + + function handleEvents() { + $dropdown.on("click", "a.git-branch-new", function (e) { + e.stopPropagation(); + closeDropdown(); + + Git.getAllBranches().catch(function (err) { + ErrorHandler.showError(err); + }).then(function (branches) { + + var compiledTemplate = Mustache.render(newBranchTemplate, { + branches: branches, + Strings: Strings + }); + + var dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate); + + var $input = dialog.getElement().find("[name='branch-name']"), + $select = dialog.getElement().find(".branchSelect"); + + $select.on("change", function () { + if (!$input.val()) { + var $opt = $select.find(":selected"), + remote = $opt.attr("remote"), + newVal = $opt.val(); + if (remote) { + newVal = newVal.substring(remote.length + 1); + if (remote !== "origin") { + newVal = remote + "#" + newVal; + } + } + $input.val(newVal); + } + }); + + _reloadBranchSelect($select, branches); + dialog.getElement().find(".fetchBranches").on("click", function () { + var $this = $(this); + const tracker = ProgressDialog.newProgressTracker(); + ProgressDialog.show(Git.fetchAllRemotes(tracker), tracker) + .then(function () { + return Git.getAllBranches().then(function (branches) { + $this.prop("disabled", true).attr("title", "Already fetched"); + _reloadBranchSelect($select, branches); + }); + }).catch(function (err) { + throw ErrorHandler.showError(err, Strings.ERROR_FETCH_REMOTE_INFO); + }); + }); + + dialog.getElement().find("input").focus(); + dialog.done(function (buttonId) { + if (buttonId === "ok") { + + var $dialog = dialog.getElement(), + branchName = $dialog.find("input[name='branch-name']").val().trim(), + $option = $dialog.find("select[name='branch-origin']").children("option:selected"), + originName = $option.val(), + isRemote = $option.attr("remote"), + track = !!isRemote; + + Git.createBranch(branchName, originName, track).catch(function (err) { + throw ErrorHandler.showError(err, Strings.ERROR_CREATE_BRANCH); + }).then(function () { + EventEmitter.emit(Events.REFRESH_ALL); + }); + } + }); + }); + + }).on("mouseenter", "a", function () { + $(this).addClass("selected"); + }).on("mouseleave", "a", function () { + $(this).removeClass("selected"); + }).on("click", "a.git-branch-link .trash-icon", function (e) { + e.stopPropagation(); + closeDropdown(); + var branchName = $(this).parent().data("branch"); + Utils.askQuestion(Strings.DELETE_LOCAL_BRANCH, + StringUtils.format(Strings.DELETE_LOCAL_BRANCH_NAME, branchName), + { booleanResponse: true }) + .then(function (response) { + if (response === true) { + return Git.branchDelete(branchName).catch(function (err) { + + return Utils.showOutput(err, "Branch deletion failed", { + question: "Do you wish to force branch deletion?" + }).then(function (response) { + if (response === true) { + return Git.forceBranchDelete(branchName).then(function (output) { + return Utils.showOutput(output || Strings.GIT_BRANCH_DELETE_SUCCESS); + }).catch(function (err) { + ErrorHandler.showError(err, Strings.ERROR_BRANCH_DELETE_FORCED); + }); + } + }); + + }); + } + }) + .catch(function (err) { + ErrorHandler.showError(err); + }); + + }).on("click", ".merge-branch", function (e) { + e.stopPropagation(); + closeDropdown(); + var fromBranch = $(this).parent().data("branch"); + doMerge(fromBranch); + }).on("click", "a.git-branch-link", function (e) { + + e.stopPropagation(); + closeDropdown(); + var newBranchName = $(this).data("branch"); + + Git.getCurrentBranchName().then(function (oldBranchName) { + Git.checkout(newBranchName).then(function () { + return closeNotExistingFiles(oldBranchName, newBranchName); + }).catch(function (err) { + throw ErrorHandler.showError(err, Strings.ERROR_SWITCHING_BRANCHES); + }); + }).catch(function (err) { + throw ErrorHandler.showError(err, Strings.ERROR_GETTING_CURRENT_BRANCH); + }); + + }); + } + + function attachCloseEvents() { + $("html").on("click", closeDropdown); + $("#project-files-container").on("scroll", closeDropdown); + $("#titlebar .nav").on("click", closeDropdown); + + currentEditor = EditorManager.getCurrentFullEditor(); + if (currentEditor) { + currentEditor._codeMirror.on("focus", closeDropdown); + } + + // $(window).on("keydown", keydownHook); + } + + function detachCloseEvents() { + $("html").off("click", closeDropdown); + $("#project-files-container").off("scroll", closeDropdown); + $("#titlebar .nav").off("click", closeDropdown); + + if (currentEditor) { + currentEditor._codeMirror.off("focus", closeDropdown); + } + + // $(window).off("keydown", keydownHook); + + $dropdown = null; + } + + function toggleDropdown(e) { + e.stopPropagation(); + + // If the dropdown is already visible, close it + if ($dropdown) { + closeDropdown(); + return; + } + + Menus.closeAll(); + + Git.getBranches().catch(function (err) { + ErrorHandler.showError(err, Strings.ERROR_GETTING_BRANCH_LIST); + }).then(function (branches) { + branches = branches.reduce(function (arr, branch) { + if (!branch.currentBranch && !branch.remote) { + arr.push(branch.name); + } + return arr; + }, []); + + $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"); + } + + PopUpManager.addPopUp($dropdown, detachCloseEvents, true, {closeCurrentPopups: true}); + PopUpManager.handleSelectionEvents($dropdown, {enableSearchFilter: true}); + attachCloseEvents(); + handleEvents(); + }); + } + + function _getHeadFilePath() { + return Preferences.get("currentGitRoot") + ".git/HEAD"; + } + + function addHeadToTheFileIndex() { + FileSystem.resolve(_getHeadFilePath(), function (err) { + if (err) { + ErrorHandler.logError(err, "Resolving .git/HEAD file failed"); + return; + } + }); + } + + function checkBranch() { + FileSystem.getFileForPath(_getHeadFilePath()).read(function (err, contents) { + if (err) { + ErrorHandler.showError(err, Strings.ERROR_READING_GIT_HEAD); + return; + } + + contents = contents.trim(); + + var m = contents.match(/^ref:\s+refs\/heads\/(\S+)/); + + // alternately try to parse the hash + if (!m) { m = contents.match(/^([a-f0-9]{40})$/); } + + if (!m) { + ErrorHandler.showError(new Error(StringUtils.format(Strings.ERROR_PARSING_BRANCH_NAME, contents))); + return; + } + + var branchInHead = m[1], + branchInUi = $gitBranchName.text(); + + if (branchInHead !== branchInUi) { + refresh(); + } + }); + } + + function refresh() { + if ($gitBranchName.length === 0) { return; } + + // show info that branch is refreshing currently + $gitBranchName + .text("\u2026") + .parent() + .show(); + + return Git.getGitRoot().then(function (gitRoot) { + var projectRoot = Utils.getProjectRoot(), + isRepositoryRootOrChild = gitRoot && projectRoot.indexOf(gitRoot) === 0; + + $gitBranchName.parent().toggle(isRepositoryRootOrChild); + + if (!isRepositoryRootOrChild) { + Preferences.set("currentGitRoot", projectRoot); + Preferences.set("currentGitSubfolder", ""); + + $gitBranchName + .off("click") + .text("not a git repo"); + Panel.disable("not-repo"); + + return; + } + + Preferences.set("currentGitRoot", gitRoot); + Preferences.set("currentGitSubfolder", projectRoot.substring(gitRoot.length)); + + // we are in a .git repo so read the head + addHeadToTheFileIndex(); + + return Git.getCurrentBranchName().then(function (branchName) { + + Git.getMergeInfo().then(function (mergeInfo) { + + if (mergeInfo.mergeMode) { + branchName += "|MERGING"; + } + + if (mergeInfo.rebaseMode) { + if (mergeInfo.rebaseHead) { + branchName = mergeInfo.rebaseHead; + } + branchName += "|REBASE"; + if (mergeInfo.rebaseNext && mergeInfo.rebaseLast) { + branchName += "(" + mergeInfo.rebaseNext + "/" + mergeInfo.rebaseLast + ")"; + } + } + + EventEmitter.emit(Events.REBASE_MERGE_MODE, mergeInfo.rebaseMode, mergeInfo.mergeMode); + + var MAX_LEN = 18; + + const tooltip = StringUtils.format(Strings.ON_BRANCH, branchName); + const html = ` ${ + branchName.length > MAX_LEN ? branchName.substring(0, MAX_LEN) + "\u2026" : branchName + }`; + $gitBranchName + .html(html) + .attr("title", tooltip) + .off("click") + .on("click", toggleDropdown); + Panel.enable(); + + }).catch(function (err) { + ErrorHandler.showError(err, Strings.ERROR_READING_GIT_STATE); + }); + + }).catch(function (ex) { + if (ErrorHandler.contains(ex, "unknown revision")) { + $gitBranchName + .off("click") + .text("no branch"); + Panel.enable(); + } else { + throw ex; + } + }); + }).catch(function (err) { + throw ErrorHandler.showError(err); + }); + } + + function init() { + // Add branch name to project tree + const $html = $(`
+ + + + +
`); + $html.appendTo($("#project-files-header")); + $gitBranchName = $("#git-branch"); + $html.on("click", function () { + $gitBranchName.click(); + return false; + }); + if(Setup.isExtensionActivated()){ + refresh(); + return; + } + $("#git-branch-dropdown-toggle").addClass("forced-inVisible"); + } + + EventEmitter.on(Events.BRACKETS_FILE_CHANGED, function (file) { + if (file.fullPath === _getHeadFilePath()) { + checkBranch(); + } + }); + + EventEmitter.on(Events.REFRESH_ALL, function () { + FileSyncManager.syncOpenDocuments(); + CommandManager.execute("file.refresh"); + refresh(); + }); + + EventEmitter.on(Events.BRACKETS_PROJECT_CHANGE, function () { + refresh(); + }); + + EventEmitter.on(Events.BRACKETS_PROJECT_REFRESH, function () { + refresh(); + }); + + EventEmitter.on(Events.GIT_ENABLED, function () { + $("#git-branch-dropdown-toggle").removeClass("forced-inVisible"); + }); + EventEmitter.on(Events.GIT_DISABLED, function () { + $("#git-branch-dropdown-toggle").addClass("forced-inVisible"); + }); + + exports.init = init; + exports.refresh = refresh; + +}); diff --git a/src/extensions/default/Git/src/Cli.js b/src/extensions/default/Git/src/Cli.js new file mode 100644 index 0000000000..682b48ab5f --- /dev/null +++ b/src/extensions/default/Git/src/Cli.js @@ -0,0 +1,240 @@ +/*globals logger, fs*/ +define(function (require, exports, module) { + + const NodeConnector = brackets.getModule('NodeConnector'); + + const ErrorHandler = require("src/ErrorHandler"), + Preferences = require("src/Preferences"), + Events = require("src/Events"), + Utils = require("src/Utils"); + + let gitTimeout = Preferences.get("gitTimeout") * 1000, + nextCliId = 0, + deferredMap = {}; + + Preferences.getExtensionPref().on("change", "gitTimeout", ()=>{ + gitTimeout = Preferences.get("gitTimeout") * 1000; + }); + + // Constants + var MAX_COUNTER_VALUE = 4294967295; // 2^32 - 1 + + let gitNodeConnector = NodeConnector.createNodeConnector("phcode-git-core", exports); + gitNodeConnector.on(Events.GIT_PROGRESS_EVENT, (_event, evtData) => { + const deferred = deferredMap[evtData.cliId]; + if(!deferred){ + ErrorHandler.logError("Progress sent for a non-existing process(" + evtData.cliId + "): " + evtData); + return; + } + if (!deferred.isResolved && deferred.progressTracker) { + deferred.progressTracker.trigger(Events.GIT_PROGRESS_EVENT, evtData.data); + } + }); + + function getNextCliId() { + if (nextCliId >= MAX_COUNTER_VALUE) { + nextCliId = 0; + } + return ++nextCliId; + } + + function normalizePathForOs(path) { + if (brackets.platform === "win") { + path = path.replace(/\//g, "\\"); + } + return path; + } + + // this functions prevents sensitive info from going further (like http passwords) + function sanitizeOutput(str) { + if (typeof str !== "string") { + if (str != null) { // checks for both null & undefined + str = str.toString(); + } else { + str = ""; + } + } + return str; + } + + function logDebug(opts, debugInfo, method, type, out) { + if (!logger.loggingOptions.logGit) { + return; + } + var processInfo = []; + + var duration = (new Date()).getTime() - debugInfo.startTime; + processInfo.push(duration + "ms"); + + if (opts.cliId) { + processInfo.push("ID=" + opts.cliId); + } + + var msg = "cmd-" + method + "-" + type + " (" + processInfo.join(";") + ")"; + if (out) { msg += ": \"" + out + "\""; } + Utils.consoleDebug(msg); + } + + function cliHandler(method, cmd, args, opts, retry) { + const cliPromise = new Promise((resolve, reject)=>{ + const cliId = getNextCliId(); + args = args || []; + opts = opts || {}; + const progressTracker = opts.progressTracker; + + const savedDefer = {resolve, reject, progressTracker}; + deferredMap[cliId] = savedDefer; + + const watchProgress = !!progressTracker || (args.indexOf("--progress") !== -1); + const startTime = (new Date()).getTime(); + + // it is possible to set a custom working directory in options + // otherwise the current project root is used to execute commands + if (!opts.cwd) { + opts.cwd = fs.getTauriPlatformPath(Preferences.get("currentGitRoot") || Utils.getProjectRoot()); + } + + // convert paths like c:/foo/bar to c:\foo\bar on windows + opts.cwd = normalizePathForOs(opts.cwd); + + // log all cli communication into console when debug mode is on + Utils.consoleDebug("cmd-" + method + (watchProgress ? "-watch" : "") + ": " + + opts.cwd + " -> " + + cmd + " " + args.join(" ")); + + let resolved = false, + timeoutLength = opts.timeout ? (opts.timeout * 1000) : gitTimeout; + + const domainOpts = { + cliId: cliId, + watchProgress: watchProgress + }; + + const debugInfo = { + startTime: startTime + }; + + if (watchProgress && progressTracker) { + progressTracker.trigger(Events.GIT_PROGRESS_EVENT, + "Running command: git " + args.join(" ")); + } + + gitNodeConnector.execPeer(method, {directory: opts.cwd, command: cmd, args: args, opts: domainOpts}) + .catch(function (err) { + if (!resolved) { + err = sanitizeOutput(err); + logDebug(domainOpts, debugInfo, method, "fail", err); + delete deferredMap[cliId]; + + err = ErrorHandler.toError(err); + + // spawn ENOENT error + var invalidCwdErr = "spawn ENOENT"; + if (err.stack && err.stack.indexOf(invalidCwdErr)) { + err.message = err.message.replace(invalidCwdErr, invalidCwdErr + " (" + opts.cwd + ")"); + err.stack = err.stack.replace(invalidCwdErr, invalidCwdErr + " (" + opts.cwd + ")"); + } + + // socket was closed so we should try this once again (if not already retrying) + if (err.stack && err.stack.indexOf("WebSocket.self._ws.onclose") !== -1 && !retry) { + cliHandler(method, cmd, args, opts, true) + .then(function (response) { + savedDefer.isResolved = true; + resolve(response); + }) + .catch(function (err) { + reject(err); + }); + return; + } + + reject(err); + } + }) + .then(function (out) { + if (!resolved) { + out = sanitizeOutput(out); + logDebug(domainOpts, debugInfo, method, "out", out); + delete deferredMap[cliId]; + resolve(out); + } + }) + .finally(function () { + progressTracker && progressTracker.off(`${Events.GIT_PROGRESS_EVENT}.${cliId}`); + resolved = true; + }); + + function timeoutPromise() { + logDebug(domainOpts, debugInfo, method, "timeout"); + var err = new Error("cmd-" + method + "-timeout: " + cmd + " " + args.join(" ")); + if (!opts.timeoutExpected) { + ErrorHandler.logError(err); + } + + // process still lives and we need to kill it + gitNodeConnector.execPeer("kill", domainOpts.cliId) + .catch(function (err) { + ErrorHandler.logError(err); + }); + + delete deferredMap[cliId]; + reject(ErrorHandler.toError(err)); + resolved = true; + progressTracker && progressTracker.off(`${Events.GIT_PROGRESS_EVENT}.${cliId}`); + } + + var lastProgressTime = 0; + function timeoutCall() { + setTimeout(function () { + if (!resolved) { + if (domainOpts.watchProgress) { + // we are watching the promise progress + // so we should check if the last message was sent in more than timeout time + const currentTime = (new Date()).getTime(); + const diff = currentTime - lastProgressTime; + if (diff > timeoutLength) { + Utils.consoleDebug("cmd(" + cliId + ") - last progress message was sent " + diff + "ms ago - timeout"); + timeoutPromise(); + } else { + Utils.consoleDebug("cmd(" + cliId + ") - last progress message was sent " + diff + "ms ago - delay"); + timeoutCall(); + } + } else { + // we don't have any custom handler, so just kill the promise here + // note that command WILL keep running in the background + // so even when timeout occurs, operation might finish after it + timeoutPromise(); + } + } + }, timeoutLength); + } + + // when opts.timeout === false then never timeout the process + if (opts.timeout !== false) { + // if we are watching for progress events, mark the time when last progress was made + if (domainOpts.watchProgress && progressTracker) { + progressTracker.off(`${Events.GIT_PROGRESS_EVENT}.${cliId}`); + progressTracker.on(`${Events.GIT_PROGRESS_EVENT}.${cliId}`, function () { + lastProgressTime = (new Date()).getTime(); + }); + } + // call the method which will timeout the promise after a certain period of time + timeoutCall(); + } + }); + return cliPromise; + } + + function which(cmd) { + return cliHandler("which", cmd); + } + + function spawnCommand(cmd, args, opts) { + return cliHandler("spawn", cmd, args, opts); + } + + // Public API + exports.cliHandler = cliHandler; + exports.which = which; + exports.spawnCommand = spawnCommand; +}); diff --git a/src/extensions/default/Git/src/CloseNotModified.js b/src/extensions/default/Git/src/CloseNotModified.js new file mode 100644 index 0000000000..57753b65b5 --- /dev/null +++ b/src/extensions/default/Git/src/CloseNotModified.js @@ -0,0 +1,65 @@ +/*jslint plusplus: true, vars: true, nomen: true */ +/*global $, brackets, define */ + +define(function (require, exports) { + + const DocumentManager = brackets.getModule("document/DocumentManager"), + Commands = brackets.getModule("command/Commands"), + CommandManager = brackets.getModule("command/CommandManager"), + Strings = brackets.getModule("strings"), + MainViewManager = brackets.getModule("view/MainViewManager"); + + const Events = require("src/Events"), + EventEmitter = require("src/EventEmitter"), + Git = require("src/git/Git"), + Preferences = require("src/Preferences"), + Constants = require("src/Constants"), + Utils = require("src/Utils"); + + let closeUnmodifiedCmd; + + function handleCloseNotModified() { + Git.status().then(function (modifiedFiles) { + var openFiles = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES), + currentGitRoot = Preferences.get("currentGitRoot"); + + openFiles.forEach(function (openFile) { + var removeOpenFile = true; + modifiedFiles.forEach(function (modifiedFile) { + if (currentGitRoot + modifiedFile.file === openFile.fullPath) { + removeOpenFile = false; + } + }); + + if (removeOpenFile) { + // check if file doesn't have any unsaved changes + const doc = DocumentManager.getOpenDocumentForPath(openFile.fullPath); + // document will not be present for images, or if the file is in working set but + // no editor is attached yet(eg. session restore on app start) + if (!doc || !doc.isDirty) { + CommandManager.execute(Commands.FILE_CLOSE_LIST, {PaneId: MainViewManager.ALL_PANES, fileList: [openFile]}); + } + } + }); + + MainViewManager.focusActivePane(); + }); + } + + function init() { + closeUnmodifiedCmd = CommandManager.register(Strings.CMD_CLOSE_UNMODIFIED, + Constants.CMD_GIT_CLOSE_UNMODIFIED, handleCloseNotModified); + Utils.enableCommand(Constants.CMD_GIT_CLOSE_UNMODIFIED, false); + } + + EventEmitter.on(Events.GIT_ENABLED, function () { + Utils.enableCommand(Constants.CMD_GIT_CLOSE_UNMODIFIED, true); + }); + + EventEmitter.on(Events.GIT_DISABLED, function () { + Utils.enableCommand(Constants.CMD_GIT_CLOSE_UNMODIFIED, false); + }); + + // Public API + exports.init = init; +}); diff --git a/src/extensions/default/Git/src/Constants.js b/src/extensions/default/Git/src/Constants.js new file mode 100644 index 0000000000..f18e884f48 --- /dev/null +++ b/src/extensions/default/Git/src/Constants.js @@ -0,0 +1,58 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +define(function (require, exports) { + exports.GIT_STRING_UNIVERSAL = "Git"; + exports.GIT_SUB_MENU = "git-submenu"; + + // Menus + exports.GIT_PANEL_CHANGES_CMENU = "git-panel-changes-cmenu"; + exports.GIT_PANEL_HISTORY_CMENU = "git-panel-history-cmenu"; + exports.GIT_PANEL_OPTIONS_CMENU = "git-panel-options-cmenu"; + + // commands + exports.CMD_GIT_INIT = "git-init"; + exports.CMD_GIT_CLONE = "git-clone"; + exports.CMD_GIT_CLONE_WITH_URL = "git-clone-url"; + exports.CMD_GIT_SETTINGS_COMMAND_ID = "git-settings"; + exports.CMD_GIT_CLOSE_UNMODIFIED = "git-close-unmodified-files"; + exports.CMD_GIT_CHECKOUT = "git-checkout"; + exports.CMD_GIT_RESET_HARD = "git-reset-hard"; + exports.CMD_GIT_RESET_SOFT = "git-reset-soft"; + exports.CMD_GIT_RESET_MIXED = "git-reset-mixed"; + exports.CMD_GIT_TOGGLE_PANEL = "git-toggle-panel"; + exports.CMD_GIT_GOTO_NEXT_CHANGE = "git-gotoNextChange"; + exports.CMD_GIT_GOTO_PREVIOUS_CHANGE = "git-gotoPrevChange"; + exports.CMD_GIT_COMMIT_CURRENT = "git-commitCurrent"; + exports.CMD_GIT_COMMIT_ALL = "git-commitAll"; + exports.CMD_GIT_FETCH = "git-fetch"; + exports.CMD_GIT_PULL = "git-pull"; + exports.CMD_GIT_PUSH = "git-push"; + exports.CMD_GIT_REFRESH = "git-refresh"; + exports.CMD_GIT_TAG = "git-tag"; + exports.CMD_GIT_DISCARD_ALL_CHANGES = "git-discard-all-changes"; + exports.CMD_GIT_UNDO_LAST_COMMIT = "git-undo-last-commit"; + exports.CMD_GIT_CHANGE_USERNAME = "git-change-username"; + exports.CMD_GIT_CHANGE_EMAIL = "git-change-email"; + exports.CMD_GIT_GERRIT_PUSH_REF = "git-gerrit-push_ref"; + exports.CMD_GIT_AUTHORS_OF_SELECTION = "git-authors-of-selection"; + exports.CMD_GIT_AUTHORS_OF_FILE = "git-authors-of-file"; + exports.CMD_GIT_TOGGLE_UNTRACKED = "git-toggle-untracked"; +}); diff --git a/src/extensions/default/Git/src/ErrorHandler.js b/src/extensions/default/Git/src/ErrorHandler.js new file mode 100644 index 0000000000..15ddbfbf90 --- /dev/null +++ b/src/extensions/default/Git/src/ErrorHandler.js @@ -0,0 +1,89 @@ +define(function (require, exports) { + + var _ = brackets.getModule("thirdparty/lodash"), + Dialogs = brackets.getModule("widgets/Dialogs"), + Mustache = brackets.getModule("thirdparty/mustache/mustache"), + NativeApp = brackets.getModule("utils/NativeApp"), + Strings = brackets.getModule("strings"), + Utils = require("src/Utils"), + errorDialogTemplate = require("text!templates/git-error-dialog.html"); + + var errorQueue = []; + + function errorToString(err) { + return Utils.encodeSensitiveInformation(err.toString()); + } + + exports.isTimeout = function (err) { + return err instanceof Error && ( + err.message.indexOf("cmd-execute-timeout") === 0 || + err.message.indexOf("cmd-spawn-timeout") === 0 + ); + }; + + exports.equals = function (err, what) { + return err.toString().toLowerCase() === what.toLowerCase(); + }; + + exports.contains = function (err, what) { + return err.toString().toLowerCase().indexOf(what.toLowerCase()) !== -1; + }; + + exports.matches = function (err, regExp) { + return err.toString().match(regExp); + }; + + exports.logError = function (err) { + var msg = err && err.stack ? err.stack : err; + Utils.consoleError("[brackets-git] " + msg); + errorQueue.push(err); + return err; + }; + + exports.showError = function (err, title, dontStripError) { + if (err.__shown) { return err; } + + exports.logError(err); + + let errorBody, + errorStack; + + if (typeof err === "string") { + errorBody = err; + } else if (err instanceof Error) { + errorBody = dontStripError ? err.toString() : errorToString(err); + errorStack = err.stack || ""; + } + + if (!errorBody || errorBody === "[object Object]") { + try { + errorBody = JSON.stringify(err, null, 4); + } catch (e) { + errorBody = "Error can't be stringified by JSON.stringify"; + } + } + + var compiledTemplate = Mustache.render(errorDialogTemplate, { + title: title, + body: window.debugMode ? `${errorBody}\n${errorStack}` : errorBody, + Strings: Strings + }); + + Dialogs.showModalDialogUsingTemplate(compiledTemplate); + if (typeof err === "string") { err = new Error(err); } + err.__shown = true; + return err; + }; + + exports.toError = function (arg) { + // FUTURE: use this everywhere and have a custom error class for this extension + if (arg instanceof Error) { return arg; } + var err = new Error(arg); + // TODO: new class for this? + err.match = function () { + return arg.match.apply(arg, arguments); + }; + return err; + }; + +}); diff --git a/src/extensions/default/Git/src/EventEmitter.js b/src/extensions/default/Git/src/EventEmitter.js new file mode 100644 index 0000000000..874d3ba3d4 --- /dev/null +++ b/src/extensions/default/Git/src/EventEmitter.js @@ -0,0 +1,40 @@ +define(function (require, exports, module) { + const EventDispatcher = brackets.getModule("utils/EventDispatcher"); + + const emInstance = {}; + EventDispatcher.makeEventDispatcher(emInstance); + + function getEmitter(eventName) { + if (!eventName) { + throw new Error("no event has been passed to get the emittor!"); + } + return function () { + emit(eventName, ...arguments); + }; + } + + function emit() { + emInstance.trigger(...arguments); + } + + function on(eventName, callback) { + emInstance.on(eventName, (...args)=>{ + // Extract everything except the first argument (_event) which is event data we don't use + const [, ...rest] = args; + callback(...rest); + }); + } + + function one(eventName, callback) { + emInstance.one(eventName, (...args)=>{ + // Extract everything except the first argument (_event) which is event data we don't use + const [, ...rest] = args; + callback(...rest); + }); + } + + exports.getEmitter = getEmitter; + exports.emit = emit; + exports.on = on; + exports.one = one; +}); diff --git a/src/extensions/default/Git/src/Events.js b/src/extensions/default/Git/src/Events.js new file mode 100644 index 0000000000..4660dea7ab --- /dev/null +++ b/src/extensions/default/Git/src/Events.js @@ -0,0 +1,59 @@ +define(function (require, exports) { + + /** + * List of Events to be used in the extension. + * Events should be structured by file who emits them. + */ + + // Brackets events + exports.BRACKETS_CURRENT_DOCUMENT_CHANGE = "brackets_current_document_change"; + exports.BRACKETS_PROJECT_CHANGE = "brackets_project_change"; + exports.BRACKETS_PROJECT_REFRESH = "brackets_project_refresh"; + exports.BRACKETS_DOCUMENT_SAVED = "brackets_document_saved"; + exports.BRACKETS_FILE_CHANGED = "brackets_file_changed"; + + // Git events + exports.GIT_PROGRESS_EVENT = "git_progress"; + exports.GIT_USERNAME_CHANGED = "git_username_changed"; + exports.GIT_EMAIL_CHANGED = "git_email_changed"; + exports.GIT_COMMITED = "git_commited"; + exports.GIT_NO_BRANCH_EXISTS = "git_no_branch_exists"; + exports.GIT_CHANGE_USERNAME = "git_change_username"; + exports.GIT_CHANGE_EMAIL = "git_change_email"; + + // Gerrit events + exports.GERRIT_TOGGLE_PUSH_REF = "gerrit_toggle_push_ref"; + exports.GERRIT_PUSH_REF_TOGGLED = "gerrit_push_ref_toggled"; + + // Startup events + exports.REFRESH_ALL = "git_refresh_all"; + exports.GIT_ENABLED = "git_enabled"; + exports.GIT_DISABLED = "git_disabled"; + exports.REBASE_MERGE_MODE = "rebase_merge_mode"; + + // Panel.js + exports.HANDLE_GIT_INIT = "handle_git_init"; + exports.HANDLE_GIT_CLONE = "handle_git_clone"; + exports.HANDLE_GIT_COMMIT = "handle_git_commit"; + exports.HANDLE_FETCH = "handle_fetch"; + exports.HANDLE_PUSH = "handle_push"; + exports.HANDLE_PULL = "handle_pull"; + exports.HANDLE_REMOTE_PICK = "handle_remote_pick"; + exports.HANDLE_REMOTE_DELETE = "handle_remote_delete"; + exports.HANDLE_REMOTE_CREATE = "handle_remote_create"; + exports.HANDLE_FTP_PUSH = "handle_ftp_push"; + exports.HISTORY_SHOW_FILE = "history_showFile"; + exports.HISTORY_SHOW_GLOBAL = "history_showGlobal"; + exports.REFRESH_COUNTERS = "refresh_counters"; + exports.REFRESH_HISTORY = "refresh_history"; + + // Git results + exports.GIT_STATUS_RESULTS = "git_status_results"; + + // Remotes.js + exports.GIT_REMOTE_AVAILABLE = "git_remote_available"; + exports.GIT_REMOTE_NOT_AVAILABLE = "git_remote_not_available"; + exports.REMOTES_REFRESH_PICKER = "remotes_refresh_picker"; + exports.FETCH_STARTED = "remotes_fetch_started"; + exports.FETCH_COMPLETE = "remotes_fetch_complete"; +}); diff --git a/src/extensions/default/Git/src/ExpectedError.js b/src/extensions/default/Git/src/ExpectedError.js new file mode 100644 index 0000000000..31d0d49f02 --- /dev/null +++ b/src/extensions/default/Git/src/ExpectedError.js @@ -0,0 +1,17 @@ +/*jslint plusplus: true, vars: true, nomen: true */ +/*global define */ + +define(function (require, exports, module) { + + function ExpectedError() { + Error.apply(this, arguments); + this.message = arguments[0]; + } + ExpectedError.prototype = new Error(); + ExpectedError.prototype.name = "ExpectedError"; + ExpectedError.prototype.toString = function () { + return this.message; + }; + + module.exports = ExpectedError; +}); diff --git a/src/extensions/default/Git/src/GutterManager.js b/src/extensions/default/Git/src/GutterManager.js new file mode 100644 index 0000000000..064a9c96db --- /dev/null +++ b/src/extensions/default/Git/src/GutterManager.js @@ -0,0 +1,379 @@ +// this file was composed with a big help from @MiguelCastillo extension Brackets-InteractiveLinter +// @see https://github.com/MiguelCastillo/Brackets-InteractiveLinter + +define(function (require, exports) { + + // Brackets modules + var _ = brackets.getModule("thirdparty/lodash"), + CommandManager = brackets.getModule("command/CommandManager"), + DocumentManager = brackets.getModule("document/DocumentManager"), + EditorManager = brackets.getModule("editor/EditorManager"), + MainViewManager = brackets.getModule("view/MainViewManager"), + ErrorHandler = require("src/ErrorHandler"), + Events = require("src/Events"), + EventEmitter = require("src/EventEmitter"), + Git = require("src/git/Git"), + Preferences = require("./Preferences"), + Strings = brackets.getModule("strings"); + + var gitAvailable = false, + gutterName = "brackets-git-gutter", + editorsWithGutters = [], + openWidgets = []; + + function clearWidgets() { + var lines = openWidgets.map(function (mark) { + var w = mark.lineWidget; + if (w.visible) { + w.visible = false; + w.widget.clear(); + } + return { + cm: mark.cm, + line: mark.line + }; + }); + openWidgets = []; + return lines; + } + + function clearOld(editor) { + var cm = editor._codeMirror; + if (!cm) { return; } + + var gutters = cm.getOption("gutters").slice(0), + io = gutters.indexOf(gutterName); + + if (io !== -1) { + gutters.splice(io, 1); + cm.clearGutter(gutterName); + cm.setOption("gutters", gutters); + cm.off("gutterClick", gutterClick); + } + + delete cm.gitGutters; + + clearWidgets(); + } + + function prepareGutter(editor) { + // add our gutter if its not already available + var cm = editor._codeMirror; + + var gutters = cm.getOption("gutters").slice(0); + if (gutters.indexOf(gutterName) === -1) { + gutters.unshift(gutterName); + cm.setOption("gutters", gutters); + cm.on("gutterClick", gutterClick); + } + + if (editorsWithGutters.indexOf(editor) === -1) { + editorsWithGutters.push(editor); + } + } + + function prepareGutters(editors) { + editors.forEach(function (editor) { + prepareGutter(editor); + }); + // clear the rest + var idx = editorsWithGutters.length; + while (idx--) { + if (editors.indexOf(editorsWithGutters[idx]) === -1) { + clearOld(editorsWithGutters[idx]); + editorsWithGutters.splice(idx, 1); + } + } + } + + function showGutters(editor, _results) { + prepareGutter(editor); + + var cm = editor._codeMirror; + cm.gitGutters = _.sortBy(_results, "line"); + + // get line numbers of currently opened widgets + var openBefore = clearWidgets(); + + cm.clearGutter(gutterName); + cm.gitGutters.forEach(function (obj) { + var $marker = $("
") + .addClass(gutterName + "-" + obj.type + " gitline-" + (obj.line + 1)) + .html(" "); + cm.setGutterMarker(obj.line, gutterName, $marker[0]); + }); + + // reopen widgets that were opened before refresh + openBefore.forEach(function (obj) { + gutterClick(obj.cm, obj.line, gutterName); + }); + } + + function gutterClick(cm, lineIndex, gutterId) { + if (!cm) { + return; + } + + if (gutterId !== gutterName && gutterId !== "CodeMirror-linenumbers") { + return; + } + + var mark = _.find(cm.gitGutters, function (o) { return o.line === lineIndex; }); + if (!mark || mark.type === "added") { return; } + + // we need to be able to identify cm instance from any mark + mark.cm = cm; + + if (mark.parentMark) { mark = mark.parentMark; } + + if (!mark.lineWidget) { + mark.lineWidget = { + visible: false, + element: $("
") + }; + var $btn = $(" + +
+ diff --git a/src/extensions/default/Git/src/dialogs/templates/progress-dialog.html b/src/extensions/default/Git/src/dialogs/templates/progress-dialog.html new file mode 100644 index 0000000000..f0f99c13ba --- /dev/null +++ b/src/extensions/default/Git/src/dialogs/templates/progress-dialog.html @@ -0,0 +1,14 @@ + diff --git a/src/extensions/default/Git/src/dialogs/templates/pull-dialog.html b/src/extensions/default/Git/src/dialogs/templates/pull-dialog.html new file mode 100644 index 0000000000..ccd40dcc5f --- /dev/null +++ b/src/extensions/default/Git/src/dialogs/templates/pull-dialog.html @@ -0,0 +1,98 @@ + diff --git a/src/extensions/default/Git/src/dialogs/templates/push-dialog.html b/src/extensions/default/Git/src/dialogs/templates/push-dialog.html new file mode 100644 index 0000000000..67161923d3 --- /dev/null +++ b/src/extensions/default/Git/src/dialogs/templates/push-dialog.html @@ -0,0 +1,98 @@ + diff --git a/src/extensions/default/Git/src/dialogs/templates/remotes-template.html b/src/extensions/default/Git/src/dialogs/templates/remotes-template.html new file mode 100644 index 0000000000..cc5b980b53 --- /dev/null +++ b/src/extensions/default/Git/src/dialogs/templates/remotes-template.html @@ -0,0 +1,39 @@ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ + +
+
+ +
+ +
diff --git a/src/extensions/default/Git/src/git/Git.js b/src/extensions/default/Git/src/git/Git.js new file mode 100644 index 0000000000..f204b2ed2a --- /dev/null +++ b/src/extensions/default/Git/src/git/Git.js @@ -0,0 +1,198 @@ +/* + This file acts as an entry point to GitCli.js and other possible + implementations of Git communication besides Cli. Application + should not access GitCli directly. +*/ +define(function (require, exports) { + + // Local modules + const Preferences = require("src/Preferences"), + GitCli = require("src/git/GitCli"), + Utils = require("src/Utils"); + + // Implementation + function pushToNewUpstream(remoteName, remoteBranch, options = {}) { + const args = ["--set-upstream"]; + + if (options.noVerify) { + args.push("--no-verify"); + } + + return GitCli.push(remoteName, remoteBranch, args, options.progressTracker); + } + + function getRemoteUrl(remote) { + return GitCli.getConfig("remote." + remote + ".url"); + } + + function setRemoteUrl(remote, url) { + return GitCli.setConfig("remote." + remote + ".url", url); + } + + function sortBranches(branches) { + return branches.sort(function (a, b) { + var ar = a.remote || "", + br = b.remote || ""; + // origin remote first + if (br && ar === "origin" && br !== "origin") { + return -1; + } else if (ar && ar !== "origin" && br === "origin") { + return 1; + } + // sort by remotes + if (ar < br) { + return -1; + } else if (ar > br) { + return 1; + } + // sort by sortPrefix (# character) + if (a.sortPrefix < b.sortPrefix) { + return -1; + } else if (a.sortPrefix > b.sortPrefix) { + return 1; + } + // master branch first + if (a.sortName === "master" && b.sortName !== "master") { + return -1; + } else if (a.sortName !== "master" && b.sortName === "master") { + return 1; + } + // sort by sortName (lowercased branch name) + return a.sortName < b.sortName ? -1 : a.sortName > b.sortName ? 1 : 0; + }); + } + + function getBranches() { + return GitCli.getBranches().then(function (branches) { + return sortBranches(branches); + }); + } + + function getAllBranches() { + return GitCli.getAllBranches().then(function (branches) { + return sortBranches(branches); + }); + } + + function getHistory(branch, skip) { + return GitCli.getHistory(branch, skip); + } + + function getFileHistory(file, branch, skip) { + return GitCli.getHistory(branch, skip, file); + } + + function resetIndex() { + return GitCli.reset(); + } + + function discardAllChanges() { + return GitCli.reset("--hard").then(function () { + return GitCli.clean(); + }); + } + + function getMergeInfo() { + var baseCheck = ["MERGE_MODE", "rebase-apply"], + mergeCheck = ["MERGE_HEAD", "MERGE_MSG"], + rebaseCheck = ["rebase-apply/next", "rebase-apply/last", "rebase-apply/head-name"], + gitFolder = Preferences.get("currentGitRoot") + ".git/"; + + return Promise.all(baseCheck.map(function (fileName) { + return Utils.loadPathContent(gitFolder + fileName); + })).then(function ([mergeMode, rebaseMode]) { + var obj = { + mergeMode: mergeMode !== null, + rebaseMode: rebaseMode !== null + }; + if (obj.mergeMode) { + + return Promise.all(mergeCheck.map(function (fileName) { + return Utils.loadPathContent(gitFolder + fileName); + })).then(function ([head, msg]) { + + if (head) { + obj.mergeHead = head.trim(); + } + var msgSplit = msg ? msg.trim().split(/conflicts:/i) : []; + if (msgSplit[0]) { + obj.mergeMessage = msgSplit[0].trim(); + } + if (msgSplit[1]) { + obj.mergeConflicts = msgSplit[1].trim().split("\n").map(function (line) { return line.trim(); }); + } + return obj; + + }); + + } + if (obj.rebaseMode) { + + return Promise.all(rebaseCheck.map(function (fileName) { + return Utils.loadPathContent(gitFolder + fileName); + })).then(function ([next, last, head]) { + + if (next) { obj.rebaseNext = next.trim(); } + if (last) { obj.rebaseLast = last.trim(); } + if (head) { obj.rebaseHead = head.trim().substring("refs/heads/".length); } + return obj; + + }); + + } + return obj; + }); + } + + function discardFileChanges(file) { + return GitCli.unstage(file).then(function () { + return GitCli.checkout(file); + }); + } + + function pushForced(remote, branch, options = {}) { + const args = ["--force"]; + + if (options.noVerify) { + args.push("--no-verify"); + } + + return GitCli.push(remote, branch, args, options.progressTracker); + } + + function deleteRemoteBranch(remote, branch, options = {}) { + const args = ["--delete"]; + + if (options.noVerify) { + args.push("--no-verify"); + } + + return GitCli.push(remote, branch, args, options.progressTracker); + } + + function undoLastLocalCommit() { + return GitCli.reset("--soft", "HEAD~1"); + } + + // Public API + exports.pushToNewUpstream = pushToNewUpstream; + exports.getBranches = getBranches; + exports.getAllBranches = getAllBranches; + exports.getHistory = getHistory; + exports.getFileHistory = getFileHistory; + exports.resetIndex = resetIndex; + exports.discardAllChanges = discardAllChanges; + exports.getMergeInfo = getMergeInfo; + exports.discardFileChanges = discardFileChanges; + exports.getRemoteUrl = getRemoteUrl; + exports.setRemoteUrl = setRemoteUrl; + exports.pushForced = pushForced; + exports.deleteRemoteBranch = deleteRemoteBranch; + exports.undoLastLocalCommit = undoLastLocalCommit; + + Object.keys(GitCli).forEach(function (method) { + if (!exports[method]) { + exports[method] = GitCli[method]; + } + }); +}); diff --git a/src/extensions/default/Git/src/git/GitCli.js b/src/extensions/default/Git/src/git/GitCli.js new file mode 100644 index 0000000000..a18884eb9f --- /dev/null +++ b/src/extensions/default/Git/src/git/GitCli.js @@ -0,0 +1,1162 @@ +/*globals jsPromise, fs*/ + +/* + This module is used to communicate with Git through Cli + Output string from Git should always be parsed here + to provide more sensible outputs than just plain strings. + Format of the output should be specified in Git.js +*/ +define(function (require, exports) { + + // Brackets modules + const _ = brackets.getModule("thirdparty/lodash"), + FileSystem = brackets.getModule("filesystem/FileSystem"), + Strings = brackets.getModule("strings"), + FileUtils = brackets.getModule("file/FileUtils"); + + // Local modules + const Cli = require("src/Cli"), + ErrorHandler = require("src/ErrorHandler"), + Events = require("src/Events"), + EventEmitter = require("src/EventEmitter"), + ExpectedError = require("src/ExpectedError"), + Preferences = require("src/Preferences"), + Utils = require("src/Utils"); + + // Module variables + let _gitPath = null, + _gitQueue = [], + _gitQueueBusy = false, + lastGitStatusResults; + + var FILE_STATUS = { + STAGED: "STAGED", + UNMODIFIED: "UNMODIFIED", + IGNORED: "IGNORED", + UNTRACKED: "UNTRACKED", + MODIFIED: "MODIFIED", + ADDED: "ADDED", + DELETED: "DELETED", + RENAMED: "RENAMED", + COPIED: "COPIED", + UNMERGED: "UNMERGED" + }; + + // This SHA1 represents the empty tree. You get it using `git mktree < /dev/null` + var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; + + // Implementation + function getGitPath() { + if (_gitPath) { return _gitPath; } + _gitPath = Preferences.get("gitPath"); + return _gitPath; + } + + Preferences.getExtensionPref().on("change", "gitPath", ()=>{ + _gitPath = Preferences.get("gitPath"); + }); + + function setGitPath(path) { + if (path === true) { path = "git"; } + Preferences.set("gitPath", path); + _gitPath = path; + } + + function strEndsWith(subjectString, searchString, position) { + if (position === undefined || position > subjectString.length) { + position = subjectString.length; + } + position -= searchString.length; + var lastIndex = subjectString.indexOf(searchString, position); + return lastIndex !== -1 && lastIndex === position; + } + + /* + function fixCygwinPath(path) { + if (typeof path === "string" && brackets.platform === "win" && path.indexOf("/cygdrive/") === 0) { + path = path.substring("/cygdrive/".length) + .replace(/^([a-z]+)\//, function (a, b) { + return b.toUpperCase() + ":/"; + }); + } + return path; + } + */ + + function _processQueue() { + // do nothing if the queue is busy + if (_gitQueueBusy) { + return; + } + // do nothing if the queue is empty + if (_gitQueue.length === 0) { + _gitQueueBusy = false; + return; + } + // get item from queue + const item = _gitQueue.shift(), + resolve = item[0], + reject = item[1], + args = item[2], + opts = item[3]; + // execute git command in a queue so no two commands are running at the same time + if (opts.nonblocking !== true) { _gitQueueBusy = true; } + Cli.spawnCommand(getGitPath(), args, opts) + .then(function (r) { + resolve(r); + }) + .catch(function (e) { + const call = "call: git " + args.join(" "); + e.stack = [call, e.stack].join("\n"); + reject(e); + }) + .finally(function () { + if (opts.nonblocking !== true) { _gitQueueBusy = false; } + _processQueue(); + }); + } + + function git(args, opts) { + return new Promise((resolve, reject) => { + _gitQueue.push([resolve, reject, args || [], opts || {}]); + setTimeout(_processQueue); + }); + } + + /* + git branch + -d --delete Delete a branch. + -D Delete a branch irrespective of its merged status. + --no-color Turn off branch colors + -r --remotes List or delete (if used with -d) the remote-tracking branches. + -a --all List both remote-tracking branches and local branches. + --track When creating a new branch, set up branch..remote and branch..merge + --set-upstream If specified branch does not exist yet or if --force has been given, acts exactly like --track + */ + + function setUpstreamBranch(remoteName, remoteBranch, progressTracker) { + if (!remoteName) { throw new TypeError("remoteName argument is missing!"); } + if (!remoteBranch) { throw new TypeError("remoteBranch argument is missing!"); } + return git(["branch", "--no-color", "-u", remoteName + "/" + remoteBranch], + {progressTracker}); + } + + function branchDelete(branchName, progressTracker) { + return git(["branch", "--no-color", "-d", branchName], {progressTracker}); + } + + function forceBranchDelete(branchName, progressTracker) { + return git(["branch", "--no-color", "-D", branchName], {progressTracker}); + } + + function getBranches(moreArgs, progressTracker) { + var args = ["branch", "--no-color"]; + if (moreArgs) { args = args.concat(moreArgs); } + + return git(args, {progressTracker}).then(function (stdout) { + if (!stdout) { return []; } + return stdout.split("\n").reduce(function (arr, l) { + var name = l.trim(), + currentBranch = false, + remote = null, + sortPrefix = ""; + + if (name.indexOf("->") !== -1) { + return arr; + } + + if (name.indexOf("* ") === 0) { + name = name.substring(2); + currentBranch = true; + } + + if (name.indexOf("remotes/") === 0) { + name = name.substring("remotes/".length); + remote = name.substring(0, name.indexOf("/")); + } + + var sortName = name.toLowerCase(); + if (remote) { + sortName = sortName.substring(remote.length + 1); + } + if (sortName.indexOf("#") !== -1) { + sortPrefix = sortName.slice(0, sortName.indexOf("#")); + } + + arr.push({ + name: name, + sortPrefix: sortPrefix, + sortName: sortName, + currentBranch: currentBranch, + remote: remote + }); + return arr; + }, []); + }); + } + + function getAllBranches(progressTracker) { + return getBranches(["-a"], progressTracker); + } + + /* + git fetch + --all Fetch all remotes. + --dry-run Show what would be done, without making any changes. + --multiple Allow several and arguments to be specified. No s may be specified. + --prune After fetching, remove any remote-tracking references that no longer exist on the remote. + --progress This flag forces progress status even if the standard error stream is not directed to a terminal. + */ + + function repositoryNotFoundHandler(err) { + var m = ErrorHandler.matches(err, /Repository (.*) not found$/gim); + if (m) { + throw new ExpectedError(m[0]); + } + throw err; + } + + function fetchRemote(remote, progressTracker) { + return git(["fetch", "--progress", remote], { + progressTracker, + timeout: false // never timeout this + }).catch(repositoryNotFoundHandler); + } + + function fetchAllRemotes(progressTracker) { + return git(["fetch", "--progress", "--all"], { + progressTracker, + timeout: false // never timeout this + }).catch(repositoryNotFoundHandler); + } + + /* + git remote + add Adds a remote named for the repository at . + rename Rename the remote named to . + remove Remove the remote named . + show Gives some information about the remote . + prune Deletes all stale remote-tracking branches under . + + */ + + function getRemotes() { + return git(["remote", "-v"]) + .then(function (stdout) { + return !stdout ? [] : _.uniq(stdout.replace(/\((push|fetch)\)/g, "").split("\n")).map(function (l) { + var s = l.trim().split("\t"); + return { + name: s[0], + url: s[1] + }; + }); + }); + } + + function createRemote(name, url) { + return git(["remote", "add", name, url]) + .then(function () { + // stdout is empty so just return success + return true; + }); + } + + function deleteRemote(name) { + return git(["remote", "rm", name]) + .then(function () { + // stdout is empty so just return success + return true; + }); + } + + /* + git pull + --no-commit Do not commit result after merge + --ff-only Refuse to merge and exit with a non-zero status + unless the current HEAD is already up-to-date + or the merge can be resolved as a fast-forward. + */ + + /** + * + * @param remote + * @param branch + * @param {boolean} [ffOnly] + * @param {boolean} [noCommit] + * @param {object} [options] + * @param [options.progressTracker] + * @returns {Promise} + */ + function mergeRemote(remote, branch, ffOnly, noCommit, options = {}) { + var args = ["merge"]; + + if (ffOnly) { args.push("--ff-only"); } + if (noCommit) { args.push("--no-commit", "--no-ff"); } + + args.push(remote + "/" + branch); + + var readMergeMessage = function () { + return Utils.loadPathContent(Preferences.get("currentGitRoot") + "/.git/MERGE_MSG").then(function (msg) { + return msg; + }); + }; + + return git(args, {progressTracker: options.progressTracker}) + .then(function (stdout) { + // return stdout if available - usually not + if (stdout) { return stdout; } + + return readMergeMessage().then(function (msg) { + if (msg) { return msg; } + return "Remote branch " + branch + " from " + remote + " was merged to current branch"; + }); + }) + .catch(function (error) { + return readMergeMessage().then(function (msg) { + if (msg) { return msg; } + throw error; + }); + }); + } + + function rebaseRemote(remote, branch, progressTracker) { + return git(["rebase", remote + "/" + branch], {progressTracker}); + } + + function resetRemote(remote, branch, progressTracker) { + return git(["reset", "--soft", remote + "/" + branch], {progressTracker}).then(function (stdout) { + return stdout || "Current branch was resetted to branch " + branch + " from " + remote; + }); + } + + function mergeBranch(branchName, mergeMessage, useNoff) { + var args = ["merge"]; + if (useNoff) { args.push("--no-ff"); } + if (mergeMessage && mergeMessage.trim()) { args.push("-m", mergeMessage); } + args.push(branchName); + return git(args); + } + + /* + git push + --porcelain Produce machine-readable output. + --delete All listed refs are deleted from the remote repository. This is the same as prefixing all refs with a colon. + --force Usually, the command refuses to update a remote ref that is not an ancestor of the local ref used to overwrite it. + --set-upstream For every branch that is up to date or successfully pushed, add upstream (tracking) reference + --progress This flag forces progress status even if the standard error stream is not directed to a terminal. + */ + + /* + returns parsed push response in this format: + { + flag: "=" + flagDescription: "Ref was up to date and did not need pushing" + from: "refs/heads/rewrite-remotes" + remoteUrl: "http://github.com/zaggino/brackets-git.git" + status: "Done" + summary: "[up to date]" + to: "refs/heads/rewrite-remotes" + } + */ + function push(remoteName, remoteBranch, additionalArgs, progressTracker) { + if (!remoteName) { throw new TypeError("remoteName argument is missing!"); } + + var args = ["push", "--porcelain", "--progress"]; + if (Array.isArray(additionalArgs)) { + args = args.concat(additionalArgs); + } + args.push(remoteName); + + if (remoteBranch && Preferences.get("gerritPushref")) { + return getConfig("gerrit.pushref").then(function (strGerritEnabled) { + if (strGerritEnabled === "true") { + args.push("HEAD:refs/for/" + remoteBranch); + } else { + args.push(remoteBranch); + } + return doPushWithArgs(args, progressTracker); + }); + } + + if (remoteBranch) { + args.push(remoteBranch); + } + + return doPushWithArgs(args, progressTracker); + } + + 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(); + } + + var retObj = {}, + lineTwo = lines[1].split("\t"); + + retObj.remoteUrl = lines[0].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]; + + switch (retObj.flag) { + case " ": + retObj.flagDescription = Strings.GIT_PUSH_SUCCESS_MSG; + break; + case "+": + retObj.flagDescription = Strings.GIT_PUSH_FORCE_UPDATED_MSG; + break; + case "-": + retObj.flagDescription = Strings.GIT_PUSH_DELETED_MSG; + break; + case "*": + retObj.flagDescription = Strings.GIT_PUSH_NEW_REF_MSG; + break; + case "!": + retObj.flagDescription = Strings.GIT_PUSH_REJECTED_MSG; + break; + case "=": + retObj.flagDescription = Strings.GIT_PUSH_UP_TO_DATE_MSG; + break; + default: + retObj.flagDescription = "Unknown push flag received: " + retObj.flag; // internal error not translated + } + + return retObj; + }); + } + + function getCurrentBranchName() { + return git(["branch", "--no-color"]).then(function (stdout) { + var branchName = _.find(stdout.split("\n"), function (l) { return l[0] === "*"; }); + if (branchName) { + branchName = branchName.substring(1).trim(); + + var m = branchName.match(/^\(.*\s(\S+)\)$/); // like (detached from f74acd4) + if (m) { return m[1]; } + + return branchName; + } + + // no branch situation so we need to create one by doing a commit + if (stdout.match(/^\s*$/)) { + EventEmitter.emit(Events.GIT_NO_BRANCH_EXISTS); + // master is the default name of the branch after git init + return "master"; + } + + // alternative + return git(["log", "--pretty=format:%H %d", "-1"]).then(function (stdout) { + var m = stdout.trim().match(/^(\S+)\s+\((.*)\)$/); + var hash = m[1].substring(0, 20); + m[2].split(",").forEach(function (info) { + info = info.trim(); + + if (info === "HEAD") { return; } + + var m = info.match(/^tag:(.+)$/); + if (m) { + hash = m[1].trim(); + return; + } + + hash = info; + }); + return hash; + }); + }); + } + + function getCurrentUpstreamBranch() { + return git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]) + .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]) + .then(function (stdout) { + var regex = /^D/; + return stdout.split("\n").reduce(function (arr, row) { + if (regex.test(row)) { + arr.push(row.substring(1).trim()); + } + return arr; + }, []); + }); + } + + function getConfig(key) { + return git(["config", key.replace(/\s/g, "")]); + } + + function setConfig(key, value, allowGlobal) { + key = key.replace(/\s/g, ""); + return git(["config", key, value]).catch(function (err) { + + if (allowGlobal && ErrorHandler.contains(err, "No such file or directory")) { + return git(["config", "--global", key, value]); + } + + throw err; + + }); + } + + function getHistory(branch, skipCommits, file) { + var separator = "_._", + newline = "_.nw._", + format = [ + "%h", // abbreviated commit hash + "%H", // commit hash + "%an", // author name + "%aI", // author date, ISO 8601 format + "%ae", // author email + "%s", // subject + "%b", // body + "%d" // tags + ].join(separator) + newline; + + var args = ["log", "-100", "--date=iso"]; + if (skipCommits) { args.push("--skip=" + skipCommits); } + args.push("--format=" + format, branch, "--"); + + // follow is too buggy - do not use + // if (file) { args.push("--follow"); } + if (file) { args.push(file); } + + return git(args).then(function (stdout) { + stdout = stdout.substring(0, stdout.length - newline.length); + return !stdout ? [] : stdout.split(newline).map(function (line) { + + var data = line.trim().split(separator), + commit = {}; + + commit.hashShort = data[0]; + commit.hash = data[1]; + commit.author = data[2]; + commit.date = data[3]; + commit.email = data[4]; + commit.subject = data[5]; + commit.body = data[6]; + + if (data[7]) { + var tags = data[7]; + var regex = new RegExp("tag: ([^,|\)]+)", "g"); + tags = tags.match(regex); + + for (var key in tags) { + if (tags[key] && tags[key].replace) { + tags[key] = tags[key].replace("tag:", ""); + } + } + commit.tags = tags; + } + + return commit; + + }); + }); + } + + function init() { + return git(["init"]); + } + + function clone(remoteGitUrl, destinationFolder, progressTracker) { + return git(["clone", remoteGitUrl, destinationFolder, "--progress"], { + progressTracker, + timeout: false // never timeout this + }); + } + + function stage(fileOrFiles, updateIndex) { + var args = ["add"]; + if (updateIndex) { args.push("-u"); } + return git(args.concat("--", fileOrFiles)); + } + + function stageAll() { + return git(["add", "--all"]); + } + + function commit(message, amend, noVerify, progressTracker) { + var lines = message.split("\n"), + args = ["commit"]; + + if (amend) { + args.push("--amend", "--reset-author"); + } + + if (noVerify) { + args.push("--no-verify"); + } + + if (lines.length === 1) { + args.push("-m", message); + return git(args, {progressTracker}); + } else { + return new Promise(function (resolve, reject) { + // FUTURE: maybe use git commit --file=- + var fileEntry = FileSystem.getFileForPath(Preferences.get("currentGitRoot") + ".bracketsGitTemp"); + jsPromise(FileUtils.writeText(fileEntry, message)) + .then(function () { + args.push("-F", ".bracketsGitTemp"); + return git(args, {progressTracker}); + }) + .then(function (res) { + fileEntry.unlink(function () { + resolve(res); + }); + }) + .catch(function (err) { + fileEntry.unlink(function () { + reject(err); + }); + }); + }); + } + } + + function reset(type, hash) { + var args = ["reset", type || "--mixed"]; // mixed is the default action + if (hash) { args.push(hash, "--"); } + return git(args); + } + + function unstage(file) { + return git(["reset", "--", file]); + } + + function checkout(hash) { + return git(["checkout", hash], { + timeout: false // never timeout this + }); + } + + function createBranch(branchName, originBranch, trackOrigin) { + var args = ["checkout", "-b", branchName]; + + if (originBranch) { + if (trackOrigin) { + args.push("--track"); + } + args.push(originBranch); + } + + return git(args); + } + + function _isquoted(str) { + return str[0] === "\"" && str[str.length - 1] === "\""; + } + + function _unquote(str) { + return str.substring(1, str.length - 1); + } + + function _isescaped(str) { + return /\\[0-9]{3}/.test(str); + } + + function status(type) { + return git(["status", "-u", "--porcelain"]).then(function (stdout) { + if (!stdout) { return []; } + + var currentSubFolder = Preferences.get("currentGitSubfolder"); + + // files that are modified both in index and working tree should be resetted + var isEscaped = false, + needReset = [], + results = [], + lines = stdout.split("\n"); + + lines.forEach(function (line) { + var statusStaged = line.substring(0, 1), + statusUnstaged = line.substring(1, 2), + status = [], + file = line.substring(3); + + // check if the file is quoted + if (_isquoted(file)) { + file = _unquote(file); + if (_isescaped(file)) { + isEscaped = true; + } + } + + if (statusStaged !== " " && statusUnstaged !== " " && + statusStaged !== "?" && statusUnstaged !== "?") { + needReset.push(file); + return; + } + + var statusChar; + if (statusStaged !== " " && statusStaged !== "?") { + status.push(FILE_STATUS.STAGED); + statusChar = statusStaged; + } else { + statusChar = statusUnstaged; + } + + switch (statusChar) { + case " ": + status.push(FILE_STATUS.UNMODIFIED); + break; + case "!": + status.push(FILE_STATUS.IGNORED); + break; + case "?": + status.push(FILE_STATUS.UNTRACKED); + break; + case "M": + status.push(FILE_STATUS.MODIFIED); + break; + case "A": + status.push(FILE_STATUS.ADDED); + break; + case "D": + status.push(FILE_STATUS.DELETED); + break; + case "R": + status.push(FILE_STATUS.RENAMED); + break; + case "C": + status.push(FILE_STATUS.COPIED); + break; + case "U": + status.push(FILE_STATUS.UNMERGED); + break; + default: + throw new Error("Unexpected status: " + statusChar); + } + + var display = file, + io = file.indexOf("->"); + if (io !== -1) { + file = file.substring(io + 2).trim(); + } + + // we don't want to display paths that lead to this file outside the project + if (currentSubFolder && display.indexOf(currentSubFolder) === 0) { + display = display.substring(currentSubFolder.length); + } + + results.push({ + status: status, + display: display, + file: file, + name: file.substring(file.lastIndexOf("/") + 1) + }); + }); + + if (isEscaped) { + return setConfig("core.quotepath", "false").then(function () { + if (type === "SET_QUOTEPATH") { + throw new Error("git status is calling itself in a recursive loop!"); + } + return status("SET_QUOTEPATH"); + }); + } + + if (needReset.length > 0) { + return Promise.all(needReset.map(function (fileName) { + if (fileName.indexOf("->") !== -1) { + fileName = fileName.split("->")[1].trim(); + } + return unstage(fileName); + })).then(function () { + if (type === "RECURSIVE_CALL") { + throw new Error("git status is calling itself in a recursive loop!"); + } + return status("RECURSIVE_CALL"); + }); + } + + return results.sort(function (a, b) { + if (a.file < b.file) { + return -1; + } + if (a.file > b.file) { + return 1; + } + return 0; + }); + }).then(function (results) { + lastGitStatusResults = results; + EventEmitter.emit(Events.GIT_STATUS_RESULTS, results); + return results; + }); + } + + function hasStatusChanged() { + const prevStatus = lastGitStatusResults; + return status().then(function (currentStatus) { + // the results are already sorted by file name + // Compare the current statuses with the previous ones + if (!prevStatus || prevStatus.length !== currentStatus.length) { + return true; + } + for (let i = 0; i < prevStatus.length; i++) { + if (prevStatus[i].file !== currentStatus[i].file || + prevStatus[i].status.join(", ") !== currentStatus[i].status.join(", ")) { + return true; + } + } + + return false; + }).catch(function (error) { + console.error("Error fetching Git status in hasStatusChanged:", error); + return false; + }); + } + + function _isFileStaged(file) { + return git(["status", "-u", "--porcelain", "--", file]).then(function (stdout) { + if (!stdout) { return false; } + return _.any(stdout.split("\n"), function (line) { + return line[0] !== " " && line[0] !== "?" && // first character marks staged status + line.lastIndexOf(" " + file) === line.length - file.length - 1; // in case another file appeared here? + }); + }); + } + + function getDiffOfStagedFiles() { + return git(["diff", "--no-ext-diff", "--no-color", "--staged"], { + timeout: false // never timeout this + }); + } + + function getDiffOfAllIndexFiles(files) { + var args = ["diff", "--no-ext-diff", "--no-color", "--full-index"]; + if (files) { + args = args.concat("--", files); + } + return git(args, { + timeout: false // never timeout this + }); + } + + function getListOfStagedFiles() { + return git(["diff", "--no-ext-diff", "--no-color", "--staged", "--name-only"], { + timeout: false // never timeout this + }); + } + + function diffFile(file) { + return _isFileStaged(file).then(function (staged) { + var args = ["diff", "--no-ext-diff", "--no-color"]; + if (staged) { args.push("--staged"); } + args.push("-U0", "--", file); + return git(args, { + timeout: false // never timeout this + }); + }); + } + + function diffFileNice(file) { + return _isFileStaged(file).then(function (staged) { + var args = ["diff", "--no-ext-diff", "--no-color"]; + if (staged) { args.push("--staged"); } + args.push("--", file); + return git(args, { + timeout: false // never timeout this + }); + }); + } + + function difftool(file) { + return _isFileStaged(file).then(function (staged) { + var args = ["difftool"]; + if (staged) { + args.push("--staged"); + } + args.push("--", file); + return git(args, { + timeout: false, // never timeout this + nonblocking: true // allow running other commands before this command finishes its work + }); + }); + } + + function clean() { + return git(["clean", "-f", "-d"]); + } + + function getFilesFromCommit(hash, isInitial) { + var args = ["diff", "--no-ext-diff", "--name-only"]; + args = args.concat((isInitial ? EMPTY_TREE : hash + "^") + ".." + hash); + args = args.concat("--"); + return git(args).then(function (stdout) { + return !stdout ? [] : stdout.split("\n"); + }); + } + + function getDiffOfFileFromCommit(hash, file, isInitial) { + var args = ["diff", "--no-ext-diff", "--no-color"]; + args = args.concat((isInitial ? EMPTY_TREE : hash + "^") + ".." + hash); + args = args.concat("--", file); + return git(args); + } + + function difftoolFromHash(hash, file, isInitial) { + return git(["difftool", (isInitial ? EMPTY_TREE : hash + "^") + ".." + hash, "--", file], { + timeout: false // never timeout this + }); + } + + function rebaseInit(branchName) { + return git(["rebase", "--ignore-date", branchName]); + } + + function rebase(whatToDo) { + return git(["rebase", "--" + whatToDo]); + } + + function getVersion() { + return git(["--version"]).then(function (stdout) { + var m = stdout.match(/[0-9].*/); + return m ? m[0] : stdout.trim(); + }); + } + + function getCommitCountsFallback() { + return git(["rev-list", "HEAD", "--not", "--remotes"]) + .then(function (stdout) { + var ahead = stdout ? stdout.split("\n").length : 0; + return "-1 " + ahead; + }) + .catch(function (err) { + ErrorHandler.logError(err); + return "-1 -1"; + }); + } + + function getCommitCounts() { + var remotes = Preferences.get("defaultRemotes") || {}; + var remote = remotes[Preferences.get("currentGitRoot")]; + return getCurrentBranchName() + .then(function (branch) { + if (!branch || !remote) { + return getCommitCountsFallback(); + } + return git(["rev-list", "--left-right", "--count", remote + "/" + branch + "...@{0}", "--"]) + .catch(function (err) { + ErrorHandler.logError(err); + return getCommitCountsFallback(); + }) + .then(function (stdout) { + var matches = /(-?\d+)\s+(-?\d+)/.exec(stdout); + return matches ? { + behind: parseInt(matches[1], 10), + ahead: parseInt(matches[2], 10) + } : { + behind: -1, + ahead: -1 + }; + }); + }); + } + + function getLastCommitMessage() { + return git(["log", "-1", "--pretty=%B"]).then(function (stdout) { + return stdout.trim(); + }); + } + + function getBlame(file, from, to) { + var args = ["blame", "-w", "--line-porcelain"]; + if (from || to) { args.push("-L" + from + "," + to); } + args.push(file); + + return git(args).then(function (stdout) { + if (!stdout) { return []; } + + var sep = "-@-BREAK-HERE-@-", + sep2 = "$$#-#$BREAK$$-$#"; + stdout = stdout.replace(sep, sep2) + .replace(/^\t(.*)$/gm, function (a, b) { return b + sep; }); + + return stdout.split(sep).reduce(function (arr, lineInfo) { + lineInfo = lineInfo.replace(sep2, sep).trimLeft(); + if (!lineInfo) { return arr; } + + var obj = {}, + lines = lineInfo.split("\n"), + firstLine = _.first(lines).split(" "); + + obj.hash = firstLine[0]; + obj.num = firstLine[2]; + obj.content = _.last(lines); + + // process all but first and last lines + for (var i = 1, l = lines.length - 1; i < l; i++) { + var line = lines[i], + io = line.indexOf(" "), + key = line.substring(0, io), + val = line.substring(io + 1); + obj[key] = val; + } + + arr.push(obj); + return arr; + }, []); + }).catch(function (stderr) { + var m = stderr.match(/no such path (\S+)/); + if (m) { + throw new Error("File is not tracked by Git: " + m[1]); + } + throw stderr; + }); + } + + function getGitRoot() { + var projectRoot = Utils.getProjectRoot(); + return git(["rev-parse", "--show-toplevel"], { + cwd: fs.getTauriPlatformPath(projectRoot) + }) + .catch(function (e) { + if (ErrorHandler.contains(e, "Not a git repository")) { + return null; + } + throw e; + }) + .then(function (root) { + if (root === null) { + return root; + } + + // paths on cygwin look a bit different + // root = fixCygwinPath(root); + + // we know projectRoot is in a Git repo now + // because --show-toplevel didn't return Not a git repository + // we need to find closest .git + + function checkPathRecursive(path) { + + if (strEndsWith(path, "/")) { + path = path.slice(0, -1); + } + + Utils.consoleDebug("Checking path for .git: " + path); + + return new Promise(function (resolve) { + + // keep .git away from file tree for now + // this branch of code will not run for intel xdk + if (typeof brackets !== "undefined" && brackets.fs && brackets.fs.stat) { + brackets.fs.stat(path + "/.git", function (err, result) { + var exists = err ? false : (result.isFile() || result.isDirectory()); + if (exists) { + Utils.consoleDebug("Found .git in path: " + path); + resolve(path); + } else { + Utils.consoleDebug("Failed to find .git in path: " + path); + path = path.split("/"); + path.pop(); + path = path.join("/"); + resolve(checkPathRecursive(path)); + } + }); + return; + } + + FileSystem.resolve(path + "/.git", function (err, item, stat) { + var exists = err ? false : (stat.isFile || stat.isDirectory); + if (exists) { + Utils.consoleDebug("Found .git in path: " + path); + resolve(path); + } else { + Utils.consoleDebug("Failed to find .git in path: " + path); + path = path.split("/"); + path.pop(); + path = path.join("/"); + resolve(checkPathRecursive(path)); + } + }); + + }); + + } + + return checkPathRecursive(projectRoot).then(function (path) { + return path + "/"; + }); + + }); + } + + function setTagName(tagname, commitHash) { + const args = ["tag", tagname]; + if (commitHash) { + args.push(commitHash); // Add the commit hash to the arguments if provided + } + return git(args).then(function (stdout) { + return stdout.trim(); + }); + } + + // Public API + exports._git = git; + exports.setGitPath = setGitPath; + exports.FILE_STATUS = FILE_STATUS; + exports.fetchRemote = fetchRemote; + exports.fetchAllRemotes = fetchAllRemotes; + exports.getRemotes = getRemotes; + exports.createRemote = createRemote; + exports.deleteRemote = deleteRemote; + exports.push = push; + exports.setUpstreamBranch = setUpstreamBranch; + exports.getCurrentBranchName = getCurrentBranchName; + exports.getCurrentUpstreamBranch = getCurrentUpstreamBranch; + exports.getConfig = getConfig; + exports.setConfig = setConfig; + exports.getBranches = getBranches; + exports.getAllBranches = getAllBranches; + exports.branchDelete = branchDelete; + exports.forceBranchDelete = forceBranchDelete; + exports.getDeletedFiles = getDeletedFiles; + exports.getHistory = getHistory; + exports.init = init; + exports.clone = clone; + exports.stage = stage; + exports.unstage = unstage; + exports.stageAll = stageAll; + exports.commit = commit; + exports.reset = reset; + exports.checkout = checkout; + exports.createBranch = createBranch; + exports.status = status; + exports.hasStatusChanged = hasStatusChanged; + exports.diffFile = diffFile; + exports.diffFileNice = diffFileNice; + exports.difftool = difftool; + exports.clean = clean; + exports.getFilesFromCommit = getFilesFromCommit; + exports.getDiffOfFileFromCommit = getDiffOfFileFromCommit; + exports.difftoolFromHash = difftoolFromHash; + exports.rebase = rebase; + exports.rebaseInit = rebaseInit; + exports.mergeRemote = mergeRemote; + exports.rebaseRemote = rebaseRemote; + exports.resetRemote = resetRemote; + exports.getVersion = getVersion; + exports.getCommitCounts = getCommitCounts; + exports.getLastCommitMessage = getLastCommitMessage; + exports.mergeBranch = mergeBranch; + exports.getDiffOfAllIndexFiles = getDiffOfAllIndexFiles; + exports.getDiffOfStagedFiles = getDiffOfStagedFiles; + exports.getListOfStagedFiles = getListOfStagedFiles; + exports.getBlame = getBlame; + exports.getGitRoot = getGitRoot; + exports.setTagName = setTagName; +}); diff --git a/src/extensions/default/Git/src/utils/Setup.js b/src/extensions/default/Git/src/utils/Setup.js new file mode 100644 index 0000000000..36ce56c0d6 --- /dev/null +++ b/src/extensions/default/Git/src/utils/Setup.js @@ -0,0 +1,126 @@ +define(function (require, exports) { + + // Brackets modules + const _ = brackets.getModule("thirdparty/lodash"); + + // Local modules + const Cli = require("src/Cli"), + Git = require("src/git/Git"), + Preferences = require("src/Preferences"); + + // Module variables + let standardGitPathsWin = [ + "C:\\Program Files (x86)\\Git\\cmd\\git.exe", + "C:\\Program Files\\Git\\cmd\\git.exe" + ]; + + let standardGitPathsNonWin = [ + "/usr/local/git/bin/git", + "/usr/local/bin/git", + "/usr/bin/git" + ]; + + let extensionActivated = false; + + // Implementation + function getGitVersion() { + return new Promise(function (resolve, reject) { + + // TODO: do this in two steps - first check user config and then check all + var pathsToLook = [Preferences.get("gitPath"), "git"].concat(brackets.platform === "win" ? standardGitPathsWin : standardGitPathsNonWin); + pathsToLook = _.unique(_.compact(pathsToLook)); + + var results = [], + errors = []; + var finish = _.after(pathsToLook.length, function () { + + var searchedPaths = "\n\nSearched paths:\n" + pathsToLook.join("\n"); + + if (results.length === 0) { + // no git found + reject("No Git has been found on this computer" + searchedPaths); + } else { + // at least one git is found + var gits = _.sortBy(results, "version").reverse(), + latestGit = gits[0], + m = latestGit.version.match(/([0-9]+)\.([0-9]+)/), + major = parseInt(m[1], 10), + minor = parseInt(m[2], 10); + + if (major === 1 && minor < 8) { + return reject("Brackets Git requires Git 1.8 or later - latest version found was " + latestGit.version + searchedPaths); + } + + // prefer the first defined so it doesn't change all the time and confuse people + latestGit = _.sortBy(_.filter(gits, function (git) { return git.version === latestGit.version; }), "index")[0]; + + // this will save the settings also + Git.setGitPath(latestGit.path); + resolve(latestGit.version); + } + + }); + + pathsToLook.forEach(function (path, index) { + Cli.spawnCommand(path, ["--version"], { + cwd: "./" + }).then(function (stdout) { + var m = stdout.match(/^git version\s+(.*)$/); + if (m) { + results.push({ + path: path, + version: m[1], + index: index + }); + } + }).catch(function (err) { + errors.push({ + path: path, + err: err + }); + }).finally(function () { + finish(); + }); + }); + + }); + } + + function isExtensionActivated() { + return extensionActivated && Preferences.get("enableGit"); + } + + /** + * Initializes the Git extension by checking for the Git executable and returns true if active. + * + * @async + * @function init + * @returns {Promise} + * A promise that resolves to a boolean indicating whether the extension was activated (`true`) + * or deactivated (`false`) due to a missing or inaccessible Git executable. + * }); + */ + function init() { + return new Promise((resolve) =>{ + if(!Preferences.get("enableGit")){ + resolve(false); + console.log("Git is disabled in preferences."); + return; + } + getGitVersion().then(function (_version) { + extensionActivated = true; + resolve(extensionActivated); + }).catch(function (err) { + extensionActivated = false; + console.warn("Failed to launch Git executable. Deactivating Git extension. Is git installed?", err); + resolve(extensionActivated); + }); + }); + } + + // Public API + exports.init = init; + exports.isExtensionActivated = isExtensionActivated; + exports.getGitVersion = getGitVersion; + +}); diff --git a/src/extensions/default/Git/styles/brackets-git.less b/src/extensions/default/Git/styles/brackets-git.less new file mode 100644 index 0000000000..e14e71bb5d --- /dev/null +++ b/src/extensions/default/Git/styles/brackets-git.less @@ -0,0 +1,673 @@ +@import "colors.less"; +@import "common.less"; +@import "mixins.less"; +@import "code-mirror.less"; +@import "history.less"; +@import "commit-diff.less"; +@import "dialogs-all.less"; +@import "brackets/brackets_core_ui_variables.less"; + +@gutterWidth: 0.65em; // using ems so that it'll be scalable on cmd +/- + +#editor-holder { + .git.spinner { + display: none; + z-index: 1000; + position: absolute; + top: 50%; + left: 50%; + &.spin { + display: block; + } + } +} + +/* Project tree */ +.jstree-brackets, .open-files-container { + li.git-modified > a:before { + content: "|"; + color: @orange; + position: absolute; + margin-left: -4px; + } + li.git-new > a:before { + content: "|"; + color: @green; + position: absolute; + margin-left: -4px; + } + li.git-ignored > a { + color: @moreDarkGrey !important; + font-style: italic; + > span.extension { + color: @moreDarkGrey !important; + } + } +} + +/* Branch information */ +#git-branch-dropdown-toggle { + display: flex; + /* adjust margins to keep position #project-title position stable after extension is loaded */ + overflow: hidden; + white-space: nowrap; + padding: 1px 5px; + margin-left: -5px; + .dropdown-arrow { + display: inline-block; + width: 7px; + height: 5px; + margin-left: 4px; + position: relative; + top: 7px; + } +} + +#git-branch { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 1 0; + .octicon { + line-height: unset; + font-size: small; + } +} + +#git-branch-dropdown { + margin-left: -12px; + position: absolute; + display: block; + max-width: none; + z-index: 100; + overflow-y: auto; + &:focus { + outline: none; + } + + a, + a:hover { + color: @bc-menu-text; + + .dark & { + color: @dark-bc-menu-text; + } + } + .git-branch-new, .git-branch-link { + padding: 5px 26px 5px 26px; + } + .git-branch-link { + position: relative; + .switch-branch { + display: inline-block; + width: 100%; + padding: 5px 0; + margin: -5px 0; + } + .trash-icon, .merge-branch { + position: absolute; + opacity: 0; + top: 27%; + background-image: none !important; + width: 16px; + height: 16px; + font-size: 20px; + color: rgba(0, 0, 0, 0.5); + line-height: 15px; + text-align: center; + &:hover { + color: rgba(0, 0, 0, 1); + } + } + .trash-icon, .merge-branch { + &:hover { + color: rgba(0, 0, 0, 1); + } + } + &:hover { + .trash-icon, .merge-branch { + opacity: 1; + } + } + .merge-branch { + right: 5px; + } + } + a { + padding: 5px 15px; + &.selected { + background: @bc-bg-highlight; + + .dark & { + background: @dark-bc-bg-highlight; + } + } + &:not(.selected):hover { + background: none; + } + } + .divider { + margin: 5px 1px; + } +} + +.hide-overflow { + overflow: hidden !important; +} + +/* Extension panel */ +#git-panel { + position: relative; + + .toolbar { + overflow: visible; + .close { + position: absolute; + top: 22px; + margin-top: -10px; + } + } + .git-more-options-btn { + position: absolute; + right: 25px; + top: 8px; + padding: 4px 8px 2px 8px; + opacity: .7; + .dark & { + opacity: .5; + } + } + .git-more-options-btn:hover { + opacity: .9; + .dark & { + opacity: .8; + } + } + table { + margin-bottom: 0; + } + .git-edited-list td { + vertical-align: middle; + } + tr.selected { + font-weight: 400; + } + td { + &.checkbox-column { + vertical-align: middle; + width: 13px; + } + &.icons-column { + padding-left: 13px; + width: 1px; + } + &.status-column { + width: 100px; + } + &:last-child { + width: 250px; + text-align: right; + padding-right: 20px; + } + } + .check-all { + margin-left: 7px; + margin-right: 10px; + } + .mainToolbar { + .btn-group { + line-height: 1; + button { + height: 26px; + } + } + } + .btn-git-diff, .btn-git-undo, .btn-git-delete { + padding: 2px 5px; + font-size: 12px; + line-height: 1em; + border-radius: 3px; + margin: 0 6px 0 0; + } + .nothing-to-commit { + padding: 15px; + } + .git-right-icons { + position:absolute; + right: 55px; + top: 5px; + } + .octicon:not(:only-child) { + margin-right: 5px; + vertical-align: -1px; + } + .btn-group.open .dropdown-toggle { + background-color: @bc-btn-bg; + box-shadow: inset 0 1px 0 @bc-btn-bg-down; + color: @bc-text; + + .dark & { + background-color: @dark-bc-btn-bg; + box-shadow: inset 0 1px 0 @dark-bc-btn-bg-down; + color: @dark-bc-text; + } + } + .git-remotes { + border-radius: 4px 0 0 4px; + padding-bottom: 5px; + .caret { + border-bottom-color: @bc-text; + margin: 7px 5px auto 0px; + + .dark & { + border-bottom-color: @dark-bc-text; + } + } + } + .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%; + } + .git-remotes-dropdown a { + .change-remote { + display: inline-block; + width: 100%; + } + .hover-icon { + opacity: 0; + background-image: none !important; + width: 16px; + height: 16px; + font-size: 20px; + color: rgba(0, 0, 0, 0.5); + line-height: 15px; + text-align: center; + &:hover { + color: rgba(0, 0, 0, 1); + } + } + &:hover .hover-icon { + opacity: 1; + } + &[class$="-remote-new"] { + font-style: italic; + } + } + + .dropdown-menu(); + + // class for buttons that are performing an action + .btn-loading, .btn-loading:active { + background-size: 30px 30px; + background-image: linear-gradient( + 45deg, + rgba(0,0,0,0.1) 25%, + transparent 25%, + transparent 50%, + rgba(0,0,0,0.1) 50%, + rgba(0,0,0,0.1) 75%, + transparent 75%, + transparent + ); + background-repeat: repeat; + -webkit-animation: btn-loading 1s linear infinite; + } + + @-webkit-keyframes btn-loading { + 0% { background-position: 0 0; } + 100% { background-position: 60px 30px; } + } + + .spinner { + display: none; + z-index: 1000; + position: absolute; + top: 50%; + left: 50%; + &.spin { + display: block; + } + } + + .git-file-history:active, + .git-history-toggle:active, + .btn.active:active { + background-color: @bc-bg-highlight-selected !important; + + .dark & { + background-color: @dark-bc-bg-highlight-selected !important; + } + } + + .btn.active:not([disabled]) { + background-color: @bc-bg-highlight-selected; + color: @bc-text-link; + + .dark & { + background-color: @dark-bc-bg-highlight-selected; + color: @dark-bc-text-alt; + } + } +} + +/* Toolbar icon */ +#git-toolbar-icon { + width: 24px; + height: 24px; + display: inline-block; + background: url("icons/git-icon.svg") no-repeat 0 0; + &.dirty { + background-position: -24px 0; + } + &.on { + background-position: 0 -24px; + } + &.on.dirty { + background-position: -24px -24px; + } + &.ok { + background-position: 0 -48px; + } + &.ok.dirty { + background-position: -24px -48px; + } +} + +/* Dialogs */ +#git-settings-dialog, +#git-question-dialog, +#git-commit-dialog, +#git-clone-dialog, +#git-diff-dialog { + .invalid { + border-color: @red; + } + input[type=text], input[type=password], textarea { + .sane-box-model; + width: 100%; + height: 2em; + } + .btn-80 { + min-width: 80px; + } +} + +#git-settings-dialog { + .modal-body { + min-height: 410px; // this needs to be set to a height that'll prevent the dialog to change size when tabs are being switched. + } + .nav-tabs { + border-bottom: 1px solid @bc-panel-separator; + + .dark & { + border-bottom: 1px solid @dark-bc-panel-separator; + } + + a { + color: @bc-text; + border: 1px solid transparent; + + .dark & { + color: @dark-bc-text; + } + } + a:hover { + background-color: rgba(0, 0, 0, 0.04); + } + > .active > a { + background-color: @bc-panel-bg !important; + border: 1px solid @bc-btn-border; + border-bottom: 1px solid @bc-panel-bg !important; + + .dark & { + background-color: @dark-bc-panel-bg !important; + border: 1px solid @dark-bc-btn-border; + border-bottom: 1px solid @dark-bc-panel-bg !important; + } + } + } + .tab-content { + margin-top: 1em; + } + select { + width: 280px; + } + .settings-info-i { + font-size: 12px; + color: #0078D4; + } +} + +#git-commit-dialog, #git-diff-dialog { + .modal-body { + .flex-box(column); + .commit-diff { + // shrink up to min-width + .flex-item(0, 1); + min-height: 100px; + } + } +} + +#git-commit-dialog { + .modal-body { + .accordion { + margin-top: 0; + margin-bottom: 1em; + } + .lint-errors { + + background-color: @bc-menu-bg; + border: 1px solid @bc-panel-separator; + + .dark & { + background-color: @dark-diff-lgray-bg; + border: 1px solid @dark-bc-btn-border; + color: @dark-diff-gray-text; + } + + border-radius: 3px; + // no grow, no shrink + .flex-item(0, 0); + max-height: 150px; + overflow: auto; + b { + color: @red-text; + } + } + .commit-message-box { + position: relative; + // no grow, no shrink + .flex-item(0, 0); + textarea[name="commit-message"] { + height: 6em; + } + input[name="commit-message"] { + padding-right: 60px; + } + input[name="commit-message-count"] { + position: absolute; + right: 0; + width: 50px; + top: 0; + border-top-left-radius:0; + border-bottom-left-radius:0; + text-align: center; + color: @bc-panel-separator; + &.over50 { + color: @orange-text; + } + &.over100 { + color: @red-text; + } + } + } + } +} + +#git-commit-diff-dialog { + -webkit-animation: none; + animation: none; + min-width: 800px; + .modal-body { + .flex-box(); + .commit-files { + .flex-item(0, 0); + margin-right: 10px; + width: 200px; + word-wrap: break-word; + overflow-y: auto; + .commit-label { + display: block; + font-weight: 500; + margin: 0 0 1em; + } + .extension { + color: @bc-panel-separator; + } + } + .commit-diff { + // shrink up to min-width + .flex-item(1, 1); + } + ul.nav-stacked { + a { + border: none; + border-radius: 0; + color: @bc-text; + cursor: pointer; + + .dark & { + color: @dark-bc-text; + } + } + a:hover { + background-color: @bc-bg-highlight; + + .dark & { + background-color: @dark-bc-bg-highlight; + } + } + .active { + background-color: #eee; + } + } + } +} + +pre.git-output { + font-size: 12px; + line-height: 18px; + background-color: @bc-input-bg; + border: 1px solid @bc-btn-border; + border-radius: @bc-border-radius; + color: @bc-text; + + .dark & { + background-color: @dark-bc-input-bg; + border: 1px solid @dark-bc-btn-border; + color: @dark-bc-text; + } +} + +/* Accordion Styles */ +.accordion { + border: 1px solid @bc-section-separator; + .dark & { + border: 1px solid @dark-bc-section-separator; + } + border-radius: 4px; + margin: 10px 0; +} + +.accordion-header { + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + cursor: pointer; + margin: 0; + font-size: 16px; + + .accordion-progress-bar { + position: absolute; + bottom: 0; /* Align with the bottom border */ + left: 0; + width: 100%; + height: 2px; /* Thin progress bar */ + background: transparent; /* Background of the progress bar container */ + overflow: hidden; /* Ensure inner progress doesn't overflow */ + } + + .accordion-progress-bar-inner { + width: 0; /* Start at 0% */ + height: 100%; + background: #007bff; /* Progress bar color */ + transition: width 0.3s ease; /* Smooth animation for progress updates */ + } +} + +.accordion-header i { + transition: transform 0.3s ease; +} + +/* Rotate the icon when expanded */ +.accordion-toggle:checked + .accordion-header i { + transform: rotate(180deg); +} + +.accordion-content { + display: none; + padding: 15px; + border-top: 1px solid @bc-section-separator; + .dark & { + border-top: 1px solid @dark-bc-section-separator; + } +} + +/* Show the content when the checkbox is checked */ +.accordion-toggle:checked + .accordion-header + .accordion-content { + display: block; +} + +/* Hide the checkbox */ +.accordion-toggle { + display: none; +} + +/* +these mixins were copied out from the Brackets, +because there's no way to import them right now +*/ + +// https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing +.sane-box-model { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/user-select +.user-select(@type: none) { + -webkit-user-select: @type; + -khtml-user-select: @type; + -moz-user-select: @type; + -ms-user-select: @type; + -o-user-select: @type; + user-select: @type; +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction +.flex-box(@direction: row) { + display: -webkit-flex; + -webkit-flex-direction: @direction; + display: flex; + flex-direction: @direction; +} + +// https://developer.mozilla.org/en-US/docs/Web/CSS/flex +.flex-item(@grow: 0, @shrink: 1, @basis: auto) { + -webkit-flex: @grow @shrink @basis; + flex: @grow @shrink @basis; +} diff --git a/src/extensions/default/Git/styles/brackets/brackets_core_ui_variables.less b/src/extensions/default/Git/styles/brackets/brackets_core_ui_variables.less new file mode 100644 index 0000000000..5df170ce39 --- /dev/null +++ b/src/extensions/default/Git/styles/brackets/brackets_core_ui_variables.less @@ -0,0 +1,211 @@ +// Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +/* + * Brackets Colors + * + * These are general purpose colors that can be used in defining + * themes or UI elements. + + * IMPORTANT: IF we want a UI element to be themeable, these variable names or + * color literals (#aaa) should not be used in its definition. + * + * Instead, a new semantically-meaningful variables/mixins should be added + * to the "brackets_theme_default.less" file, and then these variables/mixins + * should be used in the definition of the UI element + * + * For UI elements we do NOT want to theme, we should use these color names + * + * All brackets color variable names (that refer to an actual color) + * are prefixed with "bc-" for "brackets". This is to avoid confusion + * with system and css color names. (We define our own colors because system + * colors are ugly.) + */ + +// General +@bc-bg-highlight: #e0f0fa; +@bc-bg-highlight-selected: #cddae0; +@bc-bg-inline-widget: #e6e9e9; +@bc-bg-tool-bar: #5D5F60; +@bc-bg-status-bar: #fff; +@bc-disabled-opacity: 0.3; +@bc-error: #f74687; +@bc-modal-backdrop-opacity: 0.4; + +// Highlights and Shadows +@bc-highlight: rgba(255, 255, 255, 0.12); +@bc-highlight-hard: rgba(255, 255, 255, 0.5); +@bc-shadow: rgba(0, 0, 0, 0.24); +@bc-shadow-large: rgba(0, 0, 0, 0.5); +@bc-shadow-small: rgba(0, 0, 0, 0.06); + +// Border Radius +@bc-border-radius: 3px; +@bc-border-radius-large: 5px; +@bc-border-radius-small: 2px; + +// Menu +@bc-menu-bg: #fff; +@bc-menu-text: #000; +@bc-menu-separator: #eaeaea; + +// Warning +@bc-warning-bg: #fdf5cc; +@bc-warning-text: #635301; + +// Text +@bc-text: #333; +@bc-text-alt: #fff; +@bc-text-emphasized: #111; +@bc-text-link: #0083e8; +@bc-text-medium: #606060; +@bc-text-quiet: #aaa; +@bc-text-thin: #000; +@bc-text-thin-quiet: #777; + +// Panel +@bc-panel-bg: #dfe2e2; +@bc-panel-bg-alt: #e6e9e9; +@bc-panel-bg-promoted: #d4d7d7; +@bc-panel-bg-hover: rgba(255, 255, 255, 0.6); +@bc-panel-bg-hover-alt: rgba(0, 0, 0, 0.04); +@bc-panel-bg-selected: #d0d5d5; +@bc-panel-bg-text-highlight: #fff; +@bc-panel-border: rgba(0, 0, 0, 0.09); +@bc-panel-separator: #c3c6c5; +@bc-section-separator: #c6bdbd; + +// Default Button +@bc-btn-bg: #e5e9e9; +@bc-btn-bg-down: #d3d7d7; +@bc-btn-bg-down-alt: #404141; +@bc-btn-border: #b2b5b5; +@bc-btn-border-error: #fa689d; +@bc-btn-border-error-glow: #ffb0cd; +@bc-btn-border-focused: #2893ef; +@bc-btn-border-focused-glow: #94ceff; +@bc-btn-triangle: #878787; +@bc-input-bg: #fff; + +// Primary Button +@bc-primary-btn-bg: #288edf; +@bc-primary-btn-bg-down: #0380e8; +@bc-primary-btn-border: #1474bf; + +// Secondary Button +@bc-secondary-btn-bg: #91cc41; +@bc-secondary-btn-bg-down: #82b839; +@bc-secondary-btn-border: #74B120; + +// Sidebar +@bc-sidebar-bg: #3C3F41; +@bc-sidebar-selection: #2D2E30; + +// images +@button-icon: "images/find-replace-sprites.svg"; +@jstree-sprite: url("images/jsTreeSprites.svg") !important; + + + +/* Dark Core UI variables -----------------------------------------------------------------------------*/ + +// General +@dark-bc-bg-highlight: #2a3b50; +@dark-bc-bg-highlight-selected: #2e3e44; +@dark-bc-bg-inline-widget: #1b1b1b; +@dark-bc-bg-tool-bar: #5D5F60; +@dark-bc-bg-status-bar: #1c1c1e; +@dark-bc-disabled-opacity: 0.3; +@dark-bc-error: #f74687; +@dark-bc-modal-backdrop-opacity: 0.7; + +// Highlights and Shadows +@dark-bc-highlight: rgba(255, 255, 255, 0.06); +@dark-bc-highlight-hard: rgba(255, 255, 255, 0.2); +@dark-bc-shadow: rgba(0, 0, 0, 0.24); +@dark-bc-shadow-medium: rgba(0, 0, 0, 0.12); +@dark-bc-shadow-large: rgba(0, 0, 0, 0.5); +@dark-bc-shadow-small: rgba(0, 0, 0, 0.06); + +// Border Radius +@dark-bc-border-radius: 3px; +@dark-bc-border-radius-large: 5px; +@dark-bc-border-radius-small: 2px; + +// Menu +@dark-bc-menu-bg: #000; +@dark-bc-menu-text: #fff; +@dark-bc-menu-separator: #343434; + +// Warning +@dark-bc-warning-bg: #c95800; +@dark-bc-warning-text: #fff; + +// Text +@dark-bc-text: #ccc; +@dark-bc-text-alt: #fff; +@dark-bc-text-emphasized: #fff; +@dark-bc-text-link: #6bbeff; +@dark-bc-text-medium: #ccc; +@dark-bc-text-quiet: #aaa; +@dark-bc-text-thin: #fff; +@dark-bc-text-thin-quiet: #bbb; + +// Panel +@dark-bc-panel-bg: #2c2c2c; +@dark-bc-panel-bg-alt: #313131; +@dark-bc-panel-bg-promoted: #222; +@dark-bc-panel-bg-hover: rgba(255, 255, 255, 0.12); +@dark-bc-panel-bg-hover-alt: rgba(0, 0, 0, 0.04); +@dark-bc-panel-bg-selected: #3d3e40; +@dark-bc-panel-bg-text-highlight: #000; +@dark-bc-panel-border: #000; +@dark-bc-panel-separator: #343434; +@dark-bc-section-separator: #202020; + +// Default Button +@dark-bc-btn-bg: #3f3f3f; +@dark-bc-btn-bg-down: #222; +@dark-bc-btn-bg-down-alt: #404141; +@dark-bc-btn-border: #202020; +@dark-bc-btn-border-error: #fa689d; +@dark-bc-btn-border-error-glow: transparent; +@dark-bc-btn-border-focused: #2893ef; +@dark-bc-btn-border-focused-glow: transparent; +@dark-bc-btn-triangle: #aaa; +@dark-bc-input-bg: #555; + +// Primary Button +@dark-bc-primary-btn-bg: #016dc4; +@dark-bc-primary-btn-bg-down: #00569b; +@dark-bc-primary-btn-border: #202020; + +// Secondary Button +@dark-bc-secondary-btn-bg: #5b9e00; +@dark-bc-secondary-btn-bg-down: #437900; +@dark-bc-secondary-btn-border: #202020; + +// Sidebar +@dark-bc-sidebar-bg: #3C3F41; +@dark-bc-sidebar-selection: #2D2E30; + +// images +@dark-button-icon: "images/find-replace-sprites-dark.svg"; +@dark-jstree-sprite: url("images/jsTreeSprites-dark.svg") !important; diff --git a/src/extensions/default/Git/styles/code-mirror.less b/src/extensions/default/Git/styles/code-mirror.less new file mode 100644 index 0000000000..e8f4d8ecda --- /dev/null +++ b/src/extensions/default/Git/styles/code-mirror.less @@ -0,0 +1,53 @@ +// main: brackets-git.less + +.CodeMirror { + .brackets-git-gutter { + width: @gutterWidth; + margin-left: 1px; + } + .brackets-git-gutter-added, + .brackets-git-gutter-modified, + .brackets-git-gutter-removed { + background-size: @gutterWidth @gutterWidth; + background-repeat: no-repeat; + font-size: 1em; + font-weight: bold; + color: @bc-menu-bg; + + .dark & { + color: @dark-bc-menu-bg; + } + } + .brackets-git-gutter-added { + background-color: @green; + } + + .brackets-git-gutter-modified { + background-color: @orange; + } + + .brackets-git-gutter-removed { + background-color: @red; + } + + .brackets-git-gutter-deleted-lines { + color: @bc-text; + background-color: lighten(@red, 25%); + .selectable-text(); + + .dark & { + background-color: darken(@red, 25%); + color: @dark-bc-text; + } + + position: relative; + .brackets-git-gutter-copy-button { + position: absolute; + left: 0; + top: 0; + padding: 1px; + height: 1.2em; + line-height: 0.5em; + } + } +} diff --git a/src/extensions/default/Git/styles/colors.less b/src/extensions/default/Git/styles/colors.less new file mode 100644 index 0000000000..786b38a281 --- /dev/null +++ b/src/extensions/default/Git/styles/colors.less @@ -0,0 +1,36 @@ +// main: brackets-git.less + +// TODO: try to reuse colors from brackets_colors.less instead of these + +@moreDarkGrey: #868888; +@green: #91CC41; +@red: #F74687; +@red-text: #F74687; +@red-background: #FF7CAD; +@blue-text: #1976DD; +@dark-blue-text: #51c0ff; +@orange: #E3B551; +@orange-text: #e28200; + +// Diff colors ('d' for 'dark', `l` for "light") +@diff-gray-text: #333333; +@diff-dgray-bg: #444444; +@diff-lgray-bg: #F0F0F7; +@diff-gray-border: #CBCBCB; +@diff-lgreen-bg: #DBFFDB; +@diff-green-bg: #CEFFCE; +@diff-green-border: #A1CFA1; +@diff-lred-bg: #FFDBDB; +@diff-red-bg: #F7C8C8; +@diff-red-border: #E9AEAE; + +@dark-diff-gray-text: #eeeeee; +@dark-diff-dgray-bg: #3f3f3f; +@dark-diff-lgray-bg: #555555; +@dark-diff-gray-border: #3f3f3f; +@dark-diff-lgreen-bg: #197e19; +@dark-diff-green-bg: #137413; +@dark-diff-green-border: #005c00; +@dark-diff-lred-bg: #af4462; +@dark-diff-red-bg: #a33a57; +@dark-diff-red-border: #831919; diff --git a/src/extensions/default/Git/styles/commit-diff.less b/src/extensions/default/Git/styles/commit-diff.less new file mode 100644 index 0000000000..46da8d167a --- /dev/null +++ b/src/extensions/default/Git/styles/commit-diff.less @@ -0,0 +1,168 @@ +// main: brackets-git.less + +.commit-diff { + @lineHeight: 15px; + color: @diff-gray-text; + .selectable-text(); + + .dark & { + background-color: @dark-diff-lgray-bg; + border: 1px solid @dark-bc-btn-border; + color: @dark-diff-gray-text; + } + + code, pre { + background-color: @diff-lgray-bg; + color: @diff-gray-text; + border: none; + padding: 0 3px; + margin: 0; + + .dark & { + background-color: @dark-diff-lgray-bg; + color: @dark-diff-gray-text; + } + } + background-color: @diff-lgray-bg; + border: 1px solid @bc-btn-border; + border-radius: 3px; + margin-bottom: 1em; + overflow: auto; + padding: 0; + + // FIXME: this part will be removed after github.com/adobe/brackets/issues/7673 + table:not(.table-striped) { + > tbody { + > tr:nth-child(even), tr:nth-child(odd) { + > td, + > th { + background-color: transparent; + } + } + } + } + table { + width: 100%; + cursor: text; + tbody { + tr.meta-file { + th { + border-top: 0; + color: @blue-text; + padding: @lineHeight / 2; + text-align: left; + + .dark & { + color: @dark-blue-text; + } + } + &:not(:first-child) { + border-top: 1px dashed @bc-btn-border; + + .dark & { + border-top: 1px dashed @dark-bc-btn-border; + } + } + } + tr.separator { + height: @lineHeight; + &:first-child, &:last-child { + display: none; + } + } + tr { + td { + border-width: 0px; + padding: 0 8px; + &.row-num { + width: 1px; + border-right: 1px solid; + border-color: @diff-gray-border; + text-align: right; + color: @bc-text; + .user-select(none); + + .dark & { + border-color: @dark-diff-gray-border; + color: @dark-bc-text; + } + } + pre { + white-space: nowrap; + .trailingWhitespace { + background-color: @diff-red-bg; + + .dark & { + background-color: @dark-diff-red-bg; + } + } + } + } + &.added { + &, & pre { + background-color: @diff-lgreen-bg; + + .dark & { + background-color: @dark-diff-lgreen-bg; + } + } + pre:before { + content: "+"; + } + .row-num { + background-color: @diff-green-bg !important; //FIXME: `!important` will be removed after github.com/adobe/brackets/issues/7673 + border-color: @diff-green-border; + + .dark & { + background-color: @dark-diff-green-bg !important; //FIXME: `!important` will be removed after github.com/adobe/brackets/issues/7673 + border-color: @dark-diff-green-border; + } + } + } + &.removed { + &, & pre { + background-color: @diff-lred-bg; + + .dark & { + background-color: @dark-diff-lred-bg; + } + } + pre:before { + content: "-"; + } + .row-num { + background-color: @diff-red-bg !important; //FIXME: `!important` will be removed after github.com/adobe/brackets/issues/7673 + border-color: @diff-red-border; + + .dark & { + background-color: @dark-diff-red-bg !important; + border-color: @dark-diff-red-border; + } + } + } + &.unchanged { + pre:before { + content: "\0A0"; //   + } + } + &.diffCmd { + color: @blue-text; + } + &.position { + td { + padding: 8px; + } + &, & pre { + color: @diff-gray-text; + background-color: @diff-lgray-bg; + + .dark & { + color: @dark-diff-gray-text; + background-color: @dark-diff-lgray-bg; + } + } + } + } + } + } +} diff --git a/src/extensions/default/Git/styles/common.less b/src/extensions/default/Git/styles/common.less new file mode 100644 index 0000000000..6fb357d304 --- /dev/null +++ b/src/extensions/default/Git/styles/common.less @@ -0,0 +1,124 @@ +// main: brackets-git.less +.git { + hr { + margin: 12px auto; + width: 95%; + height: 1px; + border: none; + background-color: @bc-panel-separator; + + .dark & { + background-color: @dark-bc-panel-separator; + } + } + + /* radio buttons until they are styled in brackets */ + input[type="radio"] { + margin: 0; + } + input[type="radio"] { + height: 13px; + width: 13px; + vertical-align: middle; + border: 1px solid @bc-btn-border; + border-radius: 13px; + background-color: @bc-btn-bg; + -webkit-appearance: none; + box-shadow: inset 0 1px 0 @bc-highlight; + + .dark & { + border: 1px solid @dark-bc-btn-border; + background-color: @dark-bc-btn-bg; + box-shadow: inset 0 1px 0 @dark-bc-highlight; + } + } + input[type="radio"]:active:not(:disabled) { + background-color: @bc-btn-bg-down; + box-shadow: inset 0 1px 0 @bc-shadow-small; + + .dark & { + background-color: @dark-bc-btn-bg-down; + box-shadow: inset 0 1px 0 @dark-bc-shadow-small; + } + } + input[type="radio"]:focus { + outline:none; + border: 1px solid @bc-btn-border-focused; + box-shadow: 0 0 0 1px @bc-btn-border-focused-glow; + + .dark & { + border: 1px solid @dark-bc-btn-border-focused; + box-shadow: 0 0 0 1px @dark-bc-btn-border-focused-glow; + } + } + input[type="radio"]:checked:before { + font-weight: bold; + color: @bc-text; + content: '\25cf'; + -webkit-margin-start: 0; + position: relative; + left: 2px; + top: -4px; + font-size: 12px; + + .dark & { + color: @dark-bc-text; + } + } + /* /radio buttons */ + + .text-bold { + font-weight: 500; + } + .text-quiet { + color: @bc-text-quiet; + + .dark & { + color: @dark-bc-text-quiet; + } + } + + @small: 5px; + + .padding-right-small { + padding-right: @small; + } +} + +// Additional icons for GitHub Octicon iconic font +.octicon { + &.octicon-expand, &.octicon-collapse { + position: relative; + width: 12px; + height: 12px; + zoom: 1.3; + &:before, &:after { + position: absolute; + } + &:before { + -webkit-transform: rotate(135deg); + } + &:after { + -webkit-transform: rotate(315deg); + } + } + &.octicon-expand { + &:before, &:after { + content: "\F071"; + } + &:before { + top: -5px; + left: 5px; + } + } + &.octicon-collapse { + -webkit-transform: translateY(1px); + &:before, &:after { + content: "\F0A1"; + } + &:before { + top: -6px; + left: 6px; + } + } +} diff --git a/src/extensions/default/Git/styles/dialogs-all.less b/src/extensions/default/Git/styles/dialogs-all.less new file mode 100644 index 0000000000..212e9a8fd9 --- /dev/null +++ b/src/extensions/default/Git/styles/dialogs-all.less @@ -0,0 +1,53 @@ +#git-progress-dialog { + textarea { + height: 300px; + width: calc(100% - 2px); + margin: unset; + padding: unset; + &[readonly="readonly"] { + cursor: default; + } + } +} + +#git-diff-dialog { + .commit-diff { + .meta-file { + display: none; + } + } +} + +#git-error-dialog { + pre { + white-space: pre; + word-wrap: normal; + overflow: scroll; + .selectable-text(); + } +} + +#git-pull-dialog { + .modal-body { + max-height: 500px; + } + .row-fluid { + label { + line-height: 28px; + } + } +} + +.git { + .current-tracking-branch { + display: flex; + gap: 8px; + } + .input-append { + display: flex; + align-items: center; + button { + height: 28px; + } + } +} diff --git a/src/extensions/default/Git/styles/fonts/octicon.less b/src/extensions/default/Git/styles/fonts/octicon.less new file mode 100644 index 0000000000..22cb55e7d2 --- /dev/null +++ b/src/extensions/default/Git/styles/fonts/octicon.less @@ -0,0 +1,611 @@ +@font-face { + font-family: 'octicons'; + src: url('octicons-regular-webfont.eot'); + src: url('octicons-regular-webfont.eot?#iefix') format('embedded-opentype'), + url('octicons-regular-webfont.woff') format('woff'), + url('octicons-regular-webfont.svg#octiconsregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.octicon { + font:normal normal 16px octicons; + line-height:1; + display:inline-block; + text-decoration:none; + -webkit-font-smoothing:antialiased +} +.mega-octicon { + font:normal normal 32px octicons; + line-height:1; + display:inline-block; + text-decoration:none; + -webkit-font-smoothing:antialiased +} +.octicon-alert:before { + content:'\f02d' +} +.octicon-alignment-align:before { + content:'\f08a' +} +.octicon-alignment-aligned-to:before { + content:'\f08e' +} +.octicon-alignment-unalign:before { + content:'\f08b' +} +.octicon-arrow-down:before { + content:'\f03f' +} +.octicon-arrow-left:before { + content:'\f040' +} +.octicon-arrow-right:before { + content:'\f03e' +} +.octicon-arrow-small-down:before { + content:'\f0a0' +} +.octicon-arrow-small-left:before { + content:'\f0a1' +} +.octicon-arrow-small-right:before { + content:'\f071' +} +.octicon-arrow-small-up:before { + content:'\f09f' +} +.octicon-arrow-up:before { + content:'\f03d' +} +.octicon-beer:before { + content:'\f069' +} +.octicon-book:before { + content:'\f007' +} +.octicon-bookmark:before { + content:'\f07b' +} +.octicon-broadcast:before { + content:'\f048' +} +.octicon-browser:before { + content:'\f0c5' +} +.octicon-bug:before { + content:'\f091' +} +.octicon-calendar:before { + content:'\f068' +} +.octicon-check:before { + content:'\f03a' +} +.octicon-checklist:before { + content:'\f076' +} +.octicon-chevron-down:before { + content:'\f0a3' +} +.octicon-chevron-left:before { + content:'\f0a4' +} +.octicon-chevron-right:before { + content:'\f078' +} +.octicon-chevron-up:before { + content:'\f0a2' +} +.octicon-circle-slash:before { + content:'\f084' +} +.octicon-clippy:before { + content:'\f035' +} +.octicon-clock:before { + content:'\f046' +} +.octicon-cloud-download:before { + content:'\f00b' +} +.octicon-cloud-upload:before { + content:'\f00c' +} +.octicon-code:before { + content:'\f05f' +} +.octicon-color-mode:before { + content:'\f065' +} +.octicon-comment:before { + content:'\f02b' +} +.octicon-comment-add:before { + content:'\f06f' +} +.octicon-comment-discussion:before { + content:'\f04f' +} +.octicon-credit-card:before { + content:'\f045' +} +.octicon-dash:before { + content:'\f0ca' +} +.octicon-dashboard:before { + content:'\f07d' +} +.octicon-database:before { + content:'\f096' +} +.octicon-device-camera:before { + content:'\f056' +} +.octicon-device-camera-video:before { + content:'\f057' +} +.octicon-device-desktop:before { + content:'\f27c' +} +.octicon-device-mobile:before { + content:'\f038' +} +.octicon-diff:before { + content:'\f04d' +} +.octicon-diff-added:before { + content:'\f06b' +} +.octicon-diff-ignored:before { + content:'\f099' +} +.octicon-diff-modified:before { + content:'\f06d' +} +.octicon-diff-removed:before { + content:'\f06c' +} +.octicon-diff-renamed:before { + content:'\f06e' +} +.octicon-ellipsis:before { + content:'\f09a' +} +.octicon-eye:before { + content:'\f04e' +} +.octicon-eye-unwatch:before { + content:'\f01e' +} +.octicon-eye-watch:before { + content:'\f01d' +} +.octicon-file-add:before { + content:'\f086' +} +.octicon-file-binary:before { + content:'\f094' +} +.octicon-file-code:before { + content:'\f010' +} +.octicon-file-directory:before { + content:'\f016' +} +.octicon-file-directory-create:before { + content:'\f095' +} +.octicon-file-media:before { + content:'\f012' +} +.octicon-file-pdf:before { + content:'\f014' +} +.octicon-file-submodule:before { + content:'\f017' +} +.octicon-file-symlink-directory:before { + content:'\f0b1' +} +.octicon-file-symlink-file:before { + content:'\f0b0' +} +.octicon-file-text:before { + content:'\f011' +} +.octicon-file-zip:before { + content:'\f013' +} +.octicon-fold:before { + content:'\f0cc' +} +.octicon-gear:before { + content:'\f02f' +} +.octicon-gift:before { + content:'\f042' +} +.octicon-gist:before { + content:'\f00e' +} +.octicon-gist-fork:before { + content:'\f079' +} +.octicon-gist-new:before { + content:'\f07a' +} +.octicon-gist-private:before { + content:'\f00f' +} +.octicon-gist-secret:before { + content:'\f08c' +} +.octicon-git-branch:before { + content:'\f020' +} +.octicon-git-branch-create:before { + content:'\f098' +} +.octicon-git-branch-delete:before { + content:'\f09b' +} +.octicon-git-commit:before { + content:'\f01f' +} +.octicon-git-compare:before { + content:'\f0ac' +} +.octicon-git-fork-private:before { + content:'\f021' +} +.octicon-git-merge:before { + content:'\f023' +} +.octicon-git-pull-request:before { + content:'\f009' +} +.octicon-git-pull-request-abandoned:before { + content:'\f090' +} +.octicon-globe:before { + content:'\f0b6' +} +.octicon-graph:before { + content:'\f043' +} +.octicon-heart:before { + content:'\2665' +} +.octicon-history:before { + content:'\f07e' +} +.octicon-home:before { + content:'\f08d' +} +.octicon-horizontal-rule:before { + content:'\f070' +} +.octicon-hourglass:before { + content:'\f09e' +} +.octicon-hubot:before { + content:'\f09d' +} +.octicon-info:before { + content:'\f059' +} +.octicon-issue-closed:before { + content:'\f028' +} +.octicon-issue-opened:before { + content:'\f026' +} +.octicon-issue-reopened:before { + content:'\f027' +} +.octicon-jersey:before { + content:'\f019' +} +.octicon-jump-down:before { + content:'\f072' +} +.octicon-jump-left:before { + content:'\f0a5' +} +.octicon-jump-right:before { + content:'\f0a6' +} +.octicon-jump-up:before { + content:'\f073' +} +.octicon-key:before { + content:'\f049' +} +.octicon-keyboard:before { + content:'\f00d' +} +.octicon-light-bulb:before { + content:'\f000' +} +.octicon-link:before { + content:'\f05c' +} +.octicon-link-external:before { + content:'\f07f' +} +.octicon-list-ordered:before { + content:'\f062' +} +.octicon-list-unordered:before { + content:'\f061' +} +.octicon-location:before { + content:'\f060' +} +.octicon-lock:before { + content:'\f06a' +} +.octicon-log-in:before { + content:'\f036' +} +.octicon-log-out:before { + content:'\f032' +} +.octicon-logo-github:before { + content:'\f092' +} +.octicon-mail:before { + content:'\f03b' +} +.octicon-mail-read:before { + content:'\f03c' +} +.octicon-mail-reply:before { + content:'\f051' +} +.octicon-mark-github:before { + content:'\f00a' +} +.octicon-mark-twitter:before { + content:'\f0ae' +} +.octicon-markdown:before { + content:'\f0c9' +} +.octicon-megaphone:before { + content:'\f077' +} +.octicon-mention:before { + content:'\f0be' +} +.octicon-microscope:before { + content:'\f089' +} +.octicon-milestone:before { + content:'\f075' +} +.octicon-mirror-private:before { + content:'\f025' +} +.octicon-mirror-public:before { + content:'\f024' +} +.octicon-move-down:before { + content:'\f0a8' +} +.octicon-move-left:before { + content:'\f074' +} +.octicon-move-right:before { + content:'\f0a9' +} +.octicon-move-up:before { + content:'\f0a7' +} +.octicon-mute:before { + content:'\f080' +} +.octicon-mute-video:before { + content:'\f0b8' +} +.octicon-no-newline:before { + content:'\f09c' +} +.octicon-octoface:before { + content:'\f008' +} +.octicon-organization:before { + content:'\f037' +} +.octicon-package:before { + content:'\f0c4' +} +.octicon-pencil:before { + content:'\f058' +} +.octicon-person:before { + content:'\f018' +} +.octicon-person-add:before { + content:'\f01a' +} +.octicon-person-follow:before { + content:'\f01c' +} +.octicon-person-remove:before { + content:'\f01b' +} +.octicon-pin:before { + content:'\f041' +} +.octicon-playback-fast-forward:before { + content:'\f0bd' +} +.octicon-playback-pause:before { + content:'\f0bb' +} +.octicon-playback-play:before { + content:'\f0bf' +} +.octicon-playback-rewind:before { + content:'\f0bc' +} +.octicon-plus:before { + content:'\f05d' +} +.octicon-podium:before { + content:'\f0af' +} +.octicon-primitive-dot:before { + content:'\f052' +} +.octicon-primitive-square:before { + content:'\f053' +} +.octicon-pulse:before { + content:'\f085' +} +.octicon-puzzle:before { + content:'\f0c0' +} +.octicon-question:before { + content:'\f02c' +} +.octicon-quote:before { + content:'\f063' +} +.octicon-radio-tower:before { + content:'\f030' +} +.octicon-remove-close:before { + content:'\f050' +} +.octicon-repo:before { + content:'\f001' +} +.octicon-repo-clone:before { + content:'\f04c' +} +.octicon-repo-create:before { + content:'\f003' +} +.octicon-repo-delete:before { + content:'\f004' +} +.octicon-repo-force-push:before { + content:'\f04a' +} +.octicon-repo-forked:before { + content:'\f002' +} +.octicon-repo-pull:before { + content:'\f006' +} +.octicon-repo-push:before { + content:'\f005' +} +.octicon-repo-sync:before { + content:'\f04b' +} +.octicon-rocket:before { + content:'\f033' +} +.octicon-rss:before { + content:'\f034' +} +.octicon-ruby:before { + content:'\f047' +} +.octicon-screen-full:before { + content:'\f066' +} +.octicon-screen-normal:before { + content:'\f067' +} +.octicon-search:before { + content:'\f02e' +} +.octicon-search-save:before { + content:'\f0cb' +} +.octicon-server:before { + content:'\f097' +} +.octicon-settings:before { + content:'\f07c' +} +.octicon-split:before { + content:'\f0c6' +} +.octicon-squirrel:before { + content:'\f0b2' +} +.octicon-star:before { + content:'\f02a' +} +.octicon-star-add:before { + content:'\f082' +} +.octicon-star-delete:before { + content:'\f083' +} +.octicon-steps:before { + content:'\f0c7' +} +.octicon-stop:before { + content:'\f08f' +} +.octicon-sync:before { + content:'\f087' +} +.octicon-tag:before { + content:'\f015' +} +.octicon-tag-add:before { + content:'\f054' +} +.octicon-tag-remove:before { + content:'\f055' +} +.octicon-telescope:before { + content:'\f088' +} +.octicon-terminal:before { + content:'\f0c8' +} +.octicon-three-bars:before { + content:'\f05e' +} +.octicon-tools:before { + content:'\f031' +} +.octicon-triangle-down:before { + content:'\f05b' +} +.octicon-triangle-left:before { + content:'\f044' +} +.octicon-triangle-right:before { + content:'\f05a' +} +.octicon-triangle-up:before { + content:'\f0aa' +} +.octicon-unfold:before { + content:'\f039' +} +.octicon-unmute:before { + content:'\f0ba' +} +.octicon-unmute-video:before { + content:'\f0b9' +} +.octicon-versions:before { + content:'\f064' +} +.octicon-x:before { + content:'\f081' +} +.octicon-zap:before { + content:'\26A1' +} diff --git a/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.eot b/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..685e629c2f078cd6dd61d1cafddd85138d9db2d8 GIT binary patch literal 35820 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 + + +Copyright (C) 2013 by GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.ttf b/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2c910f99b4caaa53931f007ebf6214f887ca78a9 GIT binary patch literal 35652 zcmd_Td0-sJbtl}fd#2|eU@#ZX!3+j7L)^qQLy;8F;4M-TB~q3tQ5FFL0}>(#KsYE- zmc^DLS+e6pzT&Jc$FS^ZZO1um%ZZ(MrM7&@S=m`D-pxv%Bg0PY*m1tBqRnX&gYWmM zdIm%4@_xyGJDBONuCA`GdiCnvRZvJ#6t7|`nvy(xv~S?&+jNqU88j$`Be(k5v zF5me{JdI~fMbU<4=O-s35B2{~MbVET?~$_;cP`M+mA^&WjY7$}iL=Guzti|_MY#v> zQ14rqUt0d1@0xE_lzY3;$a)2B)r<{8xy6U}ZTyw5i*qjdd|Tuoch~3N^0N~)JO4@X zIYfa!YqA6}S2} z^%>;7Tb|MW0dBpjI5oTEWvA<=_f9EBw)oQBvh8^9cTmJ`Ik%1fYw<&x z^UFWJS5BADqCE0n@oa9^#ht3uYv)($z%{plf7S0Om*;ZTX>t8+nOCl(d=~BCdhDO5 z)2_EVulzo7c5T*07i?JIneu!2tYR!G@2I+0u9I-T(#KC-T9(Ujz2!Q}@8N9l z+Jo|Zzw%Db!}aAq73Bnm^L_k8n+?>vF4JVme7+~jnYhM%Q4jL?EZSfp{a96d_#Xd0 zW}o@KDbwN|xE5#Rseh>`JS!@&S7cg6nesDgHB4}y+fx3O;Xx>0PM6R84u%`oBi@JW z@_YEY`p0F&7?jHx^1586TnE=(Nu#W&7kQO($P@3d@0IiUj8FMivptUjE` zb1icGijpm6KlWnk#Vs$s_r*`Y_?Z_!_u?PEXubHom+sy`f9$);i|H3TUwl9A{vPgr z@x`ynyU#uH+(fNbtJYGjQnOK0s`+KjFKYfJcwKqs^7F6sA^nwd|F6tzSGFmCKyJF3 zo=_9&IrVwX)LwGTIlk|_!TF3nqMy?r*MH=Sxo&WM!d>US#r?4RpFOvFp7kE^{-JM+ z?_u9X|5pE}1Bt-<1J48_!E1w$27g+k*L2t1U-Nj)OSMMri=hLdh0x=5-n#qiJ`=tx z{8U7VL?SyQcSjzMd?ND2$TQK}=$+Bui+(wlh#iaF9arM};=fU^*FReSLc>7AxrQ$` z{A*)fW3ut?#uu7yZT2>IH@~&{{mqZIC@u9Z11;CJm@R+U@=~HPu_tjX@o?fBt=F|a z*80!Mk>oweXHs>keW|-r??|nt9#1`!`un!4+y1!inY5A~NROvi+jq6Uqy6{V|3~|a znYPT0nRjG9oq0AJ%RZ9*yIdrSeX+joKBMng-(ugp`X1Tp-g@)a4{iNgzqkL+0c&vA z;NwHyq2{51p?yP-4?Q*fQhsZGF8^r$r-jDCjfIni-z)sxws&p&#!nwb}-O1b(Gj79BP0%tYE>dAiX{x!QSnC!k7HUDFWS|@-?FLF=(l#pX%0A^!{2vX{6Z9*gL%+;F@QFGN-4 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 .header { + .flex-item(0, 0); + .author-line { + margin-bottom: 1em; + } + .commit-author { + @lineHeight: 36px; + height: @lineHeight; + line-height: @lineHeight; + .commit-author-avatar { + .commit-author-avatar-mixin(@lineHeight); + display: inline-block; + top: @lineHeight / 3; + } + .commit-author-name, .commit-author-email { + margin-left: 1em; + .selectable-text(); + } + } + padding: 25px 30px 5px; + &.shadow { + box-shadow: 0 -1px 3px 2px rgba(0, 0, 0, 0.15); + z-index: 1; + } + .commit-title { + font-size: 20px; + line-height: 24px; + font-weight: 500; + margin: 0 0 5px; + width: 90% + } + .close { + margin: -5px -3px 0 0; + padding-left: 50px; + } + .commit-hash, .commit-author, .commit-time { + margin-right: 10px; + display: inline-block; + color: @bc-text-thin-quiet; + + .dark & { + color: @dark-bc-text-thin-quiet; + } + i { + color: @bc-text-thin-quiet; + margin-right: 2px; + + .dark & { + color: @dark-bc-text-thin-quiet; + } + } + } + .actions { + float: right; + margin: -10px; + >* { + margin-right: 10px; + display: inline-block; + } + } + + } + > .body { + .flex-item(1, 1); + position: relative; + overflow-y: scroll; + li > a { + background: @bc-btn-bg; + border: 1px solid transparent; + border-right: 0; + border-left: 0; + color: @bc-text; + margin: 10px; + margin-bottom: 0; + + .dark & { + background: @dark-bc-btn-bg; + color: @dark-bc-text; + } + } + .commit-diff { + > pre { + white-space: pre-line + } + max-height: 0px; + margin: 10px; + opacity: 0; + transition: all ease 0.3s; + transition-property: padding, height, opacity; + padding-top: 0; + padding-bottom: 0; + border-top: 0; + margin-top: 0px; + border-radius: 0 0 3px 3px; + border-color: @bc-btn-border; + margin-bottom: 0; + + .dark & { + border-color: @dark-bc-btn-border; + } + .separator, .meta-file { + display: none; + } + } + .active+.commit-diff { + max-height: 99999px; + opacity: 1; + border-color: @bc-btn-border; + margin-bottom: 10px; + padding: 10px 0; + + .dark & { + border-color: @dark-bc-btn-border; + } + } + .opened { + display: none; + margin-top: 7px; + } + .closed { + display: inline-block; + margin-top: 5px; + } + .active { + background: @bc-menu-bg; + border: 1px solid @bc-btn-border; + margin-bottom: 0; + border-bottom: 0; + border-radius: 4px 4px 0 0; + + .dark & { + background: @dark-bc-menu-bg; + border: 1px solid @dark-bc-btn-border; + } + + a { + border: none; + background-color: transparent; + } + .opened { + display: inline-block; + } + .closed { + display: none; + } + } + .caret { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid @bc-text; + margin-right: 2px; + + .dark & { + border-top: 5px solid @dark-bc-text; + } + } + .caret.caret-right { + border-bottom: 4px solid transparent; + border-top: 4px solid transparent; + border-left: 5px solid @bc-text; + margin-right: -1px; + margin-left: 3px; + + .dark & { + border-left: 5px solid @dark-bc-text; + } + } + .commitBody { + padding: 0 30px; + } + .commit-files { + padding: 0 20px; + .openFile, .difftool { + vertical-align: -1px; + margin-left: 10px; + cursor: pointer; + opacity: 0.7; + } + } + .filesContainer { + margin-bottom: 10px; + } + .loadMore { + margin-bottom: 10px; + margin-left: 10px; + } + } + .message { + display: block; + padding: 10px; + } + + .dropdown-menu(); + + .toggle-diffs { + cursor: pointer; + margin-right: -10px; + margin-top: 14px; + .collapse { + display: none; + } + span.collapse { + height: auto; + } + .expand { + vertical-align: middle; + } + span.expand { + margin-left: 2px; + } + &.opened { + .expand { + display: none; + } + .collapse { + display: inline-block; + vertical-align: middle; + } + } + } +} diff --git a/src/extensions/default/Git/styles/icons/git-icon.svg b/src/extensions/default/Git/styles/icons/git-icon.svg new file mode 100644 index 0000000000..9f3b75e272 --- /dev/null +++ b/src/extensions/default/Git/styles/icons/git-icon.svg @@ -0,0 +1,19 @@ + + Brackets Git + Git Logo by Jason Long is licensed under the Creative Commons Attribution 3.0 Unported License. + + + + + + + + + + + + + diff --git a/src/extensions/default/Git/styles/mixins.less b/src/extensions/default/Git/styles/mixins.less new file mode 100644 index 0000000000..60683d5603 --- /dev/null +++ b/src/extensions/default/Git/styles/mixins.less @@ -0,0 +1,72 @@ +// main: brackets-git.less + +.selectable-text { + .user-select(text); +} + +.dropdown-menu() { + .dropdown-menu { + // 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%; + a { + padding: 5px 26px 5px 26px; + color: @bc-menu-text; + + .dark & { + color: @dark-bc-menu-text; + } + + &:hover { + background: @bc-bg-highlight; + color: @bc-menu-text; + + .dark & { + background: @dark-bc-bg-highlight; + color: @dark-bc-menu-text; + } + } + } + border: none; + border-radius: @bc-border-radius; + box-shadow: 0 3px 9px @bc-shadow; + + // mixin + .both() { + background: @bc-highlight; + color: @bc-menu-text; + + .dark & { + background: @dark-bc-bg-highlight; + color: @dark-bc-menu-text; + } + } + + > li { + > a { + cursor: default; + &:hover, > &:focus { + .both(); + } + } + } + + .dropdown-submenu:hover, .dropdown-submenu:focus { + > a { + .both(); + } + } + + .dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: @bc-menu-text; + + .dark & { + color: @dark-bc-menu-text; + } + } + + } +} diff --git a/src/extensions/default/Git/templates/authors-dialog.html b/src/extensions/default/Git/templates/authors-dialog.html new file mode 100644 index 0000000000..236e147e61 --- /dev/null +++ b/src/extensions/default/Git/templates/authors-dialog.html @@ -0,0 +1,29 @@ + diff --git a/src/extensions/default/Git/templates/branch-merge-dialog.html b/src/extensions/default/Git/templates/branch-merge-dialog.html new file mode 100644 index 0000000000..97ba80f01d --- /dev/null +++ b/src/extensions/default/Git/templates/branch-merge-dialog.html @@ -0,0 +1,49 @@ + diff --git a/src/extensions/default/Git/templates/branch-new-dialog.html b/src/extensions/default/Git/templates/branch-new-dialog.html new file mode 100644 index 0000000000..a21bac4b6c --- /dev/null +++ b/src/extensions/default/Git/templates/branch-new-dialog.html @@ -0,0 +1,32 @@ + diff --git a/src/extensions/default/Git/templates/default-gitignore b/src/extensions/default/Git/templates/default-gitignore new file mode 100644 index 0000000000..d3f11de80c --- /dev/null +++ b/src/extensions/default/Git/templates/default-gitignore @@ -0,0 +1,5 @@ +# https://git-scm.com/docs/gitignore +# https://help.github.com/articles/ignoring-files +# Example .gitignore files: https://github.com/github/gitignore +/bower_components/ +/node_modules/ \ No newline at end of file diff --git a/src/extensions/default/Git/templates/format-diff.html b/src/extensions/default/Git/templates/format-diff.html new file mode 100644 index 0000000000..d22c1c743d --- /dev/null +++ b/src/extensions/default/Git/templates/format-diff.html @@ -0,0 +1,17 @@ + + + {{#files}} + + + + {{#lines}} + + + + + + {{/lines}} + + {{/files}} + +
{{name}}
{{numLineOld}}{{numLineNew}}
{{{line}}}
diff --git a/src/extensions/default/Git/templates/git-branches-menu.html b/src/extensions/default/Git/templates/git-branches-menu.html new file mode 100644 index 0000000000..e9763993f3 --- /dev/null +++ b/src/extensions/default/Git/templates/git-branches-menu.html @@ -0,0 +1,23 @@ + diff --git a/src/extensions/default/Git/templates/git-commit-dialog-lint-results.html b/src/extensions/default/Git/templates/git-commit-dialog-lint-results.html new file mode 100644 index 0000000000..29329f4ee6 --- /dev/null +++ b/src/extensions/default/Git/templates/git-commit-dialog-lint-results.html @@ -0,0 +1,18 @@ +
    + {{#lintResults}} + {{#hasErrors}} +
  • + {{filename}} + +
  • + {{/hasErrors}} + {{/lintResults}} +
diff --git a/src/extensions/default/Git/templates/git-commit-dialog.html b/src/extensions/default/Git/templates/git-commit-dialog.html new file mode 100644 index 0000000000..6c25089dae --- /dev/null +++ b/src/extensions/default/Git/templates/git-commit-dialog.html @@ -0,0 +1,40 @@ + diff --git a/src/extensions/default/Git/templates/git-diff-dialog.html b/src/extensions/default/Git/templates/git-diff-dialog.html new file mode 100644 index 0000000000..d00bcb879f --- /dev/null +++ b/src/extensions/default/Git/templates/git-diff-dialog.html @@ -0,0 +1,11 @@ + diff --git a/src/extensions/default/Git/templates/git-error-dialog.html b/src/extensions/default/Git/templates/git-error-dialog.html new file mode 100644 index 0000000000..f2d596fbb2 --- /dev/null +++ b/src/extensions/default/Git/templates/git-error-dialog.html @@ -0,0 +1,12 @@ + diff --git a/src/extensions/default/Git/templates/git-output.html b/src/extensions/default/Git/templates/git-output.html new file mode 100644 index 0000000000..b87062e833 --- /dev/null +++ b/src/extensions/default/Git/templates/git-output.html @@ -0,0 +1,19 @@ + diff --git a/src/extensions/default/Git/templates/git-panel-history-commits.html b/src/extensions/default/Git/templates/git-panel-history-commits.html new file mode 100644 index 0000000000..574fe3e98f --- /dev/null +++ b/src/extensions/default/Git/templates/git-panel-history-commits.html @@ -0,0 +1,12 @@ +{{#commits}} + + +
+ {{avatarLetter}} +
+ + {{date.shown}} {{Strings.HISTORY_COMMIT_BY}} {{author}} + {{subject}} {{#hasTag}}{{tags}}{{/hasTag}} + {{hashShort}} + +{{/commits}} diff --git a/src/extensions/default/Git/templates/git-panel-history.html b/src/extensions/default/Git/templates/git-panel-history.html new file mode 100644 index 0000000000..c4ee9bbd3f --- /dev/null +++ b/src/extensions/default/Git/templates/git-panel-history.html @@ -0,0 +1,5 @@ + + + {{> commits}} + +
diff --git a/src/extensions/default/Git/templates/git-panel-results.html b/src/extensions/default/Git/templates/git-panel-results.html new file mode 100644 index 0000000000..4f94cf3f54 --- /dev/null +++ b/src/extensions/default/Git/templates/git-panel-results.html @@ -0,0 +1,20 @@ + + + {{#files}} + + + + + + + + {{/files}} + +
+ {{#allowDiff}}{{/allowDiff}} + {{statusText}}{{display}} +
+ {{#allowUndo}} {{/allowUndo}} + {{#allowDelete}} {{/allowDelete}} +
+
diff --git a/src/extensions/default/Git/templates/git-panel.html b/src/extensions/default/Git/templates/git-panel.html new file mode 100644 index 0000000000..94f2d4099a --- /dev/null +++ b/src/extensions/default/Git/templates/git-panel.html @@ -0,0 +1,76 @@ +
+
+ + + +
+ +
+
+
+ + + + +
+
+
+
+ + + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+
+ + + + + +
+
+
+ × + +
+
+
diff --git a/src/extensions/default/Git/templates/git-question-dialog.html b/src/extensions/default/Git/templates/git-question-dialog.html new file mode 100644 index 0000000000..b36fc97dfc --- /dev/null +++ b/src/extensions/default/Git/templates/git-question-dialog.html @@ -0,0 +1,20 @@ + diff --git a/src/extensions/default/Git/templates/git-remotes-picker.html b/src/extensions/default/Git/templates/git-remotes-picker.html new file mode 100644 index 0000000000..f8da894074 --- /dev/null +++ b/src/extensions/default/Git/templates/git-remotes-picker.html @@ -0,0 +1,10 @@ + +{{#remotes}} +
  • + + {{#deletable}}×{{/deletable}} + {{name}} + +
  • +{{/remotes}} +
  • {{Strings.CREATE_NEW_REMOTE}}
  • diff --git a/src/extensions/default/Git/templates/git-settings-dialog.html b/src/extensions/default/Git/templates/git-settings-dialog.html new file mode 100644 index 0000000000..d3eed835df --- /dev/null +++ b/src/extensions/default/Git/templates/git-settings-dialog.html @@ -0,0 +1,109 @@ + diff --git a/src/extensions/default/Git/templates/git-tag-dialog.html b/src/extensions/default/Git/templates/git-tag-dialog.html new file mode 100644 index 0000000000..2619776934 --- /dev/null +++ b/src/extensions/default/Git/templates/git-tag-dialog.html @@ -0,0 +1,15 @@ + diff --git a/src/extensions/default/Git/templates/history-viewer-files.html b/src/extensions/default/Git/templates/history-viewer-files.html new file mode 100644 index 0000000000..152ad72d74 --- /dev/null +++ b/src/extensions/default/Git/templates/history-viewer-files.html @@ -0,0 +1,12 @@ +{{#files}} +
  • + + + + {{name}}{{extension}} + + {{#useDifftool}}{{/useDifftool}} + +
    +
  • +{{/files}} diff --git a/src/extensions/default/Git/templates/history-viewer.html b/src/extensions/default/Git/templates/history-viewer.html new file mode 100644 index 0000000000..22d9c759c4 --- /dev/null +++ b/src/extensions/default/Git/templates/history-viewer.html @@ -0,0 +1,46 @@ +
    +
    +
    + × +
    +
    + +
    + {{commit.avatarLetter}} +
    + {{commit.author}} + <{{commit.email}}> +
    + +   + {{commit.date.shown}} + + +  {{commit.hashShort}} + + +
    + + + + {{Strings.EXPAND_ALL}} + {{Strings.COLLAPSE_ALL}} + +
    +
    +
    +

    {{commit.subject}}

    +
    +
    +
    +
    {{{bodyMarkdown}}}
    +
    +
    + + +
    +
    +
    +
    diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index e947391465..813176191a 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1261,5 +1261,272 @@ define({ // indent guides extension "DESCRIPTION_INDENT_GUIDES_ENABLED": "true to show indent guide lines, else false.", "DESCRIPTION_HIDE_FIRST": "true to show the first Indent Guide line else false.", - "DESCRIPTION_CSS_COLOR_PREVIEW": "true to display color previews in the gutter, else false." + "DESCRIPTION_CSS_COLOR_PREVIEW": "true to display color previews in the gutter, else false.", + + // Git extension + "ENABLE_GIT": "Enable Git", + "ACTION": "Action", + "STATUSBAR_SHOW_GIT": "Git Panel", + "ADD_ENDLINE_TO_THE_END_OF_FILE": "Add endline at the end of file", + "ADD_TO_GITIGNORE": "Add to .gitignore", + "AMEND_COMMIT": "Amend last commit", + "SKIP_COMMIT_CHECKS": "Skip pre-commit checks (--no-verify)", + "AMEND_COMMIT_FORBIDDEN": "Cannot amend commit when there are no unpushed commits", + "_ANOTHER_BRANCH": "another branch", + "AUTHOR": "Author", + "AUTHORS_OF": "Authors of", + "SYSTEM_CONFIGURATION": "System configuration", + "BRACKETS_GIT_ERROR": "Git encountered an error", + "BRANCH_NAME": "Branch name", + "BUTTON_CANCEL": "Cancel", + "CHECKOUT_COMMIT": "Checkout", + "CHECKOUT_COMMIT_DETAIL": "Commit Message: {0}
    Commit hash: {1}", + "GIT_CLONE": "Clone", + "BUTTON_CLOSE": "Close", + "BUTTON_COMMIT": "Commit", + "BUTTON_DEFAULTS": "Restore defaults", + "BUTTON_FIND_CONFLICTS": "Find conflicts", + "GIT_INIT": "Init", + "BUTTON_MERGE_ABORT": "Abort merge", + "BUTTON_OK": "OK", + "BUTTON_REBASE_ABORT": "Abort", + "BUTTON_REBASE_CONTINUE": "Continue rebase", + "BUTTON_REBASE_SKIP": "Skip", + "MENU_RESET_HARD": "Discard changes and commits after this (hard reset)", + "MENU_RESET_MIXED": "Discard commits after this but keep changes unstaged (mixed reset)", + "MENU_RESET_SOFT": "Discard commits after this but keep changes staged (soft reset)", + "MENU_TAG_COMMIT": "Tag Commit", + "RESET_HARD_TITLE": "Confirm Hard Reset", + "RESET_MIXED_TITLE": "Confirm Mixed Reset", + "RESET_SOFT_TITLE": "Confirm Soft Reset", + "RESET_DETAIL": "Commits after the following will be discarded:
    Commit Message: {0}
    Git Command: {1}", + "RESET_HARD_MESSAGE": "This action will discard all changes and commits after the selected commit. This operation cannot be undone easily. Are you sure you want to proceed?

    ÔÜá´©Å Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", + "RESET_MIXED_MESSAGE": "This action will discard all commits after the selected commit but keep all changes unstaged. This operation cannot be undone easily. Are you sure you want to proceed?

    ÔÜá´©Å Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", + "RESET_SOFT_MESSAGE": "This action will discard all commits after the selected commit but keep all changes staged for a new commit. This operation cannot be undone easily. Are you sure you want to proceed?

    ÔÜá´©Å Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", + "BUTTON_SAVE": "Save", + "RESET": "Reset", + "CHANGE_USER_EMAIL_TITLE": "Change git email", + "CHANGE_USER_EMAIL": "Change git emailÔǪ", + "CHANGE_USER_EMAIL_MENU": "Change git email ({0})ÔǪ", + "CHANGE_USER_NAME_TITLE": "Change git username", + "CHANGE_USER_NAME": "Change git usernameÔǪ", + "CHANGE_USER_NAME_MENU": "Change git username ({0})ÔǪ", + "REQUIRES_APP_RESTART_SETTING": "Changing this setting requires an app restart to take effect", + "CLEAN_FILE_END": "File cleaned", + "CLEAN_FILE_START": "Cleaning file", + "CLEANING_WHITESPACE_PROGRESS": "Cleaning whitespace from filesÔǪ", + "CLEAR_WHITESPACE_ON_FILE_SAVE": "Clear whitespace on file save", + "CLONE_REPOSITORY": "Clone repository", + "CODE_INSPECTION_PROBLEMS": "Code inspection problems", + "CODE_INSPECTION_IN_PROGRESS": "Code inspection in progressÔǪ", + "CODE_INSPECTION_PROBLEMS_NONE": "No problems detected", + "CODE_INSPECTION_DONE_FILES": "{0} of {1} files doneÔǪ", + "PLEASE_WAIT": "Please waitÔǪ", + "COMMIT": "Commit", + "COMMIT_ALL_SHORTCUT": "Commit all filesÔǪ", + "COMMIT_CURRENT_SHORTCUT": "Commit current fileÔǪ", + "COMMIT_MESSAGE_PLACEHOLDER": "Enter commit message hereÔǪ", + "CREATE_NEW_BRANCH": "Create new branchÔǪ", + "CREATE_NEW_BRANCH_TITLE": "Create new branch", + "CREATE_NEW_GITFTP_SCOPE": "Create new Git-FTP remoteÔǪ", + "CREATE_NEW_REMOTE": "Create new remoteÔǪ", + "CURRENT_TRACKING_BRANCH": "Current tracking branch", + "_CURRENT_TRACKING_BRANCH": "current tracking branch", + "DEFAULT_GIT_TIMEOUT": "Default Git operation timeout (in seconds)", + "DELETE_FILE_BTN": "Delete fileÔǪ", + "DELETE_LOCAL_BRANCH": "Delete local branch", + "DELETE_LOCAL_BRANCH_NAME": "Do you really wish to delete local branch \"{0}\"?", + "DELETE_REMOTE": "Delete remote", + "DELETE_REMOTE_NAME": "Do you really wish to delete remote \"{0}\"?", + "DELETE_SCOPE": "Delete Git-FTP scope", + "DELETE_SCOPE_NAME": "Do you really wish to delete Git-FTP scope \"{0}\"?", + "DIALOG_CHECKOUT": "When checking out a commit, the repo will go into a DETACHED HEAD state. You can't make any commits unless you create a branch based on this.", + "DIALOG_PULL_TITLE": "Pull from remote", + "DIALOG_PUSH_TITLE": "Push to remote", + "SKIP_PRE_PUSH_CHECKS": "Skip pre-push checks (--no-verify)", + "DIFF": "Diff", + "DIFFTOOL": "Diff with difftool", + "DIFF_FAILED_SEE_FILES": "Git diff failed to provide diff results. This is the list of staged files to be commited:", + "DIFF_TOO_LONG": "Diff too long to display", + "ENABLE_GERRIT_PUSH_REF": "Use Gerrit-compatible push ref", + "ENTER_NEW_USER_EMAIL": "Enter email", + "ENTER_NEW_USER_NAME": "Enter username", + "ENTER_PASSWORD": "Enter password:", + "ENTER_REMOTE_GIT_URL": "Enter Git URL of the repository you want to clone:", + "ENTER_REMOTE_NAME": "Enter name of the new remote:", + "ENTER_REMOTE_URL": "Enter URL of the new remote:", + "ENTER_USERNAME": "Enter username:", + "ERROR_NOTHING_SELECTED": "Nothing is selected!", + "ERROR_SAVE_FIRST": "Save the document first!", + "ERROR_TERMINAL_NOT_FOUND": "Terminal was not found for your OS, you can define a custom Terminal command in the settings", + "EXTENDED_COMMIT_MESSAGE": "EXTENDED", + "GETTING_STAGED_DIFF_PROGRESS": "Getting diff of staged filesÔǪ", + "GIT_COMMIT": "Git commitÔǪ", + "GIT_COMMIT_IN_PROGRESS": "Git Commit in Progress", + "GIT_DIFF": "Git diff —", + "GIT_PULL_RESPONSE": "Git Pull response", + "GIT_PUSH_RESPONSE": "Git Push response", + "GIT_REMOTES": "Git remotes", + "GIT_SETTINGS": "Git SettingsÔǪ", + "GIT_SETTINGS_TITLE": "Git Settings", + "GOTO_NEXT_GIT_CHANGE": "Go to next Git change", + "GOTO_PREVIOUS_GIT_CHANGE": "Go to previous Git change", + "GUTTER_CLICK_DETAILS": "Click for more details", + "HIDE_UNTRACKED": "Hide untracked files in panel", + "HISTORY": "History", + "HISTORY_COMMIT_BY": "by", + "LINES": "Lines", + "_LINES": "lines", + "MARK_MODIFIED_FILES_IN_TREE": "Mark modified files in file tree", + "MERGE_BRANCH": "Merge branch", + "MERGE_MESSAGE": "Merge message", + "MERGE_RESULT": "Merge result", + "NORMALIZE_LINE_ENDINGS": "Normalize line endings (to \\n)", + "NOTHING_TO_COMMIT": "Nothing to commit, working directory clean.", + "OPERATION_IN_PROGRESS_TITLE": "Git operation in progressÔǪ", + "ORIGIN_BRANCH": "Origin branch", + "ON_BRANCH": "'{0}' - Current Git branch", + "PANEL_COMMAND": "Show Git panel", + "PASSWORD": "Password", + "PASSWORDS": "Passwords", + "PATH_TO_GIT_EXECUTABLE": "Path to Git executable", + "PULL_AVOID_MERGING": "Avoid manual merging", + "PULL_DEFAULT": "Default merge", + "PULL_FROM": "Pull from", + "PULL_MERGE_NOCOMMIT": "Merge without commit", + "PULL_REBASE": "Use rebase", + "PULL_RESET": "Use soft reset", + "PULL_SHORTCUT": "Pull from remoteÔǪ", + "PULL_SHORTCUT_BEHIND": "Pull from remote ({0} behind)ÔǪ", + "PULL_BEHAVIOR": "Pull Behavior", + "FETCH_SHORTCUT": "Fetch from remote", + "PUSH_DEFAULT": "Default push", + "PUSH_DELETE_BRANCH": "Delete remote branch", + "PUSH_SEND_TAGS": "Send tags", + "PUSH_FORCED": "Forced push", + "PUSH_SHORTCUT": "Push to remoteÔǪ", + "PUSH_SHORTCUT_AHEAD": "Push to remote ({0} ahead)ÔǪ", + "PUSH_TO": "Push to", + "PUSH_BEHAVIOR": "Push Behavior", + "Q_UNDO_CHANGES": "Reset changes to file {0}?", + "REBASE_RESULT": "Rebase result", + "REFRESH_GIT": "Refresh Git", + "REMOVE_BOM": "Remove BOM from files", + "REMOVE_FROM_GITIGNORE": "Remove from .gitignore", + "RESET_LOCAL_REPO": "Discard all changes since last commitÔǪ", + "DISCARD_CHANGES": "Discard Changes", + "RESET_LOCAL_REPO_CONFIRM": "Do you wish to discard all changes done since last commit? This action can't be reverted.", + "UNDO_LOCAL_COMMIT_CONFIRM": "Are you sure you want to undo the last non-pushed commit?", + "MORE_OPTIONS": "More Options", + "CREDENTIALS": "Credentials", + "SAVE_CREDENTIALS_HELP": "You don't need to fill out username/password if your credentials are managed elsewhere. Use this only when your operation keeps timing out.", + "SAVE_CREDENTIALS_IN_URL": "Save credentials to remote url (in plain text)", + "SET_THIS_BRANCH_AS_TRACKING": "Set this branch as a new tracking branch", + "STRIP_WHITESPACE_FROM_COMMITS": "Strip trailing whitespace from commits", + "TARGET_BRANCH": "Target branch", + "TITLE_CHECKOUT": "Checkout Commit?", + "TOOLTIP_CLONE": "Clone existing repository", + "TOOLTIP_COMMIT": "Commit the selected files", + "TOOLTIP_FETCH": "Fetch all remotes and refresh counters", + "TOOLTIP_FIND_CONFLICTS": "Starts a search for Git merge/rebase conflicts in the project", + "TOOLTIP_HIDE_FILE_HISTORY": "Hide file history", + "TOOLTIP_HIDE_HISTORY": "Hide history", + "TOOLTIP_INIT": "Initialize repository", + "TOOLTIP_MERGE_ABORT": "Abort the merge operation and reset HEAD to the last local commit", + "TOOLTIP_PICK_REMOTE": "Pick preferred remote", + "TOOLTIP_PULL": "Git Pull", + "TOOLTIP_PUSH": "Git Push", + "TOOLTIP_REBASE_ABORT": "Abort the rebase operation and reset HEAD to the original branch", + "TOOLTIP_REBASE_CONTINUE": "Restart the rebasing process after having resolved a merge conflict", + "TOOLTIP_REBASE_SKIP": "Restart the rebasing process by skipping the current patch", + "TOOLTIP_REFRESH_PANEL": "Refresh panel", + "TOOLTIP_SHOW_FILE_HISTORY": "Show file history", + "TOOLTIP_SHOW_HISTORY": "Show history", + "UNDO_CHANGES": "Discard changes", + "UNDO_CHANGES_BTN": "Discard changesÔǪ", + "UNDO_LAST_LOCAL_COMMIT": "Undo last local (not pushed) commitÔǪ", + "UNDO_COMMIT": "Undo Commit", + "URL": "URL", + "USERNAME": "Username", + "USER_ABORTED": "User aborted!", + "USE_GIT_GUTTER": "Use Git gutter marks", + "USE_NOFF": "Create a merge commit even when the merge resolves as a fast-forward (--no-ff)", + "USE_REBASE": "Use REBASE", + "USE_VERBOSE_DIFF": "Show verbose output in diffs", + "USE_DIFFTOOL": "Use difftool for diffs", + "VIEW_AUTHORS_FILE": "View authors of fileÔǪ", + "VIEW_AUTHORS_SELECTION": "View authors of selectionÔǪ", + "VIEW_THIS_FILE": "View this file", + "TAG_NAME_PLACEHOLDER": "Enter tag name hereÔǪ", + "TAG_NAME": "Tag", + "CMD_CLOSE_UNMODIFIED": "Close Unmodified Files", + "FILE_ADDED": "New file", + "FILE_COPIED": "Copied", + "FILE_DELETED": "Deleted", + "FILE_IGNORED": "Ignored", + "FILE_MODIFIED": "Modified", + "FILE_RENAMED": "Renamed", + "FILE_STAGED": "Staged", + "FILE_UNMERGED": "Unmerged", + "FILE_UNMODIFIED": "Unmodified", + "FILE_UNTRACKED": "Untracked", + "GIT_PUSH_SUCCESS_MSG": "Successfully pushed fast-forward", + "GIT_PUSH_FORCE_UPDATED_MSG": "Successful forced update", + "GIT_PUSH_DELETED_MSG": "Successfully deleted ref", + "GIT_PUSH_NEW_REF_MSG": "Successfully pushed new ref", + "GIT_PUSH_REJECTED_MSG": "Ref was rejected or failed to push", + "GIT_PUSH_UP_TO_DATE_MSG": "Ref was up to date and did not need pushing", + "GIT_PULL_SUCCESS": "Successfully completed git pull", + "GIT_MERGE_SUCCESS": "Successfully completed git merge", + "GIT_REBASE_SUCCESS": "Successfully completed git rebase", + "GIT_BRANCH_DELETE_SUCCESS": "Successfully deleted git branch", + "INIT_NEW_REPO_FAILED": "Failed to initialize new repository", + "GIT_CLONE_REMOTE_FAILED": "Cloning remote repository failed!", + "GIT_CLONE_ERROR_EXPLAIN": "The selected directory `{0}`\n is not empty. Git clone requires a clean, empty directory.\nIf it appears empty, check for hidden files.", + "FOLDER_NOT_WRITABLE": "The selected directory `{0}`\n is not writable.", + "GIT_NOT_FOUND_MESSAGE": "Git is not installed or cannot be found on your system. Please install Git or provide the correct path to the Git executable in the text field below.", + "ERROR_GETTING_BRANCH_LIST": "Getting branch list failed", + "ERROR_READING_GIT_HEAD": "Reading .git/HEAD file failed", + "ERROR_PARSING_BRANCH_NAME": "Failed parsing branch name from {0}", + "ERROR_READING_GIT_STATE": "Reading .git state failed", + "ERROR_GETTING_DELETED_FILES": "Getting list of deleted files failed", + "ERROR_SWITCHING_BRANCHES": "Switching branches failed", + "ERROR_GETTING_CURRENT_BRANCH": "Getting current branch name failed", + "ERROR_REBASE_FAILED": "Rebase failed", + "ERROR_MERGE_FAILED": "Merge failed", + "ERROR_BRANCH_DELETE_FORCED": "Forced branch deletion failed", + "ERROR_FETCH_REMOTE_INFO": "Fetching remote information failed", + "ERROR_CREATE_BRANCH": "Creating new branch failed", + "ERROR_REFRESH_GUTTER": "Refreshing gutter failed!", + "ERROR_GET_HISTORY": "Failed to get history", + "ERROR_GET_MORE_HISTORY": "Failed to load more history rows", + "ERROR_GET_CURRENT_BRANCH": "Failed to get current branch name", + "ERROR_GET_DIFF_FILE_COMMIT": "Failed to get diff", + "ERROR_GET_DIFF_FILES": "Failed to load list of diff files", + "ERROR_MODIFY_GITIGNORE": "Failed modifying .gitignore", + "ERROR_UNDO_LAST_COMMIT_FAILED": "Impossible to undo last commit", + "ERROR_MODIFY_FILE_STATUS_FAILED": "Failed to modify file status", + "ERROR_CHANGE_USERNAME_FAILED": "Impossible to change user name", + "ERROR_CHANGE_EMAIL_FAILED": "Impossible to change user email", + "ERROR_TOGGLE_GERRIT_PUSH_REF_FAILED": "Impossible to toggle gerrit push ref", + "ERROR_RESET_LOCAL_REPO_FAILED": "Reset of local repository failed", + "ERROR_CREATE_TAG": "Create tag failed", + "ERROR_CODE_INSPECTION_FAILED": "CodeInspection.inspectFile failed to execute for file", + "ERROR_CANT_GET_STAGED_DIFF": "Cant get diff for staged files", + "ERROR_GIT_COMMIT_FAILED": "Git Commit failed", + "ERROR_GIT_BLAME_FAILED": "Git Blame failed", + "ERROR_GIT_DIFF_FAILED": "Git Diff failed", + "ERROR_DISCARD_CHANGES_FAILED": "Discard changes to a file failed", + "ERROR_COULD_NOT_RESOLVE_FILE": "Could not resolve file", + "ERROR_MERGE_ABORT_FAILED": "Merge abort failed", + "ERROR_MODIFIED_DIALOG_FILES": "The files you were going to commit were modified while commit dialog was displayed. Aborting the commit as the result would be different then what was shown in the dialog.", + "ERROR_GETTING_REMOTES": "Getting remotes failed!", + "ERROR_REMOTE_CREATION": "Remote creation failed", + "ERROR_PUSHING_REMOTE": "Pushing to remote failed", + "ERROR_PULLING_REMOTE": "Pulling from remote failed", + "ERROR_PULLING_OPERATION": "Pulling operation failed", + "ERROR_PUSHING_OPERATION": "Pushing operation failed", + "ERROR_NO_REMOTE_SELECTED": "No remote has been selected for {0}!", + "ERROR_BRANCH_LIST": "Getting branch list failed", + "ERROR_FETCH_REMOTE": "Fetching remote information failed" }); From d97d792df518d1092286e101a04dabbd7716e58c Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 14 Jan 2025 21:04:59 +0530 Subject: [PATCH 2/2] fix: eslint issues in strings and unicode u2026 not copied properly err --- src/extensions/default/DefaultExtensions.json | 2 +- src/nls/root/strings.js | 68 +++++++++---------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/extensions/default/DefaultExtensions.json b/src/extensions/default/DefaultExtensions.json index f88f657411..cddad77610 100644 --- a/src/extensions/default/DefaultExtensions.json +++ b/src/extensions/default/DefaultExtensions.json @@ -33,4 +33,4 @@ "bib-locked-live-preview", "brackets-display-shortcuts", "changing-tags", "brackets-indent-guides" ] } -} \ No newline at end of file +} diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 813176191a..c7bbda3a20 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1276,7 +1276,7 @@ define({ "AUTHOR": "Author", "AUTHORS_OF": "Authors of", "SYSTEM_CONFIGURATION": "System configuration", - "BRACKETS_GIT_ERROR": "Git encountered an errorÔǪ", + "BRACKETS_GIT_ERROR": "Git encountered an error\u2026", "BRANCH_NAME": "Branch name", "BUTTON_CANCEL": "Cancel", "CHECKOUT_COMMIT": "Checkout", @@ -1285,7 +1285,7 @@ define({ "BUTTON_CLOSE": "Close", "BUTTON_COMMIT": "Commit", "BUTTON_DEFAULTS": "Restore defaults", - "BUTTON_FIND_CONFLICTS": "Find conflictsÔǪ", + "BUTTON_FIND_CONFLICTS": "Find conflicts\u2026", "GIT_INIT": "Init", "BUTTON_MERGE_ABORT": "Abort merge", "BUTTON_OK": "OK", @@ -1300,40 +1300,40 @@ define({ "RESET_MIXED_TITLE": "Confirm Mixed Reset", "RESET_SOFT_TITLE": "Confirm Soft Reset", "RESET_DETAIL": "Commits after the following will be discarded:
    Commit Message: {0}
    Git Command: {1}", - "RESET_HARD_MESSAGE": "This action will discard all changes and commits after the selected commit. This operation cannot be undone easily. Are you sure you want to proceed?

    ÔÜá´©Å Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", - "RESET_MIXED_MESSAGE": "This action will discard all commits after the selected commit but keep all changes unstaged. This operation cannot be undone easily. Are you sure you want to proceed?

    ÔÜá´©Å Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", - "RESET_SOFT_MESSAGE": "This action will discard all commits after the selected commit but keep all changes staged for a new commit. This operation cannot be undone easily. Are you sure you want to proceed?

    ÔÜá´©Å Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", + "RESET_HARD_MESSAGE": "This action will discard all changes and commits after the selected commit. This operation cannot be undone easily. Are you sure you want to proceed?

    ⚠️ Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", + "RESET_MIXED_MESSAGE": "This action will discard all commits after the selected commit but keep all changes unstaged. This operation cannot be undone easily. Are you sure you want to proceed?

    ⚠️ Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", + "RESET_SOFT_MESSAGE": "This action will discard all commits after the selected commit but keep all changes staged for a new commit. This operation cannot be undone easily. Are you sure you want to proceed?

    ⚠️ Warning: This rewrites history and should not be used on commits that have been pushed to a shared branch.", "BUTTON_SAVE": "Save", "RESET": "Reset", "CHANGE_USER_EMAIL_TITLE": "Change git email", - "CHANGE_USER_EMAIL": "Change git emailÔǪ", - "CHANGE_USER_EMAIL_MENU": "Change git email ({0})ÔǪ", + "CHANGE_USER_EMAIL": "Change git email\u2026", + "CHANGE_USER_EMAIL_MENU": "Change git email ({0})\u2026", "CHANGE_USER_NAME_TITLE": "Change git username", - "CHANGE_USER_NAME": "Change git usernameÔǪ", - "CHANGE_USER_NAME_MENU": "Change git username ({0})ÔǪ", + "CHANGE_USER_NAME": "Change git username\u2026", + "CHANGE_USER_NAME_MENU": "Change git username ({0})\u2026", "REQUIRES_APP_RESTART_SETTING": "Changing this setting requires an app restart to take effect", "CLEAN_FILE_END": "File cleaned", "CLEAN_FILE_START": "Cleaning file", - "CLEANING_WHITESPACE_PROGRESS": "Cleaning whitespace from filesÔǪ", + "CLEANING_WHITESPACE_PROGRESS": "Cleaning whitespace from files\u2026", "CLEAR_WHITESPACE_ON_FILE_SAVE": "Clear whitespace on file save", "CLONE_REPOSITORY": "Clone repository", "CODE_INSPECTION_PROBLEMS": "Code inspection problems", - "CODE_INSPECTION_IN_PROGRESS": "Code inspection in progressÔǪ", + "CODE_INSPECTION_IN_PROGRESS": "Code inspection in progress\u2026", "CODE_INSPECTION_PROBLEMS_NONE": "No problems detected", - "CODE_INSPECTION_DONE_FILES": "{0} of {1} files doneÔǪ", - "PLEASE_WAIT": "Please waitÔǪ", + "CODE_INSPECTION_DONE_FILES": "{0} of {1} files done\u2026", + "PLEASE_WAIT": "Please wait\u2026", "COMMIT": "Commit", - "COMMIT_ALL_SHORTCUT": "Commit all filesÔǪ", - "COMMIT_CURRENT_SHORTCUT": "Commit current fileÔǪ", - "COMMIT_MESSAGE_PLACEHOLDER": "Enter commit message hereÔǪ", - "CREATE_NEW_BRANCH": "Create new branchÔǪ", + "COMMIT_ALL_SHORTCUT": "Commit all files\u2026", + "COMMIT_CURRENT_SHORTCUT": "Commit current file\u2026", + "COMMIT_MESSAGE_PLACEHOLDER": "Enter commit message here\u2026", + "CREATE_NEW_BRANCH": "Create new branch\u2026", "CREATE_NEW_BRANCH_TITLE": "Create new branch", - "CREATE_NEW_GITFTP_SCOPE": "Create new Git-FTP remoteÔǪ", - "CREATE_NEW_REMOTE": "Create new remoteÔǪ", + "CREATE_NEW_GITFTP_SCOPE": "Create new Git-FTP remote\u2026", + "CREATE_NEW_REMOTE": "Create new remote\u2026", "CURRENT_TRACKING_BRANCH": "Current tracking branch", "_CURRENT_TRACKING_BRANCH": "current tracking branch", "DEFAULT_GIT_TIMEOUT": "Default Git operation timeout (in seconds)", - "DELETE_FILE_BTN": "Delete fileÔǪ", + "DELETE_FILE_BTN": "Delete file\u2026", "DELETE_LOCAL_BRANCH": "Delete local branch", "DELETE_LOCAL_BRANCH_NAME": "Do you really wish to delete local branch \"{0}\"?", "DELETE_REMOTE": "Delete remote", @@ -1360,14 +1360,14 @@ define({ "ERROR_SAVE_FIRST": "Save the document first!", "ERROR_TERMINAL_NOT_FOUND": "Terminal was not found for your OS, you can define a custom Terminal command in the settings", "EXTENDED_COMMIT_MESSAGE": "EXTENDED", - "GETTING_STAGED_DIFF_PROGRESS": "Getting diff of staged filesÔǪ", - "GIT_COMMIT": "Git commitÔǪ", + "GETTING_STAGED_DIFF_PROGRESS": "Getting diff of staged files\u2026", + "GIT_COMMIT": "Git commit\u2026", "GIT_COMMIT_IN_PROGRESS": "Git Commit in Progress", "GIT_DIFF": "Git diff —", "GIT_PULL_RESPONSE": "Git Pull response", "GIT_PUSH_RESPONSE": "Git Push response", "GIT_REMOTES": "Git remotes", - "GIT_SETTINGS": "Git SettingsÔǪ", + "GIT_SETTINGS": "Git Settings\u2026", "GIT_SETTINGS_TITLE": "Git Settings", "GOTO_NEXT_GIT_CHANGE": "Go to next Git change", "GOTO_PREVIOUS_GIT_CHANGE": "Go to previous Git change", @@ -1383,7 +1383,7 @@ define({ "MERGE_RESULT": "Merge result", "NORMALIZE_LINE_ENDINGS": "Normalize line endings (to \\n)", "NOTHING_TO_COMMIT": "Nothing to commit, working directory clean.", - "OPERATION_IN_PROGRESS_TITLE": "Git operation in progressÔǪ", + "OPERATION_IN_PROGRESS_TITLE": "Git operation in progress\u2026", "ORIGIN_BRANCH": "Origin branch", "ON_BRANCH": "'{0}' - Current Git branch", "PANEL_COMMAND": "Show Git panel", @@ -1396,16 +1396,16 @@ define({ "PULL_MERGE_NOCOMMIT": "Merge without commit", "PULL_REBASE": "Use rebase", "PULL_RESET": "Use soft reset", - "PULL_SHORTCUT": "Pull from remoteÔǪ", - "PULL_SHORTCUT_BEHIND": "Pull from remote ({0} behind)ÔǪ", + "PULL_SHORTCUT": "Pull from remote\u2026", + "PULL_SHORTCUT_BEHIND": "Pull from remote ({0} behind)\u2026", "PULL_BEHAVIOR": "Pull Behavior", "FETCH_SHORTCUT": "Fetch from remote", "PUSH_DEFAULT": "Default push", "PUSH_DELETE_BRANCH": "Delete remote branch", "PUSH_SEND_TAGS": "Send tags", "PUSH_FORCED": "Forced push", - "PUSH_SHORTCUT": "Push to remoteÔǪ", - "PUSH_SHORTCUT_AHEAD": "Push to remote ({0} ahead)ÔǪ", + "PUSH_SHORTCUT": "Push to remote\u2026", + "PUSH_SHORTCUT_AHEAD": "Push to remote ({0} ahead)\u2026", "PUSH_TO": "Push to", "PUSH_BEHAVIOR": "Push Behavior", "Q_UNDO_CHANGES": "Reset changes to file {0}?", @@ -1413,7 +1413,7 @@ define({ "REFRESH_GIT": "Refresh Git", "REMOVE_BOM": "Remove BOM from files", "REMOVE_FROM_GITIGNORE": "Remove from .gitignore", - "RESET_LOCAL_REPO": "Discard all changes since last commitÔǪ", + "RESET_LOCAL_REPO": "Discard all changes since last commit\u2026", "DISCARD_CHANGES": "Discard Changes", "RESET_LOCAL_REPO_CONFIRM": "Do you wish to discard all changes done since last commit? This action can't be reverted.", "UNDO_LOCAL_COMMIT_CONFIRM": "Are you sure you want to undo the last non-pushed commit?", @@ -1443,8 +1443,8 @@ define({ "TOOLTIP_SHOW_FILE_HISTORY": "Show file history", "TOOLTIP_SHOW_HISTORY": "Show history", "UNDO_CHANGES": "Discard changes", - "UNDO_CHANGES_BTN": "Discard changesÔǪ", - "UNDO_LAST_LOCAL_COMMIT": "Undo last local (not pushed) commitÔǪ", + "UNDO_CHANGES_BTN": "Discard changes\u2026", + "UNDO_LAST_LOCAL_COMMIT": "Undo last local (not pushed) commit\u2026", "UNDO_COMMIT": "Undo Commit", "URL": "URL", "USERNAME": "Username", @@ -1454,10 +1454,10 @@ define({ "USE_REBASE": "Use REBASE", "USE_VERBOSE_DIFF": "Show verbose output in diffs", "USE_DIFFTOOL": "Use difftool for diffs", - "VIEW_AUTHORS_FILE": "View authors of fileÔǪ", - "VIEW_AUTHORS_SELECTION": "View authors of selectionÔǪ", + "VIEW_AUTHORS_FILE": "View authors of file\u2026", + "VIEW_AUTHORS_SELECTION": "View authors of selection\u2026", "VIEW_THIS_FILE": "View this file", - "TAG_NAME_PLACEHOLDER": "Enter tag name hereÔǪ", + "TAG_NAME_PLACEHOLDER": "Enter tag name here\u2026", "TAG_NAME": "Tag", "CMD_CLOSE_UNMODIFIED": "Close Unmodified Files", "FILE_ADDED": "New file",