activity: reformat `CommentListViewController` class

--- client/Social/Activity/views/comments/commentlistviewcontroller.coffee
+++ client/Social/Activity/views/comments/commentlistviewcontroller.coffee
@@ -1,8 +1,12 @@
 class CommentListViewController extends KDListViewController
-  constructor:->
-    super
+
+  constructor: (options = {}, data) ->
+
+    super options, data
+
     @_hasBackgrounActivity = no
     @startListeners()
+

   loadView: (mainView) ->

@@ -26,10 +30,12 @@

     windowController.on "ReceivedMouseUpElsewhere", @bound 'mouseUpHappened'

-  instantiateListItems:(items, keepDeletedComments = no)->
+
+  instantiateListItems: (items, keepDeletedComments = no) ->
+
     newItems = []

-    items.sort (a,b) =>
+    items.sort (a, b) ->
       a = a.meta.createdAt
       b = b.meta.createdAt
       if a<b then -1 else if a>b then 1 else 0
@@ -54,14 +60,17 @@

     return newItems

-  startListeners:->
+
+  startListeners: ->
+
     listView = @getListView()

-    listView.on 'ItemWasAdded', (view, index)=>
+    listView.on 'ItemWasAdded', (view, index) ->
+
       view.on 'CommentIsDeleted', ->
         listView.emit "CommentIsDeleted"

-    listView.on "AllCommentsLinkWasClicked", (commentHeader)=>
+    listView.on "AllCommentsLinkWasClicked", (commentHeader) =>

       return if @_hasBackgrounActivity

@@ -75,46 +84,56 @@
       @_removedBefore = no
       @fetchRelativeComments 10, meta.createdAt

-  fetchCommentsByRange:(from,to,callback)->
-    [to,callback] = [callback,to] unless callback
-    query = {from,to}
+
+  fetchCommentsByRange: (from, to, callback) ->
+
+    [to, callback] = [callback, to] unless callback
+
+    query   = {from, to}
     message = @getListView().getData()

-    message.commentsByRange query,(err,comments)=>
+    message.commentsByRange query, (err, comments) =>
+
       @getListView().emit "BackgroundActivityFinished"
-      callback err,comments
+      callback err, comments

-  fetchAllComments:(skipCount=3, callback = noop)->
+
+  fetchAllComments: (skipCount = 3, callback = noop) ->

     listView = @getListView()
     listView.emit "BackgroundActivityStarted"
+
     message = @getListView().getData()
-    message.restComments skipCount, (err, comments)=>
+    message.restComments skipCount, (err, comments) ->
       listView.emit "BackgroundActivityFinished"
       listView.emit "AllCommentsWereAdded"
       callback err, comments

-  fetchRelativeComments:(_limit = 10, _after, continuous = yes, _sort = 1)->
+
+  fetchRelativeComments: (limit = 10, after, continuous = yes, sort = 1) ->
+
     listView = @getListView()
-    message = @getListView().getData()
-    message.fetchRelativeComments limit:_limit, after:_after, sort:_sort, (err, comments)=>
+    message  = listView.getData()
+    message.fetchRelativeComments {limit, after, sort}, (err, comments) =>

       if not @_removedBefore
         @removeAllItems()
         @_removedBefore = yes

-      @instantiateListItems comments[_limit-10...], yes
+      @instantiateListItems comments[limit - 10...], yes

-      if comments.length is _limit
-        startTime = comments[comments.length-1].meta.createdAt
-        @fetchRelativeComments ++_limit, startTime, continuous, _sort  if continuous
+      if comments.length is limit
+        startTime = comments[comments.length - 1].createdAt
+        @fetchRelativeComments ++limit, startTime, continuous, sort  if continuous
       else
         listView = @getListView()
         listView.emit "BackgroundActivityFinished"
         listView.emit "AllCommentsWereAdded"
         @_hasBackgrounActivity = no

-  replaceAllComments:(comments)->
+
+  replaceAllComments: (comments) ->
+
     @removeAllItems()
     @instantiateListItems comments


Remove obsolete koding-broker-client installation step

Signed-off-by: Sonmez Kartal <d2d15c3d37396a5f2a09f8b42cf5f27d43a3fbde@koding.com>

--- config/generateRunFile.coffee
+++ config/generateRunFile.coffee
@@ -94,13 +94,6 @@
       mkdir $HOME/.kite &>/dev/null
       echo copying #{kiteKeyFile} to $HOME/.kite/kite.key
       cp -f #{kiteKeyFile} $HOME/.kite/kite.key
-
-      echo '#---> BUILDING BROKER-CLIENT @chris <---#'
-      echo "building koding-broker-client."
-      cd #{options.projectRoot}/node_modules_koding/koding-broker-client
-      cake build
-      cd #{options.projectRoot}
-

       echo
       echo


NewStatusUpdate: fix MR comments

NewStatusUpdate: fix MR comments

--- workers/social/lib/social/models/messages/newstatusupdate/index.coffee
+++ workers/social/lib/social/models/messages/newstatusupdate/index.coffee
@@ -225,13 +225,12 @@

       @getExemptUserIdsIfNeeded client, options, (err, ids)=>
         return callback err  if err
-
-        selector = 'meta.createdAt': "$lt" : to
+        selector = 'meta.createdAt': $lt: to
         # remove exempts from result set
         selector.originId = $nin : ids if ids.length > 0

         options.sort = 'meta.createdAt' : -1
-        options.limit or= 20
+        options.limit ?= 20

         @some selector, options, (err, data)=>
           return callback err if err
@@ -277,7 +276,7 @@

         query =
           targetName: 'JNewStatusUpdate'
-          "timestamp": "$lt": to
+          "timestamp": $lt: to

         showExempt = @checkForTrollMode delegate, options
         # add isExempt query if necessary


rename method

rename method

--- coffee/controllers/issueCtrl.coffee
+++ coffee/controllers/issueCtrl.coffee
@@ -34,7 +34,7 @@

     # on change selected Project, load issues and queries.
     DataAdapter.addEventListener DataAdapter.SELECTED_PROJECT_CHANGED, () ->
-      loadIssuesFirstPage()
+      loadIssues()

    # on change selected Query, set query to project, and load issues.
     DataAdapter.addEventListener DataAdapter.SELECTED_QUERY_CHANGED, () ->
@@ -65,12 +65,11 @@
     if queryId is QUERY_ALL_ID then queryId = undefined
     DataAdapter.selectedProject.queryId = queryId
     Project.setParam(targetUrl, targetId, { 'queryId': queryId })
-    loadIssuesFirstPage()
+    loadIssues()

   # load issues on P.1
-  loadIssuesFirstPage = () ->
-    # $scope.editState.currentPage = 1
+  loadIssues = () ->
     $scope.editState.loadAllTicketOnProject()


allow flush to succeed when doc is already deleted

--- services/document-updater/app/coffee/ProjectManager.coffee
+++ services/document-updater/app/coffee/ProjectManager.coffee
@@ -20,10 +20,15 @@
 				do (doc_id) ->
 					jobs.push (callback) ->
 						DocumentManager.flushDocIfLoadedWithLock project_id, doc_id, (error) ->
-							if error?
+							if error? and error instanceof Errors.NotFoundError
+								logger.warn err: error, project_id: project_id, doc_id: doc_id, "found deleted doc when flushing, removing from redis"
+								callback()
+							else if error?
 								logger.error err: error, project_id: project_id, doc_id: doc_id, "error flushing doc"
 								errors.push(error)
-							callback()
+								callback()
+							else
+								callback()

 			logger.log project_id: project_id, doc_ids: doc_ids, "flushing docs"
 			async.series jobs, () ->


Add a 'testEmail' email type

Add a 'testEmail' email type

--- services/web/app/coffee/Features/Email/EmailBuilder.coffee
+++ services/web/app/coffee/Features/Email/EmailBuilder.coffee
@@ -123,8 +123,6 @@
 				description: "Join #{ opts.project.name } at ShareLaTeX"
 		})

-
-
 templates.completeJoinGroupAccount =
 	subject: _.template "Verify Email to join <%= group_name %> group"
 	layout: BaseWithHeaderEmailLayout
@@ -149,6 +147,21 @@
 			gmailGoToAction: null
 		})

+templates.testEmail =
+	subject: _.template "Test Email from ShareLaTeX"
+	layout: PersonalEmailLayout
+	type: 'notification'
+	plainTextTemplate: _.template """
+Hi,
+
+This is a test email, from ShareLaTeX.
+
+#{settings.appName} - <%= siteUrl %>
+"""
+	compiledTemplate: _.template """
+
This is a test email, from ShareLaTeX.

+	"""
+
 module.exports =
 	templates: templates

@@ -163,4 +176,4 @@
 			html: template.layout(opts)
 			text: template?.plainTextTemplate?(opts)
 			type:template.type
-		}+		}


Support core:move-up/down in ScrollView

Support core:move-up/down in ScrollView

Closes #1828

--- src/scroll-view.coffee
+++ src/scroll-view.coffee
@@ -6,6 +6,8 @@
 # the following events won't be handled by the ScrollView.
 #
 # ## Events
+#   * `core:move-up`
+#   * `core:move-down`
 #   * `core:page-up`
 #   * `core:page-down`
 #   * `core:move-to-top`
@@ -19,6 +21,8 @@
 module.exports =
 class ScrollView extends View
   initialize: ->
+    @on 'core:move-up', => @scrollUp()
+    @on 'core:move-down', => @scrollDown()
     @on 'core:page-up', => @pageUp()
     @on 'core:page-down', => @pageDown()
     @on 'core:move-to-top', => @scrollToTop()


feat(tabbar): now based on bp-sref

feat(tabbar): now based on bp-sref

--- modules/scripts/directives/tabbar.coffee
+++ modules/scripts/directives/tabbar.coffee
@@ -10,48 +10,54 @@

 angular.module('bp.directives').directive 'bpTab', deps [
   '$state'
+  '$compile'
   '$timeout'
   ], (
   $state
+  $compile
   $timeout
   )->
-  scope: true
   restrict: 'E'
-  transclude: true
-  template: '<bp-icon></bp-icon>'
-  compile: (elem, attrs, transcludeFn) ->
-    (scope, element, attrs) ->
-      element.attr
-        role: 'tab'
+  scope:
+    bpSref: '@'
+    bpTabIcon: '@'
+    bpTabTitle: '@'
+  link: (scope, element, attrs) ->
+    element.attr
+      role: 'tab'

-      scope.tabState = attrs.bpState or ''
+    state = $state.get scope.bpSref
+    unless attrs.bpTabTitle?
+      attrs.bpTabTitle =
+        state.data?.title or
+        state.name?.charAt(0).toUpperCase() + state.name?.slice(1)

