Skip to content

Commit d119286

Browse files
committed
Improve behavior of updating aliases
This adds an important check for the `alias` subcommand to prevent setting an alias on two different versions, allows for updating an alias via the `alias` subcommand, and generally improves docs about aliases.
1 parent d60c381 commit d119286

File tree

8 files changed

+170
-55
lines changed

8 files changed

+170
-55
lines changed

CHANGES.md

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Allow deploying docs to a subdirectory within the target branch via `--prefix`
1414
- Add support for custom templates with `mike set-default`
1515
- Read from `remote_branch` and `remote_name` if set in `mkdocs.yml`
16+
- Allow updating an existing alias with `mike alias -u`
1617

1718
### Breaking changes
1819
- Require Python 3.6+
@@ -21,6 +22,8 @@
2122

2223
### Bug fixes
2324
- Canonical URLs in generated documentation now point to the correct location
25+
- `mike alias` now checks for existing aliases to prevent erroneously setting an
26+
alias for two different versions
2427
- Replace `packaging` dependency with `verspec` for future stability
2528

2629
[material-mike]: https://squidfunk.github.io/mkdocs-material/setup/setting-up-versioning/#versioning

README.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -78,18 +78,21 @@ particularly-relevant version of your docs somewhere special (e.g. `latest`):
7878
mike deploy [version] [alias]...
7979
```
8080

81+
If `[version]` already exists, this command will *also* update all of the
82+
pre-existing aliases for it. Normally, if an alias specified on the command line
83+
is already associated with another version, this will return an error. If you
84+
*do* want to move an alias from another version to this version (e.g. when
85+
releasing a new version and updating the `latest` alias to point to this new
86+
version), you can pass `-u`/`--update-aliases` to allow this.
87+
8188
By default, aliases create a simple HTML redirect to the real version of the
8289
docs; to create a copy of the docs for each alias, you can pass `--no-redirect`.
8390
If you're using redirects, you can customize the redirect template with
8491
`-T`/`--template`; this takes a path to a [Jinja][jinja] template that accepts
8592
an `{{href}}` variable.
8693

8794
If you'd like to specify a title for this version that doesn't match the version
88-
string, you can pass `-t TITLE`/`--title=TITLE` as well. If `version` already
89-
exists, this command will *also* update all of the pre-existing aliases for it.
90-
If you want to move an alias from a previous version to this version (e.g. when
91-
releasing a new version and updating a `latest` alias), you can pass
92-
`-u`/`--update-aliases` to allow this.
95+
string, you can pass `-t TITLE`/`--title=TITLE` as well.
9396

9497
In addition, you can specify where to deploy your docs via `-b`/`--branch`,
9598
`-r`/`--remote`, and `--prefix`, specifying the branch, remote, and directory
@@ -185,6 +188,9 @@ your documentation. You can use the `alias` command for this:
185188
mike alias [version-or-alias] [alias]...
186189
```
187190

191+
As with `deploy`, you can pass `-u`/`--update-aliases` to change where an
192+
existing alias points to.
193+
188194
Once again, you can specify `--branch`, `--push`, etc to control how the commit
189195
is handled.
190196

