greatly improve snapshot logging; ensure that snapshot servers just keep running; make them not cross filesystem boundaries

--- salvus/snap.coffee
+++ salvus/snap.coffee
@@ -57,9 +57,9 @@
         bup_dir : required
         cb      : (err, output) ->
             if err
-                winston.info("error -- #{err}")
+                winston.info("bup: error -- #{err}")
             else
-                winston.info("bup output -- #{misc.to_json(output)}")
+                winston.info("bup: output -- #{misc.to_json(output)}")

     if typeof(opts.args) == "string"
         command = "/usr/bin/bup " + opts.args
@@ -67,7 +67,7 @@
     else
         command = "/usr/bin/bup"

-    winston.debug("bup_dir = '#{opts.bup_dir}'")
+    winston.debug("bup: bup_dir = '#{opts.bup_dir}'")
     misc_node.execute_code
         command : command
         args    : opts.args
@@ -523,7 +523,7 @@
     user = "#{opts.location.username}@#{opts.location.host}"   # TODO; port and path?
     async.series([
         (cb) ->
-             # check if project is currently locked -- if so, returns an error.
+             winston.debug("create_lock: check if project is already locked")
              misc_node.execute_code
                  command     : 'ssh'
                  args        : [user, "cat '#{opts.lockfile}'"]
@@ -536,7 +536,9 @@
                          try
                              lock = misc.from_json(output.stdout)
                              if lock.expire > misc.walltime()
-                                 cb("project at #{user} is currently locked")
+                                 mesg = "create_lock: project at #{user} is currently locked"
+                                 winston.debug(mesg)
+                                 cb(mesg)
                                  return
                          catch e
                              # corrupt lock file -- we take over
@@ -544,8 +546,8 @@
         (cb) ->
              # create the lock
              lock = misc.to_json
-                    expire : misc.walltime() + opts.ttl
-                    server_id: server_id
+                    expire    : misc.walltime() + opts.ttl
+                    server_id : server_id
              misc_node.execute_code
                  command : 'ssh'
                  args    : [user, "mkdir -p .bup; echo '#{lock}' > '#{opts.lockfile}'"]
@@ -604,12 +606,12 @@

     info = opts.rollback_info

-    winston.debug("Rolling back last commit attempt in '#{info.bup_dir}'")
+    winston.debug("rollback_last_save: rolling back last commit attempt in '#{info.bup_dir}'")

     async.series([
         (cb) ->
             if info.master?
-                fs.writeFile "#{info.bup_dir}/refs/heads/master", info.master, cb
+                fs.writeFile("#{info.bup_dir}/refs/heads/master", info.master, cb)
             else
                 cb()
         (cb) ->
@@ -647,303 +649,338 @@
     winston.debug("adding #{opts.project_id} to queue")
     snapshot_queue.push({project_id:opts.project_id, cbs:[opts.cb]})

+
+
 repository_is_corrupt = false
+
 monitor_snapshot_queue = () ->
-    if snapshot_queue.length > 0
-
-        if repository_is_corrupt
-            while snapshot_queue.length > 0
-                {project_id, cbs} = snapshot_queue.shift()
-                for cb in cbs
-                    cb?("the repository is totally corrupt")
-            return
-
-        user = undefined
-        winston.debug("snapshot_queue = #{misc.to_json(snapshot_queue)}")
-        {project_id, cbs} = snapshot_queue.shift()
-
-        location    = undefined
-        user        = undefined
-        timestamp   = undefined
-        size_before = undefined
-        size_after  = undefined
-        repo_id     = undefined
-        bup_active  = undefined
-        retry_later = false
-        nothing_to_do = false
-        rollback_info = undefined
-        rollback_file = undefined
-        modified_files = undefined
-        async.series([
-            (cb) ->
-                winston.debug("snapshot: checking if disabled for #{project_id}")
-                database.select_one
-                    table   : 'projects'
-                    where   : {project_id: project_id}
-                    columns : ['snapshots_disabled']
-                    objectify : false
-                    cb      : (err, result) ->
-                        if err
-                            cb(err)
-                        else
-                            if result[0]
-                                cb("snapshots disabled for #{project_id}t")
-                            else
-                                cb()
-
-            (cb) ->
-                winston.debug("snapshot: getting active path")
-                active_path (err, path) ->
+    monitor_snapshot_queue_last_run = misc.walltime()
+    if snapshot_queue.length == 0
+        # check again soon
+        setTimeout(monitor_snapshot_queue, 3000)
+        return
+
+    if repository_is_corrupt  # TODO: I think this is never used anymore.
+        winston.debug("monitor_snapshot_queue: repository is corrupt -- emptying snapshot queue and giving up.")
+        while snapshot_queue.length > 0
+            {project_id, cbs} = snapshot_queue.shift()
+            for cb in cbs
+                cb?("the repository is totally corrupt")
+        return
+
+    user = undefined
+    winston.debug("monitor_snapshot_queue: snapshot_queue = #{misc.to_json(snapshot_queue)}")
+
+    # the code below handles exactly one project that is currently in the snapshot queue.
+    {project_id, cbs} = snapshot_queue.shift()
+
+    location       = undefined
+    user           = undefined
+    timestamp      = undefined
+    size_before    = undefined
+    size_after     = undefined
+    repo_id        = undefined
+    bup_active     = undefined
+    retry_later    = false
+    nothing_to_do  = false
+    rollback_info  = undefined
+    rollback_file  = undefined
+    modified_files = undefined
+
+    async.series([
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: checking if disabled for #{project_id}")
+            database.select_one
+                table   : 'projects'
+                where   : {project_id: project_id}
+                columns : ['snapshots_disabled']
+                objectify : false
+                cb      : (err, result) ->
                     if err
                         cb(err)
                     else
-                        repo_id = path.repo_id
-                        bup_active = path.active_path
-                        rollback_file = "#{bup_active}/rollback"
-                        cb()
-
-            # compute disk usage before save
-            (cb) ->
-                winston.debug("snapshot: compute disk usage before save")
-                misc_node.disk_usage bup_active, (err, usage) ->
-                    winston.debug("usage: #{err}, #{usage} bytes")
-                    size_before = usage
+                        if result[0]
+                            cb("snapshots disabled for #{project_id}")
+                        else
+                            cb()
+
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: getting active path")
+            active_path (err, path) ->
+                if err
                     cb(err)
-
-            # check for a leftover rollback file; this will be left around when the
-            # server is killed while making a backup.
-            (cb) ->
-                fs.exists rollback_file, (exists) ->
-                    winston.debug("found rollback file")
-                    if exists
-                        fs.readFile rollback_file, (err, data) ->
-                            if err
-                                cb(err)
-                            else
-                                s = data.toString()
-                                try
-                                    rollback_info = misc.from_json(s)
-                                    winston.debug("rollback info: '#{s}'")
-                                catch
-                                    # Something went wrong parsing the rollback JSON file; maybe it is corrupt
-                                    # or empty.  This would be a good place to take more dramatic measures,
-                                    # once they are implemented.
-                                    winston.debug("rollback file *corrupt* (not using); content='#{s}'")
-                                    rollback_info = undefined
-
-                                if rollback_info?
-                                    rollback_last_save
-                                        rollback_info : rollback_info
-                                        cb            : (err) ->
-                                            if err
-                                                cb(err)
-                                            else
-                                                fs.unlink(rollback_file, cb)
-                                else
-                                    cb()
-                    else
-                        cb()
-
-            (cb) ->
-                get_rollback_info
-                    bup_dir : bup_active
-                    cb      : (err, info) ->
-                        rollback_info = info
-                        cb(err)
-
-            # get deployed location of project (which can change at any time!)
-            (cb) ->
-                database.get_project_location
-                    project_id : project_id
-                    cb         : (err, _location) ->
+                else
+                    repo_id = path.repo_id
+                    bup_active = path.active_path
+                    rollback_file = "#{bup_active}/rollback"
+                    cb()
+
+        # compute disk usage before save
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: compute disk usage before save")
+            misc_node.disk_usage bup_active, (err, usage) ->
+                winston.debug("monitor_snapshot_queue: -- usage: err=#{err}, #{usage} bytes")
+                size_before = usage
+                cb(err)
+
+        # check for a leftover rollback file; this will be left around when the
+        # server is killed while making a backup.
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: checking for leftover rollback file")
+            fs.exists rollback_file, (exists) ->
+                if exists
+                    winston.debug("monitor_snapshot_queue: found rollback file")
+                    fs.readFile rollback_file, (err, data) ->
                         if err
-                            cb(err)
+                            cb(err)  # no reason this should happen --  can't read a file that exists? -- or if it does, should be intermittent.
                         else
-                            if not _location or not _location.username?
-                                cb("can't snapshot #{project_id} since it is not deployed")
-                                return
-                            location = _location
-                            user = "#{location.username}@#{location.host}"
-                            # TODO: support location.port != 22 and location.path != '.'   !!?
-                            cb()
-
-            # get a lock on the deployed project
-            (cb) ->
-                winston.debug("Trying to lock #{project_id} for snapshotting.")
-                create_lock
-                    location : location
-                    cb       : (err) ->
-                        if err
-                            retry_later = true
-                            winston.debug("Couldn't lock #{project_id}, so put back in the queue to try again later (in 30 seconds)")
-                            f = () ->
-                                for c in cbs
-                                    snapshot_project(project_id:project_id, cb:c)
-                            setTimeout(f, 30000)
-                        cb(err)
-
-            # create index
-            (cb) ->
-                t = misc.walltime()
-                bup
-                    args : ['on', user, 'index', '.']
-                    bup_dir : bup_active
-                    cb   : (err) ->
-                        winston.debug("time to index #{project_id}: #{misc.walltime(t)} s")
-                        cb(err)
-
-            # List the interesting modified files.
-            # This only makes sense because we just made the index above successfully.
-            # Note that "modified" is global to *all* snap servers, so this is actually
-            # a really sensible thing to do.  And if one snap server makes a snapshot,
-            # then no other snapshot server will make exactly the same snapshot (by a different name),
-            # which is what we want, since we will (eventually-when-i-implement-it!)
-            # get redundancy by rsyncing them around.
-            (cb) ->
-                misc_node.execute_code
-                    command : "/usr/bin/bup on #{user} index -m . 2>&1 | grep -v ^./.forever |grep -v ^./.sagemathcloud|grep -v '^./$'"
-                    timeout : 30  # should be very fast no matter what.
-                    bash    : true
-                    env     : {BUP_DIR : bup_active}
-                    err_on_exit : false  # if a grep along the way above is empty -- i.e., no file changes, then get a 1 exit code.  STUPID, but yep.
-                    cb      : (err, output) ->
-                        if err
-                            winston.debug("SERIOUS BUG issue: error determining number of modified files for #{project_id}: #{err}")
-                            cb(err)
-                        else
-                            modified_files = (x.slice(2) for x in output.stdout.trim().split('\n'))
-                            modified_files = (x for x in modified_files when x != "")
-                            n = modified_files.length
-                            winston.debug("#{n} modified files for #{project_id}")
-                            winston.debug("modified files = #{misc.trunc(misc.to_json(modified_files),512)}")
-                            if n == 0
-                                nothing_to_do = true
-                                _last_snapshot_cache[project_id] = misc.walltime() # don't try again to snapshot this project for a few minutes (otherwise we would index the whole thing every few seconds!)
-                                remove_lock
-                                    location : location
-                                    cb       : (err) -> cb(true)
+                            s = data.toString()
+                            try
+                                rollback_info = misc.from_json(s)
+                                winston.debug("monitor_snapshot_queue: rollback info: '#{s}'")
+                            catch
+                                # Something went wrong parsing the rollback JSON file; maybe it is corrupt
+                                # or empty.  This would be a good place to take more dramatic measures,
+                                # once they are implemented.
+                                winston.debug("monitor_snapshot_queue: rollback file *corrupt* (not using); content='#{s}'")
+                                rollback_info = undefined
+
+                            if rollback_info?
+                                winston.debug("monitor_snapshot_queue: doing rollback of last save")
+                                rollback_last_save
+                                    rollback_info : rollback_info
+                                    cb            : (err) ->
+                                        if err
+                                            cb(err)
+                                        else
+                                            fs.unlink(rollback_file, cb)
                             else
                                 cb()
-
-            # write rollback file
-            (cb) ->
-                fs.writeFile rollback_file, misc.to_json(rollback_info), cb
-
-            # save
-            (cb) ->
-                winston.debug("doing the actual save...")
-                t = misc.walltime()
-                d = Math.ceil(misc.walltime())
-                bup
-                    args : ['on', user, 'save', '-d', d, '--strip', '-q', '-n', 'master', '.']
-                    bup_dir : bup_active
-                    cb   : (err) ->
-                        winston.debug("time to save snapshot of #{project_id}: #{misc.walltime(t)} s")
-
-                        # If bup is killed during save, then this doesn't get removed, and on use
-                        # we rollback as specified.
-                        fs.unlink "#{bup_active}/rollback"
-
-                        if not err
-                            # used below when creating database entry
-                            timestamp = moment(new Date(d*1000)).format('YYYY-MM-DD-HHmmss')
-                        # No matter what happens, we need to remove the lock we just made *now*.
-                        remove_lock
-                            location : location
-                            cb       : (lock_err) ->
-                                if lock_err
-                                    winston.debug("non fatal error removing snapshot lock (#{project_id}) -- #{lock_err}")
-                                if err
-                                    rollback_last_save
-                                        rollback_info:rollback_info
-                                        cb : (rb_err) ->
-                                            cb(err)
-                                else
-                                    cb(err) # err is the outer err from saving
-
-            # Do a "bup ls", which causes the cache to be updated (so a user doesn't have to wait several seconds the
-            # first time they do a view on a snapshot).  Also, if this fails for some reason, we do *NOT* want to
-            # ever record this as a successful snapshot in the database.
-            (cb) ->
-                t = misc.walltime()
-                bup
-                    args    : ['ls', "master/#{timestamp}"]
-                    bup_dir : bup_active
-                    cb      : (err) ->
-                        winston.debug("time to get ls of new snapshot #{timestamp}: #{misc.walltime(t)} s")
+                else
+                    winston.debug("monitor_snapshot_queue: no rollback file")
+                    cb()
+
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: getting info in case we end up having to rollback this attempt")
+            get_rollback_info
+                bup_dir : bup_active
+                cb      : (err, info) ->
+                    rollback_info = info
+                    cb(err)
+
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: get deployed location of project (which can change at any time!)")
+            database.get_project_location
+                project_id : project_id
+                cb         : (err, _location) ->
+                    if err
                         cb(err)
-
-            # Compute disk usage after snapshot -- how much disk space was used by making this snapshot
-            # (The main reason for doing this is to protect against projects that have random data
-            # files in them -- we can refuse to snapshot after a certain total amount of usage.)
-            (cb) ->
-                misc_node.disk_usage bup_active, (err, usage) ->
-                    size_after = usage
-                    # Also, save total size of bup archive to global variable, so it can
-                    # be reported on next update the snap_servers table.
-                    size_of_bup_archive = size_after
+                    else
+                        if not _location or not _location.username?
+                            cb("monitor_snapshot_queue: can't snapshot #{project_id} since it is not deployed")
+                            return
+                        location = _location
+                        user = "#{location.username}@#{location.host}"
+                        # TODO: support location.port != 22 and location.path != '.'   !!?
+                        cb()
+
+        # get a lock on the deployed project
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: trying to lock #{project_id} for snapshotting.")
+            create_lock
+                location : location
+                cb       : (err) ->
+                    if err
+                        retry_later = true
+                        winston.debug("monitor_snapshot_queue: couldn't lock #{project_id}, so put back in the queue to try again later (in 30 seconds)")
+                        f = () ->
+                            for c in cbs
+                                snapshot_project(project_id:project_id, cb:c)
+                        setTimeout(f, 30000)
                     cb(err)

-            # record (in db) that we successfully made a snapshot, unless snapshot was too big.
-            (cb) ->
-                size = size_after - size_before
-                winston.debug("Snapshot of #{project_id}: increased archive by #{size} bytes")
-                if size > MAX_SNAPSHOT_SIZE
-                    err = "*** Snapshot size of #{project_id} exceeds max snapshot usage size of #{MAX_SNAPSHOT_SIZE}, so we are removing it, and turned off snapshots of this project in the database."
-                    winston.debug(err)
-
-                    database.update  # fire and forget this
-                        table : "projects"
-                        set   : {"snapshots_disabled":true}
-                        where : {project_id : project_id}
-
-                    rollback_last_save
-                        rollback_info : rollback_info
-                        cb : (rb_err) ->
-                            cb(err)
-                    return
-
-                t = misc.walltime()
-                _last_snapshot_cache[project_id] = t
-                database.update
-                    table : 'snap_commits'
-                    set   : {size: size, repo_id:repo_id, modified_files:modified_files}
-                    json  : ['modified_files']
-                    where :
-                        server_id  : server_id
-                        project_id : project_id
-                        timestamp  : timestamp
-                    cb    : (err) ->
-                        winston.debug("time to record commit to database: #{misc.walltime(t)}")
-                        cb()
-
-                # Also add row to table for each modified file.  There could be a 50,000 files though,
-                # so this is slow, so we don't block on it.  TODO: do this in one transaction someday.
-                f = (filename, cb) ->
-                    database.update
-                        table : 'snap_modified_files'
-                        set   : {dummy: true}
-                        where :
-                            project_id : project_id
-                            filename   : filename
-                            timestamp : timestamp
-                        cb : cb
-                t = misc.walltime()
-                async.map modified_files, f, (err) ->
-                    winston.debug("finished recording snap_modified_files for project #{project_id}, time = #{misc.walltime(t)}")
-
-        ], (err) ->
-            # wait 3 seconds, to ensure uniqueness of time stamp, not be too aggressive checking locks, etc.
-            setTimeout(monitor_snapshot_queue, 3000)
-            if nothing_to_do
-                err = undefined
-            if not retry_later
-                for cb in cbs
-                    cb?(err)
-        )
-    else
-        # check again soon
+        # create index
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: creating index for #{project_id}")
+            t = misc.walltime()
+            bup
+                args    : ['on', user, 'index', '--one-file-system', '.']        # --one-file-system option below so that sshfs-mounted filesystems (etc.) don't get sucked up.
+                bup_dir : bup_active
+                cb      : (err) ->
+                    winston.debug("monitor_snapshot_queue: time to index #{project_id}: #{misc.walltime(t)} s")
+                    cb(err)
+
+        # List the interesting modified files.
+        # This only makes sense because we just made the index above successfully.
+        # Note that "modified" is global to *all* snap servers, so this is actually
+        # a really sensible thing to do.  And if one snap server makes a snapshot,
+        # then no other snapshot server will make exactly the same snapshot (by a different name),
+        # which is what we want, since we will (eventually-when-i-implement-it!)
+        # get redundancy by rsyncing them around.
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: getting a list of the interesting modified files for #{project_id}")
+            # We use the --one-file-system option below so that sshfs-mounted filesystems don't get sucked up into our snapshots,
+            # as they could be huge and contain private information users don't want snapshotted.
+            misc_node.execute_code
+                command : "/usr/bin/bup on #{user} index --one-file-system -m . 2>&1 | grep -v ^./.forever |grep -v ^./.sagemathcloud|grep -v '^./$'"
+                timeout : 30  # should be very fast no matter what.
+                bash    : true
+                env     : {BUP_DIR : bup_active}
+                err_on_exit : false  # if a grep along the way above is empty -- i.e., no file changes, then get a 1 exit code.  STUPID, but yep.
+                cb      : (err, output) ->
+                    if err
+                        winston.debug("monitor_snapshot_queue: SERIOUS BUG issue -- error determining number of modified files for #{project_id}: #{err}")
+                        cb(err)
+                    else
+                        modified_files = (x.slice(2) for x in output.stdout.trim().split('\n'))
+                        modified_files = (x for x in modified_files when x != "")
+                        n = modified_files.length
+                        winston.debug("monitor_snapshot_queue: #{n} modified files for #{project_id}; modified files = #{misc.trunc(misc.to_json(modified_files),512)}")
+                        if n == 0
+                            nothing_to_do = true
+                            # Record the time of this snapshot, so that we won't try again to snapshot this project for a
+                            # few minutes; otherwise, we would index the whole thing every few seconds!
+                            _last_snapshot_cache[project_id] = misc.walltime()
+                            remove_lock
+                                location : location
+                                cb       : (err) -> cb(true)
+                        else
+                            cb()
+
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: write rollback file")
+            fs.writeFile(rollback_file, misc.to_json(rollback_info), cb)
+
+        # save
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: doing the actual bup save...")
+            t = misc.walltime()
+            d = Math.ceil(misc.walltime())
+            bup
+                args    : ['on', user, 'save', '-d', d, '--strip', '-q', '-n', 'master', '.']
+                bup_dir : bup_active
+                cb      : (err) ->
+                    winston.debug("monitor_snapshot_queue: time to save snapshot of #{project_id}: #{misc.walltime(t)} s")
+
+                    # If bup is killed during save, then this doesn't get removed, and on use
+                    # we rollback as specified.
+                    fs.unlink("#{bup_active}/rollback")
+
+                    if not err
+                        # used below when creating database entry
+                        timestamp = moment(new Date(d*1000)).format('YYYY-MM-DD-HHmmss')
+
+                    winston.debug("monitor_snapshot_queue: snapshot name = #{timestamp} of #{project_id}")
+                    # No matter what happens, we need to remove the lock we just made *now*.
+                    remove_lock
+                        location : location
+                        cb       : (lock_err) ->
+                            if lock_err
+                                winston.debug("monitor_snapshot_queue: non fatal error removing snapshot lock (#{project_id}) -- #{lock_err}")
+                            if err
+                                rollback_last_save
+                                    rollback_info:rollback_info
+                                    cb : (rb_err) ->
+                                        cb(err)  # err is the outer err from saving
+                            else
+                                cb(err) # err is the outer err from saving
+
+        # Do a "bup ls", which causes the cache to be updated (so a user doesn't have to wait several seconds the
+        # first time they do a view on a snapshot).  Also, if this fails for some reason, we do *NOT* want to
+        # ever record this as a successful snapshot in the database.
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: doing 'bup ls' to verify integrity of snapshot")
+            t = misc.walltime()
+            bup
+                args    : ['ls', "master/#{timestamp}"]
+                bup_dir : bup_active
+                cb      : (err) ->
+                    winston.debug("monitor_snapshot_queue: time to get ls of new snapshot #{timestamp}: #{misc.walltime(t)} s")
+                    cb(err)
+
+        # Compute disk usage after snapshot -- how much disk space was used by making this snapshot?
+        # (The main reason for doing this is to protect against projects that have random data
+        # files in them -- we can refuse to snapshot after a certain total amount of usage.)
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: compute disk usage after snapshot")
+            misc_node.disk_usage bup_active, (err, usage) ->
+                size_after = usage
+                winston.debug("monitor_snapshot_queue: disk usage = #{usage}")
+                # Also, save total size of bup archive to global variable, so it can
+                # be reported on next update the snap_servers table.
+                size_of_bup_archive = size_after
+                cb(err)
+
+        (cb) ->
+            winston.debug("monitor_snapshot_queue: record (in db) that we successfully made a snapshot, unless snapshot was too big.")
+            size = size_after - size_before
+            winston.debug("monitor_snapshot_queue: snapshot of #{project_id} increased archive by #{size} bytes")
+            if size > MAX_SNAPSHOT_SIZE
+                err = "monitor_snapshot_queue: *** snapshot size of #{project_id} exceeds max snapshot usage size of #{MAX_SNAPSHOT_SIZE}, so we are removing it, and turning off snapshots of this project in the database."
+                winston.debug(err)
+
+                database.update  # fire and forget this
+                    table : "projects"
+                    set   : {"snapshots_disabled":true}
+                    where : {project_id : project_id}
+
+                rollback_last_save
+                    rollback_info : rollback_info
+                    cb : (rb_err) ->
+                        cb(err)
+                return
+
+            t = misc.walltime()
+            _last_snapshot_cache[project_id] = t
+            database.update
+                table : 'snap_commits'
+                set   : {size: size, repo_id:repo_id, modified_files:modified_files}
+                json  : ['modified_files']
+                where :
+                    server_id  : server_id
+                    project_id : project_id
+                    timestamp  : timestamp
+                cb    : (err) ->
+                    if err
+                        winston.debug("monitor_snapshot_queue: FAILED to record a commit to database: #{misc.walltime(t)}")
+                    else
+                        winston.debug("monitor_snapshot_queue: recorded commit to database: #{misc.walltime(t)}")
+                        # Also add row to table for each modified file.  There could be (say) 50,000 files though,
+                        # so this is slow, so we don't block on it.  TODO: do this in one transaction someday (?),
+                        # or use collections...
+                        f = (filename, cb) ->
+                            database.update
+                                table : 'snap_modified_files'
+                                set   : {dummy: true}
+                                where :
+                                    project_id : project_id
+                                    filename   : filename
+                                    timestamp  : timestamp
+                                cb : cb
+                        t = misc.walltime()
+                        async.map modified_files, f, (err) ->
+                            winston.debug("monitor_snapshot_queue: finished recording snap_modified_files for project #{project_id}, time = #{misc.walltime(t)}")
+
+                    cb(err)
+
+    ], (err) ->
+        # wait 3 seconds, to ensure uniqueness of time stamp, not be too aggressive checking locks, etc.
         setTimeout(monitor_snapshot_queue, 3000)
+        if nothing_to_do
+            err = undefined
+        if not retry_later
+            for cb in cbs
+                cb?(err)
+    )
+
+# The monitor_snapshot_queue function above is really long -- if anything goes wrong with the callback chain, then
+# the entire snapshot server stops making new snapshots.  So as a just in case measure, we check on it every
+# 30 minutes.
+ensure_snapshot_queue_working = () ->
+    if monitor_snapshot_queue_last_run?
+        if misc.walltime() - monitor_snapshot_queue_last_run > 60*30
+            winston.debug("BUG/ERROR ** monitor_snapshot_queue has not been called in over 30 minutes -- restarting, but you need to fix this. check logs!")
+            monitor_snapshot_queue()

 # snapshot all projects in the given input array, and call opts.cb on completion.
 snapshot_projects = (opts) ->
@@ -1341,17 +1378,22 @@
     active = "#{bup_dir}/active"
     fs.readFile active, (err, data) ->
         if not err
+            # there's already a bup repository file.
             repo_id = data.toString().trim()
             cb(false, {active_path:"#{bup_dir}/#{repo_id}", repo_id:repo_id})
         else
             misc_node.ensure_containing_directory_exists active, (err) ->
+                if err
+                    cb(err); return
+                # create new bup repository.
                 u = uuid.v4()
                 path = "#{bup_dir}/#{u}"
                 fs.writeFile active, u, (err) ->
                     bup
                         args    : ['init']
                         bup_dir : path
-                        cb      : (err) -> cb(err, {active_path:path, repo_id:u})
+                        cb      : (err) ->
+                            cb(err, {active_path:path, repo_id:u})

 # Write entry to the database periodicially (with ttl) that this
 # snap server is up and running, and provide the key.
@@ -1466,6 +1508,7 @@
                 cb()
         (cb) ->
             monitor_snapshot_queue()
+            setInterval(ensure_snapshot_queue_working, 60000)
             cb()
         (cb) ->
             setInterval(register_with_database, 1000*registration_interval_seconds)


return instance from settouch

return instance from settouch

--- maskew.coffee
+++ maskew.coffee
@@ -126,6 +126,7 @@
     for eventPair in eventPairs
       for eString in eventPair then do (fn = '_on' + eventPair[0]) =>
         @_outerMask[listenFn] eString.toLowerCase(), @[fn], false
+    @

   destroy: =>


Return a richer object to the callback from `loadAvatars`

--- scripts/ayp.coffee
+++ scripts/ayp.coffee
@@ -334,7 +334,7 @@

       if names.length == 1
         # The only person speaking is centered in the frame
-        char = avatars[names[0]]
+        char = avatars[names[0]].img
         left = (frame.width / 2) - (char.width / 2)
         top = (frame.height - char.height)
         @compositeImage frame, char, left, top
@@ -343,7 +343,7 @@
         first = true
         for line in lines
           [who, what] = line
-          char = avatars[who]
+          char = avatars[who].img
           top = (frame.height - char.height)

           if first
@@ -415,7 +415,7 @@
   ## Helpers and utilities

   # Load avatars for `names` and invoke `cb` as:
-  # `cb(err, {"Nickname": avatarImg, ...})`
+  # `cb(err, {"Nickname": {"img": avatarImg, "default": false}, ...})`
   # mapping each name to an avatar image that
   # can be used to represent it.
   #
@@ -446,7 +446,9 @@
           if names.every((o) -> o.img)
             avatars = {}
             for obj in names
-              avatars[obj.name] = obj.img
+              avatars[obj.name] =
+                img: obj.img
+                default: !!obj.img.match(/default\.png$/)
             cb(null, avatars)

   # Returns the bounding box of `msg`


Basic new view test

Basic new view test

--- test/integration/new_view.coffee
+++ test/integration/new_view.coffee
@@ -1,85 +1,51 @@
-$ = jQuery = require 'jquery'
-Browser = require 'zombie'
+wd = require 'wd'
 should = require 'should'

-url = 'http://localhost:3001' # DRY DRY DRY
+browser = wd.remote()
+wd40 = require('../wd40')(browser)
+
+url = 'http://localhost:3001'
 login_url = "#{url}/login"

-describe 'Use a plugin to create a new view', ->
-  browser = new Browser()
-  browser.waitDuration = "10s"
+describe 'New view tool', ->
+  before (done) ->
+    wd40.init ->
+      browser.get login_url, done

   before (done) ->
-    browser.visit login_url, done
-    @datasetUrl = null
-    @viewPathname = null
+    wd40.fill '#username', 'ehg', ->
+      wd40.fill '#password', 'testing', ->
+        wd40.click '#login', done

-  before (done) ->
-    browser.fill '#username', 'ehg'
-    browser.fill '#password', 'testing'
-    browser.pressButton '#login', ->
-      browser.wait done
+  context 'when I click on an Apricot dataset', ->
+    before (done) ->
+      # wait for tiles to fade in
+      setTimeout ->
+        browser.elementByPartialLinkText 'Apricot', (err, link) ->
+          link.click done
+      , 500

-  context 'when I am on the datasets page', ->
+    it 'takes me to the Apricot dataset page', (done) ->
+      wd40.trueURL (err, result) ->
+        result.should.match /\/dataset\/(\w+)/
+        done()

-    context 'when I click on a dataset', ->
+    context 'when I click the "new view" button', ->
       before (done) ->
-        link = browser.query('a.dataset')
-        browser.fire 'click', link, ->
-          browser.wait 1000, ->
-            browser.wait done
+        wd40.click '.new-view', ->
+          browser.waitForElementByCss '#chooser .tool', 4000, done

-      it "takes me to the dataset's page", ->
-        result = browser.location.href
-        result.should.include "#{url}/dataset/"
-        @datasetUrl = result
+      context 'when I click on the newview tool', ->
+        before (done) ->
+          wd40.click '.newview.tool', =>
+            browser.waitForElementByTagName 'iframe', 10000, =>
+              browser.url (err, url) =>
+                @currentUrl = url
+                done()

-      xcontext 'when I click on the Code Your Own View tool', ->
-        before (done) ->
-          link = browser.query('a.code-your-own-view')
-          browser.fire 'click', link, ->
-            # Zarino isn't quite sure why we have
-            # two levels of .wait() in here, but he's
-            # going to leave it in just in case.
-            browser.wait 1000, ->
-              browser.wait done
+        it 'takes me to the view page', ->
+          @currentUrl.should.match new RegExp("#{url}/dataset/[^/]+/view/[^/]+")

-        it 'takes me to the new view', ->
-          result = browser.location.href
-          result.should.match RegExp('dataset/[^/]+/view/[^/]+')
-          @viewPathname = browser.location.pathname
-
-      xcontext "when I return to the dataset's page", ->
-        before (done) ->
-          browser.visit @datasetUrl, done
-
-        it 'lists the new view', ->
-          link = browser.query("a.view[href='#{@viewPathname}']")
-          should.exist link
-
-      xcontext "when I return to the dataset list", ->
-        before (done) ->
-          browser.visit "#{url}/", done
-
-        it 'lists the new view', ->
-          link = browser.query("a.view[href='#{@viewPathname}']")
-          should.exist link
-
-        context 'when I click the "hide" button on the view', ->
-          before (done) ->
-            link = browser.query("a.view[href='#{@viewPathname}'] .delete")
-            browser.fire 'click', link, ->
-              browser.wait done
-
-          it 'the view disappears from the homepage immediately', ->
-            body = browser.query('body')
-            naughty = $(body).find("a.view[href='#{@viewPathname}']:visible")
-            naughty.length.should.equal 0
-
-        context 'when I revisit the homepage', ->
-          before (done) ->
-            browser.reload done
-
-          it 'the view stays hidden', ->
-            naughty = browser.query("a.view[href='#{@viewPathname}']")
-            should.not.exist naughty
+    after (done) ->
+      browser.quit ->
+        done()


Got rid of mock models

Got rid of mock models

--- test/unit/rest_controller.coffee
+++ test/unit/rest_controller.coffee
@@ -1,17 +1,23 @@
 _ = require 'underscore'
+Backbone = require 'backbone'
 Queue = require 'queue-async'

 JSONUtils = require 'backbone-node/json_utils'
-MockServerModel = require 'backbone-node/mocks/server_model'
+class MemoryModel extends Backbone.Model
+  sync: require('backbone-node/memory_backbone_sync')(MemoryModel)
 Fabricator = require 'backbone-node/fabricator'

 test_parameters =
-  model_type: MockServerModel
+  model_type: MemoryModel
   route: 'mock_models'
   beforeEach: (callback) ->
     queue = new Queue(1)
-    queue.defer (callback) -> MockServerModel.destroy {}, callback
-    queue.defer (callback) -> Fabricator.create(MockServerModel, 10, {name: Fabricator.uniqueId('album_'), created_at: Fabricator.date, updated_at: Fabricator.date}, callback)
+    queue.defer (callback) -> MemoryModel.destroy {}, callback
+    queue.defer (callback) -> Fabricator.create(MemoryModel, 10, {
+      name: Fabricator.uniqueId('album_')
+      created_at: Fabricator.date
+      updated_at: Fabricator.date
+    }, callback)
     queue.await (err) -> callback(null, _.map(_.toArray(arguments).pop(), (test) -> JSONUtils.valueToJSON(test.toJSON())))

 require('../../lib/test_generators/backbone_rest')(test_parameters)


Collect env variables

Collect env variables

--- scripts/esa.coffee
+++ scripts/esa.coffee
@@ -11,7 +11,7 @@
 #   HUBOT_ESA_ACCESS_TOKEN
 #   HUBOT_ESA_TEAM_NAME
 #   HUBOT_ESA_WEBHOOK_DEFAULT_ROOM
-#   HUBOT_ESA_WEBHOOK_IGNORE_DIRECTORY
+#   HUBOT_ESA_WEBHOOK_ENDPOINT
 #
 # Author:
 #   hmsk <k.hamasaki@gmail.com>
@@ -36,8 +36,6 @@

 class EsaClientRobot
   constructor: (@robot, @team, @access_token) ->
-    unless @team then @robot.logger.warning "HUBOT_ESA_TEAM_NAME is not set."
-    unless @team then @robot.logger.warning "HUBOT_ESA_ACCESS_TOKEN is not set."

   baseUrl: ->
     "https://api.esa.io/v1/teams/#{@team}"
@@ -50,29 +48,39 @@
         @robot.logger.warning "esa API GET request failed: #{error}"

 module.exports = (robot) ->
-  esa = new EsaClientRobot(robot, process.env.HUBOT_ESA_TEAM_NAME, process.env.HUBOT_ESA_ACCESS_TOKEN)
-  robot.router.post '/hubot/esa', (req, res) ->
+  options =
+    team: process.env.HUBOT_ESA_TEAM
+    token: process.env.HUBOT_ESA_ACCESS_TOKEN
+    room: process.env.HUBOT_ESA_WEBHOOK_DEFAULT_ROOM
+    endpoint: process.env.HUBOT_ESA_WEBHOOK_ENDPOINT || '/hubot/esa'
+
+  return robot.logger.error "Missing configuration: HUBOT_ESA_TEAM" unless options.team?
+  return robot.logger.error "Missing configuration: HUBOT_ESA_ACCESS_TOKEN" unless options.token?
+  return robot.logger.error "Missing configuration: HUBOT_ESA_WEBHOOK_DEFAULT_ROOM" unless options.room?
+
+  esa = new EsaClientRobot(robot, options.team, options.token)
+  robot.router.post options.endpoint, (req, res) ->
+    # https://docs.esa.io/posts/37#3-4-0
+    unless req.headers['user-agent'] == 'esa-Hookshot/v1'
+      robot.logger.warning "Requested unknown user agent: #{req.headers['user-agent']}"
+      res.writeHead(403)
+      res.end()
+      return
+    robot.messageRoom options.room, handleWebhook(req.body or {})
     res.writeHead(204)
     res.end()
-    unless process.env.HUBOT_ESA_WEBHOOK_DEFAULT_ROOM
-      robot.logger.warning "HUBOT_ESA_WEBHOOK_DEFAULT_ROOM is not set."
-      return
-    unless req.headers['user-agent'] == 'esa-Hookshot/v1'
-      robot.logger.warning "Requested unknown user agent: #{req.headers['user-agent']}"
-      return
-    robot.messageRoom process.env.HUBOT_ESA_WEBHOOK_DEFAULT_ROOM, handleWebhook(req.body or {})

   robot.respond /esa stats/, (res) ->
     esa.getRequest("/stats", (stats) ->
       res.send "Members: #{stats.members}\nPosts: #{stats.posts}\nComments: #{stats.comments}\nStars: #{stats.stars}\nDaily Active Users: #{stats.daily_active_users}\nWeekly Active Users: #{stats.weekly_active_users}\nMonthly Active Users: #{stats.monthly_active_users}"
     )
   robot.hear /https:\/\/(.+)\.esa\.io\/posts\/(\d+)\b/, (res) ->
-    unless res.match[1] == process.env.HUBOT_ESA_TEAM_NAME then return
+    unless res.match[1] == options.team then return
     esa.getRequest("/posts/#{res.match[2]}", (post) ->
       res.send "esa: #{post.full_name}"
     )
   robot.hear /https:\/\/(.+)\.esa\.io\/posts\/(\d+)\#comment-(\d+)/, (res) ->
-    unless res.match[1] == process.env.HUBOT_ESA_TEAM_NAME then return
+    unless res.match[1] == options.team then return
     esa.getRequest("/comments/#{res.match[3]}", (comment) ->
       res.send "#{comment.body_md}"
     )


slash command use simple ... for loading

--- lib/repliers/fancy_replier.coffee
+++ lib/repliers/fancy_replier.coffee
@@ -68,7 +68,10 @@

   startLoading: (cb) ->

-    sass = sassyMessages[Math.floor(Math.random() * sassyMessages.length)]
+    sass = if @replyContext.isSlashCommand()
+      "..."
+    else
+      sassyMessages[Math.floor(Math.random() * sassyMessages.length)]

     if process.env.DEV == "true"
       sass = "[DEVELOPMENT] #{sass}"


Rework assigning PTY values.

Rework assigning PTY values.

--- lib/rubinius-terminal-view.coffee
+++ lib/rubinius-terminal-view.coffee
@@ -50,25 +50,20 @@
         "HOME": process.env.HOME
       stdio: 'pipe'

-    @ptyProcess = spawn rbx, [pty, @opts.shell], options
-
-    @ptyProcess.on 'exit', (code, signal) => @destroy()
-    [@ptyRead, @ptyWrite] = [@ptyProcess.stdin, @ptyProcess.stdout]
-
-    @ptyProcess
+    spawn rbx, [pty, @opts.shell], options

   initialize: (@state) ->
     {cols, rows} = @getDimensions()
     {cwd, shell, shellArguments, runCommand, colors, cursorBlink, scrollback} = @opts
     args = shellArguments.split(/\s+/g).filter (arg)-> arg

-    @createPTY args
+    @ptyProcess = @createPTY args
+    [@ptyRead, @ptyWrite] = [@ptyProcess.stdin, @ptyProcess.stdout]

     @ptyWrite.on 'data', (data) =>
       @terminal.write data.toString()

-    # TODO: investigate this lifetime
-    # @ptyProcess.on 'atom-rubinius-terminal:exit', (data) => @destroy()
+    @ptyProcess.on 'exit', (code, signal) => @destroy()

     colorsArray = (colorCode for colorName, colorCode of colors)
     @terminal = terminal = new Terminal {


Updated for styles

Updated for styles

--- scripts/wsc.coffee
+++ scripts/wsc.coffee
@@ -1002,8 +1002,10 @@

   robot.respond /wsc list/im, (msg) ->
     questions = wscQuestions().map((val, index) ->
+      question = "Q#{val.number} - #{val.question}"
+      response = bold(question, robot.adapterName)
       """
-      **Q#{val.number} - #{val.question}**
+      #{response}
       """
     )

@@ -1016,8 +1018,9 @@
   robot.respond /wsc q ([1-9]$|[1-9]\d$|10[0-7]$)/im, (msg) ->
     questions = wscQuestions()
     question = findByNumber(questions, msg.match[1])
-
-    msg.send "**#{question.question}**"
+    question = bold(question.question, robot.adapterName)
+
+    msg.send "#{question}"

   robot.respond /wsc a ([1-9]$|[1-9]\d$|10[0-7]$)/im, (msg) ->
     questions = wscQuestions()
@@ -1026,42 +1029,18 @@
     msg.send "#{question.answer}"

   robot.respond /wsc qa ([1-9]$|[1-9]\d$|10[0-7]$)/im, (msg) ->
-    questions = wscQuestions()
-    question = findByNumber(questions, msg.match[1])
-
-    msg.send """
-    **#{question.question}**
-
-    #{question.answer}
-    """
+    msg.send getQuestionAndAnswer(msg.match[1], robot.adapterName)

   robot.respond /wsc proof ([1-9]$|[1-9]\d$|10[0-7]$)/im, (msg) ->
-    questions = wscQuestions()
-    q = findByNumber(questions, msg.match[1])
-    proofs = q.proofTexts
-    proof = ""
-
-    for own key, value of proofs
-      proof += """
-      **#{key}:**
-
-      """
-
-      texts = value.join "\n    "
-      texts = "    " + texts + "\n\n"
-
-      proof += texts
-
-    msg.send proof
+    msg.send getQuestionProofs(msg.match[1], robot.adapterName)

   robot.respond /wsc full ([1-9]$|[1-9]\d$|10[0-7]$)/im, (msg) ->
     number = msg.match[1]
-    qa = getQuestionAndAnswer(number)
-    proofs = getQuestionProofs(number)
+    qa = getQuestionAndAnswer(number, robot.adapterName)
+    proofs = getQuestionProofs(number, robot.adapterName)

     msg.send """
     #{qa}
-
     #{proofs}
     """

@@ -1073,31 +1052,32 @@
     proofs = getQuestionProofs(question.number)

     msg.send """
-    **#{question.number}.** #{qa}
-
+    #{bold(question.number, robot.adapterName)}. #{qa}
     #{proofs}
     """

-getQuestionAndAnswer = (number) ->
+getQuestionAndAnswer = (number, adapter) ->
   questions = wscQuestions()
   question = findByNumber(questions, number)
+  boldQuestion = bold(question.question, adapter)

   return """
-  **#{question.question}**
-
+  #{boldQuestion}
   #{question.answer}
   """

-getQuestionProofs = (number) ->
+getQuestionProofs = (number, adapter) ->
   questions = wscQuestions()
   q = findByNumber(questions, number)
   proofs = q.proofTexts
   proof = ""

   for own key, value of proofs
+    key = "#{key}:"
+    proofNumber = bold(key, adapter)
+
     proof += """
-    **#{key}:**
-
+    #{proofNumber}
     """

     texts = value.join "\n    "
@@ -1120,4 +1100,13 @@
 findByNumber = (source, number) ->
   return source.filter((obj) ->
     return +obj.number == +number;
-  )[0];+  )[0];
+
+bold = (text, adapter) ->
+  switch adapter
+    when "discord"
+      "**#{text}**"
+    when "slack"
+      "*#{text}*"
+    else
+      "#{text}"


bug fixes

bug fixes

--- client/group.coffee
+++ client/group.coffee
@@ -64,12 +64,6 @@
       group: routeGroup()
       sortBy: sortBy().key

-Template.postButtons.events
-  'click .sortSetDefault': (e) ->
-    e.stopPropagation()
-    console.log "Setting default sort for #{routeGroup()} to #{if sortBy().reverse then '-' else '+'}#{sortBy().key}"
-    Meteor.call 'groupDefaultSort', routeGroup(), sortBy()
-
 Template.registerHelper 'groups', ->
   Groups.find()

@@ -86,6 +80,13 @@
   @autorun ->
     setTitle()

+Template.group.helpers
+  topMessageCount: ->
+    pluralize(Messages.find
+      group: @group
+      root: null
+    .count(), 'root message')
+
 Template.postButtons.helpers
   disableClass: ->
     if canPost @group
@@ -104,6 +105,11 @@
   $('[data-toggle="tooltip"]').tooltip()

 Template.postButtons.events
+  'click .sortSetDefault': (e) ->
+    e.stopPropagation()
+    console.log "Setting default sort for #{routeGroup()} to #{if sortBy().reverse then '-' else '+'}#{sortBy().key}"
+    Meteor.call 'groupDefaultSort', routeGroup(), sortBy()
+
   'click .postButton': (e) ->
     e.preventDefault()
     e.stopPropagation()
@@ -121,13 +127,13 @@
         else
           console.error "messageNew did not return problem -- not authorized?"

-Template.importButton.events
+Template.importButtons.events
   'click .importButton': (e, t) ->
     e.preventDefault()
     e.stopPropagation()
-    t.find('.importInput').click()
+    t.find(".importInput[data-format='#{e.target.getAttribute('data-format')}']").click()
   'change .importInput': (e, t) ->
-    importFiles t.data.group, e.target.files
+    importFiles e.target.getAttribute('data-format'), t.data.group, e.target.files
     e.target.value = ''
   'dragenter .importButton': (e) ->
     e.preventDefault()
@@ -138,7 +144,7 @@
   'drop .importButton': (e, t) ->
     e.preventDefault()
     e.stopPropagation()
-    importFiles t.data.group, e.originalEvent.dataTransfer.files
+    importFiles e.target.getAttribute('data-format'), t.data.group, e.originalEvent.dataTransfer.files

   'click .superdeleteImportButton': (e, t) ->
     e.preventDefault()
@@ -193,11 +199,6 @@
           0
       msgs.reverse() if sort.reverse
     msgs
-  topMessageCount: ->
-    pluralize(Messages.find
-      group: @group
-      root: null
-    .count(), 'root message')

 Template.messageShort.onRendered ->
   mathjax()


rebase fail

rebase fail

--- resources/assets/lib/beatmaps/search-sort.coffee
+++ resources/assets/lib/beatmaps/search-sort.coffee
@@ -1,76 +0,0 @@
-# Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
-# See the LICENCE file in the repository root for full licence text.
-
-import { Observer } from 'mobx-react'
-import core from 'osu-core-singleton'
-import * as React from 'react'
-import { div, a, i, span } from 'react-dom-factories'
-el = React.createElement
-
-export class SearchSort extends React.Component
-  render: =>
-    el Observer, null, () =>
-      div className: 'sort sort--beatmapsets',
-        div className: 'sort__items',
-          span className: 'sort__item sort__item--title', osu.trans('sort._')
-          for field in @fields()
-            selected = @selected(field)
-
-            a
-              key: field
-              href: '#'
-              className: "sort__item sort__item--button #{'sort__item--active' if selected}"
-              onClick: @select
-              'data-field': field
-              osu.trans "beatmaps.listing.search.sorting.#{field}"
-              span
-                className: 'sort__item-arrow'
-                i className: "fas fa-caret-#{if @props.filters.searchSort.order == 'asc' then 'up' else 'down'}"
-
-
-  fields: =>
-    fields =
-      title: true
-      artist: true
-      difficulty: true
-      updated: false
-      ranked: false
-      rating: true
-      plays: true
-      favourites: true
-      relevance: false
-      nominations: false
-
-    if !_.isEmpty(@props.filters.query)
-      fields.relevance = true
-
-    if @props.filters.status in ['graveyard', 'pending', 'wip']
-      fields.updated = true
-    else if @props.filters.status in ['any', 'favourites', 'mine']
-      fields.updated = true
-      fields.ranked = true
-    else
-      fields.ranked = true
-
-    if @props.filters.status == 'pending'
-      fields.nominations = true
-
-
-    field for own field, enabled of fields when enabled
-
-
-  select: (e) =>
-    e.preventDefault()
-    field = e.currentTarget.dataset.field
-    order = @props.filters.searchSort.order
-
-    if @selected(field)
-      order = if order == 'asc' then 'desc' else 'asc'
-    else
-      order = 'desc'
-
-    core.beatmapsetSearchController.filters.update 'sort', "#{field}_#{order}"
-
-
-  selected: (field) =>
-    @props.filters.searchSort.field == field


Beginning to develop DB access API layer


do attempt a second call

do attempt a second call


Added: atom-beautify userid

Added: atom-beautify userid

--- dotfiles/atom/config.cson
+++ dotfiles/atom/config.cson
@@ -158,3 +158,5 @@
     disableWhenNoEslintrcFileInPath: true
   tabs:
     showIcons: false
+  "atom-beautify":
+    _analyticsUserId: "c8ae3b6d-a781-4233-9780-a96292c2afa8"


upped the caboose-model max listeners

upped the caboose-model max listeners

--- caboose-model/index.coffee
+++ caboose-model/index.coffee
@@ -74,6 +74,7 @@
   }

 module.exports = global['caboose-model'] = caboose_model = new CabooseModel()
+caboose_model.setMaxListeners(100)

 caboose_model.Builder = require './lib/builder'
 caboose_model.Compiler = require './lib/model_compiler'


Register: 2FA: Updated validate email module to check if user has 2fa enabled

--- servers/lib/server/handlers/validateemail.coffee
+++ servers/lib/server/handlers/validateemail.coffee
@@ -4,7 +4,7 @@
 module.exports = (req, res) ->

   { JUser }           = koding.models
-  { password, email } = req.body
+  { password, email, tfcode } = req.body

   return res.status(400).send 'Bad request'  unless email?

@@ -14,11 +14,17 @@

   if clientId

-    JUser.login clientId, { username : email, password }, (err, info) ->
+    JUser.login clientId, { username : email, password, tfcode }, (err, info) ->

       {isValid : isEmail} = JUser.validateAt 'email', email, yes

-      if err and isEmail
+      if err?.name is 'VERIFICATION_CODE_NEEDED'
+        return res.status(400).send 'TwoFactor auth Enabled'
+
+      else if err?.message is 'Access denied!'
+        return res.status(400).send 'Bad request'
+
+      else if err and isEmail
         JUser.emailAvailable email, (err_, response) ->
           return res.status(400).send 'Bad request'  if err_


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/ageable.coffee
+++ src/ageable.coffee
@@ -7,6 +7,7 @@

     #=> player.I.age == 1

+Ageable module
 @name Ageable
 @module
 @constructor


add contributors Cliff Warren and Evo Kellerman

--- src/app/app.coffee
+++ src/app/app.coffee
@@ -34,12 +34,12 @@
   'joostfunkekupper'    # Joost Funke Kupper
   'rohanliston'         # Rohan Liston
   'lukehorvat'          # Luke Horvat
+  'hellola'             # Evo Kellerman
+  'AvDongle'            # Cliff Warren

   #
   # TODO: Find out account names for...
-  # '???'                 # Evo Kellerman
   # '???'                 # Reuben Wilson
-  # '???'                 # Cliff Warren
   # '???'                 # Angus Morton
   #
 ])


List available commands in usage

List available commands in usage

--- src/apm-cli.coffee
+++ src/apm-cli.coffee
@@ -7,7 +7,13 @@

 parseOptions = (args=[]) ->
   options = optimist(args)
-  options.usage('Usage: apm <command>')
+  options.usage """
+
+    Usage: apm <command>
+
+    where <command> is one of:
+        available, help, install, list, publish
+  """
   options.alias('v', 'version').describe('v', 'Print the apm version')
   options.alias('h', 'help').describe('h', 'Print this usage message')
   remainingArguments = options.argv._
@@ -37,6 +43,7 @@
       options.showHelp()
     else if command
       switch command
+        when 'help' then options.showHelp()
         when 'install' then new Installer().run(options)
         when 'list', 'ls' then new Lister().run(options)
         when 'publish' then new Publisher().run(options)


Add missing specs for up.util.findWithSelf

--- spec_app/spec/javascripts/up/util_spec.js.coffee
+++ spec_app/spec/javascripts/up/util_spec.js.coffee
@@ -697,6 +697,31 @@
       expect(object.b).toBe('b value')
       expect(object.c).toBe('a value')

+  describe 'up.util.findWithSelf', ->
+
+    it 'finds the selector in descendants of the given element', ->
+      $container = affix('div')
+      $child1 = $container.affix('div.match')
+      $child2 = $container.affix('div')
+      $child2Child1 = $child2.affix('div.match')
+      matches = u.findWithSelf($container, '.match')
+      expect(matches).toEqual [$child1.get(0), $child2Child1.get(0)]
+
+    it 'finds the element itself if the element matches the given selector', ->
+      $container = affix('div.match')
+      $child1 = $container.affix('div')
+      $child1Child1 = $child1.affix('div.match')
+      matches = u.findWithSelf($container, '.match')
+      expect(matches).toEqual [$container.get(0), $child1Child1.get(0)]
+
+    it 'returns multiple matches in the same subtree', ->
+      $container = affix('div.match')
+      $child1 = $container.affix('div')
+      $child2 = $container.affix('div.match')
+      $child2Child1 = $child2.affix('div.match')
+      matches = u.findWithSelf($container, '.match')
+      expect(matches).toEqual [$container.get(0), $child2.get(0), $child2Child1.get(0)]
+
   describe 'up.util.memoize', ->

     it 'returns a function that calls the memoized function', ->


Code style consistency, comments

Code style consistency, comments

--- src/chaplin/views/collection_view.coffee
+++ src/chaplin/views/collection_view.coffee
@@ -21,7 +21,7 @@

     # By default, fading in is done by javascript function which can be
     # slow on mobile devices. CSS animations are faster,
-    # but require user's manual definitions.
+    # but require user’s manual definitions.
     # CSS classes used are: animated-item-view, animated-item-view-end.
     useCssAnimation: false

@@ -144,7 +144,7 @@
     render: ->
       super

-      # Set the $list property
+      # Set the $list property with the actual list container
       @$list = if @listSelector then @$(@listSelector) else @$el

       @initFallback()
@@ -220,9 +220,9 @@

           # Apply filter to the item
           included = if typeof filterer is 'function'
-              filterer item, index
-            else
-              true
+            filterer item, index
+          else
+            true

           # Show/hide the view accordingly
           view = @viewsByCid[item.cid]
@@ -287,7 +287,7 @@
       view = @renderItem item
       @insertView item, view, index

-    # Instantiate and render an item using the viewsByCid hash as a cache
+    # Instantiate and render an item using the `viewsByCid` hash as a cache
     renderItem: (item) ->
       # Get the existing view
       view = @viewsByCid[item.cid]
@@ -313,9 +313,9 @@

       # Is the item included in the filter?
       included = if typeof @filterer is 'function'
-          @filterer item, position
-        else
-          true
+        @filterer item, position
+      else
+        true

       # Get the view’s top element
       viewEl = view.el


removed o.resize function since it is not been reused

--- src/coffee/responsive.breadcrumbs.coffee
+++ src/coffee/responsive.breadcrumbs.coffee
@@ -313,44 +313,42 @@
                             o.opts.holderAnimation.onAfterHide(_this) if $.isFunction(o.opts.holderAnimation.onAfterHide)

             # bind resize event to the window
-            o.browserWindow.on o.resizeKey, o.resize
-
-            # execute custom code after the plugin has loaded
-            o.opts.onAfterLoad(_this) if $.isFunction(o.opts.onAfterLoad)
-
-            # trigger the resize event after the plugin has loaded
-            o.browserWindow.trigger o.resizeKey
-
-        o.resize = (e) ->
-            current = $ this
-
-            crumbHeights = o.el
-                .children('li')
-                .map( ->
-                    return ($ this).outerHeight true
-                )
-                .get()
-
-            o.optimalCrumbHeight = _.max crumbHeights
-
-            if o.optimalCrumbHeight isnt o.el.height()
-                o.el
-                    .addClass(o.opts.wrappedClass)
-                    .trigger "compress.#{ pluginName }"
-
-            if o.windowWidth isnt o.browserWindow.width()
-                if o.browserWindow.width() < o.windowWidth and o.optimalCrumbHeight isnt o.el.height()
+            o.browserWindow.on o.resizeKey, ->
+                current = $ this
+
+                crumbHeights = o.el
+                    .children('li')
+                    .map( ->
+                        return ($ this).outerHeight true
+                    )
+                    .get()
+
+                o.optimalCrumbHeight = _.max crumbHeights
+
+                if o.optimalCrumbHeight isnt o.el.height()
                     o.el
                         .addClass(o.opts.wrappedClass)
                         .trigger "compress.#{ pluginName }"
-                else
-                    o.el
-                        .removeClass(o.opts.wrappedClass)
-                        .trigger "decompress.#{ pluginName }"
-
-                o.windowWidth = o.browserWindow.width()
-
-            return null
+
+                if o.windowWidth isnt o.browserWindow.width()
+                    if o.browserWindow.width() < o.windowWidth and o.optimalCrumbHeight isnt o.el.height()
+                        o.el
+                            .addClass(o.opts.wrappedClass)
+                            .trigger "compress.#{ pluginName }"
+                    else
+                        o.el
+                            .removeClass(o.opts.wrappedClass)
+                            .trigger "decompress.#{ pluginName }"
+
+                    o.windowWidth = o.browserWindow.width()
+
+                return null
+
+            # execute custom code after the plugin has loaded
+            o.opts.onAfterLoad(_this) if $.isFunction(o.opts.onAfterLoad)
+
+            # trigger the resize event after the plugin has loaded
+            o.browserWindow.trigger o.resizeKey

         o.getChildrenWidth = ->
             totalWidth = 0


Delete mistakenly added package from the snapshot require list

--- src/initialize-application-window.coffee
+++ src/initialize-application-window.coffee
@@ -30,7 +30,6 @@
   require('find-and-replace')
   require('fuzzy-finder')
   require('git-diff')
-  require('github')
   require('go-to-line')
   require('grammar-selector')
   require('image-view')


Poll Insteon devices’ status

Poll Insteon devices’ status

--- lib/adapters/insteon-cloud/InsteonCloudAdapter.coffee
+++ lib/adapters/insteon-cloud/InsteonCloudAdapter.coffee
@@ -10,6 +10,8 @@
   name: "Insteon Cloud"
   defaults:
     initialStatusCheck:   true # Check status on all devices upon connection?
+    statusCheckInterval:  900  # Time between automatic status refreshes
+                               # in seconds. Set null to disable.
     batchCommandInterval: 2000 # How long to wait between repeated commands (ms)
     proxyDevices:         {}   # Set "deviceId1": "deviceId2" to redirect all
                                # status messages from the key device to the
@@ -58,8 +60,14 @@
       @hasDiscovered = true
       @setValid true
       if @get('initialStatusCheck') then @requestAllDevicesStatus()
+      scInterval = @get('statusCheckInterval')
+      if scInterval?
+        @log 'debug', "Will check all device status at #{scInterval}s interval"
+        @_statusCheck = setInterval(
+          _.bind(@requestAllDevicesStatus, this), scInterval * 1000)

   requestAllDevicesStatus: ->
+    # TODO don't request status on devices that won't reply (e.g. leak sensors)
     ids = _.without(_.keys(@_nodesById), _.keys(@get('proxyDevices'))...)
     interval = @get('batchCommandInterval')
     @log 'debug', "Requesting status of #{ids.length} devices " +
@@ -69,6 +77,7 @@

   requestDeviceStatus: (deviceId) ->
     # Right now they're all lights...
+    # TODO notice when they don't respond
     @requestLightStatus deviceId

   requestLightStatus: (deviceId) ->
@@ -98,7 +107,7 @@
     @_api.command _.extend(baseCommand, params) # Returns a promise

   _handleCommandReceived: (cmd) ->
-    @log 'verbose', "Received Insteon command for device #{cmd.device_insteon_id}"
+    @log 'verbose', "Received Insteon command: #{JSON.stringify(cmd)}"
     node = @_nodesByHexId[cmd.device_insteon_id]
     proxyFor = @get('proxyDevices')[node.id]
     if proxyFor?


Pending step definitions for 'language'

Pending step definitions for 'language'

--- features/step_definitions/steps.coffee
+++ features/step_definitions/steps.coffee
@@ -74,34 +74,34 @@

     callback()

-  this.Then /^it should be a success$/, (callback) ->
+  @Then /^it should be a success$/, (callback) ->
     if result instanceof Error
       throw result
       callback.fail(Error)
     else
       callback()

-  this.Then /^the result should be a Tuple representation$/, (callback) ->
+  @Then /^the result should be a Tuple representation$/, (callback) ->
     unless result instanceof Object
       callback.fail new Error("#{result} is not an object")
     callback()

-  this.Then /^its '(.*)' attribute should be a Date representation$/, (attr, callback) ->
+  @Then /^its '(.*)' attribute should be a Date representation$/, (attr, callback) ->
     unless result[attr] instanceof Date
       callback.fail new Error("attribute is not a Date, got #{result[attr]}")
     callback()

-  this.Then /^the result should be a representation for Nil$/, (callback) ->
+  @Then /^the result should be a representation for Nil$/, (callback) ->
     unless result == null
       callback.fail new Error("#{result} is not a representation for Nil")
     callback()

-  this.Then /^the result should be a representation for (.*?)$/, (type,callback) ->
+  @Then /^the result should be a representation for (.*?)$/, (type,callback) ->
     unless system.fetch(type).include(result)
       callback.fail new Error("#{JSON.stringify(result)} is not a representation for #{type}")
     callback()

-  this.Then /^it should be a TypeError as:$/, (table, callback) ->
+  @Then /^it should be a TypeError as:$/, (table, callback) ->
     unless result instanceof Finitio.TypeError
       callback.fail result

@@ -147,3 +147,29 @@
     unless (result instanceof Date) and (result.toISOString() == expected.toISOString())
       callback.fail new Error("#{result} <> 13st of March 2014 at 08:30")
     callback()
+
+  #### Parser
+
+  @Given /^the grammar rule is expr$/, (callback) ->
+    callback.pending()
+
+  @Given /^the source is$/, (string, callback) ->
+    callback.pending()
+
+  @Then /^evaluating it should yield (\d+)$/, (arg1, callback) ->
+    callback.pending()
+
+  @Then /^evaluating it with x=(.*) should yield (.*)$/, (x, expected, callback) ->
+    callback.pending()
+
+  @Given /^the grammar rule is literal$/, (callback) ->
+    callback.pending()
+
+  @Then /^it should compile to a (.*)$/, (type, callback) ->
+    callback.pending()
+
+  @Given /^the System source is$/, (string, callback) ->
+    callback.pending()
+
+  @Then /^it should compile fine$/, (callback) ->
+    callback.pending()


Remove alias from test —verbose.

Remove alias from test —verbose.

--- src/command.coffee
+++ src/command.coffee
@@ -114,7 +114,6 @@
       help: "Run tests for a brunch project"
       opts:
         verbose:
-          abbr: "v"
           flag: yes
           help: "set verbose option for test runner"
       callback: (options) ->


bootup: removed some unused code

bootup: removed some unused code

--- client/app/lib/index.coffee
+++ client/app/lib/index.coffee
@@ -62,36 +62,15 @@
   # and that file is not a direct dependency of this file, you are most
   # probably causing harm to sea lions and sea turtles. -og

-  status           = new Status
-  mainController   = new MainController
+  status         = new Status
+  mainController = new MainController

   require('./routehandler')()

-  currentNotif     = null
-  firstLoad        = yes
+  currentNotif = null
+  firstLoad    = yes

   mainController.tempStorage = {}
-
-  if /edge.koding.com/.test global.location.href
-    name = getFullnameFromAccount whoami()
-    modal = new KDModalView
-      title            : "Hi #{name},"
-      width            : 600
-      overlay          : yes
-      cssClass         : "new-kdmodal"
-      content          : """
-

-
Thanks for trying out the bleeding 'edge' of Koding features. Most of the new features here are experimental and hence prone to breaking.

-
-
If you run into problems, we'd love to hear what went wrong and how to reproduce the error. Please send an email to edge@koding.com. Note that even though this environment is experimental, your vms are the same production vms as on koding.com.

-
-
By continuing you acknowledge the experimental nature of this environment.

-

-"""
-      buttons          :
-        "I Agree"      :
-          style        : "modal-clean-green"
-          callback     : -> modal.destroy()

   ###
   # CONNECTIVITY EVENTS


start renaming op to fn

start renaming op to fn

--- src/context.coffee
+++ src/context.coffee
@@ -10,8 +10,8 @@
   ignored = (object) -> object == ignore

   handle_cli_cmd = (object) ->
-    if object?._lead_op?
-      object._lead_op.cli_fn.apply @
+    if object?._lead_context_fn?
+      object._lead_context_fn.cli_fn.apply @
       true

   handle_renderable = (object) ->
@@ -35,19 +35,19 @@
     true

-  bind_cli = (run_context, ops) ->
-    bind_op = (op) ->
+  bind_cli = (run_context, fns) ->
+    bind_fn = (op) ->
       bound = (args...) ->
         # if the function returned a value, unwrap it. otherwise, ignore it
         op.fn.apply(run_context.root_context.current_context, args)?._lead_cli_value ? ignore
-      bound._lead_op = op
+      bound._lead_context_fn = op
       bound

-    bound_ops = {}
-    for k, op of ops
-      bound_ops[k] = bind_op op
+    bound_fns = {}
+    for k, fn of fns
+      bound_fns[k] = bind_fn fn

-    bound_ops
+    bound_fns

   create_run_context = ($el, opts={}) ->
     {extra_contexts, ops, function_names, vars} = _.defaults {}, opts,


mixpanel: minor fixes

mixpanel: minor fixes

--- client/ide/fs/fsfile.coffee
+++ client/ide/fs/fsfile.coffee
@@ -176,4 +176,3 @@
       if err then warn err
       @emit "fs.save.finished", err, res
       callback? err,res
-      KD.mixpanel "Save file, success"  unless err


remove a debugger statement

remove a debugger statement

--- client/ide/fs/fsitem.coffee
+++ client/ide/fs/fsitem.coffee
@@ -124,11 +124,8 @@
     [isTarGz, extractFolder] =
       if tarPattern.test file.name
         [yes, path.replace tarPattern, '']
-
       else if zipPattern.test file.name
         [no, path.replace zipPattern, '']
-
-    debugger

     osKite.vmStart()


IDE: Don't assign destroyed tabview to activeTabView variable

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -240,6 +240,8 @@
   setActiveTabView: (tabView) ->

     return  if tabView is @activeTabView
+    return  if tabView.isDestroyed
+
     @setActivePaneFocus off
     @activeTabView = tabView
     @setActivePaneFocus on


refactor: Clearly separate the code specific to API Blueprint

--- src/compile.coffee
+++ src/compile.coffee
@@ -69,37 +69,45 @@
     refractTransition = refractTransitions[transitionNo]
     refractHttpTransactions = children(refractTransition, {element: 'httpTransaction'})

-    # API Blueprint has a concept of transaction examples and
-    # the API Blueprint AST used to expose it. The concept isn't present
-    # in API Elements anymore, so we have to detect and backport them.
     if mediaType is 'text/vnd.apiblueprint'
+      # API Blueprint has a concept of transaction examples and
+      # the API Blueprint AST used to expose it. The concept isn't present
+      # in API Elements anymore, so we have to detect and backport them, because
+      # the example numbers are used in the transaction names for hooks.
+      #
+      # This is very specific to API Blueprint and to backwards compatibility
+      # of Dredd. There's a plan to migrate to so-called "transaction paths"
+      # in the future (apiaryio/dredd#227), which won't use the concept
+      # of transaction examples anymore.
       exampleNumbersPerTransaction = detectExampleNumbersPerTransaction(transition)
-    else
-      exampleNumbersPerTransaction = transition.transactions.map( -> 1)
-    hasMoreExamples = Math.max(exampleNumbersPerTransaction...) > 1
-
-    # Dredd supports only testing of the first request-response pair within
-    # each transaction example. So if we're dealing with API Blueprint, we
-    # iterate over available transactions and skip those, which are not first
-    # within a particular example.
-    exampleNo = 0
-    transition.transactions.forEach((httpTransaction, httpTransactionNo) ->
-      httpTransactionNo = httpTransactionNo.toValue()
-      httpTransactionExampleNo = exampleNumbersPerTransaction[httpTransactionNo]
-
-      transactionInfo =
-        refract: refractHttpTransactions[httpTransactionNo]
-        apiElements: httpTransaction
-        exampleNo: if hasMoreExamples then httpTransactionExampleNo else null
-
-      if mediaType is 'text/vnd.apiblueprint'
+      hasMoreExamples = Math.max(exampleNumbersPerTransaction...) > 1
+
+      # Dredd supports only testing of the first request-response pair within
+      # each transaction example. We iterate over available transactions and
+      # skip those, which are not first within a particular example.
+      exampleNo = 0
+      transition.transactions.forEach((httpTransaction, httpTransactionNo) ->
+        httpTransactionNo = httpTransactionNo.toValue()
+        httpTransactionExampleNo = exampleNumbersPerTransaction[httpTransactionNo]
+
+        transactionInfo =
+          refract: refractHttpTransactions[httpTransactionNo]
+          apiElements: httpTransaction
+          exampleNo: if hasMoreExamples then httpTransactionExampleNo else null
+
         if httpTransactionExampleNo isnt exampleNo
           relevantTransactions.push(transactionInfo)
-      else
-        relevantTransactions.push(transactionInfo)
-
-      exampleNo = httpTransactionExampleNo
-    )
+
+        exampleNo = httpTransactionExampleNo
+      )
+    else
+      # All other formats then API Blueprint
+      transition.transactions.forEach((httpTransaction, httpTransactionNo) ->
+        relevantTransactions.push(
+          refract: refractHttpTransactions[httpTransactionNo.toValue()]
+          apiElements: httpTransaction
+        )
+      )
   )

   return relevantTransactions