-      # Extract Icon
-      $icon = element.find 'bp-icon'
-      angular.forEach element.attr('class').split(' '), (value) ->
-        if /^bp\-icon\-/.test value
-          $icon.addClass value
-          element.removeClass value
-      # Extract Label
-      transcludeFn scope, (clone) ->
-        element.append clone
+    $icon = $compile("
+      ")(scope)

-      scope.$on '$stateChangeSuccess', ->
-        if $state.includes scope.tabState
-          element
-            .addClass('bp-tab-active')
-            .attr
-              'aria-selected': 'true'
-        else
-          element
-            .removeClass('bp-tab-active')
-            .attr
-              'aria-selected': 'false'
+    $title = $compile("
+
+        {{bpTabTitle}}
+      ")(scope)

-      element.bind 'touchstart', ->
-        $timeout ->
-          element.trigger 'touchend'
-        , 500
+    element.append $icon, $title

-      scope.$on '$destroy', ->
-        element.unbind 'touchstart'
+    scope.$on '$stateChangeSuccess', ->
+      if $state.includes scope.bpSref
+        element
+          .addClass('bp-tab-active')
+          .attr
+            'aria-selected': 'true'
+      else
+        element
+          .removeClass('bp-tab-active')
+          .attr
+            'aria-selected': 'false'
+
+    element.bind 'touchstart', ->
+      $timeout ->
+        element.trigger 'touchend'
+      , 500
+
+    scope.$on '$destroy', ->
+      element.unbind 'touchstart'


* Added new artifact types.  Full list of those now supported:     * DE - Defect     * DS - Defect suite     * F - Feature     * I - Initiative     * T - Theme     * TA - Task     * TC - Test case     * US - user story * Refactored to be data driven

--- src/scripts/rally.coffee
+++ src/scripts/rally.coffee
@@ -1,5 +1,5 @@
 # Description:
-#   Rally information for bugs, stories, and users
+#   Rally information for artifacts
 #
 # Dependencies:
 #   None
@@ -9,13 +9,12 @@
 #   HUBOT_RALLY_PASSWORD
 #
 # Commands:
-#   hubot rally me <defect id | task id | story id> - Lookup a task, story or defect from Rally
+#   hubot rally me <formattedID> - Lookup a task, story, defect, etc. from Rally
 #
 # Notes:
 #   Since Rally supports rich text for description fields, it will come back as HTML
 #   to pretty print this we can run it through lynx. Make sure you have lynx installed
 #   and PATH accessible, otherwise we will degrade to just showing the html description.
-#   ENV Variables Required:
 #
 # Author:
 #   brianmichel
@@ -24,107 +23,156 @@

 user = process.env.HUBOT_RALLY_USERNAME
 pass = process.env.HUBOT_RALLY_PASSWORD
-api_version = '1.40'
+api_version = 'v2.0'
+
+logger = null
+
+typeInfoByPrefix =
+  DE:
+    name: 'defect'
+    extraOutputFields: [
+      'State'
+      'ScheduleState'
+      'Severity'
+    ]
+  DS:
+    name: 'defectsuite'
+    extraOutputFields: [
+      'ScheduleState'
+    ]
+  F:
+    name: 'feature'
+    queryName: 'portfolioitem/feature'
+    linkName: 'portfolioitem/feature'
+    extraOutputFields: [
+      'State._refObjectName'
+      'Parent._refObjectName'
+    ]
+  I:
+    name: 'initiative'
+    queryName: 'portfolioitem/initiative'
+    linkName: 'portfolioitem/initiative'
+    extraOutputFields: [
+      'State._refObjectName'
+      'Parent._refObjectName'
+    ]
+  T:
+    name: 'theme'
+    queryName: 'portfolioitem/theme'
+    linkName: 'portfolioitem/theme'
+    extraOutputFields: [
+      'State._refObjectName'
+      'Parent._refObjectName'
+    ]
+  TA:
+    name: 'task'
+    extraOutputFields: [
+      'State'
+      'WorkProduct._refObjectName'
+    ]
+  TC:
+    name: 'testcase'
+    extraOutputFields: [
+      'WorkProduct._refObjectName'
+      'Type'
+    ]
+  US:
+    name: 'story'
+    queryName: 'hierarchicalrequirement'
+    linkName: 'userstory'
+    extraOutputFields: [
+      'ScheduleState'
+      'Parent._refObjectName'
+      'Feature._refObjectName'
+    ]
+

 module.exports = (robot) ->
-	robot.respond /(rally)( me)? (.*)/i, (msg) ->
-		if user && pass
-			switch msg.match[3].toUpperCase().substring(0,1)
-				when "D" then bugRequest msg, msg.match[3], (string) ->
-					msg.send string
-				when "T" then taskRequest msg, msg.match[3], (string) ->
-					msg.send string
-				when "S" then storyRequest msg, msg.match[3], (string) ->
-					msg.send string
-				else msg.send "Uhh, that doesn't work"
-		else
-		  msg.send "You need to set HUBOT_RALLY_USERNAME & HUBOT_RALLY_PASSWORD before making requests!"
+  logger = robot.logger
+  robot.respond /(rally)( me)? ([a-z]+)(\d+)/i, (msg) ->
+    if user && pass
+      idPrefix = msg.match[3].toUpperCase()
+      idNumber = msg.match[4]
+      if typeInfoByPrefix.hasOwnProperty(idPrefix)
+        queryRequest msg, typeInfoByPrefix[idPrefix], idNumber, (string) ->
+          msg.send string
+      else
+        msg.send "Uhh, I don't know that formatted ID prefix"
+    else
+      msg.send 'You need to set HUBOT_RALLY_USERNAME & HUBOT_RALLY_PASSWORD before making requests!'

-bugRequest = (msg, defectId, cb) ->
-	query_string = "/defect.js?query=(FormattedID = #{defectId})&fetch=true"
-	rallyRequest msg, query_string, (json) ->
-		if json && json.QueryResult.TotalResultCount > 0
-			getLinkToItem msg, json.QueryResult.Results[0], "defect"
-			description = "No Description"
-			if json.QueryResult.Results[0].Description
-				prettifyDescription json.QueryResult.Results[0].Description, (output) ->
-					description = output
-					return_array = [
-						"#{json.QueryResult.Results[0].FormattedID} - #{json.QueryResult.Results[0]._refObjectName}"
-						"Owner - #{ if json.QueryResult.Results[0].Owner then json.QueryResult.Results[0].Owner._refObjectName else "No Owner" }"
-						"Project - #{ if json.QueryResult.Results[0].Project then json.QueryResult.Results[0].Project._refObjectName else "No Project" }"
-						"Severity - #{json.QueryResult.Results[0].Severity}"
-						"State - #{json.QueryResult.Results[0].State}"
-						"#{description}"
-						]
-					cb return_array.join("\n")
-		else
-			cb "Aww snap, I couldn't find that bug!"
+queryRequest = (msg, typeInfo, idNumber, cb) ->
+  queryName = typeInfo.queryName || typeInfo.name
+  queryString = "/#{queryName}.js?query=(FormattedID = #{idNumber})&fetch=true"
+  rallyRequest msg, queryString, (json) ->
+    if json && json.QueryResult.TotalResultCount > 0
+      result = json.QueryResult.Results[0]
+      linkName = typeInfo.linkName || typeInfo.name
+      getLinkToItem msg, result, linkName
+      description = 'No Description'
+      prettifyDescription result.Description, (output) ->
+        description = output || description
+        returnArray = [
+          "#{result.FormattedID} - #{result.Name}"
+          labeledField(result, 'Owner._refObjectName')
+          labeledField(result, 'Project._refObjectName')
+        ]
+        returnArray.push(labeledField(result, field)) for field in typeInfo.extraOutputFields
+        returnArray.push("Description:")
+        returnArray.push("#{description}")
+        cb returnArray.join("\n")
+    else
+      cb "Aww snap, I couldn't find that #{typeInfo.name}!"

-taskRequest = (msg, taskId, cb) ->
-	query_string = "/task.js?query=(FormattedID = #{taskId})&fetch=true"
-	rallyRequest msg, query_string, (json) ->
-		if json && json.QueryResult.TotalResultCount > 0
-			getLinkToItem msg, json.QueryResult.Results[0], "task"
-			return_array = [
-				"#{json.QueryResult.Results[0].FormattedID} - #{json.QueryResult.Results[0].Name}"
-				"Owner - #{ if json.QueryResult.Results[0].Owner then json.QueryResult.Results[0].Owner._refObjectName else "No Owner" }"
-				"Project - #{ if json.QueryResult.Results[0].Project then json.QueryResult.Results[0].Project._refObjectName else "No Project" }"
-				"State - #{json.QueryResult.Results[0].State}"
-				"Feature - #{if json.QueryResult.Results[0].WorkProduct ? json.QueryResult.Results[0].WorkProduct._refObjectName  else "Not associated with any feature"}"
-				]
-			cb return_array.join("\n")
-		else
-			cb "Aww snap, I couldn't find that task!"
+labeledField = (result, field) ->
+  match = field.match(/^(\w+)\._refObjectName$/)
+  if match
+    "#{match[1]}: #{refObjectName(result, match[1])}"
+  else
+    "#{field}: #{result[field]}"

-storyRequest = (msg, storyId, cb) ->
-	query_string = "/hierarchicalrequirement.js?query=(FormattedID = #{storyId})&fetch=true"
-	rallyRequest msg, query_string, (json) ->
-		if json && json.QueryResult.TotalResultCount > 0
-			getLinkToItem msg, json.QueryResult.Results[0], "userstory"
-			description = "No Description"
-			prettifyDescription json.QueryResult.Results[0].Description, (output) ->
-				description = output || description
-				return_array = [
-					"#{json.QueryResult.Results[0].FormattedID} - #{json.QueryResult.Results[0].Name}"
-					"Owner - #{ if json.QueryResult.Results[0].Owner then json.QueryResult.Results[0].Owner._refObjectName else "No Owner" }"
-					"Project - #{ if json.QueryResult.Results[0].Project then json.QueryResult.Results[0].Project._refObjectName else "No Project" }"
-					"ScheduleState - #{json.QueryResult.Results[0].ScheduleState}"
-					"#{description}"
-					]
-				cb return_array.join("\n")
-		else
-			cb "Aww snap, I couldn't find that story!"
+refObjectName = (result, field) ->
+  if result[field] then result[field]._refObjectName else "No #{field}"

 rallyRequest = (msg, query, cb) ->
-	rally_url = 'https://rally1.rallydev.com/slm/webservice/' + api_version + query
-	basicAuthRequest msg, rally_url, (json) ->
-		cb json
+  rally_url = 'https://rally1.rallydev.com/slm/webservice/' + api_version + query
+#  logger.debug "rally_url = #{rally_url}"
+  basicAuthRequest msg, rally_url, (json) ->
+#    if json
+#      logger.debug "json = #{JSON.stringify(json)}"
+    cb json

 basicAuthRequest = (msg, url, cb) ->
-	auth = 'Basic ' + new Buffer(user + ':' + pass).toString('base64');
-	msg.http(url)
-		.headers(Authorization: auth, Accept: 'application/json')
-		.get() (err, res, body) ->
-			json_body = null
-			switch res.statusCode
-				when 200 then json_body = JSON.parse(body)
-				else json_body = null
-			cb json_body
+  auth = 'Basic ' + new Buffer(user + ':' + pass).toString('base64');
+  msg.http(url)
+    .headers(Authorization: auth, Accept: 'application/json')
+    .get() (err, res, body) ->
+      json_body = null
+      switch res.statusCode
+        when 200 then json_body = JSON.parse(body)
+        else json_body = null
+      cb json_body

 getLinkToItem = (msg, object, type) ->
-	project = if object && object.Project then object.Project else null
-	if project
-		objectId = object.ObjectID
-		jsPos = project._ref.lastIndexOf ".js"
-		lastSlashPos = project._ref.lastIndexOf "/"
-		projectId = project._ref[(lastSlashPos+1)..(jsPos-1)]
-		msg.send "https://rally1.rallydev.com/slm/rally.sp#/#{projectId}/detail/#{type}/#{objectId}"
-	else
-		#do nothing
+  project = if object && object.Project then object.Project else null
+  if project
+    objectId = object.ObjectID
+    jsPos = project._ref.lastIndexOf '.js'
+    lastSlashPos = project._ref.lastIndexOf '/'
+    projectId = project._ref[(lastSlashPos+1)..(jsPos-1)]
+    msg.send "https://rally1.rallydev.com/#/#{projectId}/detail/#{type}/#{objectId}"
+  else
+    #do nothing
+
 prettifyDescription = (html_description, cb) ->
-	child = exec "echo \"#{html_description}\" | lynx -dump -stdin", (error, stdout, stderr) ->
-		return_text = html_description
-		if !error
-		  return_text = stdout
-		cb return_text
+  child = exec "echo \"#{html_description}\" | lynx -dump -stdin", (error, stdout, stderr) ->
+    return_text = html_description
+    if !error
+      return_text = stdout
+    cb return_text
+
+stripHtml = (html, cb) ->
+  return_text = html.replace(/<style.+\/style>/g, '')
+  return_text = return_text.replace(/<\/?.+?>/g, '').replace(/<br ?\/?>/g, "\n\n").replace(/ /g, ' ').replace(/[ ]+/g, ' ').replace(/%22/g, '"').replace(/&/g, '&')
+  return_text = return_text.replace(/>/g, '>').replace(/</g, '<')
+  cb return_text


Update scalr.coffee

Update scalr.coffee

And of course I forgot the https:// at the beginning.

--- src/scripts/scalr.coffee
+++ src/scripts/scalr.coffee
@@ -28,7 +28,7 @@
   'Victory!',
 ]

-scalr_script_executer = "execute-scalr-script.eu01.aws.af.cm"
+scalr_script_executer = "https://execute-scalr-script.eu01.aws.af.cm"

 config_check = ->
   failure = (


fix the strip_insignificant_zeros behavior in the coffee code

--- coffee/number_helpers.coffee
+++ coffee/number_helpers.coffee
@@ -100,7 +100,17 @@
     integer = NumberHelpers.number_with_delimiter(integer, {delimiter: _delimiter})

     # Strip Insignificant Digits
-    decimal = '' if _strip_insignificant_zeros
+    if _strip_insignificant_zeros
+      dlen = decimal.length
+      newlen = dlen
+
+      while newlen > 0 and decimal[newlen - 1] == '0'
+        newlen = newlen - 1
+
+      if newlen == 0
+        decimal = ''
+      else if newlen != dlen
+        decimal = decimal.slice(0, newlen)

     # Remove separator if no decimal
     _separator = '' unless decimal


Tests: Bump up timeout on standalone handoff new listener test

--- test/handoffs/standalone.coffee
+++ test/handoffs/standalone.coffee
@@ -232,10 +232,11 @@
                 done()

         it "new process should accept a new listener connection", (done) ->
+            this.timeout 5000
             l2 = new StreamListener "127.0.0.1", main_port, STREAM1.key

-            l2.connect 1500, (err) =>
+            l2.connect 4500, (err) =>
                 throw err if err

                 l2.once "bytes", ->
-                    done()+                    done()


more tests for fsExtensions

more tests for fsExtensions

--- spec/fsExtenstionsSpec.coffee
+++ spec/fsExtenstionsSpec.coffee
@@ -24,7 +24,9 @@
     describe 'when reading dir as tree and provided path to empty dir', ->

         it 'empty dir tree should be returned', ->
-            spyOn(fsExtensions.fs,"lstatSync").andReturn({isDirectory:-> false})
+
+            spyOn(fsExtensions.fs,"lstatSync").andReturn({isDirectory:-> true})
+            spyOn(fsExtensions.fs,"readdirSync").andReturn({sort:-> []})

             expect( fsExtensions.readDirAsTree('empty')).toEqual({dirName:'empty',dirs:[],files:[]})

@@ -35,6 +37,53 @@

             expect( fsExtensions.readDirAsTree('_hidden')).toEqual({dirName:'_hidden',dirs:[],files:[]})

+    describe 'when reading dir as tree and provided path to dir with two dirs nest', ->
+        dirDepth = 0
+        it 'dir tree with two nest level should be returned returned', ->

-# gdy file jest hidden
-# przyklad jakiś zagdniezdzonych katalogow+            spyOn(fsExtensions.fs,"lstatSync").andReturn(
+                    {
+                    isDirectory:->
+                        if dirDepth++ < 5
+                            true
+                        else
+                            false
+                    }
+                )
+            spyOn(fsExtensions.fs,"readdirSync").andReturn(
+                    {sort:->
+                        ['one', 'two', 'three', 'four', 'five'].slice(dirDepth)
+                    }
+            )
+
+            expect( fsExtensions.readDirAsTree('tst')).toEqual(
+                    {
+                    dirName : 'tst',
+                    dirs : [ {dirName : 'two',dirs : [ {dirName : 'four', dirs : [  ], files : [  ] }],files : [ 'five' ]}],
+                    files : [ 'three', 'four', 'five' ]
+                    })
+    describe 'when reading dir as tree and provided path to dir with two dirs nest and hidden dir and file', ->
+        dirDepth = 0
+        it 'dir tree with two nest level should be returned returned without hidden dir and file', ->
+
+            spyOn(fsExtensions.fs,"lstatSync").andReturn(
+                    {
+                    isDirectory:->
+                        if dirDepth++ < 5
+                            true
+                        else
+                            false
+                    }
+                )
+            spyOn(fsExtensions.fs,"readdirSync").andReturn(
+                    {sort:->
+                        ['one', 'two', 'three', '_four', 'five'].slice(dirDepth)
+                    }
+            )
+
+            expect( fsExtensions.readDirAsTree('tst')).toEqual(
+                    {
+                    dirName : 'tst',
+                    dirs : [ {dirName : 'two',dirs : [ {dirName : 'five', dirs : [  ], files : [] }], files : []}],
+                    files : [ 'three', 'five' ]
+                    })


Rm log

Rm log

--- components/autocomplete/view.coffee
+++ components/autocomplete/view.coffee
@@ -47,7 +47,6 @@
       @$el.removeClass 'is-active'

   __search__: ->
-    console.log 'search'
     term = _s.trim (@$input ?= @$('input')).val()

     if @term isnt term and not _.isEmpty(term)


:shirt: Unnecessary fat arrow

:shirt: Unnecessary fat arrow

--- spec/themes-panel-spec.coffee
+++ spec/themes-panel-spec.coffee
@@ -117,7 +117,7 @@
       spyOn(packageManager, 'runCommand').andCallFake (args, callback) ->
         installCallback = callback
         onWillThrowError: ->
-      spyOn(atom.packages, 'loadPackage').andCallFake (name) =>
+      spyOn(atom.packages, 'loadPackage').andCallFake (name) ->
         installed.user.push {name, theme: 'ui'}

       expect(panel.communityCount.text().trim()).toBe '1'


Remove breaking include

Remove breaking include

--- client.coffee
+++ client.coffee
@@ -5,7 +5,6 @@
 Plugin = require 'plugin'
 Page = require 'page'
 Server = require 'server'
-Timer = require 'timer'
 Ui = require 'ui'
 Icon = require 'icon'
 Form = require 'form'


HoverItem coffee - Fix adjustText and checkPlugins init

--- app/assets/javascripts/martian_components/components/hover_group.coffee
+++ app/assets/javascripts/martian_components/components/hover_group.coffee
@@ -13,6 +13,9 @@
     @slider = $('[data-slick-carousel]', @el).not('[data-slick-carousel="false"]')

     @initSlider()
+
+    @items.map 'checkPlugins'
+    @items.map 'adjustText'

   initSlider: =>
     defaultOptions =
@@ -40,12 +43,8 @@

   onResponsiveSizeChange: ->
     @items.map 'checkPlugins'
+    @items.map 'adjustText'
     @slider.slick('slickGoTo', 0)
-
-  onResize: ->
-    super
-    @items.map 'adjustText'
-

 class @MC.HoverItem extends @MC.Base
   constructor: (@el, @hoverGroup) ->


[Fixes #60961796] resetting filters should restore the default time range

--- app/assets/javascripts/trade/controllers/shipments_controller.js.coffee
+++ app/assets/javascripts/trade/controllers/shipments_controller.js.coffee
@@ -75,14 +75,20 @@
       return params
     return []

+  defaultTimeStart: ( ->
+    new Date().getFullYear() - 5
+  ).property()
+  defaultTimeEnd: ( ->
+    new Date().getFullYear()
+  ).property()
   years: ( ->
     [1975..new Date().getFullYear()].reverse()
   ).property()
   selectedTimeStart: ( ->
-    new Date().getFullYear() - 5
+    @get('defaultTimeStart')
   ).property()
   selectedTimeEnd: ( ->
-    new Date().getFullYear()
+    @get('defaultTimeEnd')
   ).property()

   allAppendices: [
@@ -163,4 +169,6 @@
           @set(property.name, null)
       @set('permitQuery', null)
       @set('taxonConceptQuery', null)
+      @set('selectedTimeStart', @get('defaultTimeStart'))
+      @set('selectedTimeEnd', @get('defaultTimeEnd'))
       @openShipmentsPage(false)


formview lazydom adjustment

formview lazydom adjustment

--- components/forms/KDFormView.coffee
+++ components/forms/KDFormView.coffee
@@ -12,17 +12,21 @@

     return inputs

+
   ###
   INSTANCE LEVEL
   ###
   constructor:(options = {}, data)->

+    options.tagName      = "form"
+    options.cssClass     = KD.utils.curryCssClass "kdformview", options.cssClass
     options.callback   or= noop     # a Function
     options.customData or= {}       # an Object of key/value pairs
     options.bind       or= "submit" # a String of space separated event names

     super options,data

+    @unsetClass "kdview"
     @valid = null
     @setCallback options.callback
     @customData = {}
@@ -32,10 +36,6 @@
     @emit 'inputWasAdded', child  if child instanceof KDInputView

     super
-
-  setDomElement:()->
-    cssClass = @getOptions().cssClass ? ""
-    @domElement = $ "<form class='kdformview #{cssClass}'></form>"

   getCustomData:(path)->
     if path


Internal server uses a single catch-all route.

--- src/server/server.coffee
+++ src/server/server.coffee
@@ -95,20 +95,26 @@

 internal = do ->
     server = express.createServer()
-    load = (ext, req, res) ->
-        pagePath = path.join(process.cwd(), 'html', "#{req.params.source}.#{ext}")
-        fs.readFile pagePath, 'utf8', (err, html) ->
-            contenttype = 'text/html' # common case
-            if err
-                throw new Error(err)
-            if ext == 'js'
-                contenttype = 'text/javascript'
-            res.writeHead 200,
-                'Content-type': contenttype
-                'Content-length': html.length
-            res.end(html)
-    server.get '/:source.js', (req, res) -> load 'js', req, res
-    server.get '/:source.html', (req, res) -> load 'html', req, res
+    server.get '*', (req, res, next) ->
+        reqPath = req.params[0]
+        contentType = null
+        if /\.js$/.test(reqPath)
+            contentType = 'text/javascript'
+        else if /\.html$/.test(reqPath)
+            contentType = 'text/html'
+        else if /\.css$/.test(reqPath)
+            contentType = 'text/css'
+        else
+            next()
+        if contentType != null
+            pagePath = path.join(process.cwd(), 'html', reqPath)
+            fs.readFile pagePath, 'utf8', (err, data) ->
+                if err
+                    throw new Error(err)
+                res.writeHead 200,
+                    'Content-type': contentType
+                    'Content-length': Buffer.byteLength(data)
+                res.end(data)
     server.listen 3001, ->
         console.log 'Internal HTTP server listening on port 3001 [TODO: remove this].'
     server


respond immediately and don't care if save worked

--- controllers/deploys.coffee
+++ controllers/deploys.coffee
@@ -8,15 +8,15 @@
 return if app.disabled('deploys')

 # create (if anyone finds the team by code, they're authorized)
-app.all '/teams/:code/deploys', [m.loadTeam], (req, res, next) ->
+app.all '/teams/:code/deploys', [m.loadTeam], (req, res) ->
   util.log "#{'DEPLOY'.green} #{req.team.name} (#{req.team.id})"
   req.session.destroy()
+  res.end 'ok'

-  attr = _.clone(req.body || {})
+  attr = _.clone req.body || {}
   attr.isProduction = false # TODO check where the request came from
   attr.teamId = req.team.id
   attr.remoteAddress = req.socket.remoteAddress
-  delete attr.code

   deploy = new Deploy attr
-  deploy.save -> res.send 'ok'
+  deploy.save()


add res.updateContext()

add res.updateContext()

 On branch master

	modified:   controllers/gallery.coffee

--- controllers/gallery.coffee
+++ controllers/gallery.coffee
@@ -12,6 +12,9 @@

     multipart = multipartParser()
     widgetServer = staticServer(GALLERY)
+
+    # The extendContext middleware is used for all requests.
+    use extendContext

     handler 'index', index
     handler 'widgetPost', multipart, widgetPost, index
@@ -31,9 +34,9 @@
     entries = entries.filter (name) ->
         return not /^_/.test(name)

+    res.updateContext({entries: entries})
+
     res.render (format, context) ->
-        context.entries = entries
-        context = extendContext(context)
         return @html(200, '/gallery/index.html', context)

     res.finish()
@@ -59,10 +62,9 @@

 widgetFrame = (req, res, next) ->
     widgetId = req.params['widget']
+    res.updateContext({item: widgetId})

     res.render (format, context) ->
-        context.item = widgetId
-        context = extendContext(context)
         return @html(200, '/gallery/preview.html', context)

     res.finish()
@@ -82,8 +84,10 @@
 # Utilities
 #

-extendContext = (context) ->
+extendContext = (req, res, next) ->
+    print !!req, !!res, !!next
     static_domain = process.env['STATIC_DOMAIN'] || 'www.pinfinity.co'
     ext =
         static_domain: "http://#{static_domain}"
-    return extend(context, ext)
+    res.updateContext(ext)
+    return next()


for user debug

for user debug

--- packages/rocketchat-ui-flextab/flex-tab/tabs/membersList.coffee
+++ packages/rocketchat-ui-flextab/flex-tab/tabs/membersList.coffee
@@ -34,6 +34,7 @@
 					utcOffset = "+#{utcOffset}"
 				utcOffset = "(UTC #{utcOffset})"
 			user = Meteor.users.findOne({username:username})
+			console.log user
 			if (user)
 				realname = user.name
 			else


SocialWorker: JAccount: remove NewWorkspaceCreated shared instance event

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -47,13 +47,7 @@
       type               : 'ascending'
     sharedEvents    :
       static        : []
-      instance      : [
-        # this is commented-out intentionally
-        # when a user sends a status update, we are sending 7 events
-        # when a user logs-in we are sending 10 events
-        # { name: 'updateInstance' }
-        { name : 'NewWorkspaceCreated' }
-      ]
+      instance      : []
     sharedMethods :
       static:
         one: [


payment: add an api to call to see if a user can purchase

--- workers/social/lib/social/models/payment.coffee
+++ workers/social/lib/social/models/payment.coffee
@@ -23,6 +23,8 @@
           (signature Object, Function)
         getToken          :
           (signature Object, Function)
+        canUserPurchase   :
+          (signature Function)

   { get, post, deleteReq } = require "./socialapi/requests"
@@ -112,6 +114,10 @@
     SiftScience = require "./siftscience"
     SiftScience.createOrder client, raw, callback

+  @canUserPurchase = secure (client, callback)->
+    client.connection.delegate.fetchUser (err, user)->
+      return callback err  if err
+      callback null, user.status is 'confirmed'

   validateParams = (requiredParams, data, callback)->
     for param in requiredParams


payment: minor fixes

payment: minor fixes

--- workers/social/lib/social/models/payment.coffee
+++ workers/social/lib/social/models/payment.coffee
@@ -88,6 +88,8 @@
     requiredParams = [ "planTitle" ]

     validateParams requiredParams, data, (err)->
+      return callback err  if err
+
       canChangePlan client, data.planTitle, callback

   @deleteAccount = (client, callback)->
@@ -96,7 +98,7 @@

     deleteReq url, {}, callback

-  @getToken = (client, data, callback)->
+  @getToken = secure (client, data, callback)->
     requiredParams = [ "planTitle", "planInterval" ]

     validateParams requiredParams, data, (err)->


Tracker: fix linter

Tracker: fix linter

--- workers/social/lib/social/models/tracker.coffee
+++ workers/social/lib/social/models/tracker.coffee
@@ -17,7 +17,7 @@
   Analytics = require('analytics-node')
   analytics = new Analytics(KONFIG.segment)
 catch e
-  console.warn "Segment disabled because of missing configuration"
+  console.warn 'Segment disabled because of missing configuration'

 module.exports = class Tracker extends bongo.Base


unicode-aware word splitter

unicode-aware word splitter

--- chrome/content/zotero-better-bibtex/BetterBibTeXPatternFormatter.coffee
+++ chrome/content/zotero-better-bibtex/BetterBibTeXPatternFormatter.coffee
@@ -58,7 +58,7 @@
     return safe

   words: (str) ->
-    return (@clean(word) for word in @innerText(str).split(/[\+\.,-\/#!$%\^&\*;:{}=\-\s`~()]+/) when word != '')
+    return (@clean(word) for word in Zotero.Utilities.XRegExp.matchChain(@innerText(str), [XRegExp("[\\p{Alphabetic}\\p{Nd}\\{Pc}\\p{M}]+", "g")]) when word != '')

   # three-letter month abbreviations. I assume these are the same ones that the
   # docs say are defined in some appendix of the LaTeX book. (i don't have the


update tooltip after clicked

update tooltip after clicked

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -130,7 +130,6 @@
 		['h1', 'h2', 'pre'].forEach (tag, i)->
 			btn = $("#tooltip .left-panel li:nth-child(#{i+1})")
 			if jqel.is tag
-				console.log btn
 				btn.addClass 'toggled'
 			else
 				btn.removeClass 'toggled'
@@ -152,17 +151,16 @@

 		# tooltip functions
 		$('#tooltip').delegate '.left-panel li:nth-child(1)', 'click', (e)->
-			self.toggleFormatBlock $('.hovered').first(), 'h1'
-			self.update()
+			self.toggleFormatBlock $('.hovered'), 'h1'
+			self.updateTooltipStatus $('.hovered')
 		$('#tooltip').delegate '.left-panel li:nth-child(2)', 'click', (e)->
-			self.toggleFormatBlock $('.hovered').first(), 'h2'
-			self.update()
+			self.toggleFormatBlock $('.hovered'), 'h2'
+			self.updateTooltipStatus $('.hovered')
 		$('#tooltip').delegate '.left-panel li:nth-child(3)', 'click', (e)->
-			self.toggleFormatBlock $('.hovered').first(), 'pre'
-			self.update()
+			self.toggleFormatBlock $('.hovered'), 'pre'
+			self.updateTooltipStatus $('.hovered')
 		$('body').delegate '#tooltip', 'mouseleave', (e)->
 			self.hideTooltip()
-			self.update()

 		# insertion enter image url
 		$('#insertion').delegate 'input', 'keydown', (e)->


Ensure app doesn't halt even if fonts time out.

--- src/state-preload.coffee
+++ src/state-preload.coffee
@@ -22,7 +22,8 @@
       # One area of additional complexity is Google webfont loading, given that
       # the font needs to be loaded before any Phaser text using it renders.
       window.WebFontConfig =
-        active: => @time.events.add Timer.SECOND, @menu
+        active: @_onWebFontLoadComplete
+        inactive: @_onWebFontLoadComplete
         google: { families: [ 'Enriqueta:400:latin' ] }

     preload: ->
@@ -62,4 +63,7 @@
       state = if developing then 'play' else 'menu'
       @state.start state

+    _onWebFontLoadComplete: =>
+      @time.events.add Timer.SECOND, @menu
+
   PreloadState


test: correct test descriptions

test: correct test descriptions

--- test/rest/roleScopesSpec.coffee
+++ test/rest/roleScopesSpec.coffee
@@ -52,7 +52,7 @@
       it 'should return a promise', ->
         promise.should.be.instanceof Promise

-      it 'should provide the roles', ->
+      it 'should provide the scopes', ->
         success.should.have.been.calledWith sinon.match [{name:'realm'}]

       it 'should not catch an error', ->
@@ -87,7 +87,7 @@
       it 'should return a promise', ->
         promise.should.be.instanceof Promise

-      it 'should not provide the roles', ->
+      it 'should not provide the scopes', ->
         success.should.not.have.been.called

       it 'should catch an error', ->
@@ -160,7 +160,7 @@
       it 'should return a promise', ->
         promise.should.be.instanceof Promise

-      it 'should not provide the role', ->
+      it 'should not provide the scope', ->
         success.should.not.have.been.called

       it 'should catch an error', ->
@@ -196,7 +196,7 @@
       it 'should return a promise', ->
         promise.should.be.instanceof Promise

-      it 'should provide the role', ->
+      it 'should provide the scope', ->
         success.should.have.been.called

       it 'should not catch an error', ->
@@ -231,7 +231,7 @@
       it 'should return a promise', ->
         promise.should.be.instanceof Promise

-      it 'should not provide the role', ->
+      it 'should not provide the scope', ->
         success.should.not.have.been.called

       it 'should catch an error', ->


go/druid: change druid credential name

go/druid: change druid credential name

--- config/generateKonfig.coffee
+++ config/generateKonfig.coffee
@@ -183,7 +183,7 @@
     mailgun                : credentials.mailgun
     segment                : credentials.segment
     dummyAdmins            : credentials.dummyAdmins
-    druidHost              : credentials.druidHost
+    druid                  : credentials.druid

     algolia                : algoliaSecret
     gatekeeper             : gatekeeper
@@ -250,7 +250,7 @@
     awsKeys                       : credentials.awsKeys
     segment                       : credentials.segment
     dummyAdmins                   : credentials.dummyAdmins
-    druidHost                     : credentials.druidHost
+    druid                         : credentials.druid

     paymentwebhook                : paymentwebhook
     regions                       : regions


Add Substitutions 2 (http://xkcd.com/1625)

--- src/substitutions.coffee
+++ src/substitutions.coffee
@@ -1,5 +1,6 @@
 substitute = (node) ->
   replacements = [
+    # http://www.xkcd.com/1288
     ['Keyboard', 'Leopard']
     ['KEYBOARD', 'LEOPARD']
     ['keyboard', 'leopard']
@@ -62,12 +63,71 @@
     ['COULD NOT BE REACHED FOR COMMENT', 'IS GUILTY AND EVERYONE KNOWS IT']
     ['Could Not Be Reached For Comment', 'Is Guilty And Everyone Knows It']
     ['could not be reached for comment', 'is guilty and everyone knows it']
+    # http://xkcd.com/1418/
     ['Force', 'Horse']
     ['FORCE', 'HORSE']
     ['force', 'horse']
     ['Forces', 'Horses']
     ['FORCES', 'HORSES']
     ['forces', 'horses']
+    # http://xkcd.com/1625
+    ['Debate', "Dance-off"]
+    ['DEBATE', "DANCE-OFF"]
+    ['debate', "dance-off"]
+    ['Self driving', 'Uncontrollably swerving']
+    ['SELF DRIVING', 'UNCONTROLLABLY SWERVING']
+    ['self driving', 'uncontrollably swerving']
+    ['Poll', 'Psychic reading']
+    ['POLL', 'PSYCHIC READING']
+    ['poll', 'psychic reading']
+    ['Candidate', 'Airbender']
+    ['CANDIDATE', 'AIRBENDER']
+    ['candidate', 'airbender']
+    ['Drone', 'Dog']
+    ['DRONE', 'DOG']
+    ['drone', 'dog']
+    ['Vows to', 'Probably won\'t']
+    ['VOWS TO', 'PROBABLY WON\'T']
+    ['vows to', 'probably won\'t']
+    ['At large', 'Very large']
+    ['AT LARGE', 'VERY LARGE']
+    ['at large', 'very large']
+    ['Successfully', 'Suddenly']
+    ['SUCCESSFULLY', 'SUDDENLY']
+    ['successfully', 'suddenly']
+    ['Expands', 'Physically expands']
+    ['EXPANDS', 'PHYSICALLY EXPANDS']
+    ['expands', 'physically expands']
+    ['First-degree', 'Friggin\' awful']
+    ['FIRST-DEGREE', 'FRIGGIN\' AWFUL']
+    ['first-degree', 'friggin\' awful']
+    ['Second-degree', 'Friggin\' awful']
+    ['SECOND-DEGREE', 'FRIGGIN\' AWFUL']
+    ['second-degree', 'friggin\' awful']
+    ['Third-degree', 'Friggin\' awful']
+    ['THIRD-DEGREE', 'FRIGGIN\' AWFUL']
+    ['third-degree', 'friggin\' awful']
+    ['An unknown number', 'Like hundreds']
+    ['AN UNKNOWN NUMBER', 'LIKE HUNDREDS']
+    ['an unknown number', 'like hundreds']
+    ['Front runner', 'Blade runner']
+    ['FRONT RUNNER', 'BLADE RUNNER']
+    ['front runner', 'blade runner']
+    ['Global', 'Spherical']
+    ['GLOBAL', 'SPHERICAL']
+    ['global', 'spherical']
+    ['Years', 'Minutes']
+    ['YEARS', 'MINUTES']
+    ['years', 'minutes']
+    ['No indication', 'Lots of signs']
+    ['NO INDICATION', 'LOTS OF SIGNS']
+    ['no indication', 'lots of signs']
+    ['Urged restraint by', 'Drunkenly egged on']
+    ['URGED RESTRAINT BY', 'DRUNKENLY EGGED ON']
+    ['urged restraint by', 'drunkenly egged on']
+    ['Horsepower', 'Tons of horsemeat']
+    ['HORSEPOWER', 'TONS OF HORSEMEAT']
+    ['horsepower', 'tons of horsemeat']
   ]

   ignore = ['STYLE', 'SCRIPT', 'NOSCRIPT', 'IFRAME', 'OBJECT', 'INPUT', 'FORM', 'TEXTAREA']


hide overlay

hide overlay

--- lib/assets/javascripts/socmap_adf/modules/gmap/views/polygon.js.coffee
+++ lib/assets/javascripts/socmap_adf/modules/gmap/views/polygon.js.coffee
@@ -115,7 +115,7 @@
     @labelView.hideOverlayAfterTime() if @labelView

   onMouseOver: (e) =>
-    @labelView.openOverlayOnHover() if @labelView && !@isEditable() && @content
+    @labelView.openOverlayOnHover() if @labelView && !@isEditable() && @content && @content.length > 0

   onMouseMove: (e) =>
     @labelView.hide() if @isEditable() && @labelView && @labelView.opened


postgres: set/get project state

postgres: set/get project state

--- src/smc-hub/postgres.coffee
+++ src/smc-hub/postgres.coffee
@@ -2055,10 +2055,34 @@
         @_get_project_column('storage_request', opts.project_id, opts.cb)

     set_project_state: (opts) =>
-        throw Error("NotImplementedError")
+        opts = defaults opts,
+            project_id : required
+            state      : required
+            time       : new Date()
+            error      : undefined
+            cb         : required
+        if typeof(opts.state) != 'string'
+            opts.cb("invalid state type")
+            return
+        if not COMPUTE_STATES[opts.state]?
+            opts.cb("state = '#{opts.state}' it not a valid state")
+            return
+        state =
+            state : opts.state
+            time  : opts.time
+        if opts.error
+            state.error = opts.error
+        @_query
+            query     : "UPDATE projects"
+            set       : "state::JSONB" : state
+            where     : 'project_id :: UUID = $' : opts.project_id
+            cb        : opts.cb

     get_project_state: (opts) =>
-        throw Error("NotImplementedError")
+        opts = defaults opts,
+            project_id : required
+            cb         : required
+        @_get_project_column('state', opts.project_id, opts.cb)

     ###
     Project quotas and upgrades


chore: disable language-javascript atom plugin

--- atom/.atom/config.cson
+++ atom/.atom/config.cson
@@ -31,6 +31,7 @@
       "markdown-preview"
       "linter-jshint"
       "omnisharp-atom"
+      "language-javascript"
     ]
     openEmptyEditorOnStart: false
     telemetryConsent: "no"


added e2e test

added e2e test

--- test/e2e.coffee
+++ test/e2e.coffee
@@ -4,11 +4,6 @@
   teardown: ->
     process.chdir __dirname

-check = require '..'
-
-gt.test 'check function', ->
-  gt.func check, 'check is a function'
-
 gt.async 'test itself', ->
   gt.exec 'node', ['index.js', '--verbose'], 0,
     'this module has all dependencies'


fix double callback in precompile resources check

--- services/web/app/coffee/Features/Compile/ClsiManager.coffee
+++ services/web/app/coffee/Features/Compile/ClsiManager.coffee
@@ -38,17 +38,17 @@
 				if validationProblems?
 					logger.log project_id:project_id, validationProblems:validationProblems, "problems with users latex before compile was attempted"
 					return callback(null, "validation-problems", null, null, validationProblems)
-			ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
-				if error?
-					logger.err err:error, project_id:project_id, "error sending request to clsi"
-					return callback(error)
-				logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI"
-				ClsiCookieManager._getServerId project_id, (err, clsiServerId)->
-					if err?
-						logger.err err:err, project_id:project_id, "error getting server id"
-						return callback(err)
-					outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
-					callback(null, response?.compile?.status, outputFiles, clsiServerId)
+				ClsiManager._postToClsi project_id, user_id, req, options.compileGroup, (error, response) ->
+					if error?
+						logger.err err:error, project_id:project_id, "error sending request to clsi"
+						return callback(error)
+					logger.log project_id: project_id, outputFilesLength: response?.outputFiles?.length, status: response?.status, compile_status: response?.compile?.status, "received compile response from CLSI"
+					ClsiCookieManager._getServerId project_id, (err, clsiServerId)->
+						if err?
+							logger.err err:err, project_id:project_id, "error getting server id"
+							return callback(err)
+						outputFiles = ClsiManager._parseOutputFiles(project_id, response?.compile?.outputFiles)
+						callback(null, response?.compile?.status, outputFiles, clsiServerId)

 	stopCompile: (project_id, user_id, options, callback = (error) ->) ->
 		compilerUrl = @_getCompilerUrl(options?.compileGroup, project_id, user_id, "compile/stop")


remove a console.log

remove a console.log

--- node_modules/jraphical/lib/module.coffee
+++ node_modules/jraphical/lib/module.coffee
@@ -247,7 +247,6 @@
       unless target.getId()
         callback new Error 'No target id!'
       else
-        console.log rdef
         new Relationship(rdef).save (err, docs)->
           if err
             callback err


useCurrent false for date time picker

useCurrent false for date time picker

--- app/assets/javascripts/folio/console/simple_form_inputs/_date_time_input.coffee
+++ app/assets/javascripts/folio/console/simple_form_inputs/_date_time_input.coffee
@@ -3,6 +3,7 @@
   sideBySide: true
   format: 'DD. MM. YYYY HH:mm'
   keepInvalid: false
+  useCurrent: false
   widgetPositioning:
     horizontal: 'auto'
     vertical: 'bottom'


adding timestamp to requestAnimationFrame fallback shim

--- src/browser_extensions.coffee
+++ src/browser_extensions.coffee
@@ -5,6 +5,6 @@
   window.msRequestAnimationFrame     ||
   (callback, element) ->
     window.setTimeout( ->
-      callback(new Date().getTime())
+      callback(+new Date())
     , 1000 / 60)


Fixed title issue in parsedRetention

Fixed title issue in parsedRetention

--- muleview/coffee/app/ParsedRetention.coffee
+++ muleview/coffee/app/ParsedRetention.coffee
@@ -15,10 +15,11 @@
       if letter == unitLetter
         name = unitName
         letterIndex = ind
-    name = name.substring(0, name.length - 1) if count = 1 # Remove the "s" for a singular form
+    name = name.substring(0, name.length - 1) if count == 1 # Remove the "s" for a singular form
+    count = parseInt(count)
     {
       name: name
-      count: parseInt(count)
+      count: count
       letter: letter
       letterValue: letterIndex
     }


removing console.log

removing console.log

--- lib/assets/javascripts/firehose/web_socket.js.coffee
+++ lib/assets/javascripts/firehose/web_socket.js.coffee
@@ -72,7 +72,6 @@

   _restartKeepAlive: =>
     doPing = =>
-      console.log "Sending keepalive ping!"
       @_sendPing()
       setNextKeepAlive()
     setNextKeepAlive = =>


remove the conditional that creates a special case when mapping.query exists

--- src/api.coffee
+++ src/api.coffee
@@ -22,21 +22,7 @@
     if (name = schema.id?.split("#")[1])?
       if (mapping = @mappings[name])?
         constructor = mapping.constructor
-        _data = data
-        if mapping.query?
-          # Some resources require query parameters to instantiate.
-          # For these, we've stuck the query definition onto the
-          # constructor.  For these cases, we substitute a simple
-          # function for the property.
-          # In usage, this looks like:
-          #   user.repository(name: "patchboard").update(content, callback)
-          data = (params) ->
-            if _data.url?
-              params.url = _data.url
-            new constructor context, {url: mapping.generate_url(params)}
-
-        else
-          data = new constructor context, _data
+        data = new constructor context, data
     return @_decorate(context, schema, data) || data


Fix pagination.

Fix pagination.

The "previous" button would try to go to the page before the first
page, resulting in a crash.

--- src/app.coffee
+++ src/app.coffee
@@ -117,7 +117,7 @@

     if page != 1
       pagestr += "⇐ First"
-      pagestr += "← Prev"
+      pagestr += "← Prev"

     for i in [start..end] by 1
       if i isnt page


Fix for bad asset compilation on production, this MUST BE REMOVED in the future

--- app/assets/javascripts/cfp/backbone/views/ranking_view.js.coffee
+++ app/assets/javascripts/cfp/backbone/views/ranking_view.js.coffee
@@ -10,4 +10,4 @@
       value: @selectedRank()

   selectedRank: =>
-    @$('#rank').val()
+    @$('#rank:checked').val()


update footer text when new data arrives

--- app/assets/javascripts/states/monitoring/helpers/graph.js.coffee
+++ app/assets/javascripts/states/monitoring/helpers/graph.js.coffee
@@ -63,13 +63,6 @@
     @el = el = $(div).find(".graph")
     @gc = el.parents("div.graph_container")

-    # set initial footer text
-    last_date   = vals[vals.length-1][0]
-    last_val    = vals[vals.length-1][1]
-    footer      = $(div).find(".footer")
-    footer_text = "Last Value: " + metric.format_value(last_val, last_date) # referenced later in unhighlightCallback
-    footer.text(footer_text)
-
     # dygraph options
     opts ||= {}
     opts = _.extend({
@@ -111,17 +104,23 @@

     g._bixby_show_spinner = false # when true, a spinner will be displayed while loading data

+    @footer = $(div).find(".footer")
+    @update_footer_text(true)
     @create_tooltip()

     # set callbacks - have to do this after initial graph created
     opts =
       highlightCallback: (e, x, pts, row, seriesName) =>
-        text = metric.format_value(pts[0].yval, x)
-        footer.text("Highlighted Value: " + text)
+        if g._bixby_dragging
+          @update_footer_text(true)
+        else
+          text = metric.format_value(pts[0].yval, x)
+          @footer.text("Highlighted Value: " + text)
+
         @show_tooltip(g, el, pts[0].canvasx, e.pageY, text)

       unhighlightCallback: (e) =>
-        footer.text(footer_text)
+        @footer.text(@footer_text)

       # allow zooming in for more granular data (don't downsample)
       zoomCallback: (minX, maxX, yRanges) =>
@@ -147,16 +146,8 @@
           # load more granular data, since we are looking at less than 12 hours of data
           g._bixby_less_granular = g.file_
           g._bixby_is_granular = true
-          new_met = new Bixby.model.Metric({
-            id: metric.id
-            host_id: metric.get("metadata").host_id
-            start: parseInt(minX / 1000)
-            end: parseInt(maxX / 1000)
-            downsample: "1m-avg"
-          })
-          Backbone.multi_fetch [ new_met ], (err, results) ->
-            metric.set({query: new_met.get("query")})
-            g.updateOptions({ file: new_met.tuples() })
+          metric.get("query").downsample = "1m-avg"
+          @fetch_more_data(minX, maxX, null, true)

     g.updateOptions(opts, true) # don't redraw here
     return g
@@ -180,6 +171,17 @@
     else if range && (matches = range.match(/^(.*?)\.\.(.*?)$/))
       # use y-axis range as given in metric info
       opts.valueRange = [ parseFloat(matches[1]), parseFloat(matches[2]) ]
+
+  # Update the footer with the last value
+  update_footer_text: (force) ->
+    if !@dygraph || (!@footer.text().match(/Last Value/) && !force)
+      return # skip updating because we are probably highlighting
+
+    pair         = @dygraph.rawData_[@dygraph.rawData_.length-1]
+    last_date    = pair[0]
+    last_val     = pair[1]
+    @footer_text = "Last Value: " + @metric.format_value(last_val, last_date)
+    @footer.text(@footer_text)

   # Enable live updating
   enable_live_update: ->
@@ -205,7 +207,7 @@
   # @param [Number] startX        start time in milliseconds
   # @param [Number] endX          end time in milliseconds
   # @param [Function] cb          [Optional] callback to fire after data has been fetched and the graph has been updated
-  fetch_more_data: (startX, endX, cb) ->
+  fetch_more_data: (startX, endX, cb, replace) ->
     @show_spinner()

     g = @dygraph
@@ -218,10 +220,15 @@
       downsample: metric.get("query").downsample || "5m-avg"
     })
     Backbone.multi_fetch [ new_met ], (err, results) =>
-      # don't replace data... add on to existing data and sort by timestamp
-      all_data = g.file_.concat(new_met.tuples()).sort (a,b) ->
-        return a[0] - b[0]
-      g.updateOptions({ file: all_data })
+      if replace
+        g.updateOptions({ file: new_met.tuples() })
+      else
+        # don't replace data... add on to existing data and sort by timestamp
+        all_data = g.file_.concat(new_met.tuples()).sort (a,b) ->
+          return a[0] - b[0]
+        g.updateOptions({ file: all_data })
+
+      @update_footer_text()
       @hide_spinner()
       if cb?
         cb.call(@)


only capture tab clicks

only capture tab clicks

--- app/assets/javascripts/typus_translate/views/tabs_view.js.coffee
+++ app/assets/javascripts/typus_translate/views/tabs_view.js.coffee
@@ -1,7 +1,7 @@
 class Typus.Translate.Views.Tabs extends Backbone.View

   events:
-    'click a': 'activateTab'
+    'click #translate-tabs a': 'activateTab'

   template: JST['typus_translate/templates/tabs']


Updating JS spec

Updating JS spec

--- spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee
+++ spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee
@@ -62,12 +62,15 @@
         id: 17

       spyOn(BulkProducts, "insertProductAfter")
+      spyOn(BulkProducts, "unpackProduct")
       BulkProducts.products = [originalProduct]
       $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200,
         product: clonedProduct
       $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, clonedProduct
       BulkProducts.cloneProduct BulkProducts.products[0]
       $httpBackend.flush()
+      expect(BulkProducts.unpackProduct).toHaveBeenCalledWith clonedProduct
+      BulkProducts.unpackProduct(clonedProduct)
       expect(BulkProducts.insertProductAfter).toHaveBeenCalledWith originalProduct, clonedProduct


Delete unused code (ssh.coffee)

Delete unused code (ssh.coffee)

Change-type: patch

--- lib/actions/local/ssh.coffee
+++ lib/actions/local/ssh.coffee
@@ -1,113 +0,0 @@
-###
-Copyright 2017 Balena
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-	 http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-###
-
-{ hostOSAccess } = require('../command-options')
-_ = require('lodash')
-
-localHostOSAccessOption = _.cloneDeep(hostOSAccess)
-localHostOSAccessOption.description = 'get a shell into the host OS'
-
-module.exports =
-	signature: 'local ssh [deviceIp]'
-	description: 'Get a shell into a balenaOS device'
-	help: '''
-		Warning: 'balena local ssh' requires an openssh-compatible client to be correctly
-		installed in your shell environment. For more information (including Windows
-		support) please check the README here: https://github.com/balena-io/balena-cli
-
-		Use this command to get a shell into the running application container of
-		your device.
-
-		The '--host' option will get you a shell into the Host OS of the balenaOS device.
-		No option will return a list of containers to enter or you can explicitly select
-		one by passing its name to the --container option
-
-		Examples:
-
-			$ balena local ssh
-			$ balena local ssh --host
-			$ balena local ssh --container chaotic_water
-			$ balena local ssh --container chaotic_water --port 22222
-			$ balena local ssh --verbose
-	'''
-	options: [
-			signature: 'verbose'
-			boolean: true
-			description: 'increase verbosity'
-			alias: 'v'
-	,
-	localHostOSAccessOption,
-			signature: 'container'
-			parameter: 'container'
-			default: null
-			description: 'name of container to access'
-			alias: 'c'
-	,
-			signature: 'port'
-			parameter: 'port'
-			description: 'ssh port number (default: 22222)'
-			alias: 'p'
-	]
-	root: true
-	action: (params, options) ->
-		child_process = require('child_process')
-		Promise = require 'bluebird'
-		_ = require('lodash')
-		{ forms } = require('balena-sync')
-
-		{ selectContainerFromDevice, getSubShellCommand } = require('./common')
-		{ exitWithExpectedError } = require('../../utils/patterns')
-
-		if (options.host is true and options.container?)
-			exitWithExpectedError('Please pass either --host or --container option')
-
-		if not options.port?
-			options.port = 22222
-
-		verbose = if options.verbose then '-vvv' else ''
-
-		Promise.try ->
-			if not params.deviceIp?
-				return forms.selectLocalBalenaOsDevice()
-			return params.deviceIp
-		.then (deviceIp) ->
-			_.assign(options, { deviceIp })
-
-			return if options.host
-
-			if not options.container?
-				return selectContainerFromDevice(deviceIp)
-
-			return options.container
-		.then (container) ->
-
-			command = "ssh \
-				#{verbose} \
-				-t \
-				-p #{options.port} \
-				-o LogLevel=ERROR \
-				-o StrictHostKeyChecking=no \
-				-o UserKnownHostsFile=/dev/null \
-				 root@#{options.deviceIp}"
-
-			if not options.host
-				shellCmd = '''/bin/sh -c $"'if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi'"'''
-				dockerCmd = "'$(if [ -f /usr/bin/balena ]; then echo \"balena\"; else echo \"docker\"; fi)'"
-				command += " #{dockerCmd} exec -ti #{container} #{shellCmd}"
-
-			subShellCommand = getSubShellCommand(command)
-			child_process.spawn subShellCommand.program, subShellCommand.args,
-				stdio: 'inherit'


Fix page links

Fix page links

--- plugins/audit/app/assets/javascripts/audit/components/shared/pagination.coffee
+++ plugins/audit/app/assets/javascripts/audit/components/shared/pagination.coffee
@@ -24,7 +24,7 @@
         for page in [1..pages]
           console.log("page: #{page} -- currentPage: #{currentPage}")
           li className: ('active' if page == currentPage), key: "page-#{page}",
-            a href: "#", onClick: ((e) -> e.preventDefault(); handlePageChange(page))
+            a href: "#", onClick: ((e) -> e.preventDefault(); handlePageChange(page)),
               page

         li null,


:memo: Doc the undo option

:memo: Doc the undo option

--- src/text-buffer.coffee
+++ src/text-buffer.coffee
@@ -547,6 +547,7 @@
   # * `text` A {String}
   # * `options` (optional) {Object}
   #   * `normalizeLineEndings` (optional) {Boolean} (default: true)
+  #   * `undo` (optional) {String} 'skip' will skip the undo system
   #
   # Returns the {Range} of the inserted text.
   setTextInRange: (range, text, options) ->
@@ -569,6 +570,7 @@
   # * `text` A {String} representing the text to insert.
   # * `options` (optional) {Object}
   #   * `normalizeLineEndings` (optional) {Boolean} (default: true)
+  #   * `undo` (optional) {String} 'skip' will skip the undo system
   #
   # Returns the {Range} of the inserted text.
   insert: (position, text, options) ->
@@ -579,6 +581,7 @@
   # * `text` A {String} representing the text text to append.
   # * `options` (optional) {Object}
   #   * `normalizeLineEndings` (optional) {Boolean} (default: true)
+  #   * `undo` (optional) {String} 'skip' will skip the undo system
   #
   # Returns the {Range} of the inserted text
   append: (text, options) ->


Move replacement down into callback

Move replacement down into callback

--- src/text-buffer.coffee
+++ src/text-buffer.coffee
@@ -21,17 +21,14 @@
     @replacementText = null
     @matchText = @match[0]

-  hasReplacedText: ->
-    @replacementText?
-
-  getReplacementText: ->
-    @replacementText
-
   getReplacementDelta: ->
-    @getReplacementText().length - @matchText.length
+    return 0 unless @replacementText?
+
+    @replacementText.length - @matchText.length

   replace: (text) =>
     @replacementText = text
+    @buffer.setTextInRange(@getRange(), @replacementText)

   stop: =>
     @stopped = true
@@ -1006,10 +1003,7 @@
     for match in matches
       callback = new SearchResultCallback(this, match, lengthDelta)
       iterator(callback)
-
-      if callback.hasReplacedText()
-        @setTextInRange(callback.getRange(), callback.getReplacementText())
-        lengthDelta += callback.getReplacementDelta() unless reverse
+      lengthDelta += callback.getReplacementDelta() unless reverse

       break unless global and callback.keepLooping()
     return


no admin button visibility test

no admin button visibility test

[#95231438]


:art: Add space before arrow

:art: Add space before arrow

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -641,7 +641,7 @@

   # Determine whether the user should be prompted to save before closing
   # this editor.
-  shouldPromptToSave: ({windowCloseRequested}={})->
+  shouldPromptToSave: ({windowCloseRequested}={}) ->
     if windowCloseRequested
       @isModified()
     else


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/text_effect.coffee
+++ src/text_effect.coffee
@@ -20,7 +20,7 @@
 ###

 ###*
-Draws th
+Draws the <code>textShadow

 @name update
 @methodOf TextEffect#


safely call to `@$` after disposal

safely call to `@$` after disposal

--- src/chaplin/views/view.coffee
+++ src/chaplin/views/view.coffee
@@ -179,6 +179,17 @@

     # Render automatically if set by options or instance property.
     @render() if @autoRender
+
+  # Override `Backbone.$`
+  # -------------------------
+
+  # This makes it safer for views to call `@$` after disposal,
+  # which happens fairly often when dealing with async callbacks.
+  $: ->
+    if @disposed
+     return $()
+    else
+      return super

   # User input event handling
   # -------------------------


Prevent same-aggregate parallel event processing in domain repository

--- lib/domain_repository.coffee
+++ lib/domain_repository.coffee
@@ -10,7 +10,7 @@
     throw new Error "Missing store" unless @store?
     throw new Error "Missing event bus emitter" unless @emitter?
     throw new Error "Missing logger" unless @logger?
-    @aggregates            = []
+    @aggregateEvents       = {}
     @directListeners       = {}
     @nextDirectListenerKey = 0
     @transacting           = false
@@ -89,7 +89,9 @@
         callback()

   add: (aggregate) ->
-    @aggregates.push aggregate
+    throw new Error "Aggregate is missing its UID" unless aggregate.uid?
+    @aggregateEvents[aggregate.uid] ?= []
+    @aggregateEvents[aggregate.uid] = @aggregateEvents[aggregate.uid].concat aggregate.appliedEvents

   # Listen for events before they are published to the event bus
   # Don't use this in reporters/reports! This is reserved for domain routines
@@ -127,17 +129,17 @@
     directListeners[directListenerKey] = listener

   _commit: (callback) ->
-    return callback null if @aggregates.length is 0
+    return callback null if Object.keys(@aggregateEvents).length is 0

     events      = [];
     savedEvents = [];

-    aggregateQueue = async.queue (aggregate, aggregateTaskCallback) =>
-      firstEvent = aggregate.appliedEvents.shift()
+    aggregateQueue = async.queue (aggregateAppliedEvents, aggregateTaskCallback) =>
+      firstEvent = aggregateAppliedEvents.shift()

       if firstEvent?
         queue = async.queue (event, eventTaskCallback) =>
-          nextEvent = aggregate.appliedEvents.shift()
+          nextEvent = aggregateAppliedEvents.shift()
           queue.push nextEvent if nextEvent?

           @logger.log "commit", "saving event \"#{event.name}\" for aggregate #{event.aggregateUid}"
@@ -166,12 +168,12 @@
       publicationQueue.drain = callback
       publicationQueue.push events

-    aggregateQueue.push @aggregates
-    @aggregates = []
+    for aggregateUid, aggregateEvents of @aggregateEvents
+      aggregateQueue.push [aggregateEvents]
+    @aggregateEvents = {}

   _rollback: (callback) ->
-    @aggregates.forEach (aggregate) =>
-      aggregate.appliedEvents = []
+    @aggregateEvents = {}
     callback()

   _publishEvent: (event, callback) ->


feat(cli): handle multiple input files

feat(cli): handle multiple input files

--- src/cli.coffee
+++ src/cli.coffee
@@ -1,3 +1,5 @@
+'use strict'
+
 path = require 'path'
 fs = require 'fs-plus'
 CSON = require 'season'
@@ -12,11 +14,11 @@
                 .describe('v', 'Output the version').alias('v', 'version').boolean('v')
                 .describe('f', 'File path to use for renderer detection when reading from stdin').alias('f', 'file-path').string('f')
   optimist.usage """
-    Usage: delexe [options] [file]
+    Usage: delexe [options] [file...]

     Input tokens from a TextMate-style lexical analyser in JSON/CSON format and render the output.

-    If no input file is specified then the token list to render is read as JSON/CSON from standard in.
+    If no input files are specified then the token list to render is read as JSON/CSON from standard in.

     If no output file is specified then the rendered output is written to standard out.
   """
@@ -30,8 +32,6 @@
     console.log(version)
     return

-  [filePath] = cli.argv._
-
   outputPath = cli.argv.output
   outputPath = path.resolve(outputPath) if outputPath

@@ -43,15 +43,16 @@
     else
       console.log(string)

-  if filePath
-    filePath = path.resolve(filePath)
-    unless fs.isFileSync(filePath)
-      console.error("Specified path is not a file: #{filePath}")
-      process.exit(1)
-      return
+  if cli.argv._.length
+    for filePath in cli.argv._
+      filePath = path.resolve(filePath)
+      unless fs.isFileSync(filePath)
+        console.error("Specified path is not a file: #{filePath}")
+        process.exit(1)
+        return

-    string = delexe.renderSync({filePath: outputPath, scopeName: cli.argv.scope})
-    output outputPath, string
+      string = delexe.renderSync({filePath: outputPath, scopeName: cli.argv.scope})
+      output outputPath, string
   else
     filePath = cli.argv.f or outputPath
     process.stdin.resume()


Print usage information in case of invalid command (fixes #18)

--- src/cli.coffee
+++ src/cli.coffee
@@ -102,8 +102,10 @@
           console.log chalk.green("#{manifest.username}/#{manifest.name}/#{manifest.version} successfully uploaded")

 command = argv._.shift() || 'help'
+if typeof commands[command] != 'function'
+  console.log 'Unknown command:', command
+  command = 'help'

-if typeof commands[command] == 'function'
-  commands[command](argv)
+commands[command].call this, argv

 logError = (err) -> console.error chalk.red(err)


-w can now watch multiple files added the -k option to activate packaging into a single templates.js

--- src/cli.coffee
+++ src/cli.coffee
@@ -1,10 +1,130 @@
-http://www.simpairs.com/scorecard.asp?session=004036&Pair=004036BotaRasaEvora003512667087341
+coffeecup = require './coffeecup'
+fs = require 'fs'
+path = require 'path'
+log = console.log
+{OptionParser} = require 'coffee-script/lib/coffee-script/optparse'
+
+argv = process.argv[2..]
+options = null
+
+handle_error = (err) -> console.log err.stack if err
+
+watch = (files, fn) ->
+  if false #fs.watch should be used, but it's not working properly
+    for file in files
+      do ->
+        currentFile = file
+        fs.watch currentFile, (event) ->
+          fn(currentFile) if event is 'change'
+  else
+    for file in files
+      do ->
+        currentFile = file
+        fs.watchFile currentFile, {persistent: true, interval: 500}, (curr, prev) ->
+          fn(currentFile) unless curr.size is prev.size and curr.mtime.getTime() is prev.mtime.getTime()
+
+compile = (input_path, output_directory, js, namespace = 'templates') ->
+  if Array.isArray input_path
+    i = 0
+    body = ''
+    appendTemplate = ->
+      if i >= input_path.length or not options.package
+        output = """
+          (function(){
+            this.#{namespace} || (this.#{namespace} = {});
+            #{body}
+          }).call(this);
+        """
+        write input_path[0], namespace, output, output_directory, '.js'
+      else
+        fs.readFile input_path[i], 'utf-8', (err, contents) ->
+          handle_error err
+          name = path.basename input_path[i], path.extname(input_path[i])
+          func = coffeecup.compile contents, options
+          body += "this.#{namespace}[#{JSON.stringify name}] = #{func};\n"
+          i += 1
+          appendTemplate()
+    return appendTemplate()
+  fs.readFile input_path, 'utf-8', (err, contents) ->
+    handle_error err
+
+    name = path.basename input_path, path.extname(input_path)
+
+    if not js
+      output = coffeecup.render contents, options
+      ext = '.html'
     else
-      file = args[0]
+      func = coffeecup.compile contents, options
+      output = """
+        (function(){
+          this.#{namespace} || (this.#{namespace} = {});
+          this.#{namespace}[#{JSON.stringify name}] = #{func};
+        }).call(this);
+      """
+      ext = '.js'
+
+    write input_path, name, output, output_directory, ext
+
+write = (input_path, name, contents, output_directory, ext) ->
+  filename = name + ext
+  dir = output_directory or path.dirname input_path
+  path.exists dir, (exists) ->
+    unless exists then fs.mkdirSync dir
+
+    output_path = path.join dir, filename
+    contents = ' ' if contents.length <= 0
+    fs.writeFile output_path, contents, (err) ->
+      handle_error err
+      log contents if options.print
+      log "Compiled #{input_path}" if options.watch
+
+usage = '''
+  Usage:
+    coffeecup [options] path/to/template.coffee
+'''
+
+switches = [
+  ['-j', '--js', 'compile template to js function (template + embedded renderer)']
+  ['-b', '--bare', 'use with -j to compile template to js (template only)' ]
+  ['-c', '--core', 'use with -j to compile renderer to js (renderer only)' ]
+  ['-n', '--namespace [name]', 'global object holding the templates (default: "templates")']
+  ['-w', '--watch', 'watch templates for changes, and recompile']
+  ['-o', '--output [dir]', 'set the directory for compiled html']
+  ['-p', '--print', 'print the compiled html to stdout']
+  ['-f', '--format', 'apply line breaks and indentation to html output']
+  ['-u', '--utils', 'add helper locals (currently only "render")']
+  ['-z', '--optimize', 'optimize resulting JS']
+  ['-v', '--version', 'display coffeecup version']
+  ['-h', '--help', 'display this help message']
+  ['-k', '--package', 'use with -j to include all templates in a single [namespace].js']
+]
+
+@run = ->
+  parser = new OptionParser switches, usage
+  options = parser.parse argv
+  args = options.arguments
+  delete options.arguments
+
+  log parser.help() if options.help or argv.length is 0
+  log coffeecup.version if options.version
+  if options.utils
+    options.locals ?= {}
+    options.locals.render = (file) ->
+      contents = fs.readFileSync file, 'utf-8'
+      coffeecup.render contents, options
+
+  if args.length > 0
+    files = args[..]

     if options.watch
-      fs.watchFile file, {persistent: true, interval: 500}, (curr, prev) ->
-        return if curr.size is prev.size and curr.mtime.getTime() is prev.mtime.getTime()
+      watch files, (file) ->
+        if options.package and options.js
+          compile files, options.output, options.js, options.namespace
+        else
+          compile file, options.output, options.js, options.namespace
+    if options.js and options.package
+      compile files, options.output, options.js, options.namespace
+    else
+      for file in files
         compile file, options.output, options.js, options.namespace

-    compile file, options.output, options.js, options.namespace


cli: Remove main() call

cli: Remove main() call

When building with Webpack there is no module.parent
so this gets called...

--- src/cli.coffee
+++ src/cli.coffee
@@ -75,5 +75,3 @@
     out = JSON.stringify results, null, 2
     console.log out
     process.exit 0
-
-main() if not module.parent


start to lay out routes

start to lay out routes

--- apps/artwork/index.coffee
+++ apps/artwork/index.coffee
@@ -11,5 +11,6 @@

 app.get '/artwork/:id/save', routes.save
 app.get '/artwork/:id/download/:filename', routes.download
+app.get '/artwork/:id/rails', routes.relatedRails
 app.get '/artwork/:id', routes.index
 app.get '/artwork/:id/:tab', routes.index


Stringify issuance response when JSON, in case null

--- apps/badges/routes.coffee
+++ apps/badges/routes.coffee
@@ -81,7 +81,7 @@
     res.send "#{cb}(#{JSON.stringify(response)})",
       'content-type': 'application/javascript'
   else
-    res.send response.toJSON(),
+    res.send JSON.stringify(response),
       'content-type': 'application/json'

 formatBadgeResponse = (req, res, badge) ->


Add method to move a resource on remote server.

--- src/coffee/sftphelpers.coffee
+++ src/coffee/sftphelpers.coffee
@@ -58,6 +58,23 @@
     deferred.promise

   ###
+  Move/rename a remote resource.
+  @param {object} sftp SFTP handle.
+  @param {string} srcPath Source path of the remote resource.
+  @param {string} destPath Destination path of the remote resource.
+  @return Promise
+  ###
+  moveFile: (sftp, srcPath, destPath) ->
+    deferred = Q.defer()
+
+    sftp.rename srcPath, destPath, (err) ->
+      if err
+        deferred.reject err
+      else
+        deferred.resolve()
+    deferred.promise
+
+  ###
   Starts an SFTP session.
   @return Promise
   ###


Aggregate updates for same inventory (avoid concurrency modification)

--- src/coffee/stockimport.coffee
+++ src/coffee/stockimport.coffee
@@ -245,15 +245,36 @@

   _createOrUpdate: (inventoryEntries, existingEntries) ->
     @logger.debug inventoryEntries, 'Inventory entries'
-    posts = _.map inventoryEntries, (entry) =>
+
+    allUpdates = _.chain(inventoryEntries)
+    .map (entry) =>
       existingEntry = @_match(entry, existingEntries)
       if existingEntry?
-        # TODO: we need to aggregate updates to same inventory, to avoid "concurrency modifications"
-        @sync.buildActions(entry, existingEntry).update()
+        @sync.buildActions(entry, existingEntry)._data
+    .filter (data) -> not _.isEmpty(data) and not _.isEmpty(data.update)
+    .reduce (acc, data) =>
+      foundUpdateWithId = _.find acc, (d) -> d.updateId is data.updateId
+      if foundUpdateWithId
+        # aggregate all updates for same inventory entry
+        @logger.debug foundUpdateWithId, "Found existing update for entry '#{data.updateId}'. About to merge actions..."
+        foundUpdateWithId.update.actions = foundUpdateWithId.update.actions.concat data.update.actions
       else
-        @client.inventoryEntries.create(entry)
-
-    @logger.debug "About to send #{_.size posts} requests"
+        @logger.debug "Update #{data.updateId} is unique, continue..."
+        acc.push data
+      acc
+    , []
+    .value()
+
+    allCreates = _.chain(inventoryEntries)
+    .map (entry) =>
+      existingEntry = @_match(entry, existingEntries)
+      @client.inventoryEntries.create(entry) unless existingEntry?
+    .filter (entry) -> not _.isEmpty entry
+    .value()
+
+    @logger.info "About to process #{_.size allUpdates} updates and #{_.size allCreates} creates"
+    posts = _.map allUpdates, (u) => @client.inventoryEntries.byId(u.updateId).update(u.update)
+    .concat allCreates
     Q.all(posts)

 module.exports = StockImport


Added negative index

Added negative index

--- src/csv.coffee
+++ src/csv.coffee
@@ -22,13 +22,18 @@

 	getStats: -> @_stats
 	getColumns: -> @_columns
-	getRow: (i) -> @_rows[i]
+	getRow: (i) ->
+		i = @getRowCount() + i if i < 0
+		return null unless @_rows[i]?
+		return @_rows[i]
 	getRows: -> @_rows
 	getRowCount: -> @_rows.length
 	getObject: (i) ->
+		row = @getRow(i)
+		return null unless row?
 		ob = {}
 		for column, j in @_columns
-			ob[column] = @_rows[i][j]
+			ob[column] = row[j] if row[j]?
 		return ob
 	getObjects: -> (@getObject(i) for i in [0...@getRowCount()])


fix async based import

fix async based import

--- client/app/views/import_view.coffee
+++ client/app/views/import_view.coffee
@@ -105,6 +105,7 @@
         # Save every imported alarms to the database.
         async.eachSeries @alarmList.collection.models, (alarm, callback) ->
             alarm.set 'tags', [calendar]
+            alarm.set 'id', null
             alarm.save null,
                 success: (model) ->
                     # When an element is successfully imported, it is added to
@@ -117,24 +118,25 @@
                     # displayed.
                     alert t 'some alarm failed to save'
                     callback()
-        , (err) ->
+        , (err) =>
             # Save every imported events to the database.
             async.eachSeries @eventList.collection.models, (event, callback) ->
                 event.set 'tags', [calendar]
+                event.set 'id', null
                 event.save null,
                     success: (model) ->
                         # When an element is successfully imported, it is added
                         # to the current calendar view.
                         app.events.add model
                         callback()
-
                     error: ->
                         # When an element failed to import, an error message is
                         # displayed.
                         alert t 'some event failed to save'
                         callback()

-            , (err) ->
+            , (err) =>
+                alert t 'import succeeded'
                 # When import is finished, the import form is reset and the
                 # calendar view is displayed.
                 @$(".confirmation").fadeOut()


wrap symbol with class for targeting

wrap symbol with class for targeting

--- client/skr/components/Currency.cjsx
+++ client/skr/components/Currency.cjsx
@@ -11,6 +11,6 @@
     render: ->
         className = _.classnames 'currency', @props.className

-            {@props.symbol}
+            {@props.symbol}
             {@props.amount.toFixed(2)}


Inlined VoteUpDownBaseView

Inlined VoteUpDownBaseView

--- core/app/backbone/views/evidence/vote_up_down_view.coffee
+++ core/app/backbone/views/evidence/vote_up_down_view.coffee
@@ -1,5 +1,8 @@
-class window.VoteUpDownBaseView extends Backbone.Marionette.ItemView
+class window.NDPEvidenceVoteView extends Backbone.Marionette.ItemView
   _.extend @prototype, Backbone.Factlink.PopoverMixin
+
+  className: 'evidence-impact-vote'
+  template: 'evidence/ndp_evidence_vote'

   events:
     "click .js-up":   "_on_up_vote"
@@ -9,18 +12,11 @@
     super
     @listenTo @model, "change", @render

-  templateHelpers: =>
-    interactive: @interactive()
-
   onRender: ->
-    @renderActive() if @interactive()
-
-  renderActive: ->
     @$('a.js-up').addClass('active') if @model.get('current_user_opinion') == 'believes'
     @$('a.js-down').addClass('active')  if @model.get('current_user_opinion') == 'disbelieves'

   _on_up_vote: ->
-    return unless @interactive()
     mp_track "Factlink: Upvote evidence click"
     if @model instanceof FactRelation && Factlink.Global.can_haz['vote_up_down_popup']
       @_open_vote_popup '.js-up', FactRelationVoteUpView
@@ -31,7 +27,6 @@
         @model.believe()

   _on_down_vote: ->
-    return unless @interactive()
     mp_track "Factlink: Downvote evidence click"
     if @model instanceof FactRelation && Factlink.Global.can_haz['vote_up_down_popup']
       @_open_vote_popup '.js-down',  FactRelationVoteDownView
@@ -46,7 +41,7 @@

     @popoverResetAll()
     @popoverAdd selector,
-      side: @side()
+      side: @_side()
       align: 'top'
       fadeTime: 40
       contentView: @_bound_popup_view view_klass
@@ -59,14 +54,7 @@

     view

-
-class window.NDPEvidenceVoteView extends VoteUpDownBaseView
-  className: 'evidence-impact-vote'
-  template: 'evidence/ndp_evidence_vote'
-
-  interactive: -> true
-
-  side: ->
+  _side: ->
     if @model.get('type') == 'believes'
       'left'
     else


<debug> Test - Parsing body of PUT response.

--- tests/controllers/driver.coffee
+++ tests/controllers/driver.coffee
@@ -48,6 +48,8 @@
             @client.sendFile "drivers", driver.file, done

         it 'should reply with the previously-created Driver', ->
+            @body = JSON.parse @body
+
             expect(@err).to.not.exist
             expect(@response.statusCode).to.equal 202
             expect(@body.id).to.equal store.driverId


remove stray console.log

remove stray console.log

--- brains.coffee
+++ brains.coffee
@@ -25,7 +25,6 @@
             @line = 0

         nextLine: (stage, user, respond) ->
-            console.log @line, stage, @answers.length
             if @line isnt stage
                 respond '...'
                 return


Remove host username logic from subscription creation


LinkShell.RemixView: no scrolling iframe.

--- coffee/src/shells/link.shell.coffee
+++ coffee/src/shells/link.shell.coffee
@@ -125,7 +125,9 @@
   render: =>
     super
     @$el.empty()
-    @$el.append acorn.util.iframe @model.get('link')
+    iframe = acorn.util.iframe @model.get('link')
+    iframe.attr 'scrolling', 'no'
+    @$el.append iframe
     @


Add tests that existed but were not referenced and ran by the test runner

--- src/resources/apps/xforms-sandbox/samples/unit-tests.coffee
+++ src/resources/apps/xforms-sandbox/samples/unit-tests.coffee
@@ -3,33 +3,37 @@

 # Test pages to run
 TestManager.setPages [
-    "test-underscore"
+    "test-bug-checkbox-update"
+    "test-bug-checkbox-update"
+    "test-control-xhtml-area"
+    "test-custom-mips"
+    "test-deferred-client-events"
+    "test-dialog"
+    "test-disabled-nested"
+    "test-do-update"
+    "test-error-ajax"
+    "test-group-delimiters"
+    "test-keypress"
+    "test-loading-indicator"
     "test-message"
     "test-orbeon-dom"
+    "test-output-update"
+    "test-repeat"
+    "test-repeat-setvalue"
+    "test-setbasepaths"
+    "test-trigger-modal"
+    "test-underscore"
+    "test-update-full"
+    "test-upload-replace-instance"
     "test-xforms-controls"
-    "test-control-xhtml-area"
-    "test-deferred-client-events"
-    "test-trigger-modal"
-    "test-upload-replace-instance"
-    "test-setbasepaths"
-    "test-output-update"
-    "test-custom-mips"
-    "test-repeat"
-    "test-error-ajax"
-    "test-bug-checkbox-update"
-    "test-do-update"
-    "test-loading-indicator"
-    "test-disabled-nested"
-    "test-bug-checkbox-update"
-    "test-repeat-setvalue"
+    "xbl/orbeon/accordion/accordion-unittest"
+    "xbl/orbeon/autocomplete/autocomplete-unittest"
+    "xbl/orbeon/button/button-unittest"
+    "xbl/orbeon/currency/currency-unittest"
+    "xbl/orbeon/datatable/datatable-dynamic-unittest"
+    "xbl/orbeon/datatable/datatable-structure-unittest"
+    "xbl/orbeon/datatable/datatable-unittest"
     "xbl/orbeon/date-picker/date-picker-unittest"
-    "xbl/orbeon/accordion/accordion-unittest"
-    "xbl/orbeon/datatable/datatable-unittest"
-    "xbl/orbeon/datatable/datatable-structure-unittest"
-    "xbl/orbeon/datatable/datatable-dynamic-unittest"
-    "xbl/orbeon/autocomplete/autocomplete-unittest"
-    "xbl/orbeon/currency/currency-unittest"
-    "xbl/orbeon/button/button-unittest"
 ]

 ORBEON.xforms.Events.orbeonLoadedEvent.subscribe () ->


Change MAX_CHECKINS_PER_SESSION

Change MAX_CHECKINS_PER_SESSION

--- server/models/Checkin.coffee
+++ server/models/Checkin.coffee
@@ -37,4 +37,4 @@

 export default Checkin

-export MAX_CHECKIN_PER_SESSION = [8, 8, 8, 8]
+export MAX_CHECKIN_PER_SESSION = [15, 15]


Fix require removed by merge

Fix require removed by merge

--- server/route_bindings.coffee
+++ server/route_bindings.coffee
@@ -14,6 +14,7 @@
 dashboardRoutes = require('./routes/dashboard')
 indicatorRoutes = require('./routes/indicators')
 sessionRoutes   = require('./routes/session')
+deployRoutes    = require('./routes/deploy')
 localeRoutes    = require('./routes/locale')
 reportRoutes    = require('./routes/reports')
 staticRoutes    = require('./routes/static')


Adding a command listener.

Adding a command listener.

--- src/irc.coffee
+++ src/irc.coffee
@@ -46,6 +46,10 @@
   part: (channel) ->
     @bot.part channel, () ->
       console.log('left %s', channel)
+
+  command: (command, strings...) ->
+    for str in strings
+      @bot.send command, str

   run: ->
     self = @


groups listing wip realmembers added

groups listing wip realmembers added

--- components/tooltip/KDTooltip.coffee
+++ components/tooltip/KDTooltip.coffee
@@ -77,8 +77,7 @@
     @view = new constructorName options, data
     @wrapper.addSubView @view

-  getView:->
-    @view
+  getView:-> @view

   delayedDisplay:(timeout = @getOptions().delayIn)->
     @utils.killWait @displayTimer


AppThumbView: Called hideLoader when app manager showed an app.

--- client/app/Applications/StartTab.kdapplication/views/appthumbview.coffee
+++ client/app/Applications/StartTab.kdapplication/views/appthumbview.coffee
@@ -230,6 +230,7 @@

     appManager.once "AppCouldntBeCreated", couldntCreate
     appManager.once "AppCreated",          appCreated
+    appManager.once "AppManagerWantsToShowAnApp", => @hideLoader()

     route = if manifest.route
       if "string" is typeof manifest.route


Social: fix multiple bugs with code

Social: fix multiple bugs with code

--- workers/social/lib/social/models/socialapi/socialaccount.coffee
+++ workers/social/lib/social/models/socialapi/socialaccount.coffee
@@ -63,7 +63,6 @@

     participantHandler = (funcName, data)->
       { group, member } = data
-      console.log "adding #{member.profile.nickname} to #{group.slug}"
       group.fetchAdmin (err, admin)=>
         return console.error "err while fetching admin", err  if err
         return console.error "couldnt find admin"  unless admin
@@ -87,17 +86,16 @@
             { socialApiChannelId, socialApiAnnouncementChannelId } = socialApiChannels

             # ensure member has socialapi id
-            member.createSocialApiId (err, socialapiId)->
+            member.createSocialApiId (err, socialApiId)->
               return console.error "couldnt create socialapi id", err  if err

               # add account into public channel
               options      =
                 channelId  : socialApiChannelId
-                accountIds : [socialapiId]
+                accountIds : [socialApiId]

               SocialChannel[funcName] client, options, (err, participants) ->
                 return console.error "couldnt add user into group socialapi chan", err, options  if err
-                console.log "added participant", participants

                 # only add koding's members to announcement channel
                 return if group.slug isnt "koding"
@@ -105,9 +103,9 @@
                 # add account into announcement channel
                 options      =
                   channelId  : socialApiChannelId
-                  accountIds : [socialApiAnnouncementChannelId]
+                  accountIds : [socialApiId]

-                SocialChannel.addParticipants client, options, (err) ->
+                SocialChannel[funcName] client, options, (err) ->
                   return console.error "couldnt add user into group socialapi chan", err, options  if err


seo: update meta data with specific activity information when a user accesses a post

--- workers/social/lib/social/render/loggedin/kodinghome.coffee
+++ workers/social/lib/social/render/loggedin/kodinghome.coffee
@@ -1,20 +1,33 @@
 module.exports = (options, callback)->
-  {account, client, bongoModels} = options
+  {account, client, bongoModels, models} = options
   getStyles       = require './../styleblock'
   fetchScripts    = require './../scriptblock'
   getTitle        = require './../title'
   getGraphMeta    = require './../graphmeta'
+  {argv}          = require 'optimist'
+  {uri}           = require('koding-config-manager').load("main.#{argv.c}")

   entryPoint         = { slug : "koding", type: "group" }
   options.entryPoint = entryPoint

-  prepareHTML = (scripts)->
+  prepareHTML = (scripts, socialApiData)->
+    if socialApiData
+      {navigated} = socialApiData
+      {slug, data:{message}} = navigated
+      {body} = message
+
+      summary = body.slice(0, 80)
+      title = "#{summary} | Koding Community"
+
+      url = if uri?.address then uri.address else "https://koding.com/"
+      shareUrl = "#{url}/#{slug}"
+
     """
     <!doctype html>
     <html lang="en">
     <head>
-      #{getTitle()}
-      #{getGraphMeta()}
+      #{getTitle title: title }
+      #{getGraphMeta shareUrl: shareUrl, body: body}
       #{getStyles()}
     </head>
     <body class='logged-in'>
@@ -29,7 +42,7 @@
     </html>
     """

-  fetchScripts options, (err, scripts)->
-    callback null, prepareHTML scripts
+  fetchScripts options, (err, scripts, socialApiData)->
+    callback null, prepareHTML scripts, socialApiData


introducing new raw, command escaping

introducing new raw, command escaping


fixing artist sorting to ignore 'the ' and stuff

--- src/mpd.coffee
+++ src/mpd.coffee
@@ -218,9 +218,7 @@
   trackComparator = makeComparator ["track", "name"]
   albumComparator = makeComparator ["year", "name"]
   artistComparator = (a, b) ->
-    a = a["name"]
-    b = b["name"]
-    if a < b then -1 else if a == b then 0 else 1
+    titleCompare a.name, b.name
   buildArtistAlbumTree: (tracks, library) =>
     # determine set of albums
     library.track_table = {}


Useless variable removed

Useless variable removed

--- src/Moon.coffee
+++ src/Moon.coffee
@@ -12,7 +12,6 @@
         this._callback = undefined
         this._stack = []
         this._step = -1
-        this._prefixes = {}
         this._loop = 1
         this._direction = true


::subscribe -> CompositeDisposable

::subscribe -> CompositeDisposable

--- src/tokenized-buffer.coffee
+++ src/tokenized-buffer.coffee
@@ -1,6 +1,6 @@
 _ = require 'underscore-plus'
 {Model} = require 'theorist'
-{Emitter} = require 'event-kit'
+{CompositeDisposable, Emitter} = require 'event-kit'
 {Point, Range} = require 'text-buffer'
 Serializable = require 'serializable'
 TokenizedLine = require './tokenized-line'
@@ -24,14 +24,18 @@

   constructor: ({@buffer, @tabLength, @invisibles}) ->
     @emitter = new Emitter
-
-    @subscribe atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated)
-    @subscribe atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated)
-
-    @subscribe @buffer.preemptDidChange (e) => @handleBufferChange(e)
-    @subscribe @buffer.onDidChangePath (@bufferPath) => @reloadGrammar()
+    @disposables = new CompositeDisposable
+
+    @disposables.add atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated)
+    @disposables.add atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated)
+
+    @disposables.add @buffer.preemptDidChange (e) => @handleBufferChange(e)
+    @disposables.add @buffer.onDidChangePath (@bufferPath) => @reloadGrammar()

     @reloadGrammar()
+
+  destroyed: ->
+    @disposables.dispose()

   serializeParams: ->
     bufferPath: @buffer.getPath()
@@ -64,11 +68,14 @@

   setGrammar: (grammar, score) ->
     return if grammar is @grammar
-    @unsubscribe(@grammar) if @grammar
+
     @grammar = grammar
     @rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
     @currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @buffer.getText())
-    @subscribe @grammar.onDidUpdate => @retokenizeLines()
+
+    @grammarUpdateDisposable?.dispose()
+    @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
+    @disposables.add(@grammarUpdateDisposable)

     @configSettings = tabLength: atom.config.get('editor.tabLength', scope: @rootScopeDescriptor)

@@ -76,7 +83,7 @@
     @grammarTabLengthSubscription = atom.config.onDidChange 'editor.tabLength', scope: @rootScopeDescriptor, ({newValue}) =>
       @configSettings.tabLength = newValue
       @retokenizeLines()
-    @subscribe @grammarTabLengthSubscription
+    @disposables.add(@grammarTabLengthSubscription)

     @retokenizeLines()


Remove -r flag from hg merge command

Remove -r flag from hg merge command

--- src/vcs/Mercurial.coffee
+++ src/vcs/Mercurial.coffee
@@ -16,7 +16,7 @@
   _doUpdate: (revision) -> @_exec(['hg', 'update', revision])

-  _doMerge: (revision) -> @_exec(['hg', 'merge', '-r', revision])
+  _doMerge: (revision) -> @_exec(['hg', 'merge', revision])

   _doStatus: -> @_exec(['hg', 'status'])


Change eq operator to work with checkboxes also

--- src/vendor_config.coffee
+++ src/vendor_config.coffee
@@ -20,7 +20,7 @@

 rivets.formatters.eq = (value, checkAgainst) ->
   if value.constructor == Array
-    value[0] == checkAgainst
+    checkAgainst in value
   else
     value == checkAgainst


removed padding in between discrete bar groups

--- src/viz/types/bar.coffee
+++ src/viz/types/bar.coffee
@@ -25,13 +25,11 @@

   stack vars, nested if vars.axes.stacked

-  space  = vars.axes[w] / vars[vars.axes.discrete].ticks.values.length
+  space   = vars.axes[w] / vars[vars.axes.discrete].ticks.values.length
+  maxSize = space - vars.labels.padding * 4

-  if vars.axes.stacked
-    maxSize = space - vars.labels.padding * 4
-  else
-    maxSize  = space / nested.length
-    maxSize -= vars.labels.padding * 2
+  unless vars.axes.stacked
+    maxSize /= nested.length
     offset   = space/2 - maxSize/2 - vars.labels.padding * 2

     x = d3.scale.linear()