diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index ca47de2bebb5f03ab42a0c330699ac5c79015721..1e1614be46adee1287436a976b7371bd4160c766 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -43,9 +43,25 @@ def find_import_state(id)
 
       private
 
+      def proceed_to_next_stage(import_state_jid, next_stage, project_id)
+        project = Project.find_by_id(project_id)
+        import_settings = Gitlab::GithubImport::Settings.new(project)
+
+        load_references(project) if import_settings.user_mapping_enabled?
+
+        super
+      end
+
       def next_stage_worker(next_stage)
         STAGES.fetch(next_stage.to_sym)
       end
+
+      def load_references(project)
+        ::Import::LoadPlaceholderReferencesWorker.perform_async(
+          ::Import::SOURCE_GITHUB,
+          project.import_state.id,
+          { 'current_user_id' => project.creator_id })
+      end
     end
   end
 end
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
index 8d5a98136afe1980939a0b67bbbd77d1059d7b4c..3f59d83a6f8ca0158fff8d877d1a5f4f50fe3753 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -13,7 +13,11 @@ class FinishImportWorker # rubocop:disable Scalability/IdempotentWorker
         # project - An instance of Project.
         def import(_, project)
           @project = project
+
+          return self.class.perform_in(30.seconds, project.id) if reference_store_pending?
+
           project.after_import
+
           report_import_time
         end
 
@@ -21,6 +25,26 @@ def import(_, project)
 
         attr_reader :project
 
+        def reference_store_pending?
+          return false unless import_settings(project).user_mapping_enabled?
+
+          return false unless placeholder_reference_store.any?
+
+          ::Import::LoadPlaceholderReferencesWorker.perform_async(
+            ::Import::SOURCE_GITHUB,
+            project.import_state.id,
+            { 'current_user_id' => project.creator.id }
+          )
+
+          info(
+            project.id,
+            message: 'Delaying finalization as placeholder references are pending',
+            placeholder_store_count: placeholder_reference_store.count
+          )
+
+          true
+        end
+
         def report_import_time
           metrics.track_finished_import
 
@@ -35,6 +59,13 @@ def report_import_time
         def metrics
           @metrics ||= Gitlab::Import::Metrics.new(:github_importer, project)
         end
+
+        def placeholder_reference_store
+          @placeholder_reference_store ||= ::Import::PlaceholderReferences::Store.new(
+            import_source: ::Import::SOURCE_GITHUB,
+            import_uid: project.import_state.id
+          )
+        end
       end
     end
   end
diff --git a/lib/gitlab/github_import/contributions_mapper.rb b/lib/gitlab/github_import/contributions_mapper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0f391666edd09063b28d3e5341aba81f32b4086a
--- /dev/null
+++ b/lib/gitlab/github_import/contributions_mapper.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module GithubImport
+    class ContributionsMapper
+      def initialize(project)
+        @project = project
+      end
+
+      def user_mapper
+        ::Gitlab::Import::SourceUserMapper.new(
+          namespace: project.root_ancestor,
+          source_hostname: project.import_url,
+          import_type: ::Import::SOURCE_GITHUB
+        )
+      end
+
+      def user_mapping_enabled?
+        Gitlab::GithubImport::Settings.new(project).user_mapping_enabled?
+      end
+
+      private
+
+      attr_reader :project
+    end
+  end
+end
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
index 97a76987e70ea78233a17e72d6a427a5abde6445..0b0a1b74128577aa4b97b2ed15536ec339454048 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -5,9 +5,10 @@ module GithubImport
     module Importer
       class IssueImporter
         include Gitlab::Import::UsernameMentionRewriter
+        include Gitlab::GithubImport::PushPlaceholderReferences
 
         attr_reader :project, :issue, :client, :user_finder, :milestone_finder,
-          :issuable_finder
+          :issuable_finder, :mapper
 
         # Imports an issue if it's a regular issue and not a pull request.
         def self.import_if_issue(issue, project, client)
@@ -24,16 +25,28 @@ def initialize(issue, project, client)
           @user_finder = GithubImport::UserFinder.new(project, client)
           @milestone_finder = MilestoneFinder.new(project)
           @issuable_finder = GithubImport::IssuableFinder.new(project, issue)
+          @mapper = Gitlab::GithubImport::ContributionsMapper.new(project)
         end
 
         def execute
           new_issue = create_issue
           issuable_finder.cache_database_id(new_issue.id)
+
+          push_issue_placeholder_references(new_issue)
+
           new_issue
         end
 
         private
 
+        # Returns a Hash of { GitLabUserId => GitHubUserId }
+        # that can be used for both importing and pushing user references.
+        def issue_assignee_map
+          @map ||= issue.assignees.each_with_object({}) do |assignee, map|
+            map[user_finder.user_id_for(assignee)] = assignee[:id]
+          end
+        end
+
         # Creates a new GitLab issue for the current GitHub issue.
         def create_issue
           author_id, author_found = user_finder.author_id_for(issue)
@@ -41,9 +54,7 @@ def create_issue
           description = wrap_mentions_in_backticks(issue.description)
           description = MarkdownText.format(description, issue.author, author_found)
 
