diff --git a/action/editcommit.php b/action/editcommit.php index 91b68a3..eeb4469 100644 --- a/action/editcommit.php +++ b/action/editcommit.php @@ -50,24 +50,81 @@ public function register(EventHandler $controller) $controller->register_hook('DOKUWIKI_DONE', 'AFTER', $this, 'handlePeriodicPull'); } - private function initRepo() + /** + * Create a GitRepo class instance according to this plugins config. + * If auto determination of git rpos is configured, this method will return null, + * if there is no git repo found. + * + * @access private + * @param string path to the file or directory to be commited (required for auto determination only) + * @return GitRepo instance or null if there is no repo related to fileOrDirPath + */ + private function initRepo($fileOrDirPath = "") { - //get path to the repo root (by default DokuWiki's savedir) - $repoPath = GitBackedUtil::getEffectivePath($this->getConf('repoPath')); + global $conf; + + //set the path to the git binary $gitPath = trim($this->getConf('gitPath')); if ($gitPath !== '') { Git::setBin($gitPath); } - //init the repo and create a new one if it is not present - io_mkdir_p($repoPath); - $repo = new GitRepo($repoPath, $this, true, true); - //set git working directory (by default DokuWiki's savedir) - $repoWorkDir = $this->getConf('repoWorkDir'); - if (!empty($repoWorkDir)) { - $repoWorkDir = GitBackedUtil::getEffectivePath($repoWorkDir); + + $configuredRepoPath = trim($this->getConf('repoPath')); + $configuredRepoWorkDir = trim($this->getConf('repoWorkDir')); + if (!empty($configuredRepoPath)) { + $configuredRepoPath = GitBackedUtil::getEffectivePath($configuredRepoPath); + } + if (!empty($configuredRepoWorkDir)) { + $configuredRepoWorkDir = GitBackedUtil::getEffectivePath($configuredRepoWorkDir); + } + $isAutoDetermineRepos = $this->getConf('autoDetermineRepos'); + if ($isAutoDetermineRepos) { + if (empty($fileOrDirPath)) { + return null; + } + $repoPath = is_dir($fileOrDirPath) ? $fileOrDirPath : dirname($fileOrDirPath); + $repo = new GitRepo($repoPath, $this, false, false); + $repoPath = $repo->get_repo_path(); + if (empty($repoPath)) { + return null; + } + // Validate that the git repoPath found is within or below the DokuWiki 'savedir' configured: + if (strpos(realpath($repoPath), realpath($conf['savedir'])) === false) { + //dbglog("GitBacked - WARNING: repoPath=" . $repoPath . " is above the configured savedir=" + // . realpath($conf['savedir']) . " => this git repo will be ignored!" + //); + return null; + } + $repoWorkDir = ''; + if (!empty($configuredRepoPath)) { + // For backward compatibility to legacy configuration: + // We will use the configured workDir, in case we have determined + // the repoPath configured. + if (realpath($configuredRepoPath) === realpath($repoPath)) { + $repoWorkDir = $configuredRepoWorkDir; + //dbglog("GitBacked - INFO: repoPath=" . $repoPath + // . " is the one explicitly configured => we use the configured workDir=[" + // . $repoWorkDir . "]" + //); + } + } + //dbglog("GitBacked - AUTO_DETERMINE_USE_CASE: repoPath=" . $repoPath); + //dbglog("GitBacked - AUTO_DETERMINE_USE_CASE: repoWorkDir=" . $repoWorkDir); + } else { + //get path to the repo root from configuration (by default DokuWiki's savedir) + $repoPath = $configuredRepoPath; + //init the repo and create a new one if it is not present + io_mkdir_p($repoPath); + $repo = new GitRepo($repoPath, $this, true, true); + //set git working directory from configuration (by default DokuWiki's savedir) + $repoWorkDir = $configuredRepoWorkDir; + //dbglog("GitBacked - CONFIG_USE_CASE: configured repoPath=" . $repoPath); + //dbglog("GitBacked - CONFIG_USE_CASE: configured repoWorkDir=" . $repoWorkDir); } + Git::setBin(empty($repoWorkDir) ? Git::getBin() : Git::getBin() . ' --work-tree ' . escapeshellarg($repoWorkDir)); + $params = str_replace( ['%mail%', '%user%'], [$this->getAuthorMail(), $this->getAuthor()], @@ -98,8 +155,10 @@ private function commitFile($filePath, $message) { if (!$this->isIgnored($filePath)) { try { - $repo = $this->initRepo(); - + $repo = $this->initRepo($filePath); + if (is_null($repo)) { + return; + } //add the changed file and set the commit message $repo->add($filePath); $repo->commit($message); @@ -178,6 +237,9 @@ public function handlePeriodicPull(Event &$event, $param) if ($lastPull + $timeToWait < $now) { try { $repo = $this->initRepo(); + if (is_null($repo)) { + return; + } if ($enableIndexUpdate) { $localPath = $this->computeLocalPath(); diff --git a/classes/GitRepo.php b/classes/GitRepo.php index b389115..f52426e 100644 --- a/classes/GitRepo.php +++ b/classes/GitRepo.php @@ -120,11 +120,15 @@ public function setRepoPath($repo_path, $create_new = false, $_init = true) if ($new_path = realpath($repo_path)) { $repo_path = $new_path; if (is_dir($repo_path)) { + $next_parent_repo_path = $this->absoluteGitDir($repo_path); + if (!empty($next_parent_repo_path)) { + $this->repo_path = $next_parent_repo_path; + $this->bare = false; // Is this a work tree? - if (file_exists($repo_path . "/.git") && is_dir($repo_path . "/.git")) { + } elseif (file_exists($repo_path . "/.git") && is_dir($repo_path . "/.git")) { $this->repo_path = $repo_path; $this->bare = false; - // Is this a bare repo? + // Is this a bare repo? } elseif (is_file($repo_path . "/config")) { $parse_ini = parse_ini_file($repo_path . "/config"); if ($parse_ini['bare']) { @@ -136,6 +140,11 @@ public function setRepoPath($repo_path, $create_new = false, $_init = true) if ($_init) { $this->run('init'); } + } elseif (!$_init) { + // If we do not have to init the repo, we just reflect that there is no repo path yet. + // This may be the case for auto determining repos, + // if there is no repo related to the current resource going to be commited. + $this->repo_path = ''; } else { throw new \Exception($this->handleRepoPathError( $repo_path, @@ -168,6 +177,17 @@ public function setRepoPath($repo_path, $create_new = false, $_init = true) } } + /** + * Get the path to the repo directory + * + * @access public + * @return string + */ + public function getRepoPath() + { + return $this->repo_path; + } + /** * Get the path to the git repo directory (eg. the ".git" directory) * @@ -201,6 +221,47 @@ public function testGit() return ($status != 127); } + /** + * Determine closest parent git repository for a given path as absolute PHP realpath(). + * + * @access public + * @return string the next parent git repo root dir as absolute PHP realpath() + * or empty string, if no parent repo found. + */ + public function absoluteGitDir($path) + { + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $pipes = []; + // Using --git-dir rather than --absolute-git-dir for a wider git versions compatibility + //$command = Git::getBin() . " rev-parse --absolute-git-dir"; + $command = Git::getBin() . " rev-parse --git-dir"; + //dbglog("GitBacked - Command: ".$command); + $resource = proc_open($command, $descriptorspec, $pipes, $path); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + foreach ($pipes as $pipe) { + fclose($pipe); + } + + $status = trim(proc_close($resource)); + if ($status == 0) { + $repo_git_dir = trim($stdout); + //dbglog("GitBacked - $command: '" . $repo_git_dir . "'"); + if (!empty($repo_git_dir)) { + if (strcmp($repo_git_dir, ".git") === 0) { + // convert to absolute path based on this command execution directory + $repo_git_dir = $path . '/' . $repo_git_dir; + } + $repo_path = dirname(realpath($repo_git_dir)); + //dbglog('GitBacked - $repo_path: ' . $repo_path); + if (file_exists($repo_path . "/.git") && is_dir($repo_path . "/.git")) { + return $repo_path; + } + } + } + return ''; + } + /** * Run a command in the git repository * @@ -212,6 +273,13 @@ public function testGit() */ protected function runCommand($command) { + //dbglog("Git->run_command: repo_path=[" . $this->repo_path . "])"); + if (empty($this->repo_path)) { + throw new \Exception($this->handleRepoPathError( + $this->repo_path, + "Failure on GitRepo->runCommand(): Git command must not be run for an empty repo path" + )); + } //dbglog("Git->runCommand(command=[".$command."])"); $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; $pipes = []; diff --git a/conf/default.php b/conf/default.php index 72909cd..700c0cc 100644 --- a/conf/default.php +++ b/conf/default.php @@ -14,7 +14,8 @@ $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['repoPath'] = $GLOBALS['conf']['savedir']; +$conf['autoDetermineRepos'] = 1; +$conf['repoPath'] = ''; //$GLOBALS['conf']['savedir'] $conf['repoWorkDir'] = ''; $conf['gitPath'] = ''; $conf['addParams'] = '-c user.name="%user%" -c user.email="<%mail%>"'; diff --git a/conf/metadata.php b/conf/metadata.php index 0cf783b..d3c1405 100644 --- a/conf/metadata.php +++ b/conf/metadata.php @@ -14,6 +14,7 @@ $meta['commitPageMsgDel'] = array('string'); $meta['commitMediaMsg'] = array('string'); $meta['commitMediaMsgDel'] = array('string'); +$meta['autoDetermineRepos'] = array('onoff'); $meta['repoPath'] = array('string'); $meta['repoWorkDir'] = array('string'); $meta['gitPath'] = array('string'); diff --git a/lang/de/settings.php b/lang/de/settings.php index 4c12e33..3bdc051 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -14,8 +14,9 @@ $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['autoDetermineRepos'] = 'Findet das nächste git Repo oberhalb des Pfades der geänderten Datei. Wenn gesetzt, dann werden mehrere Repos z.B. in Namespaces oder separate Repos für Pages und Media generisch unterstützt.'; +$lang['repoPath'] = 'Veraltete Konfiguration: Pfad des git Repo (z.B. das savedir $GLOBALS[\'conf\'][\'savedir\'])
Hinweis: Diese Einstellung ist nur für Rückwärtskompatibilität einer vorhandenen Konfiguration gedacht. Wenn autoDetermineRepos aktiviert ist, dann sollte diese Einstellung für neue Installationen nicht gesetzt werden.'; +$lang['repoWorkDir'] = 'Veraltete Konfiguration: Pfad des git working tree. Dieser muss die "pages" and "media" Verzeichnisse enthalten (z.B. das savedir $GLOBALS[\'conf\'][\'savedir\'])
Hinweis: Diese Einstellung wird nur berücksichtigt, wenn repoPath gesetzt ist. In diesem Fall wird es nur für das Repo in repoPath angewandt.'; $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 1486e36..0211093 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -14,8 +14,9 @@ $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['repoWorkDir'] = 'Path of the git working tree, must contain "pages" and "media" directories (e.g. the savedir ' . $GLOBALS['conf']['savedir'] . ')'; +$lang['autoDetermineRepos'] = 'Auto determine the next git repo path upwards from the path of the file to commit. If enabled, then multiple repos e.g. within namespaces or separate repos for pages and media are supported in a generic way.'; +$lang['repoPath'] = 'Legacy config: Path of the git repo (e.g. the savedir $GLOBALS[\'conf\'][\'savedir\'])
NOTE: This config is for backward compatibility of an existing configuration only. If autoDetermineRepos is on, then this config should not be set for new installations.'; +$lang['repoWorkDir'] = 'Legacy config: Path of the git working tree, must contain "pages" and "media" directories (e.g. the savedir $GLOBALS[\'conf\'][\'savedir\'])
NOTE: This config is considered only, if repoPath is set. In this case it does apply for the repo in repoPath only.'; $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)';