jekyll-github-pages-gem 1.0.0 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Services
4
4
  ##
5
- # The base class for service classes responsible for performing operations on posts
6
- class BasePostService
5
+ # The base class for service classes responsible for performing operations on the Jekyll website
6
+ class BaseEditingService
7
7
  def initialize(repo_name, access_token)
8
8
  @github_service = GithubService.new(repo_name, access_token)
9
9
  @kramdown_service = KramdownService.new
@@ -4,8 +4,6 @@ require 'octokit'
4
4
  require 'base64'
5
5
  require 'date'
6
6
  require 'cgi'
7
- require_relative 'kramdown_service'
8
- require_relative '../factories/post_factory'
9
7
 
10
8
  module Services
11
9
  ##
@@ -13,92 +11,13 @@ module Services
13
11
  class GithubService
14
12
  def initialize(full_repo_name, access_token)
15
13
  @full_repo_name = full_repo_name
16
- @access_token = access_token
17
-
18
- @kramdown_service = Services::KramdownService.new
19
- @post_factory = Factories::PostFactory.new
20
- end
21
-
22
- ##
23
- # This method fetches all the markdown contents of all the posts on a Jekyll website
24
- # that have been written and returns a list of models representing a Post.
25
- def get_all_posts
26
- result = []
27
- client = create_octokit_client
28
- posts = client.contents(@full_repo_name, path: '_posts')
29
- posts.each do |post|
30
- post_api_response = client.contents(@full_repo_name, path: post.path)
31
-
32
- post_model = create_post_from_api_response(post_api_response, nil)
33
- image_paths = @kramdown_service.get_all_image_paths(post_model.contents)
34
-
35
- images = []
36
- image_paths.each do |image_path|
37
- image_content = client.contents(@full_repo_name, path: image_path)
38
- images << create_post_image(image_path, image_content.content)
39
- end
40
-
41
- post_model.images = images
42
-
43
- result << post_model
44
- end
45
- result
46
- end
47
-
48
- ##
49
- # This method fetches all of the posts that have been written but have not been merged into master yet.
50
- def get_all_posts_in_pr(pr_body)
51
- result = []
52
- client = create_octokit_client
53
- pull_requests_for_user = get_open_jekyll_pull_requests(pr_body)
54
-
55
- pull_requests_for_user.each do |pull_request|
56
- pull_request_files = client.pull_request_files(@full_repo_name, pull_request[:number])
57
-
58
- post = nil
59
- images = []
60
- pull_request_files.each do |pull_request_file|
61
- contents_url_params = CGI.parse(pull_request_file[:contents_url])
62
-
63
- # The CGI.parse method returns a hash with the key being the URL and the value being an array of
64
- # URI parameters so in order to get the ref we need to grab the first value in the hash and the first
65
- # URI parameter in the first hash value
66
- ref = contents_url_params.values.first.first
67
- file_contents = client.contents(@full_repo_name, path: pull_request_file[:filename], ref: ref)
68
-
69
- if pull_request_file[:filename].end_with?('.md')
70
- post = create_post_from_api_response(file_contents, ref)
71
- result << post
72
- else
73
- images << create_post_image(pull_request_file[:filename], file_contents.content)
74
- end
75
- end
76
-
77
- post.images = images
78
- end
79
- result
80
- end
81
-
82
- ##
83
- # This method fetches a single post from a Jekyll website given a post title
84
- # and returns a Post model
85
- #
86
- # Params:
87
- # +title+:: A title of a Jekyll website post
88
- # +ref+::a sha for a ref indicating the head of a branch a post is pushed to on the GitHub server
89
- def get_post_by_title(title, ref)
90
- result = nil
91
- result = get_all_posts_in_pr.find { |x| x.title == title } if ref
92
- result = get_all_posts.find { |x| x.title == title } unless ref
93
- result&.images&.each { |x| PostImageManager.instance.add_downloaded_image(x) }
94
- result
14
+ @client = Octokit::Client.new(access_token: access_token)
95
15
  end
96
16
 
97
17
  ##
98
18
  # This method gets the sha of the commit at the head of master in a Jekyll website repo
99
19
  def get_master_head_sha
100
- client = create_octokit_client
101
- client.ref(@full_repo_name, 'heads/master')[:object][:sha]
20
+ @client.ref(@full_repo_name, 'heads/master')[:object][:sha]
102
21
  end
