angular .module("anonymous-github", [ "ngRoute", "ngSanitize", "ui.ace", "ngPDFViewer", "pascalprecht.translate", "admin", ]) .config([ "$routeProvider", "$locationProvider", "$translateProvider", function ($routeProvider, $locationProvider, $translateProvider) { $translateProvider.useStaticFilesLoader({ prefix: "/i18n/locale-", suffix: ".json", }); $translateProvider.preferredLanguage("en"); $routeProvider .when("/", { templateUrl: "/partials/home.htm", controller: "homeController", title: "Anonymous GitHub – Share the code, not the author", }) .when("/dashboard", { templateUrl: "/partials/dashboard.htm", controller: "unifiedDashboardController", title: "Your anonymizations – Anonymous GitHub", }) .when("/pr-dashboard", { redirectTo: "/dashboard", }) .when("/anonymize/:repoId?", { templateUrl: "/partials/anonymize.htm", controller: "anonymizeController", title: "New anonymization – Anonymous GitHub", }) .when("/pull-request-anonymize/:pullRequestId?", { templateUrl: "/partials/anonymize.htm", controller: "anonymizeController", title: "Anonymize a pull request – Anonymous GitHub", }) .when("/status/:repoId", { templateUrl: "/partials/status.htm", controller: "statusController", title: "Repository status – Anonymous GitHub", }) .when("/conferences", { templateUrl: "/partials/conferences.htm", controller: "conferencesController", title: "Your conferences – Anonymous GitHub", }) .when("/conference/new", { templateUrl: "/partials/newConference.htm", controller: "newConferenceController", title: "New conference – Anonymous GitHub", }) .when("/conference/:conferenceId/edit", { templateUrl: "/partials/newConference.htm", controller: "newConferenceController", title: "Edit conference – Anonymous GitHub", }) .when("/conference/:conferenceId", { templateUrl: "/partials/conference.htm", controller: "conferenceController", title: "Conference – Anonymous GitHub", }) .when("/faq", { templateUrl: "/partials/faq.htm", controller: "faqController", title: "FAQ – Anonymous GitHub", }) .when("/profile", { templateUrl: "/partials/profile.htm", controller: "profileController", title: "Your settings – Anonymous GitHub", }) .when("/claim", { templateUrl: "/partials/claim.htm", controller: "claimController", title: "Claim an anonymization – Anonymous GitHub", }) .when("/pr/:pullRequestId", { templateUrl: "/partials/pullRequest.htm", controller: "pullRequestController", title: "Anonymous pull request – Anonymous GitHub", reloadOnUrl: false, }) .when("/r/:repoId/:path*?", { templateUrl: "/partials/explorer.htm", controller: "exploreController", title: "Anonymous repository – Anonymous GitHub", reloadOnUrl: false, }) .when("/repository/:repoId/:path*?", { templateUrl: "/partials/explorer.htm", controller: "exploreController", title: "Anonymous repository – Anonymous GitHub", reloadOnUrl: false, }) .when("/admin/", { templateUrl: "/partials/admin/repositories.htm", controller: "repositoriesAdminController", title: "Admin · Repositories – Anonymous GitHub", }) .when("/admin/users", { templateUrl: "/partials/admin/users.htm", controller: "usersAdminController", title: "Admin · Users – Anonymous GitHub", }) .when("/admin/users/:username", { templateUrl: "/partials/admin/user.htm", controller: "userAdminController", title: "Admin · User details – Anonymous GitHub", }) .when("/admin/conferences", { templateUrl: "/partials/admin/conferences.htm", controller: "conferencesAdminController", title: "Admin · Conferences – Anonymous GitHub", }) .when("/admin/queues", { templateUrl: "/partials/admin/queues.htm", controller: "queuesAdminController", title: "Admin · Queues – Anonymous GitHub", }) .when("/404", { templateUrl: "/partials/404.htm", title: "Page not found – Anonymous GitHub", }) .otherwise({ templateUrl: "/partials/404.htm", title: "Page not found – Anonymous GitHub", }); $locationProvider.html5Mode(true); }, ]) .filter("humanFileSize", function () { return humanFileSize; }) .filter("humanTime", function () { return function humanTime(seconds) { if (!seconds) { return "never"; } if (seconds instanceof Date) seconds = Math.round((Date.now() - seconds) / 1000); if (typeof seconds == "string" || typeof seconds == "number") seconds = Math.round((Date.now() - new Date(seconds)) / 1000); var suffix = seconds < 0 ? "from now" : "ago"; // more than 2 days ago display Date if (Math.abs(seconds) > 2 * 60 * 60 * 24) { const now = new Date(); now.setSeconds(now.getSeconds() - seconds); return "on " + now.toLocaleDateString(); } seconds = Math.abs(seconds); var times = [ seconds / 60 / 60 / 24 / 365, // years seconds / 60 / 60 / 24 / 30, // months seconds / 60 / 60 / 24 / 7, // weeks seconds / 60 / 60 / 24, // days seconds / 60 / 60, // hours seconds / 60, // minutes seconds, // seconds ]; var names = ["year", "month", "week", "day", "hour", "minute", "second"]; for (var i = 0; i < names.length; i++) { var time = Math.floor(times[i]); var name = names[i]; if (time > 1) name += "s"; if (time >= 1) return time + " " + name + " " + suffix; } return "0 seconds " + suffix; }; }) .filter("title", function () { return function (str) { if (!str) return str; str = str.toLowerCase(); var words = str.split(" "); var capitalized = words.map(function (word) { return word.charAt(0).toUpperCase() + word.substring(1, word.length); }); return capitalized.join(" "); }; }) .filter("diff", [ "$sce", function ($sce) { const esc = (s) => s.replace(/&/g, "&").replace(//g, ">"); function flushFile(out, file) { if (!file) return; const headerName = file.newPath && file.newPath !== "/dev/null" ? file.newPath : file.oldPath || ""; const status = file.oldPath === "/dev/null" ? "added" : file.newPath === "/dev/null" ? "deleted" : file.oldPath && file.newPath && file.oldPath !== file.newPath ? "renamed" : "modified"; out.push('
'); out.push( '
' + '' + esc(headerName) + "" + '' + status + "
" ); if (file.lines.length) { out.push(''); for (const line of file.lines) { out.push( '' + '" + '" + '" + '" + "" ); } out.push("
' + (line.oldNo || "") + "' + (line.newNo || "") + "' + (line.kind === "add" ? "+" : line.kind === "remove" ? "-" : line.kind === "hunk" ? "@" : "") + "' + esc(line.text) + "
"); } out.push("
"); } return function (str) { if (!str) return str; const out = []; let file = null; let oldNo = 0; let newNo = 0; const ensureFile = () => { if (!file) file = { oldPath: "", newPath: "", lines: [] }; return file; }; const startNewFileIfNeeded = () => { if (file && (file.lines.length || file.oldPath || file.newPath)) { flushFile(out, file); file = null; } }; const lines = str.split("\n"); for (let i = 0; i < lines.length; i++) { const ln = lines[i]; if (ln.startsWith("diff --git")) { startNewFileIfNeeded(); ensureFile(); continue; } if (ln.startsWith("--- ")) { // New file boundary if the previous file already had lines. if (file && file.lines.length) startNewFileIfNeeded(); ensureFile().oldPath = ln.replace(/^--- (a\/)?/, "").trim(); continue; } if (ln.startsWith("+++ ")) { ensureFile().newPath = ln.replace(/^\+\+\+ (b\/)?/, "").trim(); continue; } if ( ln.startsWith("index ") || ln.startsWith("similarity index") || ln.startsWith("rename ") || ln.startsWith("new file mode") || ln.startsWith("deleted file mode") || ln.startsWith("Binary files") ) { continue; } if (ln.startsWith("@@")) { const m = ln.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/); if (m) { oldNo = parseInt(m[1], 10); newNo = parseInt(m[2], 10); } ensureFile().lines.push({ kind: "hunk", oldNo: "", newNo: "", text: ln }); continue; } if (!file) continue; if (ln.startsWith("+")) { file.lines.push({ kind: "add", oldNo: "", newNo: newNo, text: ln.slice(1) }); newNo++; } else if (ln.startsWith("-")) { file.lines.push({ kind: "remove", oldNo: oldNo, newNo: "", text: ln.slice(1) }); oldNo++; } else { file.lines.push({ kind: "ctx", oldNo: oldNo, newNo: newNo, text: ln.startsWith(" ") ? ln.slice(1) : ln }); oldNo++; newNo++; } } flushFile(out, file); return $sce.trustAsHtml(out.join("")); }; }, ]) .directive("markdown", [ "$location", function ($location) { return { restrict: "E", scope: { terms: "=", options: "=", content: "=", }, link: function (scope, elem, attrs) { function update() { elem.html(renderMD(scope.content, $location.url() + "/../")); } scope.$watch(attrs.terms, update); scope.$watch("terms", update); scope.$watch("options", update); scope.$watch("content", update); }, }; }, ]) .directive("tree", [ function () { return { restrict: "E", scope: { file: "=", parent: "@" }, controller: [ "$element", "$scope", "$routeParams", "$compile", function ($element, $scope, $routeParams, $compile) { $scope.repoId = document.location.pathname.split("/")[2]; $scope.opens = {}; if ($routeParams.path) { let accumulatedPath = ""; $routeParams.path.split("/").forEach((f) => { $scope.opens[accumulatedPath + "/" + f] = true; accumulatedPath = accumulatedPath + "/" + f; }); } const toArray = function (arr) { const output = []; const keys = { "": { child: output } }; for (let file of arr) { let current = keys[file.path].child; let fPath = `${file.path}/${file.name}`; if (fPath.startsWith("/")) { fPath = fPath.substring(1); } if (file.size != null) { // it is a file current.push({ name: file.name, size: file.size, sha: file.sha, }); } else { const dir = { name: file.name, child: [], }; keys[fPath] = dir; current.push(dir); } } return output; }; const sortFiles = (f1, f2) => { const f1d = !!f1.child; const f2d = !!f2.child; if (f1d && f2d) { return f1.name - f2.name; } if (f1d) { return -1; } if (f2d) { return 1; } return f1.name - f2.name; }; function isTruncated(folderPath) { const truncated = ($scope.$parent.options && $scope.$parent.options.truncatedFolders) || []; if (!truncated.length) return false; const normalized = folderPath.startsWith("/") ? folderPath.substring(1) : folderPath; return truncated.indexOf(normalized) !== -1; } function generate(current, parentPath) { if (!current) return ""; current = current.sort(sortFiles); const afiles = current; let output = ""; } function display() { $element.html(""); const output = generate(toArray($scope.file).sort(sortFiles), ""); $compile(output)($scope, (clone) => { $element.append(clone); }); } // #496 — expand folders whose children are already loaded so // reviewers see the whole tree without clicking through. Skip // folders with empty children to avoid emitting an empty