-          assignee_ids = issue.assignees.filter_map do |assignee|
-            user_finder.user_id_for(assignee)
-          end
+          assignee_ids = issue_assignee_map.keys.compact
 
           attributes = {
             iid: issue.iid,
@@ -63,6 +74,25 @@ def create_issue
 
           project.issues.create!(attributes.merge(importing: true))
         end
+
+        def push_issue_placeholder_references(new_issue)
+          return unless mapper.user_mapping_enabled?
+
+          user_mapper = mapper.user_mapper
+
+          push_with_record(new_issue, :author_id, issue.author.id, user_mapper)
+
+          new_issue.issue_assignees.each do |issue_assignee|
+            github_user_id = issue_assignee_map[issue_assignee.user_id]
+            push_with_composite_key(
+              issue_assignee,
+              :user_id,
+              { 'user_id' => issue_assignee.user_id, 'issue_id' => issue_assignee.issue_id },
+              github_user_id,
+              user_mapper
+            )
+          end
+        end
       end
     end
   end
diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb
index ce8aad344349176fc6b693013c25e08afd082275..69f1e6daccfa9444ea9ada844b77931ed08efc89 100644
--- a/lib/gitlab/github_import/importer/note_importer.rb
+++ b/lib/gitlab/github_import/importer/note_importer.rb
@@ -5,8 +5,9 @@ module GithubImport
     module Importer
       class NoteImporter
         include Gitlab::Import::UsernameMentionRewriter
+        include Gitlab::GithubImport::PushPlaceholderReferences
 
-        attr_reader :note, :project, :client, :user_finder
+        attr_reader :note, :project, :client, :user_finder, :mapper
 
         # note - An instance of `Gitlab::GithubImport::Representation::Note`.
         # project - An instance of `Project`.
@@ -16,6 +17,7 @@ def initialize(note, project, client)
           @project = project
           @client = client
           @user_finder = GithubImport::UserFinder.new(project, client)
+          @mapper = Gitlab::GithubImport::ContributionsMapper.new(project)
         end
 
         def execute
@@ -47,7 +49,9 @@ def execute
           # Note: if you're going to replace `legacy_bulk_insert` with something that trigger callback
           # to generate HTML version - you also need to regenerate it in
           # Gitlab::GithubImport::Importer::NoteAttachmentsImporter.
-          ApplicationRecord.legacy_bulk_insert(Note.table_name, [attributes]) # rubocop:disable Gitlab/BulkInsert
+          ids = ApplicationRecord.legacy_bulk_insert(Note.table_name, [attributes], return_ids: true) # rubocop:disable Gitlab/BulkInsert
+
+          push_note_refs_with_ids(ids, mapper.user_mapper) if mapper.user_mapping_enabled?
         end
 
         # Returns the ID of the issue or merge request to create the note for.
diff --git a/lib/gitlab/github_import/push_placeholder_references.rb b/lib/gitlab/github_import/push_placeholder_references.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5a7f695ef4cc55593c0104e5db0a541fd7eab476
--- /dev/null
+++ b/lib/gitlab/github_import/push_placeholder_references.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module GithubImport
+    # Contains methods to push placeholder references for user contribution mapping
+    module PushPlaceholderReferences
+      # Pushes a placeholder reference using .from_record
+      # Used when the record is available and the reference only requires a numeric key
+      def push_with_record(record, attribute, source_user_identifier, user_mapper)
+        source_user = user_mapper.find_source_user(source_user_identifier)
+
+        return if source_user.accepted_status?
+
+        ::Import::PlaceholderReferences::PushService.from_record(
+          import_source: ::Import::SOURCE_GITHUB,
+          import_uid: project.import_state.id,
+          record: record,
+          source_user: source_user,
+          user_reference_column: attribute
+        ).execute
+      end
+
+      # Pushes placeholder references for each Note record found via an id look-up using .new
+      # This is used as Note records are created using legacy_bulk_insert which
+      # can return the ids of records created, but not the records themselves
+      def push_note_refs_with_ids(ids, user_mapper)
+        ids.each do |id|
+          source_user = user_mapper.find_source_user(note[:author].id)
+
+          next if source_user.accepted_status?
+
+          ::Import::PlaceholderReferences::PushService.new(
+            import_source: ::Import::SOURCE_GITHUB,
+            import_uid: project.import_state.id,
+            source_user_id: source_user.id,
+            source_user_namespace_id: source_user.namespace_id,
+            model: Note,
+            user_reference_column: :author_id,
+            numeric_key: id).execute
+        end
+      end
+
+      # Pushes a placeholder reference using a composite key.
+      # This is used when the record requires a composite key for the reference.
+      def push_with_composite_key(record, attribute, composite_key, source_user_identifier, user_mapper)
+        source_user = user_mapper.find_source_user(source_user_identifier)
+        return if source_user.accepted_status?
+
+        ::Import::PlaceholderReferences::PushService.new(
+          import_source: ::Import::SOURCE_GITHUB,
+          import_uid: project.import_state.id,
+          source_user_id: source_user.id,
+          source_user_namespace_id: source_user.namespace_id,
+          model: record.class,
+          user_reference_column: attribute,
+          composite_key: composite_key
+        ).execute
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb
index 603851b1de392e91cc61fc8f4288340d1f9f0def..6e9749ec82ae2cfe14f0c138675c74cc434bc3c5 100644
--- a/lib/gitlab/github_import/user_finder.rb
+++ b/lib/gitlab/github_import/user_finder.rb
@@ -14,7 +14,7 @@ module GithubImport
     class UserFinder
       include Gitlab::ExclusiveLeaseHelpers
 
-      attr_reader :project, :client
+      attr_reader :project, :client, :mapper
 
       # The base cache key to use for caching user IDs for a given GitHub user ID.
       ID_CACHE_KEY = 'github-import/user-finder/user-id/%s'
@@ -41,6 +41,7 @@ class UserFinder
       def initialize(project, client)
         @project = project
         @client = client
+        @mapper = Gitlab::GithubImport::ContributionsMapper.new(project)
       end
 
       # Returns the GitLab user ID of an object's author.
@@ -62,6 +63,8 @@ def author_id_for(object, author_key: :author)
                       object ? object[:author] : nil
                     end
 
+        # TODO when improved user mapping is released we can refactor everything below to just
+        # user_id_for(user_info)
         id = user_info ? user_id_for(user_info) : GithubImport.ghost_user_id
 
         if id
@@ -80,7 +83,22 @@ def assignee_id_for(issuable)
       #
       # user - An instance of `Gitlab::GithubImport::Representation::User` or `Hash`.
       def user_id_for(user)
-        find(user[:id], user[:login]) if user.present?
+        return unless user.present?
+
+        if mapper.user_mapping_enabled?
+          source_user(user).mapped_user_id
+        else
+          find(user[:id], user[:login])
+        end
+      end
+
+      # Returns the GitLab user ID from placeholder or reassigned_to user.
+      def source_user(user)
+        mapper.user_mapper.find_or_create_source_user(
+          source_name: user[:login],
+          source_username: user[:login],
+          source_user_identifier: user[:id]
+        )
       end
 
       # Returns the GitLab ID for the given GitHub ID or username.
@@ -250,7 +268,7 @@ def email_fetched_for_project?(username)
       def fetch_email_from_github(username, etag: nil)
         log(EMAIL_API_CALL_LOGGING_MESSAGE[etag.present?], username: username)
 
-        # Only make a rate-limited API call if the ETAG is not available })
+        # Only make a rate-limited API call if the ETAG is not available )
         user = client.user(username, { headers: { 'If-None-Match' => etag }.compact })
         user[:email] || '' if user
       end
diff --git a/spec/lib/gitlab/github_import/contributions_mapper_spec.rb b/spec/lib/gitlab/github_import/contributions_mapper_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e01ab0437db3fa2bd798d7170d9e8521376be8ea
--- /dev/null
+++ b/spec/lib/gitlab/github_import/contributions_mapper_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::ContributionsMapper, :clean_gitlab_redis_shared_state, feature_category: :importers do
+  let_it_be(:project) { create(:project, :with_import_url) }
+
+  let(:mapper) { described_class.new(project) }
+
+  let(:user_mapping_enabled) { true }
+
+  before do
+    project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: user_mapping_enabled })
+  end
+
+  describe '#user_mapper' do
+    it 'creates an instance of a source user mapper' do
+      expect(mapper.user_mapper).to be_an_instance_of(::Gitlab::Import::SourceUserMapper)
+    end
+  end
+
+  describe '#user_mapping_enabled?' do
+    context 'when user mapping is enabled' do
+      it 'returns true' do
+        expect(mapper.user_mapping_enabled?).to be true
+      end
+    end
+
+    context 'when user mapping is disbled' do
+      let(:user_mapping_enabled) { false }
+
+      it 'returns false' do
+        expect(mapper.user_mapping_enabled?).to be false
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
index eb68059f35d917c3098e109b7b05565c529948b6..7fa35bf0287b7ad86f633d1158141ab2d3a02171 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -3,16 +3,18 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_shared_state, feature_category: :importers do
+  include Import::UserMappingHelper
+
   let_it_be(:work_item_type_id) { ::WorkItems::Type.default_issue_type.id }
