diff --git a/assets/scripts/app/controllers/build.coffee b/assets/scripts/app/controllers/build.coffee index 289672e1..751565f1 100644 --- a/assets/scripts/app/controllers/build.coffee +++ b/assets/scripts/app/controllers/build.coffee @@ -24,5 +24,5 @@ Travis.BuildController = Ember.Controller.extend ).property('commit.committerEmail') hasLoaded: (-> - @set('controllers.log.job', @get('build.firstJob')) if @get('build.isLoaded') && !@get('build.isMatrix') - ).observes('build.id', 'loading') + @set('controllers.log.job', @get('build.firstJob')) if @get('build.firstJob') && !@get('build.isMatrix') + ).observes('build.id', 'build.firstJob') diff --git a/assets/scripts/app/models/build.coffee b/assets/scripts/app/models/build.coffee index 47f87d30..4e802557 100644 --- a/assets/scripts/app/models/build.coffee +++ b/assets/scripts/app/models/build.coffee @@ -31,7 +31,7 @@ require 'travis/model' ).property('jobs.length') firstJob: (-> - @get('jobs.firstObject') + @get('jobs').objectAt(0) ).property('jobs.length') isFinished: (-> diff --git a/assets/scripts/app/models/job.coffee b/assets/scripts/app/models/job.coffee index 7ccb72b9..374619b2 100644 --- a/assets/scripts/app/models/job.coffee +++ b/assets/scripts/app/models/job.coffee @@ -26,6 +26,7 @@ require 'travis/model' _config: DS.attr('object') log: ( -> + @set('isLogAccessed', true) Travis.Log.create(job: this) ).property() @@ -46,7 +47,9 @@ require 'travis/model' ).property('state') clearLog: -> - @get('log').clear() + # This is needed if we don't want to fetch log just to clear it + if @get('isLogAccessed') + @get('log').clear() sponsor: (-> worker = @get('log.workerName') diff --git a/assets/scripts/app/views/log.coffee b/assets/scripts/app/views/log.coffee index 8efeb9b4..a96bc012 100644 --- a/assets/scripts/app/views/log.coffee +++ b/assets/scripts/app/views/log.coffee @@ -1,7 +1,159 @@ require 'log' +require 'travis/ordered_log' Log.DEBUG = true +Travis.UnorderedLogEngineMixin = Ember.Mixin.create + setupEngine: -> + console.log 'log view: create engine' if Log.DEBUG + @limit = new Log.Limit + @scroll = new Log.Scroll + @engine = Log.create(listeners: [new Log.FragmentRenderer, new Log.Folds, @scroll]) + @observeParts() + @numberLineOnHover() + + destroyEngine: -> + parts = @get('log.parts') + parts.removeArrayObserver(@, didChange: 'partsDidChange', willChange: 'noop') + + observeParts: -> + parts = @get('log.parts') + parts.addArrayObserver(@, didChange: 'partsDidChange', willChange: 'noop') + parts = parts.slice(0) + @partsDidChange(parts, 0, null, parts.length) + + partsDidChange: (parts, start, _, added) -> + console.log 'log view: parts did change' if Log.DEBUG + for part, i in parts.slice(start, start + added) + # console.log "limit in log view: #{@get('limited')}" + break if @get('limited') + @engine.set(part.number, part.content) + @propertyDidChange('limited') + + lineNumberDidChange: (-> + @scroll.set(number) if !@get('isDestroyed') && number = @get('controller.lineNumber') + ).observes('controller.lineNumber') + +Travis.OrderedLogEngineMixin = Ember.Mixin.create + setupEngine: -> + @set('logManager', Travis.OrderedLog.create(target: this)) + + @get('logManager').append @get('log.parts').map( (part) -> Ember.get(part, 'content') ) + + @get('log.parts').addArrayObserver this, + didChange: 'partsDidChange' + willChange: 'partsWillChange' + + destroyEngine: (view) -> + @get('logManager').destroy() + @get('log.parts').removeArrayObserver this, + didChange: 'partsDidChange' + willChange: 'noop' + + partsDidChange: (parts, index, removedCount, addedCount) -> + addedParts = parts.slice(index, index + addedCount).map( (part) -> Ember.get(part, 'content') ) + @get('logManager').append addedParts + + lineNumberDidChange: (-> + if number = @get('controller.lineNumber') + @tryScrollingToHashLineNumber(number) + ).observes('controller.lineNumber') + + scrollTo: (id) -> + # and this is even more weird, when changing hash in URL in firefox + # to other value, for example #L10, it actually scrolls just #main + # element... this is probably some CSS issue, I don't have time to + # investigate at the moment + # TODO: fix this + $('#main').scrollTop 0 + + # weird, html works in chrome, body in firefox + $('html,body').scrollTop $(id).offset().top + + @set 'controller.lineNumber', null + + tryScrollingToHashLineNumber: (number) -> + id = "#L#{number}" + checker = => + return if @get('isDestroyed') + + if $(id).length + @scrollTo(id) + else + setTimeout checker, 100 + + checker() + + appendLog: (payloads) -> + url = @get('logUrl') + + leftOut = [] + cut = false + fragment = document.createDocumentFragment() + + # TODO: refactor this loop, it's getting messy + for payload in payloads + line = payload.content + number = payload.number + + if payload.logWasCut + cut = true + else + unless payload.append + pathWithNumber = "#{url}#L#{number}" + p = document.createElement('p') + p.innerHTML = "#{line}" + line = p + + if payload.fold && !payload.foldContinuation + div = document.createElement('div') + div.appendChild line + div.className = "fold #{payload.fold} show-first-line" + line = div + + if payload.replace + if link = fragment.querySelector("#L#{number}") + link.parentElement.innerHTML = line.innerHTML + else + this.$("#L#{number}").parent().replaceWith line + else if payload.append + if link = fragment.querySelector("#L#{number}") + link.parentElement.innerHTML += line + else + this.$("#L#{number}").parent().append line + else if payload.foldContinuation + folds = fragment.querySelectorAll(".fold.#{payload.fold}") + if fold = folds[folds.length - 1] + fold.appendChild line + else + this.$("#log .fold.#{payload.fold}:last").append line + else + fragment.appendChild(line) + + if payload.openFold + folds = fragment.querySelectorAll(".fold.#{payload.openFold}") + if fold = folds[folds.length - 1] + fold = $(fold) + else + fold = this.$(".fold.#{payload.openFold}:last") + + fold.removeClass('show-first-line').addClass('open') + + if payload.foldEnd + folds = fragment.querySelectorAll(".fold.#{payload.fold}") + if fold = folds[folds.length - 1] + fold = $(fold) + else + fold = this.$(".fold.#{payload.fold}:last") + + fold.removeClass('show-first-line') + + this.$('#log')[0].appendChild fragment + if cut + url = Travis.Urls.plainTextLog(@get('log.id')) + this.$("#log").append $("

