github_api 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. data/README.md +3 -3
  2. data/features/cassettes/events/issue.yml +64 -32
  3. data/features/cassettes/events/network.yml +63 -32
  4. data/features/cassettes/events/org.yml +61 -31
  5. data/features/cassettes/events/performed.yml +63 -32
  6. data/features/cassettes/events/public.yml +61 -31
  7. data/features/cassettes/events/received.yml +63 -32
  8. data/features/cassettes/events/repo.yml +63 -32
  9. data/features/cassettes/gists/comments/all.yml +32 -1
  10. data/features/cassettes/gists/comments/first.yml +32 -1
  11. data/features/cassettes/gists/fork.yml +63 -31
  12. data/features/cassettes/gists/gist.yml +32 -1
  13. data/features/cassettes/gists/gists/public_all.yml +32 -1
  14. data/features/cassettes/gists/gists/starred.yml +60 -29
  15. data/features/cassettes/gists/gists/user_all.yml +87 -1
  16. data/features/cassettes/gists/star.yml +58 -27
  17. data/features/cassettes/gists/starred.yml +32 -1
  18. data/features/cassettes/gists/unstar.yml +54 -25
  19. data/features/cassettes/git_data/references/all.yml +32 -1
  20. data/features/cassettes/git_data/references/all_tags.yml +32 -1
  21. data/features/cassettes/git_data/references/one.yml +32 -1
  22. data/features/cassettes/issues/create.yml +46 -0
  23. data/features/cassettes/issues/edit.yml +44 -0
  24. data/features/cassettes/issues/get.yml +62 -0
  25. data/features/cassettes/issues/list/repo.yml +251 -0
  26. data/features/cassettes/issues/list/user.yml +44 -0
  27. data/features/cassettes/orgs/get.yml +60 -30
  28. data/features/cassettes/orgs/list/oauth_user.yml +60 -29
  29. data/features/cassettes/orgs/list/user.yml +87 -57
  30. data/features/cassettes/pagination/issues/list/first.yml +65 -33
  31. data/features/cassettes/pagination/issues/list/last.yml +66 -33
  32. data/features/cassettes/pagination/repos/commits/list.yml +32 -1
  33. data/features/cassettes/pagination/repos/commits/sha.yml +32 -1
  34. data/features/cassettes/pagination/repos/diff.yml +32 -1
  35. data/features/cassettes/pagination/repos/list.yml +32 -1
  36. data/features/cassettes/pagination/repos/per_page/first.yml +32 -1
  37. data/features/cassettes/pull_requests/get.yml +120 -0
  38. data/features/cassettes/pull_requests/list.yml +254 -0
  39. data/features/cassettes/repos/branches.yml +32 -1
  40. data/features/cassettes/repos/contents/archive.yml +95 -0
  41. data/features/cassettes/repos/contents/get.yml +226 -0
  42. data/features/cassettes/repos/contents/readme.yml +271 -0
  43. data/features/cassettes/repos/get.yml +61 -31
  44. data/features/cassettes/repos/languages.yml +61 -30
  45. data/features/cassettes/repos/list.yml +32 -1
  46. data/features/cassettes/repos/tags.yml +32 -1
  47. data/features/cassettes/search/email.yml +99 -0
  48. data/features/cassettes/search/issues.yml +180 -0
  49. data/features/cassettes/search/repos.yml +396 -0
  50. data/features/cassettes/search/users.yml +54 -0
  51. data/features/cassettes/users/emails/add.yml +61 -30
  52. data/features/cassettes/users/emails/all.yml +61 -30
  53. data/features/cassettes/users/get/oauth.yml +61 -0
  54. data/features/cassettes/users/get/user.yml +62 -0
  55. data/features/issues.feature +64 -0
  56. data/features/pull_requests.feature +27 -0
  57. data/features/repos/contents.feature +35 -0
  58. data/features/search.feature +48 -0
  59. data/features/users.feature +23 -0
  60. data/lib/github_api.rb +1 -0
  61. data/lib/github_api/api.rb +0 -15
  62. data/lib/github_api/client.rb +4 -0
  63. data/lib/github_api/constants.rb +4 -0
  64. data/lib/github_api/error.rb +1 -0
  65. data/lib/github_api/error/unknown_value.rb +18 -0
  66. data/lib/github_api/filter.rb +1 -0
  67. data/lib/github_api/gists.rb +2 -6
  68. data/lib/github_api/gists/comments.rb +2 -2
  69. data/lib/github_api/git_data/blobs.rb +1 -1
  70. data/lib/github_api/git_data/commits.rb +1 -1
  71. data/lib/github_api/git_data/references.rb +2 -2
  72. data/lib/github_api/git_data/tags.rb +1 -1
  73. data/lib/github_api/git_data/trees.rb +2 -3
  74. data/lib/github_api/issues.rb +6 -6
  75. data/lib/github_api/issues/comments.rb +2 -2
  76. data/lib/github_api/issues/labels.rb +2 -2
  77. data/lib/github_api/issues/milestones.rb +3 -3
  78. data/lib/github_api/orgs/teams.rb +4 -4
  79. data/lib/github_api/params_hash.rb +31 -0
  80. data/lib/github_api/pull_requests.rb +2 -2
  81. data/lib/github_api/pull_requests/comments.rb +2 -2
  82. data/lib/github_api/repos.rb +8 -10
  83. data/lib/github_api/repos/commits.rb +2 -2
  84. data/lib/github_api/repos/contents.rb +64 -0
  85. data/lib/github_api/repos/downloads.rb +1 -1
  86. data/lib/github_api/repos/hooks.rb +2 -2
  87. data/lib/github_api/repos/keys.rb +1 -1
  88. data/lib/github_api/result.rb +8 -0
  89. data/lib/github_api/search.rb +98 -0
  90. data/lib/github_api/users.rb +8 -6
  91. data/lib/github_api/users/emails.rb +2 -2
  92. data/lib/github_api/validations/format.rb +4 -3
  93. data/lib/github_api/validations/required.rb +5 -2
  94. data/lib/github_api/version.rb +2 -2
  95. data/spec/fixtures/repos/content.json +14 -0
  96. data/spec/fixtures/repos/readme.json +14 -0
  97. data/spec/fixtures/search/email.json +22 -0
  98. data/spec/fixtures/search/issues.json +23 -0
  99. data/spec/fixtures/search/repositories.json +29 -0
  100. data/spec/fixtures/search/users.json +24 -0
  101. data/spec/github/error/unknown_value_spec.rb +21 -0
  102. data/spec/github/repos/contents_spec.rb +65 -0
  103. data/spec/github/search_spec.rb +87 -0
  104. data/spec/github/users_spec.rb +7 -7
  105. data/spec/github/validations/format_spec.rb +6 -6
  106. data/spec/github/validations/required_spec.rb +3 -3
  107. metadata +69 -35
