getPassword()
58 |
59 | /**
60 | * {@link CopySpec} content to add into repository.
61 | *
62 | * contents {
63 | * from 'src/pages'
64 | * from(javadoc) {
65 | * into 'api'
66 | * }
67 | * }
68 | *
69 | * @see gradle docs
70 | */
71 | CopySpec contents
72 | /**
73 | * What to keep (or remote) in existing branch.
74 | * E.g. to keep version 1.0.0 files except temp.txt file:
75 | *
76 | * preserve {
77 | * include '1.0.0/**'
78 | * exclude '1.0.0/temp.txt'
79 | * }
80 | *
81 | *
82 | * By default, only ".git" folder preserved.
83 | *
84 | * @see
85 | * gradle doc
86 | */
87 | PatternFilterable preserve
88 |
89 | GitPublishExtension(Project project) {
90 | this.contents = project.copySpec()
91 | this.preserve = new PatternSet()
92 | this.preserve.include('.git/**/*')
93 | }
94 |
95 | void contents(Action super CopySpec> action) {
96 | action.execute(contents)
97 | }
98 |
99 | void preserve(Action super PatternFilterable> action) {
100 | action.execute(preserve)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/MkdocsBuildPlugin.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs
2 |
3 | import groovy.transform.CompileStatic
4 | import groovy.transform.TypeCheckingMode
5 | import org.gradle.api.Plugin
6 | import org.gradle.api.Project
7 | import ru.vyarus.gradle.plugin.mkdocs.task.MkdocsBuildTask
8 | import ru.vyarus.gradle.plugin.mkdocs.task.MkdocsInitTask
9 | import ru.vyarus.gradle.plugin.mkdocs.task.MkdocsTask
10 | import ru.vyarus.gradle.plugin.python.PythonExtension
11 | import ru.vyarus.gradle.plugin.python.PythonPlugin
12 |
13 | /**
14 | * Base mkdocs plugin without publishing tasks (and so without grgit plugin registration). May be used in cases when
15 | * publication is not required or when there are problems with grgit plugin.
16 | *
17 | * Provides tasks:
18 | *
19 | * - mkdocsInit - create documentation site
20 | *
- mkdocsBuild - build site
21 | *
- mkdocsServe - start livereload server (for development)
22 | *
23 | *
24 | * mkdocksInit not use 'mkdocs new', instead more advanced template used with pre-initialized material theme.
25 | *
26 | * Plugin will also apply all required pip modules to use mkdocks with material theme and basic plugins
27 | * (see {@link ru.vyarus.gradle.plugin.mkdocs.MkdocsExtension#DEFAULT_MODULES}).
28 | *
29 | * @author Vyacheslav Rusakov
30 | * @since 28.10.2022
31 | */
32 | @CompileStatic
33 | class MkdocsBuildPlugin implements Plugin {
34 |
35 | private static final List STRICT = ['-s']
36 | private static final String DEV_ADDR = '--dev-addr'
37 |
38 | public static final String MKDOCS_BUILD_TASK = 'mkdocsBuild'
39 | public static final String DOCUMENTATION_GROUP = 'documentation'
40 |
41 | @Override
42 | void apply(Project project) {
43 | MkdocsExtension extension = project.extensions.create('mkdocs', MkdocsExtension, project)
44 |
45 | project.plugins.apply(PythonPlugin)
46 | applyDefaults(project)
47 | configureMkdocsTasks(project, extension)
48 | configureServe(project, extension)
49 | }
50 |
51 | private void applyDefaults(Project project) {
52 | // apply default mkdocs, material and minimal plugins
53 | // user will be able to override versions, if required
54 | project.extensions.getByType(PythonExtension).pip(MkdocsExtension.DEFAULT_MODULES)
55 | }
56 |
57 | @SuppressWarnings('AbcMetric')
58 | private void configureMkdocsTasks(Project project, MkdocsExtension extension) {
59 | project.tasks.register(MKDOCS_BUILD_TASK, MkdocsBuildTask) { task ->
60 | task.description = 'Build mkdocs documentation'
61 | task.group = DOCUMENTATION_GROUP
62 | task.extraArgs.convention project.provider { extension.strict ? STRICT : [] }
63 | task.outputDir.convention(project.file("${getBuildOutputDir(extension)}"))
64 | task.updateSiteUrl.convention(extension.updateSiteUrl)
65 | task.versionPath.convention(extension.resolveDocPath())
66 | task.versionName.convention(extension.resolveVersionTitle())
67 | task.rootRedirectPath
68 | .convention(extension.publish.rootRedirect ? extension.resolveRootRedirectionPath() : null)
69 | task.versionAliases.convention(extension.publish.versionAliases
70 | ? extension.publish.versionAliases as List : [])
71 | task.buildDir.convention(project.file(extension.buildDir))
72 | task.existingVersionFile.convention(extension.publish.existingVersionsFile)
73 | }
74 |
75 | project.tasks.register('mkdocsInit', MkdocsInitTask) { task ->
76 | task.description = 'Create mkdocs documentation'
77 | task.group = DOCUMENTATION_GROUP
78 | task.sourcesDir.convention(extension.sourcesDir)
79 | }
80 |
81 | project.tasks.withType(MkdocsTask).configureEach { task ->
82 | task.workDir.convention(extension.sourcesDir)
83 | task.sourcesDir.convention(extension.sourcesDir)
84 | task.extras.convention(project.provider {
85 | // resolving lazy gstrings ("${-> something}") because they can't be serialized
86 | Map res = [:]
87 | extension.extras.each {
88 | res.put(it.key, it.value?.toString())
89 | }
90 | res
91 | })
92 | }
93 |
94 | // simplify direct task usage
95 | project.extensions.extraProperties.set(MkdocsTask.simpleName, MkdocsTask)
96 | }
97 |
98 | @CompileStatic(TypeCheckingMode.SKIP)
99 | private void configureServe(Project project, MkdocsExtension extension) {
100 | project.tasks.register('mkdocsServe', MkdocsTask) { task ->
101 | task.description = 'Start mkdocs live reload server'
102 | task.group = DOCUMENTATION_GROUP
103 | task.command.set('serve')
104 | if (dockerUsed) {
105 | // mkdocs in strict mode does not allow external mappings, so avoid strict, event if configured
106 | // also ip must be changed, otherwise server would be invisible outside docker
107 | task.extraArgs DEV_ADDR, "0.0.0.0:${extension.devPort}"
108 | } else {
109 | List args = extension.strict ? new ArrayList<>(STRICT) : []
110 | args += [DEV_ADDR, "127.0.0.1:${extension.devPort}"]
111 | task.extraArgs.addAll(args)
112 | }
113 | // docker activation is still up to global configuration - here just required tuning
114 | // task would be started in exclusive container in order to stream output immediately
115 | task.docker.exclusive.set(true)
116 | task.docker.ports extension.devPort
117 | }
118 | }
119 |
120 | private String getBuildOutputDir(MkdocsExtension extension) {
121 | return extension.buildDir + (extension.multiVersion ? '/' + extension.resolveDocPath() : '')
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/service/GrgitService.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.service
2 |
3 | import groovy.transform.CompileStatic
4 | import org.ajoberstar.grgit.Grgit
5 | import org.gradle.api.services.BuildService
6 | import org.gradle.api.services.BuildServiceParameters
7 | import org.gradle.tooling.events.FinishEvent
8 | import org.gradle.tooling.events.OperationCompletionListener
9 |
10 | /**
11 | * Git service to manage single grgit instance between publish tasks.
12 | *
13 | * @author Vyacheslav Rusakov
14 | * @since 09.04.2024
15 | */
16 | @CompileStatic
17 | @SuppressWarnings(['AbstractClassWithoutAbstractMethod', 'ConfusingMethodName'])
18 | abstract class GrgitService implements BuildService,
19 | OperationCompletionListener, AutoCloseable {
20 |
21 | /**
22 | * Grgit instance. Initiated by gitReset task and used by other git tasks.
23 | */
24 | Grgit grgit
25 |
26 | @Override
27 | @SuppressWarnings('EmptyMethodInAbstractClass')
28 | void onFinish(FinishEvent finishEvent) {
29 | // not used, just to keep service alive
30 | }
31 |
32 | @Override
33 | void close() throws Exception {
34 | if (grgit != null) {
35 | grgit.close()
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/source/RepoUriValueSource.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.source
2 |
3 | import groovy.transform.CompileStatic
4 | import org.ajoberstar.grgit.Configurable
5 | import org.ajoberstar.grgit.Grgit
6 | import org.ajoberstar.grgit.operation.OpenOp
7 | import org.gradle.api.provider.Property
8 | import org.gradle.api.provider.ValueSource
9 | import org.gradle.api.provider.ValueSourceParameters
10 |
11 | /**
12 | * Git repository detection in project root. Custom value source is required for configuration cache support because
13 | * all external processes must be wrapped (even if this values could be easily cached).
14 | *
15 | * @author Vyacheslav Rusakov
16 | * @since 10.04.2024
17 | */
18 | @CompileStatic
19 | @SuppressWarnings('AbstractClassWithoutAbstractMethod')
20 | abstract class RepoUriValueSource implements ValueSource {
21 |
22 | @SuppressWarnings(['UnnecessaryCast', 'CatchException'])
23 | String obtain() {
24 | try {
25 | Grgit repo = Grgit.open({ OpenOp op -> op.dir = parameters.rootDir.get() } as Configurable)
26 | return repo.remote.list().find { it.name == 'origin' }?.url
27 | } catch (Exception ignored) {
28 | // repository not initialized case - do nothing (most likely user is just playing with the plugin)
29 | }
30 | return null
31 | }
32 |
33 | interface Params extends ValueSourceParameters {
34 | Property getRootDir()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/task/MkdocsBuildTask.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.task
2 |
3 | import groovy.transform.CompileStatic
4 | import groovy.transform.TypeCheckingMode
5 | import org.apache.tools.ant.taskdefs.condition.Os
6 | import org.gradle.api.GradleException
7 | import org.gradle.api.file.Directory
8 | import org.gradle.api.provider.ListProperty
9 | import org.gradle.api.provider.Property
10 | import org.gradle.api.provider.Provider
11 | import org.gradle.api.tasks.*
12 | import ru.vyarus.gradle.plugin.mkdocs.MkdocsExtension
13 | import ru.vyarus.gradle.plugin.mkdocs.util.MkdocsConfig
14 | import ru.vyarus.gradle.plugin.mkdocs.util.TemplateUtils
15 | import ru.vyarus.gradle.plugin.mkdocs.util.VersionsFileUtils
16 |
17 | /**
18 | * Builds mkdocs site. If version is configured as default for publication, then generate extra index.html.
19 | * If mkdocs.yml contains site_url value then current path will applied in config (original config reverted after
20 | * the build).
21 | *
22 | * @author Vyacheslav Rusakov
23 | * @since 14.11.2017
24 | */
25 | @CompileStatic
26 | @SuppressWarnings(['DuplicateStringLiteral', 'AbstractClassWithoutAbstractMethod',
27 | 'AbstractClassWithPublicConstructor'])
28 | abstract class MkdocsBuildTask extends MkdocsTask {
29 |
30 | private static final String SITE_URL = 'site_url'
31 |
32 | /**
33 | * Output directory.
34 | */
35 | @OutputDirectory
36 | abstract Property getOutputDir()
37 |
38 | /**
39 | * Update 'site_url' in mkdocs configuration to correct path. Required for multi-version publishing to
40 | * correctly update version url.
41 | */
42 | @Input
43 | abstract Property getUpdateSiteUrl()
44 |
45 | /**
46 | * Version directory path. "." if no version directories used.
47 | * @see {@link ru.vyarus.gradle.plugin.mkdocs.MkdocsExtension.Publish#docPath}
48 | */
49 | @Input
50 | abstract Property getVersionPath()
51 |
52 | /**
53 | * Version name (what to show in version dropdown).
54 | * @see {@link ru.vyarus.gradle.plugin.mkdocs.MkdocsExtension.Publish#versionTitle}
55 | */
56 | @Input
57 | @Optional
58 | abstract Property getVersionName()
59 |
60 | /**
61 | * Root redirection path or null to disable root redirection.
62 | */
63 | @Input
64 | @Optional
65 | abstract Property getRootRedirectPath()
66 |
67 | /**
68 | * Version aliases.
69 | */
70 | @Input
71 | @Optional
72 | abstract ListProperty getVersionAliases()
73 |
74 | /**
75 | * Mkdocs build directory.
76 | */
77 | @OutputDirectory
78 | abstract Property getBuildDir()
79 |
80 | /**
81 | * Versions file to update (when publish tasks not used).
82 | */
83 | @Input
84 | @Optional
85 | abstract Property getExistingVersionFile()
86 |
87 | @Internal
88 | Provider projectBuildDir = project.layout.buildDirectory
89 |
90 | MkdocsBuildTask() {
91 | command.set(project.provider {
92 | boolean isWindows = Os.isFamily(Os.FAMILY_WINDOWS)
93 | String path = outputDir.get().canonicalPath
94 | if (isWindows) {
95 | // always wrap into quotes for windows
96 | path = "\"$path\""
97 | }
98 | // use array to avoid escaping spaces in path (and consequent args parsing)
99 | return ['build', '-c', '-d', path]
100 | })
101 | }
102 |
103 | @Override
104 | void run() {
105 | String path = versionPath.get()
106 | boolean multiVersion = path != MkdocsExtension.SINGLE_VERSION_PATH
107 | Closure action = { super.run() }
108 |
109 | if (updateSiteUrl.get() && multiVersion) {
110 | // update mkdocs.yml site_url from global to published folder (in order to build correct sitemap)
111 | withModifiedConfig(path, action)
112 | } else {
113 | action.call()
114 | }
115 |
116 | // output directory must be owned by current user, not root, otherwise clean would fail
117 | dockerChown(outputDir.get().toPath())
118 |
119 | // optional remote versions file update
120 | updateVersions()
121 |
122 | if (multiVersion) {
123 | // add root index.html
124 | copyRedirect(path)
125 | copyAliases(path)
126 | }
127 | }
128 |
129 | @InputDirectory
130 | File getSourcesDirectory() {
131 | return fs.file(sourcesDir.get())
132 | }
133 |
134 | private void withModifiedConfig(String path, Closure action) {
135 | MkdocsConfig conf = new MkdocsConfig(fs, sourcesDir.get())
136 | String url = conf.find(SITE_URL)
137 |
138 | // site_url not defined or already points to correct location
139 | if (!url || url.endsWith(path) || url.endsWith("$path/")) {
140 | action.call()
141 | return
142 | }
143 |
144 | File backup = conf.backup()
145 | try {
146 | String slash = '/'
147 | String folderUrl = (url.endsWith(slash) ? url : (url + slash)) + path
148 | conf.set(SITE_URL, folderUrl)
149 | logger.lifecycle("Modified ${fs.relativePath(conf.config)}: '$SITE_URL: $folderUrl'")
150 | action.call()
151 | } finally {
152 | conf.restoreBackup(backup)
153 | logger.lifecycle("Original ${fs.relativePath(conf.config)} restored")
154 | }
155 | }
156 |
157 | private void updateVersions() {
158 | String versions = existingVersionFile.orNull
159 | if (versions) {
160 | File target
161 | if (versions.contains(':')) {
162 | target = new File(projectBuildDir.get().asFile, 'old-versions.json')
163 | download(versions, target)
164 | } else {
165 | target = fs.file(versions)
166 | }
167 | File res = VersionsFileUtils.getTarget(buildDir.get())
168 | logger.lifecycle('Creating versions file: {}', fs.relativePath(res))
169 | Map> parse = VersionsFileUtils.parse(target)
170 | if (target.exists()) {
171 | logger.lifecycle("\tExisting versions file '{}' loaded with {} versions", versions, parse.size())
172 | } else {
173 | logger.warn("\tWARNING: configured versions file '{}' does not exist - creating new file instead",
174 | versions)
175 | }
176 |
177 | String currentVersion = versionPath.get()
178 | if (VersionsFileUtils.addVersion(parse, currentVersion)) {
179 | logger.lifecycle('\tNew version added: {}', currentVersion)
180 | }
181 | VersionsFileUtils.updateVersion(parse,
182 | currentVersion, versionName.get(), versionAliases.get())
183 | VersionsFileUtils.write(parse, res)
184 | logger.lifecycle('\tVersions written to file: {}', parse.keySet().join(', '))
185 | }
186 | }
187 |
188 | @CompileStatic(TypeCheckingMode.SKIP)
189 | private void download(String src, File target) {
190 | // ignore all errors (create new file if failed to load)
191 | ant.get(src: src, dest: target, maxtime: 5, skipexisting: true, ignoreerrors: true)
192 | }
193 |
194 | private void copyRedirect(String path) {
195 | if (rootRedirectPath.orNull) {
196 | String target = rootRedirectPath.get()
197 | List possible = [path]
198 | possible.addAll(versionAliases.get())
199 | if (!possible.contains(target)) {
200 | throw new GradleException("Invalid mkdocs.publish.rootRedirectTo option value: '$target'. " +
201 | "Possible values are: ${possible.join(', ')} ('\$docPath' for actual version)")
202 | }
203 | // create root redirection file
204 | TemplateUtils.copy(fs, '/ru/vyarus/gradle/plugin/mkdocs/template/publish/',
205 | fs.relativePath(buildDir.get()), [docPath: target])
206 | logger.lifecycle('Root redirection enabled to: {}', target)
207 | } else {
208 | // remove stale index.html (to avoid unintentional redirect override)
209 | // of course, build always must be called after clean, but at least minimize damage on incorrect usage
210 | File index = new File(buildDir.get(), 'index.html')
211 | if (index.exists()) {
212 | index.delete()
213 | }
214 | }
215 | }
216 |
217 | @CompileStatic(TypeCheckingMode.SKIP)
218 | private void copyAliases(String version) {
219 | File baseDir = buildDir.get()
220 |
221 | List aliases = versionAliases.get()
222 | if (aliases) {
223 | aliases.each { String alias ->
224 | fs.copy { spec ->
225 | spec.from new File(baseDir, version)
226 | spec.into new File(baseDir, alias)
227 | }
228 | }
229 | logger.lifecycle('Version aliases added: {}', aliases.join(', '))
230 | }
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/task/MkdocsInitTask.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.task
2 |
3 | import groovy.transform.CompileStatic
4 | import org.gradle.api.DefaultTask
5 | import org.gradle.api.GradleException
6 | import org.gradle.api.internal.file.FileOperations
7 | import org.gradle.api.provider.Property
8 | import org.gradle.api.provider.Provider
9 | import org.gradle.api.tasks.Input
10 | import org.gradle.api.tasks.TaskAction
11 | import ru.vyarus.gradle.plugin.mkdocs.util.TemplateUtils
12 |
13 | import javax.inject.Inject
14 |
15 | /**
16 | * Generate initial mkdocs site. Does not use "mkdocs new" command. Custom template is used instead in order
17 | * to pre-configure target site with the material theme and enable some extensions.
18 | *
19 | * @author Vyacheslav Rusakov
20 | * @since 13.11.2017
21 | */
22 | @CompileStatic
23 | abstract class MkdocsInitTask extends DefaultTask {
24 |
25 | /**
26 | * Documentation sources folder (mkdocs sources root folder).
27 | */
28 | @Input
29 | abstract Property getSourcesDir()
30 |
31 | protected Provider projectName = project.provider { project.name }
32 | protected Provider projectDesc = project.provider { project.description }
33 |
34 | @TaskAction
35 | void run() {
36 | String sourcesPath = sourcesDir.get()
37 | File dir = fs.file(sourcesPath)
38 | if (dir.exists() && dir.listFiles().length > 0) {
39 | throw new GradleException("Can't init new mkdocs site because target directory is not empty: $dir")
40 | }
41 |
42 | TemplateUtils.copy(fs, '/ru/vyarus/gradle/plugin/mkdocs/template/init/', dir, [
43 | projectName: projectName.get(),
44 | projectDescription: projectDesc.orNull ?: '',
45 | docDir: sourcesPath,
46 | ])
47 | logger.lifecycle("Mkdocs site initialized: $sourcesPath")
48 | }
49 |
50 | @Inject
51 | protected abstract FileOperations getFs()
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/task/MkdocsTask.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.task
2 |
3 | import groovy.transform.CompileStatic
4 | import org.gradle.api.GradleException
5 | import org.gradle.api.internal.file.FileOperations
6 | import org.gradle.api.provider.MapProperty
7 | import org.gradle.api.provider.Property
8 | import org.gradle.api.tasks.Input
9 | import ru.vyarus.gradle.plugin.mkdocs.util.MkdocsConfig
10 | import ru.vyarus.gradle.plugin.python.task.PythonTask
11 |
12 | import javax.inject.Inject
13 |
14 | /**
15 | * General mkdocs task.
16 | *
17 | * Provides support for gradle-driven variables. When variables be declared in {@code mkdocs.extras} map,
18 | * task would generate special data file: {@code [mkdocs.yml location]/docs/_data/gradle.yml}.
19 | * Markdownextradata plugin must be activated in mkdocs.yml (exception will be thrown if not). Plugin searches
20 | * by default in @{code [mkdocs.yml location]/docs/_data/} dir for variable files (no magic behaviour).
21 | * In documentation variables may be used as
{{ gradle.var_name }}
.
22 | *
23 | * @author Vyacheslav Rusakov
24 | * @since 13.11.2017
25 | * @see markdownextradata plugin documentation
26 | */
27 | @CompileStatic
28 | @SuppressWarnings(['AbstractClassWithoutAbstractMethod', 'AbstractClassWithPublicConstructor'])
29 | abstract class MkdocsTask extends PythonTask {
30 |
31 | MkdocsTask() {
32 | // restrict commands to mkdocs module
33 | module.set('mkdocs')
34 | }
35 |
36 | /**
37 | * Documentation sources folder (mkdocs sources root folder).
38 | */
39 | @Input
40 | abstract Property getSourcesDir()
41 |
42 | /**
43 | * Extra gradle-provided variables to use in documentation.
44 | */
45 | @Input
46 | abstract MapProperty getExtras()
47 |
48 | @Override
49 | @SuppressWarnings('UnnecessaryGetter')
50 | void run() {
51 | if (extras.get().isEmpty()) {
52 | // no vars - simple run
53 | super.run()
54 | } else {
55 | runWithVariables()
56 | }
57 | }
58 |
59 | @Inject
60 | protected abstract FileOperations getFs()
61 |
62 | private void runWithVariables() {
63 | File data = resolveDataDir()
64 | boolean removeDataDir = !data.exists()
65 | File gen = new File(data, 'gradle.yml')
66 | try {
67 | if (removeDataDir) {
68 | data.mkdirs()
69 | }
70 | // assuming this file owned by gradle exclusively and may remain only because of incorrect task shutdown
71 | if (gen.exists()) {
72 | gen.delete()
73 | }
74 | logger.lifecycle('Generating mkdocs data file: {}', getFilePath(gen))
75 | String report = ''
76 | gen.withWriter { BufferedWriter writer ->
77 | extras.get().each { k, v ->
78 | // Object value used for deferred evaluation (GString may use lazy placeholders)
79 | String line = k.replaceAll('[ -]', '_') + ': ' + (v ?: '')
80 | writer.writeLine(line)
81 | report += "\t$line\n"
82 | }
83 | }
84 | logger.lifecycle(report)
85 | super.run()
86 | } finally {
87 | gen.delete()
88 | if (removeDataDir) {
89 | data.delete()
90 | }
91 | }
92 | }
93 |
94 | private File resolveDataDir() {
95 | MkdocsConfig config = new MkdocsConfig(fs, sourcesDir.get())
96 |
97 | if (!config.contains('plugins.markdownextradata')) {
98 | throw new GradleException(
99 | 'Gradle-defined extra properties require \'markdownextradata\' plugin active in ' +
100 | 'your mkdocs.yml file, which is currently not the case. \nEither remove extra properties ' +
101 | 'declaration (in build.gradle) or declare plugin (in mkdocs.yml) like this: \n' +
102 | 'plugins:\n' +
103 | ' - search\n' +
104 | ' - markdownextradata')
105 | }
106 |
107 | // mkdocs.yml location
108 | File root = fs.file(sourcesDir.get())
109 |
110 | // configuration may override default "docs" location
111 | String docsPath = config.find('docs_dir') ?: 'docs'
112 | File docs = new File(docsPath)
113 | // docs_dir config value may contain absolute path declaration
114 | if (!docs.absolute) {
115 | docs = new File(root, docs.path)
116 | }
117 |
118 | return new File(docs, '_data')
119 | }
120 |
121 | /**
122 | * Looks if file inside project and relative path would be reasonable, otherwise return absolute path.
123 | *
124 | * @param file file
125 | * @return relative or absolute file path
126 | */
127 | private String getFilePath(File file) {
128 | if (file.path.startsWith(gradleEnv.get().rootDir.path)) {
129 | return fs.relativePath(file)
130 | }
131 | return file.absolutePath
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/task/publish/GitPublishCommit.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.task.publish
2 |
3 | import groovy.transform.CompileStatic
4 | import org.ajoberstar.grgit.Configurable
5 | import org.ajoberstar.grgit.Grgit
6 | import org.ajoberstar.grgit.operation.AddOp
7 | import org.ajoberstar.grgit.operation.CommitOp
8 | import org.gradle.api.DefaultTask
9 | import org.gradle.api.Task
10 | import org.gradle.api.provider.Property
11 | import org.gradle.api.specs.Spec
12 | import org.gradle.api.tasks.*
13 | import ru.vyarus.gradle.plugin.mkdocs.service.GrgitService
14 |
15 | import java.util.stream.Collectors
16 | import java.util.stream.Stream
17 |
18 | /**
19 | * Git commit task. Based on https://github.com/ajoberstar/gradle-git-publish.
20 | *
21 | * @author Vyacheslav Rusakov
22 | * @since 08.04.2024
23 | */
24 | @CompileStatic
25 | @SuppressWarnings('AbstractClassWithPublicConstructor')
26 | abstract class GitPublishCommit extends DefaultTask {
27 |
28 | /**
29 | * Commit message.
30 | */
31 | @Input
32 | abstract Property getMessage()
33 |
34 | /**
35 | * Signing commits. Omit to use the default from your gitconfig.
36 | */
37 | @Input
38 | @Optional
39 | abstract Property getSign()
40 |
41 | // grgit instance initiated under reset task
42 | @Internal
43 | abstract Property getGrgit()
44 |
45 | GitPublishCommit() {
46 | // always consider this task out of date
47 | this.outputs.upToDateWhen({ t -> false } as Spec super Task>)
48 | }
49 |
50 | @OutputDirectory
51 | File getRepoDirectory() {
52 | return grgit.get().grgit.repository.rootDir
53 | }
54 |
55 | @TaskAction
56 | void commit() {
57 | Grgit git = grgit.get().grgit
58 | git.add({ AddOp op ->
59 | op.patterns = Stream.of('.').collect(Collectors.toSet())
60 | } as Configurable)
61 |
62 | // check if anything has changed
63 | if (git.status().clean) {
64 | didWork = false
65 | } else {
66 | git.commit({ CommitOp op ->
67 | op.message = message.get()
68 | if (sign.present) {
69 | op.sign = sign.get()
70 | }
71 | } as Configurable)
72 | didWork = true
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/task/publish/GitPublishPush.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.task.publish
2 |
3 | import groovy.transform.CompileStatic
4 | import org.ajoberstar.grgit.BranchStatus
5 | import org.ajoberstar.grgit.Configurable
6 | import org.ajoberstar.grgit.Grgit
7 | import org.ajoberstar.grgit.operation.BranchChangeOp
8 | import org.ajoberstar.grgit.operation.BranchStatusOp
9 | import org.ajoberstar.grgit.operation.PushOp
10 | import org.gradle.api.DefaultTask
11 | import org.gradle.api.Task
12 | import org.gradle.api.provider.Property
13 | import org.gradle.api.specs.Spec
14 | import org.gradle.api.tasks.Input
15 | import org.gradle.api.tasks.Internal
16 | import org.gradle.api.tasks.OutputDirectory
17 | import org.gradle.api.tasks.TaskAction
18 | import ru.vyarus.gradle.plugin.mkdocs.service.GrgitService
19 |
20 | /**
21 | * Git push task. Based on https://github.com/ajoberstar/gradle-git-publish.
22 | *
23 | * @author Vyacheslav Rusakov
24 | * @since 08.04.2024
25 | */
26 | @CompileStatic
27 | @SuppressWarnings('AbstractClassWithPublicConstructor')
28 | abstract class GitPublishPush extends DefaultTask {
29 |
30 | /**
31 | * Target branch (would be created if doesn't exists).
32 | */
33 | @Input
34 | abstract Property getBranch()
35 |
36 | // grgit instance initiated under reset task
37 | @Internal
38 | abstract Property getGrgit()
39 |
40 | GitPublishPush() {
41 | // always consider this task out of date
42 | this.outputs.upToDateWhen({ t -> false } as Spec super Task>)
43 | // NOTE: onlyIf removed due to conflicts with configuration cache so task would never be up to date
44 | }
45 |
46 | @OutputDirectory
47 | File getRepoDirectory() {
48 | return grgit.get().grgit.repository.rootDir
49 | }
50 |
51 | @TaskAction
52 | void push() {
53 | Grgit git = grgit.get().grgit
54 | // moved from onlyIf to avoid calling git under configuration stage
55 | if (!isPushRequired(git)) {
56 | return
57 | }
58 | String pubBranch = branch.get()
59 | git.push({ PushOp op ->
60 | op.refsOrSpecs = Arrays.asList(String.format('refs/heads/%s:refs/heads/%s', pubBranch, pubBranch))
61 | } as Configurable)
62 | // ensure tracking is set
63 | git.branch.change({ BranchChangeOp op ->
64 | op.name = pubBranch
65 | op.startPoint = 'origin/' + pubBranch
66 | op.mode = BranchChangeOp.Mode.TRACK
67 | } as Configurable)
68 | }
69 |
70 | private boolean isPushRequired(Grgit git) {
71 | try {
72 | BranchStatus status = git.branch.status({ BranchStatusOp op ->
73 | op.name = branch.get()
74 | } as Configurable)
75 | return status.aheadCount > 0
76 | } catch (IllegalStateException e) {
77 | // if we're not tracking anything yet (i.e. orphan) we need to push
78 | return true
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/util/MkdocsConfig.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.util
2 |
3 | import groovy.transform.CompileStatic
4 | import org.gradle.api.GradleException
5 | import org.gradle.api.internal.file.FileOperations
6 |
7 | import java.util.regex.Matcher
8 |
9 | /**
10 | * Utility to work with mkdocs config (mkdocs.yml).
11 | *
12 | * @author Vyacheslav Rusakov
13 | * @since 07.12.2017
14 | */
15 | @CompileStatic
16 | class MkdocsConfig {
17 |
18 | private final FileOperations fs
19 | private final String configLocation
20 |
21 | MkdocsConfig(FileOperations fs, String sourceDir) {
22 | this.fs = fs
23 | this.configLocation = (sourceDir ? "$sourceDir/" : '') + 'mkdocs.yml'
24 | }
25 |
26 | /**
27 | * @return mkdocs config file
28 | * @throws GradleException if config does not exist
29 | */
30 | File getConfig() {
31 | File config = fs.file(configLocation)
32 | if (!config.exists()) {
33 | throw new GradleException("Mkdocs config file not found: ${fs.relativePath(config)}")
34 | }
35 | return config
36 | }
37 |
38 | /**
39 | * @param option config option to find
40 | * @return option value or null if option commented or not defined
41 | */
42 | String find(String option) {
43 | String line = config.readLines().find {
44 | it.startsWith("$option:")
45 | }
46 | if (line) {
47 | int pos = line.indexOf(':')
48 | // special case: no value defined on property
49 | String res = pos < line.length() - 1 ? line[pos + 1..-1].trim() : ''
50 | // remove quotes
51 | return res.replaceAll(/^['"]/, '').replaceAll(/['"]$/, '')
52 | }
53 | return null
54 | }
55 |
56 | /**
57 | * Searches for specified property. Supports nesting: if property contains dots then it will search for
58 | * each property part sequentially (note that multiline list value also counted as property).
59 | *
60 | * Looks only not commented lines. Counts hierarchy.
61 | *
62 | * @param option option (maybe composite: separated with dots) name to find
63 | * @return true is string found, false otherwise
64 | */
65 | boolean contains(String option) {
66 | String[] parts = option.split('\\.')
67 | int i = 0
68 | int whitespace = 0
69 | String line = config.readLines().find {
70 | // line must not be commented, contain enough whitespace and required option part
71 | // allowed: [ prop, prop:, - prop, -prop ]
72 | if (!it.trim().startsWith('#') && it.find(
73 | /${whitespace == 0 ? '^' : '\\s{' + whitespace + ',}'}(-\s{0,})?${parts[i]}(:|$|\s)/)) {
74 | if (whitespace == 0) {
75 | whitespace++
76 | } else {
77 | // count starting whitespace (to correctly recognize structure)
78 | Matcher matcher = it =~ /^(\s+)/
79 | if (!matcher.find()) {
80 | throw new IllegalStateException("Failed to recognize preceeding whitespace in '$it'")
81 | }
82 | whitespace = matcher.group(1).length() + 1
83 | }
84 | i++
85 | }
86 | return i == parts.length
87 | }
88 | return line != null
89 | }
90 |
91 | /**
92 | * Replace option value in mkdocks config.
93 | *
94 | * @param option option name
95 | * @param value new option value
96 | */
97 | void set(String option, String value) {
98 | config.text = config.text.replaceAll(/(?m)^$option:.*/, "$option: $value")
99 | }
100 |
101 | /**
102 | * Backup configuration file.
103 | *
104 | * @return configuration backup file
105 | */
106 | File backup() {
107 | File backup = new File(config.parentFile, 'mkdocs.yml.bak')
108 | if (backup.exists()) {
109 | backup.delete()
110 | }
111 | backup << config.text
112 | return backup
113 | }
114 |
115 | /**
116 | * Replace current configuration file with provided backup.
117 | *
118 | * @param backup configuration backup
119 | * @throws IllegalStateException if backup file or config does not exists
120 | */
121 | void restoreBackup(File backup) {
122 | if (!backup.exists()) {
123 | throw new IllegalStateException("No backup file found: ${fs.relativePath(backup)}")
124 | }
125 | File cfg = config
126 | cfg.delete()
127 | if (!backup.renameTo(cfg)) {
128 | throw new IllegalStateException("Failed to rename ${fs.relativePath(backup.absolutePath)} back " +
129 | "to ${fs.relativePath(cfg.absolutePath)}. Please rename manually to recover.")
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/util/TemplateUtils.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.util
2 |
3 | import groovy.transform.CompileStatic
4 | import groovy.transform.TypeCheckingMode
5 | import org.apache.tools.ant.filters.ReplaceTokens
6 | import org.gradle.api.file.FileCopyDetails
7 | import org.gradle.api.file.FileTree
8 | import org.gradle.api.internal.file.FileOperations
9 |
10 | /**
11 | * Utility to extract templates from classpath (plugin jar) and copy to target location with placeholders processing.
12 | *
13 | * @author Vyacheslav Rusakov
14 | * @since 05.12.2017
15 | */
16 | @CompileStatic
17 | class TemplateUtils {
18 |
19 | private static final String SLASH = '/'
20 |
21 | /**
22 | * Copies templates from classpath (inside the jar) with placeholders substitution.
23 | *
24 | * @param fs file operations
25 | * @param path path to folder on classpath to copy (path must start with '/')
26 | * @param to target location (string, File)
27 | * @param tokens substitution tokens
28 | */
29 | @CompileStatic(TypeCheckingMode.SKIP)
30 | static void copy(FileOperations fs, String path, Object to, Map tokens) {
31 | URL folder = getUrl(path)
32 | FileTree tree
33 | boolean isJar = folder.toString().startsWith('jar:')
34 | if (isJar) {
35 | tree = fs.zipTree(getJarUrl(folder))
36 | } else {
37 | tree = fs.fileTree(folder)
38 | }
39 |
40 | fs.copy { spec ->
41 | spec.from tree
42 | spec.into to
43 | if (isJar) {
44 | spec.include pathToWildcard(path)
45 | // cut off path
46 | spec.eachFile { FileCopyDetails f ->
47 | f.path = f.path.replaceFirst(pathToCutPrefix(path), '')
48 | }
49 | spec.includeEmptyDirs = false
50 | }
51 | spec.filter(ReplaceTokens, tokens: tokens)
52 | }
53 | }
54 |
55 | private static URL getUrl(String path) {
56 | if (!path.startsWith(SLASH)) {
57 | throw new IllegalArgumentException("Path must be absolute (start with '/'): $path")
58 | }
59 | URL folder = TemplateUtils.getResource(path)
60 | if (folder == null) {
61 | // workaround for tests
62 | folder = Thread.currentThread().contextClassLoader.getResource(path.replaceAll('^/', ''))
63 | }
64 | if (folder == null) {
65 | throw new IllegalArgumentException("No resources found on path: $path")
66 | }
67 | return folder
68 | }
69 |
70 | private static URL getJarUrl(URL fileUrl) {
71 | return ((JarURLConnection) fileUrl.openConnection()).jarFileURL
72 | }
73 |
74 | private static String pathToWildcard(String path) {
75 | return (path.endsWith(SLASH) ? path : path + SLASH) + '**'
76 | }
77 |
78 | private static String pathToCutPrefix(String path) {
79 | String res = path[1..-1]
80 | return res.endsWith(SLASH) ? res : res + SLASH
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/groovy/ru/vyarus/gradle/plugin/mkdocs/util/VersionsFileUtils.groovy:
--------------------------------------------------------------------------------
1 | package ru.vyarus.gradle.plugin.mkdocs.util
2 |
3 | import groovy.json.JsonOutput
4 | import groovy.json.JsonSlurper
5 | import groovy.transform.CompileStatic
6 |
7 | /**
8 | * Versions file utilities.
9 | *
10 | * @author Vyacheslav Rusakov
11 | * @since 30.10.2022
12 | */
13 | @CompileStatic
14 | class VersionsFileUtils {
15 |
16 | static final String VERSIONS_FILE = 'versions.json'
17 | static final String ALIASES = 'aliases'
18 |
19 | private static final Comparator VERSIONS_COMPARATOR = VersionsComparator.comparingVersions(false)
20 |
21 | /**
22 | * @param file versions file to parse
23 | * @return parsed versions or empty map
24 | */
25 | static Map> parse(File file) {
26 | // self-sorted
27 | Map> res =
28 | new TreeMap>(VERSIONS_COMPARATOR.reversed())
29 |
30 | if (file.exists()) {
31 | ((List