103
22
 
104
23
  ##
@@ -107,8 +26,7 @@ module Services
107
26
  # Params
108
27
  # +head_sha+::the sha of the head of a certain branch
109
28
  def get_base_tree_for_branch(head_sha)
110
- client = create_octokit_client
111
- client.commit(@full_repo_name, head_sha)[:commit][:tree][:sha]
29
+ @client.commit(@full_repo_name, head_sha)[:commit][:tree][:sha]
112
30
  end
113
31
 
114
32
  ##
@@ -117,8 +35,7 @@ module Services
117
35
  # Params
118
36
  # +text+::the text content to create a blob for
119
37
  def create_text_blob(text)
120
- client = create_octokit_client
121
- client.create_blob(@full_repo_name, text)
38
+ @client.create_blob(@full_repo_name, text)
122
39
  end
123
40
 
124
41
  ##
@@ -127,8 +44,7 @@ module Services
127
44
  # Params
128
45
  # +content+::the base 64 encoded content to create a blob for
129
46
  def create_base64_encoded_blob(content)
130
- client = create_octokit_client
131
- client.create_blob(@full_repo_name, content, 'base64')
47
+ @client.create_blob(@full_repo_name, content, 'base64')
132
48
  end
133
49
 
134
50
  ##
@@ -140,7 +56,6 @@ module Services
140
56
  # +file_information+::an array of hashes containing the file path and the blob sha for a file
141
57
  # +sha_base_tree+::the sha of the base tree
142
58
  def create_new_tree_with_blobs(file_information, sha_base_tree)
143
- client = create_octokit_client
144
59
  blob_information = []
145
60
  file_information.each do |file|
146
61
  # This mode property on this hash represents the file mode for a GitHub tree.
@@ -150,24 +65,24 @@ module Services
150
65
  type: 'blob',
151
66
  sha: file[:blob_sha] }
152
67
  end
153
- client.create_tree(@full_repo_name, blob_information, base_tree: sha_base_tree)[:sha]
68
+ @client.create_tree(@full_repo_name, blob_information, base_tree: sha_base_tree)[:sha]
154
69
  end
155
70
 
156
71
  ##
157
- # This method commits and pushes a tree to a Jekyll website repo
72
+ # This method commits and pushes a tree to a Jekyll website repo and returns the sha of the new commit
158
73
  #
159
74
  # Params:
160
75
  # +commit_message+::the message for the new commit
161
76
  # +tree_sha+::the sha of the tree to commit
162
77
  # +head_sha+::the sha of the head to commit from
163
78
  def commit_and_push_to_repo(commit_message, tree_sha, head_sha, ref_name)
164
- client = create_octokit_client
165
- sha_new_commit = client.create_commit(@full_repo_name, commit_message, tree_sha, head_sha)[:sha]
166
- client.update_ref(@full_repo_name, ref_name, sha_new_commit)
79
+ sha_new_commit = @client.create_commit(@full_repo_name, commit_message, tree_sha, head_sha)[:sha]
80
+ @client.update_ref(@full_repo_name, ref_name, sha_new_commit)
81
+ sha_new_commit
167
82
  end
168
83
 
169
84
  ##
170
- # This method creates a pull request for a branch in a Jekyll website repo
85
+ # This method creates a pull request for a branch in a Jekyll website repo and returns the url of the newly created pull request
171
86
  #
172
87
  # Params:
173
88
  # +source_branch+::the source branch for the PR
@@ -176,9 +91,9 @@ module Services
176
91
  # +pr_body+::the body for the PR
177
92
  # +reviewers+::an array of pull request reviewers for the PR
178
93
  def create_pull_request(source_branch, base_branch, pr_title, pr_body, reviewers)
179
- client = create_octokit_client
180
- pull_number = client.create_pull_request(@full_repo_name, base_branch, source_branch, pr_title, pr_body)[:number]
181
- client.request_pull_request_review(@full_repo_name, pull_number, reviewers: reviewers)
94
+ pull_request = @client.create_pull_request(@full_repo_name, base_branch, source_branch, pr_title, pr_body)
95
+ @client.request_pull_request_review(@full_repo_name, pull_request[:number], reviewers: reviewers)
96
+ pull_request[:html_url]
182
97
  end
