{"action":"created","comment":{"url":"https://api.github.com/repos/certbot/certbot/pulls/comments/241962955","pull_request_review_id":185369519,"id":241962955,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI0MTk2Mjk1NQ==","diff_hunk":"@@ -0,0 +1,334 @@\n+\"\"\"\n+Library to lock a directory or a file using a lockfile.\n+This implementation heavily relies on the work of benediktschmitt\n+(https://github.com/benediktschmitt/py-filelock)\n+and should be considered as a fork of his project.\n+\"\"\"\n+import atexit\n+import logging\n+import os\n+import threading\n+\n+try:\n+    import msvcrt\n+except ImportError:\n+    msvcrt = None  # type: ignore\n+\n+try:\n+    import fcntl\n+except ImportError:\n+    fcntl = None  # type: ignore\n+\n+from certbot import errors\n+from acme.magic_typing import List  # pylint: disable=unused-import, no-name-in-module\n+\n+logger = logging.getLogger(__name__)\n+\n+# Handling exit\n+# ~~~~~~~~~~~~~\n+_INITIAL_PID = os.getpid()\n+_LOCKS = []  # type: List[BaseFileLock]\n+\n+\n+def _release_all_locks():\n+    \"\"\"Release all locks acquired by FileLock.\"\"\"\n+    if _INITIAL_PID == os.getpid():\n+        for lock in _LOCKS:\n+            if lock.is_locked:\n+                try:\n+                    lock.release()\n+                    logger.debug('Lock released: %s', lock.lock_file)\n+                except (OSError, IOError):  # pragma: no cover\n+                    logger.error('Exception occurred releasing lock: %s',\n+                                 lock.lock_file, exc_info=True)\n+\n+\n+# Every lock is released at least when Certbot exit.\n+atexit.register(_release_all_locks)\n+\n+# Classes\n+# ------------------------------------------------\n+\n+\n+class BaseFileLock(object):\n+    \"\"\"\n+    Implements the base class of a file lock.\n+    \"\"\"\n+\n+    def __init__(self, lock_file):\n+        \"\"\"\n+        Create a new Lock. Note that this lock is set to be release when program exit.\n+        \"\"\"\n+        # The path to the lock file.\n+        self._lock_file = lock_file\n+\n+        # The file descriptor for the *_lock_file* as it is returned by the\n+        # os.open() function.\n+        # This file lock is only NOT None, if the object currently holds the\n+        # lock.\n+        self._lock_file_fd = None\n+\n+        # We use this lock primarily for the lock counter.\n+        self._thread_lock = threading.Lock()\n+\n+        # The lock counter is used for implementing the nested locking\n+        # mechanism. Whenever the lock is acquired, the counter is increased and\n+        # the lock is only released, when this value is 0 again.\n+        self._lock_counter = 0\n+\n+        # Pass itelf to the _LOCK global variable to ensure that all locks are\n+        # released at least when the program exit.\n+        _LOCKS.append(self)\n+\n+    @property\n+    def lock_file(self):\n+        \"\"\"\n+        The path to the lock file.\n+        \"\"\"\n+        return self._lock_file\n+\n+    # Platform dependent locking\n+    # --------------------------------------------\n+\n+    def _acquire(self):\n+        \"\"\"\n+        Platform dependent. If the file lock could be\n+        acquired, self._lock_file_fd holds the file descriptor\n+        of the lock file.\n+        \"\"\"\n+        raise NotImplementedError()  # pragma: no cover\n+\n+    def _release(self):\n+        \"\"\"\n+        Releases the lock and sets self._lock_file_fd to None.\n+        \"\"\"\n+        raise NotImplementedError()  # pragma: no cover\n+\n+    # Platform independent methods\n+    # --------------------------------------------\n+\n+    @property\n+    def is_locked(self):\n+        \"\"\"\n+        True, if the object holds the file lock.\n+        .. versionchanged:: 2.0.0\n+            This was previously a method and is now a property.\n+        \"\"\"\n+        return self._lock_file_fd is not None\n+\n+    def acquire(self):\n+        \"\"\"\n+        Acquires the file lock or fails with a :exc:`Timeout` error.\n+        .. code-block:: python\n+            # You can use this method in the context manager (recommended)\n+            with lock.acquire():\n+                pass\n+            # Or use an equivalent try-finally construct:\n+            lock.acquire()\n+            try:\n+                pass\n+            finally:\n+                lock.release()\n+        \"\"\"\n+        # Increment the number right at the beginning.\n+        # We can still undo it, if something fails.\n+        with self._thread_lock:\n+            self._lock_counter += 1\n+\n+        lock_id = id(self)\n+        lock_filename = self._lock_file\n+\n+        try:\n+            with self._thread_lock:\n+                if not self.is_locked:\n+                    logger.debug('Attempting to acquire lock %s on %s', lock_id, lock_filename)\n+                    self._acquire()\n+\n+            if self.is_locked:\n+                logger.info('Lock %s acquired on %s', lock_id, lock_filename)\n+            else:\n+                self._raise_for_certbot_lock()\n+        except (OSError, IOError):  # pragma: no cover\n+            # Something did go wrong, so decrement the counter.\n+            with self._thread_lock:\n+                self._lock_counter = max(0, self._lock_counter - 1)\n+\n+            raise\n+\n+        class ReturnProxy(object):\n+            \"\"\"\n+            This class wraps the lock to make sure __enter__ is not called\n+            twice when entering the with statement.\n+            If we would simply return *self*, the lock would be acquired again\n+            in the *__enter__* method of the BaseFileLock, but not released again\n+            automatically.\n+            \"\"\"\n+            def __init__(self, lock):\n+                self.lock = lock\n+\n+            def __enter__(self):\n+                return self.lock\n+\n+            def __exit__(self, exc_type, exc_value, traceback):\n+                self.lock.release()\n+                return None\n+\n+        return ReturnProxy(lock=self)\n+\n+    def release(self, force=False):\n+        \"\"\"\n+        Releases the file lock.\n+        Please note, that the lock is only completely released, if the lock\n+        counter is 0.\n+        Also note, that the lock file itself is not automatically deleted.\n+        :arg bool force:\n+            If true, the lock counter is ignored and the lock is released in\n+            every case.\n+        \"\"\"\n+        with self._thread_lock:\n+\n+            if self.is_locked:\n+                self._lock_counter -= 1\n+\n+                if self._lock_counter == 0 or force:\n+                    lock_id = id(self)\n+                    lock_filename = self._lock_file\n+\n+                    logger.debug('Attempting to release lock %s on %s', lock_id, lock_filename)\n+                    self._release()\n+                    self._lock_counter = 0\n+                    logger.info('Lock %s released on %s', lock_id, lock_filename)\n+\n+        return None\n+\n+    def _raise_for_certbot_lock(self):\n+        raise errors.LockError((\n+            'Error, the filelock \"{0}\" could not be acquired. '\n+            'It is likely that another Certbot instance is still running.'\n+            .format(self.lock_file)))\n+\n+    def __enter__(self):\n+        self.acquire()\n+        return self\n+\n+    def __exit__(self, exc_type, exc_value, traceback):\n+        self.release()\n+        return None\n+\n+    def __del__(self):\n+        self.release(force=True)\n+        return None\n+\n+# Windows locking mechanism\n+# ~~~~~~~~~~~~~~~~~~~~~~~~~\n+\n+\n+class WindowsFileLock(BaseFileLock):\n+    \"\"\"\n+    Uses the :func:`msvcrt.locking` function to hard lock the lock file on\n+    windows systems.\n+    \"\"\"\n+\n+    def _acquire(self):\n+        open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC\n+\n+        try:\n+            fd = os.open(self._lock_file, open_mode)\n+        except OSError:\n+            pass","path":"certbot/filelock.py","position":null,"original_position":238,"commit_id":"a655e466213933f8ad1efd8aa09838d2573aa108","original_commit_id":"191a8a92751581de96ebbafa6ed4901b89e53c6d","user":{"login":"adferrand","id":9728851,"node_id":"MDQ6VXNlcjk3Mjg4NTE=","avatar_url":"https://avatars0.githubusercontent.com/u/9728851?v=4","gravatar_id":"","url":"https://api.github.com/users/adferrand","html_url":"https://github.com/adferrand","followers_url":"https://api.github.com/users/adferrand/followers","following_url":"https://api.github.com/users/adferrand/following{/other_user}","gists_url":"https://api.github.com/users/adferrand/gists{/gist_id}","starred_url":"https://api.github.com/users/adferrand/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/adferrand/subscriptions","organizations_url":"https://api.github.com/users/adferrand/orgs","repos_url":"https://api.github.com/users/adferrand/repos","events_url":"https://api.github.com/users/adferrand/events{/privacy}","received_events_url":"https://api.github.com/users/adferrand/received_events","type":"User","site_admin":false},"body":"Not relevant anymore with the refactoring.","created_at":"2018-12-15T22:48:38Z","updated_at":"2018-12-15T22:48:38Z","html_url":"https://github.com/certbot/certbot/pull/6440#discussion_r241962955","pull_request_url":"https://api.github.com/repos/certbot/certbot/pulls/6440","author_association":"COLLABORATOR","_links":{"self":{"href":"https://api.github.com/repos/certbot/certbot/pulls/comments/241962955"},"html":{"href":"https://github.com/certbot/certbot/pull/6440#discussion_r241962955"},"pull_request":{"href":"https://api.github.com/repos/certbot/certbot/pulls/6440"}},"in_reply_to_id":239978125},"pull_request":{"url":"https://api.github.com/repos/certbot/certbot/pulls/6440","id":225462413,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI1NDYyNDEz","html_url":"https://github.com/certbot/certbot/pull/6440","diff_url":"https://github.com/certbot/certbot/pull/6440.diff","patch_url":"https://github.com/certbot/certbot/pull/6440.patch","issue_url":"https://api.github.com/repos/certbot/certbot/issues/6440","number":6440,"state":"open","locked":false,"title":"[Windows|Unix] New platform independent locking mechanism","user":{"login":"adferrand","id":9728851,"node_id":"MDQ6VXNlcjk3Mjg4NTE=","avatar_url":"https://avatars0.githubusercontent.com/u/9728851?v=4","gravatar_id":"","url":"https://api.github.com/users/adferrand","html_url":"https://github.com/adferrand","followers_url":"https://api.github.com/users/adferrand/followers","following_url":"https://api.github.com/users/adferrand/following{/other_user}","gists_url":"https://api.github.com/users/adferrand/gists{/gist_id}","starred_url":"https://api.github.com/users/adferrand/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/adferrand/subscriptions","organizations_url":"https://api.github.com/users/adferrand/orgs","repos_url":"https://api.github.com/users/adferrand/repos","events_url":"https://api.github.com/users/adferrand/events{/privacy}","received_events_url":"https://api.github.com/users/adferrand/received_events","type":"User","site_admin":false},"body":"Certbot coding design is influenced by the pros and the cons of Unix-like systems. This is particularly noticeable for the files resource handling.\r\n\r\nOn Linux, you usually do not really care about how you acquire and release file descriptors on the filesystem, because the constraints are quite permissive, and the kernel does a lot of things for you. In particular, obtaining an access to a file is non-blocking by default if no other strategy is specified. This means that you can read and write on the same file from different processes. You will need to handle carefully the concurrent access, but it is possible out-of-the-box. More than that, filesystem operations are atomic. For instance, a file can be renamed (including moving its location) while there is an opened file descriptor, because the filesystem will maintain a valid pointer to it until the resource is released. And of course you can delete files without any effect to the existing processes that operate on them. \r\n\r\nOn top of that, Python does a smart cleaning: file descriptors are quietly closed and released with the end of the process, so that resource leaks, at least in the point of view of file descriptors, are very unlikely.\r\n\r\nAll of that to say that there is almost no fine resource handling on files in Certbot codebase, because it was not needed. The only thing, in fact, is that these relaxed constraints are a problem for Certbot, because by essence it is a program that operates on files, and it needs to handle concurrent access very strictly. So the current file lock mechanism does that, by creating files with blocking access along to the resources (files or directories) that Certbot want to access exclusively (at least for other Certbot instances, other programs can just ignore that if they want).\r\n\r\nEnter Windows.\r\n\r\nForget all of that, file handling in Windows is a journey in the 7th circle of Hell. First, everything is blocking. Opening a file is also posing a lock on it by default, for writting AND reading. Quite funny, if Certbot was built on Windows, its custom file lock logic would simply not exist, as opening a file IS creating a lock on it. You can make non-blocking access in theory, but you may not have the permissions to do that.\r\n\r\nFurthermore, file operations are not ACID. You cannot move a file while it is opened. You cannot delete a file before releasing its file descriptor. Forget read stream and write stream in parallel in a process.\r\n\r\nThis is quite problematic on tests. Hundreds of tearDown processes should fail, because file descriptors are never closed before the tearDown kicks in to remove temporary folders. I hide this currently by letting the temporary folders in place, and display a warning message instead. But in the long-term, it is not a solution. The real solution is to use a complete resource handling as exposed in Python like in any language: good old `try catch finally`, or its elegant alternative by context managers in Python, the `with`.\r\n\r\nThis PR tries to make things more platform independent about files:\r\n* first, by proposing a new `FileLock` that works for both Linux and Windows seamlessly\r\n* second, by exposing an API on it that follows conventions about resource handling\r\n\r\nFor that, the new file lock is inspired from the work of https://github.com/benediktschmitt/py-filelock, that proposes a cross-compatible file lock mechanism, with added features like multiple file locking. It just removes the timeout feature, as a failed lock should immediately trigger an exception in Certbot, and improved the Unix implementation with existing code in Certbot for concurrent locks.\r\n\r\nThis FileLock class produces instances that are context managers. So you do not:\r\n```python\r\nlock = filelock.lock_for_dir('/my/path')\r\nlock.acquire()\r\nraise ValueError('Troll exception ;)')\r\nlock.release() # ... and forget about this execution\r\n```\r\n\r\nOr even:\r\n```\r\nlock = filelock.lock_for_dir('/my/path')\r\ntry:\r\n  lock.acquire()\r\n  raise ValueError('Sad troll is sad')\r\nfinally:\r\n  lock.release() # ... better, but boring.\r\n```\r\n\r\nBut instead:\r\n```python\r\nwith filelock.lock_for_dir('/my/path') as lock:\r\n  print(lock.is_locked) # True (indeed)\r\n  raise ValueError('You will not break my code!')\r\n```\r\n\r\nAnd everything in the `with` statement will be executed in the context of `/my/path` to be locked from any other Certbot instance, and the lock will be released automatically while leaving `with`. Thanks Python.\r\n\r\nI think it is quite a good convention to handle explicitly file locking context (and in general opened file descriptor context, but it is another story).\r\n\r\nAll codebase is updated accordingly, with several methods that becomes context manager providers.\r\n\r\nThe number of warnings on Windows because of opened file descriptors is divided by 3. 10 more tests are fixed for Windows. ","created_at":"2018-10-24T16:02:23Z","updated_at":"2018-12-15T22:48:38Z","closed_at":null,"merged_at":null,"merge_commit_sha":"1149f31716de82b1af55cfc8925da1f50993edfa","assignee":{"login":"bmw","id":6504915,"node_id":"MDQ6VXNlcjY1MDQ5MTU=","avatar_url":"https://avatars0.githubusercontent.com/u/6504915?v=4","gravatar_id":"","url":"https://api.github.com/users/bmw","html_url":"https://github.com/bmw","followers_url":"https://api.github.com/users/bmw/followers","following_url":"https://api.github.com/users/bmw/following{/other_user}","gists_url":"https://api.github.com/users/bmw/gists{/gist_id}","starred_url":"https://api.github.com/users/bmw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bmw/subscriptions","organizations_url":"https://api.github.com/users/bmw/orgs","repos_url":"https://api.github.com/users/bmw/repos","events_url":"https://api.github.com/users/bmw/events{/privacy}","received_events_url":"https://api.github.com/users/bmw/received_events","type":"User","site_admin":false},"assignees":[{"login":"bmw","id":6504915,"node_id":"MDQ6VXNlcjY1MDQ5MTU=","avatar_url":"https://avatars0.githubusercontent.com/u/6504915?v=4","gravatar_id":"","url":"https://api.github.com/users/bmw","html_url":"https://github.com/bmw","followers_url":"https://api.github.com/users/bmw/followers","following_url":"https://api.github.com/users/bmw/following{/other_user}","gists_url":"https://api.github.com/users/bmw/gists{/gist_id}","starred_url":"https://api.github.com/users/bmw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bmw/subscriptions","organizations_url":"https://api.github.com/users/bmw/orgs","repos_url":"https://api.github.com/users/bmw/repos","events_url":"https://api.github.com/users/bmw/events{/privacy}","received_events_url":"https://api.github.com/users/bmw/received_events","type":"User","site_admin":false}],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":890251927,"node_id":"MDU6TGFiZWw4OTAyNTE5Mjc=","url":"https://api.github.com/repos/certbot/certbot/labels/current%20sprint","name":"current sprint","color":"0e8a16","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/certbot/certbot/pulls/6440/commits","review_comments_url":"https://api.github.com/repos/certbot/certbot/pulls/6440/comments","review_comment_url":"https://api.github.com/repos/certbot/certbot/pulls/comments{/number}","comments_url":"https://api.github.com/repos/certbot/certbot/issues/6440/comments","statuses_url":"https://api.github.com/repos/certbot/certbot/statuses/a655e466213933f8ad1efd8aa09838d2573aa108","head":{"label":"adferrand:windows-file-lock","ref":"windows-file-lock","sha":"a655e466213933f8ad1efd8aa09838d2573aa108","user":{"login":"adferrand","id":9728851,"node_id":"MDQ6VXNlcjk3Mjg4NTE=","avatar_url":"https://avatars0.githubusercontent.com/u/9728851?v=4","gravatar_id":"","url":"https://api.github.com/users/adferrand","html_url":"https://github.com/adferrand","followers_url":"https://api.github.com/users/adferrand/followers","following_url":"https://api.github.com/users/adferrand/following{/other_user}","gists_url":"https://api.github.com/users/adferrand/gists{/gist_id}","starred_url":"https://api.github.com/users/adferrand/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/adferrand/subscriptions","organizations_url":"https://api.github.com/users/adferrand/orgs","repos_url":"https://api.github.com/users/adferrand/repos","events_url":"https://api.github.com/users/adferrand/events{/privacy}","received_events_url":"https://api.github.com/users/adferrand/received_events","type":"User","site_admin":false},"repo":{"id":144460400,"node_id":"MDEwOlJlcG9zaXRvcnkxNDQ0NjA0MDA=","name":"certbot","full_name":"adferrand/certbot","private":false,"owner":{"login":"adferrand","id":9728851,"node_id":"MDQ6VXNlcjk3Mjg4NTE=","avatar_url":"https://avatars0.githubusercontent.com/u/9728851?v=4","gravatar_id":"","url":"https://api.github.com/users/adferrand","html_url":"https://github.com/adferrand","followers_url":"https://api.github.com/users/adferrand/followers","following_url":"https://api.github.com/users/adferrand/following{/other_user}","gists_url":"https://api.github.com/users/adferrand/gists{/gist_id}","starred_url":"https://api.github.com/users/adferrand/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/adferrand/subscriptions","organizations_url":"https://api.github.com/users/adferrand/orgs","repos_url":"https://api.github.com/users/adferrand/repos","events_url":"https://api.github.com/users/adferrand/events{/privacy}","received_events_url":"https://api.github.com/users/adferrand/received_events","type":"User","site_admin":false},"html_url":"https://github.com/adferrand/certbot","description":"Certbot is EFF's tool to obtain certs from Let's Encrypt and (optionally) auto-enable HTTPS on your server.  It can also act as a client for any other CA that uses the ACME protocol.","fork":true,"url":"https://api.github.com/repos/adferrand/certbot","forks_url":"https://api.github.com/repos/adferrand/certbot/forks","keys_url":"https://api.github.com/repos/adferrand/certbot/keys{/key_id}","collaborators_url":"https://api.github.com/repos/adferrand/certbot/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/adferrand/certbot/teams","hooks_url":"https://api.github.com/repos/adferrand/certbot/hooks","issue_events_url":"https://api.github.com/repos/adferrand/certbot/issues/events{/number}","events_url":"https://api.github.com/repos/adferrand/certbot/events","assignees_url":"https://api.github.com/repos/adferrand/certbot/assignees{/user}","branches_url":"https://api.github.com/repos/adferrand/certbot/branches{/branch}","tags_url":"https://api.github.com/repos/adferrand/certbot/tags","blobs_url":"https://api.github.com/repos/adferrand/certbot/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/adferrand/certbot/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/adferrand/certbot/git/refs{/sha}","trees_url":"https://api.github.com/repos/adferrand/certbot/git/trees{/sha}","statuses_url":"https://api.github.com/repos/adferrand/certbot/statuses/{sha}","languages_url":"https://api.github.com/repos/adferrand/certbot/languages","stargazers_url":"https://api.github.com/repos/adferrand/certbot/stargazers","contributors_url":"https://api.github.com/repos/adferrand/certbot/contributors","subscribers_url":"https://api.github.com/repos/adferrand/certbot/subscribers","subscription_url":"https://api.github.com/repos/adferrand/certbot/subscription","commits_url":"https://api.github.com/repos/adferrand/certbot/commits{/sha}","git_commits_url":"https://api.github.com/repos/adferrand/certbot/git/commits{/sha}","comments_url":"https://api.github.com/repos/adferrand/certbot/comments{/number}","issue_comment_url":"https://api.github.com/repos/adferrand/certbot/issues/comments{/number}","contents_url":"https://api.github.com/repos/adferrand/certbot/contents/{+path}","compare_url":"https://api.github.com/repos/adferrand/certbot/compare/{base}...{head}","merges_url":"https://api.github.com/repos/adferrand/certbot/merges","archive_url":"https://api.github.com/repos/adferrand/certbot/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/adferrand/certbot/downloads","issues_url":"https://api.github.com/repos/adferrand/certbot/issues{/number}","pulls_url":"https://api.github.com/repos/adferrand/certbot/pulls{/number}","milestones_url":"https://api.github.com/repos/adferrand/certbot/milestones{/number}","notifications_url":"https://api.github.com/repos/adferrand/certbot/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/adferrand/certbot/labels{/name}","releases_url":"https://api.github.com/repos/adferrand/certbot/releases{/id}","deployments_url":"https://api.github.com/repos/adferrand/certbot/deployments","created_at":"2018-08-12T11:17:30Z","updated_at":"2018-12-13T19:39:21Z","pushed_at":"2018-12-15T22:21:40Z","git_url":"git://github.com/adferrand/certbot.git","ssh_url":"git@github.com:adferrand/certbot.git","clone_url":"https://github.com/adferrand/certbot.git","svn_url":"https://github.com/adferrand/certbot","homepage":"","size":18378,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"certbot:master","ref":"master","sha":"353d09258575a5a8bfcfe7cb145834f787f2b7bb","user":{"login":"certbot","id":17889013,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE3ODg5MDEz","avatar_url":"https://avatars2.githubusercontent.com/u/17889013?v=4","gravatar_id":"","url":"https://api.github.com/users/certbot","html_url":"https://github.com/certbot","followers_url":"https://api.github.com/users/certbot/followers","following_url":"https://api.github.com/users/certbot/following{/other_user}","gists_url":"https://api.github.com/users/certbot/gists{/gist_id}","starred_url":"https://api.github.com/users/certbot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/certbot/subscriptions","organizations_url":"https://api.github.com/users/certbot/orgs","repos_url":"https://api.github.com/users/certbot/repos","events_url":"https://api.github.com/users/certbot/events{/privacy}","received_events_url":"https://api.github.com/users/certbot/received_events","type":"Organization","site_admin":false},"repo":{"id":26516210,"node_id":"MDEwOlJlcG9zaXRvcnkyNjUxNjIxMA==","name":"certbot","full_name":"certbot/certbot","private":false,"owner":{"login":"certbot","id":17889013,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE3ODg5MDEz","avatar_url":"https://avatars2.githubusercontent.com/u/17889013?v=4","gravatar_id":"","url":"https://api.github.com/users/certbot","html_url":"https://github.com/certbot","followers_url":"https://api.github.com/users/certbot/followers","following_url":"https://api.github.com/users/certbot/following{/other_user}","gists_url":"https://api.github.com/users/certbot/gists{/gist_id}","starred_url":"https://api.github.com/users/certbot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/certbot/subscriptions","organizations_url":"https://api.github.com/users/certbot/orgs","repos_url":"https://api.github.com/users/certbot/repos","events_url":"https://api.github.com/users/certbot/events{/privacy}","received_events_url":"https://api.github.com/users/certbot/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/certbot/certbot","description":"Certbot is EFF's tool to obtain certs from Let's Encrypt and (optionally) auto-enable HTTPS on your server.  It can also act as a client for any other CA that uses the ACME protocol.","fork":false,"url":"https://api.github.com/repos/certbot/certbot","forks_url":"https://api.github.com/repos/certbot/certbot/forks","keys_url":"https://api.github.com/repos/certbot/certbot/keys{/key_id}","collaborators_url":"https://api.github.com/repos/certbot/certbot/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/certbot/certbot/teams","hooks_url":"https://api.github.com/repos/certbot/certbot/hooks","issue_events_url":"https://api.github.com/repos/certbot/certbot/issues/events{/number}","events_url":"https://api.github.com/repos/certbot/certbot/events","assignees_url":"https://api.github.com/repos/certbot/certbot/assignees{/user}","branches_url":"https://api.github.com/repos/certbot/certbot/branches{/branch}","tags_url":"https://api.github.com/repos/certbot/certbot/tags","blobs_url":"https://api.github.com/repos/certbot/certbot/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/certbot/certbot/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/certbot/certbot/git/refs{/sha}","trees_url":"https://api.github.com/repos/certbot/certbot/git/trees{/sha}","statuses_url":"https://api.github.com/repos/certbot/certbot/statuses/{sha}","languages_url":"https://api.github.com/repos/certbot/certbot/languages","stargazers_url":"https://api.github.com/repos/certbot/certbot/stargazers","contributors_url":"https://api.github.com/repos/certbot/certbot/contributors","subscribers_url":"https://api.github.com/repos/certbot/certbot/subscribers","subscription_url":"https://api.github.com/repos/certbot/certbot/subscription","commits_url":"https://api.github.com/repos/certbot/certbot/commits{/sha}","git_commits_url":"https://api.github.com/repos/certbot/certbot/git/commits{/sha}","comments_url":"https://api.github.com/repos/certbot/certbot/comments{/number}","issue_comment_url":"https://api.github.com/repos/certbot/certbot/issues/comments{/number}","contents_url":"https://api.github.com/repos/certbot/certbot/contents/{+path}","compare_url":"https://api.github.com/repos/certbot/certbot/compare/{base}...{head}","merges_url":"https://api.github.com/repos/certbot/certbot/merges","archive_url":"https://api.github.com/repos/certbot/certbot/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/certbot/certbot/downloads","issues_url":"https://api.github.com/repos/certbot/certbot/issues{/number}","pulls_url":"https://api.github.com/repos/certbot/certbot/pulls{/number}","milestones_url":"https://api.github.com/repos/certbot/certbot/milestones{/number}","notifications_url":"https://api.github.com/repos/certbot/certbot/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/certbot/certbot/labels{/name}","releases_url":"https://api.github.com/repos/certbot/certbot/releases{/id}","deployments_url":"https://api.github.com/repos/certbot/certbot/deployments","created_at":"2014-11-12T02:52:20Z","updated_at":"2018-12-15T21:55:26Z","pushed_at":"2018-12-15T22:21:42Z","git_url":"git://github.com/certbot/certbot.git","ssh_url":"git@github.com:certbot/certbot.git","clone_url":"https://github.com/certbot/certbot.git","svn_url":"https://github.com/certbot/certbot","homepage":"","size":20500,"stargazers_count":23808,"watchers_count":23808,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2373,"mirror_url":null,"archived":false,"open_issues_count":901,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":2373,"open_issues":901,"watchers":23808,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/certbot/certbot/pulls/6440"},"html":{"href":"https://github.com/certbot/certbot/pull/6440"},"issue":{"href":"https://api.github.com/repos/certbot/certbot/issues/6440"},"comments":{"href":"https://api.github.com/repos/certbot/certbot/issues/6440/comments"},"review_comments":{"href":"https://api.github.com/repos/certbot/certbot/pulls/6440/comments"},"review_comment":{"href":"https://api.github.com/repos/certbot/certbot/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/certbot/certbot/pulls/6440/commits"},"statuses":{"href":"https://api.github.com/repos/certbot/certbot/statuses/a655e466213933f8ad1efd8aa09838d2573aa108"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"adferrand"}}