UNPKG

quadre-git

Version:
195 lines (164 loc) 6.56 kB
import { _, FileSystem, ProjectManager } from "./brackets-modules"; import EventEmitter from "./EventEmitter"; import * as Events from "./Events"; import * as Git from "./git/GitCli"; import * as Preferences from "./Preferences"; import * as Promise from "bluebird"; let ignoreEntries = []; let newPaths = []; let modifiedPaths = []; function loadIgnoreContents() { const defer = Promise.defer(); const gitRoot = Preferences.get("currentGitRoot"); let excludeContents; let gitignoreContents; const finish = _.after(2, () => defer.resolve(excludeContents + "\n" + gitignoreContents)); FileSystem.getFileForPath(gitRoot + ".git/info/exclude").read((err, content) => { excludeContents = err ? "" : content; finish(); }); FileSystem.getFileForPath(gitRoot + ".gitignore").read((err, content) => { gitignoreContents = err ? "" : content; finish(); }); return defer.promise; } 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((content: string) => { const gitRoot = Preferences.get("currentGitRoot"); ignoreEntries = _.compact(_.map(content.split("\n"), (_line) => { // Rules: http://git-scm.com/docs/gitignore let line = _line; let type = "deny"; let leadingSlash; let trailingSlash; let regex; line = line.trim(); if (!line || line.indexOf("#") === 0) { return null; } // 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 }; })); }); } function isIgnored(path) { let ignored = false; _.forEach(ignoreEntries, (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; } function _refreshOpenFiles() { $("#working-set-list-container").find("li").each(function () { const $li = $(this); const data = $li.data("file"); if (data) { const fullPath = data.fullPath; $li.toggleClass("git-ignored", isIgnored(fullPath)) .toggleClass("git-new", isNew(fullPath)) .toggleClass("git-modified", isModified(fullPath)); } }); } const refreshOpenFiles = _.debounce(() => _refreshOpenFiles(), 100); function attachEvents() { $("#working-set-list-container").on("contentChanged", refreshOpenFiles).triggerHandler("contentChanged"); } function detachEvents() { $("#working-set-list-container").off("contentChanged", refreshOpenFiles); } if (Preferences.get("markModifiedInTree")) { // init here ProjectManager.addClassesProvider((data) => { const fullPath = data.fullPath; if (isIgnored(fullPath)) { return "git-ignored"; } if (isNew(fullPath)) { return "git-new"; } if (isModified(fullPath)) { return "git-modified"; } return null; }); // this will refresh ignore entries when .gitignore is modified EventEmitter.on(Events.BRACKETS_FILE_CHANGED, (evt, file) => { if (file.fullPath === Preferences.get("currentGitRoot") + ".gitignore") { refreshIgnoreEntries().finally(() => { refreshOpenFiles(); }); } }); // this will refresh new/modified paths on every status results EventEmitter.on(Events.GIT_STATUS_RESULTS, (files) => { const gitRoot = Preferences.get("currentGitRoot"); newPaths = []; modifiedPaths = []; files.forEach((entry) => { const _isNew = entry.status.indexOf(Git.FILE_STATUS.UNTRACKED) !== -1 || entry.status.indexOf(Git.FILE_STATUS.ADDED) !== -1; const 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, () => { refreshIgnoreEntries(); attachEvents(); }); // this will clear entries when non-git project is opened EventEmitter.on(Events.GIT_DISABLED, () => { ignoreEntries = []; newPaths = []; modifiedPaths = []; detachEvents(); }); }