split context and run context

split context and run context

--- src/context.coffee
+++ src/context.coffee
@@ -55,9 +55,15 @@

     bound_fns

+  create_context = (extra_contexts) ->
+    context = _.extend {}, extra_contexts...
+    context_fns = collect_context_fns context
+    context.fns = fns = bind_context_fns context, context_fns
+    context.vars = collect_context_vars context
+    context
+
   create_run_context = ($el, opts={}) ->
-    {extra_contexts} = _.defaults {}, opts,
-      extra_contexts: []
+    {extra_contexts} = _.extend {extra_contexts: []}, opts

     scroll_to_top = ->
       setTimeout ->
@@ -81,7 +87,7 @@
         $target.append $item
         $item

-    run_context =
+    run_context = _.extend create_context(extra_contexts),
       current_options: {}
       output: output $el
       scroll_to_top: scroll_to_top
@@ -155,10 +161,6 @@

     run_context.current_context = run_context
     run_context.root_context = run_context
-    _.defaults run_context, extra_contexts...
-    context_fns = collect_context_fns run_context
-    run_context.fns = fns = bind_context_fns run_context, context_fns
-    run_context.vars = collect_context_vars run_context

     run_context


Bug fixes

Bug fixes

--- src/convert.coffee
+++ src/convert.coffee
@@ -57,7 +57,7 @@
       next["#{u},#{v}"] = null
     next["#{face[face.length-1]},#{face[0]}"] = null
     ## Outside face is clockwise; exclude it.
