diff --git a/CHANGELOG b/CHANGELOG
index 247eb1e364328955a86ef975842e804daed5b011..dd48bab978c8c27b5391d498b36e1294f0123b48 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -47,6 +47,7 @@ v 7.14.1
   - Only include base URL in OmniAuth full_host parameter (Stan Hu)
   - Fix Error 500 in API when accessing a group that has an avatar (Stan Hu)
   - Ability to enable SSL verification for Webhooks
+  - Add FogBugz project import (Jared Szechy)
 
 v 7.14.0
   - Fix bug where non-project members of the target project could set labels on new merge requests.
diff --git a/Gemfile b/Gemfile
index cca8fc38e5791f891b12dc1f9173b2ed0ff4f063..d609ff6610f1ceaa127851ea24e2781ae3e00843 100644
--- a/Gemfile
+++ b/Gemfile
@@ -157,6 +157,9 @@ gem "slack-notifier", "~> 1.0.0"
 # Asana integration
 gem 'asana', '~> 0.0.6'
 
+# FogBugz integration
+gem 'ruby-fogbugz'
+
 # d3
 gem 'd3_rails', '~> 3.5.5'
 
@@ -259,6 +262,7 @@ group :test do
   gem 'email_spec', '~> 1.6.0'
   gem 'webmock', '~> 1.21.0'
   gem 'test_after_commit'
+  gem 'sham_rack'
 end
 
 group :production do
diff --git a/Gemfile.lock b/Gemfile.lock
index 2df318f33825ca2229809b5d63cf92452d8090f1..dce7e4964a60074f9ff93922f7992fc220e86d08 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -575,6 +575,8 @@ GEM
       powerpack (~> 0.0.6)
       rainbow (>= 1.99.1, < 3.0)
       ruby-progressbar (~> 1.4)
+    ruby-fogbugz (0.1.1)
+      crack
     ruby-progressbar (1.7.1)
     ruby-saml (1.0.0)
       nokogiri (>= 1.5.10)
@@ -609,6 +611,8 @@ GEM
       thor (~> 0.14)
     settingslogic (2.0.9)
     sexp_processor (4.4.5)
+    sham_rack (1.3.6)
+      rack
     shoulda-matchers (2.8.0)
       activesupport (>= 3.0.0)
     sidekiq (3.3.0)
@@ -845,12 +849,14 @@ DEPENDENCIES
   rqrcode-rails3
   rspec-rails (~> 3.3.0)
   rubocop (= 0.28.0)
+  ruby-fogbugz
   sanitize (~> 2.0)
   sass-rails (~> 4.0.5)
   sdoc
   seed-fu
   select2-rails (~> 3.5.9)
   settingslogic
+  sham_rack
   shoulda-matchers (~> 2.8.0)
   sidekiq (~> 3.3)
   sidetiq (= 0.6.3)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index cb1cf13d34dfd398a9bf859be637fd8beac76864..4c112534ae65658c442380ec8ed91aff14d2fc56 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,4 +1,5 @@
 require 'gon'
+require 'fogbugz'
 
 class ApplicationController < ActionController::Base
   include Gitlab::CurrentSettings
@@ -20,7 +21,7 @@ class ApplicationController < ActionController::Base
   protect_from_forgery with: :exception
 
   helper_method :abilities, :can?, :current_application_settings
-  helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :git_import_enabled?
+  helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
 
   rescue_from Encoding::CompatibilityError do |exception|
     log_exception(exception)
@@ -337,6 +338,10 @@ class ApplicationController < ActionController::Base
     current_application_settings.import_sources.include?('google_code')
   end
 
