Repository: mkdocs/mkdocs

Issue:
Broken 'Edit on GitHub' link
We recently updated to 0.17.0 and it seems to have broken our 'Edit on GitHub' link, you can see this in action [here](https://opensciencegrid.github.io/docs/). The link given is https://github.com/edit/master/docs/index.md when it should be https://github.com/opensciencegrid/docs/edit/master/docs/index.md. [repo_url](https://github.com/opensciencegrid/docs/blob/master/mkdocs.yml#L3) appears to be set properly.


Discussion:
Thanks for the report. It appears that the problem presents itself if you are missing a closing slash at the end of the repo_url. Add the closing slash and it should work correctly:

```yaml
repo_url: https://github.com/opensciencegrid/docs/
```

I'm just guessing, but it could be that the added support for fragment and query string based schemes was confused by the lack of a closing slash and messed things up. In any event, it appears that we only tested with the slash, not without. After all, MkDocs own config includes the slash and the example in the documentation shows a slash.

I'm undecided if the closing slash should be a requirement. Although, apparently, it previously worked fine without it, so requiring it would be a regression for some users.
Does that mean the `site_url` should also have a trailing slash?
This is unique to `repo_url`.  Consider the following examples:

```
http://example.com/user/project?branch=master&action=edit&path=path/to/page.md
http://example.com/user/project#branch/path/to/page.md
http://example.com/user/project/edit/branch/path/to/page.md
```

In the first two examples, the `repo_url` does not end in a slash, but it does in the third example. We support all three. So if your host uses a scheme which requires a slash between the `repo_url` and the `edit_uri`, then the setting should include one. However, if your host does not use a slash between the `repo_url` and the `edit_uri`, then your setting should not include one. At least that's how it currently works. The question is if that's how it *should* work.
I looked at the tests and realized that we were missing a test. We have a test were `repo_url` is set to `http://example.com` but not `http://example.com/foo`. The tests pass for the first one, but when I added the second, it failed. This is definitely a bug. 

We were also missing a test for `http://example.com/foo/` (closing slash included) but that works as it should. For now, the workaround is to end the `repo_url` with a slash.

Resolving patch:
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -276,7 +276,7 @@ def post_validation(self, config, key_name):
         if edit_uri:
             if not edit_uri.startswith(('?', '#')) \
                     and not config['repo_url'].endswith('/'):
-                edit_uri = '/' + edit_uri
+                config['repo_url'] += '/'
             if not edit_uri.endswith('/'):
                 edit_uri += '/'
 


Test patch:
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -196,7 +196,7 @@ def test_edit_uri_github(self):
         option = config_options.RepoURL()
         config = {'repo_url': "https://github.com/mkdocs/mkdocs"}
         option.post_validation(config, 'repo_url')
-        self.assertEqual(config['edit_uri'], '/edit/master/docs/')
+        self.assertEqual(config['edit_uri'], 'edit/master/docs/')
 
     def test_edit_uri_bitbucket(self):
 
@@ -218,7 +218,7 @@ def test_repo_name_custom_and_empty_edit_uri(self):
         config = {'repo_url': "https://github.com/mkdocs/mkdocs",
                   'repo_name': 'mkdocs'}
         option.post_validation(config, 'repo_url')
-        self.assertEqual(config.get('edit_uri'), '/edit/master/docs/')
+        self.assertEqual(config.get('edit_uri'), 'edit/master/docs/')
 
 
 class DirTest(unittest.TestCase):
diff --git a/mkdocs/tests/nav_tests.py b/mkdocs/tests/nav_tests.py
--- a/mkdocs/tests/nav_tests.py
+++ b/mkdocs/tests/nav_tests.py
@@ -466,9 +466,6 @@ def test_nesting(self):
         self.assertEqual(str(site_navigation).strip(), expected)
 
     def test_edit_uri(self):
-        """
-        Ensure that set_edit_url creates well formed URLs for edit_uri
-        """
 
         pages = [
             'index.md',
@@ -500,6 +497,47 @@ def test_edit_uri(self):
         for idx, page in enumerate(site_navigation.walk_pages()):
             self.assertEqual(page.edit_url, expected_results[idx])
 
+    def test_edit_uri_sub_dir(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub/internal.md',
+            'sub1/sub2/internal.md',
+        ]
+
+        # Basic test
+        repo_url = 'http://example.com/foo/'
+        edit_uri = 'edit/master/docs/'
+
+        site_navigation = nav.SiteNavigation(load_config(
+            pages=pages,
+            repo_url=repo_url,
+            edit_uri=edit_uri,
+            site_dir='site',
+            site_url='',
+            use_directory_urls=True
+        ))
+
+        expected_results = (
+            repo_url + edit_uri + pages[0],
+            repo_url + edit_uri + pages[1],
+            repo_url + edit_uri + pages[2],
+            repo_url + edit_uri + pages[3],
+        )
+
+        for idx, page in enumerate(site_navigation.walk_pages()):
+            self.assertEqual(page.edit_url, expected_results[idx])
+
+    def test_edit_uri_missing_slash(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub/internal.md',
+            'sub1/sub2/internal.md',
+        ]
+
         # Ensure the '/' is added to the repo_url and edit_uri
         repo_url = 'http://example.com'
         edit_uri = 'edit/master/docs'
@@ -513,9 +551,57 @@ def test_edit_uri(self):
             use_directory_urls=True
         ))
 
