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/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 0000000000..685e629c2f Binary files /dev/null and b/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.eot differ diff --git a/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.svg b/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.svg new file mode 100644 index 0000000000..20beb8ca2a --- /dev/null +++ b/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.svg @@ -0,0 +1,215 @@ + + + +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 0000000000..2c910f99b4 Binary files /dev/null and b/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.ttf differ diff --git a/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.woff b/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.woff new file mode 100644 index 0000000000..347f051867 Binary files /dev/null and b/src/extensions/default/Git/styles/fonts/octicons-regular-webfont.woff differ diff --git a/src/extensions/default/Git/styles/history.less b/src/extensions/default/Git/styles/history.less new file mode 100644 index 0000000000..c69b07e2ac --- /dev/null +++ b/src/extensions/default/Git/styles/history.less @@ -0,0 +1,299 @@ +// main: brackets-git.less + +.commit-author-avatar-mixin(@size) { + position: relative; + width: @size; + height: @size; + text-align: center; + span, img { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + height: @size; + width: @size; + border-radius: 2px; + } + span { + color: @bc-text; + font-weight: 500; + font-size: @size; + line-height: @size; + text-transform: uppercase; + + .dark & { + color: @dark-bc-text; + } + } +} + +#git-history-list { + table-layout: fixed; + tbody tr td { + vertical-align: middle; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + &:nth-child(1) { // author avatar + width: 30px; + .commit-author-avatar { + .commit-author-avatar-mixin(18px); + } + } + &:nth-child(2) { // commit date/author + width: 250px; + .commit-author { + font-weight: 500; + } + } + &:nth-child(3) { // commit title + .commit-tags { + float: right; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + } + } + &:last-child { // commit hash + width: 50px; + } + } +} + +#history-viewer { + position: absolute; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; + overflow: hidden; + background: @bc-panel-bg; + .flex-box(column); + + .dark & { + background: @dark-bc-panel-bg; + } + + > .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..c7bbda3a20 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\u2026", + "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\u2026", + "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\u2026", + "CHANGE_USER_EMAIL_MENU": "Change git email ({0})\u2026", + "CHANGE_USER_NAME_TITLE": "Change git username", + "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\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\u2026", + "CODE_INSPECTION_PROBLEMS_NONE": "No problems detected", + "CODE_INSPECTION_DONE_FILES": "{0} of {1} files done\u2026", + "PLEASE_WAIT": "Please wait\u2026", + "COMMIT": "Commit", + "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\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\u2026", + "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\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\u2026", + "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\u2026", + "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\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\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}?", + "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\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?", + "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\u2026", + "UNDO_LAST_LOCAL_COMMIT": "Undo last local (not pushed) commit\u2026", + "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\u2026", + "VIEW_AUTHORS_SELECTION": "View authors of selection\u2026", + "VIEW_THIS_FILE": "View this file", + "TAG_NAME_PLACEHOLDER": "Enter tag name here\u2026", + "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" });