mike/commands.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ def delete(versions=None, all=False, *, branch='gh-pages', message=None,
131131
commit.add_file(versions_to_file_info(all_versions, prefix))
132132

133133

134-
def alias(cfg, version, aliases, redirect=True, template=None, *,
135-
branch='gh-pages', message=None, prefix=''):
134+
def alias(cfg, version, aliases, update_aliases=False, redirect=True,
135+
template=None, *, branch='gh-pages', message=None, prefix=''):
136136
all_versions = list_versions(branch, prefix)
137137
try:
138138
real_version = all_versions.find(version, strict=True)[0]
@@ -150,7 +150,8 @@ def alias(cfg, version, aliases, redirect=True, template=None, *,
150150
mike_version=app_version
151151
)
152152

153-
new_aliases = all_versions.update(real_version, aliases=aliases)
153+
new_aliases = all_versions.update(real_version, aliases=aliases,
154+
update_aliases=update_aliases)
154155
destdirs = [os.path.join(prefix, i) for i in new_aliases]
155156

156157
if redirect and destdirs:

mike/driver.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,9 @@ def delete(args):
141141
def alias(args):
142142
cfg = load_mkdocs_config(args)
143143
check_remote_status(args, strict=True)
144-
commands.alias(cfg, args.version, args.alias, args.redirect, args.template,
145-
branch=args.branch, message=args.message,
146-
prefix=args.prefix)
144+
commands.alias(cfg, args.version, args.alias, args.update_aliases,
145+
args.redirect, args.template, branch=args.branch,
146+
message=args.message, prefix=args.prefix)
147147
if args.push:
148148
git_utils.push_branch(args.remote, args.branch, args.force)
149149

@@ -223,7 +223,7 @@ def main():
223223
deploy_p.add_argument('-t', '--title',
224224
help='short descriptive title for this version')
225225
deploy_p.add_argument('-u', '--update-aliases', action='store_true',
226-
help='allow aliases pointing to other versions')
226+
help='update aliases pointing to other versions')
227227
deploy_p.add_argument('--no-redirect', dest='redirect', default=True,
228228
action='store_false',
229229
help='make copies of docs for each alias')
@@ -249,6 +249,8 @@ def main():
249249
'alias', description=alias_desc, help='alias docs from a branch'
250250
)
251251
alias_p.set_defaults(func=alias)
252+
alias_p.add_argument('-u', '--update-aliases', action='store_true',
253+
help='update aliases pointing to other versions')
252254
alias_p.add_argument('--no-redirect', dest='redirect', default=True,
253255
action='store_false',
254256
help='make copies of docs for each alias')

mike/versions.py

+29-7
Original file line numberDiff line numberDiff line change
@@ -82,30 +82,52 @@ def find(self, version, strict=False):
8282
raise KeyError(version)
8383
return None
8484

85-
def add(self, version, title=None, aliases=[], update_aliases=False):
86-
v = _ensure_version(version)
85+
def _ensure_unique_aliases(self, version, aliases, update_aliases=False):
8786
removed_aliases = []
8887
for i in aliases:
8988
key = self.find(i)
90-
if key and key[0] != v:
91-
if not update_aliases or len(key) == 1:
92-
raise ValueError('{!r} already exists'.format(i))
89+
if key and key[0] != version:
90+
if len(key) == 1:
91+
raise ValueError(
92+
'alias {!r} already specified as a version'.format(i)
93+
)
94+
if not update_aliases:
95+
raise ValueError(
96+
'alias {!r} already exists for version {}'
97+
.format(i, str(key[0]))
98+
)
9399
removed_aliases.append(key)
100+
return removed_aliases
101+
102+
def add(self, version, title=None, aliases=[], update_aliases=False):
103+
v = _ensure_version(version)
104+
removed_aliases = self._ensure_unique_aliases(
105+
v, aliases, update_aliases
106+
)
94107

95108
if v in self._data:
96109
self._data[v].update(title, aliases)
97110
else:
98111
if self.find(version):
99-
raise ValueError('{!r} already exists'.format(version))
112+
raise ValueError('version {} already exists'.format(version))
100113
self._data[v] = VersionInfo(version, title, aliases)
101114

115+
# Remove aliases from old versions that we've moved to this version.
102116
for i in removed_aliases:
103117
self._data[i[0]].aliases.remove(i[1])
104118

105119
return self._data[v]
106120

107-
def update(self, version, title=None, aliases=[]):
121+
def update(self, version, title=None, aliases=[], update_aliases=False):
108122
key = self.find(version, strict=True)
123+
removed_aliases = self._ensure_unique_aliases(
124+
key[0], aliases, update_aliases
125+
)
126+
127+
# Remove aliases from old versions that we've moved to this version.
128+
for i in removed_aliases:
129+
self._data[i[0]].aliases.remove(i[1])
130+
109131
return self._data[key[0]].update(title, aliases)
110132

111133
def _remove_by_key(self, key):

test/integration/test_alias.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ def _deploy(self, branch=None, versions=['1.0'], prefix=''):
2020
assertPopen(['mike', 'deploy', i] + extra_args)
2121