+  def fogbugz_import_enabled?
+    current_application_settings.import_sources.include?('fogbugz')
+  end
+
   def git_import_enabled?
     current_application_settings.import_sources.include?('git')
   end
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bda534fb4de1902174b7afda272e814894c4565b
--- /dev/null
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -0,0 +1,106 @@
+class Import::FogbugzController < Import::BaseController
+  before_action :verify_fogbugz_import_enabled
+  before_action :user_map, only: [:new_user_map, :create_user_map]
+
+  # Doesn't work yet due to bug in ruby-fogbugz, see below
+  rescue_from Fogbugz::AuthenticationException, with: :fogbugz_unauthorized
+
+  def new
+
+  end
+
+  def callback
+    begin
+      res = Gitlab::FogbugzImport::Client.new(import_params.symbolize_keys)
+    rescue
+      # Needed until https://github.com/firmafon/ruby-fogbugz/pull/9 is merged
+      return redirect_to :back, alert: 'Could not authenticate with FogBugz, check your URL, email, and password'
+    end
+    session[:fogbugz_token] = res.get_token
+    session[:fogbugz_uri] = params[:uri]
+
+    redirect_to new_user_map_import_fogbugz_path
+  end
+
+  def new_user_map
+
+  end
+
+  def create_user_map
+    user_map = params[:users]
+
+    unless user_map.is_a?(Hash) && user_map.all? { |k, v| !v[:name].blank? }
+      flash.now[:alert] = 'All users must have a name.'
+
+      render 'new_user_map' and return
+    end
+
+    session[:fogbugz_user_map] = user_map
+
+    flash[:notice] = 'The user map has been saved. Continue by selecting the projects you want to import.'
+
+    redirect_to status_import_fogbugz_path
+  end
+
+  def status
+    unless client.valid?
+      return redirect_to new_import_fogbugz_path
+    end
+
+    @repos = client.repos
+
+    @already_added_projects = current_user.created_projects.where(import_type: 'fogbugz')
+    already_added_projects_names = @already_added_projects.pluck(:import_source)
+
+    @repos.reject! { |repo| already_added_projects_names.include? repo.name }
+  end
+
+  def jobs
+    jobs = current_user.created_projects.where(import_type: 'fogbugz').to_json(only: [:id, :import_status])
+    render json: jobs
+  end
+
+  def create
+    @repo_id = params[:repo_id]
+    repo = client.repo(@repo_id)
+    fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] }
+    @target_namespace = current_user.namespace
+    @project_name = repo.name
+
+    namespace = @target_namespace
+
+    umap = session[:fogbugz_user_map] || client.user_map
+
+    @project = Gitlab::FogbugzImport::ProjectCreator.new(repo, fb_session, namespace, current_user, umap).execute
+  end
+
+  private
+
+  def client
+    @client ||= Gitlab::FogbugzImport::Client.new(token: session[:fogbugz_token], uri: session[:fogbugz_uri])
+  end
+
+  def user_map
+    @user_map ||= begin
+      user_map = client.user_map
+
+      stored_user_map = session[:fogbugz_user_map]
+      user_map.update(stored_user_map) if stored_user_map
+
+      user_map
+    end
+  end
+
+  def fogbugz_unauthorized(exception)
+    flash[:alert] = exception.message
+    redirect_to new_import_fogbugz_path
+  end
+
+  def import_params
+    params.permit(:uri, :email, :password)
+  end
+
+  def verify_fogbugz_import_enabled
+    not_found! unless fogbugz_import_enabled?
+  end
+end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8f27e35d723c68e86dbc7220f79bde829214da8f..c8841178e933079568b33e330775dce0ce21fd27 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -83,7 +83,7 @@ class ApplicationSetting < ActiveRecord::Base
       default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
       default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
       restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
-      import_sources: ['github','bitbucket','gitlab','gitorious','google_code','git']
+      import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
     )
   end
 
diff --git a/app/models/project.rb b/app/models/project.rb
index 8e33a4b2f0f9781d93d46681977599a598fc0eab..c2d7fa5fcb8c0aef17e8822248ec3b4b601c5721 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -43,6 +43,8 @@ class Project < ActiveRecord::Base
   extend Gitlab::ConfigHelper
   extend Enumerize
 