-  let_it_be(:project) { create(:project) }
-  let_it_be(:user) { create(:user) }
-  let_it_be(:user_2) { create(:user) }
+  let_it_be(:group) { create(:group) }
+  let_it_be(:project) { create(:project, :with_import_url, group: group) }
   let_it_be(:milestone) { create(:milestone, project: project) }
 
   let(:client) { instance_double(Gitlab::GithubImport::Client) }
   let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
   let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
   let(:description) { 'This is my issue' }
+  let(:author) { Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice') }
 
   let(:issue) do
     Gitlab::GithubImport::Representation::Issue.new(
@@ -22,11 +24,11 @@
       milestone_number: 1,
       state: :opened,
       assignees: [
-        Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+        author,
         Gitlab::GithubImport::Representation::User.new(id: 5, login: 'bob')
       ],
       label_names: %w[bug],
-      author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+      author: author,
       created_at: created_at,
       updated_at: updated_at,
       pull_request: false,
@@ -34,6 +36,31 @@
     )
   end
 
+  let(:user_mapping_enabled) { true }
+  let_it_be(:source_user_alice) do
+    create(
+      :import_source_user,
+      source_user_identifier: '4',
+      source_hostname: project.import_url,
+      namespace_id: group.id
+    )
+  end
+
+  let_it_be(:source_user_bob) do
+    create(
+      :import_source_user,
+      source_user_identifier: '5',
+      source_hostname: project.import_url,
+      namespace_id: group.id
+    )
+  end
+
+  let(:importer) { described_class.new(issue, project, client) }
+
+  before do
+    project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: user_mapping_enabled })
+  end
+
   describe '.import_if_issue' do
     it 'imports an issuable if it is a regular issue' do
       expect_next_instance_of(Gitlab::GithubImport::Importer::IssueImporter, issue, project, client) do |importer|
