{"action":"created","comment":{"url":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/comments/34732698","id":34732698,"diff_hunk":"@@ -0,0 +1,141 @@\n+﻿using System;\n+using System.IO;\n+using System.Linq;\n+using System.Reflection;\n+using System.Text;\n+using CurlSharp;\n+using log4net;\n+\n+namespace CKAN.NetKAN.Services\n+{\n+    internal sealed partial class CachingHttpService : IHttpService\n+    {\n+        private static readonly ILog Log = LogManager.GetLogger(typeof(CachingHttpService));\n+\n+        private readonly NetFileCache _cache;\n+        private CurlEasy _curl;\n+\n+        public CachingHttpService(NetFileCache cache)\n+        {\n+            _cache = cache;\n+\n+            CurlSharp.Curl.GlobalInit(CurlInitFlag.All);\n+\n+            _curl = new CurlEasy { UserAgent = CKAN.Net.UserAgentString };\n+\n+            var caBundle = ResolveCurlCaBundle();\n+            if (caBundle != null)\n+            {\n+                _curl.CaInfo = caBundle;\n+            }\n+        }\n+\n+        public string DownloadPackage(Uri url, string identifier)\n+        {\n+            EnsureNotDisposed();\n+\n+            return _cache.GetCachedFilename(url) ?? \n+                _cache.Store(url, CKAN.Net.Download(url), string.Format(\"netkan-{0}\", identifier), move: true);\n+        }\n+\n+        public string DownloadText(Uri url)\n+        {\n+            EnsureNotDisposed();\n+\n+            Log.DebugFormat(\"About to download {0}\", url);\n+\n+            var content = string.Empty;\n+\n+            _curl.Url = url.ToString();\n+            _curl.WriteData = null;\n+            _curl.WriteFunction = delegate(byte[] buf, int size, int nmemb, object extraData)\n+            {\n+                content += Encoding.UTF8.GetString(buf);\n+                return size * nmemb;\n+            };\n+\n+            var result = _curl.Perform();\n+\n+            if (result != CurlCode.Ok)\n+            {\n+                throw new Exception(\"Curl download failed with error \" + result);\n+            }\n+\n+            Log.DebugFormat(\"Download from {0}:\\n\\n{1}\", url, content);\n+\n+            return content;\n+        }\n+\n+        /// <summary>\n+        /// Resolves the location of the cURL CA bundle file to use.\n+        /// </summary>\n+        /// <returns>The absolute file path to the bundle file or null if none is found.</returns>\n+        private static string ResolveCurlCaBundle()\n+        {\n+            const string caBundleFileName = \"curl-ca-bundle.crt\";\n+            const string ckanSubDirectoryName = \"CKAN\";\n+\n+            var bundle = new[]\n+            {\n+                // Working Directory\n+                Environment.CurrentDirectory,\n+\n+                // Executable Directory\n+                Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),\n+\n+                // %LOCALAPPDATA%/CKAN\n+                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ckanSubDirectoryName),\n+\n+                // %APPDATA%/CKAN\n+                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ckanSubDirectoryName),\n+\n+                // %PROGRAMDATA%/CKAN\n+                Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), ckanSubDirectoryName),\n+            }\n+            .Select(i => Path.Combine(i, caBundleFileName))\n+            .FirstOrDefault(File.Exists);\n+\n+            Log.InfoFormat(\"Using curl-ca bundle: {0}\",bundle ?? \"(none)\");\n+\n+            return bundle;\n+        }\n+    }\n+\n+    internal sealed partial class CachingHttpService\n+    {\n+        private bool _isDisposed;\n+\n+        ~CachingHttpService()\n+        {\n+            Dispose(disposing: false);","path":"Netkan/Services/CachingHttpService.cs","position":110,"original_position":110,"commit_id":"440ab34eabd793a748863c4d1299db82adb47ccb","original_commit_id":"440ab34eabd793a748863c4d1299db82adb47ccb","user":{"login":"pjf","id":12072,"avatar_url":"https://avatars.githubusercontent.com/u/12072?v=3","gravatar_id":"","url":"https://api.github.com/users/pjf","html_url":"https://github.com/pjf","followers_url":"https://api.github.com/users/pjf/followers","following_url":"https://api.github.com/users/pjf/following{/other_user}","gists_url":"https://api.github.com/users/pjf/gists{/gist_id}","starred_url":"https://api.github.com/users/pjf/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pjf/subscriptions","organizations_url":"https://api.github.com/users/pjf/orgs","repos_url":"https://api.github.com/users/pjf/repos","events_url":"https://api.github.com/users/pjf/events{/privacy}","received_events_url":"https://api.github.com/users/pjf/received_events","type":"User","site_admin":false},"body":"I'm not as well versed in C# destructors as I'd like to be. To check my understanding here:\r\n\r\n- If  `Dispose` is called directly (eg, by leaving a `using` scope), then we run our curl clean-up, and set `_isDisposed` to true.\r\n- If our `CachingHttpService` is destroyed, we don't touch curl and just set `_isDisposed` to true.\r\n- We can call `EnsureNotDisposed()` to make sure our object hasn't been disposed (but presumably hasn't been destroyed, because if it's destroyed we couldn't have a reference to it).\r\n\r\nSo mostly I'm checking under what circumstances we'd see the destructor called *without* `Dispose` also being invoked just before it. I feel like I'm missing something with the logic flow here. (I don't know 100% how the .NET garbage collector works, but I presume it won't go destroying something you have an active reference to.)","created_at":"2015-07-15T21:37:18Z","updated_at":"2015-07-15T21:37:18Z","html_url":"https://github.com/KSP-CKAN/CKAN/pull/1218#discussion_r34732698","pull_request_url":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/1218","_links":{"self":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/comments/34732698"},"html":{"href":"https://github.com/KSP-CKAN/CKAN/pull/1218#discussion_r34732698"},"pull_request":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/1218"}}},"pull_request":{"url":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/1218","id":38733819,"html_url":"https://github.com/KSP-CKAN/CKAN/pull/1218","diff_url":"https://github.com/KSP-CKAN/CKAN/pull/1218.diff","patch_url":"https://github.com/KSP-CKAN/CKAN/pull/1218.patch","issue_url":"https://api.github.com/repos/KSP-CKAN/CKAN/issues/1218","number":1218,"state":"open","locked":false,"title":"Netkan Refactoring","user":{"login":"dbent","id":1547727,"avatar_url":"https://avatars.githubusercontent.com/u/1547727?v=3","gravatar_id":"","url":"https://api.github.com/users/dbent","html_url":"https://github.com/dbent","followers_url":"https://api.github.com/users/dbent/followers","following_url":"https://api.github.com/users/dbent/following{/other_user}","gists_url":"https://api.github.com/users/dbent/gists{/gist_id}","starred_url":"https://api.github.com/users/dbent/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dbent/subscriptions","organizations_url":"https://api.github.com/users/dbent/orgs","repos_url":"https://api.github.com/users/dbent/repos","events_url":"https://api.github.com/users/dbent/events{/privacy}","received_events_url":"https://api.github.com/users/dbent/received_events","type":"User","site_admin":false},"body":"This is a mondo refactoring of NetKAN. Viewing the diff is a pretty useless way to review this, I'd recommend just stepping through the code in sequence. This refactoring does a few different things:\r\n\r\n- Refactors the entire \"pipeline\" into 5 obvious steps:\r\n  1. Read `.netkan`\r\n  2. Validate `.netkan`\r\n  3. Transform `.netkan` into `.ckan`\r\n  4. Validate `.ckan`\r\n  5. Write `.ckan`\r\n- Includes the changes in #1204\r\n- Includes the changes in #1206\r\n- Adds a bunch more tests\r\n- Changed a bunch of stuff to be more idiomatic C#\r\n\r\n##### Pipeline\r\nThe major refactoring is the introduction of `ITransformer` and `IValidator`. An `ITransformer` has a single method, `Metadata Transform(Metadata)`, that takes a `Metadata` object (a wrapper around a plain `JObject`, that exposes some useful parsed fields) and produces another `Metadata` object. For simplicity's sake `Metadata` objects are immutable. *Every* alternation necessary to turn a `.netkan` into a `.ckan` is implemented as an `ITransformer`. There are 13 \"normal\" implementations of `ITransformer`, called in this specific sequence:\r\n\r\n- `MetaNetkanTransformer` Perform a recursive lookup of another NetKAN file.\r\n- `KerbalstuffTransformer` Looks up data from Kerbal Stuff.\r\n- `GithubTransformer` Looks up data from GitHub.\r\n- `HttpTransformer` Looks up data from an arbitrary HTTP endpoint.\r\n- `JenkinsTransformer` Looks up data from a Jenkins build server.\r\n- `InternalCkanTransformer` Populates data from a CKAN file included in the distribution package.\r\n- `AvcTransformer` Populates version data from an AVC version file included in the distribution package.\r\n- `ForcedVTransformer` Prepends the letter 'v' to the version if necessary.\r\n- `EpochTransformer` Uses a particular version epoch if necessary.\r\n- `VersionedOverrideTransformer` Overrides properties based on the version.\r\n- `DownloadSizeTransformer` Adds the size of the download.\r\n- `GeneratedByTransformer` Adds a property to indicate the program responsible for generating the metadata.\r\n- `StripNetkanMetadataTransformer` Removes metadata useful only to NetKAN.\r\n\r\nIf a particular `ITransformer` isn't applicable to the current state of the `Metadata`, it simply passes the `Metadata` through the pipeline unchanged.\r\n\r\nThere is also one \"special\" `ITransformer`, `NetkanTransformer`, that can transform NetKAN metadata into CKAN metadata, it does this by having knowledge of all the other transformers and calling them in the aforementioned sequence.\r\n\r\nNetKAN was effectively already operating as a pipeline but the implementation wasn't explicit. I think this is easier to reason about and change, and probably most importantly: *test*. Every `ITransformer` does *one* thing (for god's sake I have a `GeneratedByTransformer`!) that makes its behavior obvious and testable. This also drastically reduces redundant code; inspecting CKANs in zip files is now done *once*, adding the size of the download is now done *once*, etc.\r\n\r\nI've also introduced an `IValidator` interface that has a single method `void Validate(Metadata)` that takes a `Metadata` object and either does nothing if it passes validation or throws an exception if it doesn't. There are two special `IValidator` implementations: `NetkanValidator` which is used to validate the incoming metadata from the `.netkan` file and `CkanValidator` which is used to validate the outgoing metadata before it's written to the `.ckan` file.\r\n\r\n##### MetaNetKANs\r\nThis folds the changes in from #1204. Basically a `$kref` of `#/ckan/netkan` is used to point to a `.netkan` file hosted somehow which is then consumed as if all its contents were included in the original `.netkan`. The recursion only goes one level, if the target `.netkan` is itself a MetaNetKAN then an exception is thrown.\r\n\r\n##### Tests\r\nI've added a bunch of tests for this stuff. Here is the [coverage report](https://dl.dropboxusercontent.com/u/123465/CKAN/NetKANTests-20150630.html) for this branch. I've also added [Moq](https://github.com/Moq/moq4) as a tests-only dependency to mock out interfaced dependencies. I've committed the package to source control like the others, but at some point in the future I'd like to modify the build to remove the committed packages and just use NuGet to restore them on-demand.","created_at":"2015-06-28T23:22:24Z","updated_at":"2015-07-15T21:37:18Z","closed_at":null,"merged_at":null,"merge_commit_sha":"664e487a54f0b27e2ba17adf03bc0721fa7dfe3a","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/1218/commits","review_comments_url":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/1218/comments","review_comment_url":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/comments{/number}","comments_url":"https://api.github.com/repos/KSP-CKAN/CKAN/issues/1218/comments","statuses_url":"https://api.github.com/repos/KSP-CKAN/CKAN/statuses/440ab34eabd793a748863c4d1299db82adb47ccb","head":{"label":"KSP-CKAN:topic/netkan_transformations","ref":"topic/netkan_transformations","sha":"440ab34eabd793a748863c4d1299db82adb47ccb","user":{"login":"KSP-CKAN","id":9023970,"avatar_url":"https://avatars.githubusercontent.com/u/9023970?v=3","gravatar_id":"","url":"https://api.github.com/users/KSP-CKAN","html_url":"https://github.com/KSP-CKAN","followers_url":"https://api.github.com/users/KSP-CKAN/followers","following_url":"https://api.github.com/users/KSP-CKAN/following{/other_user}","gists_url":"https://api.github.com/users/KSP-CKAN/gists{/gist_id}","starred_url":"https://api.github.com/users/KSP-CKAN/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/KSP-CKAN/subscriptions","organizations_url":"https://api.github.com/users/KSP-CKAN/orgs","repos_url":"https://api.github.com/users/KSP-CKAN/repos","events_url":"https://api.github.com/users/KSP-CKAN/events{/privacy}","received_events_url":"https://api.github.com/users/KSP-CKAN/received_events","type":"Organization","site_admin":false},"repo":{"id":23050594,"name":"CKAN","full_name":"KSP-CKAN/CKAN","owner":{"login":"KSP-CKAN","id":9023970,"avatar_url":"https://avatars.githubusercontent.com/u/9023970?v=3","gravatar_id":"","url":"https://api.github.com/users/KSP-CKAN","html_url":"https://github.com/KSP-CKAN","followers_url":"https://api.github.com/users/KSP-CKAN/followers","following_url":"https://api.github.com/users/KSP-CKAN/following{/other_user}","gists_url":"https://api.github.com/users/KSP-CKAN/gists{/gist_id}","starred_url":"https://api.github.com/users/KSP-CKAN/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/KSP-CKAN/subscriptions","organizations_url":"https://api.github.com/users/KSP-CKAN/orgs","repos_url":"https://api.github.com/users/KSP-CKAN/repos","events_url":"https://api.github.com/users/KSP-CKAN/events{/privacy}","received_events_url":"https://api.github.com/users/KSP-CKAN/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/KSP-CKAN/CKAN","description":"The Comprehensive Kerbal Archive Network","fork":false,"url":"https://api.github.com/repos/KSP-CKAN/CKAN","forks_url":"https://api.github.com/repos/KSP-CKAN/CKAN/forks","keys_url":"https://api.github.com/repos/KSP-CKAN/CKAN/keys{/key_id}","collaborators_url":"https://api.github.com/repos/KSP-CKAN/CKAN/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/KSP-CKAN/CKAN/teams","hooks_url":"https://api.github.com/repos/KSP-CKAN/CKAN/hooks","issue_events_url":"https://api.github.com/repos/KSP-CKAN/CKAN/issues/events{/number}","events_url":"https://api.github.com/repos/KSP-CKAN/CKAN/events","assignees_url":"https://api.github.com/repos/KSP-CKAN/CKAN/assignees{/user}","branches_url":"https://api.github.com/repos/KSP-CKAN/CKAN/branches{/branch}","tags_url":"https://api.github.com/repos/KSP-CKAN/CKAN/tags","blobs_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/refs{/sha}","trees_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/trees{/sha}","statuses_url":"https://api.github.com/repos/KSP-CKAN/CKAN/statuses/{sha}","languages_url":"https://api.github.com/repos/KSP-CKAN/CKAN/languages","stargazers_url":"https://api.github.com/repos/KSP-CKAN/CKAN/stargazers","contributors_url":"https://api.github.com/repos/KSP-CKAN/CKAN/contributors","subscribers_url":"https://api.github.com/repos/KSP-CKAN/CKAN/subscribers","subscription_url":"https://api.github.com/repos/KSP-CKAN/CKAN/subscription","commits_url":"https://api.github.com/repos/KSP-CKAN/CKAN/commits{/sha}","git_commits_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/commits{/sha}","comments_url":"https://api.github.com/repos/KSP-CKAN/CKAN/comments{/number}","issue_comment_url":"https://api.github.com/repos/KSP-CKAN/CKAN/issues/comments{/number}","contents_url":"https://api.github.com/repos/KSP-CKAN/CKAN/contents/{+path}","compare_url":"https://api.github.com/repos/KSP-CKAN/CKAN/compare/{base}...{head}","merges_url":"https://api.github.com/repos/KSP-CKAN/CKAN/merges","archive_url":"https://api.github.com/repos/KSP-CKAN/CKAN/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/KSP-CKAN/CKAN/downloads","issues_url":"https://api.github.com/repos/KSP-CKAN/CKAN/issues{/number}","pulls_url":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls{/number}","milestones_url":"https://api.github.com/repos/KSP-CKAN/CKAN/milestones{/number}","notifications_url":"https://api.github.com/repos/KSP-CKAN/CKAN/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/KSP-CKAN/CKAN/labels{/name}","releases_url":"https://api.github.com/repos/KSP-CKAN/CKAN/releases{/id}","created_at":"2014-08-17T21:11:54Z","updated_at":"2015-07-14T06:46:42Z","pushed_at":"2015-07-15T21:17:50Z","git_url":"git://github.com/KSP-CKAN/CKAN.git","ssh_url":"git@github.com:KSP-CKAN/CKAN.git","clone_url":"https://github.com/KSP-CKAN/CKAN.git","svn_url":"https://github.com/KSP-CKAN/CKAN","homepage":"","size":28080,"stargazers_count":216,"watchers_count":216,"language":"C#","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":66,"mirror_url":null,"open_issues_count":245,"forks":66,"open_issues":245,"watchers":216,"default_branch":"master"}},"base":{"label":"KSP-CKAN:master","ref":"master","sha":"0e607b2da6d2bd44c75459db52c0bc2435e09c2a","user":{"login":"KSP-CKAN","id":9023970,"avatar_url":"https://avatars.githubusercontent.com/u/9023970?v=3","gravatar_id":"","url":"https://api.github.com/users/KSP-CKAN","html_url":"https://github.com/KSP-CKAN","followers_url":"https://api.github.com/users/KSP-CKAN/followers","following_url":"https://api.github.com/users/KSP-CKAN/following{/other_user}","gists_url":"https://api.github.com/users/KSP-CKAN/gists{/gist_id}","starred_url":"https://api.github.com/users/KSP-CKAN/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/KSP-CKAN/subscriptions","organizations_url":"https://api.github.com/users/KSP-CKAN/orgs","repos_url":"https://api.github.com/users/KSP-CKAN/repos","events_url":"https://api.github.com/users/KSP-CKAN/events{/privacy}","received_events_url":"https://api.github.com/users/KSP-CKAN/received_events","type":"Organization","site_admin":false},"repo":{"id":23050594,"name":"CKAN","full_name":"KSP-CKAN/CKAN","owner":{"login":"KSP-CKAN","id":9023970,"avatar_url":"https://avatars.githubusercontent.com/u/9023970?v=3","gravatar_id":"","url":"https://api.github.com/users/KSP-CKAN","html_url":"https://github.com/KSP-CKAN","followers_url":"https://api.github.com/users/KSP-CKAN/followers","following_url":"https://api.github.com/users/KSP-CKAN/following{/other_user}","gists_url":"https://api.github.com/users/KSP-CKAN/gists{/gist_id}","starred_url":"https://api.github.com/users/KSP-CKAN/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/KSP-CKAN/subscriptions","organizations_url":"https://api.github.com/users/KSP-CKAN/orgs","repos_url":"https://api.github.com/users/KSP-CKAN/repos","events_url":"https://api.github.com/users/KSP-CKAN/events{/privacy}","received_events_url":"https://api.github.com/users/KSP-CKAN/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/KSP-CKAN/CKAN","description":"The Comprehensive Kerbal Archive Network","fork":false,"url":"https://api.github.com/repos/KSP-CKAN/CKAN","forks_url":"https://api.github.com/repos/KSP-CKAN/CKAN/forks","keys_url":"https://api.github.com/repos/KSP-CKAN/CKAN/keys{/key_id}","collaborators_url":"https://api.github.com/repos/KSP-CKAN/CKAN/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/KSP-CKAN/CKAN/teams","hooks_url":"https://api.github.com/repos/KSP-CKAN/CKAN/hooks","issue_events_url":"https://api.github.com/repos/KSP-CKAN/CKAN/issues/events{/number}","events_url":"https://api.github.com/repos/KSP-CKAN/CKAN/events","assignees_url":"https://api.github.com/repos/KSP-CKAN/CKAN/assignees{/user}","branches_url":"https://api.github.com/repos/KSP-CKAN/CKAN/branches{/branch}","tags_url":"https://api.github.com/repos/KSP-CKAN/CKAN/tags","blobs_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/refs{/sha}","trees_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/trees{/sha}","statuses_url":"https://api.github.com/repos/KSP-CKAN/CKAN/statuses/{sha}","languages_url":"https://api.github.com/repos/KSP-CKAN/CKAN/languages","stargazers_url":"https://api.github.com/repos/KSP-CKAN/CKAN/stargazers","contributors_url":"https://api.github.com/repos/KSP-CKAN/CKAN/contributors","subscribers_url":"https://api.github.com/repos/KSP-CKAN/CKAN/subscribers","subscription_url":"https://api.github.com/repos/KSP-CKAN/CKAN/subscription","commits_url":"https://api.github.com/repos/KSP-CKAN/CKAN/commits{/sha}","git_commits_url":"https://api.github.com/repos/KSP-CKAN/CKAN/git/commits{/sha}","comments_url":"https://api.github.com/repos/KSP-CKAN/CKAN/comments{/number}","issue_comment_url":"https://api.github.com/repos/KSP-CKAN/CKAN/issues/comments{/number}","contents_url":"https://api.github.com/repos/KSP-CKAN/CKAN/contents/{+path}","compare_url":"https://api.github.com/repos/KSP-CKAN/CKAN/compare/{base}...{head}","merges_url":"https://api.github.com/repos/KSP-CKAN/CKAN/merges","archive_url":"https://api.github.com/repos/KSP-CKAN/CKAN/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/KSP-CKAN/CKAN/downloads","issues_url":"https://api.github.com/repos/KSP-CKAN/CKAN/issues{/number}","pulls_url":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls{/number}","milestones_url":"https://api.github.com/repos/KSP-CKAN/CKAN/milestones{/number}","notifications_url":"https://api.github.com/repos/KSP-CKAN/CKAN/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/KSP-CKAN/CKAN/labels{/name}","releases_url":"https://api.github.com/repos/KSP-CKAN/CKAN/releases{/id}","created_at":"2014-08-17T21:11:54Z","updated_at":"2015-07-14T06:46:42Z","pushed_at":"2015-07-15T21:17:50Z","git_url":"git://github.com/KSP-CKAN/CKAN.git","ssh_url":"git@github.com:KSP-CKAN/CKAN.git","clone_url":"https://github.com/KSP-CKAN/CKAN.git","svn_url":"https://github.com/KSP-CKAN/CKAN","homepage":"","size":28080,"stargazers_count":216,"watchers_count":216,"language":"C#","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":66,"mirror_url":null,"open_issues_count":245,"forks":66,"open_issues":245,"watchers":216,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/1218"},"html":{"href":"https://github.com/KSP-CKAN/CKAN/pull/1218"},"issue":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/issues/1218"},"comments":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/issues/1218/comments"},"review_comments":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/1218/comments"},"review_comment":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/pulls/1218/commits"},"statuses":{"href":"https://api.github.com/repos/KSP-CKAN/CKAN/statuses/440ab34eabd793a748863c4d1299db82adb47ccb"}}}}