Log was too long to display. Download the the raw version to get the full log.

") + + Travis.reopen LogView: Travis.View.extend templateName: 'jobs/log' @@ -15,19 +167,18 @@ Travis.reopen toTop: () -> $(window).scrollTop(0) - PreView: Em.View.extend + PreView: Em.View.extend(Travis.OrderedLogEngineMixin, { templateName: 'jobs/pre' didInsertElement: -> console.log 'log view: did insert' if Log.DEBUG @_super.apply this, arguments - @createEngine() + @setupEngine() @lineNumberDidChange() willDestroyElement: -> console.log 'log view: will destroy' if Log.DEBUG - parts = @get('log.parts') - parts.removeArrayObserver(@, didChange: 'partsDidChange', willChange: 'noop') + @destroyEngine() versionDidChange: (-> @rerender() if @get('inDOM') @@ -38,35 +189,9 @@ Travis.reopen @rerender() if @get('inDOM') ).observes('log') - createEngine: -> - console.log 'log view: create engine' if Log.DEBUG - @limit = new Log.Limit - @scroll = new Log.Scroll - @engine = Log.create(listeners: [new Log.FragmentRenderer, new Log.Folds, @scroll]) - @observeParts() - @numberLineOnHover() - - observeParts: -> - parts = @get('log.parts') - parts.addArrayObserver(@, didChange: 'partsDidChange', willChange: 'noop') - parts = parts.slice(0) - @partsDidChange(parts, 0, null, parts.length) - - partsDidChange: (parts, start, _, added) -> - console.log 'log view: parts did change' if Log.DEBUG - for part, i in parts.slice(start, start + added) - # console.log "limit in log view: #{@get('limited')}" - break if @get('limited') - @engine.set(part.number, part.content) - @propertyDidChange('limited') - - lineNumberDidChange: (-> - @scroll.set(number) if !@get('isDestroyed') && number = @get('controller.lineNumber') - ).observes('controller.lineNumber') - - limited: (-> - @limit && @limit.limited - ).property() + #limited: (-> + # @limit && @limit.limited + #).property() plainTextLogUrl: (-> Travis.Urls.plainTextLog(id) if id = @get('log.job.id') @@ -89,11 +214,26 @@ Travis.reopen target = $(event.target) target.closest('.fold').toggleClass('open') + logUrl: (-> + repo = @get('log.job.repo') + item = @get('controller.currentItem') + + if repo && item + name = if item.constructor == Travis.Build + 'build' + else + 'job' + + Travis.__container__.lookup('router:main').generate(name, repo, item) + ).property('job.repo', 'parentView.currentItem') + lineNumberClicked: (number) -> - window.history.pushState(null, null, "#{window.location.pathname}#L#{number}"); + path = @get('logUrl') + "#L#{number}" + window.history.replaceState({ path: path }, null, path); @set('controller.lineNumber', number) noop: -> # TODO required? + }) Log.Scroll = -> Log.Scroll.prototype = $.extend new Log.Listener, diff --git a/assets/scripts/lib/travis/ordered_log.coffee b/assets/scripts/lib/travis/ordered_log.coffee new file mode 100644 index 00000000..65c33fcd --- /dev/null +++ b/assets/scripts/lib/travis/ordered_log.coffee @@ -0,0 +1,152 @@ +# TODO: revisit those patterns +FOLDS = [ + Em.Object.create(name: 'schema', startPattern: /^\$ (?:bundle exec )?rake( db:create)? db:schema:load/, endPattern: /^(<\/span>)?\$/) + Em.Object.create(name: 'migrate', startPattern: /^\$ (?:bundle exec )?rake( db:create)? db:migrate/, endPattern: /^(<\/span>)?\$/) + Em.Object.create(name: 'bundle', startPattern: /^\$ bundle install/, endPattern: /^(<\/span>)?\$/) +] + +@Travis.OrderedLog = Em.Object.extend + init: -> + @set 'folds', [] + @set 'line', 1 + @set 'lineNumber', 1 + @initial = true + + for fold in FOLDS + @addFold fold + + append: (lines) -> + return unless lines + return if @get('lineNumber') > 5000 + + log = @join lines + log = @escape log + log = @deansi log + lines = @split log + + target = @get 'target' + index = 0 + currentFold = @currentFold + + result = [] + + for line in lines + if line == '\r' + @set 'replace', true + else if line == '\n' + @set 'newline', true + index += 1 + else + if currentFold && ( @isFoldEnding(currentFold, line) ) + # end of the fold, send fold to target + if result.length > 0 + result.slice(-1)[0].foldEnd = true + target.appendLog result + + @currentFold = currentFold = null + @set 'foldContinuation', false + result = [] + + if !currentFold && ( currentFold = @foldByStart(line) ) + # beginning new fold, send current lines to target + if result.length > 0 + target.appendLog result + + result = [] + start = index + + payload = { content: line } + + if currentFold + payload.fold = currentFold.get('name') + + if @get 'foldContinuation' + payload.foldContinuation = true + + payload.number = @get('lineNumber') + index + + if @get 'replace' + @set 'replace', false + payload.replace = true + else if @get 'newline' + @set 'newline', false + else if !@initial + payload.append = true + + @initial = false + + if payload.foldContinuation && payload.content.match(/Done. Build script exited|Your build has been stopped/) + # script ended, but fold is still closed, which most probably means + # error, end the fold and open it. + # TODO: we need log marks to make it easier + payload.foldContinuation = null + payload.openFold = payload.fold + payload.fold = null + + result.pushObject payload + + if currentFold + @set 'foldContinuation', true + + if @get('lineNumber') + index >= 5000 + result.pushObject logWasCut: true + break + + if result.length > 0 + if currentFold + @currentFold = currentFold + + target.appendLog result + + nextLineNumber = @get('lineNumber') + index + @set 'lineNumber', nextLineNumber + + join: (lines) -> + if typeof lines == 'string' + lines + else + lines.toArray().join '' + + split: (log) -> + log = log.replace /\r\n/g, '\n' + lines = log.split(/(\n)/) + + if lines.slice(-1)[0] == '' + lines.popObject() + + result = [] + for line in lines + result.pushObjects line.split(/(\r)/) + + result + + escape: (log) -> + Handlebars.Utils.escapeExpression log + + deansi: (log) -> + log = log.replace(/\r\r/g, '\r') + .replace(/\033\[K\r/g, '\r') + .replace(/\[2K/g, '') + .replace(/\033\(B/g, '') + .replace(/\033\[\d+G/g, '') + + ansi = ansiparse(log) + + text = '' + ansi.forEach (part) -> + classes = [] + part.foreground and classes.push(part.foreground) + part.background and classes.push('bg-' + part.background) + part.bold and classes.push('bold') + part.italic and classes.push('italic') + text += (if classes.length then ('' + part.text + '') else part.text) + text.replace /\033/g, '' + + addFold: (fold) -> + @get('folds').pushObject fold + + foldByStart: (line) -> + @get('folds').find (fold) -> line.match(fold.get('startPattern')) + + isFoldEnding: (fold, line) -> + line.match(fold.get('endPattern'))