2222
def _test_alias(self, expected_message=None,
23-
expected_versions=[versions.VersionInfo('1.0')],
24-
redirect=True, directory='.'):
23+
expected_versions=[
24+
versions.VersionInfo('1.0', aliases=['latest'])
25+
], redirect=True, directory='.'):
2526
message = assertPopen(['git', 'log', '-1', '--pretty=%B']).rstrip()
2627
if expected_message:
2728
self.assertEqual(message, expected_message)
@@ -43,9 +44,8 @@ def _test_alias(self, expected_message=None,
4344
assertDirectory(directory, files, allow_extra=True)
4445

4546
with open(os.path.join(directory, 'versions.json')) as f:
46-
self.assertEqual(list(versions.Versions.loads(f.read())), [
47-
versions.VersionInfo('1.0', aliases=['latest']),
48-
])
47+
self.assertEqual(list(versions.Versions.loads(f.read())),
48+
expected_versions)
4949

5050

5151
class TestAlias(AliasTestCase):
@@ -65,6 +65,19 @@ def test_alias(self):
6565
with open('latest/index.html') as f:
6666
self.assertRegex(f.read(), match_redir('../1.0/'))
6767

68+
def test_update_aliases(self):
69+
assertPopen(['mike', 'deploy', '1.0', 'latest'])
70+
assertPopen(['mike', 'deploy', '2.0'])
71+
assertPopen(['mike', 'alias', '2.0', 'latest', '-u'])
72+
check_call_silent(['git', 'checkout', 'gh-pages'])
73+
self._test_alias(expected_versions=[
74+
versions.VersionInfo('2.0', aliases=['latest']),
75+
versions.VersionInfo('1.0', ),
76+
])
77+
78+
with open('latest/index.html') as f:
79+
self.assertRegex(f.read(), match_redir('../2.0/'))
80+
6881
def test_alias_copy(self):
6982
self._deploy()
7083
assertPopen(['mike', 'alias', '1.0', 'latest', '--no-redirect'])

test/unit/test_commands.py

+58-24
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ def _test_deploy(self, expected_message=None,
8888

8989
self._test_state(expected_message, expected_versions, **kwargs)
9090

91+
def _mock_commit(self):
92+
with git_utils.Commit('gh-pages', 'add versions.json') as commit:
93+
commit.add_file(git_utils.FileInfo(
94+
'versions.json',
95+
'[{"version": "1.0", "title": "1.0", "aliases": ["latest"]}]',
96+
))
97+
commit.add_file(git_utils.FileInfo('1.0/page.html', ''))
98+
commit.add_file(git_utils.FileInfo('1.0/file.txt', ''))
99+
commit.add_file(git_utils.FileInfo('1.0/dir/index.html', ''))
100+
commit.add_file(git_utils.FileInfo('latest/page.html', ''))
101+
commit.add_file(git_utils.FileInfo('latest/dir/index.html', ''))
102+
91103
def test_default(self):
92104
commands.deploy(self.cfg, '1.0')
93105
check_call_silent(['git', 'checkout', 'gh-pages'])
@@ -179,18 +191,24 @@ def test_overwrite_version(self):
179191
versions.VersionInfo('1.0', '1.0.1', ['latest', 'greatest'])
180192
])
181193

182-
def test_overwrite_alias(self):
183-
with git_utils.Commit('gh-pages', 'add versions.json') as commit:
184-
commit.add_file(git_utils.FileInfo(
185-
'versions.json',
186-
'[{"version": "1.0", "title": "1.0", "aliases": ["latest"]}]',
187-
))
188-
commit.add_file(git_utils.FileInfo('1.0/page.html', ''))
189-
commit.add_file(git_utils.FileInfo('1.0/file.txt', ''))
190-
commit.add_file(git_utils.FileInfo('1.0/dir/index.html', ''))
191-
commit.add_file(git_utils.FileInfo('latest/page.html', ''))
192-
commit.add_file(git_utils.FileInfo('latest/dir/index.html', ''))
194+
def test_overwrite_same_alias(self):
195+
self._mock_commit()
196+
commands.deploy(self.cfg, '1.0', '1.0.1', ['latest'])
197+
check_call_silent(['git', 'checkout', 'gh-pages'])
198+
self._test_deploy(expected_versions=[
199+
versions.VersionInfo('1.0', '1.0.1', ['latest'])
200+
])
193201

