function routingConfig(steps) { return function($routeProvider) { $routeProvider = angular.extend($routeProvider, { // AngularJS doesn't support regular expressions in routes. // http://stackoverflow.com/questions/12685085/angularjs-route-how-to-match-star-as-a-path 'whenPath': function(path, depth, route) { for (var i = 1; i <= depth; i++) { path += '/:path' + i; this.when(path, route); } return this; } }); if (steps.length > 0) { $routeProvider. whenPath('/:basedir/.git', 10, {controller:GitCtrl, templateUrl:'common/templates/git.html'}). otherwise({redirectTo:'/' + steps[0].name + '/.git/'}); } else { $routeProvider. whenPath('/.git', 10, {controller:GitCtrl, templateUrl:'common/templates/git.html'}). otherwise({redirectTo:'/.git/'}); } } } angular.module('GitObjectBrowser', ['ngResource']) .config(routingConfig(config.steps)) .directive('scrollBottom', function() { return function(scope, elm, attr) { var rawDomElement = elm[0]; angular.element(window).unbind('scroll'); angular.element(window).bind('scroll', function() { if (! scope.scrollBottomEnabled) return; var rectObject = rawDomElement.getBoundingClientRect(); if (rectObject.bottom - window.innerHeight < 50) { scope.$apply(attr.scrollBottom); } }); }; }) .directive('entryIcon', function($parse) { return function(scope, element, attrs) { var icons = { 'directory': 'icon-folder-open', 'ref': 'icon-map-marker', 'reflog': 'icon-file', 'info_refs': 'icon-map-marker', 'packed_refs': 'icon-map-marker', 'index': 'icon-list', 'file': 'icon-file', 'object': 'icon-comment', 'blob': 'icon-file', 'tree': 'icon-folder-open', 'commit': 'icon-ok', 'tag': 'icon-tag', 'ofs_delta': 'icon-arrow-up', 'ref_delta': 'icon-arrow-down' }; var entryType = ($parse(attrs.entryIcon))(scope); element.addClass(icons[entryType]); } }) .directive('modeIcon', function($parse) { return function(scope, element, attrs) { var mode = ($parse(attrs.modeIcon))(scope); var iconClass; if (120000 <= mode) { iconClass = 'icon-share-alt'; } else if (100000 <= mode) { iconClass = 'icon-file'; } else { iconClass = 'icon-folder-open'; } element.addClass(iconClass); } }) .directive('refHref', function($parse, $rootScope) { return function(scope, element, attrs) { var entry = ($parse(attrs.refHref))(scope); var href = ""; var sha1 = null; if (typeof(entry) == 'string') { sha1 = entry; } else if (entry && entry.sha1) { sha1 = entry.sha1; } if (sha1 !== null) { href = '#' + $rootScope.basedir + '/.git/objects/' + sha1.substr(0, 2) + '/' + sha1.substr(2); } else if (entry && entry.ref) { href = '#' + $rootScope.basedir + '/.git/' + entry.ref; } element.attr('href', href); } }) .filter('unixtime', function($filter) { return function(input, format) { return ($filter('date'))(input * 1000, format); } }); function GitCtrl($scope, $location, $routeParams, $rootScope, $resource, $http) { if (! $rootScope.diffCache) $rootScope.diffCache = {}; if (! $rootScope.noteCache) $rootScope.noteCache = {}; // reset scrollBottom event handler angular.element(window).unbind('scroll'); var objectTable = function(entries) { var rows = []; var hash = {}; angular.forEach(entries, function(entry) { hash[entry.basename] = entry; }) var hex = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']; for (var i = 0; i < 16; i++) { var cols = []; for (var j = 0; j < 16; j++) { if (hash[hex[i] + hex[j]]) { cols[j] = hash[hex[i] + hex[j]]; } else { cols[j] = {'basename': hex[i] + hex[j], 'type': 'empty'}; } } rows[i] = cols; } return rows; } var findIndexEntry = function(sha1) { var entries = $scope.object.entries; var entry = null; angular.forEach(entries, function(value) { if (value.sha1 == sha1) { entry = value; return; } }); return entry; }; var indexEntryKeys = function(version) { var keys = [ 'ctime', 'cnano', 'mtime', 'mnano', 'dev', 'ino', 'object_type', 'unix_permission', 'uid', 'gid', 'size', 'sha1', 'assume_valid_flag', 'extended_flag', 'stage', 'name_length' ] if (version > 2) { keys = keys.concat([ 'skip_worktree', 'intent_to_add', ]); } keys.push('path') return keys; }; var resourceLoaded = function(json) { $scope.workingdir = json.workingdir; $scope.root = json.root; $scope.gitPath = json.path; if (json.path == "") { $scope.path = ".git"; } else { $scope.path = ".git/" + json.path; } $scope.object = json.object; $scope.keys = indexEntryKeys($scope.object.version); var template; if (json.path == "objects") { template = "objects"; $scope.objectTable = objectTable($scope.object.entries); loadDiffData(); } else if (json.type == "index" && $routeParams.sha1) { template = "index_entry"; $scope.entry = findIndexEntry($routeParams.sha1); } else if (json.type == "packed_refs" && $routeParams.ref) { template = json.type; var entries = []; angular.forEach($scope.object.entries, function(entry) { if (entry.ref == $routeParams.ref) { entries.push(entry); $scope.limited = true; } }); $scope.object.entries = entries; } else if (json.type == "packed_object") { template = json.type; $scope.unpacked = json.unpacked; } else if (json.type == "directory") { template = json.type; loadDiffData(); } else { template = json.type; } $scope.template = 'common/templates/' + template + '.html'; }; var loadNote = function() { if (! config.loadNote) return; var basedir, url; if ($rootScope.basedir) { basedir = $rootScope.basedir; url = 'notes' + basedir + '.html'; } else { basedir = ''; url = 'note.html'; } if ($rootScope.noteCache[basedir] !== undefined) { $rootScope.note = $rootScope.noteCache[basedir]; return; } $http.get(url) .success(function(data) { $rootScope.noteCache[basedir] = data; $rootScope.note = data; }).error(function() { $rootScope.noteCache[basedir] = ''; $rootScope.note = null; }); }; var loadDiffData = function() { if (! config.loadDiff) return; var stepPath = $rootScope.basedir; if (! stepPath) return; $scope.diff = {} var base = '.git/'; if ($scope.object.path !== '') { base = base + $scope.object.path + '/'; } angular.forEach($scope.object.entries, function(entry) { $scope.diff[base + entry.basename] = null; }); var cache = $rootScope.diffCache[stepPath]; if (cache) { diffDataLoaded(cache); return } $http.get('json' + stepPath + '/_diff.json').success(function(diffData) { $rootScope.diffCache[stepPath] = diffData; diffDataLoaded(diffData); }).error(function() { $rootScope.diffCache[stepPath] = []; }); }; var diffDataLoaded = function(data) { var newDiffData = {} angular.forEach($scope.diff, function(value, key) { newDiffData[key] = isDiffEntry(data, key); }); $scope.diff = newDiffData; } var isDiffEntry = function(data, key) { for(var i = 0; i < data.length; i++) { if (('.git/' + data[i]).indexOf(key) === 0) { return '#fee'; } } return null; } $scope.resourceError = function(path) { return function(data, status, headers, config) { if (status == 404) { resourceNotFound(path); } else { $scope.status = status; $scope.path = path; $scope.template = 'common/templates/error.html'; } }; }; var packedObjectFinder = function(sha1) { var indexes = []; var find = function() { $http.get('json' + $rootScope.basedir + '/objects/pack.json') .success(startLoadPackDigest) .error(showNotFound); }; var startLoadPackDigest = function(json) { angular.forEach(json.object.entries, function(entry) { if (entry.basename.match(/\.idx$/)) { indexes.push(entry); } }); loadPackDigest(); }; var loadPackDigest = function() { if (indexes.length == 0) { showNotFound(); return; } var entry = indexes.shift(); $http.get('json' + $rootScope.basedir + '/objects/pack/' + entry.basename + '.json') .success(findPackObject) .error(showNotFound); }; var findPackObject = function(json) { var i = 0; angular.forEach(json.object.entries, function(digestSha1) { if (sha1 <= digestSha1) { return; } i++; }); if (i == 0) { loadPackDigest(); return; } $http.get('json' + $rootScope.basedir + '/' + json.path + '/sha1/' + i + '.json') .success(loadPagedIndex) .error(showNotFound); }; var loadPagedIndex = function(json) { var found = false; angular.forEach(json.object.entries, function(entry) { if (entry.sha1 == sha1) { var path = json.path.replace(/.idx$/, '.pack'); found = true; $routeParams.offset = entry.offset; loadJson([$rootScope.basedir, path]); } }); if (! found) loadPackDigest(); } find(); }; var resourceNotFound = function(path) { $scope.path = path; if (path.match(/^json\/[^\/]+\/objects\/([0-9a-f]{2})\/([0-9a-f]{38})\.json$/)) { packedObjectFinder(RegExp.$1 + RegExp.$2); } else if (path.match(/^json\/[^\/]+\/(refs\/.+)\.json$/)) { $routeParams.ref = RegExp.$1; loadJson([$rootScope.basedir, 'packed-refs']) } else { $scope.template = 'common/templates/notfound.html'; } }; var showNotFound = function() { $scope.template = 'common/templates/notfound.html'; } // ['', '/.git/xxx'] or // ['/basedir', '/.git/xxx'] var buildPath = function() { var path = ''; var basedir = ''; if ($routeParams['basedir'] !== undefined) { basedir = '/' + $routeParams['basedir']; } for (var i = 1; i <= 10; i++) { if ($routeParams['path' + i]) { if (i > 1) path += '/'; path += $routeParams['path' + i]; } } return [basedir, path]; } var loadJson = function(path) { $rootScope.basedir = path[0]; $scope.template = 'common/templates/loading.html'; if (path[1].match(/^objects\/pack\/pack-[0-9a-f]{40}\.pack$/) && $routeParams.offset) { var offset = '0000' + $routeParams.offset; path = 'json' + path[0] + '/' + path[1] + '/' + offset.slice(-2) + '/' + offset.slice(-4, -2) + '/' + $routeParams.offset + '.json'; } else if (path[1].match(/^objects\/pack\/pack-[0-9a-f]{40}\.idx$/)) { var order = $routeParams.order == 'offset' ? 'offset' : 'sha1'; var page = $routeParams.page || 1; path = 'json' + path[0] + '/' + path[1] + '/' + order + '/' + page + '.json'; } else { if (path[1] == '') path[1] = '_git'; path = 'json' + path[0] + '/' + path[1] + '.json'; } $http.get(path).success(resourceLoaded).error($scope.resourceError(path)); }; loadJson(buildPath()); loadNote(); } function PackFileCtrl($scope, $location, $routeParams) { $scope.indexUrl = $scope.path.replace(/.pack$/, '.idx'); } function PackIndexCtrl($scope, $location, $routeParams, $rootScope, $resource, $http) { $scope.packUrl = $scope.path.replace(/.idx$/, '.pack'); $scope.lastPage = 1; $scope.scrollBottomEnabled = true; var resourceLoaded = function(json) { $scope.object.entries = $scope.object.entries.concat(json.object.entries); var last_page = Math.ceil(json.entry_count / json.per_page); $scope.scrollBottomEnabled = (json.page < last_page); $scope.loading = false; } $scope.loadNextPage = function() { $scope.lastPage += 1; var order = $routeParams.order == 'offset' ? 'offset' : 'sha1'; var path = 'json' + $rootScope.basedir + '/' + $scope.gitPath + '/' + order + '/' + $scope.lastPage + '.json'; $http.get(path).success(resourceLoaded).error($scope.resourceError(path)); }; $scope.scrollBottom = function() { $scope.scrollBottomEnabled = false; $scope.loading = true; $scope.loadNextPage(); } } function MenuCtrl($scope, $location, $routeParams) { $scope.steps = config.steps; $scope.stepPrev = function() { var idx = getStepIndex(); if (idx.index > 0) { $location.path('/' + $scope.steps[idx.index - 1].name + '/' + idx.file); } } $scope.stepNext = function() { var idx = getStepIndex(); if (idx.index < $scope.steps.length - 1) { $location.path('/' + $scope.steps[idx.index + 1].name + '/' + idx.file); } } var getStepIndex = function() { var path = $location.path(); if (! path.match(/\/([^\/]+)\/(.+)/)) return null; var stepName = RegExp.$1; var file = RegExp.$2; var obj = { stepName: stepName, file: file, index: 0 }; for (var i = 0; i < $scope.steps.length; i++) { if (stepName == $scope.steps[i].name) { obj.index = i; return obj; } } return obj; } }