diff --git a/action/editcommit.php b/action/editcommit.php index 9990936..f8be78e 100644 --- a/action/editcommit.php +++ b/action/editcommit.php @@ -18,7 +18,7 @@ class action_plugin_gitbacked_editcommit extends DokuWiki_Action_Plugin { - function __construct() { + function __construct() { global $conf; $this->temp_dir = $conf['tmpdir'].'/gitbacked'; io_mkdir_p($this->temp_dir); @@ -30,14 +30,22 @@ public function register(Doku_Event_Handler $controller) { $controller->register_hook('MEDIA_UPLOAD_FINISH', 'AFTER', $this, 'handle_media_upload'); $controller->register_hook('MEDIA_DELETE_FILE', 'AFTER', $this, 'handle_media_deletion'); $controller->register_hook('DOKUWIKI_DONE', 'AFTER', $this, 'handle_periodic_pull'); + $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'handle_code_or_config_on_start'); + $controller->register_hook('DOKUWIKI_DONE', 'AFTER', $this, 'handle_code_or_config_on_done', null, 10); + $controller->register_hook('AJAX_CALL_UNKNOWN', 'AFTER', $this, 'handle_code_or_config_on_ajax'); + } - private function initRepo() { + private function initRepo($repoPathConfigKey, $repoWorkDirConfigKey) { //get path to the repo root (by default DokuWiki's savedir) + $configuredRepoPath = trim($this->getConf($repoPathConfigKey)); + if (empty($configuredRepoPath)) { + return null; + } if(defined('DOKU_FARM')) { - $repoPath = $this->getConf('repoPath'); + $repoPath = $configuredRepoPath; } else { - $repoPath = DOKU_INC.$this->getConf('repoPath'); + $repoPath = DOKU_INC.$configuredRepoPath; } //set the path to the git binary $gitPath = trim($this->getConf('gitPath')); @@ -48,8 +56,13 @@ private function initRepo() { io_mkdir_p($repoPath); $repo = new GitRepo($repoPath, $this, true, true); //set git working directory (by default DokuWiki's savedir) - $repoWorkDir = DOKU_INC.$this->getConf('repoWorkDir'); - Git::set_bin(Git::get_bin().' --work-tree '.escapeshellarg($repoWorkDir)); + if (!empty($repoWorkDirConfigKey)) { + $configuredRepoWorkDir = trim($this->getConf($repoWorkDirConfigKey)); + if (!empty($configuredRepoWorkDir)) { + $repoWorkDir = DOKU_INC.$configuredRepoWorkDir; + Git::set_bin(Git::get_bin().' --work-tree '.escapeshellarg($repoWorkDir)); + } + } $params = str_replace( array('%mail%','%user%'), @@ -75,11 +88,29 @@ private function isIgnored($filePath) { return $ignore; } - private function commitFile($filePath,$message) { + private function pullRepo($repoPathConfigKey,$repoWorkDirConfigKey) { + try { + $repo = $this->initRepo($repoPathConfigKey,$repoWorkDirConfigKey); + if (is_null($repo)) { + return; + } + //execute the pull request + $repo->pull('origin',$repo->active_branch()); + } catch (Exception $e) { + if (!$this->isNotifyByEmailOnGitCommandError()) { + throw new Exception('Git command failed to perform pull: '.$e->getMessage(), 2, $e); + } + return; + } + } + + private function commitFile($repoPathConfigKey,$repoWorkDirConfigKey,$filePath,$message) { if (!$this->isIgnored($filePath)) { try { - $repo = $this->initRepo(); - + $repo = $this->initRepo($repoPathConfigKey,$repoWorkDirConfigKey); + if (is_null($repo)) { + return; + } //add the changed file and set the commit message $repo->add($filePath); $repo->commit($message); @@ -97,6 +128,35 @@ private function commitFile($filePath,$message) { } } + private function commitAll($repoPathConfigKey,$repoWorkDirConfigKey,$message) { + try { + $repo = $this->initRepo($repoPathConfigKey,$repoWorkDirConfigKey); + if (is_null($repo)) { + return; + } + $gitStatus = $repo->status(false, '-s'); + dbglog("GitBacked - commitAll[".$repoPathConfigKey."] - status BEFORE: (".strlen($gitStatus).") [".$gitStatus."]"); + if (empty($gitStatus)) { + return; + } + $repo->addAll(); + dbglog("GitBacked - commitAll[".$repoPathConfigKey."] - AFTER addAll()"); + $repo->commit($message); + dbglog("GitBacked - commitAll[".$repoPathConfigKey."] - AFTER commit"); + $gitStatus = $repo->status(false, '-s'); + dbglog("GitBacked - commitAll[".$repoPathConfigKey."] - status AFTER: (".strlen($gitStatus).") [".$gitStatus."]"); + + //if the push after Commit option is set we push the active branch to origin + if ($this->getConf('pushAfterCommit')) { + $repo->push('origin',$repo->active_branch()); + } + } catch (Exception $e) { + if (!$this->isNotifyByEmailOnGitCommandError()) { + throw new Exception('Git committing or pushing failed: '.$e->getMessage(), 1, $e); + } + } + } + private function getAuthor() { return $GLOBALS['USERINFO']['name']; } @@ -105,6 +165,11 @@ private function getAuthorMail() { return $GLOBALS['USERINFO']['mail']; } + private function pageID($nameSpace, $pageName) { + $id = empty($nameSpace) ? $pageName : $nameSpace.':'.$pageName; + return $id; + } + public function handle_periodic_pull(Doku_Event &$event, $param) { if ($this->getConf('periodicPull')) { $lastPullFile = $this->temp_dir.'/lastpull.txt'; @@ -120,18 +185,8 @@ public function handle_periodic_pull(Doku_Event &$event, $param) { //if it is time to run a pull request if ($lastPull+$timeToWait < $now) { - try { - $repo = $this->initRepo(); - - //execute the pull request - $repo->pull('origin',$repo->active_branch()); - } catch (Exception $e) { - if (!$this->isNotifyByEmailOnGitCommandError()) { - throw new Exception('Git command failed to perform periodic pull: '.$e->getMessage(), 2, $e); - } - return; - } - + $this->pullRepo('repoPath', 'repoWorkDir'); + $this->pullRepo('repoPathMedia', 'repoWorkDirMedia'); //save the current time to the file to track the last pull execution file_put_contents($lastPullFile,serialize(time())); } @@ -140,22 +195,26 @@ public function handle_periodic_pull(Doku_Event &$event, $param) { public function handle_media_deletion(Doku_Event &$event, $param) { $mediaPath = $event->data['path']; - $mediaName = $event->data['name']; + $mediaName = $event->data['id']; $message = str_replace( array('%media%','%user%'), array($mediaName,$this->getAuthor()), $this->getConf('commitMediaMsgDel') ); + + if (!empty($this->getConf('repoPathMedia'))) { + $this->commitFile('repoPathMedia','repoWorkDirMedia',$mediaPath,$message); + } else { + $this->commitFile('repoPath','repoWorkDir',$mediaPath,$message); + } - $this->commitFile($mediaPath,$message); - - } + } public function handle_media_upload(Doku_Event &$event, $param) { - $mediaPath = $event->data[1]; - $mediaName = $event->data[2]; + $mediaPath = $event->data[1]; // $fn + $mediaName = $event->data[2]; // $id $message = str_replace( array('%media%','%user%'), @@ -163,7 +222,11 @@ public function handle_media_upload(Doku_Event &$event, $param) { $this->getConf('commitMediaMsg') ); - $this->commitFile($mediaPath,$message); + if (!empty($this->getConf('repoPathMedia'))) { + $this->commitFile('repoPathMedia','repoWorkDirMedia',$mediaPath,$message); + } else { + $this->commitFile('repoPath','repoWorkDir',$mediaPath,$message); + } } @@ -178,10 +241,11 @@ public function handle_io_wikipage_write(Doku_Event &$event, $param) { if (!$rev) { $pagePath = $event->data[0][0]; + $nameSpace = $event->data[1]; $pageName = $event->data[2]; $pageContent = $event->data[0][1]; - // get the summary directly from the form input + // get the summary directly from the form input // as the metadata hasn't updated yet $editSummary = $GLOBALS['INPUT']->str('summary'); @@ -202,15 +266,150 @@ public function handle_io_wikipage_write(Doku_Event &$event, $param) { $message = str_replace( array('%page%','%summary%','%user%'), - array($pageName,$editSummary,$this->getAuthor()), + array($this->pageID($nameSpace,$pageName),$editSummary,$this->getAuthor()), $msgTemplate ); - $this->commitFile($pagePath,$message); + $this->commitFile('repoPath','repoWorkDir',$pagePath,$message); } } - + + public function handle_code_or_config_on_start(Doku_Event &$event, $param) { + + global $INPUT; + + $message = ''; + $isPluginChanged = false; + + dbglog("GitBacked - handle_code_or_config_on_start - event=['".$event."'], data=['".$event->data."'], page='".$INPUT->str('page')."', save=".$INPUT->bool('save').", arr('config')=[".$INPUT->arr('config')."]"); + // configuration manager + if ($INPUT->str('page') === 'config' + && $INPUT->str('do') === 'admin' + && $INPUT->bool('save') === true + && !empty($INPUT->arr('config')) + ) { + //$this->logAdmin(['save config']); + $message = $this->getAuthor().' changed config'; + dbglog("GitBacked - handle_code_or_config_on_start - config change['".$message."']"); + } + + // template design manager + if ($INPUT->str('page') === 'styling' + && $INPUT->str('do') === 'admin' + && !empty($INPUT->extract('run')->str('run')) + && !empty($INPUT->arr('tpl')) + ) { + //$this->logAdmin(['save template style']); + $message = $this->getAuthor().' changed template style'; + dbglog("GitBacked - handle_code_or_config_on_start - template style change['".$message."']"); + } + + // extension manager + if ($INPUT->str('page') === 'extension') { + if ($INPUT->post->has('fn')) { + $aChangedExtensions = array(); + $actions = $INPUT->post->arr('fn'); + foreach ($actions as $action => $extensions) { + foreach ($extensions as $extname => $label) { + //$this->logAdmin([$action, $extname]); + $changedExtension = $action."['".$extname."']"; + array_push($aChangedExtensions, $changedExtension); + dbglog("GitBacked - handle_code_or_config_on_start - action[extension] = ".$changedExtension); + } + } + $isPluginChanged = true; + $message = $this->getAuthor().' changed plugins: '.implode (', ', $aChangedExtensions); + dbglog("GitBacked - handle_code_or_config_on_start - EXTENSION change['".$message."']"); + } elseif ($INPUT->post->str('installurl')) { + //$this->logAdmin(['installurl', $INPUT->post->str('installurl')]); + $isPluginChanged = true; + $message = $this->getAuthor().' installed plugin by URL: '.$INPUT->post->str('installurl'); + dbglog("GitBacked - handle_code_or_config_on_start - PLUGIN_URL change['".$message."']"); + } elseif (isset($_FILES['installfile'])) { + //$this->logAdmin(['installfile', $_FILES['installfile']['name']]); + $isPluginChanged = true; + $message = $this->getAuthor().' installed plugin by file: '.$_FILES['installfile']['name']; + dbglog("GitBacked - handle_code_or_config_on_start - PLUGIN_FILE change['".$message."']"); + } + } + + // ACL manager + if ($INPUT->str('page') === 'acl' && $INPUT->has('cmd')) { + $cmd = $INPUT->extract('cmd')->str('cmd'); + $del = $INPUT->arr('del'); + if ($cmd === 'update' && !empty($del)) { + $cmd = 'delete'; + $rule = $del; + } else { + $rule = [ + 'ns' => $INPUT->str('ns'), + 'acl_t' => $INPUT->str('acl_t'), + 'acl_w' => $INPUT->str('acl_w'), + 'acl' => $INPUT->str('acl') + ]; + } + + //$this->logAdmin([$cmd, $rule]); + $message = $this->getAuthor().' changed ACLs'; + dbglog("GitBacked - handle_code_or_config_on_start - ACL change['".$message."']"); + } + + if (!empty($message)) { + $confOrCodeChangeMessageFile = $this->temp_dir.'/confOrCodeChangeMessage.txt'; + file_put_contents($confOrCodeChangeMessageFile,serialize($message)); + } + if ($isPluginChanged == true) { + $isPluginChangedFile = $this->temp_dir.'/isPluginChanged.txt'; + file_put_contents($isPluginChangedFile,serialize($isPluginChanged)); + } + + } + + public function handle_code_or_config_on_done(Doku_Event &$event, $param) { + global $INPUT; + + dbglog("GitBacked - handle_code_or_config_on_done - event=['".$event."'], data=['".$event->data."'], page='".$INPUT->str('page')."', save=".$INPUT->bool('save').", arr('config')=[".$INPUT->arr('config')."]"); + + $message = ''; + $confOrCodeChangeMessageFile = $this->temp_dir.'/confOrCodeChangeMessage.txt'; + if (is_file($confOrCodeChangeMessageFile)) { + $message = unserialize(file_get_contents($confOrCodeChangeMessageFile)); + } + $isPluginChangedFile = $this->temp_dir.'/isPluginChanged.txt'; + $isPluginChanged = is_file($isPluginChangedFile); + if ($isPluginChanged == true) { + dbglog("GitBacked - commitAll CODE['".$message."']"); + $this->commitAll('repoPathCode',null,$message); + @unlink($isPluginChangedFile); + } + if (!empty($message)) { + dbglog("GitBacked - commitAll CONF['".$message."']"); + $this->commitAll('repoPathConf',null,$message); + @unlink($confOrCodeChangeMessageFile); + } + + } + + /** + * Catch admin actions performed via Ajax + * + * @param Doku_Event $event + */ + public function handle_code_or_config_on_ajax(Doku_Event &$event, $param) { + global $INPUT; + + dbglog("GitBacked - handle_code_or_config_on_ajax - event=['".$event."'], data=['".$event->data."'], page='".$INPUT->str('page')."'"); + + // extension manager + if ($event->data === 'plugin_extension') { + //$this->logAdmin([$INPUT->str('act') . ' ' . $INPUT->str('ext')], 'extension'); + $message = $this->getAuthor().' '.$INPUT->str('act').' plugin '.$INPUT->str('ext'); + $this->commitAll('repoPathCode',null,$message); + $this->commitAll('repoPathConf',null,$message); + } + } + // ====== Error notification helpers ====== /** * Notifies error on create_new @@ -228,7 +427,7 @@ public function notify_create_new_error($repo_path, $reference, $error_message) 'GIT_ERROR_MESSAGE' => $error_message ); return $this->notifyByMail('mail_create_new_error_subject', 'mail_create_new_error', $template_replacements); - } + } /** * Notifies error on setting repo path @@ -300,7 +499,7 @@ public function notify_command_success($repo_path, $cwd, $command) { */ public function notifyByMail($subject_id, $template_id, $template_replacements) { $ret = false; - dbglog("GitBacked - notifyByMail: [subject_id=".$subject_id.", template_id=".$template_id.", template_replacements=".$template_replacements."]"); + //dbglog("GitBacked - notifyByMail: [subject_id=".$subject_id.", template_id=".$template_id.", template_replacements=".$template_replacements."]"); if (!$this->isNotifyByEmailOnGitCommandError()) { return $ret; } @@ -311,16 +510,16 @@ public function notifyByMail($subject_id, $template_id, $template_replacements) $mailer = new \Mailer(); $mailer->to($this->getEmailAddressOnErrorConfigured()); - dbglog("GitBacked - lang check['".$subject_id."']: ".$this->getLang($subject_id)); - dbglog("GitBacked - template text['".$template_id."']: ".$template_text); - dbglog("GitBacked - template html['".$template_id."']: ".$template_html); + //dbglog("GitBacked - lang check['".$subject_id."']: ".$this->getLang($subject_id)); + //dbglog("GitBacked - template text['".$template_id."']: ".$template_text); + //dbglog("GitBacked - template html['".$template_id."']: ".$template_html); $mailer->subject($this->getLang($subject_id)); $mailer->setBody($template_text, $template_replacements, null, $template_html); $ret = $mailer->send(); - + return $ret; } - + /** * Check, if eMail is to be sent on a Git command error. * @@ -331,7 +530,7 @@ public function isNotifyByEmailOnGitCommandError() { $emailAddressOnError = $this->getEmailAddressOnErrorConfigured(); return !empty($emailAddressOnError); } - + /** * Get the eMail address configured for notifications. * diff --git a/conf/default.php b/conf/default.php index 8b642e3..bbec4aa 100644 --- a/conf/default.php +++ b/conf/default.php @@ -8,14 +8,18 @@ $conf['pushAfterCommit'] = 0; $conf['periodicPull'] = 0; $conf['periodicMinutes'] = 60; -$conf['commitPageMsg'] = 'Wiki page %page% changed with summary [%summary%] by %user%'; -$conf['commitPageMsgDel'] = 'Wiki page %page% deleted with reason [%summary%] by %user%'; -$conf['commitMediaMsg'] = 'Wiki media %media% uploaded by %user%'; -$conf['commitMediaMsgDel'] = 'Wiki media %media% deleted by %user%'; +$conf['commitPageMsg'] = 'Page UPDATE by %user% [%summary%]: %page%'; +$conf['commitPageMsgDel'] = 'Page DELETE by %user% [%summary%]: %page%'; +$conf['commitMediaMsg'] = 'Media UPLOAD by %user%: %media%'; +$conf['commitMediaMsgDel'] = 'Media DELETE by %user%: %media%'; $conf['repoPath'] = $GLOBALS['conf']['savedir']; $conf['repoWorkDir'] = $GLOBALS['conf']['savedir']; +$conf['repoPathMedia']= ''; +$conf['repoWorkDirMedia'] = ''; +$conf['repoPathConf']= ''; +$conf['repoPathCode']= ''; $conf['gitPath'] = ''; -$conf['addParams'] = ''; +$conf['addParams'] = '-c user.name="%user%" -c user.email="%mail%"'; $conf['ignorePaths'] = ''; $conf['emailAddressOnError'] = ''; $conf['notifyByMailOnSuccess'] = 0; diff --git a/conf/metadata.php b/conf/metadata.php index 8ff5d3a..a12a5d9 100644 --- a/conf/metadata.php +++ b/conf/metadata.php @@ -14,6 +14,10 @@ $meta['commitMediaMsgDel'] = array('string'); $meta['repoPath'] = array('string'); $meta['repoWorkDir'] = array('string'); +$meta['repoPathMedia']= array('string'); +$meta['repoWorkDirMedia'] = array('string'); +$meta['repoPathConf']= array('string'); +$meta['repoPathCode']= array('string'); $meta['gitPath'] = array('string'); $meta['addParams'] = array('string'); $meta['ignorePaths'] = array('string'); diff --git a/lang/de/settings.php b/lang/de/settings.php index a0b6c17..87aaa14 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -13,8 +13,12 @@ $lang['commitPageMsgDel'] = 'Commit Kommentar für gelöschte Seiten (%user%,%summary%,%page% werden durch die tatsächlichen Werte ersetzt)'; $lang['commitMediaMsg'] = 'Commit Kommentar for media Dateien (%user%,%media% werden durch die tatsächlichen Werte ersetzt)'; $lang['commitMediaMsgDel'] = 'Commit Kommentar für gelöschte media Dateien (%user%,%media% werden durch die tatsächlichen Werte ersetzt)'; -$lang['repoPath'] = 'Pfad des git repo (z.B. das savedir '.$GLOBALS['conf']['savedir'].')'; -$lang['repoWorkDir'] = 'Pfad des git working tree. Dieser muss die "pages" and "media" Verzeichnisse enthalten (z.B. das savedir '.$GLOBALS['conf']['savedir'].')'; +$lang['repoPath'] = 'Pfad des git repo für pages und media per default (z.B. das savedir '.$GLOBALS['conf']['savedir'].')'; +$lang['repoWorkDir'] = 'Pfad des git working tree. Dieser muss die "pages" und per default das "media" Verzeichnisse enthalten (z.B. das savedir '.$GLOBALS['conf']['savedir'].')'; +$lang['repoPathMedia'] = 'Optional: Pfad eines eigenen git repo für media. Wenn dies nicht gesetz ist, wird das in repoPath definierte repo benutzt'; +$lang['repoWorkDirMedia'] = 'Optional: Pfad des git working tree für das media repo. Wenn dies nicht definiert ist, wird der Pfad repoWorkDir benutzt'; +$lang['repoPathConf']= 'Optional: Pfad des git repo für die DokuWiki config. Wenn nicht gesetzt, wird keine config Änderungen commitet'; +$lang['repoPathCode']= 'Optional: Pfad des git repo für den PHP code der Installation. Wenn nicht gesetzt, werden keine Änderungen an der Installation commitet'; $lang['gitPath'] = 'Pfad zum git binary (Wenn leer, dann wird der Standard "/usr/bin/git" verwendet)'; $lang['addParams'] = 'Zusätzliche git Parameter (diese werden dem git Kommando zugefügt) (%user% und %mail% werden durch die tatsächlichen Werte ersetzt)'; $lang['ignorePaths'] = 'Pfade/Dateien die ignoriert werden und nicht von git archiviert werden sollen (durch Kommata getrennt)'; diff --git a/lang/en/settings.php b/lang/en/settings.php index 3701ec0..b1eb56e 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -13,8 +13,12 @@ $lang['commitPageMsgDel'] = 'Commit message for deleted pages (%user%,%summary%,%page% are replaced by the corresponding values)'; $lang['commitMediaMsg'] = 'Commit message for media files (%user%,%media% are replaced by the corresponding values)'; $lang['commitMediaMsgDel'] = 'Commit message for deleted media files (%user%,%media% are replaced by the corresponding values)'; -$lang['repoPath'] = 'Path of the git repo(s) (e.g. the savedir '.$GLOBALS['conf']['savedir'].')'; +$lang['repoPath'] = 'Path of the git repo for pages and media by default (e.g. the savedir '.$GLOBALS['conf']['savedir'].')'; $lang['repoWorkDir'] = 'Path of the git working tree, must contain "pages" and "media" directories (e.g. the savedir '.$GLOBALS['conf']['savedir'].')'; +$lang['repoPathMedia'] = 'Optional: Path of a dedicated git repo for media. If this is empty, the repo defined by repoPath will be used for media as well.'; +$lang['repoWorkDirMedia'] = 'Optional: Path of the git working tree for the media repo. If this is empty, the repo defined by repoWorkDir will be used'; +$lang['repoPathConf']= 'Optional: Path of the git repo for the DokuWiki config. If this empty, no config changes will be commited'; +$lang['repoPathCode']= 'Optional: Path of the git repo for the PHP code of the installation. If this empty, no installation changes will be commited'; $lang['gitPath'] = 'Path to the git binary (if empty, the default "/usr/bin/git" will be used)'; $lang['addParams'] = 'Additional git parameters (added to the git execution command) (%user% and %mail% are replaced by the corresponding values)'; $lang['ignorePaths'] = 'Paths/files which are ignored and not added to git (comma-separated)'; diff --git a/lib/Git.php b/lib/Git.php index d09535f..cfd2197 100644 --- a/lib/Git.php +++ b/lib/Git.php @@ -138,7 +138,9 @@ class GitRepo { protected $repo_path = null; protected $bare = false; protected $envopts = array(); - protected ?\action_plugin_gitbacked_editcommit $plugin = null; + // @bugfix for PHP <= 7.3 compatibility: class property type declarations are supported by PHP >= 7.4 only + // protected ?\action_plugin_gitbacked_editcommit $plugin = null; + protected $plugin = null; /** * Create a new git repository @@ -426,10 +428,15 @@ protected function handle_command_success($repo_path, $cwd, $command) { * * @access public * @param bool return string with
+ * @param string status command options * @return string */ - public function status($html = false) { - $msg = $this->run("status"); + public function status($html = false, $options = '') { + $cmd = 'status'; + if (!empty($options)) { + $cmd .= ' '.$options; + } + $msg = $this->run($cmd); if ($html == true) { $msg = str_replace("\n", "
", $msg); } @@ -452,6 +459,16 @@ public function add($files = "*") { return $this->run("add $files -v"); } + /** + * Runs a `git add -A` call + * + * @access public + * @return string + */ + public function addAll() { + return $this->run("add -A -v"); + } + /** * Runs a `git rm` call * diff --git a/plugin.info.txt b/plugin.info.txt index 08e56b4..0cea3fe 100644 --- a/plugin.info.txt +++ b/plugin.info.txt @@ -1,7 +1,7 @@ base gitbacked author Wolfgang Gassler (@woolfg), Carsten Teibes (@carstene1ns), Markus Hoffrogge (@mhoffrog) email wolfgang@gassler.org -date 2021-03-19 +date 2021-03-28 name gitbacked plugin desc Pages and Media are stored in Git url https://github.com/woolfg/dokuwiki-plugin-gitbacked