@@ -53,29 +80,17 @@
   end
 
   describe '#execute' do
-    let(:importer) { described_class.new(issue, project, client) }
-
-    before do
-      allow_next_instance_of(Gitlab::GithubImport::MilestoneFinder) do |finder|
-        allow(finder).to receive(:id_for).with(issue).and_return(milestone.id)
-      end
-
-      allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
-        allow(finder).to receive(:author_id_for).with(issue).and_return([project.creator_id, false])
-        allow(finder).to receive(:user_id_for).and_return(nil)
-      end
-    end
-
     it 'creates the issue' do
       expect { importer.execute }.to change { Issue.count }.by(1)
+
       expect(Issue.last).to have_attributes(
         iid: 42,
         title: 'My Issue',
-        author_id: project.creator_id,
-        assignee_ids: [],
+        author_id: source_user_alice.mapped_user_id,
+        assignee_ids: contain_exactly(source_user_bob.mapped_user_id, source_user_alice.mapped_user_id),
         project_id: project.id,
         namespace_id: project.project_namespace_id,
-        description: "*Created by: alice*\n\nThis is my issue",
+        description: "This is my issue",
         milestone_id: milestone.id,
         state_id: 1,
         created_at: created_at,
@@ -93,43 +108,139 @@
       expect(database_id).to eq(Issue.last.id)
     end
 
-    context 'when author is mapped to a user' do
-      it 'sets the author ID to the mapped user and preserves the original issue description' do
-        expect_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
-          expect(finder).to receive(:author_id_for).and_return([user.id, true])
-          expect(finder).to receive(:user_id_for).and_return(nil).twice
-        end
+    it 'pushes the author and assignee references' do
+      importer.execute
+
+      created_issue = Issue.last
+      user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
+
+      expect(user_references).to match_array([
+        ['Issue', created_issue.id, 'author_id', source_user_alice.id],
+        [
+          'IssueAssignee', { 'user_id' => source_user_alice.mapped_user_id, 'issue_id' => created_issue.id },
+          'user_id', source_user_alice.id
+        ],
+        [
+          'IssueAssignee', { 'user_id' => source_user_bob.mapped_user_id, 'issue_id' => created_issue.id },
+          'user_id', source_user_bob.id
+        ]
+      ])
+    end
+
+    context 'when the description has user mentions' do
+      let(:description) { 'You can ask @knejad by emailing xyz@gitlab.com' }
 
+      it 'adds backticks to the username' do
         importer.execute
 
-        expect(Issue.last).to have_attributes(
-          description: "This is my issue",
-          author_id: user.id
-        )
+        expect(Issue.last.description).to eq("You can ask `@knejad` by emailing xyz@gitlab.com")
       end
     end
+  end
 
-    context 'when assigness are mapped to users' do
-      it 'sets the assignee IDs to the mapped users' do
-        expect_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
-          expect(finder).to receive(:author_id_for).and_return([user.id, true])
-          expect(finder).to receive(:user_id_for).and_return(user.id)
-          expect(finder).to receive(:user_id_for).and_return(user_2.id)
+  context 'when user_mapping is not enabled' do
+    let(:user_mapping_enabled) { false }
+
+    describe '.import_if_issue' do
+      it 'imports an issuable if it is a regular issue' do
+        expect_next_instance_of(Gitlab::GithubImport::Importer::IssueImporter, issue, project, client) do |importer|
+          expect(importer).to receive(:execute)
         end
 
-        importer.execute
+        described_class.import_if_issue(issue, project, client)
+      end
 
-        expect(Issue.last.assignee_ids).to match_array([user.id, user_2.id])
+      it 'does not import the issuable if it is a pull request' do
+        expect(issue).to receive(:pull_request?).and_return(true)
+
+        expect(described_class).not_to receive(:new)
+
+        described_class.import_if_issue(issue, project, client)
       end
     end
 
-    context 'when the description has user mentions' do
-      let(:description) { 'You can ask @knejad by emailing xyz@gitlab.com' }
+    describe '#execute' do
+      let_it_be(:user) { create(:user) }
+      let_it_be(:user_2) { create(:user) }
+      let(:importer) { described_class.new(issue, project, client) }
 
-      it 'adds backticks to the username' do
+      before do
+        allow_next_instance_of(Gitlab::GithubImport::MilestoneFinder) do |finder|
+          allow(finder).to receive(:id_for).with(issue).and_return(milestone.id)
+        end
+
+        allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+          allow(finder).to receive(:author_id_for).with(issue).and_return([project.creator_id, false])
+          allow(finder).to receive(:user_id_for).and_return(nil)
+        end
+      end
+
+      it 'creates the issue' do
+        expect { importer.execute }.to change { Issue.count }.by(1)
+        expect(Issue.last).to have_attributes(
+          iid: 42,
+          title: 'My Issue',
+          author_id: project.creator_id,
+          assignee_ids: [],
+          project_id: project.id,
+          namespace_id: project.project_namespace_id,
+          description: "*Created by: alice*\n\nThis is my issue",
+          milestone_id: milestone.id,
+          state_id: 1,
+          created_at: created_at,
+          updated_at: updated_at,
+          work_item_type_id: work_item_type_id,
+          imported_from: 'github'
+        )
+      end
+
+      it 'caches the created issue ID' do
         importer.execute
 
-        expect(Issue.last.description).to eq("*Created by: alice*\n\nYou can ask `@knejad` by emailing xyz@gitlab.com")
+        database_id = Gitlab::GithubImport::IssuableFinder.new(project, issue).database_id
+
+        expect(database_id).to eq(Issue.last.id)
+      end
+
+      context 'when author is mapped to a user' do
+        it 'sets the author ID to the mapped user and preserves the original issue description' do
+          expect_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+            expect(finder).to receive(:author_id_for).and_return([user.id, true])
+            expect(finder).to receive(:user_id_for).and_return(nil).twice
+          end
+
+          importer.execute
+
+          expect(Issue.last).to have_attributes(
+            description: "This is my issue",
+            author_id: user.id
+          )
+        end
+      end
+
+      context 'when assigness are mapped to users' do
+        it 'sets the assignee IDs to the mapped users' do
+          expect_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder|
+            expect(finder).to receive(:author_id_for).and_return([user.id, true])
+            expect(finder).to receive(:user_id_for).and_return(user.id)
+            expect(finder).to receive(:user_id_for).and_return(user_2.id)
+          end
+
+          importer.execute
+
+          expect(Issue.last.assignee_ids).to match_array([user.id, user_2.id])
+        end
+      end
+
+      context 'when the description has user mentions' do
+        let(:description) { 'You can ask @knejad by emailing xyz@gitlab.com' }
+
+        it 'adds backticks to the username' do
+          importer.execute
+
+          expect(Issue.last.description)
+            .to eq("*Created by: alice*\n\nYou can ask `@knejad` by emailing xyz@gitlab.com")
+        end
       end
     end
   end
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
index 893ed47584327ac12fccc9180bcf0d820bef3a5e..1a3d222a168220d21f237e509c1c666238cca4e6 100644
--- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -3,20 +3,31 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::GithubImport::Importer::NoteImporter, feature_category: :importers do
+  let_it_be(:imported_from) { ::Import::HasImportSource::IMPORT_SOURCES[:github] }
+  let_it_be(:project) { create(:project, :with_import_url) }
+  let_it_be(:user) { create(:user) }
+
+  let_it_be(:source_user) do
+    create(:import_source_user,
+      placeholder_user_id: user.id,
+      namespace_id: project.root_ancestor.id,
+      source_user_identifier: '4',
+      source_hostname: project.import_url
+    )
+  end
+
   let(:client) { double(:client) }
-  let(:project) { create(:project) }
-  let(:user) { create(:user) }
   let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
   let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
   let(:note_body) { 'This is my note' }
-  let_it_be(:imported_from) { ::Import::HasImportSource::IMPORT_SOURCES[:github] }
+  let(:import_state) { create(:import_state, :started, project: project) }
 
   let(:github_note) do
     Gitlab::GithubImport::Representation::Note.new(
       note_id: 100,
       noteable_id: 1,
       noteable_type: 'Issue',
-      author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice'),
+      author: Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice', email: 'alice@alice.com'),
       note: note_body,
       created_at: created_at,
       updated_at: updated_at
@@ -24,6 +35,11 @@
   end
 
   let(:importer) { described_class.new(github_note, project, client) }
+  let(:user_mapping_enabled) { true }
+
+  before do
+    project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: user_mapping_enabled })
+  end
 
   describe '#execute' do
     context 'when the noteable exists' do
