├── .editorconfig ├── .github └── workflows │ ├── build_release.yml │ ├── docs │ └── HowTo_MANAGE_RELEASES.md │ ├── dokuwiki.yml │ └── resources │ └── RELEASE_HEAD.md ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── _test └── GeneralTest.php ├── action └── editcommit.php ├── classes ├── Git.php ├── GitBackedUtil.php └── GitRepo.php ├── conf ├── default.php └── metadata.php ├── lang ├── de │ ├── lang.php │ ├── mail_command_error.txt │ ├── mail_command_success.txt │ ├── mail_create_new_error.txt │ ├── mail_repo_path_error.txt │ └── settings.php └── en │ ├── lang.php │ ├── mail_command_error.txt │ ├── mail_command_success.txt │ ├── mail_create_new_error.txt │ ├── mail_repo_path_error.txt │ └── settings.php ├── loader.php └── plugin.info.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.{json,jsonc,yml,yaml}] 14 | indent_size = 2 15 | 16 | [{vendor,inc/phpseclib}/**] 17 | ; Use editor default (possible autodetection). 18 | indent_style = 19 | indent_size = 20 | end_of_line = 21 | trim_trailing_whitespace = 22 | insert_final_newline = 23 | -------------------------------------------------------------------------------- /.github/workflows/build_release.yml: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------------------ 2 | # This is a workflow to release this project as a zipped installable artifact. 3 | # Release version numbering and release notes generation is following standards defined by: 4 | # 5 | # https://semver.org 6 | # https://keepachangelog.com 7 | # https://common-changelog.org 8 | # 9 | # Note: Since DokuWiki is using version numbering in format YYYY-MM-DD we use this numbering 10 | # format rather than a dotted numbering scheme. 11 | # The git tag names have to use a 'v' as prefix to the DokuWiki version number. 12 | # 13 | # ------------------------------------------------------------------------------------------ 14 | name: Build a release 15 | 16 | on: 17 | # Triggers the workflow on push of a tag filtering the tag to meet 18 | # semantic version numbering according to https://semver.org 19 | # Here we use the DokuWiki conform version number pattern. 20 | push: 21 | tags: 22 | ['v[0-9]+-[0-9]+-[0-9]+'] 23 | 24 | # Allows you to run this workflow manually from the Actions tab 25 | workflow_dispatch: 26 | 27 | jobs: 28 | # Ensure that we run on tag references only 29 | validate_github_reference: 30 | name: Validate the tag reference 31 | # The type of runner that the job will run on 32 | runs-on: ubuntu-latest 33 | # Validate tag 34 | if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 35 | steps: 36 | - run: | 37 | echo "The selected git ref=${{ github.ref }} is NOT a valid release tag. Please select a valid release TAG as reference." 38 | exit 1 39 | 40 | # Create a release 41 | release: 42 | name: Release 43 | # The type of runner that the job will run on 44 | runs-on: ubuntu-latest 45 | # Set job wide environment 46 | env: 47 | APP_NAME: dokuwiki-plugin-gitbacked 48 | APP_INFO_FILE: plugin.info.txt 49 | APP_INFO_FILE_VERSION_KEY: date 50 | BUILD_DIR: build 51 | ZIP_EXCLUSIONS: '*.git* .editorconfig /*.github/* /*.vscode/* /*build/* /*_test/* RELEASE_HEAD.md' 52 | 53 | steps: 54 | # Log use case if triggered manually 55 | - name: Log use case if triggered manually 56 | if: ${{ github.event_name == 'workflow_dispatch' }} 57 | run: | 58 | echo "Workflow has been triggered manually" 59 | 60 | # Log use case if triggered by push 61 | - name: Log use case if triggered by push 62 | if: ${{ github.event_name == 'push' }} 63 | run: | 64 | echo "Workflow has been triggered by push to ${{ github.ref }}" 65 | 66 | # Check out this repo 67 | - name: Checkout 68 | uses: actions/checkout@v3 69 | 70 | # Set version tags as global environment properties 71 | - name: Prepare Version Tags 72 | run: | 73 | #echo "MAJOR_VERSION=$(echo ${GITHUB_REF/refs\/tags\//} | awk -F'-' '{print $1}')" >> $GITHUB_ENV 74 | #echo "MINOR_VERSION=$(echo ${GITHUB_REF/refs\/tags\//} | awk -F'-' '{print $1"-"$2}')" >> $GITHUB_ENV 75 | #echo "FULL_VERSION=$(echo ${GITHUB_REF/refs\/tags\//} | awk -F'-' '{print $1"-"$2"-"$3}')" >> $GITHUB_ENV 76 | echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV 77 | echo "RELEASE_VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV 78 | echo "APP_INFO_VERSION=$(sed -n -E 's/^${{ env.APP_INFO_FILE_VERSION_KEY }}[ \t]+([0-9-]+).*/\1/p' ${{ env.APP_INFO_FILE }})" >> $GITHUB_ENV 79 | 80 | # Validate app info version and set release name 81 | - name: Validate app info version and set release name 82 | run: | 83 | if [ "${{ env.RELEASE_VERSION }}" != "${{ env.APP_INFO_VERSION }}" ]; then 84 | echo "Mismatch of release version=${{ env.RELEASE_VERSION }} and application info version=${{ env.APP_INFO_VERSION }}!" >&2 85 | echo "Please review the value for key=${{ env.APP_INFO_FILE_VERSION_KEY }} in file ${{ env.APP_INFO_FILE }}." 86 | exit 1 87 | fi 88 | echo "RELEASE_NAME=Release ${{ env.APP_INFO_VERSION }}" >> $GITHUB_ENV 89 | 90 | - name: Validate CHANGELOG.md for this release version 91 | # explanation of sed command: 92 | # 1. select lines between SED_VERSION_BEGIN_PATTERN and SED_VERSION_END_PATTERN 93 | # 2. invert this selection 94 | # 3. delete it 95 | # => only selection is remaining in stream 96 | run: | 97 | SED_VERSION_BEGIN_PATTERN="/^## \\[${{ env.RELEASE_VERSION }}\\]/" 98 | SED_VERSION_END_PATTERN="/^## /" 99 | echo "Pattern used for sed: ${SED_VERSION_BEGIN_PATTERN},${SED_VERSION_END_PATTERN} ! d" 100 | # 101 | # Extract the release notes for this RELEASE_VERSION including the line of the previous version: 102 | # 103 | RELEASE_NOTES_WITH_PREV_VERSION=$(sed -e "${SED_VERSION_BEGIN_PATTERN},${SED_VERSION_END_PATTERN} ! d" CHANGELOG.md) 104 | echo ">>>>>> RELEASE_NOTES_WITH_PREV_VERSION - BEGIN >>>>>>" 105 | echo "${RELEASE_NOTES_WITH_PREV_VERSION}" 106 | echo "<<<<<< RELEASE_NOTES_WITH_PREV_VERSION - END <<<<<<" 107 | # 108 | # Format the release notes: 109 | # 110 | # 1. Remove last 2 lines: head -n 2 111 | # 2. Remove any empty line from the end: sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' 112 | # (s. http://sed.sourceforge.net/sed1line.txt for reference) 113 | # 114 | #RELEASE_VERSION_NOTES=$(echo "$RELEASE_NOTES_WITH_PREV_VERSION" | head -n -2 | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}') 115 | #echo "${RELEASE_VERSION_NOTES}" >> RELEASE.md 116 | #printf "\n" >> RELEASE.md 117 | # 118 | # Extract previous release version: 119 | # 120 | # 1. Cut the last line only: tail -1 121 | # 2. Get the version from the enclosing [] brackets: awk -F "[][]" '{ print $2 }' 122 | # 123 | PREV_RELEASE_VERSION=$(echo "$RELEASE_NOTES_WITH_PREV_VERSION" | tail -1 | awk -F "[][]" '{ print $2 }') 124 | if [ -z "$PREV_RELEASE_VERSION" ]; then 125 | EXPECTED_COMPARE_URL="${{ github.server_url }}/${{ github.repository }}/releases/tag/v${{ env.RELEASE_VERSION }}" 126 | else 127 | EXPECTED_COMPARE_URL="${{ github.server_url }}/${{ github.repository }}/compare/v${PREV_RELEASE_VERSION}..v${{ env.RELEASE_VERSION }}" 128 | fi 129 | # Validate CHANGELOG.md content 130 | IS_OK="true" 131 | if ! grep -q "^## \\[${{ env.RELEASE_VERSION }}\\]" CHANGELOG.md; then 132 | IS_OK="false" 133 | echo "ERROR: CHANGELOG.md does not contain an entry for this release version of format: ## [${{ env.RELEASE_VERSION }}]" 134 | fi 135 | if ! grep -q "^\\[${{ env.RELEASE_VERSION }}\\]: ${EXPECTED_COMPARE_URL}" CHANGELOG.md; then 136 | IS_OK="false" 137 | echo "ERROR: CHANGELOG.md does not contain a line with a compare link of format: [${{ env.RELEASE_VERSION }}]: ${EXPECTED_COMPARE_URL}" 138 | fi 139 | if [ "$IS_OK" != "true" ]; then 140 | echo "Please review CHANGELOG.md and update it for the content expected." 141 | exit 1 142 | fi 143 | 144 | # Prepare release notes and build directory 145 | - name: Prepare release notes and build directory 146 | run: | 147 | mkdir ${{ env.BUILD_DIR }} 148 | #cp ./README.md ${{ env.BUILD_DIR }}/README.md 149 | touch ${{ env.BUILD_DIR }}/README.md 150 | cp ./CHANGELOG.md ${{ env.BUILD_DIR }}/CHANGELOG.md 151 | cp ./.github/workflows/resources/RELEASE_HEAD.md ${{ env.BUILD_DIR }}/RELEASE_HEAD.md 152 | 153 | # Format the filename of this release 154 | - name: Format release filename 155 | id: format_release_filename 156 | run: | 157 | echo "FILE_NAME=${{ env.APP_NAME }}-${{ env.APP_INFO_VERSION }}.zip" >> $GITHUB_OUTPUT 158 | 159 | # Create archive file 160 | - name: Build release archive 161 | uses: GHCICD/zip-release@master 162 | with: 163 | type: 'zip' 164 | filename: ${{ env.BUILD_DIR }}/${{ steps.format_release_filename.outputs.FILE_NAME }} 165 | exclusions: ${{ env.ZIP_EXCLUSIONS }} 166 | 167 | # Create release notes by release-notes-from-changelog 168 | - name: Create release notes by GHCICD/release-notes-from-changelog@v1 169 | uses: GHCICD/release-notes-from-changelog@v1 170 | with: 171 | version: ${{ env.RELEASE_VERSION }} 172 | working-directory: ${{ env.BUILD_DIR }} 173 | 174 | - name: Log RELEASE.md 175 | run: | 176 | echo ">>>>> build/RELEASE.md:" 177 | cat ${{ env.BUILD_DIR }}/RELEASE.md 178 | echo "<<<<<" 179 | # echo ">>>>> build/CHANGELOG.md:" 180 | # cat ${{ env.BUILD_DIR }}/CHANGELOG.md 181 | # echo "<<<<<" 182 | 183 | # Create release with info from CHANGELOG.md 184 | - name: Create GitHub release by ncipollo/release-action@v1 185 | id: create_release 186 | uses: ncipollo/release-action@v1 187 | with: 188 | token: ${{ secrets.GITHUB_TOKEN }} 189 | name: ${{ env.RELEASE_NAME }} 190 | tag: ${{ env.VERSION_TAG }} 191 | bodyFile: ${{ env.BUILD_DIR }}/RELEASE.md 192 | artifacts: ${{ env.BUILD_DIR }}/${{ steps.format_release_filename.outputs.FILE_NAME }} 193 | artifactContentType: application/zip 194 | # 195 | # EOF 196 | # 197 | -------------------------------------------------------------------------------- /.github/workflows/docs/HowTo_MANAGE_RELEASES.md: -------------------------------------------------------------------------------- 1 | # Manage Releases 2 | 3 | ## Release version naming 4 | - This plugin is provided as released DokuWiki installable ZIP packages with detailed release notes 5 | via this repos [Release](https://github.com/woolfg/dokuwiki-plugin-gitbacked/releases) page. 6 | - The name of a release is identical to the `date` property in `plugin.info.txt` of that release. 7 | 8 | ## Pre-Requisites 9 | - The release notes have to be maintained manually in [CHANGELOG.md](https://github.com/woolfg/dokuwiki-plugin-gitbacked/blob/master/CHANGELOG.md) - any details can be found in the comment section within this file. 10 | 11 | ## Building a release 12 | - Releases are built by the `build_release.yml` GitHub Action workflow of this project. 13 | - A release build is triggered by applying a tag with name '**v**YYYY-MM-DD' to the corresponding most recent commit of this release. 14 | - The release workflow is not triggered, if: 15 | - The release tag is not of format `v[0-9]+-[0-9]+-[0-9]+` 16 | - The release workflow is failing and no release will be created, if: 17 | - The release version after the 'v'-prefix does not match the `date` property in file `plugin.info.txt` 18 | - The `CHANGELOG.md` does not contain a line of format '# [YYYY-MM-DD]' matching the release version 19 | - The `CHANGELOG.md` does not contain an appropriate compare link versus the previous release version at the end of the `CHANGELOG.md` file 20 | -------------------------------------------------------------------------------- /.github/workflows/dokuwiki.yml: -------------------------------------------------------------------------------- 1 | name: DokuWiki Default Tasks 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '22 21 12 * *' 7 | 8 | 9 | jobs: 10 | all: 11 | uses: dokuwiki/github-action/.github/workflows/all.yml@main 12 | -------------------------------------------------------------------------------- /.github/workflows/resources/RELEASE_HEAD.md: -------------------------------------------------------------------------------- 1 | # dokuwiki-plugin-gitbacked 2 | 3 | gitbacked Plugin for DokuWiki - Store/Sync pages and media files in a git repository 4 | 5 | Release notes for this version: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | manager.dat 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bmewburn.vscode-intelephense-client", 4 | "eamodio.gitlens", 5 | "editorconfig.editorconfig", 6 | "esbenp.prettier-vscode", 7 | "github.vscode-github-actions", 8 | "samuelhinchliffe.phpdoc-generator-2022" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "git.autofetch": true, 4 | "[php]": { 5 | "editor.detectIndentation": false, 6 | "editor.defaultFormatter": "bmewburn.vscode-intelephense-client", 7 | "editor.formatOnSave": true, 8 | "editor.insertSpaces": true, 9 | "editor.tabSize": 4, 10 | //"prettier.tabWidth": 4, 11 | //"prettier.useTabs": false 12 | "editor.wordWrap": "wordWrapColumn", 13 | "editor.wordWrapColumn": 120, 14 | "editor.rulers": [120] 15 | }, 16 | "[markdown]": { 17 | "editor.wordWrap": "on" 18 | }, 19 | "[json]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[jsonc]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[html]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[javascript]": { 29 | "editor.detectIndentation": false, 30 | "editor.defaultFormatter": "esbenp.prettier-vscode", 31 | //"editor.insertSpaces": true, 32 | //"editor.tabSize": 4, 33 | //"prettier.tabWidth": 4, 34 | //"prettier.useTabs": false 35 | }, 36 | "[typescript]": { 37 | "editor.defaultFormatter": "esbenp.prettier-vscode" 38 | //"prettier.tabWidth": 4, 39 | //"prettier.useTabs": true 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | 9 | 10 | 40 | 61 | 62 | ## [Unreleased] 63 | 64 | ### Changed 65 | - TBD 66 | 67 | 68 | ## [2025-03-03] 69 | 70 | ### Added 71 | - Add code style config files 72 | - Add code style check 73 | - Add auto loader `loader.php` 74 | 75 | ### Fixed 76 | - Replace references to deprecated classes by non deprecated classes 77 | - Update code to meet DokuWiki standard code style 78 | - Make use of plugin specific namespace for `classes/*.php` classes 79 | 80 | 81 | ## [2025-02-26] 82 | 83 | ### Added 84 | - Add config `'updateIndexOnPull'` - PR [#93], [#94] 85 | 86 | ### Changed 87 | - Avoid using $_ENV in `lib/Git.php#run_command` - PR [#91] 88 | - ensuring more controlled and secure handling of environment variables 89 | - fixes probable warning 'Array to string conversion' 90 | 91 | 92 | ## [2023-05-07] 93 | 94 | ### Fixed 95 | - Deprecation warnings raised on `action/editcommit.php` - fixes [#86] 96 | 97 | 98 | ## [2023-03-07] 99 | 100 | ### Changed 101 | - Allow absolute path in `'repoPath'` and/or `'repoWorkDir'` - implements [#80] 102 | - `'repoWorkDir'` is configured empty by default now 103 | - `--work-tree` option is ommited, if `'repoWorkDir'` is empty - addressing [#79] 104 | 105 | ### Fixed 106 | - Cyrillic commit messages not being corrupted anymore - fixes [#82] 107 | 108 | 109 | ## [2022-02-06] 110 | 111 | ### Changed 112 | - Created LICENSE file and removed corresponding text from the README.md - implements [#67] 113 | - Use DokuWiki's user name & email address as commit author - implements [#63], [#66] 114 | - Updated default setting for `$conf['addParams']` to apply DokuWiki user name as commit author and DokuWiki user eMail as eMail. 115 | - If DokuWiki user eMail is empty, then the eMail assigned to the commit will be empty as well. 116 | - Updated README.md: 117 | - Added a link to the referred COPYING license file originally hosted on the DokuWiki master branch to simplify a probable lookup. 118 | - Issues linked on startpage, motivate people to contribute 119 | 120 | ### Fixed 121 | - Allow empty commits - fixes [#39] 122 | 123 | 124 | ## [2022-01-20] 125 | 126 | ### Fixed 127 | - Fix for compatibility to PHP versions <7.4 - was introduced by previous release - fixes [#69] 128 | 129 | 130 | ## [2021-03-19] 131 | 132 | ### Added 133 | - Extended to send error messages to a configurable eMail address - implements [#53] 134 | - Added config `'emailAddressOnError'` 135 | - Added config `'notifyByMailOnSuccess'` 136 | - Added localizations for error messages 137 | - Added eMail templates for mail notifications 138 | - German translations added 139 | 140 | 141 | ## [2016-08-14] 142 | 143 | ### Changed 144 | - Updated last change date to current date - fix [#38] 145 | 146 | ### Fixed 147 | - Adjusted method signatures to match parent in action/editcommit.php 148 | - Corrected method signature for php7-compatibility in action/editcommit.php 149 | 150 | 151 | ## [2015-10-03] 152 | 153 | ### Added 154 | - Allow name and mail user variables in addParams. 155 | - Add an option for customizing git working tree 156 | - Added setting ignorePaths to ignore specified paths in add/commit-process 157 | 158 | ### Changed 159 | - Use Markdown for the GitHub README. 160 | - Update plugin date and URL, added Carsten Teibes as author 161 | - Pull latest git php library (0.1.4) 162 | - Allow to set the path to the git binary - implements [#8] 163 | - Use relative path for Git.php and `$conf['tempdir']` for temp file. 164 | - Coding compliance change: move handle_periodic_pull down, together with other "handle"s. 165 | 166 | ### Fixed 167 | - Fix passing additional arguments to git binary 168 | - Fix lang typos. 169 | - Coding compliance change, tabs to spaces, fix typos. 170 | - dokuwiki Farm fix 171 | 172 | 173 | ## [2012-10-31] 174 | 175 | ### Added 176 | - Initial release 177 | 178 | ### Comments 179 | - The release name complies with the date property of plugin.info.txt 180 | - The recent commit within this release is [2dbc1a5](https://github.com/woolfg/dokuwiki-plugin-gitbacked/commit/2dbc1a5564516b801dbda239b68152edb5be0303) of 13-Nov-2012 181 | 182 | 188 | 189 | [Unreleased]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2025-03-01..HEAD 190 | [2025-03-01]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2025-02-26..v2025-03-01 191 | [2025-02-26]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2023-05-07..v2025-02-26 192 | [2023-05-07]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2023-03-07..v2023-05-07 193 | [2023-03-07]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2022-02-06..v2023-03-07 194 | [2022-02-06]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2022-01-20..v2022-02-06 195 | [2022-01-20]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2021-03-19..v2022-01-20 196 | [2021-03-19]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2016-08-14..v2021-03-19 197 | [2016-08-14]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2015-10-03..v2016-08-14 198 | [2015-10-03]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/compare/v2012-10-31..v2015-10-03 199 | [2012-10-31]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/releases/tag/v2012-10-31 200 | [#94]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/pull/94 201 | [#93]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/pull/93 202 | [#91]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/pull/91 203 | [#86]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/86 204 | [#82]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/82 205 | [#80]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/80 206 | [#79]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/79 207 | [#69]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/69 208 | [#67]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/67 209 | [#66]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/66 210 | [#63]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/63 211 | [#53]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/53 212 | [#39]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/39 213 | [#38]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/38 214 | [#8]: https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues/8 215 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitbacked Plugin for DokuWiki 2 | [![GNU General Public License v2.0](https://img.shields.io/github/license/woolfg/dokuwiki-plugin-gitbacked?label=License)](./LICENSE) 3 | [![CI](https://github.com/woolfg/dokuwiki-plugin-gitbacked/actions/workflows/dokuwiki.yml/badge.svg)](https://github.com/woolfg/dokuwiki-plugin-gitbacked/actions/workflows/dokuwiki.yml) 4 | 5 | ## :green_heart: Contributions welcome :green_heart: 6 | 7 | You want to support Open Source, even if you are new to the game? 8 | Feel free to grab an issue: 9 | 10 | - [Smaller issues, also well suited for newcomers](https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues?q=is%3Aissue+is%3Aopen+label%3Acontributionwelcome) 11 | - [Feature requests and other cool ideas](https://github.com/woolfg/dokuwiki-plugin-gitbacked/issues?q=is%3Aissue+is%3Aopen+label%3A%22feature+request%22) 12 | 13 | If you have encountered a problem, you have a good idea, or just have a question, please, create a new issue. 14 | 15 | ## gitbacked Plugin for DokuWiki 16 | 17 | Store/Sync pages and media files in a git repository 18 | 19 | All documentation for this plugin can be found at 20 | http://www.dokuwiki.org/plugin:gitbacked 21 | 22 | This plugin is provided as released DokuWiki installable ZIP packages with detailed [release notes](https://github.com/woolfg/dokuwiki-plugin-gitbacked/blob/master/CHANGELOG.md) 23 | via this repos [Releases](https://github.com/woolfg/dokuwiki-plugin-gitbacked/releases). 24 | Detailed technical information on how releases are built can be found at [HowTo manage releases](https://github.com/woolfg/dokuwiki-plugin-gitbacked/blob/master/.github/workflows/docs/HowTo_MANAGE_RELEASES.md). 25 | 26 | If you install this plugin manually, make sure that: 27 | - you download a **released** `dokuwiki-plugin-gitbacked-YYYY-DD-MM.zip` file 28 | from the [Release](https://github.com/woolfg/dokuwiki-plugin-gitbacked/releases) page. 29 | - this file is to be unpacked in `/lib/plugins/gitbacked/` - if this folder 30 | is called differently it will not work! 31 | 32 | Please refer to http://www.dokuwiki.org/plugins for additional info 33 | on how to install plugins in DokuWiki. 34 | 35 | ## Changelog 36 | 37 | See [CHANGELOG.md](CHANGELOG.md) 38 | 39 | ## Maintainers 40 | 41 | - [@mhoffrog (Markus Hoffrogge)](https://github.com/mhoffrog) 42 | - [@woolfg (Wolfgang Gassler)](https://github.com/woolfg) 43 | 44 | ## License 45 | 46 | This plugin is licensed under GPLv2, see [LICENSE](LICENSE). 47 | 48 | See the [COPYING](https://github.com/splitbrain/dokuwiki/blob/master/COPYING) file in your DokuWiki folder for details 49 | -------------------------------------------------------------------------------- /_test/GeneralTest.php: -------------------------------------------------------------------------------- 1 | assertFileExists($file); 23 | 24 | $info = confToHash($file); 25 | 26 | $this->assertArrayHasKey('base', $info); 27 | $this->assertArrayHasKey('author', $info); 28 | $this->assertArrayHasKey('email', $info); 29 | $this->assertArrayHasKey('date', $info); 30 | $this->assertArrayHasKey('name', $info); 31 | $this->assertArrayHasKey('desc', $info); 32 | $this->assertArrayHasKey('url', $info); 33 | 34 | $this->assertEquals('gitbacked', $info['base']); 35 | $this->assertRegExp('/^https?:\/\//', $info['url']); 36 | $this->assertTrue(mail_isvalid($info['email'])); 37 | $this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']); 38 | $this->assertTrue(false !== strtotime($info['date'])); 39 | } 40 | 41 | /** 42 | * Test to ensure that every conf['...'] entry in conf/default.php has a corresponding meta['...'] entry in 43 | * conf/metadata.php. 44 | */ 45 | public function testPluginConf(): void 46 | { 47 | $conf_file = __DIR__ . '/../conf/default.php'; 48 | $meta_file = __DIR__ . '/../conf/metadata.php'; 49 | 50 | if (!file_exists($conf_file) && !file_exists($meta_file)) { 51 | self::markTestSkipped('No config files exist -> skipping test'); 52 | } 53 | 54 | if (file_exists($conf_file)) { 55 | include($conf_file); 56 | } 57 | if (file_exists($meta_file)) { 58 | include($meta_file); 59 | } 60 | 61 | $this->assertEquals( 62 | gettype($conf), 63 | gettype($meta), 64 | 'Both ' . DOKU_PLUGIN . 'gitbacked/conf/default.php and ' . DOKU_PLUGIN . 'gitbacked/conf/metadata.php have to exist and contain the same keys.' 65 | ); 66 | 67 | if ($conf !== null && $meta !== null) { 68 | foreach ($conf as $key => $value) { 69 | $this->assertArrayHasKey( 70 | $key, 71 | $meta, 72 | 'Key $meta[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'gitbacked/conf/metadata.php' 73 | ); 74 | } 75 | 76 | foreach ($meta as $key => $value) { 77 | $this->assertArrayHasKey( 78 | $key, 79 | $conf, 80 | 'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'gitbacked/conf/default.php' 81 | ); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /action/editcommit.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols 11 | // must be run within Dokuwiki 12 | if (!defined('DOKU_INC')) die(); 13 | 14 | if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 15 | if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 16 | if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 17 | 18 | require_once __DIR__ . '/../loader.php'; 19 | 20 | use dokuwiki\Extension\ActionPlugin; 21 | use dokuwiki\Extension\EventHandler; 22 | use dokuwiki\Extension\Event; 23 | use dokuwiki\Search\Indexer; 24 | 25 | use woolfg\dokuwiki\plugin\gitbacked\Git; 26 | use woolfg\dokuwiki\plugin\gitbacked\GitRepo; 27 | use woolfg\dokuwiki\plugin\gitbacked\GitBackedUtil; 28 | 29 | // phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace 30 | // phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps 31 | class action_plugin_gitbacked_editcommit extends ActionPlugin 32 | { 33 | /** 34 | * Temporary directory for this gitbacked plugin. 35 | * 36 | * @var string 37 | */ 38 | private $temp_dir; 39 | 40 | public function __construct() 41 | { 42 | $this->temp_dir = GitBackedUtil::getTempDir(); 43 | } 44 | 45 | public function register(EventHandler $controller) 46 | { 47 | $controller->register_hook('IO_WIKIPAGE_WRITE', 'AFTER', $this, 'handleIOWikiPageWrite'); 48 | $controller->register_hook('MEDIA_UPLOAD_FINISH', 'AFTER', $this, 'handleMediaUpload'); 49 | $controller->register_hook('MEDIA_DELETE_FILE', 'AFTER', $this, 'handleMediaDeletion'); 50 | $controller->register_hook('DOKUWIKI_DONE', 'AFTER', $this, 'handlePeriodicPull'); 51 | } 52 | 53 | private function initRepo() 54 | { 55 | //get path to the repo root (by default DokuWiki's savedir) 56 | $repoPath = GitBackedUtil::getEffectivePath($this->getConf('repoPath')); 57 | $gitPath = trim($this->getConf('gitPath')); 58 | if ($gitPath !== '') { 59 | Git::setBin($gitPath); 60 | } 61 | //init the repo and create a new one if it is not present 62 | io_mkdir_p($repoPath); 63 | $repo = new GitRepo($repoPath, $this, true, true); 64 | //set git working directory (by default DokuWiki's savedir) 65 | $repoWorkDir = $this->getConf('repoWorkDir'); 66 | if (!empty($repoWorkDir)) { 67 | $repoWorkDir = GitBackedUtil::getEffectivePath($repoWorkDir); 68 | } 69 | Git::setBin(empty($repoWorkDir) ? Git::getBin() 70 | : Git::getBin() . ' --work-tree ' . escapeshellarg($repoWorkDir)); 71 | $params = str_replace( 72 | ['%mail%', '%user%'], 73 | [$this->getAuthorMail(), $this->getAuthor()], 74 | $this->getConf('addParams') 75 | ); 76 | if ($params) { 77 | Git::setBin(Git::getBin() . ' ' . $params); 78 | } 79 | return $repo; 80 | } 81 | 82 | private function isIgnored($filePath) 83 | { 84 | $ignore = false; 85 | $ignorePaths = trim($this->getConf('ignorePaths')); 86 | if ($ignorePaths !== '') { 87 | $paths = explode(',', $ignorePaths); 88 | foreach ($paths as $path) { 89 | if (strstr($filePath, $path)) { 90 | $ignore = true; 91 | } 92 | } 93 | } 94 | return $ignore; 95 | } 96 | 97 | private function commitFile($filePath, $message) 98 | { 99 | if (!$this->isIgnored($filePath)) { 100 | try { 101 | $repo = $this->initRepo(); 102 | 103 | //add the changed file and set the commit message 104 | $repo->add($filePath); 105 | $repo->commit($message); 106 | 107 | //if the push after Commit option is set we push the active branch to origin 108 | if ($this->getConf('pushAfterCommit')) { 109 | $repo->push('origin', $repo->activeBranch()); 110 | } 111 | } catch (Exception $e) { 112 | if (!$this->isNotifyByEmailOnGitCommandError()) { 113 | throw new Exception('Git committing or pushing failed: ' . $e->getMessage(), 1, $e); 114 | } 115 | return; 116 | } 117 | } 118 | } 119 | 120 | private function getAuthor() 121 | { 122 | return $GLOBALS['USERINFO']['name']; 123 | } 124 | 125 | private function getAuthorMail() 126 | { 127 | return $GLOBALS['USERINFO']['mail']; 128 | } 129 | 130 | private function computeLocalPath() 131 | { 132 | global $conf; 133 | $repoPath = str_replace('\\', '/', realpath(GitBackedUtil::getEffectivePath($this->getConf('repoPath')))); 134 | $datadir = $conf['datadir']; // already normalized 135 | if (substr($datadir, 0, strlen($repoPath)) !== $repoPath) { 136 | throw new Exception('Datadir not inside repoPath ??'); 137 | } 138 | return substr($datadir, strlen($repoPath) + 1); 139 | } 140 | 141 | private function updatePage($page) 142 | { 143 | 144 | if (is_callable(Indexer::class . '::getInstance')) { 145 | $Indexer = Indexer::getInstance(); 146 | $success = $Indexer->addPage($page, false, false); 147 | } elseif (class_exists('Doku_Indexer')) { 148 | $success = idx_addPage($page, false, false); 149 | } else { 150 | // Failed to index the page. Your DokuWiki is older than release 2011-05-25 "Rincewind" 151 | $success = false; 152 | } 153 | 154 | echo "Update $page: $success
"; 155 | } 156 | 157 | public function handlePeriodicPull(Event &$event, $param) 158 | { 159 | if ($this->getConf('periodicPull')) { 160 | $enableIndexUpdate = $this->getConf('updateIndexOnPull'); 161 | $lastPullFile = $this->temp_dir . '/lastpull.txt'; 162 | //check if the lastPullFile exists 163 | if (is_file($lastPullFile)) { 164 | $lastPull = unserialize(file_get_contents($lastPullFile)); 165 | } else { 166 | $lastPull = 0; 167 | } 168 | //calculate time between pulls in seconds 169 | $timeToWait = $this->getConf('periodicMinutes') * 60; 170 | $now = time(); 171 | 172 | //if it is time to run a pull request 173 | if ($lastPull + $timeToWait < $now) { 174 | try { 175 | $repo = $this->initRepo(); 176 | if ($enableIndexUpdate) { 177 | $localPath = $this->computeLocalPath(); 178 | 179 | // store current revision id 180 | $revBefore = $repo->run('rev-parse HEAD'); 181 | } 182 | 183 | //execute the pull request 184 | $repo->pull('origin', $repo->activeBranch()); 185 | 186 | if ($enableIndexUpdate) { 187 | // store new revision id 188 | $revAfter = $repo->run('rev-parse HEAD'); 189 | 190 | if (strcmp($revBefore, $revAfter) != 0) { 191 | // if there were some changes, get the list of all changed files 192 | $changedFilesPage = $repo->run('diff --name-only ' . $revBefore . ' ' . $revAfter); 193 | $changedFiles = preg_split("/\r\n|\n|\r/", $changedFilesPage); 194 | 195 | foreach ($changedFiles as $cf) { 196 | // check if the file is inside localPath, that is, it's a page 197 | if (substr($cf, 0, strlen($localPath)) === $localPath) { 198 | // convert from relative filename to page name 199 | // for example: local/path/dir/subdir/test.txt -> dir:subdir:test 200 | // -4 removes .txt 201 | $page = str_replace('/', ':', substr($cf, strlen($localPath) + 1, -4)); 202 | 203 | // update the page 204 | $this->updatePage($page); 205 | } else { 206 | echo "Page NOT to update: $cf
"; 207 | } 208 | } 209 | } 210 | } 211 | } catch (Exception $e) { 212 | if (!$this->isNotifyByEmailOnGitCommandError()) { 213 | throw new Exception('Git command failed to perform periodic pull: ' . $e->getMessage(), 2, $e); 214 | } 215 | return; 216 | } 217 | 218 | //save the current time to the file to track the last pull execution 219 | file_put_contents($lastPullFile, serialize(time())); 220 | } 221 | } 222 | } 223 | 224 | public function handleMediaDeletion(Event &$event, $param) 225 | { 226 | $mediaPath = $event->data['path']; 227 | $mediaName = $event->data['name']; 228 | 229 | $message = str_replace( 230 | ['%media%', '%user%'], 231 | [$mediaName, $this->getAuthor()], 232 | $this->getConf('commitMediaMsgDel') 233 | ); 234 | 235 | $this->commitFile($mediaPath, $message); 236 | } 237 | 238 | public function handleMediaUpload(Event &$event, $param) 239 | { 240 | 241 | $mediaPath = $event->data[1]; 242 | $mediaName = $event->data[2]; 243 | 244 | $message = str_replace( 245 | ['%media%', '%user%'], 246 | [$mediaName, $this->getAuthor()], 247 | $this->getConf('commitMediaMsg') 248 | ); 249 | 250 | $this->commitFile($mediaPath, $message); 251 | } 252 | 253 | public function handleIOWikiPageWrite(Event &$event, $param) 254 | { 255 | 256 | $rev = $event->data[3]; 257 | 258 | /* On update to an existing page this event is called twice, 259 | * once for the transfer of the old version to the attic (rev will have a value) 260 | * and once to write the new version of the page into the wiki (rev is false) 261 | */ 262 | if (!$rev) { 263 | $pagePath = $event->data[0][0]; 264 | $pageName = $event->data[2]; 265 | $pageContent = $event->data[0][1]; 266 | 267 | // get the summary directly from the form input 268 | // as the metadata hasn't updated yet 269 | $editSummary = $GLOBALS['INPUT']->str('summary'); 270 | 271 | // empty content indicates a page deletion 272 | if ($pageContent == '') { 273 | // get the commit text for deletions 274 | $msgTemplate = $this->getConf('commitPageMsgDel'); 275 | 276 | // bad hack as DokuWiki deletes the file after this event 277 | // thus, let's delete the file by ourselves, so git can recognize the deletion 278 | // DokuWiki uses @unlink as well, so no error should be thrown if we delete it twice 279 | @unlink($pagePath); 280 | } else { 281 | //get the commit text for edits 282 | $msgTemplate = $this->getConf('commitPageMsg'); 283 | } 284 | 285 | $message = str_replace( 286 | ['%page%', '%summary%', '%user%'], 287 | [$pageName, $editSummary, $this->getAuthor()], 288 | $msgTemplate 289 | ); 290 | 291 | $this->commitFile($pagePath, $message); 292 | } 293 | } 294 | 295 | // ====== Error notification helpers ====== 296 | /** 297 | * Notifies error on create_new 298 | * 299 | * @access public 300 | * @param string repository path 301 | * @param string reference path / remote reference 302 | * @param string error message 303 | * @return bool 304 | */ 305 | public function notifyCreateNewError($repo_path, $reference, $error_message) 306 | { 307 | $template_replacements = [ 308 | 'GIT_REPO_PATH' => $repo_path, 309 | 'GIT_REFERENCE' => (empty($reference) ? 'n/a' : $reference), 310 | 'GIT_ERROR_MESSAGE' => $error_message 311 | ]; 312 | return $this->notifyByMail('mail_create_new_error_subject', 'mail_create_new_error', $template_replacements); 313 | } 314 | 315 | /** 316 | * Notifies error on setting repo path 317 | * 318 | * @access public 319 | * @param string repository path 320 | * @param string error message 321 | * @return bool 322 | */ 323 | public function notifyRepoPathError($repo_path, $error_message) 324 | { 325 | $template_replacements = [ 326 | 'GIT_REPO_PATH' => $repo_path, 327 | 'GIT_ERROR_MESSAGE' => $error_message 328 | ]; 329 | return $this->notifyByMail('mail_repo_path_error_subject', 'mail_repo_path_error', $template_replacements); 330 | } 331 | 332 | /** 333 | * Notifies error on git command 334 | * 335 | * @access public 336 | * @param string repository path 337 | * @param string current working dir 338 | * @param string command line 339 | * @param int exit code of command (status) 340 | * @param string error message 341 | * @return bool 342 | */ 343 | public function notifyCommandError($repo_path, $cwd, $command, $status, $error_message) 344 | { 345 | $template_replacements = [ 346 | 'GIT_REPO_PATH' => $repo_path, 347 | 'GIT_CWD' => $cwd, 348 | 'GIT_COMMAND' => $command, 349 | 'GIT_COMMAND_EXITCODE' => $status, 350 | 'GIT_ERROR_MESSAGE' => $error_message 351 | ]; 352 | return $this->notifyByMail('mail_command_error_subject', 'mail_command_error', $template_replacements); 353 | } 354 | 355 | /** 356 | * Notifies success on git command 357 | * 358 | * @access public 359 | * @param string repository path 360 | * @param string current working dir 361 | * @param string command line 362 | * @return bool 363 | */ 364 | public function notifyCommandSuccess($repo_path, $cwd, $command) 365 | { 366 | if (!$this->getConf('notifyByMailOnSuccess')) { 367 | return false; 368 | } 369 | $template_replacements = [ 370 | 'GIT_REPO_PATH' => $repo_path, 371 | 'GIT_CWD' => $cwd, 372 | 'GIT_COMMAND' => $command 373 | ]; 374 | return $this->notifyByMail('mail_command_success_subject', 'mail_command_success', $template_replacements); 375 | } 376 | 377 | /** 378 | * Send an eMail, if eMail address is configured 379 | * 380 | * @access public 381 | * @param string lang id for the subject 382 | * @param string lang id for the template(.txt) 383 | * @param array array of replacements 384 | * @return bool 385 | */ 386 | public function notifyByMail($subject_id, $template_id, $template_replacements) 387 | { 388 | $ret = false; 389 | //dbglog("GitBacked - notifyByMail: [subject_id=" . $subject_id 390 | // . ", template_id=" . $template_id 391 | // . ", template_replacements=" . $template_replacements . "]"); 392 | if (!$this->isNotifyByEmailOnGitCommandError()) { 393 | return $ret; 394 | } 395 | //$template_text = rawLocale($template_id); // this works for core artifacts only - not for plugins 396 | $template_filename = $this->localFN($template_id); 397 | $template_text = file_get_contents($template_filename); 398 | $template_html = $this->render_text($template_text); 399 | 400 | $mailer = new \Mailer(); 401 | $mailer->to($this->getEmailAddressOnErrorConfigured()); 402 | //dbglog("GitBacked - lang check['".$subject_id."']: ".$this->getLang($subject_id)); 403 | //dbglog("GitBacked - template text['".$template_id."']: ".$template_text); 404 | //dbglog("GitBacked - template html['".$template_id."']: ".$template_html); 405 | $mailer->subject($this->getLang($subject_id)); 406 | $mailer->setBody($template_text, $template_replacements, null, $template_html); 407 | 408 | $ret = $mailer->send(); 409 | 410 | return $ret; 411 | } 412 | 413 | /** 414 | * Check, if eMail is to be sent on a Git command error. 415 | * 416 | * @access public 417 | * @return bool 418 | */ 419 | public function isNotifyByEmailOnGitCommandError() 420 | { 421 | $emailAddressOnError = $this->getEmailAddressOnErrorConfigured(); 422 | return !empty($emailAddressOnError); 423 | } 424 | 425 | /** 426 | * Get the eMail address configured for notifications. 427 | * 428 | * @access public 429 | * @return string 430 | */ 431 | public function getEmailAddressOnErrorConfigured() 432 | { 433 | $emailAddressOnError = trim($this->getConf('emailAddressOnError')); 434 | return $emailAddressOnError; 435 | } 436 | } 437 | // phpcs:enable Squiz.Classes.ValidClassName.NotCamelCaps 438 | // phpcs:enable PSR1.Classes.ClassDeclaration.MissingNamespace 439 | 440 | // vim:ts=4:sw=4:et: 441 | -------------------------------------------------------------------------------- /classes/Git.php: -------------------------------------------------------------------------------- 1 | ['pipe', 'w'], 2 => ['pipe', 'w']]; 141 | $pipes = []; 142 | // Using --git-dir rather than --absolute-git-dir for a wider git versions compatibility 143 | //$command = Git::getBin()." rev-parse --absolute-git-dir"; 144 | $command = Git::getBin() . " rev-parse --git-dir"; 145 | //dbglog("GitBacked - Command: ".$command); 146 | $resource = proc_open($command, $descriptorspec, $pipes, $path); 147 | $stdout = stream_get_contents($pipes[1]); 148 | stream_get_contents($pipes[2]); 149 | foreach ($pipes as $pipe) { 150 | fclose($pipe); 151 | } 152 | 153 | $status = trim(proc_close($resource)); 154 | if ($status == 0) { 155 | $repo_git_dir = trim($stdout); 156 | //dbglog("GitBacked - $command: '".$repo_git_dir."'"); 157 | if (!empty($repo_git_dir)) { 158 | if (strcmp($repo_git_dir, ".git") === 0) { 159 | // convert to absolute path based on this command execution directory 160 | $repo_git_dir = $path . '/' . $repo_git_dir; 161 | } 162 | $repo_path = dirname(realpath($repo_git_dir)); 163 | $doku_inc_path = dirname(realpath(DOKU_INC)); 164 | if (strlen($repo_path) < strlen($doku_inc_path)) { 165 | // This code should never be reached! 166 | // If we get here, then we are beyond DOKU_INC - so this not supposed to be for us! 167 | return ''; 168 | } 169 | //dbglog('GitBacked - $repo_path: '.$repo_path); 170 | if (file_exists($repo_path . "/.git") && is_dir($repo_path . "/.git")) { 171 | return $repo_path; 172 | } 173 | } 174 | } 175 | return ''; 176 | } 177 | } 178 | /* End of file */ 179 | -------------------------------------------------------------------------------- /classes/GitRepo.php: -------------------------------------------------------------------------------- 1 | = 7.4 only. 28 | // protected ?\action_plugin_gitbacked_editcommit $plugin = null; 29 | protected $plugin; 30 | 31 | /** 32 | * Create a new git repository 33 | * 34 | * Accepts a creation path, and, optionally, a source path 35 | * 36 | * @access public 37 | * @param string repository path 38 | * @param \action_plugin_gitbacked_editcommit plugin 39 | * @param string directory to source 40 | * @param string reference path 41 | * @return GitRepo or null in case of an error 42 | */ 43 | public static function &createNew( 44 | $repo_path, 45 | \action_plugin_gitbacked_editcommit $plugin = null, 46 | $source = null, 47 | $remote_source = false, 48 | $reference = null 49 | ) { 50 | if (is_dir($repo_path) && file_exists($repo_path . "/.git") && is_dir($repo_path . "/.git")) { 51 | throw new \Exception(self::handleCreateNewError( 52 | $repo_path, 53 | $reference, 54 | '"' . $repo_path . '" is already a git repository', 55 | $plugin 56 | )); 57 | } else { 58 | $repo = new self($repo_path, $plugin, true, false); 59 | if (is_string($source)) { 60 | if ($remote_source) { 61 | if (!is_dir($reference) || !is_dir($reference . '/.git')) { 62 | throw new \Exception(self::handleCreateNewError( 63 | $repo_path, 64 | $reference, 65 | '"' . $reference . '" is not a git repository. Cannot use as reference.', 66 | $plugin 67 | )); 68 | } elseif (strlen($reference)) { 69 | $reference = realpath($reference); 70 | $reference = "--reference $reference"; 71 | } 72 | $repo->cloneRemote($source, $reference); 73 | } else { 74 | $repo->cloneFrom($source); 75 | } 76 | } else { 77 | $repo->run('init'); 78 | } 79 | return $repo; 80 | } 81 | } 82 | 83 | /** 84 | * Constructor 85 | * 86 | * Accepts a repository path 87 | * 88 | * @access public 89 | * @param string repository path 90 | * @param \action_plugin_gitbacked_editcommit plugin 91 | * @param bool create if not exists? 92 | * @return void 93 | */ 94 | public function __construct( 95 | $repo_path = null, 96 | \action_plugin_gitbacked_editcommit $plugin = null, 97 | $create_new = false, 98 | $_init = true 99 | ) { 100 | $this->plugin = $plugin; 101 | if (is_string($repo_path)) { 102 | $this->setRepoPath($repo_path, $create_new, $_init); 103 | } 104 | } 105 | 106 | /** 107 | * Set the repository's path 108 | * 109 | * Accepts the repository path 110 | * 111 | * @access public 112 | * @param string repository path 113 | * @param bool create if not exists? 114 | * @param bool initialize new Git repo if not exists? 115 | * @return void 116 | */ 117 | public function setRepoPath($repo_path, $create_new = false, $_init = true) 118 | { 119 | if (is_string($repo_path)) { 120 | if ($new_path = realpath($repo_path)) { 121 | $repo_path = $new_path; 122 | if (is_dir($repo_path)) { 123 | // Is this a work tree? 124 | if (file_exists($repo_path . "/.git") && is_dir($repo_path . "/.git")) { 125 | $this->repo_path = $repo_path; 126 | $this->bare = false; 127 | // Is this a bare repo? 128 | } elseif (is_file($repo_path . "/config")) { 129 | $parse_ini = parse_ini_file($repo_path . "/config"); 130 | if ($parse_ini['bare']) { 131 | $this->repo_path = $repo_path; 132 | $this->bare = true; 133 | } 134 | } elseif ($create_new) { 135 | $this->repo_path = $repo_path; 136 | if ($_init) { 137 | $this->run('init'); 138 | } 139 | } else { 140 | throw new \Exception($this->handleRepoPathError( 141 | $repo_path, 142 | '"' . $repo_path . '" is not a git repository' 143 | )); 144 | } 145 | } else { 146 | throw new \Exception($this->handleRepoPathError( 147 | $repo_path, 148 | '"' . $repo_path . '" is not a directory' 149 | )); 150 | } 151 | } elseif ($create_new) { 152 | if ($parent = realpath(dirname($repo_path))) { 153 | mkdir($repo_path); 154 | $this->repo_path = $repo_path; 155 | if ($_init) $this->run('init'); 156 | } else { 157 | throw new \Exception($this->handleRepoPathError( 158 | $repo_path, 159 | 'cannot create repository in non-existent directory' 160 | )); 161 | } 162 | } else { 163 | throw new \Exception($this->handleRepoPathError( 164 | $repo_path, 165 | '"' . $repo_path . '" does not exist' 166 | )); 167 | } 168 | } 169 | } 170 | 171 | /** 172 | * Get the path to the git repo directory (eg. the ".git" directory) 173 | * 174 | * @access public 175 | * @return string 176 | */ 177 | public function gitDirectoryPath() 178 | { 179 | return ($this->bare) ? $this->repo_path : $this->repo_path . "/.git"; 180 | } 181 | 182 | /** 183 | * Tests if git is installed 184 | * 185 | * @access public 186 | * @return bool 187 | */ 188 | public function testGit() 189 | { 190 | $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; 191 | $pipes = []; 192 | $resource = proc_open(Git::getBin(), $descriptorspec, $pipes); 193 | 194 | stream_get_contents($pipes[1]); 195 | stream_get_contents($pipes[2]); 196 | foreach ($pipes as $pipe) { 197 | fclose($pipe); 198 | } 199 | 200 | $status = trim(proc_close($resource)); 201 | return ($status != 127); 202 | } 203 | 204 | /** 205 | * Run a command in the git repository 206 | * 207 | * Accepts a shell command to run 208 | * 209 | * @access protected 210 | * @param string command to run 211 | * @return string or null in case of an error 212 | */ 213 | protected function runCommand($command) 214 | { 215 | //dbglog("Git->runCommand(command=[".$command."])"); 216 | $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; 217 | $pipes = []; 218 | $cwd = $this->repo_path; 219 | //dbglog("GitBacked - cwd: [".$cwd."]"); 220 | /* Provide any $this->envopts via putenv 221 | * and call proc_open with env=null to inherit the rest 222 | * of env variables from the original process of the system. 223 | * Note: Variables set by putenv live for a 224 | * single PHP request run only. These variables 225 | * are visible "locally". They are NOT listed by getenv(), 226 | * but they are visible to the process forked by proc_open(). 227 | */ 228 | foreach ($this->envopts as $k => $v) { 229 | putenv(sprintf("%s=%s", $k, $v)); 230 | } 231 | $resource = proc_open($command, $descriptorspec, $pipes, $cwd, null); 232 | 233 | $stdout = stream_get_contents($pipes[1]); 234 | $stderr = stream_get_contents($pipes[2]); 235 | foreach ($pipes as $pipe) { 236 | fclose($pipe); 237 | } 238 | 239 | $status = trim(proc_close($resource)); 240 | //dbglog("GitBacked: runCommand status: ".$status); 241 | if ($status) { 242 | //dbglog("GitBacked - stderr: [".$stderr."]"); 243 | // Remove a probable password from the Git URL, if the URL is contained in the error message 244 | $error_message = preg_replace( 245 | $this::REGEX_GIT_URL_FILTER_PWD, 246 | $this::REGEX_GIT_URL_FILTER_PWD_REPLACE_PATTERN, 247 | $stderr 248 | ); 249 | //dbglog("GitBacked - error_message: [".$error_message."]"); 250 | throw new \Exception($this->handleCommandError( 251 | $this->repo_path, 252 | $cwd, 253 | $command, 254 | $status, 255 | $error_message 256 | )); 257 | } else { 258 | $this->handleCommandSuccess($this->repo_path, $cwd, $command); 259 | } 260 | 261 | return $stdout; 262 | } 263 | 264 | /** 265 | * Run a git command in the git repository 266 | * 267 | * Accepts a git command to run 268 | * 269 | * @access public 270 | * @param string command to run 271 | * @return string 272 | */ 273 | public function run($command) 274 | { 275 | return $this->runCommand(Git::getBin() . " " . $command); 276 | } 277 | 278 | /** 279 | * Handles error on create_new 280 | * 281 | * @access protected 282 | * @param string repository path 283 | * @param string error message 284 | * @return string error message 285 | */ 286 | protected static function handleCreateNewError($repo_path, $reference, $error_message, $plugin) 287 | { 288 | if ($plugin instanceof \action_plugin_gitbacked_editcommit) { 289 | $plugin->notifyCreateNewError($repo_path, $reference, $error_message); 290 | } 291 | return $error_message; 292 | } 293 | 294 | /** 295 | * Handles error on setting the repo path 296 | * 297 | * @access protected 298 | * @param string repository path 299 | * @param string error message 300 | * @return string error message 301 | */ 302 | protected function handleRepoPathError($repo_path, $error_message) 303 | { 304 | if ($this->plugin instanceof \action_plugin_gitbacked_editcommit) { 305 | $this->plugin->notifyRepoPathError($repo_path, $error_message); 306 | } 307 | return $error_message; 308 | } 309 | 310 | /** 311 | * Handles error on git command 312 | * 313 | * @access protected 314 | * @param string repository path 315 | * @param string current working dir 316 | * @param string command line 317 | * @param int exit code of command (status) 318 | * @param string error message 319 | * @return string error message 320 | */ 321 | protected function handleCommandError($repo_path, $cwd, $command, $status, $error_message) 322 | { 323 | if ($this->plugin instanceof \action_plugin_gitbacked_editcommit) { 324 | $this->plugin->notifyCommandError($repo_path, $cwd, $command, $status, $error_message); 325 | } 326 | return $error_message; 327 | } 328 | 329 | /** 330 | * Handles success on git command 331 | * 332 | * @access protected 333 | * @param string repository path 334 | * @param string current working dir 335 | * @param string command line 336 | * @return void 337 | */ 338 | protected function handleCommandSuccess($repo_path, $cwd, $command) 339 | { 340 | if ($this->plugin instanceof \action_plugin_gitbacked_editcommit) { 341 | $this->plugin->notifyCommandSuccess($repo_path, $cwd, $command); 342 | } 343 | } 344 | 345 | /** 346 | * Runs a 'git status' call 347 | * 348 | * Accept a convert to HTML bool 349 | * 350 | * @access public 351 | * @param bool return string with
352 | * @return string 353 | */ 354 | public function status($html = false) 355 | { 356 | $msg = $this->run("status"); 357 | if ($html == true) { 358 | $msg = str_replace("\n", "
", $msg); 359 | } 360 | return $msg; 361 | } 362 | 363 | /** 364 | * Runs a `git add` call 365 | * 366 | * Accepts a list of files to add 367 | * 368 | * @access public 369 | * @param mixed files to add 370 | * @return string 371 | */ 372 | public function add($files = "*") 373 | { 374 | if (is_array($files)) { 375 | $files = '"' . implode('" "', $files) . '"'; 376 | } 377 | return $this->run("add $files -v"); 378 | } 379 | 380 | /** 381 | * Runs a `git rm` call 382 | * 383 | * Accepts a list of files to remove 384 | * 385 | * @access public 386 | * @param mixed files to remove 387 | * @param Boolean use the --cached flag? 388 | * @return string 389 | */ 390 | public function rm($files = "*", $cached = false) 391 | { 392 | if (is_array($files)) { 393 | $files = '"' . implode('" "', $files) . '"'; 394 | } 395 | return $this->run("rm " . ($cached ? '--cached ' : '') . $files); 396 | } 397 | 398 | 399 | /** 400 | * Runs a `git commit` call 401 | * 402 | * Accepts a commit message string 403 | * 404 | * @access public 405 | * @param string commit message 406 | * @param boolean should all files be committed automatically (-a flag) 407 | * @return string 408 | */ 409 | public function commit($message = "", $commit_all = true) 410 | { 411 | $flags = $commit_all ? '-av' : '-v'; 412 | $msgfile = GitBackedUtil::createMessageFile($message); 413 | try { 414 | return $this->run("commit --allow-empty " . $flags . " --file=" . $msgfile); 415 | } finally { 416 | unlink($msgfile); 417 | } 418 | } 419 | 420 | /** 421 | * Runs a `git clone` call to clone the current repository 422 | * into a different directory 423 | * 424 | * Accepts a target directory 425 | * 426 | * @access public 427 | * @param string target directory 428 | * @return string 429 | */ 430 | public function cloneTo($target) 431 | { 432 | return $this->run("clone --local " . $this->repo_path . " $target"); 433 | } 434 | 435 | /** 436 | * Runs a `git clone` call to clone a different repository 437 | * into the current repository 438 | * 439 | * Accepts a source directory 440 | * 441 | * @access public 442 | * @param string source directory 443 | * @return string 444 | */ 445 | public function cloneFrom($source) 446 | { 447 | return $this->run("clone --local $source " . $this->repo_path); 448 | } 449 | 450 | /** 451 | * Runs a `git clone` call to clone a remote repository 452 | * into the current repository 453 | * 454 | * Accepts a source url 455 | * 456 | * @access public 457 | * @param string source url 458 | * @param string reference path 459 | * @return string 460 | */ 461 | public function cloneRemote($source, $reference) 462 | { 463 | return $this->run("clone $reference $source " . $this->repo_path); 464 | } 465 | 466 | /** 467 | * Runs a `git clean` call 468 | * 469 | * Accepts a remove directories flag 470 | * 471 | * @access public 472 | * @param bool delete directories? 473 | * @param bool force clean? 474 | * @return string 475 | */ 476 | public function clean($dirs = false, $force = false) 477 | { 478 | return $this->run("clean" . (($force) ? " -f" : "") . (($dirs) ? " -d" : "")); 479 | } 480 | 481 | /** 482 | * Runs a `git branch` call 483 | * 484 | * Accepts a name for the branch 485 | * 486 | * @access public 487 | * @param string branch name 488 | * @return string 489 | */ 490 | public function createBranch($branch) 491 | { 492 | return $this->run("branch $branch"); 493 | } 494 | 495 | /** 496 | * Runs a `git branch -[d|D]` call 497 | * 498 | * Accepts a name for the branch 499 | * 500 | * @access public 501 | * @param string branch name 502 | * @return string 503 | */ 504 | public function deleteBranch($branch, $force = false) 505 | { 506 | return $this->run("branch " . (($force) ? '-D' : '-d') . " $branch"); 507 | } 508 | 509 | /** 510 | * Runs a `git branch` call 511 | * 512 | * @access public 513 | * @param bool keep asterisk mark on active branch 514 | * @return array 515 | */ 516 | public function listBranches($keep_asterisk = false) 517 | { 518 | $branchArray = explode("\n", $this->run("branch")); 519 | foreach ($branchArray as $i => &$branch) { 520 | $branch = trim($branch); 521 | if (! $keep_asterisk) { 522 | $branch = str_replace("* ", "", $branch); 523 | } 524 | if ($branch == "") { 525 | unset($branchArray[$i]); 526 | } 527 | } 528 | return $branchArray; 529 | } 530 | 531 | /** 532 | * Lists remote branches (using `git branch -r`). 533 | * 534 | * Also strips out the HEAD reference (e.g. "origin/HEAD -> origin/master"). 535 | * 536 | * @access public 537 | * @return array 538 | */ 539 | public function listRemoteBranches() 540 | { 541 | $branchArray = explode("\n", $this->run("branch -r")); 542 | foreach ($branchArray as $i => &$branch) { 543 | $branch = trim($branch); 544 | if ($branch == "" || strpos($branch, 'HEAD -> ') !== false) { 545 | unset($branchArray[$i]); 546 | } 547 | } 548 | return $branchArray; 549 | } 550 | 551 | /** 552 | * Returns name of active branch 553 | * 554 | * @access public 555 | * @param bool keep asterisk mark on branch name 556 | * @return string 557 | */ 558 | public function activeBranch($keep_asterisk = false) 559 | { 560 | $branchArray = $this->listBranches(true); 561 | $activeBranch = preg_grep("/^\*/", $branchArray); 562 | reset($activeBranch); 563 | if ($keep_asterisk) { 564 | return current($activeBranch); 565 | } else { 566 | return str_replace("* ", "", current($activeBranch)); 567 | } 568 | } 569 | 570 | /** 571 | * Runs a `git checkout` call 572 | * 573 | * Accepts a name for the branch 574 | * 575 | * @access public 576 | * @param string branch name 577 | * @return string 578 | */ 579 | public function checkout($branch) 580 | { 581 | return $this->run("checkout $branch"); 582 | } 583 | 584 | 585 | /** 586 | * Runs a `git merge` call 587 | * 588 | * Accepts a name for the branch to be merged 589 | * 590 | * @access public 591 | * @param string $branch 592 | * @return string 593 | */ 594 | public function merge($branch) 595 | { 596 | return $this->run("merge $branch --no-ff"); 597 | } 598 | 599 | 600 | /** 601 | * Runs a git fetch on the current branch 602 | * 603 | * @access public 604 | * @return string 605 | */ 606 | public function fetch() 607 | { 608 | return $this->run("fetch"); 609 | } 610 | 611 | /** 612 | * Add a new tag on the current position 613 | * 614 | * Accepts the name for the tag and the message 615 | * 616 | * @param string $tag 617 | * @param string $message 618 | * @return string 619 | */ 620 | public function addTag($tag, $message = null) 621 | { 622 | if ($message === null) { 623 | $message = $tag; 624 | } 625 | $msgfile = GitBackedUtil::createMessageFile($message); 626 | try { 627 | return $this->run("tag -a $tag --file=" . $msgfile); 628 | } finally { 629 | unlink($msgfile); 630 | } 631 | } 632 | 633 | /** 634 | * List all the available repository tags. 635 | * 636 | * Optionally, accept a shell wildcard pattern and return only tags matching it. 637 | * 638 | * @access public 639 | * @param string $pattern Shell wildcard pattern to match tags against. 640 | * @return array Available repository tags. 641 | */ 642 | public function listTags($pattern = null) 643 | { 644 | $tagArray = explode("\n", $this->run("tag -l $pattern")); 645 | foreach ($tagArray as $i => &$tag) { 646 | $tag = trim($tag); 647 | if ($tag == '') { 648 | unset($tagArray[$i]); 649 | } 650 | } 651 | 652 | return $tagArray; 653 | } 654 | 655 | /** 656 | * Push specific branch to a remote 657 | * 658 | * Accepts the name of the remote and local branch 659 | * 660 | * @param string $remote 661 | * @param string $branch 662 | * @return string 663 | */ 664 | public function push($remote, $branch) 665 | { 666 | return $this->run("push --tags $remote $branch"); 667 | } 668 | 669 | /** 670 | * Pull specific branch from remote 671 | * 672 | * Accepts the name of the remote and local branch 673 | * 674 | * @param string $remote 675 | * @param string $branch 676 | * @return string 677 | */ 678 | public function pull($remote, $branch) 679 | { 680 | return $this->run("pull $remote $branch"); 681 | } 682 | 683 | /** 684 | * List log entries. 685 | * 686 | * @param strgin $format 687 | * @return string 688 | */ 689 | public function log($format = null) 690 | { 691 | if ($format === null) { 692 | return $this->run('log'); 693 | } else { 694 | return $this->run('log --pretty=format:"' . $format . '"'); 695 | } 696 | } 697 | 698 | /** 699 | * Sets the project description. 700 | * 701 | * @param string $new 702 | */ 703 | public function setDescription($new) 704 | { 705 | $path = $this->gitDirectoryPath(); 706 | file_put_contents($path . "/description", $new); 707 | } 708 | 709 | /** 710 | * Gets the project description. 711 | * 712 | * @return string 713 | */ 714 | public function getDescription() 715 | { 716 | $path = $this->gitDirectoryPath(); 717 | return file_get_contents($path . "/description"); 718 | } 719 | 720 | /** 721 | * Sets custom environment options for calling Git 722 | * 723 | * @param string key 724 | * @param string value 725 | */ 726 | public function setenv($key, $value) 727 | { 728 | $this->envopts[$key] = $value; 729 | } 730 | } 731 | 732 | /* End of file */ 733 | -------------------------------------------------------------------------------- /conf/default.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | $conf['pushAfterCommit'] = 0; 10 | $conf['periodicPull'] = 0; 11 | $conf['periodicMinutes'] = 60; 12 | $conf['updateIndexOnPull'] = 0; 13 | $conf['commitPageMsg'] = 'Wiki page %page% changed with summary [%summary%] by %user%'; 14 | $conf['commitPageMsgDel'] = 'Wiki page %page% deleted with reason [%summary%] by %user%'; 15 | $conf['commitMediaMsg'] = 'Wiki media %media% uploaded by %user%'; 16 | $conf['commitMediaMsgDel'] = 'Wiki media %media% deleted by %user%'; 17 | $conf['repoPath'] = $GLOBALS['conf']['savedir']; 18 | $conf['repoWorkDir'] = ''; 19 | $conf['gitPath'] = ''; 20 | $conf['addParams'] = '-c user.name="%user%" -c user.email="<%mail%>"'; 21 | $conf['ignorePaths'] = ''; 22 | $conf['emailAddressOnError'] = ''; 23 | $conf['notifyByMailOnSuccess'] = 0; 24 | -------------------------------------------------------------------------------- /conf/metadata.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | $meta['pushAfterCommit'] = array('onoff'); 10 | $meta['periodicPull'] = array('onoff'); 11 | $meta['updateIndexOnPull'] = array('onoff'); 12 | $meta['periodicMinutes'] = array('numeric'); 13 | $meta['commitPageMsg'] = array('string'); 14 | $meta['commitPageMsgDel'] = array('string'); 15 | $meta['commitMediaMsg'] = array('string'); 16 | $meta['commitMediaMsgDel'] = array('string'); 17 | $meta['repoPath'] = array('string'); 18 | $meta['repoWorkDir'] = array('string'); 19 | $meta['gitPath'] = array('string'); 20 | $meta['addParams'] = array('string'); 21 | $meta['ignorePaths'] = array('string'); 22 | $meta['emailAddressOnError'] = array('string'); 23 | $meta['notifyByMailOnSuccess'] = array('onoff'); 24 | -------------------------------------------------------------------------------- /lang/de/lang.php: -------------------------------------------------------------------------------- 1 | @GIT_REPO_PATH@ 4 | 5 | * **Aktuelles Arbeitsverzeichnis:** @GIT_CWD@ 6 | 7 | * **Befehl:** @GIT_COMMAND@ 8 | 9 | * **Exitcode:** @GIT_COMMAND_EXITCODE@ 10 | 11 | * **Fehler:** @GIT_ERROR_MESSAGE@ 12 | 13 | * **Anwender:** @NAME@ 14 | 15 | * **eMail:** @MAIL@ 16 | 17 | * **Browser:** @BROWSER@ 18 | 19 | * **Wiki:** @TITLE@ @DOKUWIKIURL@ 20 | -------------------------------------------------------------------------------- /lang/de/mail_command_success.txt: -------------------------------------------------------------------------------- 1 | ==== Der folgende Git Befehl wurde ausgeführt auf @DOKUWIKIURL@ ==== 2 | 3 | * **Repo Pfad:** @GIT_REPO_PATH@ 4 | 5 | * **Aktuelles Arbeitsverzeichnis:** @GIT_CWD@ 6 | 7 | * **Befehl:** @GIT_COMMAND@ 8 | 9 | * **Anwender:** @NAME@ 10 | 11 | * **eMail:** @MAIL@ 12 | 13 | * **Browser:** @BROWSER@ 14 | 15 | * **Wiki:** @TITLE@ @DOKUWIKIURL@ 16 | -------------------------------------------------------------------------------- /lang/de/mail_create_new_error.txt: -------------------------------------------------------------------------------- 1 | ==== FEHLER beim Anlegen eines neuen Git Repositories auf @DOKUWIKIURL@ ==== 2 | 3 | * **Repo Pfad:** @GIT_REPO_PATH@ 4 | 5 | * **Referenz:** @GIT_REFERENCE@ 6 | 7 | * **Fehler:** @GIT_ERROR_MESSAGE@ 8 | 9 | * **Anwender:** @NAME@ 10 | 11 | * **eMail:** @MAIL@ 12 | 13 | * **Browser:** @BROWSER@ 14 | 15 | * **Wiki:** @TITLE@ @DOKUWIKIURL@ 16 | -------------------------------------------------------------------------------- /lang/de/mail_repo_path_error.txt: -------------------------------------------------------------------------------- 1 | ==== Ein FEHLER mit dem konfigurierten Git Repository Pfad ist aufgetreten auf @DOKUWIKIURL@ ==== 2 | 3 | * **Repo Pfad:** @GIT_REPO_PATH@ 4 | 5 | * **Fehler:** @GIT_ERROR_MESSAGE@ 6 | 7 | * **Anwender:** @NAME@ 8 | 9 | * **eMail:** @MAIL@ 10 | 11 | * **Browser:** @BROWSER@ 12 | 13 | * **Wiki:** @TITLE@ @DOKUWIKIURL@ 14 | -------------------------------------------------------------------------------- /lang/de/settings.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | $lang['pushAfterCommit'] = 'Push des aktiven Branch zum remote origin nach jedem commit'; 10 | $lang['periodicPull'] = 'Pull des remote git Repositories alle "periodicMinutes", getriggert von einem http Page Request'; 11 | $lang['updateIndexOnPull'] = 'Index von Seiten aktualisieren, die durch einen Pull geändert wurden'; 12 | $lang['periodicMinutes'] = 'Zeitraum (in Minuten) zwischen den periodischen pull requests'; 13 | $lang['commitPageMsg'] = 'Commit Kommentar für Seitenänderungen (%user%,%summary%,%page% werden durch die tatsächlichen Werte ersetzt)'; 14 | $lang['commitPageMsgDel'] = 'Commit Kommentar für gelöschte Seiten (%user%,%summary%,%page% werden durch die tatsächlichen Werte ersetzt)'; 15 | $lang['commitMediaMsg'] = 'Commit Kommentar for media Dateien (%user%,%media% werden durch die tatsächlichen Werte ersetzt)'; 16 | $lang['commitMediaMsgDel'] = 'Commit Kommentar für gelöschte media Dateien (%user%,%media% werden durch die tatsächlichen Werte ersetzt)'; 17 | $lang['repoPath'] = 'Pfad des git repo (z.B. das savedir ' . $GLOBALS['conf']['savedir'] . ')'; 18 | $lang['repoWorkDir'] = 'Pfad des git working tree. Dieser muss die "pages" and "media" Verzeichnisse enthalten (z.B. das savedir ' . $GLOBALS['conf']['savedir'] . ')'; 19 | $lang['gitPath'] = 'Pfad zum git binary (Wenn leer, dann wird der Standard "/usr/bin/git" verwendet)'; 20 | $lang['addParams'] = 'Zusätzliche git Parameter (diese werden dem git Kommando zugefügt) (%user% und %mail% werden durch die tatsächlichen Werte ersetzt)'; 21 | $lang['ignorePaths'] = 'Pfade/Dateien die ignoriert werden und nicht von git archiviert werden sollen (durch Kommata getrennt)'; 22 | $lang['emailAddressOnError'] = 'Wenn definiert, dann wird bei einem Fehler eine eMail an diese Adresse(n) gesendet, anstatt den aktuellen Endanwender mit einer Exception zu verunsichern. Mehrere Adressen können durch Kommata getrennt konfiguriert werden'; 23 | $lang['notifyByMailOnSuccess'] = 'Wenn emailAddressOnError definiert ist, dann wird bei jedem Commit eine eMail gesendet. Diese Einstellung sollte nur zum Testen der eMail Benachrichtigung aktiviert werden'; 24 | -------------------------------------------------------------------------------- /lang/en/lang.php: -------------------------------------------------------------------------------- 1 | @GIT_REPO_PATH@ 4 | 5 | * **Current working dir:** @GIT_CWD@ 6 | 7 | * **Command:** @GIT_COMMAND@ 8 | 9 | * **Exitcode:** @GIT_COMMAND_EXITCODE@ 10 | 11 | * **Error:** @GIT_ERROR_MESSAGE@ 12 | 13 | * **User:** @NAME@ 14 | 15 | * **eMail:** @MAIL@ 16 | 17 | * **Browser:** @BROWSER@ 18 | 19 | * **Wiki:** @TITLE@ @DOKUWIKIURL@ 20 | -------------------------------------------------------------------------------- /lang/en/mail_command_success.txt: -------------------------------------------------------------------------------- 1 | ==== The following Git command was performed on @DOKUWIKIURL@ ==== 2 | 3 | * **Repo path:** @GIT_REPO_PATH@ 4 | 5 | * **Current working dir:** @GIT_CWD@ 6 | 7 | * **Command:** @GIT_COMMAND@ 8 | 9 | * **User:** @NAME@ 10 | 11 | * **eMail:** @MAIL@ 12 | 13 | * **Browser:** @BROWSER@ 14 | 15 | * **Wiki:** @TITLE@ @DOKUWIKIURL@ 16 | -------------------------------------------------------------------------------- /lang/en/mail_create_new_error.txt: -------------------------------------------------------------------------------- 1 | ==== The creation of a new Git repo FAILED on @DOKUWIKIURL@ ==== 2 | 3 | * **Repo path:** @GIT_REPO_PATH@ 4 | 5 | * **Reference:** @GIT_REFERENCE@ 6 | 7 | * **Error:** @GIT_ERROR_MESSAGE@ 8 | 9 | * **User:** @NAME@ 10 | 11 | * **eMail:** @MAIL@ 12 | 13 | * **Browser:** @BROWSER@ 14 | 15 | * **Wiki:** @TITLE@ @DOKUWIKIURL@ 16 | -------------------------------------------------------------------------------- /lang/en/mail_repo_path_error.txt: -------------------------------------------------------------------------------- 1 | ==== An FAILURE with the Git repo path occurred on @DOKUWIKIURL@ ==== 2 | 3 | * **Repo path:** @GIT_REPO_PATH@ 4 | 5 | * **Error:** @GIT_ERROR_MESSAGE@ 6 | 7 | * **User:** @NAME@ 8 | 9 | * **eMail:** @MAIL@ 10 | 11 | * **Browser:** @BROWSER@ 12 | 13 | * **Wiki:** @TITLE@ @DOKUWIKIURL@ 14 | -------------------------------------------------------------------------------- /lang/en/settings.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | 9 | $lang['pushAfterCommit'] = 'Push active branch to remote origin after every commit'; 10 | $lang['periodicPull'] = 'Pull the remote git repository every "periodicMinutes" triggered by a http page request'; 11 | $lang['updateIndexOnPull'] = 'Update index of pages changed on pull'; 12 | $lang['periodicMinutes'] = 'Timespan (in minutes) between periodic pull requests'; 13 | $lang['commitPageMsg'] = 'Commit message for page edits (%user%,%summary%,%page% are replaced by the corresponding values)'; 14 | $lang['commitPageMsgDel'] = 'Commit message for deleted pages (%user%,%summary%,%page% are replaced by the corresponding values)'; 15 | $lang['commitMediaMsg'] = 'Commit message for media files (%user%,%media% are replaced by the corresponding values)'; 16 | $lang['commitMediaMsgDel'] = 'Commit message for deleted media files (%user%,%media% are replaced by the corresponding values)'; 17 | $lang['repoPath'] = 'Path of the git repo(s) (e.g. the savedir ' . $GLOBALS['conf']['savedir'] . ')'; 18 | $lang['repoWorkDir'] = 'Path of the git working tree, must contain "pages" and "media" directories (e.g. the savedir ' . $GLOBALS['conf']['savedir'] . ')'; 19 | $lang['gitPath'] = 'Path to the git binary (if empty, the default "/usr/bin/git" will be used)'; 20 | $lang['addParams'] = 'Additional git parameters (added to the git execution command) (%user% and %mail% are replaced by the corresponding values)'; 21 | $lang['ignorePaths'] = 'Paths/files which are ignored and not added to git (comma-separated)'; 22 | $lang['emailAddressOnError'] = 'If set, in case of a git error an eMail will be sent to this address rather than confusing the end user by the Exception raised. Multiple mail addresses can be configured comma separated'; 23 | $lang['notifyByMailOnSuccess'] = 'If emailAddressOnError is defined, an eMail will be sent on any git commit. This is supposed to be used for eMail notification test purposes only'; 24 | -------------------------------------------------------------------------------- /loader.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/classes/']; 15 | 16 | foreach ($namespaces as $prefix => $base_dir) { 17 | // does the class use the namespace prefix? 18 | $len = strlen($prefix); 19 | if (strncmp($prefix, $class, $len) !== 0) { 20 | // no, move to the next 21 | continue; 22 | } 23 | 24 | // get the relative class name 25 | $relative_class = substr($class, $len); 26 | 27 | // replace the namespace prefix with the base directory, replace namespace 28 | // separators with directory separators in the relative class name, append 29 | // with .php 30 | $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; 31 | 32 | // if the file exists, require it 33 | if (file_exists($file)) { 34 | require $file; 35 | } 36 | } 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /plugin.info.txt: -------------------------------------------------------------------------------- 1 | base gitbacked 2 | author Wolfgang Gassler (@woolfg), Carsten Teibes (@carstene1ns), Markus Hoffrogge (@mhoffrog) 3 | email wolfgang@gassler.org 4 | date 2025-02-26 5 | name gitbacked plugin 6 | desc Pages and Media are stored in Git 7 | url https://github.com/woolfg/dokuwiki-plugin-gitbacked 8 | --------------------------------------------------------------------------------