+  UNKNOWN_IMPORT_URL = 'http://unknown.git'
+
   default_value_for :archived, false
   default_value_for :visibility_level, gitlab_config_features.visibility_level
   default_value_for :issues_enabled, gitlab_config_features.issues
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..99f22293d0dd0e182f4b936e4e03a371e0c507de
--- /dev/null
+++ b/app/services/projects/download_service.rb
@@ -0,0 +1,43 @@
+module Projects
+  class DownloadService < BaseService
+
+    WHITELIST = [
+      /^[^.]+\.fogbugz.com$/
+    ]
+
+    def initialize(project, url)
+      @project, @url = project, url
+    end
+
+    def execute
+      return nil unless valid_url?(@url)
+
+      uploader = FileUploader.new(@project)
+      uploader.download!(@url)
+      uploader.store!
+
+      filename = uploader.image? ? uploader.file.basename : uploader.file.filename
+
+      {
+        'alt'       => filename,
+        'url'       => uploader.secure_url,
+        'is_image'  => uploader.image?
+      }
+    end
+
+    private
+
+    def valid_url?(url)
+      url && http?(url) && valid_domain?(url)
+    end
+
+    def http?(url)
+      url =~ /\A#{URI::regexp(['http', 'https'])}\z/
+    end
+
+    def valid_domain?(url)
+      host = URI.parse(url).host
+      WHITELIST.any? { |entry| entry === host }
+    end
+  end
+end
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..e1bb88ca4ed4bb6f80ce233b1bf8a0d19c85dcfb
--- /dev/null
+++ b/app/views/import/fogbugz/new.html.haml
@@ -0,0 +1,25 @@
+- page_title "FogBugz Import"
+%h3.page-title
+  %i.fa.fa-bug
+  Import projects from FogBugz
+%hr
+
+= form_tag callback_import_fogbugz_path, class: 'form-horizontal' do
+  %p
+    To get started you enter your FogBugz URL and login information below.
+    In the next steps, you'll be able to map users and select the projects
+    you want to import.
+  .form-group
+    = label_tag :uri, 'FogBugz URL', class: 'control-label'
+    .col-sm-4
+      = text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control'
+  .form-group
+    = label_tag :email, 'FogBugz Email', class: 'control-label'
+    .col-sm-4
+      = text_field_tag :email, nil, class: 'form-control'
+  .form-group
+    = label_tag :password, 'FogBugz Password', class: 'control-label'
+    .col-sm-4
+      = password_field_tag :password, nil, class: 'form-control'
+  .form-actions
+    = submit_tag 'Continue to the next step', class: 'btn btn-create'
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..25cebfb36650b49137f5ce6818d882691afe72ba
--- /dev/null
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -0,0 +1,49 @@
+- page_title 'User map', 'FogBugz import'
+%h3.page-title
+  %i.fa.fa-bug
+  Import projects from FogBugz
+%hr
+
+= form_tag create_user_map_import_fogbugz_path, class: 'form-horizontal' do
+  %p
+    Customize how FogBugz email addresses and usernames are imported into GitLab.
+    In the next step, you'll be able to select the projects you want to import.
+  %p
+    The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames wil be imported into GitLab. You can change this by populating the table below.
+  %ul
+    %li
+      %strong Default: Map a FogBugz account ID to a full name
+      %p
+        An empty GitLab User field will add the FogBugz user's full name
+        (e.g. "By John Smith") in the description of all issues and comments.
+        It will also associate and/or assign these issues and comments with
+        the project creator.
+    %li
+      %strong Map a FogBugz account ID to a GitLab user
+      %p
+        Selecting a GitLab user will add a link to the GitLab user in the descriptions
+        of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also 
+        associate and/or assign these issues and comments with the selected user.
+
+  %table.table
+    %thead
+      %tr
+        %th ID
+        %th Name
+        %th Email
+        %th GitLab User
+    %tbody
+      - @user_map.each do |id, user|
+        %tr
+          %td= id
+          %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
+          %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
+          %td
+            = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control',
+              scope: :all, email_user: true, selected: user[:gitlab_user])
+
+  .form-actions
+    = submit_tag 'Continue to the next step', class: 'btn btn-create'
+
+:coffeescript
+  new UsersSelect()
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f179ece402d1f81c88308e4a4e63b0ed7154d7e5
--- /dev/null
+++ b/app/views/import/fogbugz/status.html.haml
@@ -0,0 +1,51 @@
+- page_title "FogBugz import"
+%h3.page-title
+  %i.fa.fa-bug
+  Import projects from FogBugz
+
+- if @repos.any?
+  %p.light
+    Select projects you want to import.
+  %p.light
+    Optionally, you can
+    = link_to 'customize', new_user_map_import_fogbugz_path
+    how FogBugz email addresses and usernames are imported into GitLab.
+  %hr
+  %p
+  = button_tag 'Import all projects', class: 'btn btn-success js-import-all'
+
+%table.table.import-jobs
+  %thead
+    %tr
+      %th From FogBugz
+      %th To GitLab
+      %th Status
+  %tbody
+    - @already_added_projects.each do |project|
+      %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+        %td
+          = project.import_source
+        %td
+          %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+        %td.job-status
+          - if project.import_status == 'finished'
+            %span
+              %i.fa.fa-check
+              done
+          - elsif project.import_status == 'started'
+            %i.fa.fa-spinner.fa-spin
+            started
+          - else
+            = project.human_import_status_name
+
+    - @repos.each do |repo|
+      %tr{id: "repo_#{repo.id}"}
+        %td
+          = repo.name
+        %td.import-target
+          = "#{current_user.username}/#{repo.name}"
+        %td.import-actions.job-status
+          = button_tag "Import", class: "btn js-add-to-import"
+
+:coffeescript
+  new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}")
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 636218368cc603229f3a658cb07ca9d527061b14..bccea21e7a82794f69880f112c44e4991de11031 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -72,6 +72,11 @@
                   %i.fa.fa-google
                   Google Code
 