+        expected_results = (
+            repo_url + '/' + edit_uri + '/' + pages[0],
+            repo_url + '/' + edit_uri + '/' + pages[1],
+            repo_url + '/' + edit_uri + '/' + pages[2],
+            repo_url + '/' + edit_uri + '/' + pages[3],
+        )
+
         for idx, page in enumerate(site_navigation.walk_pages()):
             self.assertEqual(page.edit_url, expected_results[idx])
 
+    def test_edit_uri_sub_dir_missing_slash(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub/internal.md',
+            'sub1/sub2/internal.md',
+        ]
+
+        # Basic test
+        repo_url = 'http://example.com/foo'
+        edit_uri = 'edit/master/docs'
+
+        site_navigation = nav.SiteNavigation(load_config(
+            pages=pages,
+            repo_url=repo_url,
+            edit_uri=edit_uri,
+            site_dir='site',
+            site_url='',
+            use_directory_urls=True
+        ))
+
+        expected_results = (
+            repo_url + '/' + edit_uri + '/' + pages[0],
+            repo_url + '/' + edit_uri + '/' + pages[1],
+            repo_url + '/' + edit_uri + '/' + pages[2],
+            repo_url + '/' + edit_uri + '/' + pages[3],
+        )
+
+        for idx, page in enumerate(site_navigation.walk_pages()):
+            self.assertEqual(page.edit_url, expected_results[idx])
+
+    def test_edit_uri_query_string(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub/internal.md',
+            'sub1/sub2/internal.md',
+        ]
+
         # Ensure query strings are supported
         repo_url = 'http://example.com'
         edit_uri = '?query=edit/master/docs/'
@@ -539,6 +625,15 @@ def test_edit_uri(self):
         for idx, page in enumerate(site_navigation.walk_pages()):
             self.assertEqual(page.edit_url, expected_results[idx])
 
+    def test_edit_uri_fragment(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub/internal.md',
+            'sub1/sub2/internal.md',
+        ]
+
         # Ensure fragment strings are supported
         repo_url = 'http://example.com'
         edit_uri = '#fragment/edit/master/docs/'
@@ -563,9 +658,6 @@ def test_edit_uri(self):
             self.assertEqual(page.edit_url, expected_results[idx])
 
     def test_edit_uri_windows(self):
