Repository: mkdocs/mkdocs

Issue:
Sometimes deploy to gh-pages fails
Encountered while fixing #966.  Originally added `--force` in #967, but it makes more sense to do some more research on the root cause.

Possibly:
 - GH token permissions
 - Same GH token used on multiple machines(?)
 - Some git call is failing silently

Will investigate repro steps and update here.

Discussion:

Patch:
diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md
index fdb21e46..e52abf87 100644
--- a/docs/about/release-notes.md
+++ b/docs/about/release-notes.md
@@ -164,6 +164,8 @@ better conform with the documented [layout].
   via the new [`edit_uri`](../user-guide/configuration.md#edit_uri) setting.
 * Bugfix: Don't override config value for strict mode if not specified on CLI
   (#738).
+* Add a `--force` flag to the `gh-deploy` command to force the push to the
+  repository (#973).
 
 ## Version 0.15.3 (2016-02-18)
 
diff --git a/mkdocs/__main__.py b/mkdocs/__main__.py
index 016564b3..de26cde9 100644
--- a/mkdocs/__main__.py
+++ b/mkdocs/__main__.py
@@ -86,6 +86,7 @@ remote_branch_help = ("The remote branch to commit to for Github Pages. This "
                       "overrides the value specified in config")
 remote_name_help = ("The remote name to commit to for Github Pages. This "
                     "overrides the value specified in config")
+force_help = "Force the push to the repository."
 
 
 @click.group(context_settings={'help_option_names': ['-h', '--help']})
@@ -198,8 +199,9 @@ def json_command(clean, config_file, strict, site_dir):
 @click.option('-m', '--message', help=commit_message_help)
 @click.option('-b', '--remote-branch', help=remote_branch_help)
 @click.option('-r', '--remote-name', help=remote_name_help)
+@click.option('--force', is_flag=True, help=force_help)
 @common_options
-def gh_deploy_command(config_file, clean, message, remote_branch, remote_name):
+def gh_deploy_command(config_file, clean, message, remote_branch, remote_name, force):
     """Deploy your documentation to GitHub Pages"""
     try:
         cfg = config.load_config(
@@ -208,7 +210,7 @@ def gh_deploy_command(config_file, clean, message, remote_branch, remote_name):
             remote_name=remote_name
         )
         build.build(cfg, dirty=not clean)
-        gh_deploy.gh_deploy(cfg, message=message)
+        gh_deploy.gh_deploy(cfg, message=message, force=force)
     except exceptions.ConfigurationError as e:  # pragma: no cover
         # Avoid ugly, unhelpful traceback
         raise SystemExit('\n' + str(e))
diff --git a/mkdocs/commands/gh_deploy.py b/mkdocs/commands/gh_deploy.py
index 9a240006..0f504c7a 100644
--- a/mkdocs/commands/gh_deploy.py
+++ b/mkdocs/commands/gh_deploy.py
@@ -49,7 +49,7 @@ def _get_remote_url(remote_name):
     return host, path
 
 
-def gh_deploy(config, message=None):
+def gh_deploy(config, message=None, force=False):
 
     if not _is_cwd_git_repo():
         log.error('Cannot deploy - this directory does not appear to be a git '
@@ -66,7 +66,7 @@ def gh_deploy(config, message=None):
              config['site_dir'], config['remote_branch'])
 
     result, error = ghp_import.ghp_import(config['site_dir'], message, remote_name,
-                                          remote_branch)
+                                          remote_branch, force)
     if not result:
         log.error("Failed to deploy to GitHub with error: \n%s", error)
         raise SystemExit(1)
diff --git a/mkdocs/utils/ghp_import.py b/mkdocs/utils/ghp_import.py
index 5339b216..62ef8c5c 100644
--- a/mkdocs/utils/ghp_import.py
+++ b/mkdocs/utils/ghp_import.py
@@ -158,7 +158,7 @@ def run_import(srcdir, branch, message, nojekyll):
         sys.stdout.write(enc("Failed to process commit.\n"))
 
 
-def ghp_import(directory, message, remote='origin', branch='gh-pages'):
+def ghp_import(directory, message, remote='origin', branch='gh-pages', force=False):
 
     if not try_rebase(remote, branch):
         log.error("Failed to rebase %s branch.", branch)
@@ -167,8 +167,12 @@ def ghp_import(directory, message, remote='origin', branch='gh-pages'):
 
     run_import(directory, branch, message, nojekyll)
 
-    proc = sp.Popen(['git', 'push', remote, branch],
-                    stdout=sp.PIPE, stderr=sp.PIPE)
+    cmd = ['git', 'push', remote, branch]
+
+    if force:
+        cmd.insert(2, '--force')
+
+    proc = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
 
     out, err = proc.communicate()
     result = proc.wait() == 0


Test patch:
diff --git a/mkdocs/tests/cli_tests.py b/mkdocs/tests/cli_tests.py
index 0014dc2a..56b01e20 100644
--- a/mkdocs/tests/cli_tests.py
+++ b/mkdocs/tests/cli_tests.py
@@ -349,6 +349,8 @@ class CLITests(unittest.TestCase):
         g_args, g_kwargs = mock_gh_deploy.call_args
         self.assertTrue('message' in g_kwargs)
         self.assertEqual(g_kwargs['message'], None)
+        self.assertTrue('force' in g_kwargs)
+        self.assertEqual(g_kwargs['force'], False)
         self.assertEqual(mock_build.call_count, 1)
         b_args, b_kwargs = mock_build.call_args
         self.assertTrue('dirty' in b_kwargs)
@@ -456,3 +458,19 @@ class CLITests(unittest.TestCase):
             remote_branch=None,
             remote_name='foo'
         )
+
+    @mock.patch('mkdocs.config.load_config', autospec=True)
+    @mock.patch('mkdocs.commands.build.build', autospec=True)
+    @mock.patch('mkdocs.commands.gh_deploy.gh_deploy', autospec=True)
+    def test_gh_deploy_force(self, mock_gh_deploy, mock_build, mock_load_config):
+
+        result = self.runner.invoke(
+            cli.cli, ['gh-deploy', '--force'], catch_exceptions=False)
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(mock_gh_deploy.call_count, 1)
+        g_args, g_kwargs = mock_gh_deploy.call_args
+        self.assertTrue('force' in g_kwargs)
+        self.assertEqual(g_kwargs['force'], True)
+        self.assertEqual(mock_build.call_count, 1)
+        self.assertEqual(mock_load_config.call_count, 1)