@@ -35,12 +51,9 @@
           .and_return(issue_row.id)
       end
 
-      context 'when the author could be found' do
-        it 'imports the note with the found author as the note author' do
-          expect(importer.user_finder)
-            .to receive(:author_id_for)
-            .with(github_note)
-            .and_return([user.id, true])
+      context 'when user_mapping_enabled is true' do
+        it 'maps the correct user and pushes a reference' do
+          expect(importer.user_finder).to receive(:author_id_for).with(github_note).and_call_original
 
           expect(ApplicationRecord)
             .to receive(:legacy_bulk_insert)
@@ -52,7 +65,7 @@
                   noteable_id: issue_row.id,
                   project_id: project.id,
                   namespace_id: project.project_namespace_id,
-                  author_id: user.id,
+                  author_id: source_user.mapped_user_id,
                   note: 'This is my note',
                   discussion_id: match(/\A[0-9a-f]{40}\z/),
                   system: false,
@@ -60,44 +73,106 @@
                   updated_at: updated_at,
                   imported_from: imported_from
                 }
-              ]
+              ],
+              { return_ids: true }
             )
             .and_call_original
 
+          expect_next_instance_of(::Import::PlaceholderReferences::PushService,
+            import_source: ::Import::SOURCE_GITHUB,
+            import_uid: project.import_state.id,
+            source_user_id: source_user.id,
+            source_user_namespace_id: project.root_ancestor.id,
+            model: Note,
+            user_reference_column: :author_id,
+            numeric_key: an_instance_of(Integer)) do |push_service|
+              expect(push_service).to receive(:execute).and_call_original
+            end
+
           importer.execute
         end
       end
 
-      context 'when the note author could not be found' do
-        it 'imports the note with the project creator as the note author' do
-          expect(importer.user_finder)
-            .to receive(:author_id_for)
-            .with(github_note)
-            .and_return([project.creator_id, false])
+      context 'when user_mapping_enabled is false' do
+        let(:user_mapping_enabled) { false }
 