@@ -0,0 +1,48 @@
1
+ Feature: Search API
2
+
3
+ Background:
4
+ Given I have "Github::Search" instance
5
+
6
+ Scenario: Issues
7
+
8
+ Given I want issues resource
9
+ And I pass the following request options:
10
+ | owner | repo | state | keyword |
11
+ | peter-murach | github | closed | api |
12
+ When I make request within a cassette named "search/issues"
13
+ Then the response status should be 200
14
+ And the response type should be JSON
15
+ And the response should not be empty
16
+
17
+ Scenario: Repositories
18
+
19
+ Given I want repositories resource
20
+ And I pass the following request options:
21
+ | keyword |
22
+ | rails |
23
+ When I make request within a cassette named "search/repos"
24
+ Then the response status should be 200
25
+ And the response type should be JSON
26
+ And the response should not be empty
27
+
28
+ Scenario: Users
29
+
30
+ Given I want users resource
31
+ And I pass the following request options:
32
+ | keyword |
33
+ | wycats |
34
+ When I make request within a cassette named "search/users"
35
+ Then the response status should be 200
36
+ And the response type should be JSON
37
+ And the response should not be empty
38
+
39
+ Scenario: Email
40
+
41
+ Given I want email resource
42
+ And I pass the following request options:
43
+ | email |
44
+ | wycats@gmail.com |
45
+ When I make request within a cassette named "search/email"
46
+ Then the response status should be 200
47
+ And the response type should be JSON
48
+ And the response should not be empty
@@ -0,0 +1,23 @@
1
+ Feature: Users API
2
+
3
+ Background:
4
+ Given I have "Github::Users" instance
5
+
6
+ Scenario: Get authenticated user
7
+
8
+ Given I want to get resource
9
+ When I make request within a cassette named "users/get/oauth"
10
+ Then the response status should be 200
11
+ And the response type should be JSON
12
+ And the response should not be empty
13
+
14
+ Scenario: Get unauthenticated user
15
+
16
+ Given I want to get resource
17
+ And I pass the following request options:
18
+ | user |
19
+ | peter-murach |
20
+ When I make request within a cassette named "users/get/user"
21
+ Then the response status should be 200
22
+ And the response type should be JSON
23
+ And the response should not be empty
data/lib/github_api.rb CHANGED
@@ -74,6 +74,7 @@ module Github
74
74
  :PullRequests => 'pull_requests',
