GitHub PullRequestReviewCommentEvent at 2014-11-05 17:56:58
Repository: crouton
Actor: drinkcat
Payload: {"action":"created","comment":{"url":"https://api.github.com/repos/dnschneid/crouton/pulls/comments/19891632","id":19891632,"diff_hunk":"@@ -0,0 +1,871 @@\n+/* Copyright (c) 2014 The crouton Authors. All rights reserved.\n+ * Use of this source code is governed by a BSD-style license that can be\n+ * found in the LICENSE file.\n+ */\n+\n+#include <sstream>\n+#include <unordered_map>\n+\n+#include \"ppapi/cpp/graphics_2d.h\"\n+#include \"ppapi/cpp/image_data.h\"\n+#include \"ppapi/cpp/input_event.h\"\n+#include \"ppapi/cpp/instance.h\"\n+#include \"ppapi/cpp/message_loop.h\"\n+#include \"ppapi/cpp/module.h\"\n+#include \"ppapi/cpp/mouse_cursor.h\"\n+#include \"ppapi/cpp/point.h\"\n+#include \"ppapi/cpp/var.h\"\n+#include \"ppapi/cpp/var_array_buffer.h\"\n+#include \"ppapi/cpp/websocket.h\"\n+#include \"ppapi/utility/completion_callback_factory.h\"\n+\n+namespace {\n+    /* Protocol data structures */\n+#include \"../../src/fbserver-proto.h\"\n+    /* SuperL keycode (search key) */\n+    const uint32_t SUPER_L = 0xffeb;\n+\n+    const int FULLFPS = 30; /* Maximum fps */\n+    const int BLURFPS = 5; /* fps when window is possibly hidden */\n+    const int HIDDENFPS = 0; /* fps when window is hidden */\n+}  /* namespace */\n+\n+class CriatInstance : public pp::Instance {\n+public:\n+    explicit CriatInstance(PP_Instance instance)\n+        : pp::Instance(instance),\n+          callback_factory_(this),\n+          scale_(1.0f),\n+          image_data_(NULL),\n+          k_(0),\n+          websocket_(this),\n+          connected_(false),\n+          target_fps_(FULLFPS),\n+          pending_mouse_move_(false),\n+          mouse_pos_(-1, -1),\n+          avgfps_(0),\n+          display_(-1),\n+          debug_(0),\n+          hidpi_(false) {}\n+\n+    virtual ~CriatInstance() {}\n+\n+    /* Register events */\n+    virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]) {\n+        RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE |\n+                           PP_INPUTEVENT_CLASS_WHEEL |\n+                           PP_INPUTEVENT_CLASS_TOUCH);\n+        RequestFilteringInputEvents(PP_INPUTEVENT_CLASS_KEYBOARD);\n+\n+        srand(pp::Module::Get()->core()->GetTime());\n+\n+        return true;\n+    }\n+\n+    /** Interface with Javascript **/\n+public:\n+    /* Handle message from Javascript */\n+    /* Format: <type>:<str> */\n+    virtual void HandleMessage(const pp::Var& var_message) {\n+        if (!var_message.is_string())\n+            return;\n+\n+        std::string message = var_message.AsString();\n+\n+        LogMessage(2, \"message=\" + message);\n+\n+        size_t pos = message.find(':');\n+        if (pos != std::string::npos) {\n+            std::string type = message.substr(0, pos);\n+            if (type == \"resize\") {\n+                size_t pos2 = message.find('/', pos+1);\n+                if (pos2 != std::string::npos) {\n+                    int width = stoi(message.substr(pos+1, pos2-pos-1));\n+                    int height = stoi(message.substr(pos2+1));\n+                    ChangeResolution(width*scale_, height*scale_);\n+                }\n+            } else if (type == \"display\") {\n+                int display = stoi(message.substr(pos+1));\n+                if (display != display_) {\n+                    display_ = display;\n+                    SocketConnect();\n+                }\n+            } else if (type == \"blur\" || type == \"hide\") {\n+                /* Release all keys */\n+                SocketSend(pp::Var(\"Q\"), false);\n+                /* Throttle/stop refresh */\n+                SetTargetFPS((type == \"blur\") ? BLURFPS : HIDDENFPS);\n+            } else if (type == \"focus\") {\n+                /* Force refresh and ask for next frame */\n+                SetTargetFPS(FULLFPS);\n+            } else if (type == \"debug\") {\n+                debug_ = stoi(message.substr(pos+1));\n+            } else if (type == \"hidpi\") {\n+                bool newhidpi = stoi(message.substr(pos+1));\n+                if (newhidpi != hidpi_) {\n+                    hidpi_ = newhidpi;\n+                    InitContext();\n+                }\n+            }\n+        }\n+    }\n+\n+private:\n+    /* Send a status message to Javascript */\n+    void StatusMessage(std::string str) {\n+        ControlMessage(\"status\", str);\n+    }\n+\n+    /* Send a logging message to Javascript */\n+    void LogMessage(int level, std::string str) {\n+        double delta = (pp::Module::Get()->core()->GetTime()-lasttime_)*1000;\n+\n+        if (level <= debug_) {\n+            std::ostringstream status;\n+            status << \"(\" << level << \") \" << (int)delta << \" \" << str;\n+            ControlMessage(\"log\", status.str());\n+        }\n+    }\n+\n+    /* Send a control message to Javascript */\n+    /* Format: <type>:<str> */\n+    void ControlMessage(std::string type, std::string str) {\n+        std::ostringstream status;\n+        status << type << \":\" << str;\n+        PostMessage(status.str());\n+    }\n+\n+    /** WebSocket interface **/\n+private:\n+    /* Connect to WebSocket server */\n+    void SocketConnect(int32_t result = 0) {\n+        if (display_ < 0) {\n+            LogMessage(-1, \"SocketConnect: No display defined yet.\");\n+            return;\n+        }\n+\n+        std::ostringstream url;\n+        url << \"ws://localhost:\" << (PORT_BASE+display_) << \"/\";\n+        websocket_.Connect(pp::Var(url.str()), NULL, 0,\n+                           callback_factory_.NewCallback(\n+                               &CriatInstance::OnSocketConnectCompletion));\n+        StatusMessage(\"Connecting...\");\n+    }\n+\n+    /* WebSocket connected (or failed to connect) */\n+    void OnSocketConnectCompletion(int32_t result) {\n+        if (result != PP_OK) {\n+            std::ostringstream status;\n+            status << \"Connection failed (\" << result << \"), retrying...\";\n+            StatusMessage(status.str());\n+            pp::Module::Get()->core()->CallOnMainThread(1000,\n+                callback_factory_.NewCallback(&CriatInstance::SocketConnect));\n+            return;\n+        }\n+\n+        cursor_cache_.clear();\n+\n+        SocketReceive();\n+\n+        StatusMessage(\"Connected.\");\n+    }\n+\n+    /* WebSocket closed */\n+    void OnSocketClosed(int32_t result = 0) {\n+        StatusMessage(\"Disconnected...\");\n+        ControlMessage(\"disconnected\", \"Socket closed\");\n+        connected_ = false;\n+        screen_flying_ = false;\n+        Paint(true);\n+    }\n+\n+    /* Check if a packet size is valid */\n+    bool CheckSize(int length, int target, std::string type) {\n+        if (length == target)\n+            return true;\n+\n+        std::stringstream status;\n+        status << \"Invalid \" << type << \" packet (\" << length\n+               << \" != \" << target << \").\";\n+        LogMessage(-1, status.str());\n+        return false;\n+    }\n+\n+    /* Received a frame from WebSocket server */\n+    void OnSocketReceiveCompletion(int32_t result) {\n+        std::stringstream status;\n+        status << \"ReadCompletion: \" << result << \".\";\n+        LogMessage(5, status.str());\n+\n+        if (result == PP_ERROR_INPROGRESS) {\n+            LogMessage(0, \"Receive error INPROGRESS (should not happen).\");\n+            /* We called SocketReceive too many times. */\n+            /* Not fatal: just wait for next call */\n+            return;\n+        } else if (result != PP_OK) {\n+            goto error;\n+        }\n+\n+        /* Get ready to receive next frame */\n+        pp::Module::Get()->core()->CallOnMainThread(0,\n+                  callback_factory_.NewCallback(&CriatInstance::SocketReceive));\n+\n+        /* Convert binary/text to char* */\n+        const char* data;\n+        int datalen;\n+        if (receive_var_.is_array_buffer()) {\n+            pp::VarArrayBuffer array_buffer(receive_var_);\n+            data = static_cast<char*>(array_buffer.Map());\n+            datalen = array_buffer.ByteLength();\n+            std::stringstream status;\n+            status << \"receive (binary): \" << data[0];\n+            LogMessage(data[0] == 'S' ? 3 : 2, status.str());\n+        } else {\n+            LogMessage(3, \"receive (text): \" + receive_var_.AsString());\n+            std::string str = receive_var_.AsString();\n+            data = str.c_str();\n+            datalen = str.length();\n+        }\n+\n+        if (data[0] == 'V') { /* Version */\n+            if (connected_) {\n+                LogMessage(-1, \"Got a version while connected?!?\");\n+                goto error;\n+            }\n+            if (strcmp(data, VERSION)) {\n+                LogMessage(-1, \"Invalid version received (\" +\n+                               std::string(data) + \").\");\n+                goto error;\n+            }\n+            connected_ = true;\n+            SocketSend(pp::Var(\"VOK\"), false);\n+            ControlMessage(\"connected\", \"Version received\");\n+            ChangeResolution(size_.width(), size_.height());\n+            // Start requesting frames\n+            OnFlush();\n+            return;\n+        }\n+\n+        if (!connected_) {\n+            LogMessage(-1, \"Got some packet before version...\");\n+            goto error;\n+        }\n+\n+        if (data[0] == 'S') { /* Screen */\n+            if (!CheckSize(datalen, sizeof(struct screen_reply), \"screen_reply\"))\n+                goto error;\n+            struct screen_reply* reply = (struct screen_reply*)data;\n+            if (reply->updated) {\n+                if (!reply->shmfailed) {\n+                    Paint();\n+                } else {\n+                    /* Blank the frame if shm failed */\n+                    Paint(true);\n+                    force_refresh_ = true;\n+                }\n+            } else {\n+                screen_flying_ = false;\n+                /* No update: Ask for next frame in 1000/target_fps_ */\n+                if (target_fps_ > 0)\n+                    pp::Module::Get()->core()->CallOnMainThread(\n+                        1000/target_fps_,\n+                        callback_factory_.NewCallback(&CriatInstance::RequestScreen),\n+                        request_token_);\n+            }\n+\n+            if (reply->cursor_updated) {\n+                /* Cursor updated: find it in cache */\n+                std::unordered_map<uint32_t, Cursor>::iterator it =\n+                    cursor_cache_.find(reply->cursor_serial);\n+                if (it == cursor_cache_.end()) {\n+                    /* No cache entry, ask for data. */\n+                    SocketSend(pp::Var(\"P\"), false);\n+                } else {\n+                    std::ostringstream status;\n+                    status << \"Cursor use cache for \" << (reply->cursor_serial);\n+                    LogMessage(2, status.str());\n+                    pp::MouseCursor::SetCursor(this, PP_MOUSECURSOR_TYPE_CUSTOM,\n+                                               it->second.img, it->second.hot);\n+                }\n+            }\n+\n+            return;\n+        } else if (data[0] == 'P') { /* New cursor data is received */\n+            if (datalen < sizeof(struct cursor_reply)) {\n+                std::stringstream status;\n+                status << \"Invalid cursor_reply packet (\" << datalen\n+                       << \" < \" << sizeof(struct cursor_reply) << \").\";\n+                LogMessage(-1, status.str());\n+                goto error;\n+            }\n+\n+            struct cursor_reply* cursor = (struct cursor_reply*)data;\n+            if (!CheckSize(datalen,\n+                           sizeof(struct cursor_reply) +\n+                               4*cursor->width*cursor->height,\n+                           \"cursor_reply\"))\n+                goto error;\n+\n+            std::ostringstream status;\n+            status << \"Cursor \" << (cursor->width) << \"/\" << (cursor->height);\n+            status << \" \" << (cursor->xhot) << \"/\" << (cursor->yhot);\n+            status << \" \" << (cursor->cursor_serial);\n+            LogMessage(0, status.str());\n+\n+            /* Scale down if needed */\n+            int scale = 1;\n+            while (cursor->width/scale > 32 || cursor->height/scale > 32)\n+                scale *= 2;\n+\n+            int w = cursor->width/scale;\n+            int h = cursor->height/scale;\n+            pp::ImageData img(this, pp::ImageData::GetNativeImageDataFormat(),\n+                              pp::Size(w, h), true);\n+            uint32_t* data = (uint32_t*)img.data();\n+            for (int y = 0; y < h; y++) {\n+                for (int x = 0; x < w; x++) {\n+                    /* Nearest neighbour is least ugly */\n+                    data[y*w+x] = cursor->pixels[scale*y*scale*w+scale*x];\n+                }\n+            }\n+            pp::Point hot(cursor->xhot/scale, cursor->yhot/scale);\n+\n+            cursor_cache_[cursor->cursor_serial].img = img;\n+            cursor_cache_[cursor->cursor_serial].hot = hot;\n+            pp::MouseCursor::SetCursor(this, PP_MOUSECURSOR_TYPE_CUSTOM,\n+                                       img, hot);\n+            return;\n+        } else if (data[0] == 'R') { /* Resolution request reply */\n+            if (!CheckSize(datalen, sizeof(struct resolution), \"resolution\"))\n+                goto error;\n+            struct resolution* r = (struct resolution*)data;\n+            std::ostringstream newres;\n+            newres << (r->width/scale_) << \"/\" << (r->height/scale_);\n+            /* Tell Javascript so that it can center us on the page */\n+            ControlMessage(\"resize\", newres.str());\n+            force_refresh_ = true;\n+            return;\n+        } else {\n+            std::stringstream status;\n+            status << \"Error: first char \" << (int)data[0];\n+            LogMessage(0, status.str());\n+            /* fall-through: disconnect */\n+        }\n+\n+    error:\n+        LogMessage(-1, \"Receive error.\");\n+        websocket_.Close(0, pp::Var(\"Receive error\"),\n+                 callback_factory_.NewCallback(&CriatInstance::OnSocketClosed));\n+    }\n+\n+    /* Ask to receive the next WebSocket frame */\n+    void SocketReceive(int32_t result = 0) {\n+        websocket_.ReceiveMessage(&receive_var_, callback_factory_.NewCallback(\n+                                    &CriatInstance::OnSocketReceiveCompletion));\n+    }\n+\n+    /* Send a WebSocket Frame, possibly flushing current mouse position first */\n+    void SocketSend(const pp::Var& var, bool flushmouse) {\n+        if (!connected_) {\n+            LogMessage(-1, \"SocketSend: not connected!\");\n+            return;\n+        }\n+\n+        if (pending_mouse_move_ && flushmouse) {\n+            struct mousemove* mm;\n+            pp::VarArrayBuffer array_buffer(sizeof(*mm));\n+            mm = static_cast<struct mousemove*>(array_buffer.Map());\n+            mm->type = 'M';\n+            mm->x = mouse_pos_.x();\n+            mm->y = mouse_pos_.y();\n+            array_buffer.Unmap();\n+            websocket_.SendMessage(array_buffer);\n+            pending_mouse_move_ = false;\n+        }\n+\n+        websocket_.SendMessage(var);\n+    }\n+\n+    /** UI functions **/\n+public:\n+    virtual void DidChangeView(const pp::View& view) {\n+        view_scale_ = view.GetDeviceScale();\n+        view_rect_ = view.GetRect();\n+        InitContext();\n+    }\n+\n+    virtual bool HandleInputEvent(const pp::InputEvent& event) {\n+        if (event.GetType() == PP_INPUTEVENT_TYPE_KEYDOWN ||\n+            event.GetType() == PP_INPUTEVENT_TYPE_KEYUP) {\n+            pp::KeyboardInputEvent key_event(event);\n+\n+            uint32_t keycode = key_event.GetKeyCode();\n+            std::string keystr = key_event.GetCode().AsString();\n+            uint32_t keysym = KeyCodeToKeySym(keycode, keystr);\n+            bool down = event.GetType() == PP_INPUTEVENT_TYPE_KEYDOWN;\n+\n+            std::ostringstream status;\n+            status << \"Key \" << (down ? \"DOWN\" : \"UP\");\n+            status << \": C:\" << keystr;\n+            status << \"/KC:\" << std::hex << keycode;\n+            status << \"/KS:\" << std::hex << keysym;\n+\n+            if (keysym == 0) {\n+                status << \" (KEY UNKNOWN!)\";\n+                LogMessage(0, status.str());\n+                return PP_TRUE;\n+            }\n+\n+            LogMessage(1, status.str());\n+\n+            if (keycode == 183) { /* Fullscreen => toggle fullscreen */\n+                if (!down)\n+                    ControlMessage(\"state\", \"fullscreen\");\n+                return PP_TRUE;\n+            } else if (keycode == 182) { /* Expos'e => minimize window */\n+                if (!down)\n+                    ControlMessage(\"state\", \"hide\");\n+                return PP_TRUE;\n+            }\n+\n+            /* We delay sending Super-L, and only \"press\" it on mouse clicks and\n+             * letter keys (a-z). This way, Home (Search+Left) appears without\n+             * modifiers (instead of Super_L+Home) */\n+            if (keystr == \"OSLeft\") {\n+                pending_super_l_ = down;\n+                return PP_TRUE;\n+            }\n+\n+            bool letter = (keycode >= 65 && keycode <= 90);\n+            if (letter && pending_super_l_ && down) SendKey(SUPER_L, 1);\n+            SendKey(keysym, down ? 1 : 0);\n+            if (letter && pending_super_l_ && !down) SendKey(SUPER_L, 0);\n+        } else if (event.GetType() == PP_INPUTEVENT_TYPE_MOUSEDOWN ||\n+                   event.GetType() == PP_INPUTEVENT_TYPE_MOUSEUP   ||\n+                   event.GetType() == PP_INPUTEVENT_TYPE_MOUSEMOVE) {\n+            pp::MouseInputEvent mouse_event(event);\n+\t    pp::Point mouse_event_pos(\n+                mouse_event.GetPosition().x() * scale_,\n+                mouse_event.GetPosition().y() * scale_);\n+            bool down = event.GetType() == PP_INPUTEVENT_TYPE_MOUSEDOWN;\n+\n+            if (mouse_pos_.x() != mouse_event_pos.x() ||\n+\t\tmouse_pos_.y() != mouse_event_pos.y()) {\n+                pending_mouse_move_ = true;\n+                mouse_pos_ = mouse_event_pos;\n+            }\n+\n+            std::ostringstream status;\n+            status << \"Mouse \" << mouse_event_pos.x() << \"x\"\n+                               << mouse_event_pos.y();\n+\n+            if (event.GetType() != PP_INPUTEVENT_TYPE_MOUSEMOVE) {\n+                status << \" \" << (down ? \"DOWN\" : \"UP\");\n+                status << \" \" << (mouse_event.GetButton());\n+\n+                SendClick(mouse_event.GetButton()+1, down ? 1 : 0);","path":"host-ext/nacl_src/criat.cc","position":465,"original_position":466,"commit_id":"368341208f859dbe7be194ba07b4fa2fd83425c0","original_commit_id":"9d1146681f5cd1dbcb0a759b0538bfca6ea6429b","user":{"login":"drinkcat","id":3467596,"avatar_url":"https://avatars.githubusercontent.com/u/3467596?v=2","gravatar_id":"","url":"https://api.github.com/users/drinkcat","html_url":"https://github.com/drinkcat","followers_url":"https://api.github.com/users/drinkcat/followers","following_url":"https://api.github.com/users/drinkcat/following{/other_user}","gists_url":"https://api.github.com/users/drinkcat/gists{/gist_id}","starred_url":"https://api.github.com/users/drinkcat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/drinkcat/subscriptions","organizations_url":"https://api.github.com/users/drinkcat/orgs","repos_url":"https://api.github.com/users/drinkcat/repos","events_url":"https://api.github.com/users/drinkcat/events{/privacy}","received_events_url":"https://api.github.com/users/drinkcat/received_events","type":"User","site_admin":false},"body":"Well, then I'd have to change the mouse wheel events. Clarified in comments that `SendClick` takes a X11-numbered button.","created_at":"2014-11-05T17:56:58Z","updated_at":"2014-11-05T17:56:58Z","html_url":"https://github.com/dnschneid/crouton/pull/1144#discussion_r19891632","pull_request_url":"https://api.github.com/repos/dnschneid/crouton/pulls/1144","_links":{"self":{"href":"https://api.github.com/repos/dnschneid/crouton/pulls/comments/19891632"},"html":{"href":"https://github.com/dnschneid/crouton/pull/1144#discussion_r19891632"},"pull_request":{"href":"https://api.github.com/repos/dnschneid/crouton/pulls/1144"}}},"pull_request":{"url":"https://api.github.com/repos/dnschneid/crouton/pulls/1144","id":23264385,"html_url":"https://github.com/dnschneid/crouton/pull/1144","diff_url":"https://github.com/dnschneid/crouton/pull/1144.diff","patch_url":"https://github.com/dnschneid/crouton/pull/1144.patch","issue_url":"https://api.github.com/repos/dnschneid/crouton/issues/1144","number":1144,"state":"open","locked":false,"title":"Crouton in a tab","user":{"login":"drinkcat","id":3467596,"avatar_url":"https://avatars.githubusercontent.com/u/3467596?v=2","gravatar_id":"","url":"https://api.github.com/users/drinkcat","html_url":"https://github.com/drinkcat","followers_url":"https://api.github.com/users/drinkcat/followers","following_url":"https://api.github.com/users/drinkcat/following{/other_user}","gists_url":"https://api.github.com/users/drinkcat/gists{/gist_id}","starred_url":"https://api.github.com/users/drinkcat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/drinkcat/subscriptions","organizations_url":"https://api.github.com/users/drinkcat/orgs","repos_url":"https://api.github.com/users/drinkcat/repos","events_url":"https://api.github.com/users/drinkcat/events{/privacy}","received_events_url":"https://api.github.com/users/drinkcat/received_events","type":"User","site_admin":false},"body":"Still a few issues to iron up, more testing, and a few missing features, but the general idea is there.\r\n\r\nKnown issues:\r\n - The extension does not start with focus (you need to click)\r\n - Touch events\r\n - IME events\r\n - \"Error target STRING not available\" sometimes appears during switch (croutonclip/cycle?)\r\n - Waiting for extension to connect before starting WM is potentially racy (the setres 1024 768 trick to provide xfce with a reasonable DPI fails as the extension starts too soon)... (actually, this is more of a xorg+dummy bug: we could get rid of that if dummy supported xrandr 1.2)\r\n - On stable channel (`6158.70.0`), the extension/Chrome sometimes crashes on disconnect (fairly frequently): clearly a Chrome bug. Never seen that problem on canary.\r\n\r\nFixed:\r\n - Window title should correspond to chroot name\r\n - A leftover FIXME in the code, we should force croutoncycle instead of removing files\r\n - Errors when loading NaCl module are not reported\r\n - Limit number of resolution changes when resizing (it's slow because we send far too many \"R\" requests)\r\n - Move the websocket functions into a common file","created_at":"2014-10-23T22:23:51Z","updated_at":"2014-11-05T17:56:58Z","closed_at":null,"merged_at":null,"merge_commit_sha":"ebd607eb16023629b212ce752ba8ac1cc9840fc4","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/dnschneid/crouton/pulls/1144/commits","review_comments_url":"https://api.github.com/repos/dnschneid/crouton/pulls/1144/comments","review_comment_url":"https://api.github.com/repos/dnschneid/crouton/pulls/comments/{number}","comments_url":"https://api.github.com/repos/dnschneid/crouton/issues/1144/comments","statuses_url":"https://api.github.com/repos/dnschneid/crouton/statuses/368341208f859dbe7be194ba07b4fa2fd83425c0","head":{"label":"drinkcat:criat","ref":"criat","sha":"368341208f859dbe7be194ba07b4fa2fd83425c0","user":{"login":"drinkcat","id":3467596,"avatar_url":"https://avatars.githubusercontent.com/u/3467596?v=2","gravatar_id":"","url":"https://api.github.com/users/drinkcat","html_url":"https://github.com/drinkcat","followers_url":"https://api.github.com/users/drinkcat/followers","following_url":"https://api.github.com/users/drinkcat/following{/other_user}","gists_url":"https://api.github.com/users/drinkcat/gists{/gist_id}","starred_url":"https://api.github.com/users/drinkcat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/drinkcat/subscriptions","organizations_url":"https://api.github.com/users/drinkcat/orgs","repos_url":"https://api.github.com/users/drinkcat/repos","events_url":"https://api.github.com/users/drinkcat/events{/privacy}","received_events_url":"https://api.github.com/users/drinkcat/received_events","type":"User","site_admin":false},"repo":{"id":9856262,"name":"chroagh","full_name":"drinkcat/chroagh","owner":{"login":"drinkcat","id":3467596,"avatar_url":"https://avatars.githubusercontent.com/u/3467596?v=2","gravatar_id":"","url":"https://api.github.com/users/drinkcat","html_url":"https://github.com/drinkcat","followers_url":"https://api.github.com/users/drinkcat/followers","following_url":"https://api.github.com/users/drinkcat/following{/other_user}","gists_url":"https://api.github.com/users/drinkcat/gists{/gist_id}","starred_url":"https://api.github.com/users/drinkcat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/drinkcat/subscriptions","organizations_url":"https://api.github.com/users/drinkcat/orgs","repos_url":"https://api.github.com/users/drinkcat/repos","events_url":"https://api.github.com/users/drinkcat/events{/privacy}","received_events_url":"https://api.github.com/users/drinkcat/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/drinkcat/chroagh","description":"Chromium OS Archlinux Chroot Environment","fork":true,"url":"https://api.github.com/repos/drinkcat/chroagh","forks_url":"https://api.github.com/repos/drinkcat/chroagh/forks","keys_url":"https://api.github.com/repos/drinkcat/chroagh/keys{/key_id}","collaborators_url":"https://api.github.com/repos/drinkcat/chroagh/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/drinkcat/chroagh/teams","hooks_url":"https://api.github.com/repos/drinkcat/chroagh/hooks","issue_events_url":"https://api.github.com/repos/drinkcat/chroagh/issues/events{/number}","events_url":"https://api.github.com/repos/drinkcat/chroagh/events","assignees_url":"https://api.github.com/repos/drinkcat/chroagh/assignees{/user}","branches_url":"https://api.github.com/repos/drinkcat/chroagh/branches{/branch}","tags_url":"https://api.github.com/repos/drinkcat/chroagh/tags","blobs_url":"https://api.github.com/repos/drinkcat/chroagh/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/drinkcat/chroagh/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/drinkcat/chroagh/git/refs{/sha}","trees_url":"https://api.github.com/repos/drinkcat/chroagh/git/trees{/sha}","statuses_url":"https://api.github.com/repos/drinkcat/chroagh/statuses/{sha}","languages_url":"https://api.github.com/repos/drinkcat/chroagh/languages","stargazers_url":"https://api.github.com/repos/drinkcat/chroagh/stargazers","contributors_url":"https://api.github.com/repos/drinkcat/chroagh/contributors","subscribers_url":"https://api.github.com/repos/drinkcat/chroagh/subscribers","subscription_url":"https://api.github.com/repos/drinkcat/chroagh/subscription","commits_url":"https://api.github.com/repos/drinkcat/chroagh/commits{/sha}","git_commits_url":"https://api.github.com/repos/drinkcat/chroagh/git/commits{/sha}","comments_url":"https://api.github.com/repos/drinkcat/chroagh/comments{/number}","issue_comment_url":"https://api.github.com/repos/drinkcat/chroagh/issues/comments/{number}","contents_url":"https://api.github.com/repos/drinkcat/chroagh/contents/{+path}","compare_url":"https://api.github.com/repos/drinkcat/chroagh/compare/{base}...{head}","merges_url":"https://api.github.com/repos/drinkcat/chroagh/merges","archive_url":"https://api.github.com/repos/drinkcat/chroagh/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/drinkcat/chroagh/downloads","issues_url":"https://api.github.com/repos/drinkcat/chroagh/issues{/number}","pulls_url":"https://api.github.com/repos/drinkcat/chroagh/pulls{/number}","milestones_url":"https://api.github.com/repos/drinkcat/chroagh/milestones{/number}","notifications_url":"https://api.github.com/repos/drinkcat/chroagh/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/drinkcat/chroagh/labels{/name}","releases_url":"https://api.github.com/repos/drinkcat/chroagh/releases{/id}","created_at":"2013-05-04T15:50:52Z","updated_at":"2014-10-29T22:05:11Z","pushed_at":"2014-11-05T02:01:46Z","git_url":"git://github.com/drinkcat/chroagh.git","ssh_url":"git@github.com:drinkcat/chroagh.git","clone_url":"https://github.com/drinkcat/chroagh.git","svn_url":"https://github.com/drinkcat/chroagh","homepage":"","size":10553,"stargazers_count":93,"watchers_count":93,"language":"Shell","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":9,"mirror_url":null,"open_issues_count":6,"forks":9,"open_issues":6,"watchers":93,"default_branch":"master"}},"base":{"label":"dnschneid:master","ref":"master","sha":"a9cb6230f9b60b7c4cb45d588c30ffd914355af9","user":{"login":"dnschneid","id":522200,"avatar_url":"https://avatars.githubusercontent.com/u/522200?v=2","gravatar_id":"","url":"https://api.github.com/users/dnschneid","html_url":"https://github.com/dnschneid","followers_url":"https://api.github.com/users/dnschneid/followers","following_url":"https://api.github.com/users/dnschneid/following{/other_user}","gists_url":"https://api.github.com/users/dnschneid/gists{/gist_id}","starred_url":"https://api.github.com/users/dnschneid/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dnschneid/subscriptions","organizations_url":"https://api.github.com/users/dnschneid/orgs","repos_url":"https://api.github.com/users/dnschneid/repos","events_url":"https://api.github.com/users/dnschneid/events{/privacy}","received_events_url":"https://api.github.com/users/dnschneid/received_events","type":"User","site_admin":false},"repo":{"id":7229063,"name":"crouton","full_name":"dnschneid/crouton","owner":{"login":"dnschneid","id":522200,"avatar_url":"https://avatars.githubusercontent.com/u/522200?v=2","gravatar_id":"","url":"https://api.github.com/users/dnschneid","html_url":"https://github.com/dnschneid","followers_url":"https://api.github.com/users/dnschneid/followers","following_url":"https://api.github.com/users/dnschneid/following{/other_user}","gists_url":"https://api.github.com/users/dnschneid/gists{/gist_id}","starred_url":"https://api.github.com/users/dnschneid/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dnschneid/subscriptions","organizations_url":"https://api.github.com/users/dnschneid/orgs","repos_url":"https://api.github.com/users/dnschneid/repos","events_url":"https://api.github.com/users/dnschneid/events{/privacy}","received_events_url":"https://api.github.com/users/dnschneid/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/dnschneid/crouton","description":"Chromium OS Universal Chroot Environment","fork":false,"url":"https://api.github.com/repos/dnschneid/crouton","forks_url":"https://api.github.com/repos/dnschneid/crouton/forks","keys_url":"https://api.github.com/repos/dnschneid/crouton/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dnschneid/crouton/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dnschneid/crouton/teams","hooks_url":"https://api.github.com/repos/dnschneid/crouton/hooks","issue_events_url":"https://api.github.com/repos/dnschneid/crouton/issues/events{/number}","events_url":"https://api.github.com/repos/dnschneid/crouton/events","assignees_url":"https://api.github.com/repos/dnschneid/crouton/assignees{/user}","branches_url":"https://api.github.com/repos/dnschneid/crouton/branches{/branch}","tags_url":"https://api.github.com/repos/dnschneid/crouton/tags","blobs_url":"https://api.github.com/repos/dnschneid/crouton/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dnschneid/crouton/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dnschneid/crouton/git/refs{/sha}","trees_url":"https://api.github.com/repos/dnschneid/crouton/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dnschneid/crouton/statuses/{sha}","languages_url":"https://api.github.com/repos/dnschneid/crouton/languages","stargazers_url":"https://api.github.com/repos/dnschneid/crouton/stargazers","contributors_url":"https://api.github.com/repos/dnschneid/crouton/contributors","subscribers_url":"https://api.github.com/repos/dnschneid/crouton/subscribers","subscription_url":"https://api.github.com/repos/dnschneid/crouton/subscription","commits_url":"https://api.github.com/repos/dnschneid/crouton/commits{/sha}","git_commits_url":"https://api.github.com/repos/dnschneid/crouton/git/commits{/sha}","comments_url":"https://api.github.com/repos/dnschneid/crouton/comments{/number}","issue_comment_url":"https://api.github.com/repos/dnschneid/crouton/issues/comments/{number}","contents_url":"https://api.github.com/repos/dnschneid/crouton/contents/{+path}","compare_url":"https://api.github.com/repos/dnschneid/crouton/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dnschneid/crouton/merges","archive_url":"https://api.github.com/repos/dnschneid/crouton/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dnschneid/crouton/downloads","issues_url":"https://api.github.com/repos/dnschneid/crouton/issues{/number}","pulls_url":"https://api.github.com/repos/dnschneid/crouton/pulls{/number}","milestones_url":"https://api.github.com/repos/dnschneid/crouton/milestones{/number}","notifications_url":"https://api.github.com/repos/dnschneid/crouton/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dnschneid/crouton/labels{/name}","releases_url":"https://api.github.com/repos/dnschneid/crouton/releases{/id}","created_at":"2012-12-18T19:31:14Z","updated_at":"2014-11-05T17:41:46Z","pushed_at":"2014-11-04T16:54:52Z","git_url":"git://github.com/dnschneid/crouton.git","ssh_url":"git@github.com:dnschneid/crouton.git","clone_url":"https://github.com/dnschneid/crouton.git","svn_url":"https://github.com/dnschneid/crouton","homepage":"https://goo.gl/fd3zc","size":14478,"stargazers_count":2102,"watchers_count":2102,"language":"Shell","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":228,"mirror_url":null,"open_issues_count":215,"forks":228,"open_issues":215,"watchers":2102,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dnschneid/crouton/pulls/1144"},"html":{"href":"https://github.com/dnschneid/crouton/pull/1144"},"issue":{"href":"https://api.github.com/repos/dnschneid/crouton/issues/1144"},"comments":{"href":"https://api.github.com/repos/dnschneid/crouton/issues/1144/comments"},"review_comments":{"href":"https://api.github.com/repos/dnschneid/crouton/pulls/1144/comments"},"review_comment":{"href":"https://api.github.com/repos/dnschneid/crouton/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/dnschneid/crouton/pulls/1144/commits"},"statuses":{"href":"https://api.github.com/repos/dnschneid/crouton/statuses/368341208f859dbe7be194ba07b4fa2fd83425c0"}}}}