{"action":"created","comment":{"url":"https://api.github.com/repos/mozilla/TUID/pulls/comments/200111828","pull_request_review_id":132899333,"id":200111828,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIwMDExMTgyOA==","diff_hunk":"@@ -1585,3 +1673,582 @@ def _daemon(self, please_stop, only_coverage_revisions=False):\n \n             if not ran_changesets:\n                 (please_stop | Till(seconds=DAEMON_WAIT_AT_NEWEST.seconds)).wait()\n+\n+\n+class Clogger:\n+    def __init__(self, conn=None, tuid_service=None, kwargs=None):\n+        try:\n+            self.config = kwargs\n+\n+            self.conn = conn if conn else sql.Sql(self.config.database.name)\n+            self.hg_cache = HgMozillaOrg(kwargs=self.config.hg_cache, use_cache=True) if self.config.hg_cache else Null\n+\n+            self.tuid_service = tuid_service if tuid_service else TUIDService(\n+                database=None, hg=None, kwargs=self.config, conn=self.conn, clogger=self\n+            )\n+            self.locker = Lock()\n+            self.tip_locker = Lock()\n+            self.backfill_locker = Lock()\n+            self.next_revnum = coalesce(self.conn.get_one(\"SELECT max(revnum)+1 FROM csetLog\")[0], 1)\n+            self.csets_todo_backwards = []\n+            self.deletions_todo = []\n+            self.at_tip = True\n+            self.config = self.config.tuid\n+\n+            # Make sure we are filled before allowing queries\n+            numrevs = self.conn.get_one(\"SELECT count(revnum) FROM csetLog\")[0]\n+            print(\"csetLog has \" + str(numrevs))\n+            if numrevs < MINIMUM_PERMANENT_CSETS:\n+                Log.note(\"Filling in csets to hold {{minim}} csets.\", minim=MINIMUM_PERMANENT_CSETS)\n+                oldest_rev = 'tip'\n+                with self.conn.transaction() as t:\n+                    tmp = t.query(\"SELECT min(revnum), revision FROM csetLog\").data[0][1]\n+                    if tmp:\n+                        oldest_rev = tmp\n+                self._fill_in_range(\n+                    MINIMUM_PERMANENT_CSETS - numrevs,\n+                    oldest_rev,\n+                    timestamp=False\n+                )\n+\n+            Log.note(\n+                \"Table is filled with atleast {{minim}} entries. Starting workers...\",\n+                minim=MINIMUM_PERMANENT_CSETS\n+            )\n+\n+            Thread.run('clogger-tip', self.fill_forward_continuous)\n+            Thread.run('clogger-backfill', self.fill_backward_with_list)\n+            Thread.run('clogger-maintenance', self.csetLog_maintenaner)\n+            Thread.run('clogger-deleter', self.csetLog_deleter)\n+\n+            Log.note(\"Started clogger workers.\")\n+        except Exception as e:\n+            Log.error(\"Can not setup clogger: {{cause}}\", cause=e)\n+\n+\n+    def revnum(self):\n+        \"\"\"\n+        :return: next tuid\n+        \"\"\"\n+        with self.locker:\n+            try:\n+                return self.next_revnum\n+            finally:\n+                self.next_revnum += 1\n+\n+\n+    def get_tip(self, transaction):\n+        return coalesce(transaction, self.conn).get_one(\n+            \"SELECT max(revnum) as revnum, revision FROM csetLog\"\n+        )\n+\n+\n+    def get_tail(self, transaction):\n+        return coalesce(transaction, self.conn).get_one(\n+            \"SELECT min(revnum) as revnum, revision FROM csetLog\"\n+        )\n+\n+    def _get_clog(self, clog_url):\n+        try:\n+            Log.note(\"Searching through changelog {{url}}\", url=clog_url)\n+            clog_obj = http.get_json(clog_url, retry=RETRY)\n+            if isinstance(clog_obj, (text_type, str)):\n+                Log.error(\n+                    \"Unexpected error getting clog for url: {{url}}\",\n+                    url=clog_url\n+                )\n+            return clog_obj\n+        except Exception as e:\n+            Log.error(\n+                \"Unexpected error getting changset-log for {{url}}: {{error}}\",\n+                url=clog_url,\n+                error=e\n+            )\n+\n+\n+    def _get_one_revision(self, transaction, cset_entry):\n+        # Returns a single revision if it exists\n+        _, rev, _ = cset_entry\n+        return transaction.get_one(\"SELECT revision FROM csetLog WHERE revision=?\", (rev,))\n+\n+\n+    def _get_one_revnum(self, transaction, rev):\n+        # Returns a single revnum if it exists\n+        return transaction.get_one(\"SELECT revnum FROM csetLog WHERE revision=?\", (rev,))\n+\n+\n+    def _get_revnum_range(self, transaction, revnum1, revnum2):\n+        # Returns a range of revision numbers (that is inclusive)\n+        high_num = revnum1\n+        low_num = revnum2\n+        if high_num < low_num:\n+            high_num = revnum2\n+            low_num = revnum1\n+\n+        return transaction.query(\n+            \"SELECT revnum, revision FROM csetLog WHERE revnum >= ? AND revnum <= ?\",\n+            (low_num, high_num)\n+        )\n+\n+\n+    def add_cset_entries(self, ordered_rev_list, timestamp=False, number_forward=True):\n+        '''\n+        Adds a list of revisions to the table. Assumes ordered_rev_list is an ordered\n+        based on how changesets are found in the changelog. Going forwards or backwards is dealt\n+        with by flipping the list\n+        :param ordered_cset_list: Order given from changeset log searching.\n+        :param timestamp: If false, records are kept indefinitely\n+                          but if holes exist: (delete, None, delete, None)\n+                          those delete's with None's around them\n+                          will not be deleted.\n+        :param numbered: If True, this function will number the revision list\n+                         by going forward from max(revNum), else it'll go backwards\n+                         from revNum, then add X to all revnums and self.next_revnum\n+                         where X is the length of ordered_rev_list\n+        :return:\n+        '''\n+        if number_forward:\n+            ordered_rev_list = ordered_rev_list[::-1]\n+\n+        insert_list = []\n+\n+        # Format insertion list\n+        for count, rev in enumerate(ordered_rev_list):\n+            tstamp = -1\n+            if timestamp:\n+                tstamp = int(time.time())\n+\n+            if number_forward:\n+                revnum = self.revnum()\n+            else:\n+                revnum = -count\n+\n+            cset_entry = (revnum, rev, tstamp)\n+            insert_list.append(cset_entry)\n+\n+        with self.conn.transaction() as t:\n+            # In case of overlapping requests\n+            fmt_insert_list = []\n+            for cset_entry in insert_list:\n+                tmp = self._get_one_revision(t, cset_entry)\n+                if not tmp:\n+                    fmt_insert_list.append(cset_entry)\n+\n+            for _, tmp_insert_list in jx.groupby(fmt_insert_list, size=SQL_CSET_BATCH_SIZE):\n+                t.execute(\n+                    \"INSERT INTO csetLog (revnum, revision, timestamp)\" +\n+                    \" VALUES \" +\n+                    sql_list(\n+                        sql_iso(sql_list(map(quote_value, (i[0], i[1], i[2])))) for i in tmp_insert_list\n+                    )\n+                )\n+\n+            # Move the revision numbers forward if needed\n+            if not number_forward:\n+                t.execute(\n+                    \"UPDATE csetLog SET revnum = revnum + \" + str(len(fmt_insert_list))\n+                )\n+\n+\n+    def _fill_in_range(self, parent_cset, child_cset, timestamp=False, number_forward=True):\n+        '''\n+        Fills cset logs in a certain range. 'parent_cset' can be an int and in that case,\n+        we get that many changesets instead. If parent_cset is an int, then we consider\n+        that we are going backwards (number_forward is False) and we ignore the first\n+        changeset of the first log, and we ignore the setting for number_forward.\n+        :param parent_cset:\n+        :param child_cset:\n+        :param timestamp:\n+        :param number_forward:\n+        :return:\n+        '''\n+        csets_to_add = []\n+        found_parent = False\n+        find_parent = False\n+        if type(parent_cset) != int:\n+            find_parent = True\n+\n+        csets_found = 0\n+        final_rev = child_cset\n+        while not found_parent:\n+            clog_url = HG_URL / self.config.hg.branch / 'json-log' / final_rev\n+            clog_obj = self._get_clog(clog_url)\n+            clog_csets_list = list(clog_obj['changesets'])\n+            for clog_cset in clog_csets_list[:-1]:\n+                if not number_forward and csets_found <= 0:\n+                    # Skip this entry it already exists\n+                    csets_found += 1\n+                    continue\n+\n+                nodes_cset = clog_cset['node'][:12]\n+                if find_parent:\n+                    if nodes_cset == parent_cset:\n+                        found_parent = True\n+                        if not number_forward:\n+                            # When going forward this entry is\n+                            # the given parent\n+                            csets_to_add.append(nodes_cset)\n+                        break\n+                else:\n+                    if csets_found + 1 > parent_cset:\n+                        found_parent = True\n+                        if not number_forward:\n+                            # When going forward this entry is\n+                            # the given parent (which is supposed\n+                            # to already exist)\n+                            csets_to_add.append(nodes_cset)\n+                        break\n+                    csets_found += 1\n+                csets_to_add.append(nodes_cset)\n+            if found_parent == True:\n+                break\n+            final_rev = clog_csets_list[-1]['node'][:12]\n+\n+        self.add_cset_entries(csets_to_add, timestamp=timestamp, number_forward=number_forward)\n+        return csets_to_add\n+\n+\n+    def fill_backward_with_list(self, please_stop=None):\n+        '''\n+        Expects requests of the tuple form: (parent_cset, timestamp)\n+        parent_cset can be an int X to go back by X changesets, or\n+        a string to search for going backwards in time. If timestamp\n+        is false, no timestamps will be added to the entries.\n+        :param please_stop:\n+        :return:\n+        '''\n+        try:\n+            while not please_stop:\n+                if len(self.csets_todo_backwards) <= 0:\n+                    (please_stop | Till(seconds=CSET_BACKFILL_WAIT_TIME)).wait()\n+                    continue\n+\n+                with self.backfill_locker:\n+                    done = []\n+                    for parent_cset, timestamp in self.csets_todo_backwards:\n+                        with self.conn.transaction() as t:\n+                            parent_revnum = self._get_one_revnum(t, parent_cset)\n+                        if parent_revnum:\n+                            done.append((parent_cset, timestamp))\n+                            continue\n+\n+                        with self.conn.transaction() as t:\n+                            oldest_revision = t.query(\"SELECT min(revNum), revision FROM csetLog\").data[0][1]\n+\n+                        with self.tip_locker:\n+                            self._fill_in_range(\n+                                parent_cset,\n+                                oldest_revision,\n+                                timestamp=timestamp,\n+                                number_forward=False\n+                            )\n+                        done.append((parent_cset,timestamp))\n+                        Log.note(\"Finished {{cset}}\", cset=parent_cset)\n+                    self.csets_todo_backwards = []\n+        except Exception as e:\n+            Log.error(\"Unknown error occurred during backfill: {{cause}}\", cause=e)\n+\n+\n+    def update_tip(self):\n+        '''\n+        Returns False if the tip is already at the newest, or True\n+        if an update has taken place.\n+        :return:\n+        '''\n+        clog_obj = self._get_clog(HG_URL / self.config.hg.branch / 'json-log' / 'tip')\n+\n+        # Get current tip in DB\n+        with self.conn.transaction() as t:\n+            newest_known_rev = t.query(\"SELECT max(revnum) AS revnum, revision FROM csetLog\").data[0][1]\n+\n+        # If we are still at the newest, wait for CSET_TIP_WAIT_TIME seconds\n+        # before checking again.\n+        first_clog_entry = clog_obj['changesets'][0]['node'][:12]\n+        if newest_known_rev == first_clog_entry:\n+            return False\n+\n+        with self.tip_locker:\n+            self.at_tip = False\n+            csets_to_gather = None\n+            if not newest_known_rev:\n+                Log.note(\n+                    \"No revisions found in table, adding {{minim}} entries...\",\n+                    minim=MINIMUM_PERMANENT_CSETS\n+                )\n+                csets_to_gather = MINIMUM_PERMANENT_CSETS\n+\n+            found_newest_known = False\n+            csets_to_add = []\n+            csets_found = 0\n+            Log.note(\"Found new revisions. Updating csetLog tip to {{rev}}...\", rev=first_clog_entry)\n+            while not found_newest_known:\n+                clog_csets_list = list(clog_obj['changesets'])\n+                for clog_cset in clog_csets_list[:-1]:\n+                    nodes_cset = clog_cset['node'][:12]\n+                    if not csets_to_gather:\n+                        if nodes_cset == newest_known_rev:\n+                            found_newest_known = True\n+                            break\n+                    else:\n+                        if csets_found >= csets_to_gather:\n+                            found_newest_known = True\n+                            break\n+                    csets_found += 1\n+                    csets_to_add.append(nodes_cset)\n+                if not found_newest_known:\n+                    # Get the next page\n+                    final_rev = clog_csets_list[-1]['node'][:12]\n+                    clog_url = HG_URL / self.config.hg.branch / 'json-log' / final_rev\n+                    clog_obj = self._get_clog(clog_url)\n+\n+            Log.note(\"Adding {{csets}}\", csets=str(csets_to_add))\n+            self.add_cset_entries(csets_to_add, timestamp=False)\n+            self.at_tip = True\n+        return True\n+\n+\n+    def fill_forward_continuous(self, please_stop=None):\n+        try:\n+            while not please_stop:\n+                did_an_update = self.update_tip()\n+                if not did_an_update:\n+                    (please_stop | Till(seconds=CSET_TIP_WAIT_TIME)).wait()\n+                    continue\n+\n+        except Exception as e:\n+            Log.error(\"Unknown error occurred during tip maintenance: {{cause}}\", cause=e)\n+\n+\n+    def csetLog_maintenaner(self, please_stop=None):\n+        '''\n+        Handles deleting old csetLog entries and timestamping\n+        revisions once they pass the length for permanent\n+        storage for deletion later.\n+        :param please_stop:\n+        :return:\n+        '''\n+        try:\n+            while not please_stop:\n+                # Wait a bit for maintenance cycle to begin\n+                (Till(seconds=CSET_MAINTENANCE_WAIT_TIME)).wait()\n+                if len(self.deletions_todo) > 0:\n+                    continue\n+\n+                with self.tip_locker and self.backfill_locker:\n+                    all_data = None\n+                    with self.conn.transaction() as t:\n+                        all_data = sorted(\n+                            t.get(\"SELECT revnum, revision, timestamp FROM csetLog\"),\n+                            key=lambda x: int(x[0])\n+                        )\n+\n+                    # Restore maximum permanents (if overflowing)\n+                    new_data = []\n+                    modified = False\n+                    for count, (revnum, revision, timestamp) in enumerate(all_data[::-1]):\n+                        if count < MINIMUM_PERMANENT_CSETS:\n+                            if timestamp != -1:\n+                                modified = True\n+                                new_data.append((revnum, revision, -1))\n+                            else:\n+                                new_data.append((revnum, revision, timestamp))\n+                        elif type(timestamp) != int:\n+                            modified = True\n+                            new_data.append((revnum, revision, int(time.time())))\n+                        else:\n+                            new_data.append((revnum, revision, timestamp))\n+\n+                    # Delete any overflowing entries\n+                    new_data2 = new_data\n+                    deleted_data = all_data[:len(all_data) - MAXIMUM_NONPERMANENT_CSETS]\n+                    delete_overflowing_revstart = None\n+                    if len(deleted_data) > 0:\n+                        _, delete_overflowing_revstart, _ = deleted_data[-1]\n+                        new_data2 = set(all_data) - set(deleted_data)\n+\n+                        # Update old frontiers if requested, otherwise\n+                        # they will all get deleted by the csetLog_deleter\n+                        # worker\n+                        if UPDATE_VERY_OLD_FRONTIERS:\n+                            _, max_revision, _ = all_data[-1]\n+                            for _, revision, _ in deleted_data:\n+                                with self.conn.transaction() as t:\n+                                    old_files = t.get(\n+                                        \"SELECT file FROM latestFileMod WHERE revision=?\",\n+                                        (revision,)\n+                                    )\n+                                if old_files is None or len(old_files) <= 0:\n+                                    continue\n+\n+                                self.tuid_service.get_tuids_from_files(\n+                                    old_files, max_revision, going_forward=True,\n+                                )\n+\n+                                still_exist = True\n+                                while still_exist:\n+                                    (Till(seconds=TUID_EXISTENCE_WAIT_TIME)).wait()\n+                                    with self.conn.transaction() as t:\n+                                        old_files = t.get(\n+                                            \"SELECT file FROM latestFileMod WHERE revision=?\",\n+                                            (revision,)\n+                                        )\n+                                    if old_files is None or len(old_files) <= 0:\n+                                        still_exist = False\n+\n+                    # Update table and schedule a deletion\n+                    if modified:\n+                        with self.conn.transaction() as t:\n+                            t.execute(\n+                                \"INSERT OR REPLACE INTO csetLog (revnum, revision, timestamp) VALUES \" +\n+                                sql_list(\n+                                    sql_iso(sql_list(map(quote_value, cset_entry)))\n+                                    for cset_entry in new_data2\n+                                )\n+                            )\n+                    if len(deleted_data) <= 0:\n+                        continue\n+\n+                    Log.note(\"Scheduling {{num_csets}} for deletion\", num_csets=str(len(deleted_data)))\n+                    self.deletions_todo.append(delete_overflowing_revstart)\n+        except Exception as e:\n+            Log.error(\"Unexpected error occured while maintaining csetLog: {{cause}}\", cause=e)\n+        return\n+\n+\n+    def csetLog_deleter(self, please_stop=None):\n+        '''\n+        Deletes changesets from the csetLog table\n+        and also changesets from the annotation table\n+        that have revisions matching the given changesets.\n+        Accepts lists of csets from self.deletions_todo.\n+        :param please_stop:\n+        :return:\n+        '''\n+        try:\n+            while not please_stop:\n+                if len(self.deletions_todo) <= 0:\n+                    Log.note(\n+                        \"Did not find any deletion requests, waiting for {{secs}} to check again.\",\n+                        secs=str(CSET_DELETION_WAIT_TIME)\n+                    )\n+                    (Till(seconds=CSET_DELETION_WAIT_TIME)).wait()\n+                    continue\n+\n+                Log.note(\"Waiting for locks...\")\n+                with self.tip_locker and self.backfill_locker:\n+                    Log.note(\"Locks acquired.\")\n+                    tmp_deletions = self.deletions_todo\n+                    for first_cset in tmp_deletions:\n+                        with self.conn.transaction() as t:\n+                            revnum = self._get_one_revnum(t, first_cset)[0]\n+                            print(\"revnum:\" + str(revnum))\n+                            csets_to_del = t.get(\n+                                \"SELECT revnum, revision FROM csetLog WHERE revnum <= ?\", (revnum,)\n+                            )\n+                            csets_to_del = [cset for _, cset in csets_to_del]\n+                            print(csets_to_del)\n+                            existing_frontiers = t.query(\n+                                \"SELECT revision FROM latestFileMod WHERE revision IN \" +\n+                                sql_iso(sql_list(map(quote_value, csets_to_del)))\n+                            ).data\n+                        del t # If we don't delete, it'll cause problems for future queries\n+\n+                        existing_frontiers = [existing_frontiers[i][0] for i, _ in enumerate(existing_frontiers)]\n+\n+                        Log.note(\n+                            \"Deleting all annotations and changeset log entries with revisions in the list: {{csets}}\",\n+                            csets=str(csets_to_del)","path":"tuid/service.py","position":null,"original_position":670,"commit_id":"383546386fc35749682189c229a91512bfe4f86c","original_commit_id":"b69db7059f105a96e57e1367d5acc7292ed63dfe","user":{"login":"klahnakoski","id":2334429,"node_id":"MDQ6VXNlcjIzMzQ0Mjk=","avatar_url":"https://avatars3.githubusercontent.com/u/2334429?v=4","gravatar_id":"","url":"https://api.github.com/users/klahnakoski","html_url":"https://github.com/klahnakoski","followers_url":"https://api.github.com/users/klahnakoski/followers","following_url":"https://api.github.com/users/klahnakoski/following{/other_user}","gists_url":"https://api.github.com/users/klahnakoski/gists{/gist_id}","starred_url":"https://api.github.com/users/klahnakoski/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/klahnakoski/subscriptions","organizations_url":"https://api.github.com/users/klahnakoski/orgs","repos_url":"https://api.github.com/users/klahnakoski/repos","events_url":"https://api.github.com/users/klahnakoski/events{/privacy}","received_events_url":"https://api.github.com/users/klahnakoski/received_events","type":"User","site_admin":false},"body":"you do not need the `str()` call","created_at":"2018-07-04T12:31:49Z","updated_at":"2018-07-04T13:01:18Z","html_url":"https://github.com/mozilla/TUID/pull/47#discussion_r200111828","pull_request_url":"https://api.github.com/repos/mozilla/TUID/pulls/47","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/mozilla/TUID/pulls/comments/200111828"},"html":{"href":"https://github.com/mozilla/TUID/pull/47#discussion_r200111828"},"pull_request":{"href":"https://api.github.com/repos/mozilla/TUID/pulls/47"}}},"pull_request":{"url":"https://api.github.com/repos/mozilla/TUID/pulls/47","id":198068979,"node_id":"MDExOlB1bGxSZXF1ZXN0MTk4MDY4OTc5","html_url":"https://github.com/mozilla/TUID/pull/47","diff_url":"https://github.com/mozilla/TUID/pull/47.diff","patch_url":"https://github.com/mozilla/TUID/pull/47.patch","issue_url":"https://api.github.com/repos/mozilla/TUID/issues/47","number":47,"state":"open","locked":false,"title":"Clogger - Review!","user":{"login":"klahnakoski","id":2334429,"node_id":"MDQ6VXNlcjIzMzQ0Mjk=","avatar_url":"https://avatars3.githubusercontent.com/u/2334429?v=4","gravatar_id":"","url":"https://api.github.com/users/klahnakoski","html_url":"https://github.com/klahnakoski","followers_url":"https://api.github.com/users/klahnakoski/followers","following_url":"https://api.github.com/users/klahnakoski/following{/other_user}","gists_url":"https://api.github.com/users/klahnakoski/gists{/gist_id}","starred_url":"https://api.github.com/users/klahnakoski/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/klahnakoski/subscriptions","organizations_url":"https://api.github.com/users/klahnakoski/orgs","repos_url":"https://api.github.com/users/klahnakoski/repos","events_url":"https://api.github.com/users/klahnakoski/events{/privacy}","received_events_url":"https://api.github.com/users/klahnakoski/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-06-28T15:30:06Z","updated_at":"2018-07-04T13:01:18Z","closed_at":null,"merged_at":null,"merge_commit_sha":"302b9f07c3cc0cfbcf19f168f415d2a77f5a641c","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/mozilla/TUID/pulls/47/commits","review_comments_url":"https://api.github.com/repos/mozilla/TUID/pulls/47/comments","review_comment_url":"https://api.github.com/repos/mozilla/TUID/pulls/comments{/number}","comments_url":"https://api.github.com/repos/mozilla/TUID/issues/47/comments","statuses_url":"https://api.github.com/repos/mozilla/TUID/statuses/383546386fc35749682189c229a91512bfe4f86c","head":{"label":"mozilla:clogger","ref":"clogger","sha":"383546386fc35749682189c229a91512bfe4f86c","user":{"login":"mozilla","id":131524,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzMTUyNA==","avatar_url":"https://avatars2.githubusercontent.com/u/131524?v=4","gravatar_id":"","url":"https://api.github.com/users/mozilla","html_url":"https://github.com/mozilla","followers_url":"https://api.github.com/users/mozilla/followers","following_url":"https://api.github.com/users/mozilla/following{/other_user}","gists_url":"https://api.github.com/users/mozilla/gists{/gist_id}","starred_url":"https://api.github.com/users/mozilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozilla/subscriptions","organizations_url":"https://api.github.com/users/mozilla/orgs","repos_url":"https://api.github.com/users/mozilla/repos","events_url":"https://api.github.com/users/mozilla/events{/privacy}","received_events_url":"https://api.github.com/users/mozilla/received_events","type":"Organization","site_admin":false},"repo":{"id":104648493,"node_id":"MDEwOlJlcG9zaXRvcnkxMDQ2NDg0OTM=","name":"TUID","full_name":"mozilla/TUID","owner":{"login":"mozilla","id":131524,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzMTUyNA==","avatar_url":"https://avatars2.githubusercontent.com/u/131524?v=4","gravatar_id":"","url":"https://api.github.com/users/mozilla","html_url":"https://github.com/mozilla","followers_url":"https://api.github.com/users/mozilla/followers","following_url":"https://api.github.com/users/mozilla/following{/other_user}","gists_url":"https://api.github.com/users/mozilla/gists{/gist_id}","starred_url":"https://api.github.com/users/mozilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozilla/subscriptions","organizations_url":"https://api.github.com/users/mozilla/orgs","repos_url":"https://api.github.com/users/mozilla/repos","events_url":"https://api.github.com/users/mozilla/events{/privacy}","received_events_url":"https://api.github.com/users/mozilla/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mozilla/TUID","description":null,"fork":false,"url":"https://api.github.com/repos/mozilla/TUID","forks_url":"https://api.github.com/repos/mozilla/TUID/forks","keys_url":"https://api.github.com/repos/mozilla/TUID/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mozilla/TUID/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mozilla/TUID/teams","hooks_url":"https://api.github.com/repos/mozilla/TUID/hooks","issue_events_url":"https://api.github.com/repos/mozilla/TUID/issues/events{/number}","events_url":"https://api.github.com/repos/mozilla/TUID/events","assignees_url":"https://api.github.com/repos/mozilla/TUID/assignees{/user}","branches_url":"https://api.github.com/repos/mozilla/TUID/branches{/branch}","tags_url":"https://api.github.com/repos/mozilla/TUID/tags","blobs_url":"https://api.github.com/repos/mozilla/TUID/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mozilla/TUID/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mozilla/TUID/git/refs{/sha}","trees_url":"https://api.github.com/repos/mozilla/TUID/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mozilla/TUID/statuses/{sha}","languages_url":"https://api.github.com/repos/mozilla/TUID/languages","stargazers_url":"https://api.github.com/repos/mozilla/TUID/stargazers","contributors_url":"https://api.github.com/repos/mozilla/TUID/contributors","subscribers_url":"https://api.github.com/repos/mozilla/TUID/subscribers","subscription_url":"https://api.github.com/repos/mozilla/TUID/subscription","commits_url":"https://api.github.com/repos/mozilla/TUID/commits{/sha}","git_commits_url":"https://api.github.com/repos/mozilla/TUID/git/commits{/sha}","comments_url":"https://api.github.com/repos/mozilla/TUID/comments{/number}","issue_comment_url":"https://api.github.com/repos/mozilla/TUID/issues/comments{/number}","contents_url":"https://api.github.com/repos/mozilla/TUID/contents/{+path}","compare_url":"https://api.github.com/repos/mozilla/TUID/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mozilla/TUID/merges","archive_url":"https://api.github.com/repos/mozilla/TUID/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mozilla/TUID/downloads","issues_url":"https://api.github.com/repos/mozilla/TUID/issues{/number}","pulls_url":"https://api.github.com/repos/mozilla/TUID/pulls{/number}","milestones_url":"https://api.github.com/repos/mozilla/TUID/milestones{/number}","notifications_url":"https://api.github.com/repos/mozilla/TUID/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mozilla/TUID/labels{/name}","releases_url":"https://api.github.com/repos/mozilla/TUID/releases{/id}","deployments_url":"https://api.github.com/repos/mozilla/TUID/deployments","created_at":"2017-09-24T14:05:05Z","updated_at":"2018-07-03T19:31:57Z","pushed_at":"2018-07-03T20:43:06Z","git_url":"git://github.com/mozilla/TUID.git","ssh_url":"git@github.com:mozilla/TUID.git","clone_url":"https://github.com/mozilla/TUID.git","svn_url":"https://github.com/mozilla/TUID","homepage":null,"size":8179,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"archived":false,"open_issues_count":12,"license":null,"forks":3,"open_issues":12,"watchers":2,"default_branch":"dev"}},"base":{"label":"mozilla:dev","ref":"dev","sha":"1191b870aa83c14eca41ac71d7ebd2ab2cf8f93c","user":{"login":"mozilla","id":131524,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzMTUyNA==","avatar_url":"https://avatars2.githubusercontent.com/u/131524?v=4","gravatar_id":"","url":"https://api.github.com/users/mozilla","html_url":"https://github.com/mozilla","followers_url":"https://api.github.com/users/mozilla/followers","following_url":"https://api.github.com/users/mozilla/following{/other_user}","gists_url":"https://api.github.com/users/mozilla/gists{/gist_id}","starred_url":"https://api.github.com/users/mozilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozilla/subscriptions","organizations_url":"https://api.github.com/users/mozilla/orgs","repos_url":"https://api.github.com/users/mozilla/repos","events_url":"https://api.github.com/users/mozilla/events{/privacy}","received_events_url":"https://api.github.com/users/mozilla/received_events","type":"Organization","site_admin":false},"repo":{"id":104648493,"node_id":"MDEwOlJlcG9zaXRvcnkxMDQ2NDg0OTM=","name":"TUID","full_name":"mozilla/TUID","owner":{"login":"mozilla","id":131524,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzMTUyNA==","avatar_url":"https://avatars2.githubusercontent.com/u/131524?v=4","gravatar_id":"","url":"https://api.github.com/users/mozilla","html_url":"https://github.com/mozilla","followers_url":"https://api.github.com/users/mozilla/followers","following_url":"https://api.github.com/users/mozilla/following{/other_user}","gists_url":"https://api.github.com/users/mozilla/gists{/gist_id}","starred_url":"https://api.github.com/users/mozilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozilla/subscriptions","organizations_url":"https://api.github.com/users/mozilla/orgs","repos_url":"https://api.github.com/users/mozilla/repos","events_url":"https://api.github.com/users/mozilla/events{/privacy}","received_events_url":"https://api.github.com/users/mozilla/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mozilla/TUID","description":null,"fork":false,"url":"https://api.github.com/repos/mozilla/TUID","forks_url":"https://api.github.com/repos/mozilla/TUID/forks","keys_url":"https://api.github.com/repos/mozilla/TUID/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mozilla/TUID/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mozilla/TUID/teams","hooks_url":"https://api.github.com/repos/mozilla/TUID/hooks","issue_events_url":"https://api.github.com/repos/mozilla/TUID/issues/events{/number}","events_url":"https://api.github.com/repos/mozilla/TUID/events","assignees_url":"https://api.github.com/repos/mozilla/TUID/assignees{/user}","branches_url":"https://api.github.com/repos/mozilla/TUID/branches{/branch}","tags_url":"https://api.github.com/repos/mozilla/TUID/tags","blobs_url":"https://api.github.com/repos/mozilla/TUID/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mozilla/TUID/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mozilla/TUID/git/refs{/sha}","trees_url":"https://api.github.com/repos/mozilla/TUID/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mozilla/TUID/statuses/{sha}","languages_url":"https://api.github.com/repos/mozilla/TUID/languages","stargazers_url":"https://api.github.com/repos/mozilla/TUID/stargazers","contributors_url":"https://api.github.com/repos/mozilla/TUID/contributors","subscribers_url":"https://api.github.com/repos/mozilla/TUID/subscribers","subscription_url":"https://api.github.com/repos/mozilla/TUID/subscription","commits_url":"https://api.github.com/repos/mozilla/TUID/commits{/sha}","git_commits_url":"https://api.github.com/repos/mozilla/TUID/git/commits{/sha}","comments_url":"https://api.github.com/repos/mozilla/TUID/comments{/number}","issue_comment_url":"https://api.github.com/repos/mozilla/TUID/issues/comments{/number}","contents_url":"https://api.github.com/repos/mozilla/TUID/contents/{+path}","compare_url":"https://api.github.com/repos/mozilla/TUID/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mozilla/TUID/merges","archive_url":"https://api.github.com/repos/mozilla/TUID/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mozilla/TUID/downloads","issues_url":"https://api.github.com/repos/mozilla/TUID/issues{/number}","pulls_url":"https://api.github.com/repos/mozilla/TUID/pulls{/number}","milestones_url":"https://api.github.com/repos/mozilla/TUID/milestones{/number}","notifications_url":"https://api.github.com/repos/mozilla/TUID/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mozilla/TUID/labels{/name}","releases_url":"https://api.github.com/repos/mozilla/TUID/releases{/id}","deployments_url":"https://api.github.com/repos/mozilla/TUID/deployments","created_at":"2017-09-24T14:05:05Z","updated_at":"2018-07-03T19:31:57Z","pushed_at":"2018-07-03T20:43:06Z","git_url":"git://github.com/mozilla/TUID.git","ssh_url":"git@github.com:mozilla/TUID.git","clone_url":"https://github.com/mozilla/TUID.git","svn_url":"https://github.com/mozilla/TUID","homepage":null,"size":8179,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"archived":false,"open_issues_count":12,"license":null,"forks":3,"open_issues":12,"watchers":2,"default_branch":"dev"}},"_links":{"self":{"href":"https://api.github.com/repos/mozilla/TUID/pulls/47"},"html":{"href":"https://github.com/mozilla/TUID/pull/47"},"issue":{"href":"https://api.github.com/repos/mozilla/TUID/issues/47"},"comments":{"href":"https://api.github.com/repos/mozilla/TUID/issues/47/comments"},"review_comments":{"href":"https://api.github.com/repos/mozilla/TUID/pulls/47/comments"},"review_comment":{"href":"https://api.github.com/repos/mozilla/TUID/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/mozilla/TUID/pulls/47/commits"},"statuses":{"href":"https://api.github.com/repos/mozilla/TUID/statuses/383546386fc35749682189c229a91512bfe4f86c"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"klahnakoski"}}