-        """
-        Ensure that set_edit_url creates well formed URLs for edit_uri with a windows path
-        """
 
         pages = [
             'index.md',
@@ -597,6 +689,47 @@ def test_edit_uri_windows(self):
         for idx, page in enumerate(site_navigation.walk_pages()):
             self.assertEqual(page.edit_url, expected_results[idx])
 
+    def test_edit_uri_sub_dir_windows(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub\\internal.md',
+            'sub1\\sub2\\internal.md',
+        ]
+
+        # Basic test
+        repo_url = 'http://example.com/foo/'
+        edit_uri = 'edit/master/docs/'
+
+        site_navigation = nav.SiteNavigation(load_config(
+            pages=pages,
+            repo_url=repo_url,
+            edit_uri=edit_uri,
+            site_dir='site',
+            site_url='',
+            use_directory_urls=True
+        ))
+
+        expected_results = (
+            repo_url + edit_uri + pages[0],
+            repo_url + edit_uri + pages[1],
+            repo_url + edit_uri + pages[2].replace('\\', '/'),
+            repo_url + edit_uri + pages[3].replace('\\', '/'),
+        )
+
+        for idx, page in enumerate(site_navigation.walk_pages()):
+            self.assertEqual(page.edit_url, expected_results[idx])
+
+    def test_edit_uri_missing_slash_windows(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub\\internal.md',
+            'sub1\\sub2\\internal.md',
+        ]
+
         # Ensure the '/' is added to the repo_url and edit_uri
         repo_url = 'http://example.com'
         edit_uri = 'edit/master/docs'
@@ -610,9 +743,57 @@ def test_edit_uri_windows(self):
             use_directory_urls=True
         ))
 
+        expected_results = (
+            repo_url + '/' + edit_uri + '/' + pages[0],
+            repo_url + '/' + edit_uri + '/' + pages[1],
+            repo_url + '/' + edit_uri + '/' + pages[2].replace('\\', '/'),
+            repo_url + '/' + edit_uri + '/' + pages[3].replace('\\', '/'),
+        )
+
         for idx, page in enumerate(site_navigation.walk_pages()):
             self.assertEqual(page.edit_url, expected_results[idx])
 
+    def test_edit_uri_sub_dir_missing_slash_windows(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub\\internal.md',
+            'sub1\\sub2\\internal.md',
+        ]
+
+        # Ensure the '/' is added to the repo_url and edit_uri
+        repo_url = 'http://example.com/foo'
+        edit_uri = 'edit/master/docs'
+
+        site_navigation = nav.SiteNavigation(load_config(
+            pages=pages,
+            repo_url=repo_url,
+            edit_uri=edit_uri,
+            site_dir='site',
+            site_url='',
+            use_directory_urls=True
+        ))
+
+        expected_results = (
+            repo_url + '/' + edit_uri + '/' + pages[0],
+            repo_url + '/' + edit_uri + '/' + pages[1],
+            repo_url + '/' + edit_uri + '/' + pages[2].replace('\\', '/'),
+            repo_url + '/' + edit_uri + '/' + pages[3].replace('\\', '/'),
+        )
+
+        for idx, page in enumerate(site_navigation.walk_pages()):
+            self.assertEqual(page.edit_url, expected_results[idx])
+
+    def test_edit_uri_query_string_windows(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub\\internal.md',
+            'sub1\\sub2\\internal.md',
+        ]
+
         # Ensure query strings are supported
         repo_url = 'http://example.com'
         edit_uri = '?query=edit/master/docs/'
@@ -636,6 +817,15 @@ def test_edit_uri_windows(self):
         for idx, page in enumerate(site_navigation.walk_pages()):
             self.assertEqual(page.edit_url, expected_results[idx])
 
+    def test_edit_uri_fragment_windows(self):
+
+        pages = [
+            'index.md',
+            'internal.md',
+            'sub\\internal.md',
+            'sub1\\sub2\\internal.md',
+        ]
+
         # Ensure fragment strings are supported
         repo_url = 'http://example.com'
         edit_uri = '#fragment/edit/master/docs/'