183
98
 
184
99
  ##
@@ -189,10 +104,9 @@ module Services
189
104
  # +ref_name+:: the name of the branch to create if necessary
190
105
  # +master_head_sha+:: the sha representing the head of master
191
106
  def create_ref_if_necessary(ref_name, master_head_sha)
192
- client = create_octokit_client
193
- client.ref(@full_repo_name, ref_name)
107
+ @client.ref(@full_repo_name, ref_name)
194
108
  rescue Octokit::NotFound
195
- client.create_ref(@full_repo_name, ref_name, master_head_sha)
109
+ @client.create_ref(@full_repo_name, ref_name, master_head_sha)
196
110
  end
197
111
 
198
112
  ##
@@ -200,38 +114,71 @@ module Services
200
114
  # It will also strip off the starting refs portion of the name
201
115
  #
202
116
  # Params:
203
- # +oauth_token+::a user's oauth access token
204
117
  # +ref_sha+:: the sha of the ref to fetch
205
118
  def get_ref_name_by_sha(ref_sha)
206
- client = create_octokit_client
207
- ref_response = client.refs(@full_repo_name).find { |x| x[:object][:sha] == ref_sha }
119
+ ref_response = @client.refs(@full_repo_name).find { |x| x[:object][:sha] == ref_sha }
208
120
  ref_response[:ref].match(%r{refs/(.*)}).captures.first
209
121
  end
210
122
 
211
- private
123
+ ##
124
+ # This method will fetch and decode contents of a given file with text contents on GitHub.
125
+ # By default, it will fetch the file contents from the master branch unless a ref to a branch
126
+ # is supplied
127
+ #
128
+ # Params:
129
+ # +file_path+::the path to a file in a GitHub repo
130
+ # +ref+::an optional ref to a branch to fetch the file from
131
+ def get_text_contents_from_file(file_path, ref = nil)
132
+ api_contents = if ref
133
+ @client.contents(@full_repo_name, path: file_path, ref: ref)
134
+ else
135
+ @client.contents(@full_repo_name, path: file_path)
136
+ end
137
+ Base64.decode64(api_contents.content).dup.force_encoding('UTF-8')
138
+ end
212
139
 
213
- def create_post_from_api_response(post, ref)
214
- # Base64.decode64 will convert our string into a ASCII string
215
- # calling force_encoding('UTF-8') will fix that problem
216
- text_contents = Base64.decode64(post.content).dup.force_encoding('UTF-8')
217
- @post_factory.create_post(text_contents, post.path, ref)
140
+ ##
141
+ # This method will fetch the GitHub contents for a given file on GitHub via the GitHub
142
+ # contents API. The full response from the API will be returned
143
+ #
144
+ # Params:
145
+ # +path+::the path to a file in a GitHub repo
146
+ def get_contents_from_path(path)
147
+ @client.contents(@full_repo_name, path: path)
218
148
  end
219
149
 
220
- def get_open_jekyll_pull_requests(pull_request_body)
221
- client = create_octokit_client
222
- open_pull_requests = client.pull_requests(@full_repo_name, state: 'open')
223
- open_pull_requests.select { |x| x[:body] == pull_request_body }
150
+ ##
151
+ # This method will fetch all open pull requests for the current user matching a specific PR body
152
+ #
153
+ # Params:
154
+ # +pull_request_body+::the body of the PR to look for
155
+ def get_open_pull_requests_with_body(pull_request_body)
156
+ open_pull_requests = @client.pull_requests(@full_repo_name, state: 'open')
157
+ open_pull_requests.select { |x| x[:body] == pull_request_body && x[:user][:login] == @client.user[:login] }
224
158
  end
225
159
 
226
- def create_post_image(filename, contents)
227
- result = PostImage.new
228
- result.filename = filename
229
- result.contents = contents
230
- result
160
+ ##
161
+ # This method will fetch all pull request files for a given pull request
162
+ #
163
+ # Params:
164
+ # +pr_number+::the pull request number for the pull request to get all files for
165
+ def get_pr_files(pr_number)
166
+ @client.pull_request_files(@full_repo_name, pr_number)
231
167
  end
232
168
 
