|
1 |
| -# TODO(ts): Rewrite data source and use one single instance to combine all |
2 |
| -# different versions for a given path. |
3 |
| -class RepoDocsDataSource < ::Nanoc::DataSources::Filesystem |
| 1 | +require 'uri' |
| 2 | + |
| 3 | +# The RepoDocs data source provides items sourced from other Git repositories. |
| 4 | +# For a given repository_url, all git version tags are fetched and for the most |
| 5 | +# recent (in order to save compilation time) tags the `docs/` folder in the |
| 6 | +# respective `release-<version>` is checked out and its content mounted under |
| 7 | +# the given `items_root`. |
| 8 | +# |
| 9 | +# As the Prometheus git repository includes several hundreds of megabytes of |
| 10 | +# vendored assets, the repository is cloned bare and all blobs are filtered by |
| 11 | +# default. Each version is then checked out in an individual working tree and |
| 12 | +# git's spare-checkout feature is used to reduce the checkout to the `docs/` |
| 13 | +# folder. The git data is cached in `tmp/repo_docs/`. |
| 14 | +class RepoDocsDataSource < ::Nanoc::DataSource |
4 | 15 | identifier :repo_docs
|
5 | 16 |
|
6 |
| - PATH = "repositories" |
| 17 | + DOCS_DIRECTORY = 'docs'.freeze |
| 18 | + BRANCH_PATTERN = 'release-*'.freeze |
| 19 | + VERSION_REGEXP = /\Av\d+\.\d+\.\d+(?:-[a-z0-9.]+)?\z/.freeze |
| 20 | + TMPDIR = 'tmp/repo_docs/'.freeze |
7 | 21 |
|
8 | 22 | def up
|
9 |
| - c = config[:config] |
10 |
| - |
11 |
| - %x( |
12 |
| - scripts/checkout.sh \ |
13 |
| - -d "#{docs_root}" \ |
14 |
| - -t "#{repo_path}" \ |
15 |
| - "#{c[:repository]}" "#{c[:refspec]}" |
16 |
| - ) |
17 |
| - if $?.exitstatus != 0 |
18 |
| - raise "Couldn't checkout repository #{c.inspect}" |
19 |
| - end |
20 |
| - |
21 |
| - super |
| 23 | + validate |
| 24 | + sync_repository |
22 | 25 | end
|
23 | 26 |
|
24 | 27 | def items
|
25 |
| - c = config.fetch(:config) |
26 |
| - super.map do |item| |
27 |
| - attrs = item.attributes.dup |
28 |
| - attrs[:repo_docs] = c |
29 |
| - attrs[:repo_docs][:items_root] = config.fetch(:items_root) |
30 |
| - # TODO(ts): Remove assumptions about the path layout, rewrite datasource. |
31 |
| - attrs[:repo_docs][:version_root] = config.fetch(:items_root).sub(%r{(.+/)[^/]+/\Z}, '\\1') |
32 |
| - # TODO(ts): Document that repo doc index.md will be ignored. |
33 |
| - if item.identifier == '/' |
34 |
| - attrs[:nav] = { strip: true } |
| 28 | + items_root = config.fetch(:items_root, '/') |
| 29 | + latest = latest_version |
| 30 | + |
| 31 | + versions.inject([]) do |list, version| |
| 32 | + branch = "release-#{version}" |
| 33 | + dir = git_checkout(branch, DOCS_DIRECTORY) |
| 34 | + fs_config = { content_dir: dir, encoding: 'utf-8', identifier_type: 'legacy' } |
| 35 | + fs = ::Nanoc::DataSources::Filesystem.new(@site_config, '/', '/', fs_config) |
| 36 | + |
| 37 | + fs.items.each do |item| |
| 38 | + attrs = item.attributes.dup |
| 39 | + attrs[:nav] = { strip: true } if item.identifier == '/' |
| 40 | + attrs[:repo_docs] = { |
| 41 | + name: version, |
| 42 | + refspec: branch, |
| 43 | + version: version, |
| 44 | + latest: latest, |
| 45 | + items_root: items_root, |
| 46 | + version_root: File.join(items_root, version, '/'), |
| 47 | + canonical_root: File.join(items_root, 'latest', '/'), |
| 48 | + repository_url: git_remote, |
| 49 | + entrypoint: config[:config][:entrypoint], |
| 50 | + } |
| 51 | + |
| 52 | + if version == latest |
| 53 | + lattrs = attrs.dup |
| 54 | + lattrs[:repo_docs] = attrs[:repo_docs].dup |
| 55 | + lattrs[:repo_docs][:name] = "latest (#{version})" |
| 56 | + lattrs[:repo_docs][:version_root] = lattrs[:repo_docs][:canonical_root] |
| 57 | + list << new_item(item.content, lattrs, item.identifier.prefix('/latest')) |
| 58 | + end |
| 59 | + |
| 60 | + list << new_item(item.content, attrs, item.identifier.prefix('/' + version)) |
35 | 61 | end
|
36 |
| - new_item(item.content, attrs, item.identifier) |
| 62 | + |
| 63 | + list |
37 | 64 | end
|
38 | 65 | end
|
39 | 66 |
|
40 |
| - def content_dir_name |
41 |
| - File.join(repo_path, docs_root) |
| 67 | + private |
| 68 | + |
| 69 | + def validate |
| 70 | + if !config[:config].has_key?(:entrypoint) |
| 71 | + fail ArgumentError, 'entrypoint config option must be set' |
| 72 | + end |
| 73 | + if !config[:config].has_key?(:repository_url) |
| 74 | + fail ArgumentError, 'repository config option must be set' |
| 75 | + end |
| 76 | + URI(config[:config][:repository_url]) # raises an exception if invalid |
42 | 77 | end
|
43 | 78 |
|
44 |
| - def layouts_dir_name |
45 |
| - 'unsupported' |
| 79 | + def git_remote |
| 80 | + config[:config][:repository_url] |
46 | 81 | end
|
47 | 82 |
|
48 |
| - private |
| 83 | + def git_dir |
| 84 | + basename = File.basename(git_remote) |
| 85 | + basename += '.git' unless basename.end_with?('.git') |
| 86 | + File.join(TMPDIR, basename) |
| 87 | + end |
| 88 | + |
| 89 | + def git_branches |
| 90 | + output = `cd #{git_dir} && git branch --format='%(refname:short)' --list '#{BRANCH_PATTERN}'` |
| 91 | + fail "Could not list git branches" if $?.exitstatus != 0 |
| 92 | + output.split("\n") |
| 93 | + end |
| 94 | + |
| 95 | + def git_tags |
| 96 | + output = `cd #{git_dir} && git tag` |
| 97 | + fail "Could not list git tags" if $?.exitstatus != 0 |
| 98 | + output.split("\n") |
| 99 | + end |
| 100 | + |
| 101 | + # git_checkout checks out the directory in the specified branch using git's |
| 102 | + # sparse checkout and returns the path to the location in the workingtree. |
| 103 | + def git_checkout(branch, directory) |
| 104 | + worktree = File.absolute_path(File.join(git_dir.delete_suffix('.git'), branch)) |
| 105 | + if !File.exist?(File.join(worktree, '.git')) |
| 106 | + run_command("cd #{git_dir} && git worktree add --no-checkout #{worktree} #{branch}") |
| 107 | + end |
| 108 | + |
| 109 | + worktree_info = File.join(git_dir, 'worktrees', branch, 'info') |
| 110 | + Dir.mkdir(worktree_info) if !Dir.exist?(worktree_info) |
| 111 | + File.write(File.join(worktree_info, 'sparse-checkout'), "/#{directory}\n") |
| 112 | + |
| 113 | + run_command("cd #{worktree} && git reset --hard --quiet && git clean --force") |
| 114 | + File.join(worktree, directory) |
| 115 | + end |
| 116 | + |
| 117 | + # sync_repository clones or updates a bare git repository and enables the |
| 118 | + # sparse checkout feature. |
| 119 | + def sync_repository |
| 120 | + if !Dir.exist?(git_dir) |
| 121 | + run_command("git clone --bare --filter=blob:none #{git_remote} #{git_dir}") |
| 122 | + run_command("cd #{git_dir} && git config core.sparseCheckout true") |
| 123 | + else |
| 124 | + run_command("cd #{git_dir} && git fetch --quiet") |
| 125 | + end |
| 126 | + end |
| 127 | + |
| 128 | + # versions returns an ordered list of major.minor version names for which |
| 129 | + # documentation should be published. Only the most recent versions for which a |
| 130 | + # corresponding release-* branch exists are returned. |
| 131 | + def versions |
| 132 | + branches = git_branches |
| 133 | + all = git_tags |
| 134 | + .select { |v| v.match(VERSION_REGEXP) } |
| 135 | + .map { |v| v.delete_prefix('v').split('.')[0, 2].join('.') } |
| 136 | + .uniq |
| 137 | + .select { |v| branches.include?('release-' + v) } |
| 138 | + .sort_by { |v| v.split('.').map(&:to_i) } |
| 139 | + .reverse |
| 140 | + |
| 141 | + # Number of versions is reduced to speed up site compilation time. |
| 142 | + grouped = all.group_by { |v| v.split('.').first } |
| 143 | + grouped.inject([]) do |list, (major, versions)| |
| 144 | + size = major == grouped.keys.first ? 10 : 1 |
| 145 | + list += versions[0, size] |
| 146 | + end |
| 147 | + end |
49 | 148 |
|
50 |
| - def docs_root |
51 |
| - c = config.fetch(:config) |
52 |
| - c.fetch(:root, 'docs/') |
| 149 | + # latest_version returns the latest released version. |
| 150 | + def latest_version |
| 151 | + tags = git_tags |
| 152 | + versions.find { |v| tags.any? { |t| t.start_with?('v' + v) && !t.include?('-') } } |
53 | 153 | end
|
54 | 154 |
|
55 |
| - def repo_path |
56 |
| - c = config.fetch(:config) |
57 |
| - base = c.fetch(:repo_base, 'repositories') |
58 |
| - File.join(base, File.basename(c[:repository]), c[:name]) |
| 155 | + def run_command(cmd) |
| 156 | + fail "Running command '#{cmd}' failed" if !system(cmd) |
59 | 157 | end
|
60 | 158 | end
|
0 commit comments