202+
def test_overwrite_include_same_alias(self):
203+
self._mock_commit()
204+
commands.deploy(self.cfg, '1.0', '1.0.1', ['latest', 'greatest'])
205+
check_call_silent(['git', 'checkout', 'gh-pages'])
206+
self._test_deploy(expected_versions=[
207+
versions.VersionInfo('1.0', '1.0.1', ['latest', 'greatest'])
208+
])
209+
210+
def test_overwrite_alias_error(self):
211+
self._mock_commit()
194212
with self.assertRaises(ValueError):
195213
commands.deploy(self.cfg, '2.0', '2.0.0', ['latest'])
196214
check_call_silent(['git', 'checkout', 'gh-pages'])
@@ -199,17 +217,7 @@ def test_overwrite_alias(self):
199217
])
200218

201219
def test_update_aliases(self):
202-
with git_utils.Commit('gh-pages', 'add versions.json') as commit:
203-
commit.add_file(git_utils.FileInfo(
204-
'versions.json',
205-
'[{"version": "1.0", "title": "1.0", "aliases": ["latest"]}]',
206-
))
207-
commit.add_file(git_utils.FileInfo('1.0/page.html', ''))
208-
commit.add_file(git_utils.FileInfo('1.0/file.txt', ''))
209-
commit.add_file(git_utils.FileInfo('1.0/dir/index.html', ''))
210-
commit.add_file(git_utils.FileInfo('latest/page.html', ''))
211-
commit.add_file(git_utils.FileInfo('latest/dir/index.html', ''))
212-
220+
self._mock_commit()
213221
commands.deploy(self.cfg, '2.0', '2.0.0', ['latest'], True)
214222
check_call_silent(['git', 'checkout', 'gh-pages'])
215223
self._test_deploy('.*', [
@@ -341,7 +349,7 @@ def test_alias_from_alias(self):
341349
self._deploy()
342350
commands.alias(self.cfg, 'latest', ['greatest'])
343351
check_call_silent(['git', 'checkout', 'gh-pages'])
344-
self._test_alias(expected_src='1.0')
352+
self._test_alias()
345353

346354
def test_alias_copy(self):
347355
self._deploy()
@@ -363,6 +371,32 @@ def test_alias_custom_redirect(self):
363371
with open('greatest/dir/index.html') as f:
364372
self.assertEqual(f.read(), '../../1.0/dir/')
365373

374+
def test_alias_overwrite_same(self):
375+
self._deploy()
376+
commands.alias(self.cfg, '1.0', ['latest'])
377+
check_call_silent(['git', 'checkout', 'gh-pages'])
378+
self._test_alias(expected_aliases=['latest'])
379+
380+
def test_alias_overwrite_include_same(self):
381+
self._deploy()
382+
commands.alias(self.cfg, '1.0', ['latest', 'greatest'])
383+
check_call_silent(['git', 'checkout', 'gh-pages'])
384+
self._test_alias(expected_aliases=['latest', 'greatest'])
385+
386+
def test_alias_overwrite_error(self):
387+
self._deploy()
388+
commands.deploy(self.cfg, '2.0')
389+
with self.assertRaises(ValueError):
390+
commands.alias(self.cfg, '2.0', ['latest'])
391+
check_call_silent(['git', 'checkout', 'gh-pages'])
392+
self._test_state(r'^Deployed \w+ to 2\.0', [
393+
versions.VersionInfo('2.0', '2.0'),
394+
versions.VersionInfo('1.0', '1.0', ['latest']),
395+
])
396+
397+
def test_alias_update(self):
398+
pass
399+
366400
def test_branch(self):
367401
self._deploy('branch')
368402
commands.alias(self.cfg, '1.0', ['greatest'], branch='branch')
@@ -381,7 +415,7 @@ def test_prefix(self):
381415
check_call_silent(['git', 'checkout', 'gh-pages'])
382416
self._test_alias(directory='prefix')
383417

384-
def test_alias_invalid(self):
418+
def test_alias_invalid_version(self):
385419
self._deploy()
386420
self.assertRaises(ValueError, commands.alias, self.cfg, '2.0',
387421
['alias'])

0 commit comments

Comments
 (0)