233
- def create_octokit_client
234
- Octokit::Client.new(access_token: @access_token)
169
+ ##
170
+ # Parses the URL for a file's contents to determine the ref of the file
171
+ # The ref is used to determine what branch the file is located on
172
+ #
173
+ # Params:
174
+ # +contents_url+::the contents url for a file in a GitHub repo
175
+ def get_ref_from_contents_url(contents_url)
176
+ contents_url_params = CGI.parse(contents_url)
177
+
178
+ # The CGI.parse method returns a hash with the key being the URL and the value being an array of
179
+ # URI parameters so in order to get the ref we need to grab the first value in the hash and the first
180
+ # URI parameter in the first hash value
181
+ contents_url_params.values.first.first
235
182
  end
236
183
  end
237
184
  end
@@ -41,7 +41,7 @@ end
41
41
 
42
42
  module Services
43
43
  ##
44
- # This class contains all operations with interacting with the kramdown engine
44
+ # This class contains operations related to the kramdown engine
45
45
  class KramdownService
46
46
  DEFAULT_HERO = 'https://source.unsplash.com/collection/145103/'
47
47
  ##
@@ -91,75 +91,8 @@ module Services
91
91
  result.compact
92
92
  end
93
93
 
94
- ##
95
- # This method takes parameters for a given post and formats them
96
- # as a valid jekyll post for a Jekyll website
97
- #
98
- # Params:
99
- # +text+:: the markdown contents of the post
100
- # +author+:: the author of the post
101
- # +title+:: the title of the post
102
- # +tags+:: tags specific to the post
103
- # +overlay+:: the overlay color of the post
104
- # +hero+:: a link to an optional background image for a post
105
- def create_jekyll_post_text(text, author, title, tags, overlay, hero)
106
- header_converted_text = fix_header_syntax(text)
107
- header_converted_text = add_line_break_to_markdown_if_necessary(header_converted_text)
108
-
109
- parsed_tags = parse_tags(tags)
110
-
111
- tag_section = %(tags:
112
- #{parsed_tags})
113
-
114
- lead_break_section = "{: .lead}\r\n<!–-break-–>"
115
-
116
- hero_to_use = hero
117
- hero_to_use = DEFAULT_HERO if hero_to_use.empty?
118
- result = %(---
119
- layout: post
120
- title: #{title}
121
- author: #{author}\r\n)
122
-
123
- result += "#{tag_section}\r\n" unless parsed_tags.empty?
124
- result += %(hero: #{hero_to_use}
125
- overlay: #{overlay.downcase}
126
- published: true
127
- ---
128
- #{lead_break_section}
129
- #{header_converted_text})
130
-
131
- result
132
- end
133
-
134
94
  private
135
95
 
136
- def parse_tags(tags)
137
- tag_array = tags.split(',')
138
- result = ''
139
- tag_array.each do |tag|
140
- result += " - #{tag.strip}"
141
- result += "\r\n" if tag != tag_array.last
142
- end
143
- result
144
- end
145
-
146
- def fix_header_syntax(text)
147
- document = Kramdown::Document.new(text)
148
- header_elements = document.root.children.select { |x| x.type == :header }
149
- lines = text.split("\n")
150
- lines = lines.map do |line|
151
- if header_elements.any? { |x| line.include? x.options[:raw_text] }
152
- # This regex matches the line into 2 groups with the first group being the repeating #
153
- # characters and the beginning of the string and the second group being the rest of the string
154
- line_match = line.match(/(#*)(.*)/)
155
- line = "#{line_match.captures.first} #{line_match.captures.last.strip}"
156
- else
157
- line.delete("\r\n")
158
- end
159
- end
160
- lines.join("\r\n")
161
- end
162
-
163
96
  def get_document_descendants(current_element, result)
164
97
  current_element.children.each do |element|
165
98
  result << element
@@ -170,17 +103,5 @@ published: true
170
103
  def get_filename_for_image_tag(image_el)
171
104
  File.basename(image_el.attr['src'])
172
105
  end
173
-
174
- def add_line_break_to_markdown_if_necessary(markdown)
175
- lines = markdown.split("\n")
176
- # The regular expression in the if statement looks for a markdown reference to a link like
177
- # [logo]: https://ieeextreme.org/wp-content/uploads/2019/05/Xtreme_colour-e1557478323964.png
178
- # If a post starts with that reference in jekyll followed by an image using that reference
179
- # the line below will be interperted as a paragraph tag instead of an image tag. To fix that
180
- # we add a line break to the start of the markdown.
181
- return "\r\n#{markdown}" if lines.first&.match?(/\[(.*)\]: (.*)/)
182
-
183
- markdown
184
- end
185
106
  end
186
107
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../factories/page_factory'
4
+
5
+ module Services
6
+ ##
7
+ # This class contains all operations related to pages on a Jekyll website
8
+ class PageService < BaseEditingService
9
+ def initialize(repo_name, access_token)
10
+ super(repo_name, access_token)
11
+ @page_factory = Factories::PageFactory.new
12
+ end
13
+
14
+ ##
15
+ # Returns a given page from a Jekyll website from the default branch unless a pull request body
16
+ # is specified. In that case then it will return the page from the source branch of the first open
17
+ # pull request matching the given body
18
+ #
19
+ # Params:
20
+ # +file_path+:: the path to the file in a GitHub repository
21
+ # +pr_body+:: an optional parameter indicating the pull request body of any updates to a given page, defaults to nil
22
+ def get_markdown_page(file_path, pr_body = nil)
23
+ if pr_body
24
+ open_prs = @github_service.get_open_pull_requests_with_body(pr_body)
25
+ unless open_prs.empty?
26
+ pr_files = @github_service.get_pr_files(open_prs[0][:number])
27
+ markdown_file = pr_files.find { |file| file[:filename].end_with?('.md') }
28
+ if markdown_file
29
+ ref = @github_service.get_ref_from_contents_url(markdown_file[:contents_url])
30
+ text_contents = @github_service.get_text_contents_from_file(file_path, ref)
31
+ return @page_factory.create_page(text_contents, ref, open_prs[0][:html_url])
32
+ end
33
+ end
34
+ end
35
+
36
+ text_contents = @github_service.get_text_contents_from_file(file_path)
37
+ @page_factory.create_page(text_contents, nil, nil)
38
+ end
39
+
40
+ ##
41
+ # Saves a given page update by updating the page contents and creating a pull request into master
42
+ # if a ref is not given. Otherwise if a ref is supplied it will update the branch matching the given ref without creating a pull request.
43
+ #
44
+ # Params:
45
+ # +file_path+:: the path to the file in a GitHub repository
46
+ # +page_title+:: the title of the page
47
+ # +ref+::an optional branch indicating the page should be updated on a branch that's not the default branch, defaults to nil
48
+ # +pr_body+::an optional pull request body when updating the page on the default branch, defaults to an empty string
49
+ # +reviewers+::an optional array of reviewers for opening a pull request when updating the page on the default branch, defaults to no reviewers
50
+ def save_page_update(file_path, page_title, file_contents, ref = nil, pr_body = '', reviewers = [])
51
+ if ref
52
+ ref_name = @github_service.get_ref_name_by_sha(ref)
53
+ sha_base_tree = @github_service.get_base_tree_for_branch(ref)
54
+
55
+ new_tree_sha = create_new_tree(file_contents, page_title, file_path, sha_base_tree)
56
+ @github_service.commit_and_push_to_repo("Edited page #{page_title}", new_tree_sha, ref, ref_name)
57
+ nil
58
+ else
59
+ branch_name = "editPage#{page_title.gsub(/\s+/, '')}"
60
+ ref_name = "heads/#{branch_name}"
61
+
62
+ master_head_sha = @github_service.get_master_head_sha
63
+ sha_base_tree = @github_service.get_base_tree_for_branch(master_head_sha)
64
+
65
+ @github_service.create_ref_if_necessary(ref_name, master_head_sha)
66
+ new_tree_sha = create_new_tree(file_contents, page_title, file_path, sha_base_tree)
67
+
68
+ ref_sha = @github_service.commit_and_push_to_repo("Edited page #{page_title}", new_tree_sha, master_head_sha, ref_name)
69
+ pull_request_url = @github_service.create_pull_request(branch_name, 'master', "Edited page #{page_title}",
70
+ pr_body,
71
+ reviewers)
72
+ create_save_page_update_result(ref_sha, pull_request_url)
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def create_save_page_update_result(ref_sha, pull_request_url)
79
+ result = Page.new
80
+ result.github_ref = ref_sha
81
+ result.pull_request_url = pull_request_url
82
+ result
83
+ end
84
+ end
85
+ end