{"action":"created","comment":{"url":"https://api.github.com/repos/CartoDB/cartodb/pulls/comments/42471373","id":42471373,"diff_hunk":"@@ -0,0 +1,506 @@\n+#!/usr/bin/env ruby\n+\n+# export-user.rb\n+# This script can be used to generate an as-is backup of the metadata of an user.\n+# It can be ran against a single user or a single user ID or a list of any of both.\n+# It will generate a SQL dump and a redis dump that can be ran against redis-cli --pipe\n+# and also the ones needed to remove that user.\n+\n+require 'pg'\n+require 'redis'\n+require 'yaml'\n+require 'optparse'\n+require 'json'\n+require 'tsort'\n+\n+require_relative 'config'\n+require_relative 'utils'\n+\n+module CartoDB\n+  module DataMover\n+    class DumpJob\n+      include CartoDB::DataMover::Utils\n+      def dump_db\n+        # find database host for user\n+        run_command(\"pg_dump #{conn_string(\n+          CartoDB::DataMover::Config[:dbuser],\n+          @database_host,\n+          CartoDB::DataMover::Config[:user_dbport],\n+          @database_name,\n+        )} -Fc -f #{@filename} --serializable-deferrable -v\")\n+      end\n+\n+      def initialize(database_host, database_name, path, filename, database_schema = nil)\n+        @database_host = database_host\n+        @database_name = database_name\n+        @filename = filename\n+        @database_schema = database_schema\n+        @path = path\n+\n+        if database_schema == nil\n+          dump_db\n+        else\n+          dump_schema\n+        end\n+      end\n+\n+      def dump_schema\n+        run_command(\"pg_dump #{conn_string(\n+          CartoDB::DataMover::Config[:dbuser],\n+          @database_host,\n+          CartoDB::DataMover::Config[:user_dbport],\n+          @database_name,\n+        )} -f #{@path}#{@database_schema}.schema.sql -n #{@database_schema} --verbose --no-tablespaces -Z 0\")\n+      end\n+\n+    end\n+  end\n+end\n+\n+module CartoDB\n+  module DataMover\n+    class TsortableHash < Hash\n+      include TSort\n+      alias tsort_each_node each_key\n+      def tsort_each_child(node, &block)\n+        fetch(node).each(&block)\n+      end\n+    end\n+    class ExportJob\n+      TABLE_NULL_EXCEPTIONS = ['table_quota'] # those won't be discarded if set to NULL\n+      include CartoDB::DataMover::Utils\n+\n+      def get_user_metadata(user_id)\n+        q = pg_conn.exec(\"SELECT * FROM users WHERE username = '#{user_id}'\")\n+        if q.count > 0\n+          @user_data = q[0]\n+        else\n+          throw \"Can't find user #{@user_id}\"\n+        end\n+        @username = @user_data[\"username\"]\n+        @user_id = @user_data[\"id\"]\n+      end\n+\n+      def get_org_metadata(organization_id)\n+        q = pg_conn.exec(\"SELECT * FROM organizations WHERE name = '#{organization_id}'\")\n+        if q.count > 0\n+          org_data = q[0]\n+        else\n+          throw \"Can't find organization #{@organization_id}\"\n+        end\n+\n+        org_data\n+      end\n+\n+      def get_org_users(organization_id)\n+        q = pg_conn.exec(\"SELECT * FROM users WHERE organization_id = '#{organization_id}'\")\n+        if q.count > 0\n+          return q\n+        else\n+          throw \"Can't find organization #{@organization_id}\"\n+        end\n+      end\n+\n+      def get_org_groups(organization_id)\n+        pg_conn.exec(\"SELECT * FROM groups WHERE organization_id = '#{organization_id}'\")\n+      end\n+\n+      def dump_user_data(redis_keys)\n+        data = dump_related_data(Carto::User, @user_id)\n+        data[Carto::User] = [@user_data]\n+        data.reject! { |key, _value| [Carto::Organization, Carto::Group, Carto::FeatureFlag].include?(key) }\n+        dump_sql_data(data, \"user_#{@user_id}\")\n+        dump_redis_keys(redis_keys)\n+      end\n+\n+      def dump_role_grants(role)\n+        roles = user_pg_conn.exec(\"SELECT oid, rolname FROM pg_roles WHERE pg_has_role( '#{role}', oid, 'member');\")\n+        roles.map { |q| q['rolname'] }.reject { |r| r == role }\n+      end\n+\n+      def dump_org_data\n+        data = dump_related_data(::Carto::Organization, @org_id)\n+        data[Carto::Organization] = [@org_metadata]\n+        data.select! { |key, _value| [::Carto::Organization, ::Carto::Group].include?(key) }\n+        dump_sql_data(data, \"org_#{@org_id}\")\n+      end\n+\n+      def dump_sql_data(data, prefix)\n+        # We sort the order of the tables to be exported so rows are imported before their dependencies, and deleted after.\n+        # For this, we generate a dependency tree and then use the built-in \"tsort\" topological sort library\n+        models = data.keys\n+        model_dependencies = models.map do |m|\n+          [m,\n+           m.reflections.values.select(&:belongs_to?)\n+           .reject { |r| !r.inverse_of.nil? && r.inverse_of.belongs_to? } # Remove mutual foreign_keys\n+           .map(&:klass).select { |s| models.include?(s) }]\n+        end\n+        models_ordered = TsortableHash[model_dependencies].tsort\n+        File.open(@options[:path] + \"#{prefix}_metadata.sql\", \"w\") do |f|\n+          models_ordered.each do |model|\n+            data[model].each do |rows|\n+              keys = rows.keys.select { |k| !TABLE_NULL_EXCEPTIONS.include?(k.to_s) == (!rows[k].nil?) }\n+              f.write generate_pg_insert_query(model.table_name, keys, rows)\n+            end\n+          end\n+        end\n+        File.open(@options[:path] + \"#{prefix}_metadata_undo.sql\", \"w\") do |f|\n+          models_ordered.reverse_each do |model|\n+            data[model].each do |rows|\n+              keys = rows.keys.select { |k| !rows[k].nil? }\n+              f.write generate_pg_delete_query(model.table_name, rows)\n+            end\n+          end\n+        end\n+      end\n+\n+      def generate_pg_delete_query(table_name, rows)\n+        \"DELETE FROM #{table_name} WHERE id = '#{rows['id']}';\\n\"\n+      end\n+\n+      def generate_pg_insert_query(table_name, keys, rows)\n+        \"INSERT INTO #{table_name}(#{keys.map { |i| \"\\\"#{i}\\\"\" }.join(',')}) VALUES(#{keys.map { |i| rows[i] == nil ? 'NULL' : \"'\" + pg_conn.escape_string(rows[i]) + \"'\" }.join(',')});\\n\"\n+      end\n+\n+      def dump_related_data(model, id, exclude = [])\n+        data = {}\n+        id = [id] if id.is_a? Integer or id.is_a? String\n+\n+        # first dump this model\n+        query = \"SELECT * FROM #{model.table_name} WHERE id IN (#{id.map { |i| \"'#{i}'\" }.join(', ')});\"\n+        result = pg_conn.exec(query)\n+        data[model] = (0..result.cmd_tuples - 1).map do |tuple_number|\n+          result[tuple_number]\n+        end\n+\n+        model.reflections.each do |_name, reflection|\n+          unless exclude.include? reflection.klass or !reflection.through_reflection.nil?\n+\n+            if reflection.belongs_to?\n+              ids = data[model].map { |t| t[reflection.association_foreign_key.to_s] }.reject { |t| t == nil }\n+              next if ids.empty?\n+              query = \"SELECT * FROM #{reflection.table_name} WHERE #{reflection.association_primary_key} IN (#{ids.map { |i| \"'#{i}'\" }.join(', ')})\"\n+            else\n+              query = \"SELECT * FROM #{reflection.table_name} WHERE #{reflection.foreign_key} IN (#{id.map { |i| \"'#{i}'\" }.join(', ')})\"\n+            end\n+            result = pg_conn.exec(query)\n+\n+            data[reflection.klass] = (0..result.cmd_tuples - 1).map do |tuple_number|\n+              result[tuple_number]\n+            end\n+\n+            ids = data[reflection.klass].map do |data_for_related_key|\n+              data_for_related_key[\"id\"]\n+            end\n+            data.merge!(dump_related_data(reflection.klass, ids, exclude + [model])) { |_, x, y| merge_without_duplicated_ids(x, y) } if ids.length > 0\n+          end\n+        end\n+\n+        data\n+      end\n+\n+      def merge_without_duplicated_ids(x, y)\n+        # this gets called when we try to merge >1 table.\n+        # it will remove duplicates by ida\n+        (x + y).uniq { |s| s['id'] }\n+      end\n+\n+      def gen_redis_proto(*cmd)\n+        proto = \"\"\n+        proto << \"*\" + cmd.length.to_s + \"\\r\\n\"\n+        cmd.each { |arg|\n+          proto << \"$\" + arg.to_s.bytesize.to_s + \"\\r\\n\"\n+          proto << arg.to_s + \"\\r\\n\"\n+        }\n+        proto\n+      end\n+\n+      def format_redis_dump(str)\n+        str.gsub(\"'\", %q(\\\\\\'))\n+      end\n+\n+      def dump_redis_keys(redis_keys)\n+        File.open(@options[:path] + \"user_#{@user_id}_metadata.redis\", \"wb\") do |dump|\n+          File.open(@options[:path] + \"user_#{@user_id}_metadata_undo.redis\", \"wb\") do |undo|\n+            redis_keys.each do |k, v|\n+              dump.write gen_redis_proto(\"SELECT\", v[:db])\n+              undo.write gen_redis_proto(\"SELECT\", v[:db])\n+              redis_conn.select(v[:db])\n+              these_redis_keys = redis_conn.keys(redis_template_user_gsub(v[:template], @user_id, @username))\n+              these_redis_keys.each do |trd|\n+                type = redis_conn.type(trd)\n+                if type == 'string'\n+                  dump.write gen_redis_proto(\"SET\", trd, redis_conn.get(trd))\n+                end\n+\n+                if type == 'hash'\n+                  redis_conn.hgetall(trd).each do |key, value|\n+                    dump.write gen_redis_proto(\"HSET\", trd, key, value)\n+                  end\n+                end\n+\n+                if type == 'zset'\n+                  r = redis_conn.zrangebyscore(trd, '-inf', '+inf')\n+                  r.each do |r_key|\n+                    k = redis_conn.zscore(trd, r_key)\n+                    dump.write gen_redis_proto(\"ZINCRBY\", trd, k, r_key)\n+                  end\n+                end\n+                undo.write gen_redis_proto(\"DEL\", trd)\n+              end\n+            end\n+            dump.write redis_oauth_keys\n+          end\n+        end\n+      end\n+\n+      def redis_oauth_keys\n+        dump = \"\"\n+        pg_conn.exec(\"SELECT token,user_id FROM oauth_tokens WHERE type='AccessToken' AND user_id = '#{@user_id}'\") do |result|\n+          dump += gen_redis_proto(\"SELECT\", 3)\n+          redis_conn.select(3)\n+          result.each do |row|\n+            redis_conn.hgetall(\"rails:oauth_access_tokens:#{row['token']}\").each do |key, value|\n+              dump += gen_redis_proto(\"HSET\", \"rails:oauth_access_tokens:#{row['token']}\", key, value)\n+            end\n+          end\n+        end\n+        dump\n+      end\n+\n+      def redis_replace_from_template(template, id)\n+        if template.include?('USERDB')\n+          user_database(id)\n+        elsif template.include?('DBUSER')\n+          database_username(id)\n+        elsif template.include?('USERNAME')\n+          @username\n+        elsif template.include?('USERID')\n+          id\n+        else\n+          ''\n+        end\n+      end\n+\n+      def redis_template_user_gsub(template, id, _username)\n+        replacement = redis_replace_from_template(template, id)\n+        if template.include?('USERDB')\n+          template.gsub('USERDB', replacement)\n+        elsif template.include?('DBUSER')\n+          template.gsub('DBUSER', replacement)\n+        elsif template.include?('USERNAME')\n+          template.gsub('USERNAME', replacement)\n+        else\n+          ''\n+        end\n+      end\n+\n+      def alter_redis_hash(redis_key, _redis_attribute, redis_value, options = {})\n+        redis_db = options['db'].nil? ? 0 : options['db']\n+        redis_conn.select(redis_db)\n+        redis_conn.hset(redis_key, @user_id, redis_value)\n+      end\n+\n+      def redis_conn\n+        @redis ||= Redis.new(host: CartoDB::DataMover::Config[:redis_host], port: CartoDB::DataMover::Config[:redis_port])\n+      end\n+\n+      def pg_conn\n+        @conn ||= PGconn.connect(host: CartoDB::DataMover::Config[:dbhost],\n+                                 user: CartoDB::DataMover::Config[:dbuser],\n+                                 dbname: CartoDB::DataMover::Config[:dbname],\n+                                 port: CartoDB::DataMover::Config[:dbport],\n+                                 password: CartoDB::DataMover::Config[:dbpass])\n+      end\n+\n+      def user_pg_conn\n+        @user_conn ||= PGconn.connect(host: @user_data['database_host'],\n+                                      user: CartoDB::DataMover::Config[:dbuser],\n+                                      dbname: @user_data['database_name'],\n+                                      port: CartoDB::DataMover::Config[:user_dbport],\n+                                      password: CartoDB::DataMover::Config[:dbpass])\n+      end\n+\n+      def run_command(cmd)\n+        p cmd\n+        IO.popen(cmd) do |io|\n+          Process.wait(io.pid)\n+        end\n+        puts \"Output code: #{$?}\"\n+      end\n+\n+      def user_roles\n+        if @user_data['database_schema'] == 'public'\n+          roles = ['publicuser', database_username(@user_id)]\n+        else\n+          roles = [\"cartodb_publicuser_#{@user_id}\", database_username(@user_id)]\n+        end\n+\n+        Hash[roles.map { |role| [role, dump_role_grants(role)] }]\n+      end\n+\n+      def user_info\n+        { user: @user_data, roles: user_roles }\n+      end\n+\n+      def reflections_for_model(model, skip = [], parents = [])\n+        result = {}\n+        parents << model.table_name.to_sym\n+        reflections = model.reflections\n+        related = reflections.keys.select { |r| reflections[r].through_reflection == nil && !parents.include?(reflections[r].table_name.to_sym) }\n+        relations = {}\n+        related.each do |reflection_name|\n+          reflection = reflections[reflection_name]\n+          relations[reflection.klass.table_name] = reflection.foreign_key\n+        end\n+        result[model.table_name] = { related: related.map { |t| reflections[t].klass.table_name }, relation_for: relations }\n+        related.each do |rel|\n+          unless skip.include?(reflections[rel].klass.table_name) || result.keys.include?(reflections[rel].klass.table_name)\n+            result.merge!(reflections_for_model(reflections[rel.to_sym].klass, (skip + result.keys).uniq, parents))\n+          end\n+        end\n+        result\n+      end\n+\n+      def initialize(options)\n+        @options = options\n+        @options[:path] ||= ''\n+\n+        @logs = []\n+\n+        redis_keys = {\n+          mapviews: {\n+            template: \"user:USERNAME:mapviews:*\",\n+            var_position: 1,\n+            type: 'zset',\n+            db: 5,\n+            separator: \":\"\n+          },\n+          map_style: {\n+            template: \"map_style|USERDB|*\",\n+            var_position: 1,\n+            separator: '|',\n+            db: 0,\n+            type: 'string'\n+          },\n+          map_template: {\n+            template: \"map_tpl|USERNAME\",\n+            no_clone: true,\n+            var_position: 1,\n+            separator: '|',\n+            db: 0,\n+            type: 'hash'\n+          },\n+          table: {\n+            template: \"rails:USERDB:*\",\n+            var_position: 1,\n+            separator: ':',\n+            db: 0,\n+            type: 'hash',\n+            attributes: {\n+              user_id: 'USERID'\n+            }\n+          },\n+          limits_tiler: {\n+            template: \"limits:tiler:USERNAME\",\n+            no_clone: true,\n+            var_position: 2,\n+            separator: ':',\n+            db: 5,\n+            type: 'hash'\n+          },\n+          user: {\n+            template: \"rails:users:USERNAME\",\n+            no_clone: true,\n+            var_position: 2,\n+            separator: ':',\n+            db: 5,\n+            type: 'hash',\n+            attributes: {\n+              database_name: 'USERDB',\n+              id: 'USERID'\n+            }\n+          }\n+        }\n+        if options[:id]\n+          get_user_metadata(options[:id])\n+          dump_user_data(redis_keys) unless options[:database_only] == true\n+          redis_conn.quit\n+          DumpJob.new(\n+            @user_data['database_host'] || '127.0.0.1',","path":"services/user-mover/export_user.rb","position":null,"original_position":430,"commit_id":"2eb52872f32b31c97e7f25d12979860b2fbe4919","original_commit_id":"6161b22450a913789619423c90581fd126351373","user":{"login":"zenitraM","id":863975,"avatar_url":"https://avatars.githubusercontent.com/u/863975?v=3","gravatar_id":"","url":"https://api.github.com/users/zenitraM","html_url":"https://github.com/zenitraM","followers_url":"https://api.github.com/users/zenitraM/followers","following_url":"https://api.github.com/users/zenitraM/following{/other_user}","gists_url":"https://api.github.com/users/zenitraM/gists{/gist_id}","starred_url":"https://api.github.com/users/zenitraM/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/zenitraM/subscriptions","organizations_url":"https://api.github.com/users/zenitraM/orgs","repos_url":"https://api.github.com/users/zenitraM/repos","events_url":"https://api.github.com/users/zenitraM/events{/privacy}","received_events_url":"https://api.github.com/users/zenitraM/received_events","type":"User","site_admin":false},"body":"@user_data is the content of the row of the user model itself, there was a time when we just added support for external database_hosts when it could be blank and that meant it was on 127.0.0.1 (can't find it now though :/)","created_at":"2015-10-20T09:07:35Z","updated_at":"2015-10-20T09:07:35Z","html_url":"https://github.com/CartoDB/cartodb/pull/5898#discussion_r42471373","pull_request_url":"https://api.github.com/repos/CartoDB/cartodb/pulls/5898","_links":{"self":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/comments/42471373"},"html":{"href":"https://github.com/CartoDB/cartodb/pull/5898#discussion_r42471373"},"pull_request":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/5898"}}},"pull_request":{"url":"https://api.github.com/repos/CartoDB/cartodb/pulls/5898","id":47039894,"html_url":"https://github.com/CartoDB/cartodb/pull/5898","diff_url":"https://github.com/CartoDB/cartodb/pull/5898.diff","patch_url":"https://github.com/CartoDB/cartodb/pull/5898.patch","issue_url":"https://api.github.com/repos/CartoDB/cartodb/issues/5898","number":5898,"state":"open","locked":false,"title":"User mover","user":{"login":"Kartones","id":2085449,"avatar_url":"https://avatars.githubusercontent.com/u/2085449?v=3","gravatar_id":"","url":"https://api.github.com/users/Kartones","html_url":"https://github.com/Kartones","followers_url":"https://api.github.com/users/Kartones/followers","following_url":"https://api.github.com/users/Kartones/following{/other_user}","gists_url":"https://api.github.com/users/Kartones/gists{/gist_id}","starred_url":"https://api.github.com/users/Kartones/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Kartones/subscriptions","organizations_url":"https://api.github.com/users/Kartones/orgs","repos_url":"https://api.github.com/users/Kartones/repos","events_url":"https://api.github.com/users/Kartones/events{/privacy}","received_events_url":"https://api.github.com/users/Kartones/received_events","type":"User","site_admin":false},"body":"","created_at":"2015-10-07T15:08:55Z","updated_at":"2015-10-20T09:07:35Z","closed_at":null,"merged_at":null,"merge_commit_sha":"c160215312805690627deb58fae24628f58450d0","assignee":{"login":"Kartones","id":2085449,"avatar_url":"https://avatars.githubusercontent.com/u/2085449?v=3","gravatar_id":"","url":"https://api.github.com/users/Kartones","html_url":"https://github.com/Kartones","followers_url":"https://api.github.com/users/Kartones/followers","following_url":"https://api.github.com/users/Kartones/following{/other_user}","gists_url":"https://api.github.com/users/Kartones/gists{/gist_id}","starred_url":"https://api.github.com/users/Kartones/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Kartones/subscriptions","organizations_url":"https://api.github.com/users/Kartones/orgs","repos_url":"https://api.github.com/users/Kartones/repos","events_url":"https://api.github.com/users/Kartones/events{/privacy}","received_events_url":"https://api.github.com/users/Kartones/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/CartoDB/cartodb/pulls/5898/commits","review_comments_url":"https://api.github.com/repos/CartoDB/cartodb/pulls/5898/comments","review_comment_url":"https://api.github.com/repos/CartoDB/cartodb/pulls/comments{/number}","comments_url":"https://api.github.com/repos/CartoDB/cartodb/issues/5898/comments","statuses_url":"https://api.github.com/repos/CartoDB/cartodb/statuses/2eb52872f32b31c97e7f25d12979860b2fbe4919","head":{"label":"CartoDB:user-mover","ref":"user-mover","sha":"2eb52872f32b31c97e7f25d12979860b2fbe4919","user":{"login":"CartoDB","id":1799254,"avatar_url":"https://avatars.githubusercontent.com/u/1799254?v=3","gravatar_id":"","url":"https://api.github.com/users/CartoDB","html_url":"https://github.com/CartoDB","followers_url":"https://api.github.com/users/CartoDB/followers","following_url":"https://api.github.com/users/CartoDB/following{/other_user}","gists_url":"https://api.github.com/users/CartoDB/gists{/gist_id}","starred_url":"https://api.github.com/users/CartoDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CartoDB/subscriptions","organizations_url":"https://api.github.com/users/CartoDB/orgs","repos_url":"https://api.github.com/users/CartoDB/repos","events_url":"https://api.github.com/users/CartoDB/events{/privacy}","received_events_url":"https://api.github.com/users/CartoDB/received_events","type":"Organization","site_admin":false},"repo":{"id":4909355,"name":"cartodb","full_name":"CartoDB/cartodb","owner":{"login":"CartoDB","id":1799254,"avatar_url":"https://avatars.githubusercontent.com/u/1799254?v=3","gravatar_id":"","url":"https://api.github.com/users/CartoDB","html_url":"https://github.com/CartoDB","followers_url":"https://api.github.com/users/CartoDB/followers","following_url":"https://api.github.com/users/CartoDB/following{/other_user}","gists_url":"https://api.github.com/users/CartoDB/gists{/gist_id}","starred_url":"https://api.github.com/users/CartoDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CartoDB/subscriptions","organizations_url":"https://api.github.com/users/CartoDB/orgs","repos_url":"https://api.github.com/users/CartoDB/repos","events_url":"https://api.github.com/users/CartoDB/events{/privacy}","received_events_url":"https://api.github.com/users/CartoDB/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/CartoDB/cartodb","description":"Location Intelligence & Data Visualization tool","fork":false,"url":"https://api.github.com/repos/CartoDB/cartodb","forks_url":"https://api.github.com/repos/CartoDB/cartodb/forks","keys_url":"https://api.github.com/repos/CartoDB/cartodb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/CartoDB/cartodb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/CartoDB/cartodb/teams","hooks_url":"https://api.github.com/repos/CartoDB/cartodb/hooks","issue_events_url":"https://api.github.com/repos/CartoDB/cartodb/issues/events{/number}","events_url":"https://api.github.com/repos/CartoDB/cartodb/events","assignees_url":"https://api.github.com/repos/CartoDB/cartodb/assignees{/user}","branches_url":"https://api.github.com/repos/CartoDB/cartodb/branches{/branch}","tags_url":"https://api.github.com/repos/CartoDB/cartodb/tags","blobs_url":"https://api.github.com/repos/CartoDB/cartodb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/CartoDB/cartodb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/CartoDB/cartodb/git/refs{/sha}","trees_url":"https://api.github.com/repos/CartoDB/cartodb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/CartoDB/cartodb/statuses/{sha}","languages_url":"https://api.github.com/repos/CartoDB/cartodb/languages","stargazers_url":"https://api.github.com/repos/CartoDB/cartodb/stargazers","contributors_url":"https://api.github.com/repos/CartoDB/cartodb/contributors","subscribers_url":"https://api.github.com/repos/CartoDB/cartodb/subscribers","subscription_url":"https://api.github.com/repos/CartoDB/cartodb/subscription","commits_url":"https://api.github.com/repos/CartoDB/cartodb/commits{/sha}","git_commits_url":"https://api.github.com/repos/CartoDB/cartodb/git/commits{/sha}","comments_url":"https://api.github.com/repos/CartoDB/cartodb/comments{/number}","issue_comment_url":"https://api.github.com/repos/CartoDB/cartodb/issues/comments{/number}","contents_url":"https://api.github.com/repos/CartoDB/cartodb/contents/{+path}","compare_url":"https://api.github.com/repos/CartoDB/cartodb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/CartoDB/cartodb/merges","archive_url":"https://api.github.com/repos/CartoDB/cartodb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/CartoDB/cartodb/downloads","issues_url":"https://api.github.com/repos/CartoDB/cartodb/issues{/number}","pulls_url":"https://api.github.com/repos/CartoDB/cartodb/pulls{/number}","milestones_url":"https://api.github.com/repos/CartoDB/cartodb/milestones{/number}","notifications_url":"https://api.github.com/repos/CartoDB/cartodb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/CartoDB/cartodb/labels{/name}","releases_url":"https://api.github.com/repos/CartoDB/cartodb/releases{/id}","created_at":"2012-07-05T15:23:45Z","updated_at":"2015-10-19T18:20:07Z","pushed_at":"2015-10-20T09:05:24Z","git_url":"git://github.com/CartoDB/cartodb.git","ssh_url":"git@github.com:CartoDB/cartodb.git","clone_url":"https://github.com/CartoDB/cartodb.git","svn_url":"https://github.com/CartoDB/cartodb","homepage":"http://cartodb.com","size":515094,"stargazers_count":963,"watchers_count":963,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":230,"mirror_url":null,"open_issues_count":813,"forks":230,"open_issues":813,"watchers":963,"default_branch":"master"}},"base":{"label":"CartoDB:master","ref":"master","sha":"8af658a5874eeb34c5ca51eeb61fb0eaa7541bd3","user":{"login":"CartoDB","id":1799254,"avatar_url":"https://avatars.githubusercontent.com/u/1799254?v=3","gravatar_id":"","url":"https://api.github.com/users/CartoDB","html_url":"https://github.com/CartoDB","followers_url":"https://api.github.com/users/CartoDB/followers","following_url":"https://api.github.com/users/CartoDB/following{/other_user}","gists_url":"https://api.github.com/users/CartoDB/gists{/gist_id}","starred_url":"https://api.github.com/users/CartoDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CartoDB/subscriptions","organizations_url":"https://api.github.com/users/CartoDB/orgs","repos_url":"https://api.github.com/users/CartoDB/repos","events_url":"https://api.github.com/users/CartoDB/events{/privacy}","received_events_url":"https://api.github.com/users/CartoDB/received_events","type":"Organization","site_admin":false},"repo":{"id":4909355,"name":"cartodb","full_name":"CartoDB/cartodb","owner":{"login":"CartoDB","id":1799254,"avatar_url":"https://avatars.githubusercontent.com/u/1799254?v=3","gravatar_id":"","url":"https://api.github.com/users/CartoDB","html_url":"https://github.com/CartoDB","followers_url":"https://api.github.com/users/CartoDB/followers","following_url":"https://api.github.com/users/CartoDB/following{/other_user}","gists_url":"https://api.github.com/users/CartoDB/gists{/gist_id}","starred_url":"https://api.github.com/users/CartoDB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/CartoDB/subscriptions","organizations_url":"https://api.github.com/users/CartoDB/orgs","repos_url":"https://api.github.com/users/CartoDB/repos","events_url":"https://api.github.com/users/CartoDB/events{/privacy}","received_events_url":"https://api.github.com/users/CartoDB/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/CartoDB/cartodb","description":"Location Intelligence & Data Visualization tool","fork":false,"url":"https://api.github.com/repos/CartoDB/cartodb","forks_url":"https://api.github.com/repos/CartoDB/cartodb/forks","keys_url":"https://api.github.com/repos/CartoDB/cartodb/keys{/key_id}","collaborators_url":"https://api.github.com/repos/CartoDB/cartodb/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/CartoDB/cartodb/teams","hooks_url":"https://api.github.com/repos/CartoDB/cartodb/hooks","issue_events_url":"https://api.github.com/repos/CartoDB/cartodb/issues/events{/number}","events_url":"https://api.github.com/repos/CartoDB/cartodb/events","assignees_url":"https://api.github.com/repos/CartoDB/cartodb/assignees{/user}","branches_url":"https://api.github.com/repos/CartoDB/cartodb/branches{/branch}","tags_url":"https://api.github.com/repos/CartoDB/cartodb/tags","blobs_url":"https://api.github.com/repos/CartoDB/cartodb/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/CartoDB/cartodb/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/CartoDB/cartodb/git/refs{/sha}","trees_url":"https://api.github.com/repos/CartoDB/cartodb/git/trees{/sha}","statuses_url":"https://api.github.com/repos/CartoDB/cartodb/statuses/{sha}","languages_url":"https://api.github.com/repos/CartoDB/cartodb/languages","stargazers_url":"https://api.github.com/repos/CartoDB/cartodb/stargazers","contributors_url":"https://api.github.com/repos/CartoDB/cartodb/contributors","subscribers_url":"https://api.github.com/repos/CartoDB/cartodb/subscribers","subscription_url":"https://api.github.com/repos/CartoDB/cartodb/subscription","commits_url":"https://api.github.com/repos/CartoDB/cartodb/commits{/sha}","git_commits_url":"https://api.github.com/repos/CartoDB/cartodb/git/commits{/sha}","comments_url":"https://api.github.com/repos/CartoDB/cartodb/comments{/number}","issue_comment_url":"https://api.github.com/repos/CartoDB/cartodb/issues/comments{/number}","contents_url":"https://api.github.com/repos/CartoDB/cartodb/contents/{+path}","compare_url":"https://api.github.com/repos/CartoDB/cartodb/compare/{base}...{head}","merges_url":"https://api.github.com/repos/CartoDB/cartodb/merges","archive_url":"https://api.github.com/repos/CartoDB/cartodb/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/CartoDB/cartodb/downloads","issues_url":"https://api.github.com/repos/CartoDB/cartodb/issues{/number}","pulls_url":"https://api.github.com/repos/CartoDB/cartodb/pulls{/number}","milestones_url":"https://api.github.com/repos/CartoDB/cartodb/milestones{/number}","notifications_url":"https://api.github.com/repos/CartoDB/cartodb/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/CartoDB/cartodb/labels{/name}","releases_url":"https://api.github.com/repos/CartoDB/cartodb/releases{/id}","created_at":"2012-07-05T15:23:45Z","updated_at":"2015-10-19T18:20:07Z","pushed_at":"2015-10-20T09:05:24Z","git_url":"git://github.com/CartoDB/cartodb.git","ssh_url":"git@github.com:CartoDB/cartodb.git","clone_url":"https://github.com/CartoDB/cartodb.git","svn_url":"https://github.com/CartoDB/cartodb","homepage":"http://cartodb.com","size":515094,"stargazers_count":963,"watchers_count":963,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":230,"mirror_url":null,"open_issues_count":813,"forks":230,"open_issues":813,"watchers":963,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/5898"},"html":{"href":"https://github.com/CartoDB/cartodb/pull/5898"},"issue":{"href":"https://api.github.com/repos/CartoDB/cartodb/issues/5898"},"comments":{"href":"https://api.github.com/repos/CartoDB/cartodb/issues/5898/comments"},"review_comments":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/5898/comments"},"review_comment":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/CartoDB/cartodb/pulls/5898/commits"},"statuses":{"href":"https://api.github.com/repos/CartoDB/cartodb/statuses/2eb52872f32b31c97e7f25d12979860b2fbe4919"}}}}