+              - if fogbugz_import_enabled?
+                = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+                  %i.fa.fa-bug
+                  Fogbugz
+
               - if git_import_enabled?
                 = link_to "#", class: 'btn js-toggle-button import_git' do
                   %i.fa.fa-git
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index f2ba2e15e7b2f7e7ef4f3e028ee13d311aed8792..ea2808045eb2e93c70202b089f1d82f1423c0338 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -7,22 +7,31 @@ class RepositoryImportWorker
   def perform(project_id)
     project = Project.find(project_id)
 
-    import_result = gitlab_shell.send(:import_repository,
+    unless project.import_url == Project::UNKNOWN_IMPORT_URL
+      import_result = gitlab_shell.send(:import_repository,
                                project.path_with_namespace,
                                project.import_url)
-    return project.import_fail unless import_result
+      return project.import_fail unless import_result
+    else
+      unless project.create_repository
+        return project.import_fail
+      end
+    end
 
-    data_import_result =  if project.import_type == 'github'
-                            Gitlab::GithubImport::Importer.new(project).execute
-                          elsif project.import_type == 'gitlab'
-                            Gitlab::GitlabImport::Importer.new(project).execute
-                          elsif project.import_type == 'bitbucket'
-                            Gitlab::BitbucketImport::Importer.new(project).execute
-                          elsif project.import_type == 'google_code'
-                            Gitlab::GoogleCodeImport::Importer.new(project).execute
-                          else
-                            true
-                          end
+    data_import_result = case project.import_type
+                         when 'github'
+                           Gitlab::GithubImport::Importer.new(project).execute
+                         when 'gitlab'
+                           Gitlab::GitlabImport::Importer.new(project).execute
+                         when 'bitbucket'
+                           Gitlab::BitbucketImport::Importer.new(project).execute
+                         when 'google_code'
+                           Gitlab::GoogleCodeImport::Importer.new(project).execute
+                         when 'fogbugz'
+                           Gitlab::FogbugzImport::Importer.new(project).execute
+                         else
+                           true
+                         end
     return project.import_fail unless data_import_result
 
     Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket'
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index c47e5dab27c7ac3c757aa06c792e1c488694ebc2..689c3f3049d9b5187a742e80c5bbe382af6fe278 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -158,7 +158,7 @@ Settings.gitlab.default_projects_features['snippets']       = false if Settings.
 Settings.gitlab.default_projects_features['visibility_level']    = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
 Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root)
 Settings.gitlab['restricted_signup_domains'] ||= []
-Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','git']
+Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
 
 #
 # Reply by email
diff --git a/config/routes.rb b/config/routes.rb
index 25c286b308376553adbabbb1722fdf5be2d4f717..5bf721d6b13658e0ec85b7bd3a5c0c14f5ec3fa8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -99,6 +99,15 @@ Gitlab::Application.routes.draw do
       get   :new_user_map,    path: :user_map
       post  :create_user_map, path: :user_map
     end
+
+    resource :fogbugz, only: [:create, :new], controller: :fogbugz do
+      get :status
+      post :callback
+      get :jobs
+
+      get   :new_user_map,    path: :user_map
+      post  :create_user_map, path: :user_map
+    end
   end
 
   #
diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md
index 5cde90993d2fa600b8fa54be8a1efd53f7a84c44..7ccf06fbd60b8ef634dc98ce93c1537f9af44e76 100644
--- a/doc/workflow/importing/README.md
+++ b/doc/workflow/importing/README.md
@@ -3,6 +3,7 @@
 1. [Bitbucket](import_projects_from_bitbucket.md)
 2. [GitHub](import_projects_from_github.md)
 3. [GitLab.com](import_projects_from_gitlab_com.md)