-    if w? and polygonOrientation(fold.vertices_coords[x] for x in face) > 0
+    if w? and geom.polygonOrientation(fold.vertices_coords[x] for x in face) > 0
       #console.log face
       fold.faces_vertices.push face
     #else


convert.flat_folded_geometry, convert.edges_vertices_to_edges_faces_edges

--- src/convert.coffee
+++ src/convert.coffee
@@ -219,6 +219,81 @@
     )
   mesh

+convert.edges_vertices_to_edges_faces_edges = (fold) ->
+  ###
+  Given a `fold` object with `edges_vertices` and `faces_vertices`,
+  fills in `faces_edges` and `edges_vertices`.
+  ###
+  fold.edges_faces = ([null, null] for edge in [0...fold.edges_vertices.length])
+  edgeMap = {}
+  for edge, vertices of fold.edges_vertices when vertices?
+    edge = parseInt edge
+    edgeMap["#{vertices[0]},#{vertices[1]}"] = [edge, 0] # forward
+    edgeMap["#{vertices[1]},#{vertices[0]}"] = [edge, 1] # backward
+  for face, vertices of fold.faces_vertices
+    face = parseInt face
+    fold.faces_edges[face] =
+      for v1, i in vertices
+        v2 = vertices[(i+1) % vertices.length]
+        [edge, orient] = edgeMap["#{v1},#{v2}"]
+        fold.edges_faces[edge][orient] = face
+        edge
+  fold
+
+convert.flat_folded_geometry = (fold, rootFace = 0) ->
+  ###
+  Assuming `fold` is a locally flat foldable crease pattern in the xy plane,
+  sets `fold.vertices_flatFoldCoords` to give the flat-folded geometry
+  as determined by repeated reflection relative to `rootFace`, and sets
+  `fold.faces_flatFoldTransform` transformation matrix mapping each face's
+  unfolded --> folded geometry.
+  Requires `fold` to have `vertices_coords` and `edges_vertices`;
+  `edges_faces` and `faces_edges` will be created if they do not exist.
+  Returns the maximum displacement error from closure constraints (multiple
+  mappings of the same vertices, or multiple transformations of the same face).
+  ###
+  if fold.vertices_coords? and fold.edges_vertices? and not
+     (fold.edges_faces? and fold.faces_edges?)
+    convert.edges_vertices_to_edges_faces_edges fold
+  maxError = 0
+  level = [rootFace]
+  fold.faces_flatFoldTransform =
+    (null for face in [0...fold.faces_edges.length])
+  fold.faces_flatFoldTransform[rootFace] = [[1,0,0],[0,1,0]] # identity
+  fold.vertices_flatFoldCoords =
+    (null for vertex in [0...fold.vertices_coords.length])
+  # Use fold.faces_edges -> fold.edges_vertices, which are both needed below,
+  # in case fold.faces_vertices isn't defined.
+  for edge in fold.faces_edges[rootFace]
+    for vertex in fold.edges_vertices[edge]
+      fold.vertices_flatFoldCoords[vertex] ?= fold.vertices_coords[vertex][..]
+  while level.length
+    nextLevel = []
+    for face in level
+      for edge in fold.faces_edges[face]
+        for face2 in fold.edges_faces[edge] when face2? and face2 != face
+          transform = geom.matrixMatrix fold.faces_flatFoldTransform[face],
+            geom.matrixReflectLine ...(
+              for vertex in fold.edges_vertices[edge]
+                fold.vertices_coords[vertex]
+            )
+          if fold.faces_flatFoldTransform[face2]?
+            for row, i in fold.faces_flatFoldTransform[face2]
+              maxError = Math.max maxError, geom.dist row, transform[i]
+          else
+            fold.faces_flatFoldTransform[face2] = transform
+            for edge2 in fold.faces_edges[face2]
+              for vertex2 in fold.edges_vertices[edge2]
+                mapped = geom.matrixVector transform, fold.vertices_coords[vertex2]
+                if fold.vertices_flatFoldCoords[vertex2]?
+                  maxError = Math.max maxError,
+                    geom.dist fold.vertices_flatFoldCoords[vertex2], mapped
+                else
+                  fold.vertices_flatFoldCoords[vertex2] = mapped
+            nextLevel.push face2
+    level = nextLevel
+  maxError
+
 convert.deepCopy = (fold) ->
   ## Given a FOLD object, make a copy that shares no pointers with the original.
   if typeof fold in ['number', 'string', 'boolean']