-          expect(ApplicationRecord)
-            .to receive(:legacy_bulk_insert)
-            .with(
-              Note.table_name,
-              [
+        before do
+          allow(importer.user_finder)
+            .to receive(:email_for_github_username)
+            .and_return('alice@alice.com')
+        end
+
+        context 'when the author could be found' do
+          it 'imports the note with the found author as the note author and does not push a placeholder reference' do
+            expect(importer.user_finder)
+              .to receive(:author_id_for)
+              .with(github_note)
+              .and_return([user.id, true])
+
+            expect(ApplicationRecord)
+              .to receive(:legacy_bulk_insert)
+              .with(
+                Note.table_name,
+                [
+                  {
+                    noteable_type: 'Issue',
+                    noteable_id: issue_row.id,
+                    project_id: project.id,
+                    namespace_id: project.project_namespace_id,
+                    author_id: user.id,
+                    note: 'This is my note',
+                    discussion_id: match(/\A[0-9a-f]{40}\z/),
+                    system: false,
+                    created_at: created_at,
+                    updated_at: updated_at,
+                    imported_from: imported_from
+                  }
+                ],
+                { return_ids: true }
+              )
+              .and_call_original
+
+            expect(::Import::PlaceholderReferences::PushService)
+              .not_to receive(:new)
+
+            importer.execute
+          end
+        end
+
+        context 'when the note author could not be found' do
+          it 'imports the note with the project creator as the note author' do
+            expect(importer.user_finder)
+              .to receive(:author_id_for)
+              .with(github_note)
+              .and_return([project.creator_id, false])
+
+            expect(ApplicationRecord)
+              .to receive(:legacy_bulk_insert)
+              .with(
+                Note.table_name,
+                [
+                  {
+                    noteable_type: 'Issue',
+                    noteable_id: issue_row.id,
+                    project_id: project.id,
+                    namespace_id: project.project_namespace_id,
+                    author_id: project.creator_id,
+                    note: "*Created by: alice*\n\nThis is my note",
+                    discussion_id: match(/\A[0-9a-f]{40}\z/),
+                    system: false,
+                    created_at: created_at,
+                    updated_at: updated_at,
+                    imported_from: imported_from
+                  }
+                ],
                 {
-                  noteable_type: 'Issue',
-                  noteable_id: issue_row.id,
-                  project_id: project.id,
-                  namespace_id: project.project_namespace_id,
-                  author_id: project.creator_id,
-                  note: "*Created by: alice*\n\nThis is my note",
-                  discussion_id: match(/\A[0-9a-f]{40}\z/),
-                  system: false,
-                  created_at: created_at,
-                  updated_at: updated_at,
-                  imported_from: imported_from
+                  return_ids: true
                 }
-              ]
-            )
-            .and_call_original
+              )
+              .and_call_original
 
-          importer.execute
+            importer.execute
+          end
         end
       end
 
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb
index b1f84213ad994c4a6e433da239ffc36ddaffb92d..b231d8eb6b5f9113ee730ce4043c661a40a8211f 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests/review_request_importer_spec.rb
@@ -2,10 +2,10 @@
 
 require 'spec_helper'
 
-RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImporter, :clean_gitlab_redis_shared_state do
+RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImporter, :clean_gitlab_redis_shared_state, feature_category: :importers do
   subject(:importer) { described_class.new(review_request, project, client) }
 
-  let(:project) { instance_double('Project') }
+  let_it_be(:project) { create(:project) }
   let(:client) { instance_double('Gitlab::GithubImport::Client') }
   let(:merge_request) { create(:merge_request) }
   let(:reviewer) { create(:user, username: 'alice') }