75
75
  :Users => 'users',
76
76
  :Events => 'events',
77
+ :Search => 'search',
77
78
  :CoreExt => 'core_ext',
78
79
  :MimeType => 'mime_type',
79
80
  :Authorization => 'authorization',
@@ -12,8 +12,6 @@ require 'github_api/api/actions'
12
12
  require 'github_api/api_factory'
13
13
 
14
14
  module Github
15
-
16
- # @private
17
15
  class API
18
16
  include Authorization
19
17
  include MimeType
@@ -23,7 +21,6 @@ module Github
23
21
  include Validations
24
22
  include Filter
25
23
 
26
-
27
24
  attr_reader *Configuration::VALID_OPTIONS_KEYS
28
25
  attr_accessor *VALID_API_KEYS
29
26
 
@@ -116,17 +113,5 @@ module Github
116
113
  # params['mime_type'] = params['mime_type'] || :raw
117
114
  end
118
115
 
119
- # TODO add to core extensions
120
- def _extract_parameters(array)
121
- if array.last.is_a?(Hash) && array.last.instance_of?(Hash)
122
- array.pop
123
- else
124
- {}
125
- end
126
- end
127
-
128
- def _token_required
129
- end
130
-
131
116
  end # API
132
117
  end # Github
@@ -45,6 +45,10 @@ module Github
45
45
  @events ||= ApiFactory.new 'Events', options
46
46
  end
47
47
 
48
+ def search(options = {})
49
+ @search ||= ApiFactory.new 'Search', options
50
+ end
51
+
48
52
  # An API for users to manage their own tokens. You can only access your own
49
53
  # tokens, and only through Basic Authentication.
50
54
  def oauth(options = {})
@@ -11,12 +11,16 @@ module Github
11
11
 
12
12
  CONTENT_LENGTH = 'content-length'.freeze
13
13
 
14
+ CACHE_CONTROL = 'cache-control'.freeze
15
+
14
16
  ETAG = 'ETag'.freeze
15
17
 
16
18
  SERVER = 'Server'.freeze
17
19
 
18
20
  DATE = 'Date'.freeze
19
21
 
22
+ LOCATION = 'Location'.freeze
23
+
20
24
  # Link headers
21
25
  HEADER_LINK = "Link".freeze
22
26
 
@@ -29,6 +29,7 @@ end # Github
29
29
  client_error
30
30
  invalid_options
31
31
  required_params
32
+ unknown_value
32
33
  ].each do |error|
33
34
  require "github_api/error/#{error}"
34
35
  end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+
3
+ module Github #:nodoc
4
+ # Raised when invalid options are passed to a request body
5
+ module Error
6
+ class UnknownValue < ClientError
7
+ def initialize(key, value, permitted)
8
+ super(
9
+ generate_message(
10
+ :problem => "Wrong value of '#{value}' for the parameter: #{key} provided for this request.",
11
+ :summary => "Github gem checks the request parameters passed to ensure that github api is not hit unnecessairly and fails fast.",
12
+ :resolution => "Permitted values are: #{permitted}, make sure these are the ones you are using"
13
+ )
14
+ )
15
+ end
16
+ end
17
+ end # Error
18
+ end # Github
@@ -32,6 +32,7 @@ module Github
32
32
  end
33
33
 
34
34
  # Removes any keys from nested hashes that don't match predefiend keys
35
+ # filter_valid_keys
35
36
  def _filter_params_keys(keys, params, options={:recursive => true}) # :nodoc:
36
37
  case params