Removed console.log

Removed console.log

--- src/counter.coffee
+++ src/counter.coffee
@@ -43,7 +43,6 @@
           console.log err
           fromFile = false
         else
-          console.log counter
           fromFile = true


Fixing url and callback parameters for get request.

--- src/cute-me.coffee
+++ src/cute-me.coffee
@@ -10,7 +10,6 @@
 # Commands:
 #   hubot cute me - Receive a cute thing
 #   unicorn chaser - Receieve a cute thing
-
 module.exports = (robot) ->
   robot.respond /cute me/, (msg) ->
     cuteMe(msg)
@@ -19,7 +18,7 @@
     cuteMe(msg)

   cuteMe = (msg) ->
-    msg.http('http://api.dailycute.com.net/v1/posts/all')
-      .get() (body) ->
+    msg.http('http://api.dailycute.net/v1/posts/all.json')
+      .get() (err, response, body) ->
         results = JSON.parse(body)
         msg.send (msg.random results.posts).image_src


Make the 'new' button create a fresh document.

This closes #2.

--- coffee/pastedown_app.coffee
+++ coffee/pastedown_app.coffee
@@ -14,6 +14,7 @@
     $("#view").on "click", =>
       return if $("#view").hasClass("selected")
       @loadRendered()
+    $("#new").on "click", => @onNew()
     $("#share").on "click", => @onShare()
     $("#contents").on "input", (e) => @onFileChange(e)
     $("#controls").on "change", "input, select", (e) => @onFileChange(e)