diff --git a/spec/lib/gitlab/github_import/push_placeholder_references_spec.rb b/spec/lib/gitlab/github_import/push_placeholder_references_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..43e64ee52c35ca51bded24fb0ccf727a45f913ef
--- /dev/null
+++ b/spec/lib/gitlab/github_import/push_placeholder_references_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::GithubImport::PushPlaceholderReferences, feature_category: :importers do
+  let(:client) { instance_double(Gitlab::GithubImport::Client) }
+  let_it_be(:source_id) { 'source_identifier' }
+  let_it_be(:project) { create(:project, :with_import_url) }
+  let_it_be(:author) { create(:user) }
+  let_it_be(:import_source_user) do
+    create(
+      :import_source_user,
+      source_user_identifier: source_id,
+      source_hostname: project.import_url,
+      namespace_id: project.root_ancestor.id,
+      placeholder_user_id: author.id
+    )
+  end
+
+  let(:github_note) do
+    Gitlab::GithubImport::Representation::Note.new(
+      note_id: 100,
+      noteable_id: 1,
+      noteable_type: 'Issue',
+      author: Gitlab::GithubImport::Representation::User.new(id: source_id, login: 'alice'),
+      note: 'This is my note',
+      created_at: Time.current,
+      updated_at: Time.current
+    )
+  end
+
+  let(:gitlab_note) { create(:note) }
+  let(:user_mapper) { Gitlab::GithubImport::ContributionsMapper.new(project).user_mapper }
+
+  let(:importer) { Gitlab::GithubImport::Importer::NoteImporter.new(github_note, project, client) }
+
+  before do
+    project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: true })
+  end
+
+  describe '#push_with_record' do
+    before do
+      allow(::Import::PlaceholderReferences::PushService)
+        .to receive_message_chain(:from_record, :execute)
+    end
+
+    it 'pushes the reference using .from_record' do
+      importer.push_with_record(gitlab_note, :author_id, source_id, user_mapper)
+
+      expect(::Import::PlaceholderReferences::PushService)
+        .to have_received(:from_record)
+        .with(
+          import_source: ::Import::SOURCE_GITHUB,
+          import_uid: project.import_state.id,
+          record: gitlab_note,
+          source_user: import_source_user,
+          user_reference_column: :author_id)
+    end
+  end
+
+  describe '#push_note_refs_with_ids' do
+    before do
+      allow(::Import::PlaceholderReferences::PushService)
+        .to receive_message_chain(:new, :execute)
+    end
+
+    it 'pushes the reference using .new' do
+      importer.push_note_refs_with_ids([gitlab_note.id], user_mapper)
+
+      expect(::Import::PlaceholderReferences::PushService)
+        .to have_received(:new)
+        .with(
+          import_source: ::Import::SOURCE_GITHUB,
+          import_uid: project.import_state.id,
+          source_user_id: import_source_user.id,
+          source_user_namespace_id: import_source_user.namespace_id,
+          model: gitlab_note.class,
+          user_reference_column: :author_id,
+          numeric_key: gitlab_note.id
+        )
+    end
+  end
+
+  describe '#push_with_composite_key' do
+    let(:composite_key) { { "user_id" => "user_id", "merge_request_id" => "merge_request_id" } }
+
+    before do
+      allow(::Import::PlaceholderReferences::PushService)
+        .to receive_message_chain(:new, :execute)
+    end
+
+    it 'pushes the reference with composite key' do
+      importer.push_with_composite_key(gitlab_note, :user_id, composite_key, source_id, user_mapper)
+
+      expect(::Import::PlaceholderReferences::PushService)
+        .to have_received(:new)
+        .with(
+          import_source: ::Import::SOURCE_GITHUB,
+          import_uid: project.import_state.id,
+          source_user_id: import_source_user.id,
+          source_user_namespace_id: import_source_user.namespace_id,
+          model: gitlab_note.class,
+          user_reference_column: :user_id,
+          composite_key: composite_key
+        )
+    end
+  end
+end
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
index 1cd1e76c5c2743c96f9fa47a99453d541805db36..63d1ac771b907be93d2160659e266af3ee480192 100644
--- a/spec/lib/gitlab/github_import/user_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -3,16 +3,22 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_shared_state, feature_category: :importers do
-  let(:project) do
+  let_it_be(:project) do
     create(
       :project,
       import_type: 'github',
-      import_url: 'https://api.github.com/user/repo'
+      import_url: 'https://github.com/user/repo.git'
     )
   end
 
-  let(:client) { double(:client) }
+  let(:client) { instance_double(Gitlab::GithubImport::Client) }
   let(:finder) { described_class.new(project, client) }
+  let(:settings) { Gitlab::GithubImport::Settings.new }
+  let(:user_mapping_enabled) { true }
+
+  before do
+    project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: user_mapping_enabled })
+  end
 
   describe '#author_id_for' do
     context 'with default author_key' do
@@ -100,15 +106,42 @@
   end
 
   describe '#user_id_for' do
-    it 'returns the user ID for the given user' do
-      user = { id: 4, login: 'kittens' }
+    context 'when user mapping is disabled' do
+      let(:user_mapping_enabled) { false }
 
-      expect(finder).to receive(:find).with(user[:id], user[:login]).and_return(42)
-      expect(finder.user_id_for(user)).to eq(42)
+      it 'returns the user ID for the given user' do
+        user = { id: 4, login: 'kittens' }
+
+        expect(finder).to receive(:find).with(user[:id], user[:login]).and_return(42)
+        expect(finder.user_id_for(user)).to eq(42)
+      end
     end
 
-    it 'does not fail with empty input' do
-      expect(finder.user_id_for(nil)).to eq(nil)
+    context 'when user mapping is enabled' do
+      let!(:source_user) do
+        create(:import_source_user,
+          namespace_id: project.root_ancestor.id,
+          source_user_identifier: '7',
+          source_hostname: 'https://github.com'
+        )
+      end
+
+      it 'returns the mapped_user_id of source user with matching user identifier' do
+        user = { id: 7, login: 'anything' }
+
+        expect(finder.user_id_for(user)).to eq(source_user.mapped_user_id)
+      end
+
+      it 'creates a new source user when user identifier does not match' do
+        user = { id: 6, login: 'anything' }
+
+        expect { finder.user_id_for(user) }.to change { Import::SourceUser.count }.by(1)
+        expect(finder.user_id_for(user)).not_to eq(source_user.mapped_user_id)
+      end
+
+      it 'does not fail with empty input' do
+        expect(finder.user_id_for(nil)).to eq(nil)
+      end
     end
   end
 
@@ -535,6 +568,10 @@
   describe '#id_for_github_id' do
     let(:id) { 4 }
 
+    before do
+      allow(project).to receive(:github_enterprise_import?).and_return(false)
+    end
+
     it 'queries and caches the user ID for a given GitHub ID' do
       expect(finder).to receive(:query_id_for_github_id)
         .with(id)
@@ -560,12 +597,8 @@
     end
 
     context 'when importing from github enterprise' do
-      let(:project) do
-        create(
-          :project,
-          import_type: 'github',
-          import_url: 'https://othergithub.net/user/repo'
-        )
+      before do
+        allow(project).to receive(:github_enterprise_import?).and_return(true)
       end
 
       it 'does not look up the user by external id' do
