PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/github/import.rb

https://gitlab.com/certik/gitlab-ce
Ruby | 392 lines | 308 code | 73 blank | 11 comment | 26 complexity | 1aecffdad7c5d3d651b89010aa90e0c3 MD5 | raw file
  1. require_relative 'error'
  2. module Github
  3. class Import
  4. include Gitlab::ShellAdapter
  5. class MergeRequest < ::MergeRequest
  6. self.table_name = 'merge_requests'
  7. self.reset_callbacks :create
  8. self.reset_callbacks :save
  9. self.reset_callbacks :commit
  10. self.reset_callbacks :update
  11. self.reset_callbacks :validate
  12. end
  13. class Issue < ::Issue
  14. self.table_name = 'issues'
  15. self.reset_callbacks :save
  16. self.reset_callbacks :create
  17. self.reset_callbacks :commit
  18. self.reset_callbacks :update
  19. self.reset_callbacks :validate
  20. end
  21. class Note < ::Note
  22. self.table_name = 'notes'
  23. self.reset_callbacks :save
  24. self.reset_callbacks :commit
  25. self.reset_callbacks :update
  26. self.reset_callbacks :validate
  27. end
  28. class LegacyDiffNote < ::LegacyDiffNote
  29. self.table_name = 'notes'
  30. self.reset_callbacks :commit
  31. self.reset_callbacks :update
  32. self.reset_callbacks :validate
  33. end
  34. attr_reader :project, :repository, :repo, :repo_url, :wiki_url,
  35. :options, :errors, :cached, :verbose
  36. def initialize(project, options = {})
  37. @project = project
  38. @repository = project.repository
  39. @repo = project.import_source
  40. @repo_url = project.import_url
  41. @wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
  42. @options = options.reverse_merge(token: project.import_data&.credentials&.fetch(:user))
  43. @verbose = options.fetch(:verbose, false)
  44. @cached = Hash.new { |hash, key| hash[key] = Hash.new }
  45. @errors = []
  46. end
  47. # rubocop: disable Rails/Output
  48. def execute
  49. puts 'Fetching repository...'.color(:aqua) if verbose
  50. fetch_repository
  51. puts 'Fetching labels...'.color(:aqua) if verbose
  52. fetch_labels
  53. puts 'Fetching milestones...'.color(:aqua) if verbose
  54. fetch_milestones
  55. puts 'Fetching pull requests...'.color(:aqua) if verbose
  56. fetch_pull_requests
  57. puts 'Fetching issues...'.color(:aqua) if verbose
  58. fetch_issues
  59. puts 'Fetching releases...'.color(:aqua) if verbose
  60. fetch_releases
  61. puts 'Cloning wiki repository...'.color(:aqua) if verbose
  62. fetch_wiki_repository
  63. puts 'Expiring repository cache...'.color(:aqua) if verbose
  64. expire_repository_cache
  65. true
  66. rescue Github::RepositoryFetchError
  67. expire_repository_cache
  68. false
  69. ensure
  70. keep_track_of_errors
  71. end
  72. private
  73. def fetch_repository
  74. begin
  75. project.ensure_repository
  76. project.repository.add_remote('github', repo_url)
  77. project.repository.set_remote_as_mirror('github')
  78. project.repository.fetch_remote('github', forced: true)
  79. rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
  80. error(:project, repo_url, e.message)
  81. raise Github::RepositoryFetchError
  82. end
  83. end
  84. def fetch_wiki_repository
  85. return if project.wiki.repository_exists?
  86. wiki_path = "#{project.disk_path}.wiki"
  87. gitlab_shell.import_repository(project.repository_storage_path, wiki_path, wiki_url)
  88. rescue Gitlab::Shell::Error => e
  89. # GitHub error message when the wiki repo has not been created,
  90. # this means that repo has wiki enabled, but have no pages. So,
  91. # we can skip the import.
  92. if e.message !~ /repository not exported/
  93. error(:wiki, wiki_url, e.message)
  94. end
  95. end
  96. def fetch_labels
  97. url = "/repos/#{repo}/labels"
  98. while url
  99. response = Github::Client.new(options).get(url)
  100. response.body.each do |raw|
  101. begin
  102. representation = Github::Representation::Label.new(raw)
  103. label = project.labels.find_or_create_by!(title: representation.title) do |label|
  104. label.color = representation.color
  105. end
  106. cached[:label_ids][label.title] = label.id
  107. rescue => e
  108. error(:label, representation.url, e.message)
  109. end
  110. end
  111. url = response.rels[:next]
  112. end
  113. end
  114. def fetch_milestones
  115. url = "/repos/#{repo}/milestones"
  116. while url
  117. response = Github::Client.new(options).get(url, state: :all)
  118. response.body.each do |raw|
  119. begin
  120. milestone = Github::Representation::Milestone.new(raw)
  121. next if project.milestones.where(iid: milestone.iid).exists?
  122. project.milestones.create!(
  123. iid: milestone.iid,
  124. title: milestone.title,
  125. description: milestone.description,
  126. due_date: milestone.due_date,
  127. state: milestone.state,
  128. created_at: milestone.created_at,
  129. updated_at: milestone.updated_at
  130. )
  131. rescue => e
  132. error(:milestone, milestone.url, e.message)
  133. end
  134. end
  135. url = response.rels[:next]
  136. end
  137. end
  138. def fetch_pull_requests
  139. url = "/repos/#{repo}/pulls"
  140. while url
  141. response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
  142. response.body.each do |raw|
  143. pull_request = Github::Representation::PullRequest.new(raw, options.merge(project: project))
  144. merge_request = MergeRequest.find_or_initialize_by(iid: pull_request.iid, source_project_id: project.id)
  145. next unless merge_request.new_record? && pull_request.valid?
  146. begin
  147. pull_request.restore_branches!
  148. author_id = user_id(pull_request.author, project.creator_id)
  149. description = format_description(pull_request.description, pull_request.author)
  150. merge_request.attributes = {
  151. iid: pull_request.iid,
  152. title: pull_request.title,
  153. description: description,
  154. source_project: pull_request.source_project,
  155. source_branch: pull_request.source_branch_name,
  156. source_branch_sha: pull_request.source_branch_sha,
  157. target_project: pull_request.target_project,
  158. target_branch: pull_request.target_branch_name,
  159. target_branch_sha: pull_request.target_branch_sha,
  160. state: pull_request.state,
  161. milestone_id: milestone_id(pull_request.milestone),
  162. author_id: author_id,
  163. assignee_id: user_id(pull_request.assignee),
  164. created_at: pull_request.created_at,
  165. updated_at: pull_request.updated_at
  166. }
  167. merge_request.save!(validate: false)
  168. merge_request.merge_request_diffs.create
  169. # Fetch review comments
  170. review_comments_url = "/repos/#{repo}/pulls/#{pull_request.iid}/comments"
  171. fetch_comments(merge_request, :review_comment, review_comments_url, LegacyDiffNote)
  172. # Fetch comments
  173. comments_url = "/repos/#{repo}/issues/#{pull_request.iid}/comments"
  174. fetch_comments(merge_request, :comment, comments_url)
  175. rescue => e
  176. error(:pull_request, pull_request.url, e.message)
  177. ensure
  178. pull_request.remove_restored_branches!
  179. end
  180. end
  181. url = response.rels[:next]
  182. end
  183. end
  184. def fetch_issues
  185. url = "/repos/#{repo}/issues"
  186. while url
  187. response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
  188. response.body.each { |raw| populate_issue(raw) }
  189. url = response.rels[:next]
  190. end
  191. end
  192. def populate_issue(raw)
  193. representation = Github::Representation::Issue.new(raw, options)
  194. begin
  195. # Every pull request is an issue, but not every issue
  196. # is a pull request. For this reason, "shared" actions
  197. # for both features, like manipulating assignees, labels
  198. # and milestones, are provided within the Issues API.
  199. if representation.pull_request?
  200. return unless representation.has_labels?
  201. merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
  202. merge_request.update_attribute(:label_ids, label_ids(representation.labels))
  203. else
  204. return if Issue.where(iid: representation.iid, project_id: project.id).exists?
  205. author_id = user_id(representation.author, project.creator_id)
  206. issue = Issue.new
  207. issue.iid = representation.iid
  208. issue.project_id = project.id
  209. issue.title = representation.title
  210. issue.description = format_description(representation.description, representation.author)
  211. issue.state = representation.state
  212. issue.label_ids = label_ids(representation.labels)
  213. issue.milestone_id = milestone_id(representation.milestone)
  214. issue.author_id = author_id
  215. issue.assignee_ids = [user_id(representation.assignee)]
  216. issue.created_at = representation.created_at
  217. issue.updated_at = representation.updated_at
  218. issue.save!(validate: false)
  219. # Fetch comments
  220. if representation.has_comments?
  221. comments_url = "/repos/#{repo}/issues/#{issue.iid}/comments"
  222. fetch_comments(issue, :comment, comments_url)
  223. end
  224. end
  225. rescue => e
  226. error(:issue, representation.url, e.message)
  227. end
  228. end
  229. def fetch_comments(noteable, type, url, klass = Note)
  230. while url
  231. comments = Github::Client.new(options).get(url)
  232. ActiveRecord::Base.no_touching do
  233. comments.body.each do |raw|
  234. begin
  235. representation = Github::Representation::Comment.new(raw, options)
  236. author_id = user_id(representation.author, project.creator_id)
  237. note = klass.new
  238. note.project_id = project.id
  239. note.noteable = noteable
  240. note.note = format_description(representation.note, representation.author)
  241. note.commit_id = representation.commit_id
  242. note.line_code = representation.line_code
  243. note.author_id = author_id
  244. note.created_at = representation.created_at
  245. note.updated_at = representation.updated_at
  246. note.save!(validate: false)
  247. rescue => e
  248. error(type, representation.url, e.message)
  249. end
  250. end
  251. end
  252. url = comments.rels[:next]
  253. end
  254. end
  255. def fetch_releases
  256. url = "/repos/#{repo}/releases"
  257. while url
  258. response = Github::Client.new(options).get(url)
  259. response.body.each do |raw|
  260. representation = Github::Representation::Release.new(raw)
  261. next unless representation.valid?
  262. release = ::Release.find_or_initialize_by(project_id: project.id, tag: representation.tag)
  263. next unless release.new_record?
  264. begin
  265. release.description = representation.description
  266. release.created_at = representation.created_at
  267. release.updated_at = representation.updated_at
  268. release.save!(validate: false)
  269. rescue => e
  270. error(:release, representation.url, e.message)
  271. end
  272. end
  273. url = response.rels[:next]
  274. end
  275. end
  276. def label_ids(labels)
  277. labels.map { |attrs| cached[:label_ids][attrs.fetch('name')] }.compact
  278. end
  279. def milestone_id(milestone)
  280. return unless milestone.present?
  281. project.milestones.select(:id).find_by(iid: milestone.iid)&.id
  282. end
  283. def user_id(user, fallback_id = nil)
  284. return unless user.present?
  285. return cached[:user_ids][user.id] if cached[:user_ids][user.id].present?
  286. gitlab_user_id = user_id_by_external_uid(user.id) || user_id_by_email(user.email)
  287. cached[:gitlab_user_ids][user.id] = gitlab_user_id.present?
  288. cached[:user_ids][user.id] = gitlab_user_id || fallback_id
  289. end
  290. def user_id_by_email(email)
  291. return nil unless email
  292. ::User.find_by_any_email(email)&.id
  293. end
  294. def user_id_by_external_uid(id)
  295. return nil unless id
  296. ::User.select(:id)
  297. .joins(:identities)
  298. .merge(::Identity.where(provider: :github, extern_uid: id))
  299. .first&.id
  300. end
  301. def format_description(body, author)
  302. return body if cached[:gitlab_user_ids][author.id]
  303. "*Created by: #{author.username}*\n\n#{body}"
  304. end
  305. def expire_repository_cache
  306. repository.expire_content_cache if project.repository_exists?
  307. end
  308. def keep_track_of_errors
  309. return unless errors.any?
  310. project.update_column(:import_error, {
  311. message: 'The remote data could not be fully imported.',
  312. errors: errors
  313. }.to_json)
  314. end
  315. def error(type, url, message)
  316. errors << { type: type, url: Gitlab::UrlSanitizer.sanitize(url), error: message }
  317. end
  318. end
  319. end