/dev/null`), SVN_MIN_VERSION, '>'); } /** * @param $path * @return object */ function get_info($path) { $esc = escapeshellarg($path); $info = @simplexml_load_string(`svn info --xml $esc 2> /dev/null`); return $info; } function get_revision($path) { return get_info($path)->entry->commit['revision']; } /** * @param $url * @return bool */ function is_valid_merge_destination($url) { return is_trunk($url) || is_experimental($url); } /** * @param $destination * @param $source * @return bool */ function is_valid_merge_source($destination, $source) { if (is_trunk($destination)) { return is_stable($source); } if (is_experimental($destination)) { return is_trunk($source); } return false; } /** * @param $branch * @return bool */ function is_valid_branch($branch) { return is_stable($branch) || is_experimental($branch); } /** * @param $branch * @return bool */ function is_stable($branch) { return dirname($branch) == full('branches') && (preg_match("/^\d+\.[\dx]+$/", basename($branch)) || preg_match("/test$/", basename($branch))); } /** * @param $branch * @return bool */ function is_experimental($branch) { return dirname($branch) == full('branches/experimental'); } /** * @param $branch * @return bool */ function is_trunk($branch) { return $branch == full('trunk'); } /** * @param $localPath * @param bool $ignore_externals */ function update_working_copy($localPath, $ignore_externals = false) { $localPath = escapeshellarg($localPath); $ignoreStr = $ignore_externals ? ' --ignore-externals' : ''; `svn up $localPath$ignoreStr`; } /** * Indicates whether versioned files have been modified in the specified checkout * * @param $localPath string Path of the checkout * @return bool true if at least 1 versioned file has been modified, added or removed, false otherwise * * @see Similar function svn_files_identical() */ function has_uncommited_changes($localPath) { $localPath = escapeshellarg($localPath); $dom = new DOMDocument(); $dom->loadXML(`svn status --xml $localPath`); $xp = new DOMXPath($dom); $count = $xp->query("/status/target/entry/wc-status[@item = 'added' or @item = 'conflicted' or @item = 'deleted' or @item = 'modified' or @item = 'replaced']"); return $count->length > 0; } /** * Get the number of changes in the specified checkout * * @param string $localPath Path of the checkout * @return array The files that differ (additions, removals and modifications) from the repository * * * @see Similar function has_uncommited_changes() */ function files_differ($localPath) { $localPath = escapeshellarg($localPath); $dom = new DOMDocument(); $dom->loadXML(`svn status --xml $localPath`); $entries = []; foreach ($dom->getElementsByTagName('entry') as $entry) { // anything modified or added gets added to the excludes for secdb if ($entry->getElementsByTagName('wc-status')->length) { $entries[$entry->getAttribute('path')] = $entry->getElementsByTagName('wc-status')[0]->getAttribute('item'); } else { $entries[$entry->getAttribute('path')] = ''; } } return $entries; } /** * @param $localPath * @return DOMNodeList */ function get_conflicts($localPath) { $localPath = escapeshellarg($localPath); $dom = new DOMDocument(); $dom->loadXML(`svn status --xml $localPath`); $xp = new DOMXPath($dom); $list = $xp->query("/status/target/entry/wc-status[@item = 'conflicted']"); return $list; } /** * @param $path * @param $source * @return int */ function find_last_merge($path, $source) { $short = preg_quote(short($source), '/'); $pattern = "/^\\[(MRG|BRANCH)\\].*$short'?\s+\d+\s+to\s+(\d+)/"; $descriptorspec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], ]; $ePath = escapeshellarg($path); $process = proc_open("svn log --stop-on-copy " . TIKIVCS, $descriptorspec, $pipes); $rev = 0; $c = 0; if (is_resource($process)) { $fp = $pipes[1]; while (! feof($fp)) { $line = fgets($fp, 1024); if (preg_match($pattern, $line, $parts)) { $rev = (int)$parts[2]; break; } $c++; if ($c > 100000) { error("[MRG] or [BRANCH] message for '$source' not found in 1000000 lines of logs, something has gone wrong..."); break; } } fclose($fp); proc_close($process); } return $rev; } /** * @param $localPath * @param $source * @param $from * @param $to */ function merge($localPath, $source, $from, $to) { $short = short($source); $source = escapeshellarg($source); $from = (int)$from; $to = (int)$to; passthru("svn merge $source -r$from:$to"); $message = "[MRG] Automatic merge, $short $from to $to"; file_put_contents('svn-commit.tmp', $message); } function add($file) { `svn add $file`; } /** * @param $msg * @param bool $displaySuccess * @param bool $dieOnRemainingChanges * @return int */ function commit($msg, $displaySuccess = true, $dieOnRemainingChanges = true) { $msg = escapeshellarg($msg); `svn ci -m $msg`; if ($dieOnRemainingChanges && has_uncommited_changes('.')) { error("Commit seems to have failed. Uncommited changes exist in the working folder.\n"); } return (int)get_info('.')->entry->commit['revision']; } /** * Commit lang files * @param $msg * @param bool $displaySuccess * @param bool $dieOnRemainingChanges * @return int */ function commit_lang($msg, $displaySuccess = true, $dieOnRemainingChanges = true) { $msg = escapeshellarg($msg); `svn ci ./lang -m $msg`; if ($dieOnRemainingChanges && has_uncommited_changes('./lang')) { error("Commit seems to have failed. Uncommited changes exist in the working folder.\n"); } return (int)get_info('./lang')->entry->commit['revision']; } function commit_specific_lang($lang, $msg, $displaySuccess = true, $dieOnRemainingChanges = true) { $msg = escapeshellarg($msg); exec("svn ci ./lang/$lang -m $msg"); if ($dieOnRemainingChanges && has_uncommited_changes("./lang/$lang")) { error("Commit seems to have failed. Uncommited changes exist in the working folder.\n"); } return (int) get_info("./lang/$lang")->entry->commit['revision']; } /** * @param $working * @param $source */ function incorporate($working, $source) { $working = escapeshellarg($working); $source = escapeshellarg($source); passthru($command = "svn merge $working $source"); } /** * @param $source * @param $branch * @param $revision * @return bool */ function branch($source, $branch, $revision) { $short = short($branch); $file = escapeshellarg("$branch/tiki-index.php"); $source = escapeshellarg($source); $branch = escapeshellarg($branch); $message = escapeshellarg("[BRANCH] Creation, $short 0 to $revision"); `svn copy $source $branch -m $message`; $f = @simplexml_load_string(`svn info --xml $file`); return isset($f->entry); } /** * @param $localPath * @param $minRevision * @param string $maxRevision * @return bool */ function get_logs($localPath, $minRevision, $maxRevision = 'HEAD') { if (empty($minRevision) || empty($maxRevision)) { return false; } $logs = `LANG=C svn log -r$maxRevision:$minRevision $localPath`; return $logs; } /** * Find the revision number for a particular tag * * @param $releaseNumber * @return int */ function get_tag_revision($releaseNumber) { $revision = 0; // --stop-on-copy makes it only return the tag commit, not the whole history since time began $log = `LANG=C svn log --stop-on-copy ^/tags/$releaseNumber/`; if (preg_match('/^r(\d+)/ms', $log, $matches)) { $revision = (int)$matches[1]; } return $revision; } /** * Get contributors * @param $path * @param $contributors * @param $minRevision * @param $maxRevision * @param int $step */ function get_contributors($path, &$contributors, $minRevision, $maxRevision, $step = 20000) { $minByStep = max($maxRevision - $step, $minRevision); $lastLogRevision = $maxRevision; echo "\rRetrieving logs from revision $minByStep to $maxRevision ...\t\t\t"; $logs = get_logs($path, $minByStep, $maxRevision); if (preg_match_all('/^r(\d+) \|\s([^\|]+)\s\|\s(\d+-\d+-\d+)\s.*\n\n(.*)\-+\n/Ums', $logs, $matches, PREG_SET_ORDER)) { foreach ($matches as $logEntry) { $mycommits[$logEntry[1]] = [$logEntry[2], $logEntry[3]]; } krsort($mycommits); foreach ($mycommits as $commitnum => $commitinfo) { if ($lastLogRevision > 0 && $commitnum != $lastLogRevision - 1 && $lastLogRevision != $maxRevision) { print "\nProblem with commit " . ($lastLogRevision - 1) . "\n (trying {$commitnum} after $lastLogRevision)"; die; } $lastLogRevision = $commitnum; $author = strtolower($commitinfo[0]); // Remove empty author or authors like (no author), which may be translated depending on server locales if (empty($author) || $author[0] == '(') { continue; } if (! isset($contributors[$author])) { $contributors[$author] = []; } $contributors[$author]['Author'] = $commitinfo[0]; $contributors[$author]['First Commit'] = $commitinfo[1]; if (isset($contributors[$author]['Number of Commits'])) { $contributors[$author]['Number of Commits']++; } else { $contributors[$author]['Last Commit'] = $commitinfo[1]; $contributors[$author]['Number of Commits'] = 1; } } } if ($lastLogRevision > $minRevision) { get_contributors($path, $contributors, $minRevision, $lastLogRevision - 1, $step); } } /** * Verify if a tag exists * @param $tag * @param bool $remote * @return bool */ function tag_exists($tag, $remote = false) { if ($remote) { $tag = full($tag); } return isset(get_info($tag)->entry); } function delete_file($file, $message = null) { `svn delete $file --force`; } /** * Delete a tag * @param $tag * @param $commit_msg */ function delete_tag($tag, $commit_msg) { $tag = full($tag); `svn rm $tag -m "$commit_msg"`; } /** * Create a new tag * @param $tag * @param $commitMsg * @param $branch * @param $revision */ function create_tag($tag, $commitMsg, $branch, $revision) { $tag = full($tag); $branch = full($branch); `svn copy $branch -r$revision $tag -m "$commitMsg"`; } /** * Export a svn project * @param $source * @param $dest * @return string|null */ function export($source, $dest) { return shell_exec('svn export ' . escapeshellarg($source) . ' ' . escapeshellarg($dest . '/.') . ' 2>&1'); }