37
38
  when Hash
@@ -7,8 +7,6 @@ module Github
7
7
  autoload_all 'github_api/gists',
8
8
  :Comments => 'comments'
9
9
 
10
- # include Github::Gists::Comments
11
-
12
10
  REQUIRED_GIST_INPUTS = %w[
13
11
  description
14
12
  public
@@ -63,9 +61,7 @@ module Github
63
61
  # github.gists.starred
64
62
  #
65
63
  def starred(params={})
66
- process_params do
67
- normalize params
68
- end
64
+ _normalize_params_keys(params)
69
65
 
70
66
  response = get_request("/gists/starred", params)
71
67
  return response unless block_given?
@@ -109,7 +105,7 @@ module Github
109
105
  #
110
106
  def create(params={})
111
107
  _normalize_params_keys(params)
112
- _validate_inputs(REQUIRED_GIST_INPUTS, params)
108
+ assert_required_keys(REQUIRED_GIST_INPUTS, params)
113
109
 
114
110
  post_request("/gists", params)
115
111
  end
@@ -55,7 +55,7 @@ module Github
55
55
  _normalize_params_keys(params)
56
56
  # _merge_mime_type(:gist_comment, params)
57
57
  _filter_params_keys(ALLOWED_GIST_COMMENT_INPUTS, params)
58
- _validate_inputs(REQUIRED_GIST_COMMENT_INPUTS, params)
58
+ assert_required_keys(REQUIRED_GIST_COMMENT_INPUTS, params)
59
59
 
60
60
  post_request("/gists/#{gist_id}/comments", params)
61
61
  end
@@ -71,7 +71,7 @@ module Github
71
71
  _validate_presence_of(comment_id)
72
72
  # _merge_mime_type(:gist_comment, params)
73
73
  _filter_params_keys(ALLOWED_GIST_COMMENT_INPUTS, params)
74
- _validate_inputs(REQUIRED_GIST_COMMENT_INPUTS, params)
74
+ assert_required_keys(REQUIRED_GIST_COMMENT_INPUTS, params)
75
75
 
76
76
  patch_request("/gists/comments/#{comment_id}", params)
77
77
  end
@@ -46,7 +46,7 @@ module Github
46
46
 
47
47
  _normalize_params_keys(params)
48
48
  _filter_params_keys(VALID_BLOB_PARAM_NAMES, params)
49
- _validate_inputs(VALID_BLOB_PARAM_NAMES, params)
49
+ assert_required_keys(VALID_BLOB_PARAM_NAMES, params)
50
50
 
51
51
  post_request("/repos/#{user}/#{repo}/git/blobs", params)
52
52
  end
@@ -78,7 +78,7 @@ module Github
78
78
  _validate_user_repo_params(user, repo) unless user? && repo?
79
79
  _normalize_params_keys(params)
80
80
  _filter_params_keys(VALID_COMMIT_PARAM_NAMES, params)
81
- _validate_inputs(REQUIRED_COMMIT_PARAMS, params)
81
+ assert_required_keys(REQUIRED_COMMIT_PARAMS, params)
82
82
 
83
83
  post_request("/repos/#{user}/#{repo}/git/commits", params)
84
84
  end
@@ -88,7 +88,7 @@ module Github
88
88
  _filter_params_keys VALID_REF_PARAM_NAMES, params
89
89
  _validate_presence_of params['ref']
90
90
  _validate_reference params['ref']
91
- _validate_inputs(%w[ ref sha ], params)
91
+ assert_required_keys(%w[ ref sha ], params)
92
92
 
93
93
  post_request("/repos/#{user}/#{repo}/git/refs", params)
94
94
  end
@@ -113,7 +113,7 @@ module Github
113
113
  _validate_reference ref
114
114
  _normalize_params_keys(params)
115
115
  _filter_params_keys(VALID_REF_PARAM_NAMES, params)
116
- _validate_inputs(%w[ sha ], params)
116
+ assert_required_keys(%w[ sha ], params)
117
117
 
118
118
  patch_request("/repos/#{user}/#{repo}/git/refs/#{ref}", params)
119
119
  end
@@ -77,7 +77,7 @@ module Github
77
77
  _normalize_params_keys(params)
78
78
 
79
79
  _filter_params_keys(VALID_TAG_PARAM_NAMES, params)
80
- _validate_params_values(VALID_TAG_PARAM_VALUES, params)
80
+ assert_valid_values(VALID_TAG_PARAM_VALUES, params)
81
81
 
82
82
  post_request("/repos/#{user}/#{repo}/git/tags", params)
83
83
  end
@@ -89,11 +89,10 @@ module Github
89
89
  _update_user_repo_params(user_name, repo_name)
90
90
  _validate_user_repo_params(user, repo) unless user? && repo?
91
91
  _normalize_params_keys(params)
92
-
93
- _validate_inputs(%w[ tree ], params)
92
+ assert_required_keys(%w[ tree ], params)
94
93
 
95
94
  _filter_params_keys(VALID_TREE_PARAM_NAMES, params['tree'])
96
- _validate_params_values(VALID_TREE_PARAM_VALUES, params['tree'])
95
+ assert_valid_values(VALID_TREE_PARAM_VALUES, params['tree'])
97
96
 
98
97
  post_request("/repos/#{user}/#{repo}/git/trees", params)
99
98
  end
@@ -82,11 +82,12 @@ module Github
82
82
  # :sort => 'comments',
83
83
  # :direction => 'asc'
84
84
  #
85
- def list(params={})
85
+ def list(*args)
86
+ params = args.extract_options!
86
87
  _normalize_params_keys(params)
87
88
  _filter_params_keys(VALID_ISSUE_PARAM_NAMES, params)
88
89
  # _merge_mime_type(:issue, params)
89
- _validate_params_values(VALID_ISSUE_PARAM_VALUES, params)
90
+ assert_valid_values(VALID_ISSUE_PARAM_VALUES, params)
90
91
 
91
92
  response = get_request("/issues", params)
92
93
  return response unless block_given?
@@ -124,15 +125,14 @@ module Github
124
125
  # :sort => 'comments',
125
126
  # :direction => 'asc'
126
127
  #
127
- # TODO: remove default nils from params
128
- def list_repo(user_name=nil, repo_name=nil, params={})
128
+ def list_repo(user_name, repo_name, params={})
129
129
  _update_user_repo_params(user_name, repo_name)
130
130
  _validate_user_repo_params(user, repo) unless user? && repo?
131
131
 
132
132
  _normalize_params_keys(params)
133
133
  _filter_params_keys(VALID_ISSUE_PARAM_NAMES, params)
134
134
  # _merge_mime_type(:issue, params)
135
- _validate_params_values(VALID_ISSUE_PARAM_VALUES, params)
135
+ assert_valid_values(VALID_ISSUE_PARAM_VALUES, params)
136
136
 
137
137
  response = get_request("/repos/#{user}/#{repo}/issues", params)
138
138
  return response unless block_given?
@@ -185,7 +185,7 @@ module Github
185
185
  _normalize_params_keys(params)
186
186
  # _merge_mime_type(:issue, params)
187
187
  _filter_params_keys(VALID_ISSUE_PARAM_NAMES, params)
188
- _validate_inputs(%w[ title ], params)
188
+ assert_required_keys(%w[ title ], params)
189
189
 
190
190
  post_request("/repos/#{user}/#{repo}/issues", params)
191
191
  end
@@ -71,7 +71,7 @@ module Github
71
71
  _normalize_params_keys(params)
72
72
  # _merge_mime_type(:issue_comment, params)
73
73
  _filter_params_keys(VALID_ISSUE_COMMENT_PARAM_NAME, params)
74
- _validate_inputs(%w[ body ], params)
74
+ assert_required_keys(%w[ body ], params)
75
75
 
76
76
  post_request("/repos/#{user}/#{repo}/issues/#{issue_id}/comments", params)
77
77
  end
@@ -94,7 +94,7 @@ module Github
94
94
  _normalize_params_keys(params)
95
95
  # _merge_mime_type(:issue_comment, params)
96
96
  _filter_params_keys(VALID_ISSUE_COMMENT_PARAM_NAME, params)
97
- _validate_inputs(%w[ body ], params)
97
+ assert_required_keys(%w[ body ], params)
98
98
 
99
99
  patch_request("/repos/#{user}/#{repo}/issues/comments/#{comment_id}")
100
100
  end
@@ -62,7 +62,7 @@ module Github
62
62
 
63
63
  _normalize_params_keys(params)
64
64
  _filter_params_keys(VALID_LABEL_INPUTS, params)
65
- _validate_inputs(VALID_LABEL_INPUTS, params)
65
+ assert_required_keys(VALID_LABEL_INPUTS, params)
66
66
 
67
67
  post_request("/repos/#{user}/#{repo}/labels", params)
68
68
  end
@@ -85,7 +85,7 @@ module Github
85
85
 
86
86
  _normalize_params_keys(params)
87
87
  _filter_params_keys(VALID_LABEL_INPUTS, params)
88
- _validate_inputs(VALID_LABEL_INPUTS, params)
88
+ assert_required_keys(VALID_LABEL_INPUTS, params)
89
89
 
90
90
  patch_request("/repos/#{user}/#{repo}/labels/#{label_id}", params)
91
91
  end
@@ -44,7 +44,7 @@ module Github
44
44
 
45
45
  _normalize_params_keys(params)
46
46
  _filter_params_keys(VALID_MILESTONE_OPTIONS.keys, params)
47
- _validate_params_values(VALID_MILESTONE_OPTIONS, params)
47
+ assert_valid_values(VALID_MILESTONE_OPTIONS, params)
48
48
 
49
49
  response = get_request("/repos/#{user}/#{repo}/milestones", params)
50
50
  return response unless block_given?
@@ -89,7 +89,7 @@ module Github
89
89
 
90
90
  _normalize_params_keys(params)
91
91
  _filter_params_keys(VALID_MILESTONE_INPUTS, params)
92
- _validate_inputs(%w[ title ], params)
92
+ assert_required_keys(%w[ title ], params)
93
93
 
94
94
  post_request("/repos/#{user}/#{repo}/milestones", params)
95
95
  end
@@ -117,7 +117,7 @@ module Github
117
117
 
118
118
  _normalize_params_keys(params)
119
119
  _filter_params_keys(VALID_MILESTONE_INPUTS, params)
120
- _validate_inputs(%w[ title ], params)
120
+ assert_required_keys(%w[ title ], params)
121
121
 
122
122
  patch_request("/repos/#{user}/#{repo}/milestones/#{milestone_id}", params)
123
123
  end
@@ -65,8 +65,8 @@ module Github
65
65
  _validate_presence_of org_name
66
66
  _normalize_params_keys(params)
67
67
  _filter_params_keys(VALID_TEAM_PARAM_NAMES, params)
68
- _validate_params_values(VALID_TEAM_PARAM_VALUES, params)
69
- _validate_inputs(%w[ name ], params)
68
+ assert_valid_values(VALID_TEAM_PARAM_VALUES, params)
69
+ assert_required_keys(%w[ name ], params)
70
70
 
71
71
  post_request("/orgs/#{org_name}/teams", params)
72
72
  end
@@ -92,8 +92,8 @@ module Github
92
92
  _normalize_params_keys(params)
93
93
 
94
94
  _filter_params_keys(VALID_TEAM_PARAM_NAMES, params)
95
- _validate_params_values(VALID_TEAM_PARAM_VALUES, params)
96
- _validate_inputs(%w[ name ], params)
95
+ assert_valid_values(VALID_TEAM_PARAM_VALUES, params)
96
+ assert_required_keys(%w[ name ], params)
97
97
 
98
98
  patch_request("/teams/#{team_name}", params)
99
99
  end
@@ -0,0 +1,31 @@
1
+ module Github
2
+ class ParamsHash < ::Hash
3
+
4
+ def initialize(*args, &block)
5
+ hash = args.extract_options!
6
+ debugger
7
+ # debugger
8
+ # normalize_keys!(hash)
9
+ # debugger
10
+ super[hash]
11
+ end
12
+
13
+ def normalize_keys!(params)
14
+ case params
15
+ when Hash
16
+ params.keys.each do |k|
17
+ params[k.to_s] = params.delete(k)
18
+ normalize_keys!(params[k.to_s])
19
+ end
20
+ when Array
21
+ params.map! do |el|
22
+ normalize_keys!(el)
23
+ end
24
+ else
25
+ params.to_s
26
+ end
27
+ return params
28
+ end
29
+
30
+ end # ParamsHash
31
+ end # Github