├── hooks ├── post-receive ├── post-receive.mirror ├── post-receive.mail ├── post-receive.bugsweb ├── commit-bugs.php └── pre-receive ├── README ├── lib ├── Git.php ├── Git │ ├── BugsWebPostReceiveHook.php │ ├── PreReceiveHook.php │ ├── PushInformation.php │ ├── ReceiveHook.php │ └── PostReceiveHook.php └── Mail.php └── README.POST_RECEIVE_MAIL /hooks/post-receive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | in=$(cat) 4 | 5 | [ -f 'hooks/post-receive.mail' ] && (echo -n "$in" | hooks/post-receive.mail ) 6 | [ -f 'hooks/post-receive.bugsweb' ] && (echo -n "$in" | hooks/post-receive.bugsweb ) 7 | [ -f 'hooks/post-receive.mirror' ] && hooks/post-receive.mirror 8 | -------------------------------------------------------------------------------- /hooks/post-receive.mirror: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # simple gitolite mirroring 4 | 5 | # flush STDIN coming from git, because gitolite's own post-receive.mirrorpush 6 | # script does the same thing 7 | [ -t 0 ] || cat >/dev/null 8 | 9 | target=`git config --get gitolite.mirror.simple` 10 | [ -z "$target" ] && exit 0 11 | 12 | 13 | if $(echo $target | grep -q "REPO"); 14 | then 15 | GL_REPO=$(pwd | perl -e '<> =~ /\/git\/repositories\/(.*?)\.git$/; print $1;' | sed s,/,-,g) 16 | # Support a REPO variable for wildcard mirrors 17 | target=$(echo $target | sed -e "s,REPO,$GL_REPO,g") 18 | fi 19 | 20 | echo "Attempting to push to mirror $target" 21 | # Do the mirror push 22 | git push --mirror $target 23 | 24 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | The git repositories at git.php.net are handled by gitolite. 2 | 3 | Import Steps: 4 | 5 | * Deny write access in SVN. Edit SVNROOT/pre-commit 6 | * Import repository from SVN using svn2git. 7 | * Change the repo name in global_avail 8 | 9 | New Repository: 10 | 11 | * Clone git.php.net:gitolite-admin.git 12 | * Edit conf/gitolite.conf 13 | - add your repository to @public, or @web 14 | * Create the repository on gitweb with the same name. 15 | * Commit 16 | * Push 17 | * Test it 18 | 19 | For Webstuff: 20 | 21 | * replace svn checkout on rsync with git checkout 22 | * edit update-mirrors in systems.git 23 | 24 | Mirroring 25 | --------- 26 | * Mirroring is done by a post-receive hook 27 | https://github.com/miracle2k/gitolite-simple-mirror 28 | 29 | -------------------------------------------------------------------------------- /lib/Git.php: -------------------------------------------------------------------------------- 1 | enableLog(); 58 | $hook->process(); 59 | } 60 | -------------------------------------------------------------------------------- /lib/Git/BugsWebPostReceiveHook.php: -------------------------------------------------------------------------------- 1 | hookInput(); 10 | 11 | $paths = []; 12 | foreach ($this->refs as $ref) { 13 | if ($ref['reftype'] == self::REF_BRANCH) { 14 | $paths[] = $this->getReceivedMessagesForRange($ref['old'], $ref['new']); 15 | } 16 | } 17 | 18 | /* remove empty lines, and flattern the array */ 19 | $flattend = array_reduce($paths, 'array_merge', []); 20 | $paths = array_filter($flattend); 21 | 22 | return array_unique($paths); 23 | } 24 | 25 | /** 26 | * Returns an array of commit messages between revision $old and $new. 27 | * 28 | * @param string $old The old revison number. 29 | * @parma string $new The new revison umber. 30 | * 31 | * @return array 32 | */ 33 | private function getReceivedMessagesForRange($old, $new) 34 | { 35 | $repourl = \Git::getRepositoryPath(); 36 | $output = []; 37 | 38 | if ($old == \Git::NULLREV) { 39 | $cmd = sprintf( 40 | "%s --git-dir=%s for-each-ref --format='%%(refname)' 'refs/heads/*'", 41 | \Git::GIT_EXECUTABLE, 42 | $repourl 43 | ); 44 | exec($cmd, $heads); 45 | 46 | $not = count($heads) > 0 ? ' --not ' . implode(' ', $this->escapeArrayShellArgs($heads)) : ''; 47 | $cmd = sprintf( 48 | '%s --git-dir=%s log --pretty=format:"[%%ae] %%H %%s" %s %s', 49 | \Git::GIT_EXECUTABLE, 50 | $repourl, 51 | escapeshellarg($new), 52 | $not 53 | ); 54 | exec($cmd, $output); 55 | } elseif ($new != \Git::NULLREV) { 56 | $cmd = sprintf( 57 | '%s --git-dir=%s log --pretty=format:"[%%ae] %%H %%s" %s..%s', 58 | \Git::GIT_EXECUTABLE, 59 | $repourl, 60 | escapeshellarg($old), 61 | escapeshellarg($new) 62 | ); 63 | exec($cmd, $output); 64 | } 65 | 66 | return $output; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/Git/PreReceiveHook.php: -------------------------------------------------------------------------------- 1 | karmaFile = $karma_file; 13 | } 14 | 15 | /** 16 | * Returns true if git option karma.ignored is set, otherwise false. 17 | * 18 | * @return boolean 19 | */ 20 | public function isKarmaIgnored() 21 | { 22 | return 'true' === \Git::gitExec('config karma.ignored'); 23 | } 24 | 25 | public function mapInput(callable $fn) 26 | { 27 | $result = []; 28 | foreach($this->refs as $input) { 29 | $result[] = $fn($input['old'], $input['new'], $input['refname']); 30 | } 31 | 32 | return $result; 33 | } 34 | 35 | /** 36 | * Return the content of the karma file from the karma repository. 37 | * 38 | * We read the content of the karma file from the karma repository index. 39 | * 40 | * @return string 41 | */ 42 | public function getKarmaFile() 43 | { 44 | return file($this->karmaFile); 45 | } 46 | 47 | public function getReceivedPaths() 48 | { 49 | // escaped branches 50 | $allBranches =$this->escapeArrayShellArgs($this->getAllBranches()); 51 | 52 | $paths = array_map( 53 | function ($input) use ($allBranches) { 54 | $paths = []; 55 | 56 | if ($input['changetype'] == self::TYPE_CREATED) { 57 | $paths = $this->getChangedPaths(escapeshellarg($input['new']) . ' --not ' . implode(' ', $allBranches)); 58 | } elseif ($input['changetype'] == self::TYPE_UPDATED) { 59 | $paths = $this->getChangedPaths(escapeshellarg($input['old'] . '..' . $input['new'])); 60 | } else { 61 | // deleted branch. we also need some paths 62 | // to check karma 63 | } 64 | 65 | return array_keys($paths); 66 | }, 67 | $this->refs 68 | ); 69 | 70 | /* flattern the array */ 71 | $paths = array_reduce($paths, 'array_merge', []); 72 | 73 | 74 | return array_unique($paths); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /hooks/post-receive.bugsweb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getReceivedMessages(); 38 | 39 | $template = "Automatic comment from GIT on behalf of %s 40 | Revision: http://git.php.net/%s 41 | Log: %s"; 42 | 43 | foreach ($rpath as $commit) { 44 | // should look like this: "[user@php.net] 0df3233344 Fixed bug #1234 It works!" 45 | $pattern = '(\[([^\]]+)\] ([0-9a-f]+)(.*Fix(ed)? (bug )?\#([0-9]+)(.*)))i'; 46 | preg_match($pattern, $commit, $matches); 47 | 48 | if (!isset($matches[1])) { 49 | continue; 50 | } 51 | 52 | $committer = $matches[1]; 53 | $commitHash = $matches[2]; 54 | $commitMsg = trim($matches[3]); 55 | $bugNumber = $matches[6]; 56 | $shortMsg = trim($matches[7]); 57 | if (strlen($shortMsg) > 0) { 58 | if ($shortMsg[0] == '(') { 59 | $shortMsg = substr($shortMsg, 1); 60 | } 61 | if ($shortMsg[strlen($shortMsg)-1] == ')') { 62 | $shortMsg = substr($shortMsg, 0, -1); 63 | } 64 | } 65 | 66 | $output = sprintf( 67 | $template, 68 | $committer, 69 | $commitHash, 70 | $commitMsg 71 | ); 72 | 73 | $commit_info = array(); 74 | $commit_info['log_message'] = $commitMsg; 75 | $commit_info['author'] = $committer; 76 | $commit_info['author'] = preg_replace("#@php\.net$#", "", $committer); 77 | $commit_info['user'] = $user; 78 | $viewvc_url_prefix = sprintf( 79 | 'http://git.php.net/?p=%s;a=commit;h=', 80 | $hook->getRepositoryName() 81 | ); 82 | $REV = $commitHash; 83 | require __DIR__ . '/commit-bugs.php'; 84 | } 85 | -------------------------------------------------------------------------------- /lib/Git/PushInformation.php: -------------------------------------------------------------------------------- 1 | hook = $hook; 12 | } 13 | 14 | /** 15 | * Returns the common ancestor revision for two given revisions 16 | * 17 | * Returns false if no sha1 was returned. Throws an exception if calling 18 | * git fails. 19 | * 20 | * @return boolean 21 | */ 22 | protected function mergeBase($oldrev, $newrev) 23 | { 24 | $baserev = \Git::gitExec('merge-base %s %s', escapeshellarg($oldrev), escapeshellarg($newrev)); 25 | 26 | $baserev = trim($baserev); 27 | 28 | 29 | if (40 != strlen($baserev)) { 30 | return false; 31 | } 32 | 33 | return $baserev; 34 | } 35 | 36 | /** 37 | * Returns true if merging $newrev would be fast forward 38 | * 39 | * @return boolean 40 | */ 41 | public function isFastForward() 42 | { 43 | $result = $this->hook->mapInput( 44 | function ($oldrev, $newrev) { 45 | if ($oldrev == \Git::NULLREV) { 46 | return true; 47 | } 48 | return $oldrev == $this->mergeBase($oldrev, $newrev); 49 | }); 50 | 51 | return array_reduce($result, function($a, $b) { return $a && $b; }, true); 52 | } 53 | 54 | /** 55 | * Returns true if updating the refs would fail if push is not forced. 56 | * 57 | * @return boolean 58 | */ 59 | public function isForced() 60 | { 61 | return !$this->isFastForward(); 62 | } 63 | 64 | public function isNewBranch() 65 | { 66 | $result = $this->hook->mapInput( 67 | function($oldrev, $newrev) { 68 | return $oldrev == \Git::NULLREV; 69 | }); 70 | return array_reduce($result, function($a, $b) { return $a || $b; }, false); 71 | } 72 | 73 | public function isTag() 74 | { 75 | $result = $this->hook->mapInput( 76 | function($oldrev, $newrev, $refname) { 77 | if (preg_match('@^refs/tags/.+@i', $refname)) { 78 | return true; 79 | } 80 | return false; 81 | }); 82 | 83 | return array_reduce($result, function($a, $b) { return $a || $b; }, false); 84 | } 85 | 86 | public function getBranches () { 87 | return $this->hook->mapInput(function ($oldrev, $newrev, $refname) { 88 | return $refname; 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /README.POST_RECEIVE_MAIL: -------------------------------------------------------------------------------- 1 | Post receive mail script solutions. 2 | 3 | 4 | digits - commits, 5 | chars - branches (except N & O) 6 | N, O - new and old revisions in push per branch. 7 | 8 | ------------ 9 | Branch mail: 10 | This part contains info only about mail per branch. For mail per commit logic different. 11 | 12 | 13 | New branch (0..N): 14 | 15 | 1 - 2 - A (already on server) 16 | \ 17 | 3 - B (pushed) 18 | 19 | We get all commits reachable from new branch and not from all other NOT NEW branches: 20 | git rev-list B --not A -> 3-B 21 | 22 | 23 | 1 - 2 - A (only local) 24 | \ 25 | 3 - B (pushed) 26 | 27 | Because A branch not pushed on server before we need mail about all commits including 1-2. e.g. 1-B 28 | 29 | 30 | 1 - 2 - 3 - 4 - A (already on server) 31 | \ / 32 | 5 - 6 - B (pushed) 33 | 34 | We can't know what we need also 5 commit in B, so we mailed only about B commit for this branch. 35 | 2-6 commits was mailed in A branch. 36 | 37 | 38 | 1 - 2 - 3 - A (already on server) 39 | \ \ 40 | 5 - 6 - B (pushed) 41 | We can't know what we need also 3 commit in B mail... any ideas? 42 | Now we send mail with 5-B commits only. 43 | 44 | 45 | 1 - 2 - 3 - A (already on server or pushed) 46 | \ 47 | 4 - 5 - B (pushed) 48 | \ 49 | 6 - C (pushed) 50 | 51 | If we use git rev-list B --not A C we skip 4-5 revisions in B. 52 | So we use git rev-list B --not A. See first situation in "New branch" section. 53 | 54 | 55 | 56 | Deleted branch (O..0): 57 | 58 | We don't care about revisions in deleted branch. (or care?) 59 | 60 | 61 | Update branch (O..N): 62 | 63 | 64 | 1 - 2 - O - 3 - 4 - N 65 | 66 | Maill full log between O and N. (e.g. 3-N) git rev-list O..N 67 | 68 | 69 | 1 - 2 - N 1 - 2 - 3 - 4 - N 70 | \\ or \\ 71 | 3 - 4 - O 5 - 6 - O 72 | 73 | It's mean user used --force option for pull and remove previosly revisions. 74 | (rewind N revision in first case and replace 5-O revisions by 4-N) 75 | We can check it by command: git rev-list N..O. 76 | If result of this command not empty - we have such cases, and this 77 | result is list of discarded revisions. 78 | 79 | 80 | ------------ 81 | Tag mail: 82 | 83 | Add, delete and update tag: 84 | If new(updated) tag is annotated - we write full info about it and target. 85 | If it not annotated - only about commit. 86 | 87 | For old(removed) tag - we write only sha of old commit/tag. 88 | 89 | 90 | ------------ 91 | Commit mail: 92 | (Realization of this part in progress) 93 | 94 | 1 - 2 - 3 - A (pushed, but not new branch) 95 | 96 | 5 - 2 - 6 - B (pushed, but not new branch) 97 | 98 | 3 - 5 - 7 - C (already on server) 99 | 100 | We must check was every commit on repository before or not. 101 | git rev-list --max-count=1 REV --not ALL BRANCHES EXCEPT PUSHED BRANCHES WITH THIS COMMIT 102 | If result empty - this commit already was pushed in one of others repository. 103 | Examples: 104 | for 2 rev : we send mail 105 | for 3 rev : we not send mail, because it already in C branch 106 | 107 | 108 | 1 - 2 - 3 - A (pushed) 109 | \ 110 | 4 - 5 - B (pushed, new) 111 | 112 | 6 - 7 - 8 - C (already on server) 113 | 114 | We must mark 1 commit as commit in branches A and B. 115 | If we didn't it and run "git rev-list --max-count=1 sha_for_1_commit --not B C" we get wrong result, 116 | because commit 1 also in B branch. 117 | For this we get "old" revision of every updated branch and check if it placed in every new branches. 118 | If it is we mark all revisions between "old" of updated branch and "new" of new branch as revision also from new branch. 119 | So we get this command "git rev-list --max-count=1 sha_for_1_commit --not C" 120 | -------------------------------------------------------------------------------- /hooks/commit-bugs.php: -------------------------------------------------------------------------------- 1 | 'http://pear.php.net/bugs', 12 | 'pecl' => 'https://bugs.php.net', 13 | 'php' => 'https://bugs.php.net', 14 | '' => 'https://bugs.php.net', 15 | ); 16 | $bug_rpc_url = 'https://bugs.php.net/rpc.php'; 17 | //$viewvc_url_prefix = 'http://svn.php.net/viewvc/?view=revision&revision='; 18 | 19 | // ----------------------------------------------------------------------------------------------------------------------------- 20 | // Get the list of mentioned bugs from the commit log 21 | if (preg_match_all($bug_pattern, $commit_info['log_message'], $matched_bugs, PREG_SET_ORDER) < 1) { 22 | // If no bugs matched, we don't have to do anything. 23 | return; 24 | } 25 | 26 | // ----------------------------------------------------------------------------------------------------------------------------- 27 | // Pick the default bug project out the of the path in the first changed dir 28 | /* 29 | switch (strtolower(substr($commit_info['dirs_changed'][0], 0, 4))) { 30 | case 'pear': 31 | $bug_project_default = 'pear'; 32 | break; 33 | case 'pecl': 34 | $bug_project_default = 'pecl'; 35 | break; 36 | default: 37 | $bug_project_default = ''; 38 | break; 39 | } 40 | */ 41 | $bug_project_default = ''; 42 | 43 | // ----------------------------------------------------------------------------------------------------------------------------- 44 | // Process the matches 45 | $bug_list = array(); 46 | foreach ($matched_bugs as $matched_bug) { 47 | $bug = array(); 48 | $bug['project'] = $matched_bug[1] === "" ? $bug_project_default : strtolower($matched_bug[1]); 49 | $bug['number'] = intval($matched_bug[2]); 50 | $bugid = $bug['project'] . $bug['number']; 51 | $url_prefix = isset($bug_url_prefixes[$bug['project']]) ? $bug_url_prefixes[$bug['project']] : $bug_url_prefixes['']; 52 | $bug['url'] = $url_prefix . '/' . $bug['number']; 53 | $bug['status'] = 'unknown'; 54 | $bug['short_desc'] = ''; 55 | $bug_list[$bugid] = $bug; 56 | } 57 | 58 | // ----------------------------------------------------------------------------------------------------------------------------- 59 | // Make an RPC call for each bug 60 | include __DIR__ . '/secret.inc'; 61 | foreach ($bug_list as $k => $bug) { 62 | if (!in_array($bug["project"], array("php", "pecl", ""))) { 63 | continue; 64 | } 65 | 66 | $comment = "Automatic comment on behalf of {$commit_info['author']}\n" . 67 | "Revision: {$viewvc_url_prefix}{$REV}\n" . 68 | "Log: {$commit_info['log_message']}\n"; 69 | 70 | $postdata = array( 71 | 'user' => $commit_info['user'], 72 | 'id' => $bug['number'], 73 | 'ncomment' => $comment, 74 | 'MAGIC_COOKIE' => $SVN_MAGIC_COOKIE, 75 | ); 76 | 77 | if (preg_match('/Fix(?:ed)?\s*(?:bug\s*)?#'. $bug['number'] .'\b/i', $commit_info['log_message'])) { 78 | /* Request the automatic closing of the bug report */ 79 | $postdata['status'] = 'Closed'; 80 | } 81 | 82 | if ($is_DEBUG) { 83 | unset($postdata['ncomment']); 84 | $postdata['getbug'] = 1; 85 | } 86 | array_walk($postdata, create_function('&$v, $k', '$v = rawurlencode($k) . "=" . rawurlencode($v);')); 87 | $postdata = implode('&', $postdata); 88 | 89 | // Hook an env var so emails can be resent without messing with bugs 90 | if (getenv('NOBUG')) { 91 | continue; 92 | } 93 | 94 | $ch = curl_init(); 95 | curl_setopt_array($ch, array( 96 | CURLOPT_URL => $bug_rpc_url, 97 | CURLOPT_RETURNTRANSFER => TRUE, 98 | CURLOPT_POST => TRUE, 99 | CURLOPT_CONNECTTIMEOUT => 5, 100 | CURLOPT_TIMEOUT => 5, 101 | CURLOPT_POSTFIELDS => $postdata, 102 | )); 103 | 104 | $result = curl_exec($ch); 105 | 106 | if ($result === FALSE) { 107 | $bug_list[$k]['error'] = curl_error($ch); 108 | } else { 109 | $bug_server_data = json_decode($result, TRUE); 110 | if (isset($bug_server_data['result']['status'])) { 111 | $bug_list[$k]['status'] = $bug_server_data['result']['status']['status']; 112 | $bug_list[$k]['short_desc'] = $bug_server_data['result']['status']['sdesc']; 113 | } else { 114 | $bug_list[$k]['error'] = $bug_server_data['result']['error']; 115 | } 116 | } 117 | curl_close($ch); 118 | } 119 | unset($SVN_MAGIC_COOKIE); 120 | 121 | // $bug_list is now available to later-running hooks 122 | ?> 123 | -------------------------------------------------------------------------------- /lib/Git/ReceiveHook.php: -------------------------------------------------------------------------------- 1 | repositoryName = $matches[1]; 30 | } 31 | } 32 | 33 | public function enableLog() { 34 | $this->isLog = true; 35 | } 36 | 37 | /** 38 | * Escape array items by escapeshellarg function 39 | * @param array $args 40 | * @return array array with escaped items 41 | */ 42 | protected function escapeArrayShellArgs(array $args) 43 | { 44 | return array_map('escapeshellarg', $args); 45 | } 46 | 47 | 48 | /** 49 | * Returns the repository name. 50 | * 51 | * A repository name is the path to the repository with the .git. 52 | * 53 | * @return string 54 | */ 55 | public function getRepositoryName() 56 | { 57 | return $this->repositoryName; 58 | } 59 | 60 | /** 61 | * Returns the short repository name. 62 | * 63 | * A short repository name is the path to the repository without the .git. 64 | * e.g. php-src.git -> php-src 65 | * 66 | * @return string 67 | */ 68 | public function getRepositoryShortName() 69 | { 70 | return preg_replace('@\.git$@', '', $this->repositoryName); 71 | } 72 | 73 | /** 74 | * Return array with changed paths as keys and change type as values 75 | * If commit is merge commit change type will have more than one char 76 | * (for example "MM") 77 | * 78 | * Required already escaped string in $revRange!!! 79 | * 80 | * @param string $revRange 81 | * @param bool $reverse reverse diff 82 | * @return array 83 | */ 84 | protected function getChangedPaths($revRange, $reverse = false) 85 | { 86 | $raw = \Git::gitExec('diff-tree ' . ($reverse ? '-R ' : '') . '-r --no-commit-id -c --name-status --pretty="format:" %s', $revRange); 87 | $paths = []; 88 | $lines = explode("\n", $raw); 89 | foreach($lines as $line) { 90 | if (preg_match('/([ACDMRTUXB*]+)\s+([^\n\s]+)/', $line , $matches)) { 91 | $paths[$matches[2]] = $matches[1]; 92 | } 93 | } 94 | 95 | return $paths; 96 | } 97 | 98 | 99 | /** 100 | * Return array with branches names in repository 101 | * 102 | * @return array 103 | */ 104 | protected function getAllBranches() 105 | { 106 | $branches = explode("\n", trim(\Git::gitExec('for-each-ref --format="%%(refname)" "refs/heads/*"'))); 107 | if ($branches[0] == '') $branches = []; 108 | return $branches; 109 | } 110 | 111 | 112 | protected function log($string) { 113 | if (!$this->isLog) return; 114 | $string = trim($string) . "\n"; 115 | file_put_contents(self::LOG_FILE_PATH, $string, FILE_APPEND); 116 | } 117 | 118 | /** 119 | * Parses the input from git. 120 | * 121 | * Git pipes a list of oldrev, newrev and revname combinations 122 | * to the hook. We parse this input. For more information about 123 | * the input see githooks(5). 124 | * 125 | * Returns an array with 'old', 'new', 'refname', 'changetype', 'reftype' 126 | * keys for each ref that will be updated. 127 | * @return array 128 | */ 129 | public function hookInput() 130 | { 131 | $this->log('New hook call ' . $this->getRepositoryName() . ' ' . date('r')); 132 | 133 | $parsed_input = []; 134 | while (!feof(STDIN)) { 135 | $line = fgets(STDIN); 136 | if (preg_match(self::INPUT_PATTERN, $line, $matches)) { 137 | 138 | $this->log($line); 139 | 140 | $ref = [ 141 | 'old' => $matches[1], 142 | 'new' => $matches[2], 143 | 'refname' => $matches[3] 144 | ]; 145 | 146 | if (preg_match('~^refs/heads/.+$~', $ref['refname'])) { 147 | // git push origin branchname 148 | $ref['reftype'] = self::REF_BRANCH; 149 | } elseif (preg_match('~^refs/tags/.+$~', $ref['refname'])) { 150 | // git push origin tagname 151 | $ref['reftype'] = self::REF_TAG; 152 | } else { 153 | // not support by this script 154 | $ref['reftype'] = -1; 155 | } 156 | 157 | if ($ref['old'] == \GIT::NULLREV) { 158 | // git branch branchname && git push origin branchname 159 | // git tag tagname rev && git push origin tagname 160 | $ref['changetype'] = self::TYPE_CREATED; 161 | } elseif ($ref['new'] == \GIT::NULLREV) { 162 | // git branch -d branchname && git push origin :branchname 163 | // git tag -d tagname && git push origin :tagname 164 | $ref['changetype'] = self::TYPE_DELETED; 165 | } else { 166 | // git push origin branchname 167 | // git tag -f tagname rev && git push origin tagname 168 | $ref['changetype'] = self::TYPE_UPDATED; 169 | } 170 | 171 | 172 | $parsed_input[$ref['refname']] = $ref; 173 | } 174 | } 175 | $this->refs = $parsed_input; 176 | return $this->refs; 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /hooks/pre-receive: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * 8 | * Licensed under the terms of the MIT license. 9 | */ 10 | namespace Karma; 11 | 12 | const KARMA_FILE = '/git/checkout/SVNROOT/global_avail'; 13 | const REPOSITORY_PATH = '/git/repositories'; 14 | const LIB_PATH = '/git/checkout/karma/lib'; 15 | 16 | set_include_path( 17 | getenv('KARMA_LIB_PATH') ?: LIB_PATH . 18 | PATH_SEPARATOR . 19 | get_include_path()); 20 | 21 | include 'Git.php'; 22 | include 'Git/PushInformation.php'; 23 | include 'Git/ReceiveHook.php'; 24 | include 'Git/PreReceiveHook.php'; 25 | 26 | $weKnowWhatWeAreDoing = ['dsp', 'johannes', 'tyrael', 'nikic']; 27 | // On restricted branches forced pushes are only possible by users listed in $weKnowWhatWeAreDoing 28 | $restrictedBranches = 29 | ['php-src.git' => [ 30 | 'refs/heads/PHP-5.3', 31 | 'refs/heads/PHP-5.4', 32 | 'refs/heads/PHP-5.5', 33 | 'refs/heads/PHP-5.6', 34 | 'refs/heads/PHP-7.0', 35 | 'refs/heads/PHP-7.1', 36 | 'refs/heads/PHP-7.2', 37 | 'refs/heads/PHP-7.3', 38 | 'refs/heads/PHP-7.4', 39 | 'refs/heads/PHP-8.0', 40 | 'refs/heads/master' 41 | ], 42 | 'playground.git' => ['refs/heads/dsp']]; 43 | // On closed branches only RMs may push 44 | $RMs = [ 45 | 'johannes', 'stas', 'dsp', 'jpauli', 'tyrael', 'ab', 'krakjoe', 'davey', 46 | 'remi', 'pollita', 'cmb', 'derick', 'petk' 47 | ]; 48 | $closedBranches = [ 49 | 'php-src.git' => [ 50 | 'refs/heads/PHP-5.3' => $RMs, 51 | 'refs/heads/PHP-5.4' => $RMs, 52 | 'refs/heads/PHP-5.5' => $RMs, 53 | 'refs/heads/PHP-5.6' => $RMs, 54 | 'refs/heads/PHP-7.0' => $RMs, 55 | 'refs/heads/PHP-7.1' => $RMs, 56 | 'refs/heads/PHP-7.2' => $RMs, 57 | 'refs/heads/PHP-7.3' => $RMs, 58 | ], 59 | 'playground.git' => ['refs/heads/johannes' => ['nobody', 'johannes']] 60 | ]; 61 | 62 | function deny($reason) 63 | { 64 | fwrite(STDERR, $reason . "\n"); 65 | exit(1); 66 | } 67 | 68 | function accept($message) 69 | { 70 | fwrite(STDOUT, $message . "\n"); 71 | exit(0); 72 | } 73 | 74 | function get_karma_for_paths($username, array $paths, array $avail_lines) 75 | { 76 | $access = array_fill_keys($paths, 'unavail'); 77 | foreach ($avail_lines as $acl_line) { 78 | $acl_line = trim($acl_line); 79 | if ('' === $acl_line || '#' === $acl_line{0}) { 80 | continue; 81 | } 82 | 83 | @list($avail, $user_str, $path_str) = explode('|', $acl_line); 84 | 85 | $allowed_paths = explode(',', $path_str); 86 | $allowed_users = explode(',', $user_str); 87 | 88 | /* ignore lines which don't contain our users or apply to all users */ 89 | if (!in_array($username, $allowed_users) && !empty($user_str)) { 90 | continue; 91 | } 92 | 93 | if (!in_array($avail, ['avail', 'unavail'])) { 94 | continue; 95 | } 96 | 97 | if (empty($path_str)) { 98 | $access = array_fill_keys($paths, $avail); 99 | } else { 100 | foreach ($access as $requested_path => $is_avail) { 101 | foreach ($allowed_paths as $path) { 102 | if (fnmatch($path . '*', $requested_path)) { 103 | $access[$requested_path] = $avail; 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | return $access; 111 | } 112 | 113 | function get_unavail_paths($username, array $paths, array $avail_lines) 114 | { 115 | return 116 | array_keys( 117 | array_filter( 118 | get_karma_for_paths($username, $paths, $avail_lines), 119 | function ($avail) { 120 | return 'unavail' === $avail; 121 | })); 122 | } 123 | 124 | 125 | error_reporting(E_ALL | E_STRICT); 126 | date_default_timezone_set('UTC'); 127 | putenv("PATH=/opt/bin:/usr/local/bin:/usr/bin:/bin"); 128 | putenv("LC_ALL=en_US.UTF-8"); 129 | 130 | $user = null; 131 | if (getenv('REMOTE_USER')) { 132 | $user = getenv('REMOTE_USER'); 133 | } else if (getenv('SSH_CONNECTION') && getenv('GL_USER')) { 134 | /* gitolite user */ 135 | $user = getenv('GL_USER'); 136 | } 137 | 138 | if (is_null($user)) { 139 | deny("Cannot determine user information. Aborted."); 140 | } 141 | 142 | fprintf(STDOUT, "Welcome $user.\n"); 143 | 144 | $hook = new \Git\PreReceiveHook(getenv('PHP_KARMA_FILE') ?: KARMA_FILE, 145 | getenv('GL_REPO_BASE_ABS') ?: REPOSITORY_PATH); 146 | 147 | if ($hook->isKarmaIgnored()) { 148 | accept("No karma check necessary. Thank you for your contribution.\n"); 149 | } 150 | 151 | deny("Upstream has migrated to GitHub, please push there instead."); 152 | 153 | $hook->hookInput(); 154 | $repo_name = $hook->getRepositoryName(); 155 | $pi = new \Git\PushInformation($hook); 156 | $req_paths = ($pi->isNewBranch() || $pi->isForced() || $pi->isTag()) ? [''] : $hook->getReceivedPaths(); 157 | 158 | if (empty($req_paths)) { 159 | $req_paths = ['']; // we can empty paths for trivial merge 160 | } 161 | 162 | if (isset($closedBranches[$repo_name])) { 163 | $closed = array_filter(array_keys($closedBranches[$repo_name]), 164 | function ($branch) use ($pi, $user, $closedBranches, $repo_name) { 165 | return in_array($branch, $pi->getBranches()) && !in_array($user, $closedBranches[$repo_name][$branch]); 166 | }); 167 | 168 | if (count($closed)) { 169 | $rms = $closedBranches[$repo_name]; 170 | deny("You can not push to closed branches. ". PHP_EOL . implode(PHP_EOL, array_map( 171 | function ($branch) use ($rms) { 172 | return "Please contact " . implode(', ', $rms[$branch]) . " for changes to ${branch}."; 173 | }, 174 | $closed))); 175 | } 176 | } 177 | 178 | $restricted = []; 179 | if (isset($restrictedBranches[$repo_name])) { 180 | $restricted = array_filter($restrictedBranches[$repo_name], 181 | function ($branch) use ($pi) { 182 | return in_array($branch, $pi->getBranches()); 183 | }); 184 | } 185 | 186 | if (count($restricted) > 0 && $pi->isForced() && !in_array($user, $weKnowWhatWeAreDoing)) { 187 | deny("You are not allowed to overwrite commits on " . implode(', ', $restricted)); 188 | } 189 | 190 | $prefix = sprintf('%s/', $repo_name); 191 | $avail_lines = $hook->getKarmaFile(); 192 | $req_paths = array_map(function ($x) use ($prefix) { return $prefix . $x;}, $req_paths); 193 | $unavail_paths = get_unavail_paths($user, $req_paths, $avail_lines); 194 | 195 | if (!empty($unavail_paths)) { 196 | deny(sprintf( 197 | "You have insufficient Karma!\n" . 198 | "I'm sorry, I cannot allow you to write to\n" . 199 | " %s\n" . 200 | "Have a nice day.", 201 | implode("\n ", $unavail_paths))); 202 | } 203 | 204 | accept("Changesets accepted. Thank you for your contribution.\n"); 205 | -------------------------------------------------------------------------------- /lib/Mail.php: -------------------------------------------------------------------------------- 1 | '', 'email' => '']; 6 | private $to = []; 7 | private $subject = ''; 8 | private $message = ''; 9 | private $files = []; 10 | private $multipart = false; 11 | private $boundary = ''; 12 | private $uniqId = ''; 13 | private $replyTo = []; 14 | private $timestamp = null; 15 | 16 | const CRLF = "\r\n"; 17 | 18 | 19 | public function __construct() 20 | { 21 | $this->uniqId = ''; 22 | } 23 | 24 | /** 25 | * Return unique id of mail 26 | * @return string unique Id of message in format: 'uniqId; 31 | } 32 | 33 | /** 34 | * Add parent mail for this mail 35 | * @param string $uniqId unique Id of message in format: 'replyTo[] = $uniqId; 40 | } 41 | 42 | /** 43 | * Add attached text file to mail 44 | * @param string $name unique file name 45 | * @param string $data file content 46 | */ 47 | public function addTextFile($name , $data) 48 | { 49 | $this->files[trim($name)] = chunk_split(base64_encode($data), 76, self::CRLF); 50 | } 51 | 52 | /** 53 | * Return length of attached file 54 | * @param string $name unique file name 55 | * @return int file length 56 | */ 57 | public function getFileLength($name) 58 | { 59 | $name = trim($name); 60 | return isset($this->files[$name]) ? strlen($this->files[$name]) : 0; 61 | } 62 | 63 | /** 64 | * Delete attached file 65 | * @param string $name unique file name 66 | */ 67 | public function dropFile($name) 68 | { 69 | $name = trim($name); 70 | unset($this->files[$name]); 71 | } 72 | 73 | /** 74 | * Set "From" address 75 | * @param string $email email author address 76 | * @param string $name author name 77 | */ 78 | public function setFrom($email, $name = '') 79 | { 80 | $this->from = ['email' => trim($email), 'name' => trim($name)]; 81 | } 82 | 83 | /** 84 | * Add recipient address 85 | * @param string $email recipient address 86 | * @param string $name recipient name 87 | */ 88 | public function addTo($email, $name = '') 89 | { 90 | $this->to[] = ['email' => trim($email), 'name' => trim($name)]; 91 | } 92 | 93 | /** 94 | * Set mail subject 95 | * @param string $subject subject 96 | */ 97 | public function setSubject($subject) 98 | { 99 | $this->subject = trim($subject); 100 | } 101 | 102 | /** 103 | * Set timestamp 104 | * @param string $timestamp timestamp 105 | */ 106 | public function setTimestamp($timestamp) 107 | { 108 | $this->timestamp = trim($timestamp); 109 | } 110 | 111 | /** 112 | * Set mail body text 113 | * @param string $message body text 114 | */ 115 | public function setMessage($message) 116 | { 117 | $this->message = $message; 118 | } 119 | 120 | 121 | /** 122 | * Format header string 123 | * @param string $name header name 124 | * @param string $value header value 125 | * @return string header string 126 | */ 127 | private function makeHeader($name, $value) 128 | { 129 | return $name . ': ' . $value; 130 | } 131 | 132 | /** 133 | * Format address string 134 | * @param array $address array with email adress and name 135 | * @return string address string 136 | */ 137 | private function makeAddress(array $address) 138 | { 139 | return $address['name'] ? $this->utf8SafeEncode($address['name'], 100) . ' <'. $address['email'] . '>' : $address['email']; 140 | } 141 | 142 | /** 143 | * Cut end encode string by mb_encode_mimeheader 144 | * @param string $value utf8 string 145 | * @param int $maxLenght max length 146 | * @return string encoded string 147 | */ 148 | private function utf8SafeEncode($value, $maxLenght = null) 149 | { 150 | if ($maxLenght) $value = mb_substr($value, 0, $maxLenght); 151 | return mb_encode_mimeheader($value, 'UTF-8', 'Q'); 152 | } 153 | 154 | /** 155 | * Prepare heade part of mail 156 | * @return string header part of mail 157 | */ 158 | private function makeHeaders() 159 | { 160 | $headers = []; 161 | $headers[] = $this->makeHeader('From', $this->makeAddress($this->from)); 162 | $headers[] = $this->makeHeader('Message-ID', $this->uniqId); 163 | if (count($this->replyTo)) { 164 | $replyTo = implode(' ', $this->replyTo); 165 | $headers[] = $this->makeHeader('References', $replyTo); 166 | $headers[] = $this->makeHeader('In-Reply-To', $replyTo); 167 | } 168 | $headers[] = $this->makeHeader('MIME-Version', '1.0'); 169 | $headers[] = $this->makeHeader('Date', date(DATE_RFC2822, $this->timestamp ?: time())); 170 | if ($this->multipart) { 171 | $this->boundary = sha1($this->uniqId); 172 | $headers[] = $this->makeHeader('Content-Type', 'multipart/mixed; boundary="' . $this->boundary . '"'); 173 | } else { 174 | $headers[] = $this->makeHeader('Content-Type', 'text/plain; charset="utf-8"'); 175 | // we use base64 for avoiding some problems sush string length limit, safety encoding etc. 176 | $headers[] = $this->makeHeader('Content-Transfer-Encoding', 'quoted-printable'); 177 | } 178 | return implode(self::CRLF , $headers); 179 | } 180 | 181 | /** 182 | * Prepare body part of mail 183 | * @return string mail body 184 | */ 185 | private function makeBody() 186 | { 187 | $body = ''; 188 | if ($this->multipart) { 189 | $body .= '--' . $this->boundary . self::CRLF; 190 | $body .= $this->makeHeader('Content-Type', 'text/plain; charset="utf-8"') . self::CRLF; 191 | $body .= $this->makeHeader('Content-Transfer-Encoding', 'quoted-printable') . self::CRLF; 192 | $body .= self::CRLF; 193 | $body .= quoted_printable_encode($this->message); 194 | foreach ($this->files as $name => $data) { 195 | $body .= self::CRLF . '--' . $this->boundary . self::CRLF; 196 | $body .= $this->makeHeader('Content-Type', 'text/plain; charset="utf-8"') . self::CRLF; 197 | $body .= $this->makeHeader('Content-Transfer-Encoding', 'base64') . self::CRLF; 198 | $body .= $this->makeHeader('Content-Disposition', 'attachment; filename="' . $name . '"') . self::CRLF; 199 | $body .= self::CRLF; 200 | $body .= $data; 201 | } 202 | $body .= self::CRLF . '--' . $this->boundary . '--'; 203 | } else { 204 | $body = quoted_printable_encode($this->message); 205 | } 206 | return $body; 207 | } 208 | 209 | /** 210 | * Send current mail 211 | * @return bool 212 | */ 213 | public function send() 214 | { 215 | $this->multipart = (bool) count($this->files); 216 | 217 | $receivers = implode(', ', array_map([$this, 'makeAddress'], $this->to)); 218 | $subject = $this->utf8SafeEncode($this->subject, 450); 219 | $headers = $this->makeHeaders(); 220 | $body = $this->makeBody(); 221 | 222 | return mail($receivers, $subject, $body, $headers, "-f noreply@php.net"); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lib/Git/PostReceiveHook.php: -------------------------------------------------------------------------------- 1 | usersFile = $usersFile; 31 | $this->pushAuthor = $pushAuthor; 32 | $this->pushAuthorName = $this->getUserName($pushAuthor); 33 | $this->mailingList = $mailingList; 34 | $this->emailPrefix = $emailPrefix; 35 | 36 | $this->allBranches = $this->getAllBranches(); 37 | } 38 | 39 | /** 40 | * Find user name by nickname in users data file 41 | * @param string $user user nickname 42 | * @return string user name 43 | */ 44 | public function getUserName($user) 45 | { 46 | $usersDB = file($this->usersFile); 47 | foreach ($usersDB as $userline) { 48 | list ($username, $fullname, $email) = explode(":", trim($userline)); 49 | if ($username === $user) { 50 | return $fullname; 51 | } 52 | } 53 | return ''; 54 | } 55 | 56 | 57 | 58 | /** 59 | * Parse input from STDIN 60 | * Mail about changes in heads(branches) and tags 61 | * Mail about new commits 62 | */ 63 | public function process() 64 | { 65 | $this->hookInput(); 66 | 67 | //cache list of old and updated branches 68 | $newBranches = []; 69 | foreach ($this->refs as $ref) { 70 | if ($ref['reftype'] == self::REF_BRANCH){ 71 | if ($ref['changetype'] == self::TYPE_UPDATED) { 72 | $this->updatedBranches[] = $ref['refname']; 73 | } elseif ($ref['changetype'] == self::TYPE_CREATED) { 74 | $newBranches[] = $ref['refname']; 75 | } 76 | } 77 | } 78 | $this->alreadyExistsBranches = array_diff($this->allBranches, $newBranches); 79 | 80 | 81 | foreach ($this->refs as $ref) { 82 | if ($ref['reftype'] == self::REF_TAG) { 83 | // tag mail 84 | $this->sendTagMail($ref['refname'], $ref['changetype'], $ref['old'], $ref['new']); 85 | } elseif ($ref['reftype'] == self::REF_BRANCH) { 86 | if ($ref['changetype'] != self::TYPE_DELETED) { 87 | // magic populate the $this->revisions 88 | $this->getBranchRevisions($ref['refname'], $ref['changetype'], $ref['old'], $ref['new']); 89 | 90 | if ($ref['changetype'] == self::TYPE_UPDATED) { 91 | $this->sendDeletedCommitsMail($ref['refname'], $ref['old'], $ref['new']); 92 | } 93 | } else { 94 | $this->sendDeletedBranchMail($ref['refname']); 95 | } 96 | } 97 | } 98 | 99 | $this->log('Found revisions: '. implode(' ', array_keys($this->revisions))); 100 | //send mails per commit 101 | foreach ($this->revisions as $revision => $branches) { 102 | // check if it commit was already in other branches 103 | if (!$this->isRevExistsInBranches($revision, array_diff($this->allBranches, $branches))) { 104 | $this->sendCommitMail($revision, $branches); 105 | } 106 | } 107 | 108 | } 109 | 110 | /** 111 | * Send mail about force deleted commits. 112 | * Subject: del %PROJECT%: %PATHS% 113 | * Body: 114 | * Branch: %BRANCH% 115 | * Deleted commits count: %REV_COUNT% 116 | * User: %USER% Thu, 08 Mar 2012 12:39:48 +0000 117 | * 118 | * --part1-- 119 | * Changed paths: 120 | * %PATHS% 121 | * --/part1-- 122 | * 123 | * --part2-- 124 | * Diff: 125 | * %DIFF% 126 | * --/part2-- 127 | * 128 | * @param string $name branch fullname (refs/heads/example) 129 | * @param string $oldrev old revision 130 | * @param string $newrev new revision 131 | */ 132 | private function sendDeletedCommitsMail($name, $oldrev, $newrev) 133 | { 134 | 135 | $deletedRevisionsCount = count($this->getRevisions(escapeshellarg($newrev . '..' . $oldrev))); 136 | 137 | if ($deletedRevisionsCount > 0) { 138 | $shortName = str_replace('refs/heads/', '', $name); 139 | 140 | $paths = $this->getChangedPaths(escapeshellarg($newrev . '..' . $oldrev), true); 141 | $pathsString = ''; 142 | foreach ($paths as $path => $action) 143 | { 144 | $pathsString .= ' ' . $action . ' ' . $path . "\n"; 145 | } 146 | 147 | $isTrivialMerge = count($paths) <= 0; 148 | 149 | if (!$isTrivialMerge) { 150 | 151 | $diff = \Git::gitExec('diff-tree --cc -r -R --no-commit-id %s', escapeshellarg($newrev . '..' . $oldrev)); 152 | 153 | $mail = new \Mail(); 154 | $mail->setSubject($this->emailPrefix . 'del ' . $this->getRepositoryShortName() . ': '. implode(' ', array_keys($paths))); 155 | $mail->setTimestamp(strtotime(date('r'))); 156 | 157 | $message = 'Branch: ' . $shortName . "\n"; 158 | $message .= 'Deleted commits count: ' . $deletedRevisionsCount . "\n"; 159 | $message .= 'User: ' . $this->pushAuthorName . ' <' . $this->pushAuthor . '@php.net> ' . date('r') . "\n"; 160 | 161 | if (strlen($pathsString) < 8192) { 162 | // inline changed paths 163 | $message .= "\nChanged paths:\n" . $pathsString . "\n"; 164 | if ((strlen($pathsString) + strlen($diff)) < 8192) { 165 | // inline diff 166 | $message .= "\nDiff:\n" . $diff . "\n"; 167 | } else { 168 | // diff attach 169 | $diffFile = 'diff_' . $newrev . '_' . $oldrev . '.txt'; 170 | $mail->addTextFile($diffFile, $diff); 171 | if ((strlen($message) + $mail->getFileLength($diffFile)) > 262144) { 172 | // diff attach exceeded max size 173 | $mail->dropFile($diffFile); 174 | $message .= "\nDiff: "; 175 | } 176 | } 177 | } else { 178 | // changed paths attach 179 | $pathsFile = 'paths_' . $newrev . '_' . $oldrev . '.txt'; 180 | $mail->addTextFile($pathsFile, $pathsString); 181 | if ((strlen($message) + $mail->getFileLength($pathsFile)) > 262144) { 182 | // changed paths attach exceeded max size 183 | $mail->dropFile($pathsFile); 184 | $message .= "\nChanged paths: "; 185 | } else { 186 | // diff attach 187 | $diffFile = 'diff_' . $newrev . '_' . $oldrev . '.txt'; 188 | $mail->addTextFile($diffFile, $diff); 189 | if ((strlen($message) + $mail->getFileLength($pathsFile) + $mail->getFileLength($diffFile)) > 262144) { 190 | // diff attach exceeded max size 191 | $mail->dropFile($diffFile); 192 | } 193 | } 194 | } 195 | 196 | $mail->setMessage($message); 197 | 198 | $mail->setFrom($this->pushAuthor . '@php.net', $this->pushAuthorName); 199 | $mail->addTo($this->mailingList); 200 | 201 | $result = $mail->send(); 202 | $this->log('revisions deleted ' . $newrev . '..' . $oldrev . ($result ? ' was send' : ' error while sending')); 203 | } 204 | 205 | 206 | } 207 | } 208 | 209 | 210 | /** 211 | * Send mail about deleted branch. 212 | * Subject: branch %PROJECT%: %BRANCH_NAME% deleted 213 | * Body: 214 | * Deleted branch: %BRANCH% 215 | * User: %USER% Thu, 08 Mar 2012 12:39:48 +0000 216 | * 217 | * @param string $name branch fullname (refs/heads/example) 218 | */ 219 | private function sendDeletedBranchMail($name) 220 | { 221 | $shortName = str_replace('refs/heads/', '', $name); 222 | 223 | $mail = new \Mail(); 224 | $mail->setSubject($this->emailPrefix . 'branch ' . $this->getRepositoryShortName() . ': '. $shortName . ' deleted'); 225 | $mail->setTimestamp(strtotime(date('r'))); 226 | 227 | $message = 'Deleted branch: ' . $shortName . "\n"; 228 | $message .= 'User: ' . $this->pushAuthorName . ' <' . $this->pushAuthor . '@php.net> ' . date('r') . "\n"; 229 | 230 | $mail->setMessage($message); 231 | 232 | $mail->setFrom($this->pushAuthor . '@php.net', $this->pushAuthorName); 233 | $mail->addTo($this->mailingList); 234 | 235 | $result = $mail->send(); 236 | $this->log('branch deleted ' . $shortName . ($result ? ' was send' : ' error while sending')); 237 | 238 | } 239 | 240 | 241 | /** 242 | * Cache revisions per branche for use it later 243 | * @param string $branchName branch fullname 244 | * @param array $revisions revisions array 245 | */ 246 | private function cacheRevisions($branchName, array $revisions) 247 | { 248 | foreach ($revisions as $revision) 249 | { 250 | $this->revisions[$revision][$branchName] = $branchName; 251 | } 252 | } 253 | 254 | 255 | /** 256 | * Send mail about tag. 257 | * Subject: tag %PROJECT%: %STATUS% tag %TAGNAME% 258 | * Body: 259 | * Tag %TAGNAME% in %PROJECT% was %STATUS% (if sha was changed)from %OLD_SHA% 260 | * Tag(if annotaded): %SHA% 261 | * Tagger(if annotaded): %USER% Thu, 08 Mar 2012 12:39:48 +0000 262 | * 263 | * Log(if annotaded): 264 | * %MESSAGE% 265 | * 266 | * Link: http://git.php.net/?p=%PROJECT_PATH%;a=tag;h=%SHA% 267 | * 268 | * Target: %SHA% 269 | * Author: %USER% Thu, 08 Mar 2012 12:39:48 +0000 270 | * Committer: %USER% Thu, 08 Mar 2012 12:39:48 +0000 271 | * Parents: %SHA_PARENTS% 272 | * Target link: http://git.php.net/?p=%PROJECT_PATH%;a=commitdiff;h=%SHA% 273 | * Target log: 274 | * %MESSAGE% 275 | * 276 | * --part1-- 277 | * Changed paths: 278 | * %PATHS% 279 | * --/part1-- 280 | * 281 | * @param string $name tag fullname (refs/tags/example) 282 | * @param int $changeType delete, create or update 283 | * @param string $oldrev old revision 284 | * @param string $newrev new revision 285 | */ 286 | private function sendTagMail($name, $changeType, $oldrev, $newrev) 287 | { 288 | 289 | $status = [self::TYPE_UPDATED => 'update', self::TYPE_CREATED => 'create', self::TYPE_DELETED => 'delete']; 290 | $shortname = str_replace('refs/tags/', '', $name); 291 | $mail = new \Mail(); 292 | $mail->setSubject($this->emailPrefix . 'tag ' . $this->getRepositoryShortName() . ': ' . $status[$changeType] . ' tag ' . $shortname); 293 | 294 | $message = 'Tag ' . $shortname . ' in ' . $this->getRepositoryName() . ' was ' . $status[$changeType] . 'd' . 295 | (($changeType != self::TYPE_CREATED) ? ' from ' . $oldrev : '' ) . "\n"; 296 | 297 | if ($changeType != self::TYPE_DELETED) { 298 | $info = $this->getTagInfo($name); 299 | $targetInfo = $this->getCommitInfo($info['target']); 300 | $targetPaths = $this->getChangedPaths(escapeshellarg($info['target'])); 301 | $pathsString = ''; 302 | foreach ($targetPaths as $path => $action) 303 | { 304 | $pathsString .= ' ' . $action . ' ' . $path . "\n"; 305 | } 306 | 307 | if ($info['annotated']) { 308 | $message .= 'Tag: ' . $info['revision'] . "\n"; 309 | $message .= 'Tagger: ' . $info['tagger'] . $info['tagger_email'] . ' ' . $info['tagger_date'] . "\n"; 310 | $message .= "Log:\n" . $info['log'] . "\n"; 311 | $mail->setTimestamp(strtotime($info['tagger_date'])); 312 | } 313 | 314 | $message .= "\n"; 315 | $message .= "Link: http://git.php.net/?p=" . $this->getRepositoryName() . ";a=tag;h=" . $info['revision'] . "\n"; 316 | $message .= "\n"; 317 | 318 | $message .= 'Target: ' . $info['target'] . "\n"; 319 | $message .= 'Author: ' . $targetInfo['author'] . ' <' . $targetInfo['author_email'] . '> ' . $targetInfo['author_date'] . "\n"; 320 | if (($targetInfo['author'] != $targetInfo['committer']) || ($targetInfo['author_email'] != $targetInfo['committer_email'])) { 321 | $message .= 'Committer: ' . $targetInfo['committer'] . ' <' . $targetInfo['committer_email'] . '> ' . $targetInfo['committer_date'] . "\n"; 322 | } 323 | if ($targetInfo['parents']) $message .= 'Parents: ' . $targetInfo['parents'] . "\n"; 324 | $message .= "Target link: http://git.php.net/?p=" . $this->getRepositoryName() . ";a=commitdiff;h=" . $info['target'] . "\n"; 325 | $message .= "Target log:\n" . $targetInfo['log'] . "\n"; 326 | 327 | 328 | if (strlen($pathsString) < 8192) { 329 | // inline changed paths 330 | $message .= "\nChanged paths:\n" . $pathsString . "\n"; 331 | } else { 332 | // changed paths attach 333 | $pathsFile = 'paths_' . $info['target'] . '.txt'; 334 | $mail->addTextFile($pathsFile, $pathsString); 335 | if ((strlen($message) + $mail->getFileLength($pathsFile)) > 262144) { 336 | // changed paths attach exceeded max size 337 | $mail->dropFile($pathsFile); 338 | $message .= "\nChanged paths: "; 339 | } 340 | } 341 | } 342 | 343 | $mail->setMessage($message); 344 | 345 | $mail->setFrom($this->pushAuthor . '@php.net', $this->pushAuthorName); 346 | $mail->addTo($this->mailingList); 347 | 348 | $result = $mail->send(); 349 | $this->log('tag ' . $name . ($result ? ' was send' : ' error while sending')); 350 | } 351 | 352 | /** 353 | * Get info for tag 354 | * It return array with items: 355 | * 'annotated' flag, 356 | * 'revision' - tag sha, 357 | * 'target' - target sha (if tag not annotated it equal 'revision') 358 | * only for annotated tag: 359 | * 'tagger', 'tagger_email', 'tagger_date' - info about tagger person 360 | * 'log' - tag message 361 | * @param string $tag tag fullname 362 | * @return array array with tag info 363 | */ 364 | private function getTagInfo($tag) 365 | { 366 | $temp = \Git::gitExec("for-each-ref --format=\"%%(objecttype)\n%%(objectname)\n%%(taggername)\n%%(taggeremail)\n%%(taggerdate)\n%%(*objectname)\n%%(contents)\" %s", escapeshellarg($tag)); 367 | $temp = explode("\n", trim($temp), 7); //6 elements separated by \n, last element - log message 368 | if ($temp[0] == 'tag') { 369 | $info = [ 370 | 'annotated' => true, 371 | 'revision' => $temp[1], 372 | 'tagger' => $temp[2], 373 | 'tagger_email' => $temp[3], 374 | 'tagger_date' => $temp[4], 375 | 'target' => $temp[5], 376 | 'log' => $temp[6] 377 | ]; 378 | } else { 379 | $info = [ 380 | 'annotated' => false, 381 | 'revision' => $temp[1], 382 | 'target' => $temp[1] 383 | ]; 384 | } 385 | return $info; 386 | } 387 | 388 | /** 389 | * Find revisions for branch change 390 | * Also cache revisions list for revisions mails 391 | * @param string $name branch fullname (refs/heads/example) 392 | * @param int $changeType delete, create or update 393 | * @param string $oldrev old revision 394 | * @param string $newrev new revision 395 | * @return array revisions list 396 | */ 397 | private function getBranchRevisions($name, $changeType, $oldrev, $newrev) 398 | { 399 | if ($changeType == self::TYPE_UPDATED) { 400 | // git rev-list old..new 401 | $revisions = $this->getRevisions(escapeshellarg($oldrev . '..' . $newrev)); 402 | } else { 403 | // for new branch we write log about new commits only 404 | $revisions = $this->getRevisions( 405 | escapeshellarg($newrev) . ' --not ' . implode(' ', $this->escapeArrayShellArgs($this->alreadyExistsBranches)) 406 | ); 407 | 408 | // for new branches we check if they was separated from other branches in same push 409 | // see README.POST_RECEIVE_MAIL "commit mail" part. 410 | foreach ($this->updatedBranches as $refname) { 411 | if ($this->isRevExistsInBranches($this->refs[$refname]['old'], [$name])) { 412 | $this->cacheRevisions($name, $this->getRevisions(escapeshellarg($this->refs[$refname]['old'] . '..' . $newrev))); 413 | } 414 | } 415 | } 416 | 417 | $this->cacheRevisions($name, $revisions); 418 | 419 | return $revisions; 420 | } 421 | 422 | 423 | /** 424 | * Get list of revisions for $revRange 425 | * 426 | * Required already escaped string in $revRange!!! 427 | * 428 | * @param string $revRange A..B or A ^B C --not D etc. 429 | * @return array revsions list 430 | */ 431 | private function getRevisions($revRange) 432 | { 433 | $output = \Git::gitExec( 434 | 'rev-list %s', 435 | $revRange 436 | ); 437 | $revisions = $output ? explode("\n", trim($output)) : []; 438 | return $revisions; 439 | } 440 | 441 | 442 | /** 443 | * Get info for commit 444 | * It return array with items: 445 | * 'parents' -list of parents sha, 446 | * 'author', 'author_email', 'author_date' - info about author person 447 | * 'committer', 'committer_email', 'committer_date' - info about committer person 448 | * 'subject' - commit subject line 449 | * 'log' - full commit message 450 | * 451 | * Also cache revision info 452 | * @param string $revision revision 453 | * @return array commit info array 454 | */ 455 | private function getCommitInfo($revision) 456 | { 457 | $raw = \Git::gitExec('rev-list -n 1 --format="%%P%%n%%an%%n%%ae%%n%%aD%%n%%cn%%n%%ce%%n%%cD%%n%%s%%n%%B" %s', escapeshellarg($revision)); 458 | $raw = explode("\n", trim($raw), 10); //10 elements separated by \n, last element - log message, first(skipped) element - "commit sha" 459 | $data = [ 460 | 'parents' => $raw[1], // %P 461 | 'author' => $raw[2], // %an 462 | 'author_email' => $raw[3], // %ae 463 | 'author_date' => $raw[4], // %aD 464 | 'committer' => $raw[5], // %cn 465 | 'committer_email' => $raw[6], // %ce 466 | 'committer_date' => $raw[7], // %cD 467 | 'subject' => $raw[8], // %s 468 | 'log' => $raw[9] // %B 469 | ]; 470 | return $data; 471 | } 472 | 473 | /** 474 | * Find info about bugs in log message 475 | * @param string $log log message 476 | * @return array array with bug numbers and links in values 477 | */ 478 | private function getBugs($log) 479 | { 480 | $bugUrlPrefixes = [ 481 | 'pear' => 'http://pear.php.net/bugs/', 482 | 'pecl' => 'https://bugs.php.net/', 483 | 'php' => 'https://bugs.php.net/', 484 | '' => 'https://bugs.php.net/' 485 | ]; 486 | $bugs = []; 487 | if (preg_match_all('/(?:(pecl|pear|php)\s*)?(?:bug|#)[\s#:]*([0-9]+)/iuX', $log, $matchedBugs, PREG_SET_ORDER)) { 488 | foreach($matchedBugs as $bug) { 489 | $bugs[$bug[2]] = $bugUrlPrefixes[strtolower($bug[1])] . $bug[2]; 490 | } 491 | } 492 | return $bugs; 493 | } 494 | 495 | /** 496 | * Send mail about commit. 497 | * Subject: com %PROJECT%: %PATHS% 498 | * Body: 499 | * Commit: %SHA% 500 | * Author: %USER% Thu, 08 Mar 2012 12:39:48 +0000 501 | * Committer: %USER% Thu, 08 Mar 2012 12:39:48 +0000 502 | * Parents: %SHA_PARENTS% 503 | * Branches: %BRANCHES% 504 | * 505 | * Link: http://git.php.net/?p=%PROJECT_PATH%;a=commitdiff;h=%SHA% 506 | * 507 | * Log: 508 | * %MESSAGE% 509 | * 510 | * Bug: %BUG% 511 | * 512 | * --part1-- 513 | * Changed paths: 514 | * %PATHS% 515 | * --/part1-- 516 | * 517 | * --part2-- 518 | * Diff: 519 | * %DIFF% 520 | * --/part2-- 521 | * 522 | * @param string $revision commit revision 523 | * @param array $branches branches in current push with this commit 524 | */ 525 | private function sendCommitMail($revision, $branches) 526 | { 527 | 528 | $paths = $this->getChangedPaths(escapeshellarg($revision)); 529 | $pathsString = ''; 530 | foreach ($paths as $path => $action) 531 | { 532 | $pathsString .= ' ' . $action . ' ' . $path . "\n"; 533 | } 534 | 535 | $isTrivialMerge = count($paths) <= 0; 536 | 537 | if (!$isTrivialMerge) { 538 | 539 | $bnames = array_map( 540 | function($x) { 541 | return str_replace('refs/heads/', '', $x); 542 | }, 543 | $branches 544 | ); 545 | 546 | $info = $this->getCommitInfo($revision); 547 | 548 | $diff = \Git::gitExec('diff-tree --cc -r --no-commit-id %s', escapeshellarg($revision)); 549 | 550 | $mail = new \Mail(); 551 | $mail->setSubject($this->emailPrefix . 'com ' . $this->getRepositoryShortName() . ': ' . $info['subject'] . ': '. implode(' ', array_keys($paths))); 552 | $mail->setTimestamp(strtotime($info['committer_date'])); 553 | 554 | $message = ''; 555 | 556 | $message .= 'Commit: ' . $revision . "\n"; 557 | $message .= 'Author: ' . $info['author'] . ' <' . $info['author_email'] . '> ' . $info['author_date'] . "\n"; 558 | if (($info['author'] != $info['committer']) || ($info['author_email'] != $info['committer_email'])) { 559 | $message .= 'Committer: ' . $info['committer'] . ' <' . $info['committer_email'] . '> ' . $info['committer_date'] . "\n"; 560 | } 561 | if ($info['parents']) $message .= 'Parents: ' . $info['parents'] . "\n"; 562 | 563 | $message .= "Branches: " . implode(' ', $bnames) . "\n"; 564 | $message .= "\n" . "Link: http://git.php.net/?p=" . $this->getRepositoryName() . ";a=commitdiff;h=" . $revision . "\n"; 565 | 566 | $message .= "\nLog:\n" . $info['log'] . "\n"; 567 | 568 | if ($bugs = $this->getBugs($info['log'])) { 569 | $message .= "\nBugs:\n" . implode("\n", $bugs) . "\n"; 570 | } 571 | 572 | if (strlen($pathsString) < 8192) { 573 | // inline changed paths 574 | $message .= "\nChanged paths:\n" . $pathsString . "\n"; 575 | if ((strlen($pathsString) + strlen($diff)) < 8192) { 576 | // inline diff 577 | $message .= "\nDiff:\n" . $diff . "\n"; 578 | } else { 579 | // diff attach 580 | $diffFile = 'diff_' . $revision . '.txt'; 581 | $mail->addTextFile($diffFile, $diff); 582 | if ((strlen($message) + $mail->getFileLength($diffFile)) > 262144) { 583 | // diff attach exceeded max size 584 | $mail->dropFile($diffFile); 585 | $message .= "\nDiff: "; 586 | } 587 | } 588 | } else { 589 | // changed paths attach 590 | $pathsFile = 'paths_' . $revision . '.txt'; 591 | $mail->addTextFile($pathsFile, $pathsString); 592 | if ((strlen($message) + $mail->getFileLength($pathsFile)) > 262144) { 593 | // changed paths attach exceeded max size 594 | $mail->dropFile($pathsFile); 595 | $message .= "\nChanged paths: "; 596 | } else { 597 | // diff attach 598 | $diffFile = 'diff_' . $revision . '.txt'; 599 | $mail->addTextFile($diffFile, $diff); 600 | if ((strlen($message) + $mail->getFileLength($pathsFile) + $mail->getFileLength($diffFile)) > 262144) { 601 | // diff attach exceeded max size 602 | $mail->dropFile($diffFile); 603 | } 604 | } 605 | } 606 | 607 | $mail->setMessage($message); 608 | 609 | $mail->setFrom($this->pushAuthor . '@php.net', $this->pushAuthorName); 610 | $mail->addTo($this->mailingList); 611 | 612 | $result = $mail->send(); 613 | $this->log('revision ' . $revision . ($result ? ' was send' : ' error while sending')); 614 | } 615 | } 616 | 617 | 618 | /** 619 | * Check if revision exists in branches list 620 | * @param string $revision revision 621 | * @param array $branches branches 622 | * @return bool 623 | */ 624 | private function isRevExistsInBranches($revision, array $branches) { 625 | $output = \Git::gitExec('rev-list --max-count=1 %s --not %s', escapeshellarg($revision), implode(' ', $this->escapeArrayShellArgs($branches))); 626 | return empty($output); 627 | } 628 | 629 | } 630 | --------------------------------------------------------------------------------