")
+ .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: $("
")
+ .addClass("brackets-git-gutter-copy-button")
+ .text("R")
+ .on("click", function () {
+ var doc = DocumentManager.getCurrentDocument();
+ doc.replaceRange(mark.content + "\n", {
+ line: mark.line,
+ ch: 0
+ });
+ CommandManager.execute("file.save");
+ refresh();
+ });
+ $("
")
+ .attr("style", "tab-size:" + cm.getOption("tabSize"))
+ .text(mark.content || " ")
+ .append($btn)
+ .appendTo(mark.lineWidget.element);
+ }
+
+ if (mark.lineWidget.visible !== true) {
+ mark.lineWidget.visible = true;
+ mark.lineWidget.widget = cm.addLineWidget(mark.line, mark.lineWidget.element[0], {
+ coverGutter: false,
+ noHScroll: false,
+ above: true,
+ showIfHidden: false
+ });
+ openWidgets.push(mark);
+ } else {
+ mark.lineWidget.visible = false;
+ mark.lineWidget.widget.clear();
+ var io = openWidgets.indexOf(mark);
+ if (io !== -1) {
+ openWidgets.splice(io, 1);
+ }
+ }
+ }
+
+ function getEditorFromPane(paneId) {
+ var currentPath = MainViewManager.getCurrentlyViewedPath(paneId),
+ doc = currentPath && DocumentManager.getOpenDocumentForPath(currentPath);
+ return doc && doc._masterEditor;
+ }
+
+ function processDiffResults(editor, diff) {
+ var added = [],
+ removed = [],
+ modified = [],
+ changesets = diff.split(/\n@@/).map(function (str) { return "@@" + str; });
+
+ // remove part before first
+ changesets.shift();
+
+ changesets.forEach(function (str) {
+ var m = str.match(/^@@ -([,0-9]+) \+([,0-9]+) @@/);
+ var s1 = m[1].split(",");
+ var s2 = m[2].split(",");
+
+ // removed stuff
+ var lineRemovedFrom;
+ var lineFrom = parseInt(s2[0], 10);
+ var lineCount = parseInt(s1[1], 10);
+ if (isNaN(lineCount)) { lineCount = 1; }
+ if (lineCount > 0) {
+ lineRemovedFrom = lineFrom - 1;
+ removed.push({
+ type: "removed",
+ line: lineRemovedFrom,
+ content: str.split("\n")
+ .filter(function (l) { return l.indexOf("-") === 0; })
+ .map(function (l) { return l.substring(1); })
+ .join("\n")
+ });
+ }
+
+ // added stuff
+ lineFrom = parseInt(s2[0], 10);
+ lineCount = parseInt(s2[1], 10);
+ if (isNaN(lineCount)) { lineCount = 1; }
+ var isModifiedMark = false;
+ var firstAddedMark = false;
+ for (var i = lineFrom, lineTo = lineFrom + lineCount; i < lineTo; i++) {
+ var lineNo = i - 1;
+ if (lineNo === lineRemovedFrom) {
+ // modified
+ var o = removed.pop();
+ o.type = "modified";
+ modified.push(o);
+ isModifiedMark = o;
+ } else {
+ var mark = {
+ type: isModifiedMark ? "modified" : "added",
+ line: lineNo,
+ parentMark: isModifiedMark || firstAddedMark || null
+ };
+ if (!isModifiedMark && !firstAddedMark) {
+ firstAddedMark = mark;
+ }
+ // added new
+ added.push(mark);
+ }
+ }
+ });
+
+ // fix displaying of removed lines
+ removed.forEach(function (o) {
+ o.line = o.line + 1;
+ });
+
+ showGutters(editor, [].concat(added, removed, modified));
+ }
+
+ function refresh() {
+ if (!gitAvailable) {
+ return;
+ }
+
+ if (!Preferences.get("useGitGutter")) {
+ return;
+ }
+
+ var currentGitRoot = Preferences.get("currentGitRoot");
+
+ // we get a list of editors, which need to be refreshed
+ var editors = _.compact(_.map(MainViewManager.getPaneIdList(), function (paneId) {
+ return getEditorFromPane(paneId);
+ }));
+
+ // we create empty gutters in all of these editors, all other editors lose their gutters
+ prepareGutters(editors);
+
+ // now we launch a diff to fill the gutters in our editors
+ editors.forEach(function (editor) {
+
+ var currentFilePath = null;
+
+ if (editor.document && editor.document.file) {
+ currentFilePath = editor.document.file.fullPath;
+ }
+
+ if (currentFilePath.indexOf(currentGitRoot) !== 0) {
+ // file is not in the current project
+ return;
+ }
+
+ var filename = currentFilePath.substring(currentGitRoot.length);
+
+ Git.diffFile(filename).then(function (diff) {
+ processDiffResults(editor, diff);
+ }).catch(function (err) {
+ // if this is launched in a non-git repository, just ignore
+ if (ErrorHandler.contains(err, "Not a git repository")) {
+ return;
+ }
+ // if this file was moved or deleted before this command could be executed, ignore
+ if (ErrorHandler.contains(err, "No such file or directory")) {
+ return;
+ }
+ ErrorHandler.showError(err, Strings.ERROR_REFRESH_GUTTER);
+ });
+
+ });
+ }
+
+ function goToPrev() {
+ var activeEditor = EditorManager.getActiveEditor();
+ if (!activeEditor) { return; }
+
+ var results = activeEditor._codeMirror.gitGutters || [];
+ var searched = _.filter(results, function (i) { return !i.parentMark; });
+
+ var currentPos = activeEditor.getCursorPos();
+ var i = searched.length;
+ while (i--) {
+ if (searched[i].line < currentPos.line) {
+ break;
+ }
+ }
+ if (i > -1) {
+ var goToMark = searched[i];
+ activeEditor.setCursorPos(goToMark.line, currentPos.ch);
+ }
+ }
+
+ function goToNext() {
+ var activeEditor = EditorManager.getActiveEditor();
+ if (!activeEditor) { return; }
+
+ var results = activeEditor._codeMirror.gitGutters || [];
+ var searched = _.filter(results, function (i) { return !i.parentMark; });
+
+ var currentPos = activeEditor.getCursorPos();
+ for (var i = 0, l = searched.length; i < l; i++) {
+ if (searched[i].line > currentPos.line) {
+ break;
+ }
+ }
+ if (i < searched.length) {
+ var goToMark = searched[i];
+ activeEditor.setCursorPos(goToMark.line, currentPos.ch);
+ }
+ }
+
+ // Event handlers
+ EventEmitter.on(Events.GIT_ENABLED, function () {
+ gitAvailable = true;
+ refresh();
+ });
+ EventEmitter.on(Events.GIT_DISABLED, function () {
+ gitAvailable = false;
+ // calling this with an empty array will remove gutters from all editor instances
+ prepareGutters([]);
+ });
+ EventEmitter.on(Events.BRACKETS_CURRENT_DOCUMENT_CHANGE, function (file) {
+ // file will be null when switching to an empty pane
+ if (!file) { return; }
+
+ // document change gets launched even when switching panes,
+ // so we check if the file hasn't already got the gutters
+ var alreadyOpened = _.filter(editorsWithGutters, function (editor) {
+ return editor.document.file.fullPath === file.fullPath;
+ }).length > 0;
+
+ if (!alreadyOpened) {
+ // TODO: here we could sent a particular file to be refreshed only
+ refresh();
+ }
+ });
+ EventEmitter.on(Events.GIT_COMMITED, function () {
+ refresh();
+ });
+ EventEmitter.on(Events.BRACKETS_FILE_CHANGED, function (file) {
+ var alreadyOpened = _.filter(editorsWithGutters, function (editor) {
+ return editor.document.file.fullPath === file.fullPath;
+ }).length > 0;
+
+ if (alreadyOpened) {
+ // TODO: here we could sent a particular file to be refreshed only
+ refresh();
+ }
+ });
+
+ // API
+ exports.goToPrev = goToPrev;
+ exports.goToNext = goToNext;
+});
diff --git a/src/extensions/default/Git/src/History.js b/src/extensions/default/Git/src/History.js
new file mode 100644
index 0000000000..b9e1a46f3d
--- /dev/null
+++ b/src/extensions/default/Git/src/History.js
@@ -0,0 +1,329 @@
+define(function (require) {
+
+ // Brackets modules
+ var _ = brackets.getModule("thirdparty/lodash"),
+ DocumentManager = brackets.getModule("document/DocumentManager"),
+ FileUtils = brackets.getModule("file/FileUtils"),
+ LocalizationUtils = brackets.getModule("utils/LocalizationUtils"),
+ Strings = brackets.getModule("strings"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache");
+
+ // Local modules
+ const ErrorHandler = require("src/ErrorHandler"),
+ Events = require("src/Events"),
+ EventEmitter = require("src/EventEmitter"),
+ Git = require("src/git/Git"),
+ HistoryViewer = require("src/HistoryViewer"),
+ Preferences = require("src/Preferences");
+
+ // Templates
+ var gitPanelHistoryTemplate = require("text!templates/git-panel-history.html"),
+ gitPanelHistoryCommitsTemplate = require("text!templates/git-panel-history-commits.html");
+
+ // Module variables
+ let $gitPanel = $(null),
+ $tableContainer = $(null),
+ $historyList = $(null),
+ commitCache = [],
+ lastDocumentSeen = null;
+
+ // Implementation
+
+ function initVariables() {
+ $gitPanel = $("#git-panel");
+ $tableContainer = $gitPanel.find(".table-container");
+ attachHandlers();
+ }
+
+ function attachHandlers() {
+ $tableContainer
+ .off(".history")
+ .on("scroll.history", function () {
+ loadMoreHistory();
+ })
+ .on("click.history", ".history-commit", function () {
+ const $tr = $(this);
+ var hash = $tr.attr("x-hash");
+ var commit = _.find(commitCache, function (commit) { return commit.hash === hash; });
+ const historyShown = HistoryViewer.toggle(commit, getCurrentDocument(), {
+ isInitial: $(this).attr("x-initial-commit") === "true"
+ });
+ $tr.parent().find("tr.selected").removeClass("selected");
+ if(historyShown){
+ $tr.addClass("selected");
+ }
+ });
+ }
+
+ var generateCssAvatar = _.memoize(function (author, email) {
+
+ // Original source: http://indiegamr.com/generate-repeatable-random-numbers-in-js/
+ var seededRandom = function (max, min, seed) {
+ max = max || 1;
+ min = min || 0;
+
+ seed = (seed * 9301 + 49297) % 233280;
+ var rnd = seed / 233280.0;
+
+ return min + rnd * (max - min);
+ };
+
+ // Use `seededRandom()` to generate a pseudo-random number [0-16] to pick a color from the list
+ var seedBase = parseInt(author.charCodeAt(3).toString(), email.length),
+ seed = parseInt(email.charCodeAt(seedBase.toString().substring(1, 2)).toString(), 16),
+ colors = [
+ "#ffb13b", "#dd5f7a", "#8dd43a", "#2f7e2f", "#4141b9", "#3dafea", "#7e3e3e", "#f2f26b",
+ "#864ba3", "#ac8aef", "#f2f2ce", "#379d9d", "#ff6750", "#8691a2", "#d2fd8d", "#88eadf"
+ ],
+ texts = [
+ "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#333333",
+ "#FEFEFE", "#FEFEFE", "#333333", "#FEFEFE", "#FEFEFE", "#FEFEFE", "#333333", "#333333"
+ ],
+ picked = Math.floor(seededRandom(0, 16, seed));
+
+ return "background-color: " + colors[picked] + "; color: " + texts[picked];
+
+ }, function (author, email) {
+ // calculate hash for memoize - both are strings so we don't need to convert
+ return author + email;
+ });
+
+ // Render history list the first time
+ function renderHistory(file) {
+ // clear cache
+ commitCache = [];
+
+ return Git.getCurrentBranchName().then(function (branchName) {
+ // Get the history commits of the current branch
+ var p = file ? Git.getFileHistory(file.relative, branchName) : Git.getHistory(branchName);
+ return p.then(function (commits) {
+
+ // calculate some missing stuff like avatars
+ commits = addAdditionalCommitInfo(commits);
+ commitCache = commitCache.concat(commits);
+
+ var templateData = {
+ commits: commits,
+ Strings: Strings
+ };
+
+ $tableContainer.append(Mustache.render(gitPanelHistoryTemplate, templateData, {
+ commits: gitPanelHistoryCommitsTemplate
+ }));
+
+ $historyList = $tableContainer.find("#git-history-list")
+ .data("file", file ? file.absolute : null)
+ .data("file-relative", file ? file.relative : null);
+
+ $historyList
+ .find("tr.history-commit:last-child")
+ .attr("x-initial-commit", "true");
+ });
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GET_HISTORY);
+ });
+ }
+
+ // Load more rows in the history list on scroll
+ function loadMoreHistory() {
+ if ($historyList.is(":visible")) {
+ if (($tableContainer.prop("scrollHeight") - $tableContainer.scrollTop()) === $tableContainer.height()) {
+ if ($historyList.attr("x-finished") === "true") {
+ return;
+ }
+ return Git.getCurrentBranchName().then(function (branchName) {
+ var p,
+ file = $historyList.data("file-relative"),
+ skipCount = $tableContainer.find("tr.history-commit").length;
+ if (file) {
+ p = Git.getFileHistory(file, branchName, skipCount);
+ } else {
+ p = Git.getHistory(branchName, skipCount);
+ }
+ return p.then(function (commits) {
+ if (commits.length === 0) {
+ $historyList.attr("x-finished", "true");
+ // marks initial commit as first
+ $historyList
+ .find("tr.history-commit:last-child")
+ .attr("x-initial-commit", "true");
+ return;
+ }
+
+ commits = addAdditionalCommitInfo(commits);
+ commitCache = commitCache.concat(commits);
+
+ var templateData = {
+ commits: commits,
+ Strings: Strings
+ };
+ var commitsHtml = Mustache.render(gitPanelHistoryCommitsTemplate, templateData);
+ $historyList.children("tbody").append(commitsHtml);
+ })
+ .catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GET_MORE_HISTORY);
+ });
+ })
+ .catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GET_CURRENT_BRANCH);
+ });
+ }
+ }
+ }
+
+ function addAdditionalCommitInfo(commits) {
+ _.forEach(commits, function (commit) {
+
+ commit.cssAvatar = generateCssAvatar(commit.author, commit.email);
+ commit.avatarLetter = commit.author.substring(0, 1);
+
+ const dateTime = new Date(commit.date);
+ if (isNaN(dateTime.getTime())) {
+ // we got invalid date, use the original date itself
+ commit.date = {
+ title: commit.date,
+ shown: commit.date
+ };
+ } else {
+ commit.date = {
+ title: LocalizationUtils.getFormattedDateTime(dateTime),
+ shown: LocalizationUtils.dateTimeFromNowFriendly(dateTime)
+ };
+ }
+ commit.hasTag = !!commit.tags;
+ });
+
+ return commits;
+ }
+
+ function getCurrentDocument() {
+ if (HistoryViewer.isVisible()) {
+ return lastDocumentSeen;
+ }
+ var doc = DocumentManager.getCurrentDocument();
+ if (doc) {
+ lastDocumentSeen = doc;
+ }
+ return doc || lastDocumentSeen;
+ }
+
+ function handleFileChange() {
+ var currentDocument = getCurrentDocument();
+
+ if ($historyList.is(":visible") && $historyList.data("file")) {
+ handleToggleHistory("FILE", currentDocument);
+ }
+ $gitPanel.find(".git-file-history").prop("disabled", !currentDocument);
+ }
+
+ // Show or hide the history list on click of .history button
+ // newHistoryMode can be "FILE", "GLOBAL" or "REFRESH"
+ function handleToggleHistory(newHistoryMode, newDocument) {
+ // this is here to check that $historyList is still attached to the DOM
+ $historyList = $tableContainer.find("#git-history-list");
+
+ let historyEnabled = $historyList.is(":visible"),
+ currentFile = $historyList.data("file") || null,
+ currentHistoryMode = historyEnabled ? (currentFile ? "FILE" : "GLOBAL") : "DISABLED",
+ doc = newDocument ? newDocument : getCurrentDocument(),
+ file;
+
+ // Variables to store scroll positions (only used for REFRESH case)
+ let savedScrollTop, savedScrollLeft, selectedCommitHash;
+ let isRefresh = false;
+ if(newHistoryMode === "REFRESH"){
+ newHistoryMode = currentHistoryMode;
+ isRefresh = true;
+ historyEnabled = true;
+ // Save current scroll positions before removing the list
+ if ($historyList.length > 0) {
+ savedScrollTop = $historyList.parent().scrollTop();
+ savedScrollLeft = $historyList.parent().scrollLeft();
+ selectedCommitHash = $historyList.find(".selected").attr("x-hash");
+ }
+ } else if (currentHistoryMode !== newHistoryMode) {
+ // we are switching the modes so enable
+ historyEnabled = true;
+ } else if (!newDocument) {
+ // we are not changing the mode and we are not switching to a new document
+ historyEnabled = !historyEnabled;
+ }
+
+ if (historyEnabled && newHistoryMode === "FILE") {
+ if (doc) {
+ file = {};
+ file.absolute = doc.file.fullPath;
+ file.relative = FileUtils.getRelativeFilename(Preferences.get("currentGitRoot"), file.absolute);
+ } else {
+ // we want a file history but no file was found
+ historyEnabled = false;
+ }
+ }
+
+ // Render #git-history-list if is not already generated or if the viewed file for file history has changed
+ var isEmpty = $historyList.find("tr").length === 0,
+ fileChanged = currentFile !== (file ? file.absolute : null);
+ if (historyEnabled && (isEmpty || fileChanged || isRefresh)) {
+ if ($historyList.length > 0) {
+ $historyList.remove();
+ }
+ var $spinner = $("
").appendTo($gitPanel);
+ renderHistory(file).finally(function () {
+ $spinner.remove();
+ if (isRefresh) {
+ // After rendering, we need to fetch the newly created #git-history-list
+ let $newHistoryList = $tableContainer.find("#git-history-list");
+ // Restore the scroll position
+ $newHistoryList.parent().scrollTop(savedScrollTop || 0);
+ $newHistoryList.parent().scrollLeft(savedScrollLeft || 0);
+ $historyList.find(`[x-hash="${selectedCommitHash}"]`).addClass("selected");
+ }
+ });
+ }
+
+ // disable commit button when viewing history
+ // refresh status when history is closed and commit button will correct its disabled state if required
+ if (historyEnabled) {
+ $gitPanel.find(".git-commit, .check-all").prop("disabled", true);
+ } else {
+ Git.status();
+ }
+
+ // Toggle visibility of .git-edited-list and #git-history-list
+ $tableContainer.find(".git-edited-list").toggle(!historyEnabled);
+ $historyList.toggle(historyEnabled);
+
+ if (!historyEnabled) { HistoryViewer.hide(); }
+
+ // Toggle history button
+ var globalButtonActive = historyEnabled && newHistoryMode === "GLOBAL",
+ fileButtonActive = historyEnabled && newHistoryMode === "FILE";
+ $gitPanel.find(".git-history-toggle").toggleClass("active", globalButtonActive)
+ .attr("title", globalButtonActive ? Strings.TOOLTIP_HIDE_HISTORY : Strings.TOOLTIP_SHOW_HISTORY);
+ $gitPanel.find(".git-file-history").toggleClass("active", fileButtonActive)
+ .attr("title", fileButtonActive ? Strings.TOOLTIP_HIDE_FILE_HISTORY : Strings.TOOLTIP_SHOW_FILE_HISTORY);
+ }
+
+ // Event listeners
+ EventEmitter.on(Events.GIT_ENABLED, function () {
+ initVariables();
+ });
+ EventEmitter.on(Events.GIT_DISABLED, function () {
+ lastDocumentSeen = null;
+ $historyList.remove();
+ $historyList = $();
+ });
+ EventEmitter.on(Events.HISTORY_SHOW_FILE, function () {
+ handleToggleHistory("FILE");
+ });
+ EventEmitter.on(Events.HISTORY_SHOW_GLOBAL, function () {
+ handleToggleHistory("GLOBAL");
+ });
+ EventEmitter.on(Events.REFRESH_HISTORY, function () {
+ handleToggleHistory("REFRESH");
+ });
+ EventEmitter.on(Events.BRACKETS_CURRENT_DOCUMENT_CHANGE, function () {
+ handleFileChange();
+ });
+
+});
diff --git a/src/extensions/default/Git/src/HistoryViewer.js b/src/extensions/default/Git/src/HistoryViewer.js
new file mode 100644
index 0000000000..887f873471
--- /dev/null
+++ b/src/extensions/default/Git/src/HistoryViewer.js
@@ -0,0 +1,293 @@
+define(function (require, exports) {
+
+ const _ = brackets.getModule("thirdparty/lodash"),
+ LanguageManager = brackets.getModule("language/LanguageManager"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache"),
+ WorkspaceManager = brackets.getModule("view/WorkspaceManager"),
+ Strings = brackets.getModule("strings"),
+ marked = brackets.getModule('thirdparty/marked.min').marked;
+
+ const ErrorHandler = require("src/ErrorHandler"),
+ Git = require("src/git/Git"),
+ Preferences = require("src/Preferences"),
+ Utils = require("src/Utils");
+
+ var historyViewerTemplate = require("text!templates/history-viewer.html"),
+ historyViewerFilesTemplate = require("text!templates/history-viewer-files.html");
+
+ let useDifftool = false,
+ isShown = false,
+ commit = null,
+ currentlyViewedCommit = null,
+ isInitial = null,
+ $viewer = null,
+ $editorHolder = null;
+
+ var setExpandState = _.debounce(function () {
+ var allFiles = $viewer.find(".commit-files a"),
+ activeFiles = allFiles.filter(".active"),
+ allExpanded = allFiles.length === activeFiles.length;
+ $viewer.find(".toggle-diffs").toggleClass("opened", allExpanded);
+ }, 100);
+
+ var PAGE_SIZE = 25;
+ var currentPage = 0;
+ var hasNextPage = false;
+
+ function toggleDiff($a) {
+ if ($a.hasClass("active")) {
+ // Close the clicked diff
+ $a.removeClass("active");
+ setExpandState();
+ return;
+ }
+
+ // Open the clicked diff
+ $(".commit-files a.active").attr("scrollPos", $(".commit-diff").scrollTop());
+
+ // If this diff was not previously loaded then load it
+ if (!$a.is(".loaded")) {
+ var $li = $a.closest("[x-file]"),
+ relativeFilePath = $li.attr("x-file"),
+ $diffContainer = $li.find(".commit-diff");
+
+ Git.getDiffOfFileFromCommit(commit.hash, relativeFilePath, isInitial).then(function (diff) {
+ $diffContainer.html(Utils.formatDiff(diff));
+ $diffContainer.scrollTop($a.attr("scrollPos") || 0);
+
+ $a.addClass("active loaded");
+ setExpandState();
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GET_DIFF_FILE_COMMIT);
+ });
+ } else {
+ // If this diff was previously loaded just open it
+ $a.addClass("active");
+ setExpandState();
+ }
+ }
+
+ function showDiff($el) {
+ var file = $el.closest("[x-file]").attr("x-file");
+ Git.difftoolFromHash(commit.hash, file, isInitial);
+ }
+
+ function expandAll() {
+ $viewer.find(".commit-files a").not(".active").trigger("click");
+ Preferences.set("autoExpandDiffsInHistory", true);
+ }
+
+ function collapseAll() {
+ $viewer.find(".commit-files a").filter(".active").trigger("click");
+ Preferences.set("autoExpandDiffsInHistory", false);
+ }
+
+ function attachEvents() {
+ $viewer
+ .on("click", ".commit-files a", function () {
+ toggleDiff($(this));
+ })
+ .on("click", ".commit-files .difftool", function (e) {
+ e.stopPropagation();
+ showDiff($(this));
+ })
+ .on("click", ".openFile", function (e) {
+ e.stopPropagation();
+ var file = $(this).closest("[x-file]").attr("x-file");
+ Utils.openEditorForFile(file, true);
+ hide();
+ })
+ .on("click", ".close", function () {
+ // Close history viewer
+ remove();
+ })
+ .on("click", ".git-extend-sha", function () {
+ // Show complete commit SHA
+ var $parent = $(this).parent(),
+ sha = $parent.data("hash");
+ $parent.find("span.selectable-text").text(sha);
+ $(this).remove();
+ })
+ .on("click", ".toggle-diffs", expandAll)
+ .on("click", ".toggle-diffs.opened", collapseAll);
+
+ // Add/Remove shadow on bottom of header
+ $viewer.find(".body")
+ .on("scroll", function () {
+ if ($viewer.find(".body").scrollTop() > 0) {
+ $viewer.find(".header").addClass("shadow");
+ } else {
+ $viewer.find(".header").removeClass("shadow");
+ }
+ });
+
+ // Expand the diffs when wanted
+ if (Preferences.get("autoExpandDiffsInHistory")) {
+ expandAll();
+ }
+ }
+
+ function renderViewerContent(files, selectedFile) {
+ var bodyMarkdown = marked(commit.body, { gfm: true, breaks: true });
+
+ $viewer.html(Mustache.render(historyViewerTemplate, {
+ commit: commit,
+ bodyMarkdown: bodyMarkdown,
+ Strings: Strings
+ }));
+
+ renderFiles(files);
+
+ if (selectedFile) {
+ var $fileEntry = $viewer.find(".commit-files li[x-file='" + selectedFile + "'] a").first();
+ if ($fileEntry.length) {
+ toggleDiff($fileEntry);
+ window.setTimeout(function () {
+ $viewer.find(".body").animate({ scrollTop: $fileEntry.position().top - 10 });
+ }, 80);
+ }
+ }
+
+ attachEvents();
+ }
+
+ function renderFiles(files) {
+ $viewer.find(".filesContainer").append(Mustache.render(historyViewerFilesTemplate, {
+ files: files,
+ Strings: Strings,
+ useDifftool: useDifftool
+ }));
+
+ // Activate/Deactivate load more button
+ $viewer.find(".loadMore")
+ .toggle(hasNextPage)
+ .off("click")
+ .on("click", function () {
+ currentPage++;
+ loadMoreFiles();
+ });
+ }
+
+ function loadMoreFiles() {
+ Git.getFilesFromCommit(commit.hash, isInitial).then(function (files) {
+
+ hasNextPage = files.slice((currentPage + 1) * PAGE_SIZE).length > 0;
+ files = files.slice(currentPage * PAGE_SIZE, (currentPage + 1) * PAGE_SIZE);
+
+ var list = files.map(function (file) {
+ var fileExtension = LanguageManager.getCompoundFileExtension(file),
+ i = file.lastIndexOf("." + fileExtension),
+ fileName = file.substring(0, fileExtension && i >= 0 ? i : file.length);
+ return {
+ name: fileName,
+ extension: fileExtension ? "." + fileExtension : "",
+ file: file
+ };
+ });
+
+ if (currentPage === 0) {
+ var file = $("#git-history-list").data("file-relative");
+ return renderViewerContent(list, file);
+ } else {
+ return renderFiles(list);
+ }
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GET_DIFF_FILES);
+ }).finally(function () {
+ $viewer.removeClass("spinner large spin");
+ });
+ }
+
+ function render() {
+ if ($viewer) {
+ // Reset the viewer listeners
+ $viewer.off("click");
+ $viewer.find(".body").off("scroll");
+ } else {
+ // Create the viewer if it doesn't exist
+ $viewer = $("
").addClass("git spinner large spin");
+ $viewer.appendTo($editorHolder);
+ }
+
+ currentPage = 0;
+ loadMoreFiles();
+ }
+
+ var initialize = _.once(function () {
+ Git.getConfig("diff.tool").then(function (config) {
+ useDifftool = !!config;
+ });
+ });
+
+ function toggle(commitInfo, doc, options) {
+ const commitHash = commitInfo.hash;
+ if(isShown && commitHash === currentlyViewedCommit) {
+ // the history view already showing the current commit, the user intent is to close
+ remove();
+ return false;
+ }
+ // a new history is to be shown
+ show(commitInfo, doc, options);
+ return true;
+ }
+
+ function show(commitInfo, doc, options) {
+ initialize();
+
+ commit = commitInfo;
+ isInitial = options.isInitial;
+
+ $editorHolder = $("#editor-holder");
+ render();
+ currentlyViewedCommit = commitInfo.hash;
+ isShown = true;
+ if ($("#first-pane").length) {
+ const firstPaneStyle =
+ $("#first-pane").prop("style") && $("#first-pane").prop("style").cssText ?
+ $("#first-pane").prop("style").cssText : "";
+ $("#first-pane").prop("style", firstPaneStyle + ";display: none !important;");
+ }
+
+ if ($("#second-pane").length) {
+ const secondPaneStyle =
+ $("#second-pane").prop("style") && $("#second-pane").prop("style").cssText ?
+ $("#second-pane").prop("style").cssText : "";
+ $("#second-pane").prop("style", secondPaneStyle + ";display: none !important;");
+ }
+ }
+
+ function onRemove() {
+ isShown = false;
+ $viewer = null;
+ currentlyViewedCommit = null;
+ $("#first-pane").show();
+ $("#second-pane").show();
+ // we need to relayout as when the history overlay is visible over the editor, we
+ // hide the editor with css, and if we resize app while history view is open, the editor wont
+ // be resized. So we relayout on panel close.
+ WorkspaceManager.recomputeLayout();
+ // detach events that were added by this viewer to another element than one added to $editorHolder
+ }
+
+ function hide() {
+ if (isShown) {
+ remove();
+ }
+ }
+
+ function remove() {
+ $viewer.remove();
+ onRemove();
+ }
+
+ function isVisible() {
+ return isShown;
+ }
+
+ // Public API
+ exports.toggle = toggle;
+ exports.show = show;
+ exports.hide = hide;
+ exports.isVisible = isVisible;
+
+});
diff --git a/src/extensions/default/Git/src/Main.js b/src/extensions/default/Git/src/Main.js
new file mode 100644
index 0000000000..a4d6db2100
--- /dev/null
+++ b/src/extensions/default/Git/src/Main.js
@@ -0,0 +1,434 @@
+define(function (require, exports) {
+
+ const _ = brackets.getModule("thirdparty/lodash"),
+ CommandManager = brackets.getModule("command/CommandManager"),
+ Commands = brackets.getModule("command/Commands"),
+ Menus = brackets.getModule("command/Menus"),
+ FileSystem = brackets.getModule("filesystem/FileSystem"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache"),
+ ProjectManager = brackets.getModule("project/ProjectManager");
+
+ const Constants = require("src/Constants"),
+ Events = require("src/Events"),
+ EventEmitter = require("src/EventEmitter"),
+ Strings = brackets.getModule("strings"),
+ StringUtils = brackets.getModule("utils/StringUtils"),
+ ErrorHandler = require("src/ErrorHandler"),
+ Panel = require("src/Panel"),
+ Branch = require("src/Branch"),
+ SettingsDialog = require("src/SettingsDialog"),
+ Dialogs = brackets.getModule("widgets/Dialogs"),
+ CloseNotModified = require("src/CloseNotModified"),
+ Setup = require("src/utils/Setup"),
+ Preferences = require("src/Preferences"),
+ Utils = require("src/Utils"),
+ Git = require("src/git/Git"),
+ gitTagDialogTemplate = require("text!templates/git-tag-dialog.html");
+
+ const CMD_ADD_TO_IGNORE = "git.addToIgnore",
+ CMD_REMOVE_FROM_IGNORE = "git.removeFromIgnore",
+ $icon = $(`
`)
+ .addClass("forced-hidden")
+ .prependTo($(".bottom-buttons"));
+
+ let gitEnabled = false;
+
+ EventEmitter.on(Events.GIT_DISABLED, function () {
+ $icon.removeClass("dirty");
+ });
+
+ EventEmitter.on(Events.GIT_STATUS_RESULTS, function (sortedResults) {
+ $icon.toggleClass("dirty", sortedResults.length !== 0);
+ });
+
+ // This only launches when Git is available
+ function initUi() {
+ // FUTURE: do we really need to launch init from here?
+ Panel.init();
+ Branch.init();
+ CloseNotModified.init();
+ // Attach events
+ $icon.on("click", Panel.toggle);
+ }
+
+ function _addRemoveItemInGitignore(selectedEntry, method) {
+ var gitRoot = Preferences.get("currentGitRoot"),
+ entryPath = "/" + selectedEntry.fullPath.substring(gitRoot.length),
+ gitignoreEntry = FileSystem.getFileForPath(gitRoot + ".gitignore");
+
+ gitignoreEntry.read(function (err, content) {
+ if (err) {
+ Utils.consoleWarn(err);
+ content = "";
+ }
+
+ // use trimmed lines only
+ var lines = content.split("\n").map(function (l) { return l.trim(); });
+ // clean start and end empty lines
+ while (lines.length > 0 && !lines[0]) { lines.shift(); }
+ while (lines.length > 0 && !lines[lines.length - 1]) { lines.pop(); }
+
+ if (method === "add") {
+ // add only when not already present
+ if (lines.indexOf(entryPath) === -1) { lines.push(entryPath); }
+ } else if (method === "remove") {
+ lines = _.without(lines, entryPath);
+ }
+
+ // always have an empty line at the end of the file
+ if (lines[lines.length - 1]) { lines.push(""); }
+
+ gitignoreEntry.write(lines.join("\n"), function (err) {
+ if (err) {
+ return ErrorHandler.showError(err, Strings.ERROR_MODIFY_GITIGNORE);
+ }
+ Panel.refresh();
+ });
+ });
+ }
+
+ function addItemToGitingore() {
+ return _addRemoveItemInGitignore(ProjectManager.getSelectedItem(), "add");
+ }
+
+ function removeItemFromGitingore() {
+ return _addRemoveItemInGitignore(ProjectManager.getSelectedItem(), "remove");
+ }
+
+ function addItemToGitingoreFromPanel() {
+ var filePath = Panel.getPanel().find("tr.selected").attr("x-file"),
+ fileEntry = FileSystem.getFileForPath(Preferences.get("currentGitRoot") + filePath);
+ return _addRemoveItemInGitignore(fileEntry, "add");
+ }
+
+ function removeItemFromGitingoreFromPanel() {
+ var filePath = Panel.getPanel().find("tr.selected").attr("x-file"),
+ fileEntry = FileSystem.getFileForPath(Preferences.get("currentGitRoot") + filePath);
+ return _addRemoveItemInGitignore(fileEntry, "remove");
+ }
+
+ function _refreshCallback() {
+ EventEmitter.emit(Events.REFRESH_ALL);
+ }
+
+ function checkoutCommit(commitHash) {
+ const commitDetail = Panel.getSelectedHistoryCommit() || {};
+ commitHash = commitHash || commitDetail.hash;
+ const commitDetailStr = commitDetail.subject || "";
+ if(!commitHash){
+ console.error(`Cannot do Git checkout as commit hash is ${commitHash}`);
+ return;
+ }
+ const displayStr = StringUtils.format(Strings.CHECKOUT_COMMIT_DETAIL, commitDetailStr, commitHash);
+ Utils.askQuestion(Strings.TITLE_CHECKOUT,
+ displayStr + "
" + Strings.DIALOG_CHECKOUT,
+ { booleanResponse: true, noescape: true, customOkBtn: Strings.CHECKOUT_COMMIT })
+ .then(function (response) {
+ if (response === true) {
+ return Git.checkout(commitHash).then(_refreshCallback);
+ }
+ });
+ }
+
+ function tagCommit(commitHash) {
+ const commitDetail = Panel.getSelectedHistoryCommit() || {};
+ commitHash = commitHash || commitDetail.hash || "";
+ const compiledTemplate = Mustache.render(gitTagDialogTemplate, { Strings }),
+ dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate),
+ $dialog = dialog.getElement();
+ $dialog.find("input").focus();
+ $dialog.find("button.primary").on("click", function () {
+ const tagname = $dialog.find("input.commit-message").val();
+ Git.setTagName(tagname, commitHash).then(function () {
+ EventEmitter.emit(Events.REFRESH_HISTORY);
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_CREATE_TAG);
+ });
+ });
+ }
+
+ function _resetOperation(operation, commitHash, title, message) {
+ const commitDetail = Panel.getSelectedHistoryCommit() || {};
+ commitHash = commitHash || commitDetail.hash;
+ const commitDetailStr = commitDetail.subject || "";
+ if(!commitHash){
+ console.error(`Cannot do Git Reset ${operation} as commit hash is ${commitHash}`);
+ return;
+ }
+ const gitCmdUsed = `git reset ${operation} ${commitHash}`;
+ const displayStr = StringUtils.format(Strings.RESET_DETAIL, commitDetailStr, gitCmdUsed);
+ Utils.askQuestion(title,
+ message + "
" + displayStr,
+ { booleanResponse: true, noescape: true ,
+ customOkBtn: Strings.RESET, customOkBtnClass: "danger"})
+ .then(function (response) {
+ if (response === true) {
+ return Git.reset(operation, commitHash).then(_refreshCallback);
+ }
+ });
+ }
+
+ function resetHard(commitHash) {
+ return _resetOperation("--hard", commitHash,
+ Strings.RESET_HARD_TITLE, Strings.RESET_HARD_MESSAGE);
+ }
+
+ function resetMixed(commitHash) {
+ return _resetOperation("--mixed", commitHash,
+ Strings.RESET_MIXED_TITLE, Strings.RESET_MIXED_MESSAGE);
+ }
+
+ function resetSoft(commitHash) {
+ return _resetOperation("--soft", commitHash,
+ Strings.RESET_SOFT_TITLE, Strings.RESET_SOFT_MESSAGE);
+ }
+
+ /**
+ * Disables all Git-related commands that were registered in `initGitMenu`.
+ * After calling this function, none of these menu items will be clickable.
+ */
+ function disableAllMenus() {
+ // Collect all command IDs that were registered in initGitMenu
+ const commandsToDisable = [
+ // File menu items
+ Constants.CMD_GIT_INIT,
+ Constants.CMD_GIT_CLONE,
+ Constants.CMD_GIT_TOGGLE_PANEL,
+ Constants.CMD_GIT_REFRESH,
+ Constants.CMD_GIT_GOTO_NEXT_CHANGE,
+ Constants.CMD_GIT_GOTO_PREVIOUS_CHANGE,
+ Constants.CMD_GIT_CLOSE_UNMODIFIED,
+ Constants.CMD_GIT_AUTHORS_OF_SELECTION,
+ Constants.CMD_GIT_AUTHORS_OF_FILE,
+ Constants.CMD_GIT_COMMIT_CURRENT,
+ Constants.CMD_GIT_COMMIT_ALL,
+ Constants.CMD_GIT_FETCH,
+ Constants.CMD_GIT_PULL,
+ Constants.CMD_GIT_PUSH,
+ Constants.CMD_GIT_GERRIT_PUSH_REF,
+ Constants.CMD_GIT_CHANGE_USERNAME,
+ Constants.CMD_GIT_CHANGE_EMAIL,
+ Constants.CMD_GIT_SETTINGS_COMMAND_ID,
+
+ // Project tree/working files commands
+ CMD_ADD_TO_IGNORE,
+ CMD_REMOVE_FROM_IGNORE,
+ // Panel context menu commands (with "2" suffix)
+ CMD_ADD_TO_IGNORE + "2",
+ CMD_REMOVE_FROM_IGNORE + "2",
+
+ // History context menu commands
+ Constants.CMD_GIT_CHECKOUT,
+ Constants.CMD_GIT_TAG,
+ Constants.CMD_GIT_RESET_HARD,
+ Constants.CMD_GIT_RESET_MIXED,
+ Constants.CMD_GIT_RESET_SOFT,
+
+ // "More options" context menu commands
+ Constants.CMD_GIT_DISCARD_ALL_CHANGES,
+ Constants.CMD_GIT_UNDO_LAST_COMMIT,
+ Constants.CMD_GIT_TOGGLE_UNTRACKED
+ ];
+
+ // Disable each command
+ commandsToDisable.forEach((cmdId) => {
+ Utils.enableCommand(cmdId, false);
+ });
+ }
+
+
+ function initGitMenu() {
+ // Register command and add it to the menu.
+ const fileMenu = Menus.getMenu(Menus.AppMenuBar.FILE_MENU);
+ let gitSubMenu = fileMenu.addSubMenu(Constants.GIT_STRING_UNIVERSAL,
+ Constants.GIT_SUB_MENU, Menus.AFTER, Commands.FILE_EXTENSION_MANAGER);
+ fileMenu.addMenuDivider(Menus.AFTER, Commands.FILE_EXTENSION_MANAGER);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_INIT, undefined, undefined, undefined, {
+ hideWhenCommandDisabled: true
+ });
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_CLONE, undefined, undefined, undefined, {
+ hideWhenCommandDisabled: true
+ });
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_TOGGLE_PANEL);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_REFRESH);
+ gitSubMenu.addMenuDivider();
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_GOTO_NEXT_CHANGE);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_GOTO_PREVIOUS_CHANGE);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_CLOSE_UNMODIFIED);
+ gitSubMenu.addMenuDivider();
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_AUTHORS_OF_SELECTION);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_AUTHORS_OF_FILE);
+ gitSubMenu.addMenuDivider();
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_COMMIT_CURRENT);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_COMMIT_ALL);
+ gitSubMenu.addMenuDivider();
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_FETCH);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_PULL);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_PUSH);
+ gitSubMenu.addMenuDivider();
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_GERRIT_PUSH_REF);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_CHANGE_USERNAME);
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_CHANGE_EMAIL);
+ gitSubMenu.addMenuDivider();
+ gitSubMenu.addMenuItem(Constants.CMD_GIT_SETTINGS_COMMAND_ID);
+
+ // register commands for project tree / working files
+ CommandManager.register(Strings.ADD_TO_GITIGNORE, CMD_ADD_TO_IGNORE, addItemToGitingore);
+ CommandManager.register(Strings.REMOVE_FROM_GITIGNORE, CMD_REMOVE_FROM_IGNORE, removeItemFromGitingore);
+
+ // create context menu for git panel
+ const panelCmenu = Menus.registerContextMenu(Constants.GIT_PANEL_CHANGES_CMENU);
+ CommandManager.register(Strings.ADD_TO_GITIGNORE, CMD_ADD_TO_IGNORE + "2", addItemToGitingoreFromPanel);
+ CommandManager.register(Strings.REMOVE_FROM_GITIGNORE, CMD_REMOVE_FROM_IGNORE + "2", removeItemFromGitingoreFromPanel);
+ panelCmenu.addMenuItem(CMD_ADD_TO_IGNORE + "2");
+ panelCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE + "2");
+
+ // create context menu for git history
+ const historyCmenu = Menus.registerContextMenu(Constants.GIT_PANEL_HISTORY_CMENU);
+ CommandManager.register(Strings.CHECKOUT_COMMIT, Constants.CMD_GIT_CHECKOUT, checkoutCommit);
+ CommandManager.register(Strings.MENU_RESET_HARD, Constants.CMD_GIT_RESET_HARD, resetHard);
+ CommandManager.register(Strings.MENU_RESET_MIXED, Constants.CMD_GIT_RESET_MIXED, resetMixed);
+ CommandManager.register(Strings.MENU_RESET_SOFT, Constants.CMD_GIT_RESET_SOFT, resetSoft);
+ CommandManager.register(Strings.MENU_TAG_COMMIT, Constants.CMD_GIT_TAG, tagCommit);
+ historyCmenu.addMenuItem(Constants.CMD_GIT_CHECKOUT);
+ historyCmenu.addMenuItem(Constants.CMD_GIT_TAG);
+ historyCmenu.addMenuDivider();
+ historyCmenu.addMenuItem(Constants.CMD_GIT_RESET_HARD);
+ historyCmenu.addMenuItem(Constants.CMD_GIT_RESET_MIXED);
+ historyCmenu.addMenuItem(Constants.CMD_GIT_RESET_SOFT);
+
+ // create context menu for git more options
+ const optionsCmenu = Menus.registerContextMenu(Constants.GIT_PANEL_OPTIONS_CMENU);
+ Menus.ContextMenu.assignContextMenuToSelector(".git-more-options-btn", optionsCmenu);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_DISCARD_ALL_CHANGES);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_UNDO_LAST_COMMIT);
+ optionsCmenu.addMenuDivider();
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_AUTHORS_OF_SELECTION);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_AUTHORS_OF_FILE);
+ optionsCmenu.addMenuDivider();
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_FETCH);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_PULL);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_PUSH);
+ optionsCmenu.addMenuDivider();
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_TOGGLE_UNTRACKED);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_GERRIT_PUSH_REF);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_CHANGE_USERNAME);
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_CHANGE_EMAIL);
+ optionsCmenu.addMenuDivider();
+ optionsCmenu.addMenuItem(Constants.CMD_GIT_SETTINGS_COMMAND_ID);
+
+ if(!Setup.isExtensionActivated()){
+ disableAllMenus();
+ }
+ }
+
+ function init() {
+ CommandManager.register(Strings.GIT_SETTINGS, Constants.CMD_GIT_SETTINGS_COMMAND_ID, SettingsDialog.show);
+ // Try to get Git version, if succeeds then Git works
+ return Setup.init().then(function (enabled) {
+ initUi();
+ initGitMenu();
+ return enabled;
+ });
+ }
+
+ var _toggleMenuEntriesState = false,
+ _divider1 = null,
+ _divider2 = null;
+ function toggleMenuEntries(bool) {
+ if (bool === _toggleMenuEntriesState) {
+ return;
+ }
+ var projectCmenu = Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU);
+ var workingCmenu = Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU);
+ if (bool) {
+ _divider1 = projectCmenu.addMenuDivider();
+ _divider2 = workingCmenu.addMenuDivider();
+ projectCmenu.addMenuItem(CMD_ADD_TO_IGNORE);
+ workingCmenu.addMenuItem(CMD_ADD_TO_IGNORE);
+ projectCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE);
+ workingCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE);
+ } else {
+ projectCmenu.removeMenuDivider(_divider1.id);
+ workingCmenu.removeMenuDivider(_divider2.id);
+ projectCmenu.removeMenuItem(CMD_ADD_TO_IGNORE);
+ workingCmenu.removeMenuItem(CMD_ADD_TO_IGNORE);
+ projectCmenu.removeMenuItem(CMD_REMOVE_FROM_IGNORE);
+ workingCmenu.removeMenuItem(CMD_REMOVE_FROM_IGNORE);
+ }
+ _toggleMenuEntriesState = bool;
+ }
+
+ function _enableAllCommands(enabled) {
+ Utils.enableCommand(Constants.CMD_GIT_REFRESH, enabled);
+
+ Utils.enableCommand(Constants.CMD_GIT_GOTO_NEXT_CHANGE, enabled);
+ Utils.enableCommand(Constants.CMD_GIT_GOTO_PREVIOUS_CHANGE, enabled);
+ Utils.enableCommand(Constants.CMD_GIT_CLOSE_UNMODIFIED, enabled);
+
+ Utils.enableCommand(Constants.CMD_GIT_AUTHORS_OF_SELECTION, enabled);
+ Utils.enableCommand(Constants.CMD_GIT_AUTHORS_OF_FILE, enabled);
+
+ Utils.enableCommand(Constants.CMD_GIT_COMMIT_CURRENT, enabled);
+ Utils.enableCommand(Constants.CMD_GIT_COMMIT_ALL, enabled);
+
+ Utils.enableCommand(Constants.CMD_GIT_FETCH, enabled);
+ Utils.enableCommand(Constants.CMD_GIT_PULL, enabled);
+ Utils.enableCommand(Constants.CMD_GIT_PUSH, enabled);
+
+ Utils.enableCommand(Constants.CMD_GIT_DISCARD_ALL_CHANGES, enabled);
+ Utils.enableCommand(Constants.CMD_GIT_UNDO_LAST_COMMIT, enabled);
+ toggleMenuEntries(enabled);
+ if(enabled){
+ $icon.removeClass("forced-hidden");
+ } else if(!$("#git-panel").is(":visible")){
+ $icon.addClass("forced-hidden");
+ }
+ }
+
+ let lastExecutionTime = 0;
+ let isCommandExecuting = false;
+ const FOCUS_SWITCH_DEDUPE_TIME = 5000;
+ function refreshOnFocusChange() {
+ // to sync external git changes after switching to app.
+ if (gitEnabled) {
+ const now = Date.now();
+
+ if (isCommandExecuting) {
+ return;
+ }
+
+ if (now - lastExecutionTime > FOCUS_SWITCH_DEDUPE_TIME) {
+ isCommandExecuting = true;
+ lastExecutionTime = Date.now();
+ Git.hasStatusChanged().then((hasChanged) => {
+ if(!hasChanged){
+ return;
+ }
+
+ CommandManager.execute(Constants.CMD_GIT_REFRESH).fail((err) => {
+ console.error("error refreshing on focus switch", err);
+ });
+ }).finally(()=>{
+ isCommandExecuting = false;
+ });
+ }
+ }
+ }
+ $(window).focus(refreshOnFocusChange);
+
+ // Event handlers
+ EventEmitter.on(Events.GIT_ENABLED, function () {
+ _enableAllCommands(true);
+ gitEnabled = true;
+ });
+ EventEmitter.on(Events.GIT_DISABLED, function () {
+ _enableAllCommands(false);
+ gitEnabled = false;
+ });
+
+ // API
+ exports.$icon = $icon;
+ exports.init = init;
+
+});
diff --git a/src/extensions/default/Git/src/NoRepo.js b/src/extensions/default/Git/src/NoRepo.js
new file mode 100644
index 0000000000..d0746e7e2e
--- /dev/null
+++ b/src/extensions/default/Git/src/NoRepo.js
@@ -0,0 +1,163 @@
+/*globals jsPromise, fs*/
+define(function (require) {
+
+ // Brackets modules
+ const FileSystem = brackets.getModule("filesystem/FileSystem"),
+ FileUtils = brackets.getModule("file/FileUtils"),
+ ProjectManager = brackets.getModule("project/ProjectManager"),
+ CommandManager = brackets.getModule("command/CommandManager"),
+ StringUtils = brackets.getModule("utils/StringUtils");
+
+ // Local modules
+ const ErrorHandler = require("src/ErrorHandler"),
+ Events = require("src/Events"),
+ EventEmitter = require("src/EventEmitter"),
+ Strings = brackets.getModule("strings"),
+ ExpectedError = require("src/ExpectedError"),
+ ProgressDialog = require("src/dialogs/Progress"),
+ CloneDialog = require("src/dialogs/Clone"),
+ Git = require("src/git/Git"),
+ Preferences = require("src/Preferences"),
+ Constants = require("src/Constants"),
+ Utils = require("src/Utils");
+
+ // Templates
+ var gitignoreTemplate = require("text!templates/default-gitignore");
+
+ // Module variables
+
+ // Implementation
+
+ function createGitIgnore() {
+ var gitIgnorePath = Preferences.get("currentGitRoot") + ".gitignore";
+ return Utils.pathExists(gitIgnorePath).then(function (exists) {
+ if (!exists) {
+ return jsPromise(
+ FileUtils.writeText(FileSystem.getFileForPath(gitIgnorePath), gitignoreTemplate));
+ }
+ });
+ }
+
+ function stageGitIgnore() {
+ return createGitIgnore().then(function () {
+ return Git.stage(".gitignore");
+ });
+ }
+
+ function handleGitInit() {
+ Utils.isProjectRootWritable().then(function (writable) {
+ if (!writable) {
+ const initPath = Phoenix.app.getDisplayPath(Utils.getProjectRoot());
+ const errorStr = StringUtils.format(Strings.FOLDER_NOT_WRITABLE, initPath);
+ throw new ExpectedError(errorStr);
+ }
+ return Git.init().catch(function (err) {
+ return new Promise((resolve, reject)=>{
+ if (ErrorHandler.contains(err, "Please tell me who you are")) {
+ EventEmitter.emit(Events.GIT_CHANGE_USERNAME, function () {
+ EventEmitter.emit(Events.GIT_CHANGE_EMAIL, function () {
+ Git.init().then(function (result) {
+ resolve(result);
+ }).catch(function (err) {
+ reject(err);
+ });
+ });
+ });
+ return;
+ }
+
+ reject(err);
+ });
+ });
+ }).then(function () {
+ return stageGitIgnore("Initial staging");
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.INIT_NEW_REPO_FAILED, true);
+ }).then(function () {
+ EventEmitter.emit(Events.REFRESH_ALL);
+ });
+ }
+
+ // This checks if the project root is empty (to let Git clone repositories)
+ function isProjectRootEmpty() {
+ return new Promise(function (resolve, reject) {
+ ProjectManager.getProjectRoot().getContents(function (err, entries) {
+ if (err) {
+ return reject(err);
+ }
+ resolve(entries.length === 0);
+ });
+ });
+ }
+
+ function handleGitClone(gitCloneURL, destPath) {
+ var $gitPanel = $("#git-panel");
+ var $cloneButton = $gitPanel.find(".git-clone");
+ $cloneButton.prop("disabled", true);
+ isProjectRootEmpty().then(function (isEmpty) {
+ if (!isEmpty) {
+ const clonePath = Phoenix.app.getDisplayPath(Utils.getProjectRoot());
+ const err = new ExpectedError(
+ StringUtils.format(Strings.GIT_CLONE_ERROR_EXPLAIN, clonePath));
+ ErrorHandler.showError(err, Strings.GIT_CLONE_REMOTE_FAILED, true);
+ return;
+ }
+ function _clone(cloneConfig) {
+ var q = Promise.resolve();
+ // put username and password into remote url
+ var remoteUrl = cloneConfig.remoteUrl;
+ if (cloneConfig.remoteUrlNew) {
+ remoteUrl = cloneConfig.remoteUrlNew;
+ }
+
+ // do the clone
+ q = q.then(function () {
+ const tracker = ProgressDialog.newProgressTracker();
+ destPath = destPath ? fs.getTauriPlatformPath(destPath) : ".";
+ return ProgressDialog.show(Git.clone(remoteUrl, destPath, tracker), tracker);
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.GIT_CLONE_REMOTE_FAILED);
+ });
+
+ // restore original url if desired
+ if (cloneConfig.remoteUrlRestore) {
+ q = q.then(function () {
+ return Git.setRemoteUrl(cloneConfig.remote, cloneConfig.remoteUrlRestore);
+ });
+ }
+
+ return q.finally(function () {
+ EventEmitter.emit(Events.REFRESH_ALL);
+ });
+ }
+ if(gitCloneURL){
+ return _clone({
+ remote: "origin",
+ remoteUrlNew: gitCloneURL
+ });
+ }
+ CloneDialog.show().then(_clone).catch(function (err) {
+ // when dialog is cancelled, there's no error
+ if (err) { ErrorHandler.showError(err, Strings.GIT_CLONE_REMOTE_FAILED); }
+ });
+ }).catch(function (err) {
+ ErrorHandler.showError(err);
+ }).finally(function () {
+ $cloneButton.prop("disabled", false);
+ });
+ }
+
+ CommandManager.register(Strings.GIT_CLONE, Constants.CMD_GIT_CLONE_WITH_URL, handleGitClone);
+
+ // Event subscriptions
+ EventEmitter.on(Events.HANDLE_GIT_INIT, function () {
+ handleGitInit();
+ });
+ EventEmitter.on(Events.HANDLE_GIT_CLONE, function () {
+ handleGitClone();
+ });
+ EventEmitter.on(Events.GIT_NO_BRANCH_EXISTS, function () {
+ stageGitIgnore();
+ });
+
+});
diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js
new file mode 100644
index 0000000000..f3c1e78cb0
--- /dev/null
+++ b/src/extensions/default/Git/src/Panel.js
@@ -0,0 +1,1414 @@
+/*globals jsPromise, path*/
+
+define(function (require, exports) {
+
+ const _ = brackets.getModule("thirdparty/lodash"),
+ CodeInspection = brackets.getModule("language/CodeInspection"),
+ CommandManager = brackets.getModule("command/CommandManager"),
+ Commands = brackets.getModule("command/Commands"),
+ Dialogs = brackets.getModule("widgets/Dialogs"),
+ DocumentManager = brackets.getModule("document/DocumentManager"),
+ EditorManager = brackets.getModule("editor/EditorManager"),
+ FileViewController = brackets.getModule("project/FileViewController"),
+ FileSystem = brackets.getModule("filesystem/FileSystem"),
+ Menus = brackets.getModule("command/Menus"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache"),
+ FindInFiles = brackets.getModule("search/FindInFiles"),
+ WorkspaceManager = brackets.getModule("view/WorkspaceManager"),
+ ProjectManager = brackets.getModule("project/ProjectManager"),
+ StringUtils = brackets.getModule("utils/StringUtils"),
+ Strings = brackets.getModule("strings"),
+ Constants = require("src/Constants"),
+ Git = require("src/git/Git"),
+ Events = require("./Events"),
+ EventEmitter = require("./EventEmitter"),
+ Preferences = require("./Preferences"),
+ Setup = require("src/utils/Setup"),
+ ErrorHandler = require("./ErrorHandler"),
+ ExpectedError = require("./ExpectedError"),
+ Main = require("./Main"),
+ GutterManager = require("./GutterManager"),
+ Utils = require("src/Utils"),
+ ProgressDialog = require("src/dialogs/Progress");
+
+ const gitPanelTemplate = require("text!templates/git-panel.html"),
+ gitPanelResultsTemplate = require("text!templates/git-panel-results.html"),
+ gitAuthorsDialogTemplate = require("text!templates/authors-dialog.html"),
+ gitCommitDialogTemplate = require("text!templates/git-commit-dialog.html"),
+ gitCommitLintResultTemplate = require("text!templates/git-commit-dialog-lint-results.html"),
+ gitDiffDialogTemplate = require("text!templates/git-diff-dialog.html"),
+ questionDialogTemplate = require("text!templates/git-question-dialog.html");
+
+ var showFileWhiteList = /^\.gitignore$/;
+
+ const COMMIT_MODE = {
+ CURRENT: "CURRENT",
+ ALL: "ALL",
+ DEFAULT: "DEFAULT"
+ };
+
+ var gitPanel = null,
+ $gitPanel = $(null),
+ $mainToolbar,
+ gitPanelDisabled = null,
+ gitPanelMode = null,
+ showingUntracked = true,
+ $tableContainer = $(null),
+ lastCommitMessage = {};
+
+ function lintFile(filename) {
+ var fullPath = Preferences.get("currentGitRoot") + filename,
+ codeInspectionPromise;
+
+ try {
+ codeInspectionPromise = CodeInspection.inspectFile(FileSystem.getFileForPath(fullPath));
+ } catch (e) {
+ ErrorHandler.logError("CodeInspection.inspectFile failed to execute for file " + fullPath);
+ ErrorHandler.logError(e);
+ codeInspectionPromise = Promise.reject(e);
+ }
+
+ return jsPromise(codeInspectionPromise);
+ }
+
+ function _makeDialogBig($dialog) {
+ var $wrapper = $dialog.parents(".modal-wrapper").first();
+ if ($wrapper.length === 0) { return; }
+
+ $dialog
+ .width("80%")
+ .children(".modal-body")
+ .css("max-height", "72vh")
+ .end();
+ }
+
+ function _showCommitDialog(stagedDiff, prefilledMessage, commitMode, files) {
+ // Open the dialog
+ const compiledTemplate = Mustache.render(gitCommitDialogTemplate, {Strings: Strings}),
+ dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate),
+ $dialog = dialog.getElement();
+ inspectFiles(files, $dialog).then(function (lintResults) {
+ // Flatten the error structure from various providers
+ lintResults = lintResults || [];
+ lintResults.forEach(function (lintResult) {
+ lintResult.errors = [];
+ const lintingFilePath = path.join(ProjectManager.getProjectRoot().fullPath, lintResult.filename);
+ if (Array.isArray(lintResult.result)) {
+ lintResult.result.forEach(function (resultSet) {
+ if (!resultSet.result || !resultSet.result.errors) { return; }
+
+ var providerName = resultSet.provider.name;
+ resultSet.result.errors.forEach(function (e) {
+ lintResult.errors.push({
+ errorLineMessage: (e.pos.line + 1) + ": " + e.message + " (" + providerName + ")",
+ line: e.pos.line,
+ ch: e.pos.ch,
+ file: lintingFilePath
+ });
+ });
+ });
+ } else {
+ ErrorHandler.logError("[brackets-git] lintResults contain object in unexpected format: " + JSON.stringify(lintResult));
+ }
+ lintResult.hasErrors = lintResult.errors.length > 0;
+ });
+
+ // Filter out only results with errors to show
+ lintResults = _.filter(lintResults, function (lintResult) {
+ return lintResult.hasErrors;
+ });
+ const compiledResultHTML = Mustache.render(gitCommitLintResultTemplate, {
+ Strings: Strings,
+ lintResults: lintResults
+ });
+ if(!$dialog || !$dialog.is(":visible")) {
+ return;
+ }
+ $dialog.find(".accordion-title").html(Strings.CODE_INSPECTION_PROBLEMS);
+ if(!lintResults.length){
+ $dialog.find(".lint-errors").html(Strings.CODE_INSPECTION_PROBLEMS_NONE);
+ $dialog.find(".accordion").addClass("forced-hidden");
+ return;
+ }
+ $dialog.find(".lint-errors").html(compiledResultHTML);
+ if(!$dialog.find(".lint-errors").is(":visible")){
+ $dialog.find(".accordion-toggle").click();
+ }
+ $dialog.find(".lint-error-commit-link").click((e)=>{
+ e.preventDefault();
+ const $el = $(e.target);
+ const fileToOpen = $el.data("file"),
+ line = $el.data("line"),
+ ch = $el.data("ch");
+ CommandManager.execute(Commands.FILE_OPEN, {fullPath: fileToOpen})
+ .done(()=>{
+ EditorManager.getCurrentFullEditor().setCursorPos(line, ch, true);
+ });
+ dialog.close();
+ });
+ });
+
+ // We need bigger commit dialog
+ _makeDialogBig($dialog);
+
+ // Show nicely colored commit diff
+ $dialog.find(".commit-diff").append(Utils.formatDiff(stagedDiff));
+
+ // Enable / Disable amend checkbox
+ var toggleAmendCheckbox = function (bool) {
+ $dialog.find(".amend-commit")
+ .prop("disabled", !bool)
+ .parent()
+ .attr("title", !bool ? Strings.AMEND_COMMIT_FORBIDDEN : null);
+ };
+ toggleAmendCheckbox(false);
+
+ Git.getCommitCounts()
+ .then(function (commits) {
+ var hasRemote = $gitPanel.find(".git-selected-remote").data("remote") != null;
+ var hasCommitsAhead = commits.ahead > 0;
+ toggleAmendCheckbox(!hasRemote || hasRemote && hasCommitsAhead);
+ })
+ .catch(function (err) {
+ ErrorHandler.logError(err);
+ });
+
+ function getCommitMessageElement() {
+ var r = $dialog.find("[name='commit-message']:visible");
+ if (r.length !== 1) {
+ r = $dialog.find("[name='commit-message']");
+ for (var i = 0; i < r.length; i++) {
+ if ($(r[i]).css("display") !== "none") {
+ return $(r[i]);
+ }
+ }
+ }
+ return r;
+ }
+
+ var $commitMessageCount = $dialog.find("input[name='commit-message-count']");
+
+ // Add event to count characters in commit message
+ var recalculateMessageLength = function () {
+ var val = getCommitMessageElement().val().trim(),
+ length = val.length;
+
+ if (val.indexOf("\n")) {
+ // longest line
+ length = Math.max.apply(null, val.split("\n").map(function (l) { return l.length; }));
+ }
+
+ $commitMessageCount
+ .val(length)
+ .toggleClass("over50", length > 50 && length <= 100)
+ .toggleClass("over100", length > 100);
+ };
+
+ var usingTextArea = false;
+
+ // commit message handling
+ function switchCommitMessageElement() {
+ usingTextArea = !usingTextArea;
+
+ var findStr = "[name='commit-message']",
+ currentValue = $dialog.find(findStr + ":visible").val();
+ $dialog.find(findStr).toggle();
+ $dialog.find(findStr + ":visible")
+ .val(currentValue)
+ .focus();
+ recalculateMessageLength();
+ }
+
+ $dialog.find("button.primary").on("click", function (e) {
+ var $commitMessage = getCommitMessageElement();
+ if ($commitMessage.val().trim().length === 0) {
+ e.stopPropagation();
+ $commitMessage.addClass("invalid");
+ } else {
+ $commitMessage.removeClass("invalid");
+ }
+ });
+
+ $dialog.find("button.extendedCommit").on("click", function () {
+ switchCommitMessageElement();
+ // this value will be set only when manually triggered
+ Preferences.set("useTextAreaForCommitByDefault", usingTextArea);
+ });
+
+ function prefillMessage(msg) {
+ if (msg.indexOf("\n") !== -1 && !usingTextArea) {
+ switchCommitMessageElement();
+ }
+ $dialog.find("[name='commit-message']:visible").val(msg);
+ recalculateMessageLength();
+ }
+
+ // Assign action to amend checkbox
+ $dialog.find(".amend-commit").on("click", function () {
+ if ($(this).prop("checked") === false) {
+ prefillMessage("");
+ } else {
+ Git.getLastCommitMessage().then(function (msg) {
+ prefillMessage(msg);
+ });
+ }
+ });
+
+ if (Preferences.get("useTextAreaForCommitByDefault")) {
+ switchCommitMessageElement();
+ }
+
+ if (prefilledMessage) {
+ prefillMessage(prefilledMessage.trim());
+ }
+
+ // Add focus to commit message input
+ getCommitMessageElement().focus();
+
+ $dialog.find("[name='commit-message']")
+ .on("keyup", recalculateMessageLength)
+ .on("change", recalculateMessageLength);
+ recalculateMessageLength();
+
+ dialog.done(function (buttonId) {
+ const commitMessageElement = getCommitMessageElement();
+ if(commitMessageElement){
+ lastCommitMessage[ProjectManager.getProjectRoot().fullPath] = commitMessageElement.val();
+ }
+ if (buttonId === "ok") {
+ if (commitMode === COMMIT_MODE.ALL || commitMode === COMMIT_MODE.CURRENT) {
+ var filePaths = _.map(files, function (next) {
+ return next.file;
+ });
+ Git.stage(filePaths)
+ .then(function () {
+ return _getStagedDiff();
+ })
+ .then(function (diff) {
+ _doGitCommit($dialog, getCommitMessageElement, diff);
+ })
+ .catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_CANT_GET_STAGED_DIFF);
+ });
+ } else {
+ _doGitCommit($dialog, getCommitMessageElement, stagedDiff);
+ }
+ } else {
+ Git.status();
+ }
+ });
+ }
+
+ function _doGitCommit($dialog, getCommitMessageElement, stagedDiff) {
+ // this event won't launch when commit-message is empty so its safe to assume that it is not
+ var commitMessage = getCommitMessageElement().val(),
+ amendCommit = $dialog.find(".amend-commit").prop("checked"),
+ noVerify = $dialog.find(".commit-no-verify").prop("checked");
+
+ // if commit message is extended and has a newline, put an empty line after first line to separate subject and body
+ var s = commitMessage.split("\n");
+ if (s.length > 1 && s[1].trim() !== "") {
+ s.splice(1, 0, "");
+ }
+ commitMessage = s.join("\n");
+
+ // save lastCommitMessage in case the commit will fail
+ lastCommitMessage[ProjectManager.getProjectRoot().fullPath] = commitMessage;
+
+ // now we are going to be paranoid and we will check if some mofo didn't change our diff
+ _getStagedDiff().then(function (diff) {
+ if (diff === stagedDiff) {
+ const tracker = ProgressDialog.newProgressTracker();
+ return ProgressDialog.show(Git.commit(commitMessage, amendCommit, noVerify, tracker),
+ tracker, {
+ title: Strings.GIT_COMMIT_IN_PROGRESS,
+ options: { preDelay: 1, postDelay: 1 }
+ })
+ .then(function () {
+ // clear lastCommitMessage because the commit was successful
+ lastCommitMessage[ProjectManager.getProjectRoot().fullPath] = null;
+ });
+ } else {
+ throw new ExpectedError(Strings.ERROR_MODIFIED_DIALOG_FILES);
+ }
+ }).catch(function (err) {
+ if (ErrorHandler.contains(err, "Please tell me who you are")) {
+ return new Promise((resolve)=>{
+ EventEmitter.emit(Events.GIT_CHANGE_USERNAME, function () {
+ EventEmitter.emit(Events.GIT_CHANGE_EMAIL, function () {
+ resolve();
+ });
+ });
+ });
+ }
+
+ ErrorHandler.showError(err, Strings.ERROR_GIT_COMMIT_FAILED);
+
+ }).finally(function () {
+ EventEmitter.emit(Events.GIT_COMMITED);
+ refresh();
+ });
+ }
+
+ function _showAuthors(file, blame, fromLine, toLine) {
+ var linesTotal = blame.length;
+ var blameStats = blame.reduce(function (stats, lineInfo) {
+ var name = lineInfo.author + " " + lineInfo["author-mail"];
+ if (stats[name]) {
+ stats[name] += 1;
+ } else {
+ stats[name] = 1;
+ }
+ return stats;
+ }, {});
+ blameStats = _.reduce(blameStats, function (arr, val, key) {
+ arr.push({
+ authorName: key,
+ lines: val,
+ percentage: Math.round(val / (linesTotal / 100))
+ });
+ return arr;
+ }, []);
+ blameStats = _.sortBy(blameStats, "lines").reverse();
+
+ if (fromLine || toLine) {
+ file += " (" + Strings.LINES + " " + fromLine + "-" + toLine + ")";
+ }
+
+ var compiledTemplate = Mustache.render(gitAuthorsDialogTemplate, {
+ file: file,
+ blameStats: blameStats,
+ Strings: Strings
+ });
+ Dialogs.showModalDialogUsingTemplate(compiledTemplate);
+ }
+
+ function _getCurrentFilePath(editor) {
+ var gitRoot = Preferences.get("currentGitRoot"),
+ document = editor ? editor.document : DocumentManager.getCurrentDocument(),
+ filePath = document.file.fullPath;
+ if (filePath.indexOf(gitRoot) === 0) {
+ filePath = filePath.substring(gitRoot.length);
+ }
+ return filePath;
+ }
+
+ function handleAuthorsSelection() {
+ var editor = EditorManager.getActiveEditor(),
+ filePath = _getCurrentFilePath(editor),
+ currentSelection = editor.getSelection(),
+ fromLine = currentSelection.start.line + 1,
+ toLine = currentSelection.end.line + 1;
+
+ // fix when nothing is selected on that line
+ if (currentSelection.end.ch === 0) { toLine = toLine - 1; }
+
+ var isSomethingSelected = currentSelection.start.line !== currentSelection.end.line ||
+ currentSelection.start.ch !== currentSelection.end.ch;
+ if (!isSomethingSelected) {
+ ErrorHandler.showError(new ExpectedError(Strings.ERROR_NOTHING_SELECTED));
+ return;
+ }
+
+ if (editor.document.isDirty) {
+ ErrorHandler.showError(new ExpectedError(Strings.ERROR_SAVE_FIRST));
+ return;
+ }
+
+ Git.getBlame(filePath, fromLine, toLine).then(function (blame) {
+ return _showAuthors(filePath, blame, fromLine, toLine);
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GIT_BLAME_FAILED);
+ });
+ }
+
+ function handleAuthorsFile() {
+ var filePath = _getCurrentFilePath();
+ Git.getBlame(filePath).then(function (blame) {
+ return _showAuthors(filePath, blame);
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GIT_BLAME_FAILED);
+ });
+ }
+
+ function handleGitDiff(file) {
+ if (Preferences.get("useDifftool")) {
+ Git.difftool(file);
+ } else {
+ Git.diffFileNice(file).then(function (diff) {
+ // show the dialog with the diff
+ var compiledTemplate = Mustache.render(gitDiffDialogTemplate, { file: file, Strings: Strings }),
+ dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate),
+ $dialog = dialog.getElement();
+ _makeDialogBig($dialog);
+ $dialog.find(".commit-diff").append(Utils.formatDiff(diff));
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GIT_DIFF_FAILED);
+ });
+ }
+ }
+
+ function handleGitUndo(file) {
+ var compiledTemplate = Mustache.render(questionDialogTemplate, {
+ title: Strings.UNDO_CHANGES,
+ question: StringUtils.format(Strings.Q_UNDO_CHANGES, _.escape(file)),
+ Strings: Strings
+ });
+ Dialogs.showModalDialogUsingTemplate(compiledTemplate).done(function (buttonId) {
+ if (buttonId === "ok") {
+ Git.discardFileChanges(file).then(function () {
+ var gitRoot = Preferences.get("currentGitRoot");
+ DocumentManager.getAllOpenDocuments().forEach(function (doc) {
+ if (doc.file.fullPath === gitRoot + file) {
+ Utils.reloadDoc(doc);
+ }
+ });
+ refresh();
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_DISCARD_CHANGES_FAILED);
+ });
+ }
+ });
+ }
+
+ function handleGitDelete(file) {
+ FileSystem.resolve(Preferences.get("currentGitRoot") + file, function (err, fileEntry) {
+ if (err) {
+ ErrorHandler.showError(err, Strings.ERROR_COULD_NOT_RESOLVE_FILE);
+ return;
+ }
+ CommandManager.execute(Commands.FILE_DELETE, {file: fileEntry});
+ });
+ }
+
+ function _getStagedDiff(commitMode, files = []) {
+ const tracker = ProgressDialog.newProgressTracker();
+ const fileNamesString = files.map(file => file.file).join(", ");
+ return ProgressDialog.show(_getStagedDiffForCommitMode(commitMode, files), tracker, {
+ title: Strings.GETTING_STAGED_DIFF_PROGRESS,
+ initialMessage: `${fileNamesString}\n${Strings.PLEASE_WAIT}`,
+ options: { preDelay: 3, postDelay: 1 }
+ })
+ .catch(function (err) {
+ if (ErrorHandler.contains(err, "cleanup")) {
+ return false; // will display list of staged files instead
+ }
+ throw err;
+ })
+ .then(function (diff) {
+ if (!diff) {
+ return Git.getListOfStagedFiles().then(function (filesList) {
+ return Strings.DIFF_FAILED_SEE_FILES + "\n\n" + filesList;
+ });
+ }
+ return diff;
+ });
+ }
+
+ function _getStagedDiffForCommitMode(commitMode, files) {
+
+ if (commitMode === COMMIT_MODE.ALL) {
+ return _getStaggedDiffForAllFiles();
+ }
+
+ if (commitMode === COMMIT_MODE.CURRENT && _.isArray(files)) {
+ if (files.length > 1) {
+ return Promise.reject("_getStagedDiffForCommitMode() got files.length > 1");
+ }
+
+ var isUntracked = files[0].status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1;
+ if (isUntracked) {
+ return _getDiffForUntrackedFiles(files[0].file);
+ } else {
+ return Git.getDiffOfAllIndexFiles(files[0].file);
+ }
+ }
+
+ return Git.getDiffOfStagedFiles();
+ }
+
+ function _getStaggedDiffForAllFiles() {
+ return Git.status().then(function (statusFiles) {
+ var untrackedFiles = [];
+ var fileArray = [];
+
+ statusFiles.forEach(function (fileObject) {
+ var isUntracked = fileObject.status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1;
+ if (isUntracked) {
+ untrackedFiles.push(fileObject.file);
+ } else {
+ fileArray.push(fileObject.file);
+ }
+ });
+
+ if (untrackedFiles.length > 0) {
+ return _getDiffForUntrackedFiles(fileArray.concat(untrackedFiles));
+ } else {
+ return Git.getDiffOfAllIndexFiles(fileArray);
+ }
+ });
+ }
+
+ function _getDiffForUntrackedFiles(files) {
+ var diff;
+ return Git.stage(files, false)
+ .then(function () {
+ return Git.getDiffOfStagedFiles();
+ })
+ .then(function (_diff) {
+ diff = _diff;
+ return Git.resetIndex();
+ })
+ .then(function () {
+ return diff;
+ });
+ }
+
+ // whatToDo gets values "continue" "skip" "abort"
+ function handleRebase(whatToDo) {
+ Git.rebase(whatToDo).then(function () {
+ EventEmitter.emit(Events.REFRESH_ALL);
+ }).catch(function (err) {
+ ErrorHandler.showError(err, "Rebase " + whatToDo + " failed");
+ });
+ }
+
+ function abortMerge() {
+ Git.discardAllChanges().then(function () {
+ EventEmitter.emit(Events.REFRESH_ALL);
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_MERGE_ABORT_FAILED);
+ });
+ }
+
+ function findConflicts() {
+ FindInFiles.doSearch(/^<<<<<<<\s|^=======\s|^>>>>>>>\s/gm);
+ }
+
+ function commitMerge() {
+ Utils.loadPathContent(Preferences.get("currentGitRoot") + "/.git/MERGE_MSG").then(function (msg) {
+ handleGitCommit(msg, true, COMMIT_MODE.DEFAULT);
+ EventEmitter.once(Events.GIT_COMMITED, function () {
+ EventEmitter.emit(Events.REFRESH_ALL);
+ });
+ }).catch(function (err) {
+ ErrorHandler.showError(err, "Merge commit failed");
+ });
+ }
+
+ function inspectFiles(gitStatusResults, $dialog) {
+ const lintResults = [];
+ let totalFiles = gitStatusResults.length,
+ filesDone = 0;
+
+ const codeInspectionPromises = gitStatusResults.map(function (fileObj) {
+ const isDeleted = fileObj.status.indexOf(Git.FILE_STATUS.DELETED) !== -1;
+
+ // Do a code inspection for the file, if it was not deleted
+ if (!isDeleted) {
+ return new Promise((resolve) => {
+ // Delay lintFile execution to give the event loop some breathing room
+ setTimeout(() => {
+ lintFile(fileObj.file)
+ .catch(function () {
+ return [
+ {
+ provider: { name: "See console [F12] for details" },
+ result: {
+ errors: [
+ {
+ pos: { line: 0, ch: 0 },
+ message: "CodeInspection failed to execute for this file."
+ }
+ ]
+ }
+ }
+ ];
+ })
+ .then(function (result) {
+ if (result) {
+ lintResults.push({
+ filename: fileObj.file,
+ result: result
+ });
+ }
+ resolve();
+ }).finally(()=>{
+ filesDone++;
+ const $progressBar = $dialog.find('.accordion-progress-bar-inner');
+ if ($progressBar.length) {
+ $progressBar[0].style.width = `${filesDone/totalFiles*100}%`;
+ }
+ if(filesDone === totalFiles){
+ $dialog.find('.accordion-progress-bar').addClass("forced-inVisible");
+ }
+ const progressString = StringUtils.format(Strings.CODE_INSPECTION_DONE_FILES, filesDone, totalFiles);
+ $dialog.find(".lint-errors").html(progressString);
+
+ });
+ }, 0); // Delay of 0ms to defer to the next tick of the event loop
+ });
+ }
+ });
+
+ return Promise.all(_.compact(codeInspectionPromises)).then(function () {
+ return lintResults;
+ });
+ }
+
+
+ function handleGitCommit(prefilledMessage, isMerge, commitMode) {
+ if(Utils.isLoading($gitPanel.find(".git-commit"))){
+ return;
+ }
+
+ var stripWhitespace = Preferences.get("stripWhitespaceFromCommits");
+
+ // Disable button (it will be enabled when selecting files after reset)
+ Utils.setLoading($gitPanel.find(".git-commit"));
+
+ var p;
+
+ // First reset staged files, then add selected files to the index.
+ if (commitMode === COMMIT_MODE.DEFAULT) {
+ p = Git.status().then(function (files) {
+ files = _.filter(files, function (file) {
+ return file.status.indexOf(Git.FILE_STATUS.STAGED) !== -1;
+ });
+
+ if (files.length === 0 && !isMerge) {
+ return ErrorHandler.showError(
+ new Error("Commit button should have been disabled"),
+ "Nothing staged to commit"
+ );
+ }
+
+ return handleGitCommitInternal(stripWhitespace,
+ files,
+ commitMode,
+ prefilledMessage);
+ });
+ } else if (commitMode === COMMIT_MODE.ALL) {
+ p = Git.status().then(function (files) {
+ return handleGitCommitInternal(stripWhitespace,
+ files,
+ commitMode,
+ prefilledMessage);
+ });
+ } else if (commitMode === COMMIT_MODE.CURRENT) {
+ p = Git.status().then(function (files) {
+ var gitRoot = Preferences.get("currentGitRoot");
+ var currentDoc = DocumentManager.getCurrentDocument();
+ if (currentDoc) {
+ var relativePath = currentDoc.file.fullPath.substring(gitRoot.length);
+ var currentFile = _.filter(files, function (next) {
+ return relativePath === next.file;
+ });
+ return handleGitCommitInternal(stripWhitespace, currentFile, commitMode, prefilledMessage);
+ }
+ });
+ }
+
+ p.catch(function (err) {
+ ErrorHandler.showError(err, "Preparing commit dialog failed");
+ }).finally(function () {
+ Utils.unsetLoading($gitPanel.find(".git-commit"));
+ });
+
+ }
+
+ function handleGitCommitInternal(stripWhitespace, files, commitMode, prefilledMessage) {
+ let queue = Promise.resolve();
+
+ if (stripWhitespace) {
+ queue = queue.then(function () {
+ const tracker = ProgressDialog.newProgressTracker();
+ return ProgressDialog.show(
+ Utils.stripWhitespaceFromFiles(files, commitMode === COMMIT_MODE.DEFAULT, tracker),
+ tracker, {
+ title: Strings.CLEANING_WHITESPACE_PROGRESS,
+ options: { preDelay: 3, postDelay: 1 }
+ }
+ );
+ });
+ }
+
+ return queue.then(function () {
+ // All files are in the index now, get the diff and show dialog.
+ return _getStagedDiff(commitMode, files).then(function (diff) {
+ return _showCommitDialog(diff, prefilledMessage, commitMode, files);
+ });
+ });
+ }
+
+ function refreshCurrentFile() {
+ var gitRoot = Preferences.get("currentGitRoot");
+ var currentDoc = DocumentManager.getCurrentDocument();
+ if (currentDoc) {
+ $gitPanel.find("tr").each(function () {
+ var currentFullPath = currentDoc.file.fullPath,
+ thisFile = $(this).attr("x-file");
+ $(this).toggleClass("selected", gitRoot + thisFile === currentFullPath);
+ });
+ } else {
+ $gitPanel.find("tr").removeClass("selected");
+ }
+ }
+
+ function shouldShow(fileObj) {
+ if (showFileWhiteList.test(fileObj.name)) {
+ return true;
+ }
+ return ProjectManager.shouldShow(fileObj);
+ }
+
+ function _refreshTableContainer(files) {
+ if (!gitPanel.isVisible()) {
+ return;
+ }
+
+ // remove files that we should not show
+ files = _.filter(files, function (file) {
+ return shouldShow(file);
+ });
+
+ var allStaged = files.length > 0 && _.all(files, function (file) { return file.status.indexOf(Git.FILE_STATUS.STAGED) !== -1; });
+ $gitPanel.find(".check-all").prop("checked", allStaged).prop("disabled", files.length === 0);
+
+ var $editedList = $tableContainer.find(".git-edited-list");
+ var visibleBefore = $editedList.length ? $editedList.is(":visible") : true;
+ $editedList.remove();
+
+ if (files.length === 0) {
+ $tableContainer.append($("
").text(Strings.NOTHING_TO_COMMIT));
+ } else {
+ // if desired, remove untracked files from the results
+ if (showingUntracked === false) {
+ files = _.filter(files, function (file) {
+ return file.status.indexOf(Git.FILE_STATUS.UNTRACKED) === -1;
+ });
+ }
+ // -
+ files.forEach(function (file) {
+ file.staged = file.status.indexOf(Git.FILE_STATUS.STAGED) !== -1;
+ file.statusText = file.status.map(function (status) {
+ return Strings["FILE_" + status];
+ }).join(", ");
+ file.allowDiff = file.status.indexOf(Git.FILE_STATUS.UNTRACKED) === -1 &&
+ file.status.indexOf(Git.FILE_STATUS.RENAMED) === -1 &&
+ file.status.indexOf(Git.FILE_STATUS.DELETED) === -1;
+ file.allowDelete = file.status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1 ||
+ file.status.indexOf(Git.FILE_STATUS.STAGED) !== -1 &&
+ file.status.indexOf(Git.FILE_STATUS.ADDED) !== -1;
+ file.allowUndo = !file.allowDelete;
+ });
+ $tableContainer.append(Mustache.render(gitPanelResultsTemplate, {
+ files: files,
+ Strings: Strings
+ }));
+
+ refreshCurrentFile();
+ }
+ $tableContainer.find(".git-edited-list").toggle(visibleBefore);
+ }
+
+ function _setName(commandID, newName) {
+ const command = CommandManager.get(commandID);
+ if (command) {
+ command.setName(newName);
+ }
+ }
+
+ function refreshCommitCounts() {
+ // Find Push and Pull buttons
+ var $pullBtn = $gitPanel.find(".git-pull");
+ var $pushBtn = $gitPanel.find(".git-push");
+ var clearCounts = function () {
+ $pullBtn.children("span").remove();
+ $pushBtn.children("span").remove();
+ _setName(Constants.CMD_GIT_PULL, Strings.PULL_SHORTCUT);
+ _setName(Constants.CMD_GIT_PUSH, Strings.PUSH_SHORTCUT);
+ };
+
+ // Check if there's a remote, resolve if there's not
+ var remotes = Preferences.get("defaultRemotes") || {};
+ var defaultRemote = remotes[Preferences.get("currentGitRoot")];
+ if (!defaultRemote) {
+ clearCounts();
+ return Promise.resolve();
+ }
+
+ // Get the commit counts and append them to the buttons
+ return Git.getCommitCounts().then(function (commits) {
+ clearCounts();
+ if (commits.behind > 0) {
+ $pullBtn.append($("
").text(" (" + commits.behind + ")"));
+ _setName(Constants.CMD_GIT_PULL,
+ StringUtils.format(Strings.PULL_SHORTCUT_BEHIND, commits.behind));
+ }
+ if (commits.ahead > 0) {
+ $pushBtn.append($("
").text(" (" + commits.ahead + ")"));
+ _setName(Constants.CMD_GIT_PUSH,
+ StringUtils.format(Strings.PUSH_SHORTCUT_AHEAD, commits.ahead));
+ }
+ }).catch(function (err) {
+ clearCounts();
+ ErrorHandler.logError(err);
+ });
+ }
+
+ function refresh() {
+ // set the history panel to false and remove the class that show the button history active when refresh
+ $gitPanel.find(".git-history-toggle").removeClass("active").attr("title", Strings.TOOLTIP_SHOW_HISTORY);
+ $gitPanel.find(".git-file-history").removeClass("active").attr("title", Strings.TOOLTIP_SHOW_FILE_HISTORY);
+
+ if (gitPanelMode === "not-repo") {
+ $tableContainer.empty();
+ return Promise.resolve();
+ }
+
+ $tableContainer.find("#git-history-list").remove();
+ $tableContainer.find(".git-edited-list").show();
+
+ var p1 = Git.status().catch(function (err) {
+ // this is an expected "error"
+ if (ErrorHandler.contains(err, "Not a git repository")) {
+ return;
+ }
+ });
+
+ var p2 = refreshCommitCounts();
+
+ // Clone button
+ $gitPanel.find(".git-clone").prop("disabled", false);
+
+ // FUTURE: who listens for this?
+ return Promise.all([p1, p2]);
+ }
+
+ function toggle(bool) {
+ if (gitPanelDisabled === true) {
+ return;
+ }
+ if (typeof bool !== "boolean") {
+ bool = !gitPanel.isVisible();
+ }
+ Preferences.set("panelEnabled", bool);
+ Main.$icon.toggleClass("on", bool);
+ Main.$icon.toggleClass("selected-button", bool);
+ gitPanel.setVisible(bool);
+
+ // Mark menu item as enabled/disabled.
+ CommandManager.get(Constants.CMD_GIT_TOGGLE_PANEL).setChecked(bool);
+
+ if (bool) {
+ $("#git-toolbar-icon").removeClass("forced-hidden");
+ refresh();
+ }
+ }
+
+ function handleToggleUntracked() {
+ showingUntracked = !showingUntracked;
+ const command = CommandManager.get(Constants.CMD_GIT_TOGGLE_UNTRACKED);
+ if (command) {
+ command.setChecked(!showingUntracked);
+ }
+
+ refresh();
+ }
+
+ function commitCurrentFile() {
+ // do not return anything here, core expects jquery promise
+ jsPromise(CommandManager.execute("file.save"))
+ .then(function () {
+ return Git.resetIndex();
+ })
+ .then(function () {
+ return handleGitCommit(lastCommitMessage[ProjectManager.getProjectRoot().fullPath], false, COMMIT_MODE.CURRENT);
+ });
+ }
+
+ function commitAllFiles() {
+ // do not return anything here, core expects jquery promise
+ jsPromise(CommandManager.execute("file.saveAll"))
+ .then(function () {
+ return Git.resetIndex();
+ })
+ .then(function () {
+ return handleGitCommit(lastCommitMessage[ProjectManager.getProjectRoot().fullPath], false, COMMIT_MODE.ALL);
+ });
+ }
+
+ // Disable "commit" button if there aren't staged files to commit
+ function _toggleCommitButton(files) {
+ var anyStaged = _.any(files, function (file) { return file.status.indexOf(Git.FILE_STATUS.STAGED) !== -1; });
+ $gitPanel.find(".git-commit").prop("disabled", !anyStaged);
+ }
+
+ EventEmitter.on(Events.GIT_STATUS_RESULTS, function (results) {
+ _refreshTableContainer(results);
+ _toggleCommitButton(results);
+ });
+
+ function undoLastLocalCommit() {
+ return Utils.askQuestion(Strings.UNDO_COMMIT, Strings.UNDO_LOCAL_COMMIT_CONFIRM, {booleanResponse: true})
+ .then(function (response) {
+ if (response) {
+ Git.undoLastLocalCommit()
+ .catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_UNDO_LAST_COMMIT_FAILED);
+ })
+ .finally(function () {
+ refresh();
+ });
+ }
+ });
+ }
+
+ var lastCheckOneClicked = null;
+
+ function attachDefaultTableHandlers() {
+ $tableContainer = $gitPanel.find(".table-container")
+ .off()
+ .on("click", ".check-one", function (e) {
+ e.stopPropagation();
+ var $tr = $(this).closest("tr"),
+ file = $tr.attr("x-file"),
+ status = $tr.attr("x-status"),
+ isChecked = $(this).is(":checked");
+
+ if (e.shiftKey) {
+ // stage/unstage all file between
+ var lc = lastCheckOneClicked.localeCompare(file),
+ lcClickedSelector = "[x-file='" + lastCheckOneClicked + "']",
+ sequence;
+
+ if (lc < 0) {
+ sequence = $tr.prevUntil(lcClickedSelector).andSelf();
+ } else if (lc > 0) {
+ sequence = $tr.nextUntil(lcClickedSelector).andSelf();
+ }
+
+ if (sequence) {
+ sequence = sequence.add($tr.parent().children(lcClickedSelector));
+ var promises = sequence.map(function () {
+ var $this = $(this),
+ method = isChecked ? "stage" : "unstage",
+ file = $this.attr("x-file"),
+ status = $this.attr("x-status");
+ return Git[method](file, status === Git.FILE_STATUS.DELETED);
+ }).toArray();
+ return Promise.all(promises).then(function () {
+ return Git.status();
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_MODIFY_FILE_STATUS_FAILED);
+ });
+ }
+ }
+
+ lastCheckOneClicked = file;
+
+ if (isChecked) {
+ Git.stage(file, status === Git.FILE_STATUS.DELETED).then(function () {
+ Git.status();
+ });
+ } else {
+ Git.unstage(file).then(function () {
+ Git.status();
+ });
+ }
+ })
+ .on("dblclick", ".check-one", function (e) {
+ e.stopPropagation();
+ })
+ .on("click", ".btn-git-diff", function (e) {
+ e.stopPropagation();
+ handleGitDiff($(e.target).closest("tr").attr("x-file"));
+ })
+ .on("click", ".btn-git-undo", function (e) {
+ e.stopPropagation();
+ handleGitUndo($(e.target).closest("tr").attr("x-file"));
+ })
+ .on("click", ".btn-git-delete", function (e) {
+ e.stopPropagation();
+ handleGitDelete($(e.target).closest("tr").attr("x-file"));
+ })
+ .on("mousedown", ".modified-file", function (e) {
+ var $this = $(e.currentTarget);
+ // we listen on mousedown event for faster file switch perception. but this results in
+ // this handler getting triggered before the above click handlers for table buttons and
+ // Check boxes. So we do a check to see if the clicked element is NOT a button,
+ // input, or tag inside a button.
+ if ($(e.target).is("button, input") || $(e.target).closest("button").length) {
+ return;
+ }
+ if ($this.attr("x-status") === Git.FILE_STATUS.DELETED) {
+ return;
+ }
+ CommandManager.execute(Commands.FILE_OPEN, {
+ fullPath: Preferences.get("currentGitRoot") + $this.attr("x-file")
+ });
+ })
+ .on("dblclick", ".modified-file", function (e) {
+ var $this = $(e.currentTarget);
+ if ($this.attr("x-status") === Git.FILE_STATUS.DELETED) {
+ return;
+ }
+ FileViewController.addToWorkingSetAndSelect(Preferences.get("currentGitRoot") + $this.attr("x-file"));
+ });
+
+ }
+
+ EventEmitter.on(Events.GIT_CHANGE_USERNAME, function (callback) {
+ return Git.getConfig("user.name").then(function (currentUserName) {
+ return Utils.askQuestion(Strings.CHANGE_USER_NAME_TITLE, Strings.ENTER_NEW_USER_NAME, { defaultValue: currentUserName })
+ .then(function (userName) {
+ if (!userName.length) { userName = currentUserName; }
+ return Git.setConfig("user.name", userName, true).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_CHANGE_USERNAME_FAILED);
+ }).then(function () {
+ EventEmitter.emit(Events.GIT_USERNAME_CHANGED, userName);
+ }).finally(function () {
+ if (callback) {
+ callback(userName);
+ }
+ });
+ });
+ });
+ });
+
+ EventEmitter.on(Events.GIT_CHANGE_EMAIL, function (callback) {
+ return Git.getConfig("user.email").then(function (currentUserEmail) {
+ return Utils.askQuestion(Strings.CHANGE_USER_EMAIL_TITLE, Strings.ENTER_NEW_USER_EMAIL, { defaultValue: currentUserEmail })
+ .then(function (userEmail) {
+ if (!userEmail.length) { userEmail = currentUserEmail; }
+ return Git.setConfig("user.email", userEmail, true).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_CHANGE_EMAIL_FAILED);
+ }).then(function () {
+ EventEmitter.emit(Events.GIT_EMAIL_CHANGED, userEmail);
+ }).finally(function () {
+ if (callback) {
+ callback(userEmail);
+ }
+ });
+ });
+ });
+ });
+
+ EventEmitter.on(Events.GERRIT_TOGGLE_PUSH_REF, function () {
+ // update preference and emit so the menu item updates
+ return Git.getConfig("gerrit.pushref").then(function (strEnabled) {
+ var toggledValue = strEnabled !== "true";
+
+ // Set the global preference
+ // Saving a preference to tell the GitCli.push() method to check for gerrit push ref enablement
+ // so we don't slow down people who aren't using gerrit.
+ Preferences.set("gerritPushref", toggledValue);
+
+ return Git.setConfig("gerrit.pushref", toggledValue, true)
+ .then(function () {
+ EventEmitter.emit(Events.GERRIT_PUSH_REF_TOGGLED, toggledValue);
+ });
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_TOGGLE_GERRIT_PUSH_REF_FAILED);
+ });
+ });
+
+ EventEmitter.on(Events.GERRIT_PUSH_REF_TOGGLED, function (enabled) {
+ setGerritCheckState(enabled);
+ });
+
+ function setGerritCheckState(enabled) {
+ const command = CommandManager.get(Constants.CMD_GIT_GERRIT_PUSH_REF);
+ if (command) {
+ command.setChecked(enabled);
+ }
+ }
+
+ function discardAllChanges() {
+ return Utils.askQuestion(Strings.RESET_LOCAL_REPO, Strings.RESET_LOCAL_REPO_CONFIRM, {
+ booleanResponse: true, customOkBtn: Strings.DISCARD_CHANGES, customOkBtnClass: "danger"})
+ .then(function (response) {
+ if (response) {
+ return Git.discardAllChanges().catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_RESET_LOCAL_REPO_FAILED);
+ }).then(function () {
+ refresh();
+ });
+ }
+ });
+ }
+
+ /**
+ * Retrieves the hash of the selected history commit in the panel. if panel not visible
+ * or if there is no selection, returns null.
+ *
+ * @returns {{hash: string, subject: string}|{}} The `hash` value and commit string
+ * of the selected history commit if visible, otherwise {}.
+ */
+ function getSelectedHistoryCommit() {
+ const $historyRow = $(".history-commit.selected");
+ if($historyRow.is(":visible")){
+ return {
+ hash: $historyRow.attr("x-hash"),
+ subject: $historyRow.find(".commit-subject").text()
+ };
+ }
+ return {};
+ }
+
+ function _panelResized(_entries) {
+ if(!$mainToolbar || !$mainToolbar.is(":visible")){
+ return;
+ }
+ const mainToolbarWidth = $mainToolbar.width();
+ let overFlowWidth = 565;
+ const breakpoints = [
+ { width: overFlowWidth, className: "hide-when-small" },
+ { width: 400, className: "hide-when-x-small" }
+ ];
+
+ if(mainToolbarWidth < overFlowWidth) {
+ $gitPanel.find(".mainToolbar").addClass("hide-overflow");
+ } else {
+ $gitPanel.find(".mainToolbar").removeClass("hide-overflow");
+ }
+ breakpoints.forEach(bp => {
+ if (mainToolbarWidth < bp.width) {
+ $gitPanel.find(`.${bp.className}`).addClass("forced-hidden");
+ } else {
+ $gitPanel.find(`.${bp.className}`).removeClass("forced-hidden");
+ }
+ });
+ }
+
+ function init() {
+ // Add panel
+ var panelHtml = Mustache.render(gitPanelTemplate, {
+ S: Strings
+ });
+ var $panelHtml = $(panelHtml);
+ $panelHtml.find(".git-available, .git-not-available").hide();
+
+ gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100);
+ $gitPanel = gitPanel.$panel;
+ const resizeObserver = new ResizeObserver(_panelResized);
+ resizeObserver.observe($gitPanel[0]);
+ $mainToolbar = $gitPanel.find(".mainToolbar");
+ $gitPanel
+ .on("click", ".close", toggle)
+ .on("click", ".check-all", function () {
+ if ($(this).is(":checked")) {
+ return Git.stageAll().then(function () {
+ Git.status();
+ });
+ } else {
+ return Git.resetIndex().then(function () {
+ Git.status();
+ });
+ }
+ })
+ .on("click", ".git-refresh", EventEmitter.getEmitter(Events.REFRESH_ALL))
+ .on("click", ".git-commit", EventEmitter.getEmitter(Events.HANDLE_GIT_COMMIT))
+ .on("click", ".git-rebase-continue", function (e) { handleRebase("continue", e); })
+ .on("click", ".git-rebase-skip", function (e) { handleRebase("skip", e); })
+ .on("click", ".git-rebase-abort", function (e) { handleRebase("abort", e); })
+ .on("click", ".git-commit-merge", commitMerge)
+ .on("click", ".git-merge-abort", abortMerge)
+ .on("click", ".git-find-conflicts", findConflicts)
+ .on("click", ".git-prev-gutter", GutterManager.goToPrev)
+ .on("click", ".git-next-gutter", GutterManager.goToNext)
+ .on("click", ".git-file-history", EventEmitter.getEmitter(Events.HISTORY_SHOW_FILE))
+ .on("click", ".git-history-toggle", EventEmitter.getEmitter(Events.HISTORY_SHOW_GLOBAL))
+ .on("click", ".git-fetch", EventEmitter.getEmitter(Events.HANDLE_FETCH))
+ .on("click", ".git-push", function () {
+ var typeOfRemote = $(this).attr("x-selected-remote-type");
+ if (typeOfRemote === "git") {
+ EventEmitter.emit(Events.HANDLE_PUSH);
+ }
+ })
+ .on("click", ".git-pull", EventEmitter.getEmitter(Events.HANDLE_PULL))
+ .on("click", ".git-init", EventEmitter.getEmitter(Events.HANDLE_GIT_INIT))
+ .on("click", ".git-clone", EventEmitter.getEmitter(Events.HANDLE_GIT_CLONE))
+ .on("click", ".change-remote", EventEmitter.getEmitter(Events.HANDLE_REMOTE_PICK))
+ .on("click", ".remove-remote", EventEmitter.getEmitter(Events.HANDLE_REMOTE_DELETE))
+ .on("click", ".git-remote-new", EventEmitter.getEmitter(Events.HANDLE_REMOTE_CREATE))
+ .on("contextmenu", "tr", function (e) {
+ const $this = $(this);
+ if ($this.hasClass("history-commit")) {
+ if(!$this.hasClass("selected")){
+ $this.click();
+ }
+ Menus.getContextMenu(Constants.GIT_PANEL_HISTORY_CMENU).open(e);
+ return;
+ }
+
+ $this.click();
+ setTimeout(function () {
+ Menus.getContextMenu(Constants.GIT_PANEL_CHANGES_CMENU).open(e);
+ }, 1);
+ });
+
+ // Attaching table handlers
+ attachDefaultTableHandlers();
+
+ // Add command to menu.
+ CommandManager.register(Strings.PANEL_COMMAND, Constants.CMD_GIT_TOGGLE_PANEL, toggle);
+ CommandManager.register(Strings.COMMIT_CURRENT_SHORTCUT, Constants.CMD_GIT_COMMIT_CURRENT, commitCurrentFile);
+ CommandManager.register(Strings.COMMIT_ALL_SHORTCUT, Constants.CMD_GIT_COMMIT_ALL, commitAllFiles);
+ CommandManager.register(Strings.PUSH_SHORTCUT, Constants.CMD_GIT_PUSH, EventEmitter.getEmitter(Events.HANDLE_PUSH));
+ CommandManager.register(Strings.PULL_SHORTCUT, Constants.CMD_GIT_PULL, EventEmitter.getEmitter(Events.HANDLE_PULL));
+ CommandManager.register(Strings.FETCH_SHORTCUT, Constants.CMD_GIT_FETCH, EventEmitter.getEmitter(Events.HANDLE_FETCH));
+ CommandManager.register(Strings.GOTO_PREVIOUS_GIT_CHANGE, Constants.CMD_GIT_GOTO_PREVIOUS_CHANGE, GutterManager.goToPrev);
+ CommandManager.register(Strings.GOTO_NEXT_GIT_CHANGE, Constants.CMD_GIT_GOTO_NEXT_CHANGE, GutterManager.goToNext);
+ CommandManager.register(Strings.REFRESH_GIT, Constants.CMD_GIT_REFRESH, EventEmitter.getEmitter(Events.REFRESH_ALL));
+ CommandManager.register(Strings.RESET_LOCAL_REPO, Constants.CMD_GIT_DISCARD_ALL_CHANGES, discardAllChanges);
+ CommandManager.register(Strings.UNDO_LAST_LOCAL_COMMIT, Constants.CMD_GIT_UNDO_LAST_COMMIT, undoLastLocalCommit);
+ CommandManager.register(Strings.CHANGE_USER_NAME, Constants.CMD_GIT_CHANGE_USERNAME, EventEmitter.getEmitter(Events.GIT_CHANGE_USERNAME));
+ CommandManager.register(Strings.CHANGE_USER_EMAIL, Constants.CMD_GIT_CHANGE_EMAIL, EventEmitter.getEmitter(Events.GIT_CHANGE_EMAIL));
+ CommandManager.register(Strings.ENABLE_GERRIT_PUSH_REF, Constants.CMD_GIT_GERRIT_PUSH_REF, EventEmitter.getEmitter(Events.GERRIT_TOGGLE_PUSH_REF));
+ CommandManager.register(Strings.VIEW_AUTHORS_SELECTION, Constants.CMD_GIT_AUTHORS_OF_SELECTION, handleAuthorsSelection);
+ CommandManager.register(Strings.VIEW_AUTHORS_FILE, Constants.CMD_GIT_AUTHORS_OF_FILE, handleAuthorsFile);
+ CommandManager.register(Strings.HIDE_UNTRACKED, Constants.CMD_GIT_TOGGLE_UNTRACKED, handleToggleUntracked);
+ CommandManager.register(Strings.GIT_INIT, Constants.CMD_GIT_INIT, EventEmitter.getEmitter(Events.HANDLE_GIT_INIT));
+ CommandManager.register(Strings.GIT_CLONE, Constants.CMD_GIT_CLONE, EventEmitter.getEmitter(Events.HANDLE_GIT_CLONE));
+
+ // Show gitPanel when appropriate
+ if (Preferences.get("panelEnabled") && Setup.isExtensionActivated()) {
+ toggle(true);
+ }
+ _panelResized();
+ } // function init() {
+
+ function enable() {
+ EventEmitter.emit(Events.GIT_ENABLED);
+ // this function is called after every Branch.refresh
+ gitPanelMode = null;
+ //
+ $gitPanel.find(".git-available").show();
+ $gitPanel.find(".git-not-available").hide();
+ Utils.enableCommand(Constants.CMD_GIT_INIT, false);
+ Utils.enableCommand(Constants.CMD_GIT_CLONE, false);
+ //
+ Main.$icon.removeClass("warning");
+ gitPanelDisabled = false;
+ // after all is enabled
+ refresh();
+ }
+
+ function disable(cause) {
+ EventEmitter.emit(Events.GIT_DISABLED, cause);
+ gitPanelMode = cause;
+ // causes: not-repo
+ if (gitPanelMode === "not-repo") {
+ $gitPanel.find(".git-available").hide();
+ $gitPanel.find(".git-not-available").show();
+ Utils.enableCommand(Constants.CMD_GIT_INIT, true);
+ Utils.enableCommand(Constants.CMD_GIT_CLONE, true);
+ } else {
+ Main.$icon.addClass("warning");
+ toggle(false);
+ gitPanelDisabled = true;
+ }
+ refresh();
+ }
+
+ // Event listeners
+ EventEmitter.on(Events.GIT_USERNAME_CHANGED, function (userName) {
+ if(userName){
+ _setName(Constants.CMD_GIT_CHANGE_USERNAME,
+ StringUtils.format(Strings.CHANGE_USER_NAME_MENU, userName));
+ } else {
+ _setName(Constants.CMD_GIT_CHANGE_USERNAME, Strings.CHANGE_USER_NAME);
+ }
+ });
+
+ EventEmitter.on(Events.GIT_EMAIL_CHANGED, function (email) {
+ $gitPanel.find(".git-user-email").text(email);
+ if(email){
+ _setName(Constants.CMD_GIT_CHANGE_EMAIL,
+ StringUtils.format(Strings.CHANGE_USER_EMAIL_MENU, email));
+ } else {
+ _setName(Constants.CMD_GIT_CHANGE_EMAIL, Strings.CHANGE_USER_EMAIL);
+ }
+ });
+
+ EventEmitter.on(Events.GIT_REMOTE_AVAILABLE, function () {
+ $gitPanel.find(".git-pull, .git-push, .git-fetch").prop("disabled", false);
+ });
+
+ EventEmitter.on(Events.GIT_REMOTE_NOT_AVAILABLE, function () {
+ $gitPanel.find(".git-pull, .git-push, .git-fetch").prop("disabled", true);
+ });
+
+ EventEmitter.on(Events.GIT_ENABLED, function () {
+ // Add info from Git to panel
+ Git.getConfig("user.name").then(function (currentUserName) {
+ EventEmitter.emit(Events.GIT_USERNAME_CHANGED, currentUserName);
+ });
+ Git.getConfig("user.email").then(function (currentEmail) {
+ EventEmitter.emit(Events.GIT_EMAIL_CHANGED, currentEmail);
+ });
+ Git.getConfig("gerrit.pushref").then(function (strEnabled) {
+ var enabled = strEnabled === "true";
+ // Handle the case where we switched to a repo that is using gerrit
+ if (enabled && !Preferences.get("gerritPushref")) {
+ Preferences.set("gerritPushref", true);
+ }
+ EventEmitter.emit(Events.GERRIT_PUSH_REF_TOGGLED, enabled);
+ });
+ });
+
+ EventEmitter.on(Events.BRACKETS_CURRENT_DOCUMENT_CHANGE, function () {
+ if (!gitPanel) { return; }
+ refreshCurrentFile();
+ });
+
+ EventEmitter.on(Events.BRACKETS_DOCUMENT_SAVED, function () {
+ if (!gitPanel) { return; }
+ refresh();
+ });
+
+ EventEmitter.on(Events.BRACKETS_FILE_CHANGED, function (fileSystemEntry) {
+ // files are added or deleted from the directory
+ if (fileSystemEntry.isDirectory) {
+ refresh();
+ }
+ });
+
+ EventEmitter.on(Events.REBASE_MERGE_MODE, function (rebaseEnabled, mergeEnabled) {
+ $gitPanel.find(".git-rebase").toggle(rebaseEnabled);
+ $gitPanel.find(".git-merge").toggle(mergeEnabled);
+ $gitPanel.find("button.git-commit").toggle(!rebaseEnabled && !mergeEnabled);
+ });
+
+ EventEmitter.on(Events.FETCH_STARTED, function () {
+ $gitPanel.find(".git-fetch")
+ .addClass("btn-loading")
+ .prop("disabled", true);
+ });
+
+ EventEmitter.on(Events.FETCH_COMPLETE, function () {
+ $gitPanel.find(".git-fetch")
+ .removeClass("btn-loading")
+ .prop("disabled", false);
+ refreshCommitCounts();
+ });
+
+ EventEmitter.on(Events.REFRESH_COUNTERS, function () {
+ refreshCommitCounts();
+ });
+
+ EventEmitter.on(Events.HANDLE_GIT_COMMIT, function () {
+ handleGitCommit(lastCommitMessage[ProjectManager.getProjectRoot().fullPath], false, COMMIT_MODE.DEFAULT);
+ });
+
+ exports.init = init;
+ exports.refresh = refresh;
+ exports.toggle = toggle;
+ exports.enable = enable;
+ exports.disable = disable;
+ exports.getSelectedHistoryCommit = getSelectedHistoryCommit;
+ exports.getPanel = function () { return $gitPanel; };
+
+});
diff --git a/src/extensions/default/Git/src/Preferences.js b/src/extensions/default/Git/src/Preferences.js
new file mode 100644
index 0000000000..88ab24dace
--- /dev/null
+++ b/src/extensions/default/Git/src/Preferences.js
@@ -0,0 +1,97 @@
+define(function (require, exports, module) {
+
+ var _ = brackets.getModule("thirdparty/lodash"),
+ PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
+ StateManager = PreferencesManager.stateManager,
+ prefix = "git";
+
+ var defaultPreferences = {
+ // features
+ "stripWhitespaceFromCommits": { "type": "boolean", "value": true },
+ "addEndlineToTheEndOfFile": { "type": "boolean", "value": true },
+ "removeByteOrderMark": { "type": "boolean", "value": false },
+ "normalizeLineEndings": { "type": "boolean", "value": false },
+ "useGitGutter": { "type": "boolean", "value": true },
+ "markModifiedInTree": { "type": "boolean", "value": true },
+ "useVerboseDiff": { "type": "boolean", "value": false },
+ "useDifftool": { "type": "boolean", "value": false },
+ "clearWhitespaceOnSave": { "type": "boolean", "value": false },
+ "gerritPushref": { "type": "boolean", "value": false },
+ // system
+ "enableGit": { "type": "boolean", "value": true },
+ "gitTimeout": { "type": "number", "value": 30 },
+ "gitPath": { "type": "string", "value": "" }
+ };
+
+ var prefs = PreferencesManager.getExtensionPrefs(prefix);
+ _.each(defaultPreferences, function (definition, key) {
+ if (definition.os && definition.os[brackets.platform]) {
+ prefs.definePreference(key, definition.type, definition.os[brackets.platform].value);
+ } else {
+ prefs.definePreference(key, definition.type, definition.value);
+ }
+ });
+ prefs.save();
+
+ function get(key) {
+ var location = defaultPreferences[key] ? PreferencesManager : StateManager;
+ arguments[0] = prefix + "." + key;
+ return location.get.apply(location, arguments);
+ }
+
+ function set(key) {
+ var location = defaultPreferences[key] ? PreferencesManager : StateManager;
+ arguments[0] = prefix + "." + key;
+ return location.set.apply(location, arguments);
+ }
+
+ function getAll() {
+ var obj = {};
+ _.each(defaultPreferences, function (definition, key) {
+ obj[key] = get(key);
+ });
+ return obj;
+ }
+
+ function getDefaults() {
+ var obj = {};
+ _.each(defaultPreferences, function (definition, key) {
+ var defaultValue;
+ if (definition.os && definition.os[brackets.platform]) {
+ defaultValue = definition.os[brackets.platform].value;
+ } else {
+ defaultValue = definition.value;
+ }
+ obj[key] = defaultValue;
+ });
+ return obj;
+ }
+
+ function getType(key) {
+ return defaultPreferences[key].type;
+ }
+
+ function getGlobal(key) {
+ return PreferencesManager.get(key);
+ }
+
+ function getExtensionPref() {
+ return prefs;
+ }
+
+ function save() {
+ PreferencesManager.save();
+ }
+
+ module.exports = {
+ get: get,
+ set: set,
+ getAll: getAll,
+ getDefaults: getDefaults,
+ getType: getType,
+ getGlobal: getGlobal,
+ getExtensionPref: getExtensionPref,
+ save: save
+ };
+
+});
diff --git a/src/extensions/default/Git/src/ProjectTreeMarks.js b/src/extensions/default/Git/src/ProjectTreeMarks.js
new file mode 100644
index 0000000000..966d4d5686
--- /dev/null
+++ b/src/extensions/default/Git/src/ProjectTreeMarks.js
@@ -0,0 +1,204 @@
+define(function (require) {
+
+ var _ = brackets.getModule("thirdparty/lodash"),
+ FileSystem = brackets.getModule("filesystem/FileSystem"),
+ ProjectManager = brackets.getModule("project/ProjectManager");
+
+ var EventEmitter = require("src/EventEmitter"),
+ Events = require("src/Events"),
+ Git = require("src/git/Git"),
+ Preferences = require("src/Preferences");
+
+ var ignoreEntries = [],
+ newPaths = [],
+ modifiedPaths = [];
+
+ if (!Preferences.get("markModifiedInTree")) {
+ // end here, no point in processing the code below
+ return;
+ }
+
+ function loadIgnoreContents() {
+ return new Promise((resolve)=>{
+ let gitRoot = Preferences.get("currentGitRoot"),
+ excludeContents,
+ gitignoreContents;
+
+ const finish = _.after(2, function () {
+ resolve(excludeContents + "\n" + gitignoreContents);
+ });
+
+ FileSystem.getFileForPath(gitRoot + ".git/info/exclude").read(function (err, content) {
+ excludeContents = err ? "" : content;
+ finish();
+ });
+
+ FileSystem.getFileForPath(gitRoot + ".gitignore").read(function (err, content) {
+ gitignoreContents = err ? "" : content;
+ finish();
+ });
+
+ });
+ }
+
+ function refreshIgnoreEntries() {
+ function regexEscape(str) {
+ // NOTE: We cannot use StringUtils.regexEscape() here because we don't wanna replace *
+ return str.replace(/([.?+\^$\\(){}|])/g, "\\$1");
+ }
+
+ return loadIgnoreContents().then(function (content) {
+ var gitRoot = Preferences.get("currentGitRoot");
+
+ ignoreEntries = _.compact(_.map(content.split("\n"), function (line) {
+ // Rules: http://git-scm.com/docs/gitignore
+ var type = "deny",
+ leadingSlash,
+ trailingSlash,
+ regex;
+
+ line = line.trim();
+ if (!line || line.indexOf("#") === 0) {
+ return;
+ }
+
+ // handle explicitly allowed files/folders with a leading !
+ if (line.indexOf("!") === 0) {
+ line = line.slice(1);
+ type = "accept";
+ }
+ // handle lines beginning with a backslash, which is used for escaping ! or #
+ if (line.indexOf("\\") === 0) {
+ line = line.slice(1);
+ }
+ // handle lines beginning with a slash, which only matches files/folders in the root dir
+ if (line.indexOf("/") === 0) {
+ line = line.slice(1);
+ leadingSlash = true;
+ }
+ // handle lines ending with a slash, which only exludes dirs
+ if (line.lastIndexOf("/") === line.length) {
+ // a line ending with a slash ends with **
+ line += "**";
+ trailingSlash = true;
+ }
+
+ // NOTE: /(.{0,})/ is basically the same as /(.*)/, but we can't use it because the asterisk
+ // would be replaced later on
+
+ // create the intial regexp here. We need the absolute path 'cause it could be that there
+ // are external files with the same name as a project file
+ regex = regexEscape(gitRoot) + (leadingSlash ? "" : "((.+)/)?") + regexEscape(line) + (trailingSlash ? "" : "(/.{0,})?");
+ // replace all the possible asterisks
+ regex = regex.replace(/\*\*$/g, "(.{0,})").replace(/(\*\*|\*$)/g, "(.+)").replace(/\*/g, "([^/]*)");
+ regex = "^" + regex + "$";
+
+ return {
+ regexp: new RegExp(regex),
+ type: type
+ };
+ }));
+ });
+ }
+
+ function isIgnored(path) {
+ var ignored = false;
+ _.forEach(ignoreEntries, function (entry) {
+ if (entry.regexp.test(path)) {
+ ignored = (entry.type === "deny");
+ }
+ });
+ return ignored;
+ }
+
+ function isNew(fullPath) {
+ return newPaths.indexOf(fullPath) !== -1;
+ }
+
+ function isModified(fullPath) {
+ return modifiedPaths.indexOf(fullPath) !== -1;
+ }
+
+ ProjectManager.addClassesProvider(function (data) {
+ var fullPath = data.fullPath;
+ if (isIgnored(fullPath)) {
+ return "git-ignored";
+ } else if (isNew(fullPath)) {
+ return "git-new";
+ } else if (isModified(fullPath)) {
+ return "git-modified";
+ }
+ });
+
+ function _refreshOpenFiles() {
+ $("#working-set-list-container").find("li").each(function () {
+ var $li = $(this),
+ data = $li.data("file");
+ if (data) {
+ var fullPath = data.fullPath;
+ $li.toggleClass("git-ignored", isIgnored(fullPath))
+ .toggleClass("git-new", isNew(fullPath))
+ .toggleClass("git-modified", isModified(fullPath));
+ }
+ });
+ }
+
+ var refreshOpenFiles = _.debounce(function () {
+ _refreshOpenFiles();
+ }, 100);
+
+ function attachEvents() {
+ $("#working-set-list-container").on("contentChanged", refreshOpenFiles).triggerHandler("contentChanged");
+ }
+
+ function detachEvents() {
+ $("#working-set-list-container").off("contentChanged", refreshOpenFiles);
+ }
+
+ // this will refresh ignore entries when .gitignore is modified
+ EventEmitter.on(Events.BRACKETS_FILE_CHANGED, function (file) {
+ if (file.fullPath === Preferences.get("currentGitRoot") + ".gitignore") {
+ refreshIgnoreEntries().finally(function () {
+ refreshOpenFiles();
+ });
+ }
+ });
+
+ // this will refresh new/modified paths on every status results
+ EventEmitter.on(Events.GIT_STATUS_RESULTS, function (files) {
+ var gitRoot = Preferences.get("currentGitRoot");
+
+ newPaths = [];
+ modifiedPaths = [];
+
+ files.forEach(function (entry) {
+ var isNew = entry.status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1 ||
+ entry.status.indexOf(Git.FILE_STATUS.ADDED) !== -1;
+
+ var fullPath = gitRoot + entry.file;
+ if (isNew) {
+ newPaths.push(fullPath);
+ } else {
+ modifiedPaths.push(fullPath);
+ }
+ });
+
+ ProjectManager.rerenderTree();
+ refreshOpenFiles();
+ });
+
+ // this will refresh ignore entries when git project is opened
+ EventEmitter.on(Events.GIT_ENABLED, function () {
+ refreshIgnoreEntries();
+ attachEvents();
+ });
+
+ // this will clear entries when non-git project is opened
+ EventEmitter.on(Events.GIT_DISABLED, function () {
+ ignoreEntries = [];
+ newPaths = [];
+ modifiedPaths = [];
+ detachEvents();
+ });
+
+});
diff --git a/src/extensions/default/Git/src/Remotes.js b/src/extensions/default/Git/src/Remotes.js
new file mode 100644
index 0000000000..63230e5c0f
--- /dev/null
+++ b/src/extensions/default/Git/src/Remotes.js
@@ -0,0 +1,390 @@
+define(function (require) {
+
+ // Brackets modules
+ var _ = brackets.getModule("thirdparty/lodash"),
+ DefaultDialogs = brackets.getModule("widgets/DefaultDialogs"),
+ Dialogs = brackets.getModule("widgets/Dialogs"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache"),
+ StringUtils = brackets.getModule("utils/StringUtils");
+
+ // Local modules
+ var ErrorHandler = require("src/ErrorHandler"),
+ Events = require("src/Events"),
+ EventEmitter = require("src/EventEmitter"),
+ Git = require("src/git/Git"),
+ Preferences = require("src/Preferences"),
+ ProgressDialog = require("src/dialogs/Progress"),
+ PullDialog = require("src/dialogs/Pull"),
+ PushDialog = require("src/dialogs/Push"),
+ Strings = brackets.getModule("strings"),
+ Utils = require("src/Utils");
+
+ // Templates
+ var gitRemotesPickerTemplate = require("text!templates/git-remotes-picker.html");
+
+ // Module variables
+ var $selectedRemote = null,
+ $remotesDropdown = null,
+ $gitPanel = null,
+ $gitPush = null;
+
+ function initVariables() {
+ $gitPanel = $("#git-panel");
+ $selectedRemote = $gitPanel.find(".git-selected-remote");
+ $remotesDropdown = $gitPanel.find(".git-remotes-dropdown");
+ $gitPush = $gitPanel.find(".git-push");
+ }
+
+ // Implementation
+
+ function getDefaultRemote(allRemotes) {
+ var defaultRemotes = Preferences.get("defaultRemotes") || {},
+ candidate = defaultRemotes[Preferences.get("currentGitRoot")];
+
+ var exists = _.find(allRemotes, function (remote) {
+ return remote.name === candidate;
+ });
+ if (!exists) {
+ candidate = null;
+ if (allRemotes.length > 0) {
+ candidate = _.first(allRemotes).name;
+ }
+ }
+
+ return candidate;
+ }
+
+ function setDefaultRemote(remoteName) {
+ var defaultRemotes = Preferences.get("defaultRemotes") || {};
+ defaultRemotes[Preferences.get("currentGitRoot")] = remoteName;
+ Preferences.set("defaultRemotes", defaultRemotes);
+ }
+
+ function clearRemotePicker() {
+ $selectedRemote
+ .html("—")
+ .data("remote", null);
+ }
+
+ function selectRemote(remoteName, type) {
+ if (!remoteName) {
+ return clearRemotePicker();
+ }
+
+ // Set as default remote only if is a normal git remote
+ if (type === "git") { setDefaultRemote(remoteName); }
+
+ // Disable pull if it is not a normal git remote
+ $gitPanel.find(".git-pull").prop("disabled", type !== "git");
+
+ // Enable push and set selected-remote-type to Git push button by type of remote
+ $gitPush
+ .prop("disabled", false)
+ .attr("x-selected-remote-type", type);
+
+ // Update remote name of $selectedRemote
+ $selectedRemote
+ .text(remoteName)
+ .attr("data-type", type) // use attr to apply CSS styles
+ .data("remote", remoteName);
+ }
+
+ function refreshRemotesPicker() {
+ Git.getRemotes().then(function (remotes) {
+ // Set default remote name and cache the remotes dropdown menu
+ var defaultRemoteName = getDefaultRemote(remotes);
+
+ // Disable Git-push and Git-pull if there are not remotes defined
+ $gitPanel
+ .find(".git-pull, .git-push, .git-fetch")
+ .prop("disabled", remotes.length === 0);
+
+ // Add options to change remote
+ remotes.forEach(function (remote) {
+ remote.deletable = remote.name !== "origin";
+ });
+
+ // Pass to Mustache the needed data
+ var compiledTemplate = Mustache.render(gitRemotesPickerTemplate, {
+ Strings: Strings,
+ remotes: remotes
+ });
+
+ // Inject the rendered template inside the $remotesDropdown
+ $remotesDropdown.html(compiledTemplate);
+
+ // Notify others that they may add more stuff to this dropdown
+ EventEmitter.emit(Events.REMOTES_REFRESH_PICKER);
+ // TODO: is it possible to wait for listeners to finish?
+
+ // TODO: if there're no remotes but there are some ftp remotes
+ // we need to adjust that something other may be put as default
+ // low priority
+ if (remotes.length > 0) {
+ selectRemote(defaultRemoteName, "git");
+ } else {
+ clearRemotePicker();
+ }
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_GETTING_REMOTES);
+ });
+ }
+
+ function handleRemoteCreation() {
+ return Utils.askQuestion(Strings.CREATE_NEW_REMOTE, Strings.ENTER_REMOTE_NAME)
+ .then(function (name) {
+ return Utils.askQuestion(Strings.CREATE_NEW_REMOTE, Strings.ENTER_REMOTE_URL).then(function (url) {
+ return [name, url];
+ });
+ })
+ .then(function ([name, url]) {
+ return Git.createRemote(name, url).then(function () {
+ return refreshRemotesPicker();
+ });
+ })
+ .catch(function (err) {
+ if (!ErrorHandler.equals(err, Strings.USER_ABORTED)) {
+ ErrorHandler.showError(err, Strings.ERROR_REMOTE_CREATION);
+ }
+ });
+ }
+
+ function deleteRemote(remoteName) {
+ return Utils.askQuestion(Strings.DELETE_REMOTE, StringUtils.format(Strings.DELETE_REMOTE_NAME, remoteName), { booleanResponse: true })
+ .then(function (response) {
+ if (response === true) {
+ return Git.deleteRemote(remoteName).then(function () {
+ return refreshRemotesPicker();
+ });
+ }
+ })
+ .catch(function (err) {
+ ErrorHandler.logError(err);
+ });
+ }
+
+ function showPushResult(result) {
+ if (typeof result.remoteUrl === "string") {
+ result.remoteUrl = Utils.encodeSensitiveInformation(result.remoteUrl);
+ }
+
+ var template = [
+ "
{{flagDescription}} ",
+ "Info:",
+ "Remote url - {{remoteUrl}}",
+ "Local branch - {{from}}",
+ "Remote branch - {{to}}",
+ "Summary - {{summary}}",
+ "
Status - {{status}} "
+ ].join("
");
+
+ Dialogs.showModalDialog(
+ DefaultDialogs.DIALOG_ID_INFO,
+ Strings.GIT_PUSH_RESPONSE, // title
+ Mustache.render(template, result) // message
+ );
+ }
+
+ function pushToRemote(remote) {
+ if (!remote) {
+ return ErrorHandler.showError(StringUtils.format(Strings.ERROR_NO_REMOTE_SELECTED, "push"));
+ }
+
+ var pushConfig = {
+ remote: remote
+ };
+
+ PushDialog.show(pushConfig)
+ .then(function (pushConfig) {
+ var q = Promise.resolve(),
+ additionalArgs = [];
+
+ if (pushConfig.tags) {
+ additionalArgs.push("--tags");
+ }
+ if (pushConfig.noVerify) {
+ additionalArgs.push("--no-verify");
+ }
+
+ // set a new tracking branch if desired
+ if (pushConfig.branch && pushConfig.setBranchAsTracking) {
+ q = q.then(function () {
+ return Git.setUpstreamBranch(pushConfig.remote, pushConfig.branch);
+ });
+ }
+ // put username and password into remote url
+ if (pushConfig.remoteUrlNew) {
+ q = q.then(function () {
+ return Git.setRemoteUrl(pushConfig.remote, pushConfig.remoteUrlNew);
+ });
+ }
+ // do the pull itself (we are not using pull command)
+ q = q.then(function () {
+ let op;
+ const progressTracker = ProgressDialog.newProgressTracker();
+ if (pushConfig.pushToNew) {
+ op = Git.pushToNewUpstream(pushConfig.remote, pushConfig.branch, {
+ noVerify: true, progressTracker});
+ } else if (pushConfig.strategy === "DEFAULT") {
+ op = Git.push(pushConfig.remote, pushConfig.branch, additionalArgs, progressTracker);
+ } else if (pushConfig.strategy === "FORCED") {
+ op = Git.pushForced(pushConfig.remote, pushConfig.branch, {
+ noVerify: true, progressTracker});
+ } else if (pushConfig.strategy === "DELETE_BRANCH") {
+ op = Git.deleteRemoteBranch(pushConfig.remote, pushConfig.branch, {
+ noVerify: true, progressTracker});
+ }
+ return ProgressDialog.show(op, progressTracker)
+ .then(function (result) {
+ return ProgressDialog.waitForClose().then(function () {
+ showPushResult(result);
+ });
+ })
+ .catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_PUSHING_REMOTE);
+ });
+ });
+ // restore original url if desired
+ if (pushConfig.remoteUrlRestore) {
+ q = q.finally(function () {
+ return Git.setRemoteUrl(pushConfig.remote, pushConfig.remoteUrlRestore);
+ });
+ }
+
+ return q.finally(function () {
+ EventEmitter.emit(Events.REFRESH_ALL);
+ });
+ })
+ .catch(function (err) {
+ // when dialog is cancelled, there's no error
+ if (err) { ErrorHandler.showError(err, Strings.ERROR_PUSHING_OPERATION); }
+ });
+ }
+
+ function pullFromRemote(remote) {
+ if (!remote) {
+ return ErrorHandler.showError(StringUtils.format(Strings.ERROR_NO_REMOTE_SELECTED, "pull"));
+ }
+
+ var pullConfig = {
+ remote: remote
+ };
+
+ PullDialog.show(pullConfig)
+ .then(function (pullConfig) {
+ var q = Promise.resolve();
+
+ // set a new tracking branch if desired
+ if (pullConfig.branch && pullConfig.setBranchAsTracking) {
+ q = q.then(function () {
+ return Git.setUpstreamBranch(pullConfig.remote, pullConfig.branch);
+ });
+ }
+ // put username and password into remote url
+ if (pullConfig.remoteUrlNew) {
+ q = q.then(function () {
+ return Git.setRemoteUrl(pullConfig.remote, pullConfig.remoteUrlNew);
+ });
+ }
+ // do the pull itself (we are not using pull command)
+ q = q.then(function () {
+ // fetch the remote first
+ const progressTracker = ProgressDialog.newProgressTracker();
+ return ProgressDialog.show(Git.fetchRemote(pullConfig.remote, progressTracker), progressTracker)
+ .then(function () {
+ if (pullConfig.strategy === "DEFAULT") {
+ return Git.mergeRemote(pullConfig.remote, pullConfig.branch,
+ false, false, {progressTracker});
+ } else if (pullConfig.strategy === "AVOID_MERGING") {
+ return Git.mergeRemote(pullConfig.remote, pullConfig.branch,
+ true, false, {progressTracker});
+ } else if (pullConfig.strategy === "MERGE_NOCOMMIT") {
+ return Git.mergeRemote(pullConfig.remote, pullConfig.branch,
+ false, true, {progressTracker});
+ } else if (pullConfig.strategy === "REBASE") {
+ return Git.rebaseRemote(pullConfig.remote, pullConfig.branch, progressTracker);
+ } else if (pullConfig.strategy === "RESET") {
+ return Git.resetRemote(pullConfig.remote, pullConfig.branch, progressTracker);
+ }
+ })
+ .then(function (result) {
+ return ProgressDialog.waitForClose().then(function () {
+ // Git writes status messages (including informational messages) to stderr,
+ // even when the command succeeds. For example, during `git pull --rebase`,
+ // the "Successfully rebased and updated" message is sent to stderr,
+ // leaving the result as empty in stdout.
+ // If we reach this point, the command has succeeded,
+ // so we display a success message if `result` is "".
+ return Utils.showOutput(result || Strings.GIT_PULL_SUCCESS,
+ Strings.GIT_PULL_RESPONSE);
+ });
+ })
+ .catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_PULLING_REMOTE);
+ });
+ });
+ // restore original url if desired
+ if (pullConfig.remoteUrlRestore) {
+ q = q.finally(function () {
+ return Git.setRemoteUrl(pullConfig.remote, pullConfig.remoteUrlRestore);
+ });
+ }
+
+ return q.finally(function () {
+ EventEmitter.emit(Events.REFRESH_ALL);
+ });
+ })
+ .catch(function (err) {
+ // when dialog is cancelled, there's no error
+ if (err) { ErrorHandler.showError(err, Strings.ERROR_PULLING_OPERATION); }
+ });
+ }
+
+ function handleFetch() {
+
+ // Tell the rest of the plugin that the fetch has started
+ EventEmitter.emit(Events.FETCH_STARTED);
+
+ const tracker = ProgressDialog.newProgressTracker();
+ return ProgressDialog.show(Git.fetchAllRemotes(tracker), tracker)
+ .catch(function (err) {
+ ErrorHandler.showError(err);
+ })
+ .then(ProgressDialog.waitForClose)
+ .finally(function () {
+ EventEmitter.emit(Events.FETCH_COMPLETE);
+ });
+ }
+
+ // Event subscriptions
+ EventEmitter.on(Events.GIT_ENABLED, function () {
+ initVariables();
+ refreshRemotesPicker();
+ });
+ EventEmitter.on(Events.HANDLE_REMOTE_PICK, function (event) {
+ var $remote = $(event.target).closest(".remote-name"),
+ remoteName = $remote.data("remote-name"),
+ type = $remote.data("type");
+ selectRemote(remoteName, type);
+ EventEmitter.emit(Events.REFRESH_COUNTERS);
+ });
+ EventEmitter.on(Events.HANDLE_REMOTE_CREATE, function () {
+ handleRemoteCreation();
+ });
+ EventEmitter.on(Events.HANDLE_REMOTE_DELETE, function (event) {
+ var remoteName = $(event.target).closest(".remote-name").data("remote-name");
+ deleteRemote(remoteName);
+ });
+ EventEmitter.on(Events.HANDLE_PULL, function () {
+ var remoteName = $selectedRemote.data("remote");
+ pullFromRemote(remoteName);
+ });
+ EventEmitter.on(Events.HANDLE_PUSH, function () {
+ var remoteName = $selectedRemote.data("remote");
+ pushToRemote(remoteName);
+ });
+ EventEmitter.on(Events.HANDLE_FETCH, function () {
+ handleFetch();
+ });
+
+});
diff --git a/src/extensions/default/Git/src/SettingsDialog.js b/src/extensions/default/Git/src/SettingsDialog.js
new file mode 100644
index 0000000000..23867d2e6a
--- /dev/null
+++ b/src/extensions/default/Git/src/SettingsDialog.js
@@ -0,0 +1,124 @@
+define(function (require, exports) {
+
+ // Brackets modules
+ const Dialogs = brackets.getModule("widgets/Dialogs"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache"),
+ Preferences = require("./Preferences"),
+ Strings = brackets.getModule("strings"),
+ Git = require("./git/Git"),
+ Setup = require("src/utils/Setup"),
+ settingsDialogTemplate = require("text!templates/git-settings-dialog.html");
+
+ var dialog,
+ $dialog;
+
+ function setValues(values) {
+ $("*[settingsProperty]", $dialog).each(function () {
+ var $this = $(this),
+ type = $this.attr("type"),
+ tag = $this.prop("tagName").toLowerCase(),
+ property = $this.attr("settingsProperty");
+ if (type === "checkbox") {
+ $this.prop("checked", values[property]);
+ } else if (tag === "select") {
+ $("option[value=" + values[property] + "]", $this).prop("selected", true);
+ } else {
+ $this.val(values[property]);
+ }
+ });
+ }
+
+ function collectDialogValues() {
+ $("*[settingsProperty]", $dialog).each(function () {
+ var $this = $(this),
+ type = $this.attr("type"),
+ property = $this.attr("settingsProperty"),
+ prefType = Preferences.getType(property);
+ if (type === "checkbox") {
+ Preferences.set(property, $this.prop("checked"));
+ } else if (prefType === "number") {
+ var newValue = parseInt($this.val().trim(), 10);
+ if (isNaN(newValue)) { newValue = Preferences.getDefaults()[property]; }
+ Preferences.set(property, newValue);
+ } else {
+ Preferences.set(property, $this.val().trim() || null);
+ }
+ });
+ Preferences.save();
+ }
+
+ function assignActions() {
+ var $useDifftoolCheckbox = $("#git-settings-useDifftool", $dialog);
+
+ Git.getConfig("diff.tool").then(function (diffToolConfiguration) {
+
+ if (!diffToolConfiguration) {
+ $useDifftoolCheckbox.prop({
+ checked: false,
+ disabled: true
+ });
+ } else {
+ $useDifftoolCheckbox.prop({
+ disabled: false
+ });
+ }
+
+ }).catch(function () {
+
+ // an error with git
+ // we were not able to check whether diff tool is configured or not
+ // so we disable it just to be sure
+ $useDifftoolCheckbox.prop({
+ checked: false,
+ disabled: true
+ });
+
+ });
+
+ $("#git-settings-stripWhitespaceFromCommits", $dialog).on("change", function () {
+ var on = $(this).is(":checked");
+ $("#git-settings-addEndlineToTheEndOfFile,#git-settings-removeByteOrderMark,#git-settings-normalizeLineEndings", $dialog)
+ .prop("checked", on)
+ .prop("disabled", !on);
+ });
+
+ $("button[data-button-id='defaults']", $dialog).on("click", function (e) {
+ e.stopPropagation();
+ setValues(Preferences.getDefaults());
+ });
+ }
+
+ function init() {
+ setValues(Preferences.getAll());
+ assignActions();
+ }
+
+ exports.show = function () {
+ const enableGitPreference = Preferences.get("enableGit");
+ const compiledTemplate = Mustache.render(settingsDialogTemplate, {
+ Strings,
+ gitDisabled: !enableGitPreference,
+ gitNotFound: enableGitPreference ? !Setup.isExtensionActivated() : false
+ });
+
+ dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate);
+ $dialog = dialog.getElement();
+
+ init();
+ $dialog.find("#git-settings-enableGit").on("change", function () {
+ const anyChecked = $dialog.find("#git-settings-enableGit:checked").length > 0;
+ if (anyChecked) {
+ $dialog.find(".git-settings-content").removeClass("forced-inVisible");
+ } else {
+ $dialog.find(".git-settings-content").addClass("forced-inVisible");
+ }
+ });
+
+ dialog.done(function (buttonId) {
+ if (buttonId === "ok") {
+ // Save everything to preferences
+ collectDialogValues();
+ }
+ });
+ };
+});
diff --git a/src/extensions/default/Git/src/Utils.js b/src/extensions/default/Git/src/Utils.js
new file mode 100644
index 0000000000..6476945df9
--- /dev/null
+++ b/src/extensions/default/Git/src/Utils.js
@@ -0,0 +1,608 @@
+/*globals jsPromise, logger*/
+define(function (require, exports, module) {
+
+ // Brackets modules
+ const _ = brackets.getModule("thirdparty/lodash"),
+ CommandManager = brackets.getModule("command/CommandManager"),
+ Commands = brackets.getModule("command/Commands"),
+ Dialogs = brackets.getModule("widgets/Dialogs"),
+ DocumentManager = brackets.getModule("document/DocumentManager"),
+ FileSystem = brackets.getModule("filesystem/FileSystem"),
+ FileUtils = brackets.getModule("file/FileUtils"),
+ LanguageManager = brackets.getModule("language/LanguageManager"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache"),
+ ProjectManager = brackets.getModule("project/ProjectManager");
+
+ // Local modules
+ const ErrorHandler = require("src/ErrorHandler"),
+ Events = require("src/Events"),
+ EventEmitter = require("src/EventEmitter"),
+ Git = require("src/git/Git"),
+ Preferences = require("src/Preferences"),
+ Setup = require("src/utils/Setup"),
+ Constants = require("src/Constants"),
+ Strings = brackets.getModule("strings");
+
+ // Module variables
+ const formatDiffTemplate = require("text!templates/format-diff.html"),
+ questionDialogTemplate = require("text!templates/git-question-dialog.html"),
+ outputDialogTemplate = require("text!templates/git-output.html"),
+ writeTestResults = {},
+ EXT_NAME = "[brackets-git] ";
+
+ // Implementation
+ function getProjectRoot() {
+ var projectRoot = ProjectManager.getProjectRoot();
+ return projectRoot ? projectRoot.fullPath : null;
+ }
+
+ // returns "C:/Users/Zaggi/AppData/Roaming/Brackets/extensions/user/zaggino.brackets-git/"
+ function getExtensionDirectory() {
+ throw new Error("api unsupported");
+ // var modulePath = ExtensionUtils.getModulePath(module);
+ // return modulePath.slice(0, -1 * "src/".length);
+ }
+
+ function formatDiff(diff) {
+ var DIFF_MAX_LENGTH = 2000;
+
+ var tabReplace = "",
+ verbose = Preferences.get("useVerboseDiff"),
+ numLineOld = 0,
+ numLineNew = 0,
+ lastStatus = 0,
+ diffData = [];
+
+ var i = Preferences.getGlobal("tabSize");
+ while (i--) {
+ tabReplace += " ";
+ }
+
+ var LINE_STATUS = {
+ HEADER: 0,
+ UNCHANGED: 1,
+ REMOVED: 2,
+ ADDED: 3,
+ EOF: 4
+ };
+
+ var diffSplit = diff.split("\n");
+
+ if (diffSplit.length > DIFF_MAX_LENGTH) {
+ return "
" + Strings.DIFF_TOO_LONG + "
";
+ }
+
+ diffSplit.forEach(function (line) {
+ if (line === " ") { line = ""; }
+
+ var lineClass = "",
+ pushLine = true;
+
+ if (line.indexOf("diff --git") === 0) {
+ lineClass = "diffCmd";
+
+ diffData.push({
+ name: line.split("b/")[1],
+ lines: []
+ });
+
+ if (!verbose) {
+ pushLine = false;
+ }
+ } else if (line.match(/index\s[A-z0-9]{7}\.\.[A-z0-9]{7}/)) {
+ if (!verbose) {
+ pushLine = false;
+ }
+ } else if (line.substr(0, 3) === "+++" || line.substr(0, 3) === "---") {
+ if (!verbose) {
+ pushLine = false;
+ }
+ } else if (line.indexOf("@@") === 0) {
+ lineClass = "position";
+
+ // Define the type of the line: Header
+ lastStatus = LINE_STATUS.HEADER;
+
+ // This read the start line for the diff and substract 1 for this line
+ var m = line.match(/^@@ -([,0-9]+) \+([,0-9]+) @@/);
+ var s1 = m[1].split(",");
+ var s2 = m[2].split(",");
+
+ numLineOld = s1[0] - 1;
+ numLineNew = s2[0] - 1;
+ } else if (line[0] === "+") {
+ lineClass = "added";
+ line = line.substring(1);
+
+ // Define the type of the line: Added
+ lastStatus = LINE_STATUS.ADDED;
+
+ // Add 1 to the num line for new document
+ numLineNew++;
+ } else if (line[0] === "-") {
+ lineClass = "removed";
+ line = line.substring(1);
+
+ // Define the type of the line: Removed
+ lastStatus = LINE_STATUS.REMOVED;
+
+ // Add 1 to the num line for old document
+ numLineOld++;
+ } else if (line[0] === " " || line === "") {
+ lineClass = "unchanged";
+ line = line.substring(1);
+
+ // Define the type of the line: Unchanged
+ lastStatus = LINE_STATUS.UNCHANGED;
+
+ // Add 1 to old a new num lines
+ numLineOld++;
+ numLineNew++;
+ } else if (line === "\\ No newline at end of file") {
+ lastStatus = LINE_STATUS.EOF;
+ lineClass = "end-of-file";
+ } else {
+ console.log("Unexpected line in diff: " + line);
+ }
+
+ if (pushLine) {
+ var _numLineOld = "",
+ _numLineNew = "";
+
+ switch (lastStatus) {
+ case LINE_STATUS.HEADER:
+ case LINE_STATUS.EOF:
+ // _numLineOld = "";
+ // _numLineNew = "";
+ break;
+ case LINE_STATUS.UNCHANGED:
+ _numLineOld = numLineOld;
+ _numLineNew = numLineNew;
+ break;
+ case LINE_STATUS.REMOVED:
+ _numLineOld = numLineOld;
+ // _numLineNew = "";
+ break;
+ // case LINE_STATUS.ADDED:
+ default:
+ // _numLineOld = "";
+ _numLineNew = numLineNew;
+ }
+
+ // removes ZERO WIDTH NO-BREAK SPACE character (BOM)
+ line = line.replace(/\uFEFF/g, "");
+
+ // exposes other potentially harmful characters
+ line = line.replace(/[\u2000-\uFFFF]/g, function (x) {
+ return "
";
+ });
+
+ line = _.escape(line)
+ .replace(/\t/g, tabReplace)
+ .replace(/\s/g, " ");
+
+ line = line.replace(/( )+$/g, function (trailingWhitespace) {
+ return "" + trailingWhitespace + " ";
+ });
+
+ if (diffData.length > 0) {
+ _.last(diffData).lines.push({
+ "numLineOld": _numLineOld,
+ "numLineNew": _numLineNew,
+ "line": line,
+ "lineClass": lineClass
+ });
+ }
+ }
+ });
+
+ return Mustache.render(formatDiffTemplate, { files: diffData });
+ }
+
+ function askQuestion(title, question, options) {
+ return new Promise(function (resolve, reject) {
+ options = options || {};
+
+ if (!options.noescape) {
+ question = _.escape(question);
+ }
+
+ var compiledTemplate = Mustache.render(questionDialogTemplate, {
+ title: title,
+ question: question,
+ stringInput: !options.booleanResponse && !options.password,
+ passwordInput: options.password,
+ defaultValue: options.defaultValue,
+ customOkBtn: options.customOkBtn,
+ customOkBtnClass: options.customOkBtnClass,
+ Strings: Strings
+ });
+
+ var dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate),
+ $dialog = dialog.getElement();
+
+ _.defer(function () {
+ var $input = $dialog.find("input:visible");
+ if ($input.length > 0) {
+ $input.focus();
+ } else {
+ $dialog.find(".primary").focus();
+ }
+ });
+
+ dialog.done(function (buttonId) {
+ if (options.booleanResponse) {
+ return resolve(buttonId === "ok");
+ }
+ if (buttonId === "ok") {
+ resolve(dialog.getElement().find("input").val().trim());
+ } else {
+ reject(Strings.USER_ABORTED);
+ }
+ });
+ });
+ }
+
+ function showOutput(output, title, options) {
+ return new Promise(function (resolve) {
+ options = options || {};
+ var compiledTemplate = Mustache.render(outputDialogTemplate, {
+ title: title,
+ output: output,
+ Strings: Strings,
+ question: options.question
+ });
+ var dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate);
+ dialog.getElement().find("button").focus();
+ dialog.done(function (buttonId) {
+ resolve(buttonId === "ok");
+ });
+ });
+ }
+
+ function isProjectRootWritable() {
+ return new Promise(function (resolve) {
+
+ var folder = getProjectRoot();
+
+ // if we previously tried, assume nothing has changed
+ if (writeTestResults[folder]) {
+ return resolve(writeTestResults[folder]);
+ }
+
+ // create entry for temporary file
+ var fileEntry = FileSystem.getFileForPath(folder + ".bracketsGitTemp");
+
+ function finish(bool) {
+ // delete the temp file and resolve
+ fileEntry.unlink(function () {
+ writeTestResults[folder] = bool;
+ resolve(bool);
+ });
+ }
+
+ // try writing some text into the temp file
+ jsPromise(FileUtils.writeText(fileEntry, ""))
+ .then(function () {
+ finish(true);
+ })
+ .catch(function () {
+ finish(false);
+ });
+ });
+ }
+
+ function pathExists(path) {
+ return new Promise(function (resolve) {
+ FileSystem.resolve(path, function (err, entry) {
+ resolve(!err && entry ? true : false);
+ });
+ });
+ }
+
+ function loadPathContent(path) {
+ return new Promise(function (resolve) {
+ FileSystem.resolve(path, function (err, entry) {
+ if (err) {
+ return resolve(null);
+ }
+ if (entry._clearCachedData) {
+ entry._clearCachedData();
+ }
+ if (entry.isFile) {
+ entry.read(function (err, content) {
+ if (err) {
+ return resolve(null);
+ }
+ resolve(content);
+ });
+ } else {
+ entry.getContents(function (err, contents) {
+ if (err) {
+ return resolve(null);
+ }
+ resolve(contents);
+ });
+ }
+ });
+ });
+ }
+
+ function isLoading($btn) {
+ return $btn.hasClass("btn-loading");
+ }
+
+ function setLoading($btn) {
+ $btn.prop("disabled", true).addClass("btn-loading");
+ }
+
+ function unsetLoading($btn) {
+ $btn.prop("disabled", false).removeClass("btn-loading");
+ }
+
+ function encodeSensitiveInformation(str) {
+ // should match passwords in http/https urls
+ str = str.replace(/(https?:\/\/)([^:@\s]*):([^:@]*)?@/g, function (a, protocol, user/*, pass*/) {
+ return protocol + user + ":***@";
+ });
+ // should match user name in windows user folders
+ str = str.replace(/(users)(\\|\/)([^\\\/]+)(\\|\/)/i, function (a, users, slash1, username, slash2) {
+ return users + slash1 + "***" + slash2;
+ });
+ return str;
+ }
+
+ function consoleWarn(msg) {
+ console.warn(encodeSensitiveInformation(msg));
+ }
+
+ function consoleError(msg) {
+ console.error(encodeSensitiveInformation(msg));
+ }
+
+ function consoleDebug(msg) {
+ if (logger.loggingOptions.logGit) {
+ console.log(EXT_NAME + encodeSensitiveInformation(msg));
+ }
+ }
+
+ /**
+ * Reloads the Document's contents from disk, discarding any unsaved changes in the editor.
+ *
+ * @param {!Document} doc
+ * @return {Promise} Resolved after editor has been refreshed; rejected if unable to load the
+ * file's new content. Errors are logged but no UI is shown.
+ */
+ function reloadDoc(doc) {
+ return jsPromise(FileUtils.readAsText(doc.file))
+ .then(function (text) {
+ doc.refreshText(text, new Date());
+ })
+ .catch(function (err) {
+ ErrorHandler.logError("Error reloading contents of " + doc.file.fullPath);
+ ErrorHandler.logError(err);
+ });
+ }
+
+ /**
+ * strips trailing whitespace from all the diffs and adds \n to the end
+ */
+ function stripWhitespaceFromFile(filename, clearWholeFile) {
+ return new Promise(function (resolve, reject) {
+
+ var fullPath = Preferences.get("currentGitRoot") + filename,
+ addEndlineToTheEndOfFile = Preferences.get("addEndlineToTheEndOfFile"),
+ removeBom = Preferences.get("removeByteOrderMark"),
+ normalizeLineEndings = Preferences.get("normalizeLineEndings");
+
+ var _cleanLines = function (lineNumbers) {
+ // do not clean if there's nothing to clean
+ if (lineNumbers && lineNumbers.length === 0) {
+ return resolve();
+ }
+ // clean the file
+ var fileEntry = FileSystem.getFileForPath(fullPath);
+ return jsPromise(FileUtils.readAsText(fileEntry))
+ .catch(function (err) {
+ ErrorHandler.logError(err + " on FileUtils.readAsText for " + fileEntry.fullPath);
+ return null;
+ })
+ .then(function (text) {
+ if (text === null) {
+ return resolve();
+ }
+
+ if (removeBom) {
+ // remove BOM - \uFEFF
+ text = text.replace(/\uFEFF/g, "");
+ }
+ if (normalizeLineEndings) {
+ // normalizes line endings
+ text = text.replace(/\r\n/g, "\n");
+ }
+ // process lines
+ var lines = text.split("\n");
+
+ if (lineNumbers) {
+ lineNumbers.forEach(function (lineNumber) {
+ if (typeof lines[lineNumber] === "string") {
+ lines[lineNumber] = lines[lineNumber].replace(/\s+$/, "");
+ }
+ });
+ } else {
+ lines.forEach(function (ln, lineNumber) {
+ if (typeof lines[lineNumber] === "string") {
+ lines[lineNumber] = lines[lineNumber].replace(/\s+$/, "");
+ }
+ });
+ }
+
+ // add empty line to the end, i've heard that git likes that for some reason
+ if (addEndlineToTheEndOfFile) {
+ var lastLineNumber = lines.length - 1;
+ if (lines[lastLineNumber].length > 0) {
+ lines[lastLineNumber] = lines[lastLineNumber].replace(/\s+$/, "");
+ }
+ if (lines[lastLineNumber].length > 0) {
+ lines.push("");
+ }
+ }
+
+ text = lines.join("\n");
+ return jsPromise(FileUtils.writeText(fileEntry, text))
+ .catch(function (err) {
+ ErrorHandler.logError("Wasn't able to clean whitespace from file: " + fullPath);
+ resolve();
+ throw err;
+ })
+ .then(function () {
+ // refresh the file if it's open in the background
+ DocumentManager.getAllOpenDocuments().forEach(function (doc) {
+ if (doc.file.fullPath === fullPath) {
+ reloadDoc(doc);
+ }
+ });
+ // diffs were cleaned in this file
+ resolve();
+ });
+ });
+ };
+
+ if (clearWholeFile) {
+ _cleanLines(null);
+ } else {
+ Git.diffFile(filename).then(function (diff) {
+ // if git returned an empty diff
+ if (!diff) { return resolve(); }
+
+ // if git detected that the file is binary
+ if (diff.match(/^binary files.*differ$/img)) { return resolve(); }
+
+ var modified = [],
+ changesets = diff.split("\n").filter(function (l) { return l.match(/^@@/) !== null; });
+ // collect line numbers to clean
+ changesets.forEach(function (line) {
+ var i,
+ m = line.match(/^@@ -([,0-9]+) \+([,0-9]+) @@/),
+ s = m[2].split(","),
+ from = parseInt(s[0], 10),
+ to = from - 1 + (parseInt(s[1], 10) || 1);
+ for (i = from; i <= to; i++) { modified.push(i > 0 ? i - 1 : 0); }
+ });
+ _cleanLines(modified);
+ }).catch(function (ex) {
+ // This error will bubble up to preparing commit dialog so just log here
+ ErrorHandler.logError(ex);
+ reject(ex);
+ });
+ }
+ });
+ }
+
+ function stripWhitespaceFromFiles(gitStatusResults, stageChanges, progressTracker) {
+ return new Promise((resolve, reject)=>{
+ const startTime = (new Date()).getTime();
+ let queue = Promise.resolve();
+
+ gitStatusResults.forEach(function (fileObj) {
+ var isDeleted = fileObj.status.indexOf(Git.FILE_STATUS.DELETED) !== -1;
+
+ // strip whitespace if the file was not deleted
+ if (!isDeleted) {
+ // strip whitespace only for recognized languages so binary files won't get corrupted
+ var langId = LanguageManager.getLanguageForPath(fileObj.file).getId();
+ if (["unknown", "binary", "image", "markdown", "audio"].indexOf(langId) === -1) {
+
+ queue = queue.then(function () {
+ var clearWholeFile = fileObj.status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1 ||
+ fileObj.status.indexOf(Git.FILE_STATUS.RENAMED) !== -1;
+
+ var t = (new Date()).getTime() - startTime;
+ progressTracker.trigger(Events.GIT_PROGRESS_EVENT,
+ t + "ms - " + Strings.CLEAN_FILE_START + ": " + fileObj.file);
+
+ return stripWhitespaceFromFile(fileObj.file, clearWholeFile).then(function () {
+ // stage the files again to include stripWhitespace changes
+ var notifyProgress = function () {
+ var t = (new Date()).getTime() - startTime;
+ progressTracker.trigger(Events.GIT_PROGRESS_EVENT,
+ t + "ms - " + Strings.CLEAN_FILE_END + ": " + fileObj.file);
+ };
+ if (stageChanges) {
+ return Git.stage(fileObj.file).then(notifyProgress);
+ } else {
+ notifyProgress();
+ }
+ });
+ });
+
+ }
+ }
+ });
+
+ queue
+ .then(function () {
+ resolve();
+ })
+ .catch(function () {
+ reject();
+ });
+ });
+ }
+
+ function openEditorForFile(file, relative) {
+ if (relative) {
+ file = getProjectRoot() + file;
+ }
+ CommandManager.execute(Commands.FILE_OPEN, {
+ fullPath: file
+ });
+ }
+
+ let clearWhitespace = Preferences.get("clearWhitespaceOnSave");
+ Preferences.getExtensionPref().on("change", "clearWhitespaceOnSave", ()=>{
+ clearWhitespace = Preferences.get("clearWhitespaceOnSave");
+ });
+
+ EventEmitter.on(Events.BRACKETS_DOCUMENT_SAVED, function (doc) {
+ if(!clearWhitespace){
+ return;
+ }
+ var fullPath = doc.file.fullPath,
+ currentGitRoot = Preferences.get("currentGitRoot"),
+ path = fullPath.substring(currentGitRoot.length);
+ stripWhitespaceFromFile(path);
+ });
+
+ function enableCommand(commandID, enabled) {
+ const command = CommandManager.get(commandID);
+ if(!command){
+ return;
+ }
+ enabled = commandID === Constants.CMD_GIT_SETTINGS_COMMAND_ID ?
+ true : enabled && Setup.isExtensionActivated();
+ command.setEnabled(enabled);
+ }
+
+ // Public API
+ exports.formatDiff = formatDiff;
+ exports.getProjectRoot = getProjectRoot;
+ exports.getExtensionDirectory = getExtensionDirectory;
+ exports.askQuestion = askQuestion;
+ exports.showOutput = showOutput;
+ exports.isProjectRootWritable = isProjectRootWritable;
+ exports.pathExists = pathExists;
+ exports.loadPathContent = loadPathContent;
+ exports.setLoading = setLoading;
+ exports.unsetLoading = unsetLoading;
+ exports.isLoading = isLoading;
+ exports.consoleWarn = consoleWarn;
+ exports.consoleError = consoleError;
+ exports.consoleDebug = consoleDebug;
+ exports.encodeSensitiveInformation = encodeSensitiveInformation;
+ exports.reloadDoc = reloadDoc;
+ exports.stripWhitespaceFromFiles = stripWhitespaceFromFiles;
+ exports.openEditorForFile = openEditorForFile;
+ exports.enableCommand = enableCommand;
+
+});
diff --git a/src/extensions/default/Git/src/dialogs/Clone.js b/src/extensions/default/Git/src/dialogs/Clone.js
new file mode 100644
index 0000000000..95b463569a
--- /dev/null
+++ b/src/extensions/default/Git/src/dialogs/Clone.js
@@ -0,0 +1,73 @@
+define(function (require, exports) {
+
+ // Brackets modules
+ const Dialogs = brackets.getModule("widgets/Dialogs"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache");
+
+ // Local modules
+ const RemoteCommon = require("src/dialogs/RemoteCommon"),
+ Strings = brackets.getModule("strings");
+
+ // Templates
+ const template = require("text!src/dialogs/templates/clone-dialog.html");
+
+ // Module variables
+ let $cloneInput;
+
+ // Implementation
+ function _attachEvents($dialog) {
+ // Detect changes to URL, disable auth if not http
+ $cloneInput.on("keyup change", function () {
+ var $authInputs = $dialog.find("input[name='username'],input[name='password'],input[name='saveToUrl']");
+ if ($(this).val().length > 0) {
+ if (/^https?:/.test($(this).val())) {
+ $authInputs.prop("disabled", false);
+
+ // Update the auth fields if the URL contains auth
+ var auth = /:\/\/([^:]+):?([^@]*)@/.exec($(this).val());
+ if (auth) {
+ $("input[name=username]", $dialog).val(auth[1]);
+ $("input[name=password]", $dialog).val(auth[2]);
+ }
+ } else {
+ $authInputs.prop("disabled", true);
+ }
+ } else {
+ $authInputs.prop("disabled", false);
+ }
+ });
+ $cloneInput.focus();
+ }
+
+ function show() {
+ return new Promise((resolve, reject)=>{
+ const templateArgs = {
+ modeLabel: Strings.CLONE_REPOSITORY,
+ Strings: Strings
+ };
+
+ var compiledTemplate = Mustache.render(template, templateArgs),
+ dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate),
+ $dialog = dialog.getElement();
+
+ $cloneInput = $dialog.find("#git-clone-url");
+
+ _attachEvents($dialog);
+
+ dialog.done(function (buttonId) {
+ if (buttonId === "ok") {
+ var cloneConfig = {};
+ cloneConfig.remote = "origin";
+ cloneConfig.remoteUrl = $cloneInput.val();
+ RemoteCommon.collectValues(cloneConfig, $dialog);
+ resolve(cloneConfig);
+ } else {
+ reject();
+ }
+ });
+
+ });
+ }
+
+ exports.show = show;
+});
diff --git a/src/extensions/default/Git/src/dialogs/Progress.js b/src/extensions/default/Git/src/dialogs/Progress.js
new file mode 100644
index 0000000000..161e6d22fa
--- /dev/null
+++ b/src/extensions/default/Git/src/dialogs/Progress.js
@@ -0,0 +1,151 @@
+define(function (require, exports) {
+ const EventDispatcher = brackets.getModule("utils/EventDispatcher");
+ // Brackets modules
+ const Dialogs = brackets.getModule("widgets/Dialogs"),
+ Strings = brackets.getModule("strings"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache");
+
+ // Local modules
+ const Events = require("src/Events");
+
+ // Templates
+ var template = require("text!src/dialogs/templates/progress-dialog.html");
+
+ // Module variables
+ var lines,
+ $textarea;
+
+ const maxLines = 5000;
+ // some git commit may have pre commit/push hooks which
+ // may run tests suits that print large amount of data on the console, so we need to
+ // debounce and truncate the git output we get in progress window.
+ function addLine(str) {
+ if (lines.length >= maxLines) {
+ lines.shift(); // Remove the oldest line
+ }
+ lines.push(str);
+ }
+ let updateTimeout = null;
+ function updateTextarea() {
+ if(updateTimeout){
+ // an update is scheduled, debounce, we dont need to print now
+ return;
+ }
+ updateTimeout = setTimeout(() => {
+ updateTimeout = null;
+ if(!$textarea || !lines.length){
+ return;
+ }
+ $textarea.val(lines.join("\n"));
+ $textarea.scrollTop($textarea[0].scrollHeight - $textarea.height());
+ }, 100);
+ }
+
+ function onProgress(str) {
+ if (typeof str === "string") {
+ addLine(str);
+ }
+ updateTextarea();
+ }
+
+ function show(promise, progressTracker, showOpts = {}) {
+ if (!promise || !promise.finally) {
+ throw new Error("Invalid promise argument for progress dialog!");
+ }
+ if(!progressTracker) {
+ throw new Error("Invalid progressTracker argument for progress dialog!");
+ }
+
+ const title = showOpts.title;
+ const options = showOpts.options || {};
+
+ return new Promise(function (resolve, reject) {
+
+ lines = showOpts.initialMessage ? [showOpts.initialMessage] : [];
+ $textarea = null;
+
+ var dialog,
+ finished = false;
+
+ function showDialog() {
+ if (finished) {
+ return;
+ }
+
+ var templateArgs = {
+ title: title || Strings.OPERATION_IN_PROGRESS_TITLE,
+ Strings: Strings
+ };
+
+ var compiledTemplate = Mustache.render(template, templateArgs);
+ dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate);
+
+ $textarea = dialog.getElement().find("textarea");
+ $textarea.val(Strings.PLEASE_WAIT);
+ onProgress();
+ }
+
+ function finish() {
+ finished = true;
+ if (dialog) {
+ dialog.close();
+ }
+ promise.then(function (val) {
+ resolve(val);
+ }).catch(function (err) {
+ reject(err);
+ });
+ }
+
+ if (!options.preDelay) {
+ showDialog();
+ } else {
+ setTimeout(function () {
+ showDialog();
+ }, options.preDelay * 1000);
+ }
+
+ progressTracker.off(`${Events.GIT_PROGRESS_EVENT}.progressDlg`);
+ progressTracker.on(`${Events.GIT_PROGRESS_EVENT}.progressDlg`, (_evt, data)=>{
+ onProgress(data);
+ });
+ promise.finally(function () {
+ progressTracker.off(`${Events.GIT_PROGRESS_EVENT}.progressDlg`);
+ onProgress("Finished!");
+ if (!options.postDelay || !dialog) {
+ finish();
+ } else {
+ setTimeout(function () {
+ finish();
+ }, options.postDelay * 1000);
+ }
+ });
+
+ });
+ }
+
+ function waitForClose() {
+ return new Promise(function (resolve) {
+ function check() {
+ var visible = $("#git-progress-dialog").is(":visible");
+ if (!visible) {
+ resolve();
+ } else {
+ setTimeout(check, 20);
+ }
+ }
+ setTimeout(check, 20);
+ });
+ }
+
+ function newProgressTracker() {
+ const tracker = {};
+ EventDispatcher.makeEventDispatcher(tracker);
+ return tracker;
+ }
+
+ exports.show = show;
+ exports.newProgressTracker = newProgressTracker;
+ exports.waitForClose = waitForClose;
+
+});
diff --git a/src/extensions/default/Git/src/dialogs/Pull.js b/src/extensions/default/Git/src/dialogs/Pull.js
new file mode 100644
index 0000000000..3d05e8c18a
--- /dev/null
+++ b/src/extensions/default/Git/src/dialogs/Pull.js
@@ -0,0 +1,65 @@
+define(function (require, exports) {
+
+ // Brackets modules
+ var Dialogs = brackets.getModule("widgets/Dialogs"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache");
+
+ // Local modules
+ var Preferences = require("src/Preferences"),
+ RemoteCommon = require("src/dialogs/RemoteCommon"),
+ Strings = brackets.getModule("strings");
+
+ // Templates
+ var template = require("text!src/dialogs/templates/pull-dialog.html"),
+ remotesTemplate = require("text!src/dialogs/templates/remotes-template.html");
+
+ // Implementation
+ function _attachEvents($dialog, pullConfig) {
+ RemoteCommon.attachCommonEvents(pullConfig, $dialog);
+
+ // load last used
+ $dialog
+ .find("input[name='strategy']")
+ .filter("[value='" + (Preferences.get("pull.strategy") || "DEFAULT") + "']")
+ .prop("checked", true);
+ }
+
+ function _show(pullConfig, resolve, reject) {
+ const templateArgs = {
+ config: pullConfig,
+ mode: "PULL_FROM",
+ modeLabel: Strings.PULL_FROM,
+ Strings: Strings
+ };
+
+ const compiledTemplate = Mustache.render(template, templateArgs, {
+ remotes: remotesTemplate
+ }),
+ dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate),
+ $dialog = dialog.getElement();
+
+ _attachEvents($dialog, pullConfig);
+
+ dialog.done(function (buttonId) {
+ if (buttonId === "ok") {
+ RemoteCommon.collectValues(pullConfig, $dialog);
+ Preferences.set("pull.strategy", pullConfig.strategy);
+ resolve(pullConfig);
+ } else {
+ reject();
+ }
+ });
+ }
+
+ function show(pullConfig) {
+ return new Promise((resolve, reject) => {
+ pullConfig.pull = true;
+ RemoteCommon.collectInfo(pullConfig).then(()=>{
+ _show(pullConfig, resolve, reject);
+ });
+ });
+ }
+
+ exports.show = show;
+
+});
diff --git a/src/extensions/default/Git/src/dialogs/Push.js b/src/extensions/default/Git/src/dialogs/Push.js
new file mode 100644
index 0000000000..86e569834d
--- /dev/null
+++ b/src/extensions/default/Git/src/dialogs/Push.js
@@ -0,0 +1,63 @@
+define(function (require, exports) {
+
+ // Brackets modules
+ const Dialogs = brackets.getModule("widgets/Dialogs"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache");
+
+ // Local modules
+ const RemoteCommon = require("src/dialogs/RemoteCommon"),
+ Strings = brackets.getModule("strings");
+
+ // Templates
+ const template = require("text!src/dialogs/templates/push-dialog.html"),
+ remotesTemplate = require("text!src/dialogs/templates/remotes-template.html");
+
+ // Implementation
+ function _attachEvents($dialog, pushConfig) {
+ RemoteCommon.attachCommonEvents(pushConfig, $dialog);
+
+ // select default - we don't want to remember forced or delete branch as default
+ $dialog
+ .find("input[name='strategy']")
+ .filter("[value='DEFAULT']")
+ .prop("checked", true);
+ }
+
+ function _show(pushConfig, resolve, reject) {
+ const templateArgs = {
+ config: pushConfig,
+ mode: "PUSH_TO",
+ modeLabel: Strings.PUSH_TO,
+ Strings: Strings
+ };
+
+ const compiledTemplate = Mustache.render(template, templateArgs, {
+ remotes: remotesTemplate
+ }),
+ dialog = Dialogs.showModalDialogUsingTemplate(compiledTemplate),
+ $dialog = dialog.getElement();
+
+ _attachEvents($dialog, pushConfig);
+
+ dialog.done(function (buttonId) {
+ if (buttonId === "ok") {
+ RemoteCommon.collectValues(pushConfig, $dialog);
+ resolve(pushConfig);
+ } else {
+ reject();
+ }
+ });
+ }
+
+ function show(pushConfig) {
+ return new Promise((resolve, reject) => {
+ pushConfig.push = true;
+ RemoteCommon.collectInfo(pushConfig).then(()=>{
+ _show(pushConfig, resolve, reject);
+ });
+ });
+ }
+
+ exports.show = show;
+
+});
diff --git a/src/extensions/default/Git/src/dialogs/RemoteCommon.js b/src/extensions/default/Git/src/dialogs/RemoteCommon.js
new file mode 100644
index 0000000000..dee30f9564
--- /dev/null
+++ b/src/extensions/default/Git/src/dialogs/RemoteCommon.js
@@ -0,0 +1,143 @@
+define(function (require, exports) {
+
+ // Brackets modules
+ const _ = brackets.getModule("thirdparty/lodash"),
+ Strings = brackets.getModule("strings"),
+ Mustache = brackets.getModule("thirdparty/mustache/mustache");
+
+ // Local modules
+ const ErrorHandler = require("src/ErrorHandler"),
+ Git = require("src/git/Git"),
+ ProgressDialog = require("src/dialogs/Progress");
+
+ // Implementation
+
+ function fillBranches(config, $dialog) {
+ Git.getAllBranches().then(function (branches) {
+ // filter only branches for this remote
+ branches = _.filter(branches, function (branch) {
+ return branch.remote === config.remote;
+ });
+
+ const template = "{{#branches}}{{name}} {{/branches}}";
+ const html = Mustache.render(template, { branches: branches });
+ $dialog.find(".branchSelect").html(html);
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_BRANCH_LIST);
+ });
+ }
+
+ exports.collectInfo = function (config) {
+ return Git.getCurrentUpstreamBranch().then(function (upstreamBranch) {
+ config.currentTrackingBranch = upstreamBranch;
+
+ return Git.getRemoteUrl(config.remote).then(function (remoteUrl) {
+ config.remoteUrl = remoteUrl;
+
+ if (remoteUrl.match(/^https?:/)) {
+ const url = new URL(remoteUrl);
+ config.remoteUsername = url.username;
+ config.remotePassword = url.password;
+ } else {
+ // disable the inputs
+ config._usernamePasswordDisabled = true;
+ }
+
+ if (!upstreamBranch) {
+ return Git.getCurrentBranchName().then(function (currentBranchName) {
+ config.currentBranchName = currentBranchName;
+ });
+ }
+ });
+ }).catch(function (err) {
+ ErrorHandler.showError(err, Strings.ERROR_FETCH_REMOTE);
+ });
+ };
+
+ exports.attachCommonEvents = function (config, $dialog) {
+ const handleRadioChange = function () {
+ const val = $dialog.find("input[name='action']:checked").val();
+ $dialog.find(".only-from-selected").toggle(val === "PULL_FROM_SELECTED" || val === "PUSH_TO_SELECTED");
+ };
+ $dialog.on("change", "input[name='action']", handleRadioChange);
+ handleRadioChange();
+
+ let trackingBranchRemote = null;
+ if (config.currentTrackingBranch) {
+ trackingBranchRemote = config.currentTrackingBranch.substring(0, config.currentTrackingBranch.indexOf("/"));
+ }
+
+ // if we're pulling from another remote than current tracking remote
+ if (config.currentTrackingBranch && trackingBranchRemote !== config.remote) {
+ if (config.pull) {
+ $dialog.find("input[value='PULL_FROM_CURRENT']").prop("disabled", true);
+ $dialog.find("input[value='PULL_FROM_SELECTED']").prop("checked", true).trigger("change");
+ } else {
+ $dialog.find("input[value='PUSH_TO_CURRENT']").prop("disabled", true);
+ $dialog.find("input[value='PUSH_TO_SELECTED']").prop("checked", true).trigger("change");
+ }
+ }
+
+ $dialog.on("click", ".fetchBranches", function () {
+ const tracker = ProgressDialog.newProgressTracker();
+ ProgressDialog.show(Git.fetchRemote(config.remote, tracker), tracker)
+ .then(function () {
+ fillBranches(config, $dialog);
+ }).catch(function (err) {
+ throw ErrorHandler.showError(err, Strings.ERROR_FETCH_REMOTE);
+ });
+ });
+ fillBranches(config, $dialog);
+
+ if (config._usernamePasswordDisabled) {
+ $dialog.find("input[name='username'],input[name='password'],input[name='saveToUrl']").prop("disabled", true);
+ }
+ };
+
+ exports.collectValues = function (config, $dialog) {
+ const action = $dialog.find("input[name='action']:checked").val();
+ if (action === "PULL_FROM_CURRENT" || action === "PUSH_TO_CURRENT") {
+
+ if (config.currentTrackingBranch) {
+ config.branch = config.currentTrackingBranch.substring(config.remote.length + 1);
+ } else {
+ config.branch = config.currentBranchName;
+ config.pushToNew = true;
+ }
+
+ } else if (action === "PULL_FROM_SELECTED" || action === "PUSH_TO_SELECTED") {
+ config.branch = $dialog.find(".branchSelect").val().substring(config.remote.length + 1);
+ config.setBranchAsTracking = $dialog.find("input[name='setBranchAsTracking']").is(":checked");
+ }
+
+ config.strategy = $dialog.find("input[name='strategy']:checked").val();
+ config.tags = $dialog.find("input[name='send_tags']:checked").val();
+ config.noVerify = $dialog.find("input[name='push-no-verify']:checked").val();
+
+ config.remoteUsername = $dialog.find("input[name='username']").val();
+ config.remotePassword = $dialog.find("input[name='password']").val();
+
+ // new url that has to be set for merging
+ let remoteUrlNew;
+ if (config.remoteUrl.match(/^https?:/)) {
+ const url = new URL(config.remoteUrl);
+ url.username = config.remoteUsername;
+ url.password = config.remotePassword;
+ remoteUrlNew = url.toString();
+ }
+
+ // assign remoteUrlNew only if it's different from the original url
+ if (remoteUrlNew && config.remoteUrl !== remoteUrlNew) {
+ config.remoteUrlNew = remoteUrlNew;
+ }
+
+ // old url that has to be put back after merging
+ const saveToUrl = $dialog.find("input[name='saveToUrl']").is(":checked");
+ // assign restore branch only if remoteUrlNew has some value
+ if (config.remoteUrlNew && !saveToUrl) {
+ config.remoteUrlRestore = config.remoteUrl;
+ }
+ };
+
+});
diff --git a/src/extensions/default/Git/src/dialogs/templates/clone-dialog.html b/src/extensions/default/Git/src/dialogs/templates/clone-dialog.html
new file mode 100644
index 0000000000..3b5457c1be
--- /dev/null
+++ b/src/extensions/default/Git/src/dialogs/templates/clone-dialog.html
@@ -0,0 +1,66 @@
+
+
+
+
+
{{Strings.ENTER_REMOTE_GIT_URL}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{Strings.CREDENTIALS}}
+
+
+
+ {{Strings.SAVE_CREDENTIALS_HELP}}
+
+
+
+
+
+ {{Strings.USERNAME}}:
+
+
+
+
+
+
+
+
+ {{Strings.PASSWORD}}:
+
+
+
+
+
+
+
+
+
+ {{Strings.SAVE_CREDENTIALS_IN_URL}}
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ {{> remotes}}
+
+
+
+ {{Strings.PULL_BEHAVIOR}}
+
+
+
+ {{Strings.PULL_DEFAULT}}
+
+
+
+
+
+ {{Strings.PULL_AVOID_MERGING}}
+
+
+
+
+
+ {{Strings.PULL_MERGE_NOCOMMIT}}
+
+
+
+
+
+ {{Strings.PULL_REBASE}}
+
+
+
+
+
+ {{Strings.PULL_RESET}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{Strings.CREDENTIALS}}
+
+
+
+ {{Strings.SAVE_CREDENTIALS_HELP}}
+
+
+
+
+
+ {{Strings.USERNAME}}:
+
+
+
+
+
+
+
+
+ {{Strings.PASSWORD}}:
+
+
+
+
+
+
+
+
+
+ {{Strings.SAVE_CREDENTIALS_IN_URL}}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ {{> remotes}}
+
+
+
+ {{Strings.PUSH_BEHAVIOR}}
+
+
+
+
+ {{Strings.PUSH_DEFAULT}}
+
+
+
+
+
+ {{Strings.PUSH_FORCED}}
+
+
+
+
+
+ {{Strings.PUSH_DELETE_BRANCH}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{Strings.PUSH_SEND_TAGS}}
+
+
+
+
+ {{Strings.SKIP_PRE_PUSH_CHECKS}}
+
+
+
+
+ {{Strings.CREDENTIALS}}
+
+
+
+ {{Strings.SAVE_CREDENTIALS_HELP}}
+
+
+
+
+
+ {{Strings.USERNAME}}:
+
+
+
+
+
+
+
+
+ {{Strings.PASSWORD}}:
+
+
+
+
+
+
+
+
+
+ {{Strings.SAVE_CREDENTIALS_IN_URL}}
+
+
+
+
+
+
+
+
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 @@
+
+
+ {{Strings.CURRENT_TRACKING_BRANCH}}:
+
+
+ {{config.currentTrackingBranch}}
+ {{^config.currentTrackingBranch}}
+ none - "{{config.currentBranchName}}" branch will be created on remote
+ {{/config.currentTrackingBranch}}
+
+
+
+
+
+ {{Strings.TARGET_BRANCH}}
+
+
+ {{modeLabel}} {{Strings._CURRENT_TRACKING_BRANCH}}
+
+
+
+
+
+ {{modeLabel}} {{Strings._ANOTHER_BRANCH}}
+
+
+
+
+
+
+
+ {{Strings.SET_THIS_BRANCH_AS_TRACKING}}
+
+
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 @@
+
+
+
+
+
+
+ {{String.AUTHOR}}
+
+
+
+
+ {{#blameStats}}
+
+
+ {{authorName}}
+
+
+ {{percentage}}% ({{lines}} {{Strings._LINES}})
+
+
+ {{/blameStats}}
+
+
+
+
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}}
+
+ {{name}}
+
+ {{#lines}}
+
+ {{numLineOld}}
+ {{numLineNew}}
+ {{{line}}}
+
+ {{/lines}}
+
+ {{/files}}
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ {{Strings.PLEASE_WAIT}}
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+ {{#title}}
+
+ {{/title}}
+
+
{{{output}}}
+ {{#question}}
+
{{question}}
+ {{/question}}
+
+
+
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 @@
+
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 @@
+
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 @@
+
+
+
+
+
+
+ {{S.BUTTON_COMMIT}}
+
+
+
+
+ {{S.BUTTON_REBASE_CONTINUE}}
+
+
+ {{S.BUTTON_REBASE_SKIP}}
+
+
+ {{S.BUTTON_REBASE_ABORT}}
+
+
+
+
+
+
+
+ {{S.BUTTON_COMMIT}}
+
+
+ {{S.BUTTON_MERGE_ABORT}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{S.GIT_INIT}}
+ {{S.GIT_CLONE}}
+
+
+
+
+
+
+
+
+
×
+
+
+
+
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 @@
+
+
+
+
+
+
+ {{Strings.ENABLE_GIT}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{Strings.CLEAR_WHITESPACE_ON_FILE_SAVE}}
+
+
+
+
+
+
{{Strings.SYSTEM_CONFIGURATION}}
+
+ {{#gitNotFound}}
+
+ {{Strings.GIT_NOT_FOUND_MESSAGE}}
+
+ {{/gitNotFound}}
+
+ {{Strings.PATH_TO_GIT_EXECUTABLE}}:
+ {{#gitNotFound}}
+
+ {{/gitNotFound}}
+
+
+
+
+ {{Strings.DEFAULT_GIT_TIMEOUT}}:
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
{{{bodyMarkdown}}}
+
+
+
+
+ Load more files from this commit
+
+
+
+
+
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"
});