diff --git a/data/model.py b/data/model.py
index 16695ffae..66aa75008 100644
--- a/data/model.py
+++ b/data/model.py
@@ -770,6 +770,16 @@ def get_all_repo_users(namespace_name, repository_name):
Repository.name == repository_name)
+def get_repository_for_resource(resource_key):
+ joined = Repository.select().join(RepositoryBuild)
+ query = joined.where(RepositoryBuild.resource_key == resource_key).limit(1)
+ result = list(query)
+ if not result:
+ return None
+
+ return result[0]
+
+
def get_repository(namespace_name, repository_name):
try:
return Repository.get(Repository.name == repository_name,
diff --git a/endpoints/api.py b/endpoints/api.py
index d3a625e59..93b374dcf 100644
--- a/endpoints/api.py
+++ b/endpoints/api.py
@@ -1157,6 +1157,7 @@ def build_status_view(build_obj):
'started': build_obj.started,
'display_name': build_obj.display_name,
'status': status,
+ 'resource_key': build_obj.resource_key
}
@@ -1221,6 +1222,14 @@ def request_repo_build(namespace, repository):
logger.debug('User requested repository initialization.')
dockerfile_id = request.get_json()['file_id']
+ # Check if the dockerfile resource has already been used. If so, then it can only be reused if the
+ # user has access to the repository for which it was used.
+ associated_repository = model.get_repository_for_resource(dockerfile_id)
+ if associated_repository:
+ if not ModifyRepositoryPermission(associated_repository.namespace, associated_repository.name):
+ abort(403)
+
+ # Start the build.
repo = model.get_repository(namespace, repository)
token = model.create_access_token(repo, 'write')
display_name = user_files.get_file_checksum(dockerfile_id)
diff --git a/static/css/quay.css b/static/css/quay.css
index 93ac21c85..fa9994afd 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -846,9 +846,19 @@ i.toggle-icon:hover {
display: none;
}
+.visible-md-inline {
+ display: none;
+}
+
.hidden-sm-inline {
display: inline;
-}
+}
+
+@media (min-width: 991px) {
+ .visible-md-inline {
+ display: inline;
+ }
+}
@media (max-width: 991px) and (min-width: 768px) {
.visible-sm-inline {
@@ -1449,7 +1459,10 @@ p.editable:hover i {
}
.repo .header {
- margin-bottom: 10px;
+ margin-bottom: 20px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #eee;
+
position: relative;
}
@@ -1499,36 +1512,12 @@ p.editable:hover i {
display: inline-block;
}
-.repo .status-boxes {
- float: right;
- margin-bottom: 20px;
-}
-
-.repo .status-boxes .status-box {
- cursor: pointer;
+.repo .repo-controls .count {
display: inline-block;
- border: 1px solid #eee;
- border-radius: 4px;
-}
-
-.repo .status-boxes .status-box .title {
- padding: 4px;
- display: inline-block;
- padding-left: 10px;
- padding-right: 10px;
-}
-
-.repo .status-boxes .status-box .title i {
- margin-right: 6px;
-}
-
-.repo .status-boxes .status-box .count {
- display: inline-block;
- background-image: linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);
- padding: 4px;
- padding-left: 10px;
- padding-right: 10px;
+ padding-left: 4px;
+ padding-right: 4px;
font-weight: bold;
+ color: #428bca;
transform: scaleX(0);
-webkit-transform: scaleX(0);
@@ -1539,13 +1528,13 @@ p.editable:hover i {
-moz-transition: -moz-transform 500ms ease-in-out;
}
-.repo .status-boxes .status-box .count.visible {
+.repo .repo-controls .count.visible {
transform: scaleX(1);
-webkit-transform: scaleX(1);
-moz-transform: scaleX(1);
}
-.repo .pull-command {
+.repo .repo-controls {
float: right;
display: inline-block;
font-size: 0.8em;
@@ -1759,7 +1748,8 @@ p.editable:hover i {
}
.repo-build .build-pane .build-logs .command-title,
-.repo-build .build-pane .build-logs .log-entry .message {
+.repo-build .build-pane .build-logs .log-entry .message,
+.repo-build .build-pane .build-logs .log-entry .message span {
font-family: Consolas, "Lucida Console", Monaco, monospace;
font-size: 13px;
}
@@ -3196,4 +3186,9 @@ pre.command:before {
font-size: 14px;
font-weight: bold;
padding: 4px 8px;
+}
+
+.file-drop {
+ padding: 10px;
+ margin: 10px;
}
\ No newline at end of file
diff --git a/static/directives/dockerfile-build-dialog.html b/static/directives/dockerfile-build-dialog.html
new file mode 100644
index 000000000..4848b341c
--- /dev/null
+++ b/static/directives/dockerfile-build-dialog.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+
+
+
diff --git a/static/directives/dockerfile-build-form.html b/static/directives/dockerfile-build-form.html
new file mode 100644
index 000000000..da808ec04
--- /dev/null
+++ b/static/directives/dockerfile-build-form.html
@@ -0,0 +1,19 @@
+
diff --git a/static/js/app.js b/static/js/app.js
index 22e0bdfad..e2e90d22e 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -102,7 +102,7 @@ function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
-quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce'], function($provide, cfpLoadingBarProvider) {
+quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml'], function($provide, cfpLoadingBarProvider) {
cfpLoadingBarProvider.includeSpinner = false;
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
@@ -1143,6 +1143,13 @@ quayApp.directive('dockerAuthDialog', function () {
});
+quayApp.filter('reverse', function() {
+ return function(items) {
+ return items.slice().reverse();
+ };
+});
+
+
quayApp.filter('bytes', function() {
return function(bytes, precision) {
if (!bytes || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return 'Unknown';
@@ -2693,6 +2700,239 @@ quayApp.directive('buildProgress', function () {
return directiveDefinitionObject;
});
+
+quayApp.directive('dockerfileBuildDialog', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/dockerfile-build-dialog.html',
+ replace: false,
+ transclude: false,
+ restrict: 'C',
+ scope: {
+ 'repository': '=repository',
+ 'showNow': '=showNow',
+ 'buildStarted': '&buildStarted'
+ },
+ controller: function($scope, $element) {
+ $scope.building = false;
+ $scope.uploading = false;
+ $scope.startCounter = 0;
+
+ $scope.handleBuildStarted = function(build) {
+ $('#dockerfilebuildModal').modal('hide');
+ if ($scope.buildStarted) {
+ $scope.buildStarted({'build': build});
+ }
+ };
+
+ $scope.handleBuildFailed = function(message) {
+ $scope.errorMessage = message;
+ };
+
+ $scope.startBuild = function() {
+ $scope.errorMessage = null;
+ $scope.startCounter++;
+ };
+
+ $scope.$watch('showNow', function(sn) {
+ if (sn && $scope.repository) {
+ $('#dockerfilebuildModal').modal({});
+ }
+ });
+ }
+ };
+ return directiveDefinitionObject;
+});
+
+
+quayApp.directive('dockerfileBuildForm', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/dockerfile-build-form.html',
+ replace: false,
+ transclude: false,
+ restrict: 'C',
+ scope: {
+ 'repository': '=repository',
+ 'startNow': '=startNow',
+ 'hasDockerfile': '=hasDockerfile',
+ 'uploadFailed': '&uploadFailed',
+ 'uploadStarted': '&uploadStarted',
+ 'buildStarted': '&buildStarted',
+ 'buildFailed': '&buildFailed',
+ 'missingFile': '&missingFile',
+ 'uploading': '=uploading',
+ 'building': '=building'
+ },
+ controller: function($scope, $element, ApiService) {
+ $scope.internal = {'hasDockerfile': false};
+
+ var handleBuildFailed = function(message) {
+ message = message || 'Dockerfile build failed to start';
+
+ var result = false;
+ if ($scope.buildFailed) {
+ result = $scope.buildFailed({'message': message});
+ }
+
+ if (!result) {
+ bootbox.dialog({
+ "message": message,
+ "title": "Cannot start Dockerfile build",
+ "buttons": {
+ "close": {
+ "label": "Close",
+ "className": "btn-primary"
+ }
+ }
+ });
+ }
+ };
+
+ var handleUploadFailed = function(message) {
+ message = message || 'Error with file upload';
+
+ var result = false;
+ if ($scope.uploadFailed) {
+ result = $scope.uploadFailed({'message': message});
+ }
+
+ if (!result) {
+ bootbox.dialog({
+ "message": message,
+ "title": "Cannot upload file for Dockerfile build",
+ "buttons": {
+ "close": {
+ "label": "Close",
+ "className": "btn-primary"
+ }
+ }
+ });
+ }
+ };
+
+ var handleMissingFile = function() {
+ var result = false;
+ if ($scope.missingFile) {
+ result = $scope.missingFile({});
+ }
+
+ if (!result) {
+ bootbox.dialog({
+ "message": 'A Dockerfile or an archive containing a Dockerfile is required',
+ "title": "Missing Dockerfile",
+ "buttons": {
+ "close": {
+ "label": "Close",
+ "className": "btn-primary"
+ }
+ }
+ });
+ }
+ };
+
+ var startBuild = function(fileId) {
+ $scope.building = true;
+
+ var repo = $scope.repository;
+ var data = {
+ 'file_id': fileId
+ };
+
+ var params = {
+ 'repository': repo.namespace + '/' + repo.name
+ };
+
+ ApiService.requestRepoBuild(data, params).then(function(resp) {
+ $scope.building = false;
+ $scope.uploading = false;
+
+ if ($scope.buildStarted) {
+ $scope.buildStarted({'build': resp});
+ }
+ }, function(resp) {
+ $scope.building = false;
+ $scope.uploading = false;
+
+ handleBuildFailed(resp.message);
+ });
+ };
+
+ var conductUpload = function(file, url, fileId, mimeType) {
+ if ($scope.uploadStarted) {
+ $scope.uploadStarted({});
+ }
+
+ var request = new XMLHttpRequest();
+ request.open('PUT', url, true);
+ request.setRequestHeader('Content-Type', mimeType);
+ request.onprogress = function(e) {
+ $scope.$apply(function() {
+ var percentLoaded;
+ if (e.lengthComputable) {
+ $scope.upload_progress = (e.loaded / e.total) * 100;
+ }
+ });
+ };
+ request.onerror = function() {
+ $scope.$apply(function() {
+ handleUploadFailed();
+ });
+ };
+ request.onreadystatechange = function() {
+ var state = request.readyState;
+ if (state == 4) {
+ $scope.$apply(function() {
+ startBuild(fileId);
+ $scope.uploading = false;
+ });
+ return;
+ }
+ };
+ request.send(file);
+ };
+
+ var startFileUpload = function(repo) {
+ $scope.uploading = true;
+ $scope.uploading_progress = 0;
+
+ var uploader = $('#file-drop')[0];
+ if (uploader.files.length == 0) {
+ handleMissingFile();
+ $scope.uploading = false;
+ return;
+ }
+
+ var file = uploader.files[0];
+ $scope.upload_file = file.name;
+
+ var mimeType = file.type || 'application/octet-stream';
+ var data = {
+ 'mimeType': mimeType
+ };
+
+ var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
+ conductUpload(file, resp.url, resp.file_id, mimeType);
+ }, function() {
+ handleUploadFailed('Could not retrieve upload URL');
+ });
+ };
+
+ $scope.$watch('internal.hasDockerfile', function(d) {
+ $scope.hasDockerfile = d;
+ });
+
+ $scope.$watch('startNow', function() {
+ if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
+ startFileUpload();
+ }
+ });
+ }
+ };
+ return directiveDefinitionObject;
+});
+
+
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
quayApp.directive('ngBlur', function() {
return function( scope, elem, attrs ) {
@@ -2702,6 +2942,22 @@ quayApp.directive('ngBlur', function() {
};
});
+quayApp.directive("filePresent", [function () {
+ return {
+ restrict: 'A',
+ scope: {
+ 'filePresent': "="
+ },
+ link: function (scope, element, attributes) {
+ element.bind("change", function (changeEvent) {
+ scope.$apply(function() {
+ scope.filePresent = changeEvent.target.files.length > 0;
+ });
+ });
+ }
+ }
+}]);
+
quayApp.directive('ngVisible', function () {
return function (scope, element, attr) {
scope.$watch(attr.ngVisible, function (visible) {
diff --git a/static/js/controllers.js b/static/js/controllers.js
index 9111a4566..d3ab397c1 100644
--- a/static/js/controllers.js
+++ b/static/js/controllers.js
@@ -341,8 +341,17 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
// Start scope methods //////////////////////////////////////////
+ $scope.buildDialogShowCounter = 0;
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
+ $scope.showNewBuildDialog = function() {
+ $scope.buildDialogShowCounter++;
+ };
+
+ $scope.handleBuildStarted = function(build) {
+ startBuildInfoTimer($scope.repo);
+ };
+
$scope.showBuild = function(buildInfo) {
$location.path('/repository/' + namespace + '/' + name + '/build');
$location.search('current', buildInfo.id);
@@ -765,7 +774,7 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
loadViewInfo();
}
-function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize) {
+function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html) {
var namespace = $routeParams.namespace;
var name = $routeParams.name;
var pollTimerHandle = null;
@@ -784,8 +793,40 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.builds = [];
$scope.polling = false;
+ $scope.buildDialogShowCounter = 0;
+
+ $scope.showNewBuildDialog = function() {
+ $scope.buildDialogShowCounter++;
+ };
+
+ $scope.handleBuildStarted = function(newBuild) {
+ $scope.builds.push(newBuild);
+ $scope.setCurrentBuild(newBuild['id'], true);
+ };
+
$scope.adjustLogHeight = function() {
- $('.build-logs').height($(window).height() - 365);
+ $('.build-logs').height($(window).height() - 385);
+ };
+
+ $scope.askRestartBuild = function(build) {
+ $('#confirmRestartBuildModal').modal({});
+ };
+
+ $scope.restartBuild = function(build) {
+ $('#confirmRestartBuildModal').modal('hide');
+
+ var data = {
+ 'file_id': build['resource_key']
+ };
+
+ var params = {
+ 'repository': namespace + '/' + name
+ };
+
+ ApiService.requestRepoBuild(data, params).then(function(newBuild) {
+ $scope.builds.push(newBuild);
+ $scope.setCurrentBuild(newBuild['id'], true);
+ });
};
$scope.hasLogs = function(container) {
@@ -806,13 +847,23 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
// Find the build.
for (var i = 0; i < $scope.builds.length; ++i) {
if ($scope.builds[i].id == buildId) {
- $scope.setCurrentBuildInternal($scope.builds[i], opt_updateURL);
+ $scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
return;
}
}
};
- $scope.setCurrentBuildInternal = function(build, opt_updateURL) {
+ $scope.processANSI = function(message, container) {
+ var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
+
+ // Note: order is important here.
+ var setup = filter.getSetupHtml();
+ var stream = filter.addInputToStream(message);
+ var teardown = filter.getTeardownHtml();
+ return setup + stream + teardown;
+ };
+
+ $scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
if (build == $scope.currentBuild) { return; }
stopPollTimer();
@@ -822,6 +873,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
$scope.currentParentEntry = null;
$scope.currentBuild = build;
+ $scope.currentBuildIndex = index;
if (opt_updateURL) {
if (build) {
@@ -893,7 +945,6 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
var getBuildStatusAndLogs = function() {
if (!$scope.currentBuild || $scope.polling) { return; }
-
$scope.polling = true;
var params = {
@@ -904,7 +955,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
// Note: We use extend here rather than replacing as Angular is depending on the
// root build object to remain the same object.
- $.extend(true, $scope.currentBuild, resp);
+ $.extend(true, $scope.builds[$scope.currentBuildIndex], resp);
checkPollTimer();
// Load the updated logs for the build.
@@ -913,9 +964,16 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
};
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
+ if ($scope.logStartIndex != null && resp['start'] != $scope.logStartIndex) {
+ $scope.polling = false;
+ return;
+ }
+
processLogs(resp['logs'], resp['start']);
$scope.logStartIndex = resp['total'];
$scope.polling = false;
+ }, function() {
+ $scope.polling = false;
});
});
};
@@ -948,7 +1006,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
if ($location.search().current) {
$scope.setCurrentBuild($location.search().current, false);
} else if ($scope.builds.length > 0) {
- $scope.setCurrentBuild($scope.builds[0].id, true);
+ $scope.setCurrentBuild($scope.builds[$scope.builds.length - 1].id, true);
}
});
};
@@ -1510,12 +1568,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
'initialize': false
};
- $('#couldnotbuildModal').on('hidden.bs.modal', function() {
- $scope.$apply(function() {
- $location.path('/repository/' + $scope.created.namespace + '/' + $scope.created.name);
- });
- });
-
// Watch the namespace on the repo. If it changes, we update the plan and the public/private
// accordingly.
$scope.isUserNamespace = true;
@@ -1562,15 +1614,36 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
$scope.repo.namespace = namespace;
};
+ $scope.handleBuildStarted = function() {
+ var repo = $scope.repo;
+ $location.path('/repository/' + repo.namespace + '/' + repo.name);
+ };
+
+ $scope.handleBuildFailed = function(message) {
+ var repo = $scope.repo;
+
+ bootbox.dialog({
+ "message": message,
+ "title": "Could not start Dockerfile build",
+ "buttons": {
+ "close": {
+ "label": "Close",
+ "className": "btn-primary",
+ "callback": function() {
+ $scope.$apply(function() {
+ $location.path('/repository/' + repo.namespace + '/' + repo.name);
+ });
+ }
+ }
+ }
+ });
+
+ return true;
+ };
+
$scope.createNewRepo = function() {
$('#repoName').popover('hide');
- var uploader = $('#file-drop')[0];
- if ($scope.repo.initialize && uploader.files.length < 1) {
- $('#missingfileModal').modal();
- return;
- }
-
$scope.creating = true;
var repo = $scope.repo;
var data = {
@@ -1586,7 +1659,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
// Repository created. Start the upload process if applicable.
if ($scope.repo.initialize) {
- startFileUpload(created);
+ $scope.createdForBuild = created;
return;
}
@@ -1615,74 +1688,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
};
-
- var startBuild = function(repo, fileId) {
- $scope.building = true;
-
- var data = {
- 'file_id': fileId
- };
-
- var params = {
- 'repository': repo.namespace + '/' + repo.name
- };
-
- ApiService.requestRepoBuild(data, params).then(function(resp) {
- $location.path('/repository/' + params.repository);
- }, function() {
- $('#couldnotbuildModal').modal();
- });
- };
-
- var conductUpload = function(repo, file, url, fileId, mimeType) {
- var request = new XMLHttpRequest();
- request.open('PUT', url, true);
- request.setRequestHeader('Content-Type', mimeType);
- request.onprogress = function(e) {
- $scope.$apply(function() {
- var percentLoaded;
- if (e.lengthComputable) {
- $scope.upload_progress = (e.loaded / e.total) * 100;
- }
- });
- };
- request.onerror = function() {
- $scope.$apply(function() {
- $('#couldnotbuildModal').modal();
- });
- };
- request.onreadystatechange = function() {
- var state = request.readyState;
- if (state == 4) {
- $scope.$apply(function() {
- $scope.uploading = false;
- startBuild(repo, fileId);
- });
- return;
- }
- };
- request.send(file);
- };
-
- var startFileUpload = function(repo) {
- $scope.uploading = true;
- $scope.uploading_progress = 0;
-
- var uploader = $('#file-drop')[0];
- var file = uploader.files[0];
- $scope.upload_file = file.name;
-
- var mimeType = file.type || 'application/octet-stream';
- var data = {
- 'mimeType': mimeType
- };
-
- var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
- conductUpload(repo, file, resp.url, resp.file_id, mimeType);
- }, function() {
- $('#couldnotbuildModal').modal();
- });
- };
var subscribedToPlan = function(sub) {
$scope.planChanging = false;
diff --git a/static/lib/ansi2html.js b/static/lib/ansi2html.js
new file mode 100644
index 000000000..bd0c3b709
--- /dev/null
+++ b/static/lib/ansi2html.js
@@ -0,0 +1,335 @@
+/**
+ * Originally from: https://github.com/jorgeecardona/ansi-to-html
+ * Modified by jschorr: Add ability to repeat existing styles and not close them out automatically.
+ */
+angular.module('ansiToHtml', []).value('ansi2html', (function() {
+ // Define the styles supported from ANSI.
+ var STYLES = {
+ 'ef0': 'color:#000',
+ 'ef1': 'color:#A00',
+ 'ef2': 'color:#0A0',
+ 'ef3': 'color:#A50',
+ 'ef4': 'color:#00A',
+ 'ef5': 'color:#A0A',
+ 'ef6': 'color:#0AA',
+ 'ef7': 'color:#AAA',
+ 'ef8': 'color:#555',
+ 'ef9': 'color:#F55',
+ 'ef10': 'color:#5F5',
+ 'ef11': 'color:#FF5',
+ 'ef12': 'color:#55F',
+ 'ef13': 'color:#F5F',
+ 'ef14': 'color:#5FF',
+ 'ef15': 'color:#FFF',
+ 'eb0': 'background-color:#000',
+ 'eb1': 'background-color:#A00',
+ 'eb2': 'background-color:#0A0',
+ 'eb3': 'background-color:#A50',
+ 'eb4': 'background-color:#00A',
+ 'eb5': 'background-color:#A0A',
+ 'eb6': 'background-color:#0AA',
+ 'eb7': 'background-color:#AAA',
+ 'eb8': 'background-color:#555',
+ 'eb9': 'background-color:#F55',
+ 'eb10': 'background-color:#5F5',
+ 'eb11': 'background-color:#FF5',
+ 'eb12': 'background-color:#55F',
+ 'eb13': 'background-color:#F5F',
+ 'eb14': 'background-color:#5FF',
+ 'eb15': 'background-color:#FFF'
+ };
+
+ // Define the default styles.
+ var DEFAULTS = {
+ fg: '#FFF',
+ bg: '#000'
+ };
+
+ var __slice = [].slice;
+
+ var toHexString = function(num) {
+ num = num.toString(16);
+ while (num.length < 2) {
+ num = "0" + num;
+ }
+ return num;
+ };
+
+ var escapeHtml = function (unsafe) {
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ };
+
+ // Define the derived styles.
+ [0, 1, 2, 3, 4, 5].forEach(
+ function(red) {
+ return [0, 1, 2, 3, 4, 5].forEach(
+ function(green) {
+ return [0, 1, 2, 3, 4, 5].forEach(
+ function(blue) {
+ var b, c, g, n, r, rgb;
+ c = 16 + (red * 36) + (green * 6) + blue;
+ r = red > 0 ? red * 40 + 55 : 0;
+ g = green > 0 ? green * 40 + 55 : 0;
+ b = blue > 0 ? blue * 40 + 55 : 0;
+ rgb = ((function() {
+ var _i, _len, _ref, _results;
+ _ref = [r, g, b];
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ n = _ref[_i];
+ _results.push(toHexString(n));
+ }
+ return _results;
+ })()).join('');
+ STYLES["ef" + c] = "color:#" + rgb;
+ return STYLES["eb" + c] = "background-color:#" + rgb;
+ });
+ });
+ });
+
+ (function() {
+ _results = [];
+ for (_i = 0; _i <= 23; _i++){ _results.push(_i); }
+ return _results;
+ }).apply(this).forEach(
+ function(gray) {
+ var c, l;
+ c = gray + 232;
+ l = toHexString(gray * 10 + 8);
+ STYLES["ef" + c] = "color:#" + l + l + l;
+ return STYLES["eb" + c] = "background-color:#" + l + l + l;
+ });
+
+ // Define the filter class which will track all the ANSI style state.
+ function Filter(options) {
+ this.opts = $.extend(true, DEFAULTS, options || {});
+ this.input = [];
+ this.stack = [];
+ }
+
+ Filter.create = function() {
+ return new Filter();
+ };
+
+ Filter.prototype.toHtml = function(input) {
+ this.resetStyles();
+ var html = this.addInputToStream(input) + this.getTeardownHtml();
+ this.resetStyles();
+ return html;
+ };
+
+ Filter.prototype.addInputToStream = function(input) {
+ var buf = [];
+
+ this.input = typeof input === 'string' ? [input] : input;
+ this.forEach(function(chunk) {
+ return buf.push(chunk);
+ });
+
+ return buf.join('');
+ };
+
+ Filter.prototype.getSetupHtml = function() {
+ return this.stack.map(function(data) {
+ return data['html'];
+ }).join('');
+ };
+
+ Filter.prototype.getTeardownHtml = function() {
+ var stackCopy = this.stack.slice();
+ return stackCopy.reverse().map(function(data) {
+ return "" + data['kind'] + ">";
+ }).join('');
+ };
+
+ Filter.prototype.forEach = function(callback) {
+ var that = this;
+ var buf = '';
+ var handleDisplay = function(code) {
+ code = parseInt(code, 10);
+ if (code === -1) {
+ callback(' ');
+ }
+ if (code === 0) {
+ callback(that.getTeardownHtml());
+ that.resetStyles();
+ }
+ if (code === 1) {
+ callback(that.pushTag('b'));
+ }
+ if (code === 2) {
+
+ }
+ if ((2 < code && code < 5)) {
+ callback(that.pushTag('u'));
+ }
+ if ((4 < code && code < 7)) {
+ callback(that.pushTag('blink'));
+ }
+ if (code === 7) {
+
+ }
+ if (code === 8) {
+ callback(that.pushStyle('display:none'));
+ }
+ if (code === 9) {
+ callback(that.pushTag('strike'));
+ }
+ if (code === 24) {
+ callback(that.closeTag('u'));
+ }
+ if ((29 < code && code < 38)) {
+ callback(that.pushStyle("ef" + (code - 30)));
+ }
+ if (code === 39) {
+ callback(that.pushStyle("color:" + that.opts.fg));
+ }
+ if ((39 < code && code < 48)) {
+ callback(that.pushStyle("eb" + (code - 40)));
+ }
+ if (code === 49) {
+ callback(that.pushStyle("background-color:" + that.opts.bg));
+ }
+ if ((89 < code && code < 98)) {
+ callback(that.pushStyle("ef" + (8 + (code - 90))));
+ }
+ if ((99 < code && code < 108)) {
+ return callback(that.pushStyle("eb" + (8 + (code - 100))));
+ }
+ };
+
+ this.input.forEach(function(chunk) {
+ buf += chunk;
+ return that.tokenize(buf, function(tok, data) {
+ switch (tok) {
+ case 'text':
+ return callback(escapeHtml(data));
+ case 'display':
+ return handleDisplay(data);
+ case 'xterm256':
+ return callback(that.pushStyle("ef" + data));
+ }
+ });
+ });
+ };
+
+ Filter.prototype.pushTag = function(tag, style) {
+ if (style == null) {
+ style = '';
+ }
+ if (style.length && style.indexOf(':') === -1) {
+ style = STYLES[style];
+ }
+ var html = ["<" + tag, (style ? " style=\"" + style + "\"" : void 0), ">"].join('');
+ this.stack.push({'html': html, 'kind': tag});
+ return html;
+ };
+
+ Filter.prototype.pushStyle = function(style) {
+ return this.pushTag("span", style);
+ };
+
+ Filter.prototype.closeTag = function(style) {
+ var last;
+ if (this.stack.slice(-1)[0]['kind'] === style) {
+ last = this.stack.pop();
+ }
+ if (last != null) {
+ return "" + style + ">";
+ }
+ };
+
+ Filter.prototype.resetStyles = function() {
+ this.stack = [];
+ };
+
+ Filter.prototype.tokenize = function(text, callback) {
+ var ansiHandler, ansiMatch, ansiMess, handler, i, length, newline, process, realText, remove, removeXterm256, tokens, _j, _len, _results1,
+ _this = this;
+ ansiMatch = false;
+ ansiHandler = 3;
+ remove = function(m) {
+ return '';
+ };
+ removeXterm256 = function(m, g1) {
+ callback('xterm256', g1);
+ return '';
+ };
+ newline = function(m) {
+ if (_this.opts.newline) {
+ callback('display', -1);
+ } else {
+ callback('text', m);
+ }
+ return '';
+ };
+ ansiMess = function(m, g1) {
+ var code, _j, _len;
+ ansiMatch = true;
+ if (g1.trim().length === 0) {
+ g1 = '0';
+ }
+ g1 = g1.trimRight(';').split(';');
+ for (_j = 0, _len = g1.length; _j < _len; _j++) {
+ code = g1[_j];
+ callback('display', code);
+ }
+ return '';
+ };
+ realText = function(m) {
+ callback('text', m);
+ return '';
+ };
+ tokens = [
+ {
+ pattern: /^\x08+/,
+ sub: remove
+ }, {
+ pattern: /^\x1b\[38;5;(\d+)m/,
+ sub: removeXterm256
+ }, {
+ pattern: /^\n+/,
+ sub: newline
+ }, {
+ pattern: /^\x1b\[((?:\d{1,3};?)+|)m/,
+ sub: ansiMess
+ }, {
+ pattern: /^\x1b\[?[\d;]{0,3}/,
+ sub: remove
+ }, {
+ pattern: /^([^\x1b\x08\n]+)/,
+ sub: realText
+ }
+ ];
+ process = function(handler, i) {
+ var matches;
+ if (i > ansiHandler && ansiMatch) {
+ return;
+ } else {
+ ansiMatch = false;
+ }
+ matches = text.match(handler.pattern);
+ text = text.replace(handler.pattern, handler.sub);
+ };
+ _results1 = [];
+ while ((length = text.length) > 0) {
+ for (i = _j = 0, _len = tokens.length; _j < _len; i = ++_j) {
+ handler = tokens[i];
+ process(handler, i);
+ }
+ if (text.length === length) {
+ break;
+ } else {
+ _results1.push(void 0);
+ }
+ }
+ return _results1;
+ };
+
+ return Filter;
+})());
\ No newline at end of file
diff --git a/static/partials/new-repo.html b/static/partials/new-repo.html
index 30d686525..7eb73d86b 100644
--- a/static/partials/new-repo.html
+++ b/static/partials/new-repo.html
@@ -12,16 +12,7 @@
-
-
Uploading file {{ upload_file }}
-
-
-
-
-
+
-
- Upload a Dockerfile or a zip file containing a Dockerfile in the root directory
-
-
-
+
@@ -123,7 +112,7 @@
+ ng-disabled="uploading || building || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || (repo.initialize && !hasDockerfile)">
Create Repository
@@ -157,26 +146,6 @@
-
-
-
-
-
-
-
- A file is required in order to initialize a repository.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- The repository could not be initialized with the selected Dockerfile. Please try again later.
-
-
-
-
-
-
-
diff --git a/static/partials/repo-build.html b/static/partials/repo-build.html
index 8d08b21b8..fb3db86d7 100644
--- a/static/partials/repo-build.html
+++ b/static/partials/repo-build.html
@@ -9,6 +9,13 @@
+
+
+
+
+ New Dockerfile Build
+
+
-
+
+
+
+ Run Build Again
+
{{ build.id }}
+
+
+
+
+
+
+
+
+
+
+ Are you sure you want to run this Dockerfile build again? The results will be immediately pushed to the repository.
+
+
+
+
+
+
diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html
index 9fdb632b7..49fd55b46 100644
--- a/static/partials/view-repo.html
+++ b/static/partials/view-repo.html
@@ -5,51 +5,65 @@
-
+
-
-
-
-
+
+
+
diff --git a/templates/base.html b/templates/base.html
index b47a2d8ae..151b6035c 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -52,6 +52,7 @@
+
diff --git a/test/testlogs.py b/test/testlogs.py
index 76a68b1bf..b30cee6ed 100644
--- a/test/testlogs.py
+++ b/test/testlogs.py
@@ -129,7 +129,12 @@ class TestBuildLogs(BuildLogs):
@staticmethod
def _generate_logs(count):
- return [(1, {'message': get_sentence()}, None) for _ in range(count)]
+ others = []
+ if random.randint(0, 10) <= 8:
+ count = count - 2
+ others = [(1, {'message': '\x1b[91m' + get_sentence()}, None), (1, {'message': '\x1b[0m'}, None)]
+
+ return others + [(1, {'message': get_sentence()}, None) for _ in range(count)]
@staticmethod
def _compute_total_completion(statuses, total_images):