+4. [FogBugz](import_projects_from_fogbugz.md)
 4. [SVN](migrating_from_svn.md)
 
 ### Note
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
new file mode 100644
index 0000000000000000000000000000000000000000..205c515bd3f804342e4302f7d012201ad0320bfc
Binary files /dev/null and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1e348d46ad6ecae30a9f0965ccbedee1cdeb03b
Binary files /dev/null and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
new file mode 100644
index 0000000000000000000000000000000000000000..ed362846909bf0edf0ef5ee220faa79df99b99ff
Binary files /dev/null and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
new file mode 100644
index 0000000000000000000000000000000000000000..d2fbd0267bdefb41309726243beab282f3130e25
Binary files /dev/null and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
new file mode 100644
index 0000000000000000000000000000000000000000..b1cc4b58525154dbb83e89479f1a25788d344f70
Binary files /dev/null and b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png differ
diff --git a/doc/workflow/importing/import_projects_from_fogbugz.md b/doc/workflow/importing/import_projects_from_fogbugz.md
new file mode 100644
index 0000000000000000000000000000000000000000..71af0f9ea44947fa98f1b776cb2e704b9b1b358f
--- /dev/null
+++ b/doc/workflow/importing/import_projects_from_fogbugz.md
@@ -0,0 +1,29 @@
+# Import your project from FogBugz to GitLab
+
+It only takes a few simple steps to import your project from FogBugz.
+The importer will import all of your cases and comments with original case
+numbers and timestamps. You will also have the opportunity to map FogBugz
+users to GitLab users.
+
+* From your GitLab dashboard click 'New project'
+
+* Click on the 'FogBugz' button
+
+![FogBugz](fogbugz_importer/fogbugz_import_select_fogbogz.png)
+
+* Enter your FogBugz URL, email address, and password.
+
+![Login](fogbugz_importer/fogbugz_import_login.png)
+
+* Create mapping from FogBugz users to GitLab users.
+
+![User Map](fogbugz_importer/fogbugz_import_user_map.png)
+
+* Select the projects you wish to import by clicking the Import buttons
+
+![Import Project](fogbugz_importer/fogbugz_import_select_project.png)
+
+* Once the import has finished click the link to take you to the project
+dashboard. Follow the directions to push your existing repository.
+
+![Finished](fogbugz_importer/fogbugz_import_finished.png)
diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
new file mode 100644
index 0000000000000000000000000000000000000000..431d50882fdcdaae3a4a1a50bafd07f1706a8514
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -0,0 +1,56 @@
+require 'fogbugz'
+
+module Gitlab
+  module FogbugzImport
+    class Client
+      attr_reader :api
+
+      def initialize(options = {})
+        if options[:uri] && options[:token]
+          @api = ::Fogbugz::Interface.new(options)
+        elsif options[:uri] && options[:email] && options[:password]
+          @api = ::Fogbugz::Interface.new(options)
+          @api.authenticate
+          @api
+        end
+      end
+
+      def get_token
+        @api.token
+      end
+
+      def valid?
+        !get_token.blank?
+      end
+
+      def user_map
+        users = {}
+        res = @api.command(:listPeople)
+        res['people']['person'].each do |user|
+          users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] }
+        end
+        users
+      end
+
+      def repos
+        res = @api.command(:listProjects)
+        @repos ||= res['projects']['project'].map { |proj| FogbugzImport::Repository.new(proj) }
+      end
+
+      def repo(id)
+        repos.find { |r| r.id.to_s == id.to_s }
+      end
+
+      def cases(project_id)
+        project_name = repo(project_id).name
+        res = @api.command(:search, q: "project:'#{project_name}'", cols: 'ixPersonAssignedTo,ixPersonOpenedBy,ixPersonClosedBy,sStatus,sPriority,sCategory,fOpen,sTitle,sLatestTextSummary,dtOpened,dtClosed,dtResolved,dtLastUpdated,events')
+        return [] unless res['cases']['count'].to_i > 0
+        res['cases']['case']
+      end
+
+      def categories
+        @api.command(:listCategories)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..61e08b2354320ce8a736cc6f2a8ad228ee20a120
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -0,0 +1,298 @@
+module Gitlab
+  module FogbugzImport
+    class Importer
+      attr_reader :project, :repo
+
+      def initialize(project)
+        @project = project
+
+        import_data = project.import_data.try(:data)
+        repo_data = import_data['repo'] if import_data
+        @repo = FogbugzImport::Repository.new(repo_data)
+
+        @known_labels = Set.new
+      end
+
+      def execute
+        return true unless repo.valid?
+
+        data = project.import_data.try(:data)
+
+        client = Gitlab::FogbugzImport::Client.new(token: data['fb_session']['token'], uri: data['fb_session']['uri'])
+
+        @cases = client.cases(@repo.id.to_i)
+        @categories = client.categories
+
+        import_cases
+
+        true
+      end
+
+      private
+
+      def user_map
+        @user_map ||= begin
+          user_map = Hash.new
+          import_data = project.import_data.try(:data)
+          stored_user_map = import_data['user_map'] if import_data
+          user_map.update(stored_user_map) if stored_user_map
+
+          user_map
+        end
+      end
+
+      def import_labels
+        @categories['categories']['category'].each do |label|
+          create_label(label['sCategory'])
+          @known_labels << name
+        end
+      end
+
+      def nice_label_color(name)
+        case name
+        when 'Blocker'
+          '#ff0000'
+        when 'Crash'
+          '#ffcfcf'
+        when 'Major'
+          '#deffcf'
+        when 'Minor'
+          '#cfe9ff'
+        when 'Bug'
+          '#d9534f'
+        when 'Feature'
+          '#44ad8e'
+        when 'Technical Task'
+          '#4b6dd0'
+        else
+          '#e2e2e2'
+        end
+      end
+
+      def create_label(name)
+        color = nice_label_color(name)
+        Label.create!(project_id: project.id, title: name, color: color)
+      end
+
+      def user_info(person_id)
+        user_hash = user_map[person_id.to_s]
+
+        user_name = ''
+        gitlab_id = nil
+
+        unless user_hash.nil?
+          user_name = user_hash['name']
+          if user = User.find_by(id: user_hash['gitlab_user'])
+            user_name = "@#{user.username}"
+            gitlab_id = user.id
+          end
+        end
+
+        { name: user_name, gitlab_id: gitlab_id }
+      end
+
+      def import_cases
+        return unless @cases
+
+        while bug = @cases.shift
+          author = user_info(bug['ixPersonOpenedBy'])[:name]
+          date = DateTime.parse(bug['dtOpened'])
+
+          comments = bug['events']['event']
+
+          content = format_content(opened_content(comments))
+          body = format_issue_body(author, date, content)
+
+          labels = []
+          [bug['sCategory'], bug['sPriority']].each do |label|
+            unless label.blank?
+              labels << label
+              unless @known_labels.include?(label)
+                create_label(label)
+                @known_labels << label
+              end
+            end
+          end
+
+          assignee_id = user_info(bug['ixPersonAssignedTo'])[:gitlab_id]
+          author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
+
+          issue = Issue.create!(
+            project_id:   project.id,
+            title:        bug['sTitle'],
+            description:  body,
+            author_id:    author_id,
+            assignee_id:  assignee_id,
+            state:        bug['fOpen'] == 'true' ? 'opened' : 'closed'
+          )
+          issue.add_labels_by_names(labels)
+
+          if issue.iid != bug['ixBug']
+            issue.update_attribute(:iid, bug['ixBug'])
+          end
+
+          import_issue_comments(issue, comments)
+
+          issue.update_attribute(:created_at, date)
+
+          last_update = DateTime.parse(bug['dtLastUpdated'])
+          issue.update_attribute(:updated_at, last_update)
+        end
+      end
+
+      def opened_content(comments)
+        while comment = comments.shift
+          if comment['sVerb'] == 'Opened'
+            return comment['s']
+          end
+        end
+        ''
+      end
+
+      def import_issue_comments(issue, comments)
+        Note.transaction do
+          while comment = comments.shift
+            verb = comment['sVerb']
+
+            next if verb == 'Opened' || verb === 'Closed'
+
+            content = format_content(comment['s'])
+            attachments = format_attachments(comment['rgAttachments'])
+            updates = format_updates(comment)
+
+            next if content.blank? && attachments.empty? && updates.empty?
+
+            author = user_info(comment['ixPerson'])[:name]
+            author_id = user_info(comment['ixPerson'])[:gitlab_id] || project.creator_id
+            date = DateTime.parse(comment['dt'])
+
+            body = format_issue_comment_body(
+              comment['ixBugEvent'],
+              author,
+              date,
+              content,
+              attachments,
+              updates
+            )
+
+            note = Note.create!(
+              project_id:     project.id,
+              noteable_type:  "Issue",
+              noteable_id:    issue.id,
+              author_id:      author_id,
+              note:           body
+            )
+
+            note.update_attribute(:created_at, date)
+            note.update_attribute(:updated_at, date)
+          end
+        end
+      end
+
+      def linkify_issues(s)
+        s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+        s = s.gsub(/([Cc]ase) ([0-9]+)/, '\1 #\2')
+        s
+      end
+
+      def escape_for_markdown(s)
+        s = s.gsub(/^#/, "\\#")
+        s = s.gsub(/^-/, "\\-")
+        s = s.gsub("`", "\\~")
+        s = s.gsub("\r", "")
+        s = s.gsub("\n", "  \n")
+        s
+      end
+
+      def format_content(raw_content)
+        return raw_content if raw_content.nil?
+        linkify_issues(escape_for_markdown(raw_content))
+      end
+
+      def format_attachments(raw_attachments)
+        return [] unless raw_attachments
+
+        attachments = case raw_attachments['attachment']
+                      when Array
+                        raw_attachments['attachment']
+                      when Hash
+                        [raw_attachments['attachment']]
+                      else
+                        []
+                      end
+
+        attachments.map! { |a| format_attachment(a) }
+        attachments.compact
+      end
+
+      def format_attachment(attachment)
+        link = build_attachment_url(attachment['sURL'])
+
+        res = ::Projects::DownloadService.new(project, link).execute
+
+        return nil if res.nil?
+
+        text = "[#{res['alt']}](#{res['url']})"
+        text = "!#{text}" if res['is_image']
+        text
+      end
+
+      def build_attachment_url(rel_url)
+        data = project.import_data.try(:data)
+        uri = data['fb_session']['uri']
+        token = data['fb_session']['token']
+        "#{uri}/#{rel_url}&token=#{token}"
+      end
+
+      def format_updates(comment)
+        updates = []
+
+        if comment['sChanges']
+          updates << "*Changes: #{linkify_issues(comment['sChanges'].chomp)}*"
+        end
+
+        if comment['evtDescription']
+          updates << "*#{comment['evtDescription']}*"
+        end
+
+        updates
+      end
+
+      def format_issue_body(author, date, content)
+        body = []
+        body << "*By #{author} on #{date} (imported from FogBugz)*"
+        body << '---'
+
+        if content.blank?
+          content = '*(No description has been entered for this issue)*'
+        end
+        body << content
+
+        body.join("\n\n")
+      end
+
+      def format_issue_comment_body(id, author, date, content, attachments, updates)
+        body = []
+        body << "*By #{author} on #{date} (imported from FogBugz)*"
+        body << '---'
+
+        if content.blank?
+          content = "*(No comment has been entered for this change)*"
+        end
+        body << content
+
+        if updates.any?
+          body << '---'
+          body += updates
+        end
+
+        if attachments.any?
+          body << '---'
+          body += attachments
+        end
+
+        body.join("\n\n")
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f02ea43910f1ac3b5ba8390c8897954e99028ef7
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/project_creator.rb
@@ -0,0 +1,38 @@
+module Gitlab
+  module FogbugzImport
+    class ProjectCreator
+      attr_reader :repo, :fb_session, :namespace, :current_user, :user_map
+
+      def initialize(repo, fb_session, namespace, current_user, user_map = nil)
+        @repo = repo
+        @fb_session = fb_session
+        @namespace = namespace
+        @current_user = current_user
+        @user_map = user_map
+      end
+
+      def execute
+        project = ::Projects::CreateService.new(current_user,
+          name: repo.safe_name,
+          path: repo.path,
+          namespace: namespace,
+          creator: current_user,
+          visibility_level: Gitlab::VisibilityLevel::INTERNAL,
+          import_type: 'fogbugz',
+          import_source: repo.name,
+          import_url: Project::UNKNOWN_IMPORT_URL
+        ).execute
+
+        import_data = project.create_import_data(
+          data: {
+            'repo' => repo.raw_data,
+            'user_map' => user_map,
+            'fb_session' => fb_session
+          }
+        )
+
+        project
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/fogbugz_import/repository.rb b/lib/gitlab/fogbugz_import/repository.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d1dc63db2b2bb8f66606b0e3f46a3106313524ce
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/repository.rb
@@ -0,0 +1,31 @@
+module Gitlab
+  module FogbugzImport
+    class Repository
+      attr_accessor :raw_data
+
+      def initialize(raw_data)
+        @raw_data = raw_data
+      end
+
+      def valid?
+        raw_data.is_a?(Hash)
+      end
+
+      def id
+        raw_data['ixProject']
+      end
+
+      def name
+        raw_data['sProject']
+      end
+
+      def safe_name
+        name.gsub(/[^\s\w.-]/, '')
+      end
+
+      def path
+        safe_name.gsub(/[\s]/, '_')
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 991b70aab6a3756b450abf1a21f07cf4c43066ba..ccfdfbe73e8fa93101969fb2e2ddb97b42a0086c 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -19,6 +19,7 @@ module Gitlab
           'GitLab.com'      => 'gitlab',
           'Gitorious.org'   => 'gitorious',
           'Google Code'     => 'google_code',
+          'FogBugz'         => 'fogbugz',
           'Any repo by URL' => 'git',
         }
       end
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..27b11267d2af3c6347b3f0a30a1a2c03ec28eff2
--- /dev/null
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+require_relative 'import_spec_helper'
+
+describe Import::FogbugzController do
+  include ImportSpecHelper
+
+  let(:user) { create(:user) }
+
+  before do
+    sign_in(user)
+  end
+
+  describe 'GET status' do
+    before do
+      @repo = OpenStruct.new(name: 'vim')
+      stub_client(valid?: true)
+    end
+
+    it 'assigns variables' do
+      @project = create(:project, import_type: 'fogbugz', creator_id: user.id)
+      stub_client(repos: [@repo])
+
+      get :status
+
+      expect(assigns(:already_added_projects)).to eq([@project])
+      expect(assigns(:repos)).to eq([@repo])
+    end
+
+    it 'does not show already added project' do
+      @project = create(:project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim')
+      stub_client(repos: [@repo])
+
+      get :status
+
+      expect(assigns(:already_added_projects)).to eq([@project])
+      expect(assigns(:repos)).to eq([])
+    end
+  end
+end
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f12e09c58c36fb0f53bc9e8c0fb4c3e047bf8a82
--- /dev/null
+++ b/spec/services/projects/download_service_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Projects::DownloadService do
+  describe 'File service' do
+    before do
+      @user = create :user
+      @project = create :project, creator_id: @user.id, namespace: @user.namespace
+    end
+
+    context 'for a URL that is not on whitelist' do
+      before do
+        url = 'https://code.jquery.com/jquery-2.1.4.min.js'
+        @link_to_file = download_file(@project, url)
+      end
+
+      it { expect(@link_to_file).to eq(nil) }
+    end
+
+    context 'for URLs that are on the whitelist' do
+      before do
+        sham_rack_app = ShamRack.at('mycompany.fogbugz.com').stub
+        sham_rack_app.register_resource('/rails_sample.jpg', File.read(Rails.root + 'spec/fixtures/rails_sample.jpg'), 'image/jpg')
+        sham_rack_app.register_resource('/doc_sample.txt', File.read(Rails.root + 'spec/fixtures/doc_sample.txt'), 'text/plain')
+      end
+
+      after do
+        ShamRack.unmount_all
+      end
+
+      context 'an image file' do
+        before do
+          url = 'http://mycompany.fogbugz.com/rails_sample.jpg'
+          @link_to_file = download_file(@project, url)
+        end
+
+        it { expect(@link_to_file).to have_key('alt') }
+        it { expect(@link_to_file).to have_key('url') }
+        it { expect(@link_to_file).to have_key('is_image') }
+        it { expect(@link_to_file['is_image']).to be true }
+        it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
+        it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
+        it { expect(@link_to_file['alt']).to eq('rails_sample') }
+      end
+
+      context 'a txt file' do
+        before do
+          url = 'http://mycompany.fogbugz.com/doc_sample.txt'
+          @link_to_file = download_file(@project, url)
+        end
+
+        it { expect(@link_to_file).to have_key('alt') }
+        it { expect(@link_to_file).to have_key('url') }
+        it { expect(@link_to_file).to have_key('is_image') }
+        it { expect(@link_to_file['is_image']).to be false }
+        it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
+        it { expect(@link_to_file['url']).to match('doc_sample.txt') }
+        it { expect(@link_to_file['alt']).to eq('doc_sample.txt') }
+      end
+    end
+  end
+
+  def download_file(repository, url)
+    Projects::DownloadService.new(repository, url).execute
+  end
+end