@@ -583,6 +616,10 @@
   describe '#id_for_github_email' do
     let(:email) { 'kittens@example.com' }
 
+    before do
+      allow(project).to receive(:github_enterprise_import?).and_return(true)
+    end
+
     it 'queries and caches the user ID for a given Email address' do
       expect(finder).to receive(:query_id_for_github_email)
         .with(email)
diff --git a/spec/support/helpers/import/user_mapping_helper.rb b/spec/support/helpers/import/user_mapping_helper.rb
index e9b3ec1ea4cceaef35fa7384013e5696ed6ccc33..49a80d39580c7d3d0fc80b3ddff2eb1591a7672e 100644
--- a/spec/support/helpers/import/user_mapping_helper.rb
+++ b/spec/support/helpers/import/user_mapping_helper.rb
@@ -23,8 +23,9 @@ def placeholder_user_references(import_type, import_uid, limit = 100)
 
       user_references.map do |item|
         item = Import::SourceUserPlaceholderReference.from_serialized(item)
+        key = item.numeric_key || item.composite_key
 
-        [item.model, item.numeric_key, item.user_reference_column, item.source_user_id]
+        [item.model, key, item.user_reference_column, item.source_user_id]
       end
     end
   end
diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
index dcf016c550b68b75e68c7cf751a9126debf54276..9e4b55bca7afe03b18156a44d550e85e59503a17 100644
--- a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
@@ -8,4 +8,39 @@
   it 'has a Sidekiq retry of 6' do
     expect(described_class.sidekiq_options['retry']).to eq(6)
   end
+
+  context 'when there are no remaining jobs' do
+    let_it_be(:project) { create(:project, :import_user_mapping_enabled, import_status: :started, import_jid: '123') }
+
+    subject(:worker) { described_class.new }
+
+    before do
+      allow(worker)
+        .to receive(:wait_for_jobs)
+        .with({ '123' => 2 })
+        .and_return({})
+    end
+
+    it 'enqueues LoadPlaceholderReferencesWorker to save placeholder references' do
+      expect(::Import::LoadPlaceholderReferencesWorker).to receive(:perform_async).with(
+        ::Import::SOURCE_GITHUB,
+        project.import_state.id,
+        { 'current_user_id' => project.creator_id }
+      )
+
+      worker.perform(project.id, { '123' => 2 }, 'finish')
+    end
+
+    context 'when user contribution mapping is disabled' do
+      before do
+        allow(Gitlab::GithubImport::Settings).to receive_message_chain(:new, :user_mapping_enabled?).and_return(false)
+      end
+
+      it 'does not enqueue LoadPlaceholderReferencesWorker' do
+        expect(::Import::LoadPlaceholderReferencesWorker).not_to receive(:perform_async)
+
+        worker.perform(project.id, { '123' => 2 }, 'finish')
+      end
+    end
+  end
 end
diff --git a/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
index ba8fcdb240602ff277511e06a0ab6718910c50ca..dea2d61e481bb64f4bf22fffdbf79c5b278dc023 100644
--- a/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
@@ -3,13 +3,18 @@
 require 'spec_helper'
 
 RSpec.describe Gitlab::GithubImport::Stage::FinishImportWorker, feature_category: :importers do
-  let_it_be(:project) { create(:project) }
+  let_it_be_with_reload(:project) { create(:project) }
+  let_it_be(:import_state) { create(:import_state, :started, project: project) }
 
   subject(:worker) { described_class.new }
 
   it_behaves_like Gitlab::GithubImport::StageMethods
 
   describe '#perform' do
+    before do
+      project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: true })
+    end
+
     it 'marks the import as finished and reports import statistics' do
       expect(project).to receive(:after_import)
       expect_next_instance_of(Gitlab::Import::Metrics) do |instance|
@@ -34,5 +39,43 @@
 
       worker.import(double(:client), project)
     end
+
+    context 'when the reference store is empty' do
+      it 'checks the reference store and does not push placeholder references' do
+        allow(described_class).to receive(:perform_in)
+        allow(worker).to receive_message_chain(:placeholder_reference_store, :any?).and_return(false)
+
+        expect(Import::LoadPlaceholderReferencesWorker).not_to receive(:perform_async)
+
+        worker.import(double(:client), project)
+
+        expect(described_class).not_to have_received(:perform_in)
+      end
+    end
+
+    context 'when the reference store is not empty' do
+      it 'checks the reference store, queues LoadPlaceholderReferencesWorker, and requeues itself' do
+        allow(described_class).to receive(:perform_in)
+        allow(worker).to receive_message_chain(:placeholder_reference_store, :any?).and_return(true)
+        allow(worker).to receive_message_chain(:placeholder_reference_store, :count).and_return(1)
+
+        expect(Import::LoadPlaceholderReferencesWorker).to receive(:perform_async)
+
+        expect(Gitlab::GithubImport::Logger)
+          .to receive(:info)
+          .with(
+            {
+              message: 'Delaying finalization as placeholder references are pending',
+              import_stage: 'Gitlab::GithubImport::Stage::FinishImportWorker',
+              placeholder_store_count: 1,
+              project_id: project.id
+            }
+          )
+
+        worker.import(double(:client), project)
+
+        expect(described_class).to have_received(:perform_in).with(30.seconds, project.id)
+      end
+    end
   end
 end