@@ -199,6 +200,15 @@
       message = "Error loading paste."
     $("#contents").html("
#{message}
")

+  onNew: ->
+    @editBoxContents = ""
+    @editBoxDirty = true
+    window.location.hash = ""
+    $("#formatChoice input[value=text]").attr("checked", "checked")
+    $("#formatChoice input[value!=text]").removeAttr("checked")
+    $("#language").val("")
+    @loadEdit()
+
   showShareMessage: ->
     message = """

Share this URL:
<input type="text" class="no-focus-outline" value="#{window.location.href}" autofocus />


util: stylefix: double linebreak between sections

--- coffee/src/util/util.coffee
+++ coffee/src/util/util.coffee
@@ -171,6 +171,7 @@
     https?://[-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_()|]
   ///

+
 # -- jQuery utils

 # Preserve image aspect ratio but contain it wholly


Export main type classes on Finitio.

Export main type classes on Finitio.

--- src/finitio.coffee
+++ src/finitio.coffee
@@ -34,5 +34,16 @@
 ##
 $u.extend Finitio, require './errors'
 $u.extend Finitio, require './support/contracts'
+##
+Finitio.AdType       = require './type/ad_type'
+Finitio.AnyType      = require './type/any_type'
+Finitio.BuiltinType  = require './type/builtin_type'
+Finitio.RelationType = require './type/relation_type'
+Finitio.SeqType      = require './type/seq_type'
+Finitio.SetType      = require './type/set_type'
+Finitio.StructType   = require './type/struct_type'
+Finitio.SubType      = require './type/sub_type'
+Finitio.TupleType    = require './type/tuple_type'
+Finitio.UnionType    = require './type/union_type'

 module.exports = Finitio


Changed encoding method to encodeURIComponent

--- src/fogbugz.coffee
+++ src/fogbugz.coffee
@@ -199,7 +199,7 @@

 EncodeOptions = (options) ->
 	for key, val of options
-		options[key] = encodeURI(val)
+		options[key] = encodeURIComponent(val)
 	return options

 # Calls the Fogbugz XML Api and returns the result as JSON


tests: Fix looking wrong page for fbp-spec

--- spec/basics.coffee
+++ spec/basics.coffee
@@ -2,7 +2,7 @@
 chai = window.chai or require 'chai'

 parseFBP = (fbpString, callback) ->
-  fbpGraph = window.fbpGraph or require 'fbp-graph'
+  fbpGraph = window.TheGraph.fbpGraph or require 'fbp-graph'
   fbpGraph.graph.loadFBP fbpString, (err, n) ->
     if err instanceof fbpGraph.Graph
       # legacy NoFlo, no error argument


fixes #28

fixes #28

--- src/grafana.coffee
+++ src/grafana.coffee
@@ -54,6 +54,7 @@
       to: 'now'
     }
     variables = ''
+    template_params = []
     pid = false
     pname = false

@@ -75,6 +76,7 @@
         # Check if it's a variable or part of the timespan
         if part.indexOf('=') >= 0
           variables = "#{variables}&var-#{part}"
+          template_params.push { "name": part.split('=')[0], "value": part.split('=')[1] }

         # Only add to the timespan if we haven't already filled out from and to
         else if timeFields.length > 0
@@ -84,6 +86,7 @@
     robot.logger.debug slug
     robot.logger.debug timespan
     robot.logger.debug variables
+    robot.logger.debug template_params
     robot.logger.debug pid
     robot.logger.debug pname

@@ -115,7 +118,11 @@
         for template in data.templating.list
           robot.logger.debug template
           continue unless template.current
-          template_map['$' + template.name] = template.current.text
+          for _param in template_params
+            if template.name == _param.name
+              template_map['$' + template.name] = _param.value
+            else
+              template_map['$' + template.name] = template.current.text

       # Return dashboard rows
       panelNumber = 0


ImageEditorStatusView Spec

ImageEditorStatusView Spec

--- spec/image-editor-view-spec.coffee
+++ spec/image-editor-view-spec.coffee
@@ -1,10 +1,24 @@
+{WorkspaceView, View} = require 'atom'
+ImageEditorStatusView = require '../lib/image-editor-status-view'
 ImageEditorView = require '../lib/image-editor-view'
 ImageEditor = require '../lib/image-editor'
+
+class StatusBarMock extends View
+  @content: ->
+    @div class: 'status-bar tool-panel panel-bottom', =>
+      @div outlet: 'leftPanel', class: 'status-bar-left'
+
+  attach: ->
+    atom.workspaceView.appendToTop(this)
+
+  appendLeft: (item) ->
+    @leftPanel.append(item)

 describe "ImageEditorView", ->
   [editor, view, filePath] = []

   beforeEach ->
+    atom.workspaceView = new WorkspaceView
     filePath = atom.project.resolve('binary-file.png')
     editor = new ImageEditor(filePath)
     view = new ImageEditorView(editor)
@@ -42,3 +56,21 @@
       view.trigger 'image-view:reset-zoom'
       expect(view.image.width()).toBe 10
       expect(view.image.height()).toBe 10
+
+  describe "ImageEditorStatusView", ->
+    [imageSizeStatus] = []
+
+    beforeEach ->
+      atom.workspaceView.statusBar = new StatusBarMock()
+      atom.workspaceView.statusBar.attach()
+      imageEditorStatusView = new ImageEditorStatusView(editor.getPath(), view.image)
+
+      imageSizeStatus = atom.workspaceView.statusBar.leftPanel.children().view()
+      expect(imageSizeStatus).toExist()
+
+    afterEach ->
+      atom.workspaceView.statusBar.remove()
+      atom.workspaceView.statusBar = null
+
+    it "displays the size of the image", ->
+      expect(imageSizeStatus.text()).toBe '10px x 10px'


Fix 2.0 tests.

Fix 2.0 tests.

--- spec/jquery.turbolinks_spec.coffee
+++ spec/jquery.turbolinks_spec.coffee
@@ -24,7 +24,6 @@
   describe "DOM isn't ready", ->

     beforeEach ->
-      $.turbo.use('page:load', 'page:fetch')
       $.turbo.isReady = false

       $(callback1 = sinon.spy())
@@ -40,18 +39,12 @@
          callback1.should.have.been.calledOnce
          callback2.should.have.been.calledOnce

-    it 'should pass jQuery object to callbacks', ->
-      $(document).trigger('page:load')
-
-      callback1.should.have.been.calledWith($)
-
-    describe '$.setReadyEvent', ->
-
+    describe '$.turbo.use', ->
       beforeEach ->
-      $.turbo.use('page:load', 'page:fetch')
+        $.turbo.use('page:load', 'page:fetch')

       it 'should unbind default (page:load) event', ->
-        $.setReadyEvent('random_event_name')
+        $.turbo.use('other1', 'other2')

         $(document).trigger('page:load')

@@ -59,8 +52,6 @@
         callback2.should.have.not.been.called

       it 'should bind ready to passed function', ->
-        $.setReadyEvent('page:change')
-
         $(document)
           .trigger('page:load')
           .trigger('page:change')


removed menu>Actions>Presentation

removed menu>Actions>Presentation

--- client/prefs.coffee
+++ client/prefs.coffee
@@ -68,11 +68,6 @@
       -
       Theme                    =THM
     _Actions
-      Presentation                  {!browser}
-        Run Demo Script...     =DMR {!browser}
-        Next Line of Demo      =DMN {!browser}
-        Previous Line of Demo  =DMP {!browser}
-      -                             {!browser}
       Weak Interrupt           =WI
       Strong Interrupt         =SI
     _Window                         {mac}


Attach elements to jasmine DOM

Attach elements to jasmine DOM

Atom migrated to using customElements from document.registerElement. Due
to this move, now there is need to attach some elements to the DOM on
some specs.

--- spec/tree-view-package-spec.coffee
+++ spec/tree-view-package-spec.coffee
@@ -2688,6 +2688,8 @@
             dotFileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))

           runs ->
+            workspaceElement = atom.views.getView(atom.workspace)
+            jasmine.attachToDOM(workspaceElement)
             atom.commands.dispatch(treeView.element, "tree-view:duplicate")
             copyDialog = atom.workspace.getModalPanels()[0].getItem()

@@ -2710,6 +2712,8 @@
             dotFileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))

           runs ->
+            workspaceElement = atom.views.getView(atom.workspace)
+            jasmine.attachToDOM(workspaceElement)
             atom.commands.dispatch(treeView.element, "tree-view:duplicate")
             copyDialog = atom.workspace.getModalPanels()[0].getItem()

@@ -2732,6 +2736,8 @@
             atom.workspace.open('tree-view.js')

           runs ->
+            workspaceElement = atom.views.getView(atom.workspace)
+            jasmine.attachToDOM(workspaceElement)
             editorElement = atom.views.getView(atom.workspace.getCenter().getActivePaneItem())
             atom.commands.dispatch(editorElement, "tree-view:duplicate")
             copyDialog = atom.workspace.getModalPanels()[0].getItem()
@@ -5244,6 +5250,7 @@
     waitForPackageActivation()

     runs ->
+      jasmine.attachToDOM(workspaceElement)
       treeView = atom.packages.getActivePackage("tree-view").mainModule.getTreeViewInstance()
       files = workspaceElement.querySelectorAll('li[is="tree-view-file"]')


Timer now uses new concept

Timer now uses new concept

--- src/6.timer.coffee
+++ src/6.timer.coffee
@@ -1,21 +1,36 @@
 # Why timer is in `Date`? Because it's not like an ordinary timer which is based only on
 # additive timespans. It's based on "time" and timespans. So it's related to `Date`.
+
 # Why default precision is 30ms? Because in most cases we don't want it to bring more CPU usage.
 # In my test if it's 1ms then CPU usage will be around 5%. If 30ms then 0.4%.
 # These two numbers are in single-core, so it's 1.25% and 0.1% in all my 4 cores.

+# Why use one "global" `setInterval` to do checking? Because in my test, multiple `setInterval`
+# or `setTimeout` are expensive. Here's the test result (both using 1ms interval):
+# Single `setInterval`, callback has a 100000-cycle loop: 16% CPU usage
+# 100 `setInterval`, callback is very simple: 13% CPU usage
+# So, single `setInterval` is better. The real production will not reach 100000 cycles.
+
 class Date.Timer
     @_endOfTime: new Date("9999-12-30T00:00:00Z")
+    @enable: (precision = 30) ->
+        Date.Timer._precision = precision
+        @_internalTimer = setInterval(=>
+            Date.Timer._onCheck.fire()
+        , precision)
+    @disable: ->
+        clearInterval(@_internalTimer)
+    @getPrecision: ->
+        Date.Timer._precision
+    @_onCheck: eventField()
     constructor: (@targetTime = Date.Timer._endOfTime) ->
         @_counter = 0
-        @_internalTimer = null
         @_running = false
         @allowsEqual = true
-        @precision = 30
         @onArrive = eventField()
     run: ->
         if @_running then return
-        @_internalTimer = setInterval(=>
+        @_checker = =>
             nowTime = new Date()
             if (if @allowsEqual then nowTime >= @targetTime else nowTime > @targetTime)
                 @_counter++
@@ -26,12 +41,12 @@
                     nowTime: nowTime
                     index: @_counter - 1
                 )
-        , @precision)
+        Date.Timer._onCheck.bind(@_checker)
         @_running = true
         @
     stop: ->
         if not @_running then return
-        clearInterval(@_internalTimer)
+        Date.Timer._onCheck.unbind(@_checker)
         @_running = false
         @
     getRunning: -> @_running
@@ -46,6 +61,9 @@
         @includesEnd = false
         @onStart = eventField()
         @onArrive.bind((event) =>
+            if @interval < Date.Timer.getPrecision() * 2
+                @stop()
+                return
             @targetTime = event.idealTime.add(@interval)
             if not @_started
                 @_started = true
@@ -63,7 +81,7 @@
         )
 class Date.Observer extends Date.IntervalTimer
     constructor: (fun) ->
-        super(200)
+        super(100)
         @_fun = fun
         @onChange = eventField()
         @onArrive.bind(=>


added minus only example

added minus only example

--- examples/use_minus.coffee
+++ examples/use_minus.coffee
@@ -1,5 +1,5 @@
 PEG = require("./minus.js")
-input = process.argv[2] || "5-1-2"
+input = process.argv[2] || "5-1-(2-1)"
 r = PEG.parse(input)
 console.log(r)


Preventing br and p filters from displaying null and undefined

--- src/filters.coffee
+++ src/filters.coffee
@@ -85,7 +85,7 @@
 # ```
 chip.filter 'p', (controller, value) ->
 	div = $('<div></div>') unless div
-	lines = (('' + value) or '').split(/\r?\n/)
+	lines = (value or '').split(/\r?\n/)
 	escaped = lines.map (line) -> div.text(line).text() or '<br>'
 	'<p>' + escaped.join('</p><p>') + '</p>'

@@ -104,7 +104,7 @@
 # ```
 chip.filter 'br', (controller, value) ->
 	div = $('

') unless div
-	lines = (('' + value) or '').split(/\r?\n/)
+	lines = (value or '').split(/\r?\n/)
 	escaped = lines.map (line) -> div.text(line).text()
 	escaped.join('
')

@@ -123,7 +123,7 @@
 # ```
 chip.filter 'newline', (controller, value) ->
 	div = $('

') unless div
-	paragraphs = (('' + value) or '').split(/\r?\n\s*\r?\n/)
+	paragraphs = (value or '').split(/\r?\n\s*\r?\n/)
 	escaped = paragraphs.map (paragraph) ->
 		lines = paragraph.split(/\r?\n/)
 		escaped = lines.map (line) -> div.text(line).text()


ensure specified template var is what is used

--- src/grafana.coffee
+++ src/grafana.coffee
@@ -59,6 +59,7 @@
       to: 'now'
     }
     variables = ''
+    template_params = []
     pid = false
     pname = false

@@ -80,6 +81,7 @@
         # Check if it's a variable or part of the timespan
         if part.indexOf('=') >= 0
           variables = "#{variables}&var-#{part}"
+          template_params.push { "name": part.split('=')[0], "value": part.split('=')[1] }

         # Only add to the timespan if we haven't already filled out from and to
         else if timeFields.length > 0
@@ -89,6 +91,7 @@
     robot.logger.debug slug
     robot.logger.debug timespan
     robot.logger.debug variables
+    robot.logger.debug template_params
     robot.logger.debug pid
     robot.logger.debug pname

@@ -120,7 +123,11 @@
         for template in data.templating.list
           robot.logger.debug template
           continue unless template.current
-          template_map['$' + template.name] = template.current.text
+          for _param in template_params
+            if template.name == _param.name
+              template_map['$' + template.name] = _param.value
+            else
+              template_map['$' + template.name] = template.current.text

       # Return dashboard rows
       panelNumber = 0