* * Gitium is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * Gitium is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Gitium. If not, see . * * @package Gitium */ if (!defined('GITIGNORE')) define('GITIGNORE', <<repo_dir = $repo_dir; } function _rrmdir( $dir ) { if ( empty( $dir ) || ! is_dir( $dir ) ) { return false; } $files = array_diff( scandir( $dir ), array( '.', '..' ) ); foreach ( $files as $file ) { $filepath = realpath("$dir/$file"); ( is_dir( $filepath ) ) ? $this->_rrmdir( $filepath ) : unlink( $filepath ); } return rmdir( $dir ); } function _log(...$args) { if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { return; } $output = ''; if (isset($args) && $args) foreach ( $args as $arg ) { $output .= var_export($arg, true).'/n/n'; } if ($output) error_log($output); } function _git_temp_key_file() { $key_file = tempnam( sys_get_temp_dir(), 'ssh-git' ); return $key_file; } function set_key( $private_key ) { $this->private_key = $private_key; } private function get_env() { $env = array( 'HOME' => getenv( 'HOME' ), ); $key_file = null; if ( defined( 'GIT_SSH' ) && GIT_SSH ) { $env['GIT_SSH'] = GIT_SSH; } else { $env['GIT_SSH'] = dirname( __FILE__ ) . '/ssh-git'; } if ( defined( 'GIT_KEY_FILE' ) && GIT_KEY_FILE ) { $env['GIT_KEY_FILE'] = GIT_KEY_FILE; } elseif ( $this->private_key ) { $key_file = $this->_git_temp_key_file(); chmod( $key_file, 0600 ); file_put_contents( $key_file, $this->private_key ); $env['GIT_KEY_FILE'] = $key_file; } return $env; } protected function _call(...$args) { $args = join( ' ', array_map( 'escapeshellarg', $args ) ); $return = -1; $response = array(); $env = $this->get_env(); $git_bin_path = apply_filters( 'gitium_git_bin_path', '' ); $cmd = "{$git_bin_path}git $args 2>&1"; $proc = proc_open( $cmd, array( 0 => array( 'pipe', 'r' ), // stdin 1 => array( 'pipe', 'w' ), // stdout ), $pipes, $this->repo_dir, $env ); if ( is_resource( $proc ) ) { fclose( $pipes[0] ); while ( $line = fgets( $pipes[1] ) ) { $response[] = rtrim( $line, "\n\r" ); } $return = (int)proc_close( $proc ); } $this->_log( "$return $cmd", join( "\n", $response ) ); if ( ! defined( 'GIT_KEY_FILE' ) && isset( $env['GIT_KEY_FILE'] ) ) { unlink( $env['GIT_KEY_FILE'] ); } if ( 0 != $return ) { $this->last_error = join( "\n", $response ); } else { $this->last_error = null; } return array( $return, $response ); } function get_last_error() { return $this->last_error; } function can_exec_git() { list( $return, ) = $this->_call( 'version' ); return ( 0 == $return ); } function is_status_working() { list( $return, ) = $this->_call( 'status', '-s' ); return ( 0 == $return ); } function get_version() { list( $return, $version ) = $this->_call( 'version' ); if ( 0 != $return ) { return ''; } if ( ! empty( $version[0] ) ) { return substr( $version[0], 12 ); } return ''; } // git rev-list @{u}.. function get_ahead_commits() { list( , $commits ) = $this->_call( 'rev-list', '@{u}..' ); return $commits; } // git rev-list ..@{u} function get_behind_commits() { list( , $commits ) = $this->_call( 'rev-list', '..@{u}' ); return $commits; } function init() { file_put_contents( "$this->repo_dir/.gitignore", $this->gitignore ); list( $return, ) = $this->_call( 'init' ); $this->_call( 'config', 'user.email', 'gitium@presslabs.com' ); $this->_call( 'config', 'user.name', 'Gitium' ); $this->_call( 'config', 'push.default', 'matching' ); return ( 0 == $return ); } function is_dot_git_dir( $dir ) { $realpath = realpath( $dir ); $git_config = realpath( $realpath . '/config' ); $git_index = realpath( $realpath . '/index' ); if ( ! empty( $realpath ) && is_dir( $realpath ) && file_exists( $git_config ) && file_exists( $git_index ) ) { return True; } return False; } function cleanup() { $dot_git_dir = realpath( $this->repo_dir . '/.git' ); if ( $this->is_dot_git_dir( $dot_git_dir ) && $this->_rrmdir( $dot_git_dir ) ) { if ( WP_DEBUG ) { error_log( "Gitium cleanup successfull. Removed '$dot_git_dir'." ); } return True; } if ( WP_DEBUG ) { error_log( "Gitium cleanup failed. '$dot_git_dir' is not a .git dir." ); } return False; } function add_remote_url( $url ) { list( $return, ) = $this->_call( 'remote', 'add', 'origin', $url ); return ( 0 == $return ); } function get_remote_url() { list( , $response ) = $this->_call( 'config', '--get', 'remote.origin.url' ); if ( isset( $response[0] ) ) { return $response[0]; } return ''; } function remove_remote() { list( $return, ) = $this->_call( 'remote', 'rm', 'origin'); return ( 0 == $return ); } function get_remote_tracking_branch() { list( $return, $response ) = $this->_call( 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}' ); if ( 0 == $return ) { return $response[0]; } return false; } function get_local_branch() { list( $return, $response ) = $this->_call( 'rev-parse', '--abbrev-ref', 'HEAD' ); if ( 0 == $return ) { return $response[0]; } return false; } function fetch_ref() { list( $return, ) = $this->_call( 'fetch', 'origin' ); return ( 0 == $return ); } protected function _resolve_merge_conflicts( $message ) { list( , $changes ) = $this->status( true ); $this->_log( $changes ); foreach ( $changes as $path => $change ) { if ( in_array( $change, array( 'UD', 'DD' ) ) ) { $this->_call( 'rm', $path ); $message .= "\n\tConflict: $path [removed]"; } elseif ( 'DU' == $change ) { $this->_call( 'add', $path ); $message .= "\n\tConflict: $path [added]"; } elseif ( in_array( $change, array( 'AA', 'UU', 'AU', 'UA' ) ) ) { $this->_call( 'checkout', '--theirs', $path ); $this->_call( 'add', '--all', $path ); $message .= "\n\tConflict: $path [local version]"; } } $this->commit( $message ); } function get_commit_message( $commit ) { list( $return, $response ) = $this->_call( 'log', '--format=%B', '-n', '1', $commit ); return ( $return !== 0 ? false : join( "\n", $response ) ); } private function strpos_haystack_array( $haystack, $needle, $offset=0 ) { if ( ! is_array( $haystack ) ) { $haystack = array( $haystack ); } foreach ( $haystack as $query ) { if ( strpos( $query, $needle, $offset) !== false ) { return true; } } return false; } private function cherry_pick( $commits ) { foreach ( $commits as $commit ) { if ( empty( $commit ) ) { return false; } list( $return, $response ) = $this->_call( 'cherry-pick', $commit ); // abort the cherry-pick if the changes are already pushed if ( false !== $this->strpos_haystack_array( $response, 'previous cherry-pick is now empty' ) ) { $this->_call( 'cherry-pick', '--abort' ); continue; } if ( $return != 0 ) { $this->_resolve_merge_conflicts( $this->get_commit_message( $commit ) ); } } } function merge_with_accept_mine(...$commits) { do_action( 'gitium_before_merge_with_accept_mine' ); if ( 1 == count($commits) && is_array( $commits[0] ) ) { $commits = $commits[0]; } // get ahead commits $ahead_commits = $this->get_ahead_commits(); // combine all commits with the ahead commits $commits = array_unique( array_merge( array_reverse( $commits ), $ahead_commits ) ); $commits = array_reverse( $commits ); // get the remote branch $remote_branch = $this->get_remote_tracking_branch(); // get the local branch $local_branch = $this->get_local_branch(); // rename the local branch to 'merge_local' $this->_call( 'branch', '-m', 'merge_local' ); // local branch set up to track remote branch $this->_call( 'branch', $local_branch, $remote_branch ); // checkout to the $local_branch list( $return, ) = $this->_call( 'checkout', $local_branch ); if ( $return != 0 ) { $this->_call( 'branch', '-M', $local_branch ); return false; } // don't cherry pick if there are no commits if ( count( $commits ) > 0 ) { $this->cherry_pick( $commits ); } if ( $this->successfully_merged() ) { // git status without states: AA, DD, UA, AU ... // delete the 'merge_local' branch $this->_call( 'branch', '-D', 'merge_local' ); return true; } else { $this->_call( 'cherry-pick', '--abort' ); $this->_call( 'checkout', '-b', 'merge_local' ); $this->_call( 'branch', '-M', $local_branch ); return false; } } function successfully_merged() { list( , $response ) = $this->status( true ); $changes = array_values( $response ); return ( 0 == count( array_intersect( $changes, array( 'DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU' ) ) ) ); } function merge_initial_commit( $commit, $branch ) { list( $return, ) = $this->_call( 'branch', '-m', 'initial' ); if ( 0 != $return ) { return false; } list( $return, ) = $this->_call( 'checkout', $branch ); if ( 0 != $return ) { return false; } list( $return, ) = $this->_call( 'cherry-pick', '--strategy', 'recursive', '--strategy-option', 'theirs', $commit ); if ( $return != 0 ) { $this->_resolve_merge_conflicts( $this->get_commit_message( $commit ) ); if ( ! $this->successfully_merged() ) { $this->_call( 'cherry-pick', '--abort' ); $this->_call( 'checkout', 'initial' ); return false; } } $this->_call( 'branch', '-D', 'initial' ); return true; } function get_remote_branches() { list( , $response ) = $this->_call( 'branch', '-r' ); $response = array_map( 'trim', $response ); $response = array_map( function( $b ) { return str_replace( "origin/", "", $b ); }, $response ); return $response; } function add(...$args) { if ( 1 == count($args) && is_array( $args[0] ) ) { $args = $args[0]; } $params = array_merge( array( 'add', '-n', '--all' ), $args ); list ( , $response ) = call_user_func_array( array( $this, '_call' ), $params ); $count = count( $response ); $params = array_merge( array( 'add', '--all' ), $args ); list ( , $response ) = call_user_func_array( array( $this, '_call' ), $params ); return $count; } function commit( $message, $author_name = '', $author_email = '' ) { $author = ''; if ( $author_email ) { if ( empty( $author_name ) ) { $author_name = $author_email; } $author = "$author_name <$author_email>"; } if ( ! empty( $author ) ) { list( $return, $response ) = $this->_call( 'commit', '-m', $message, '--author', $author ); } else { list( $return, $response ) = $this->_call( 'commit', '-m', $message ); } if ( $return !== 0 ) { return false; } list( $return, $response ) = $this->_call( 'rev-parse', 'HEAD' ); return ( $return === 0 ) ? $response[0] : false; } function push( $branch = '' ) { if ( ! empty( $branch ) ) { list( $return, ) = $this->_call( 'push', '--porcelain', '-u', 'origin', $branch ); } else { list( $return, ) = $this->_call( 'push', '--porcelain', '-u', 'origin', 'HEAD' ); } return ( $return == 0 ); } /* * Get uncommited changes with status porcelain * git status --porcelain * It returns an array like this: array( file => deleted|modified ... ) */ function get_local_changes() { list( $return, $response ) = $this->_call( 'status', '--porcelain' ); if ( 0 !== $return ) { return array(); } $new_response = array(); if ( ! empty( $response ) ) { foreach ( $response as $line ) : $work_tree_status = substr( $line, 1, 1 ); $path = substr( $line, 3 ); if ( ( '"' == $path[0] ) && ('"' == $path[strlen( $path ) - 1] ) ) { // git status --porcelain will put quotes around paths with whitespaces // we don't want the quotes, let's get rid of them $path = substr( $path, 1, strlen( $path ) - 2 ); } if ( 'D' == $work_tree_status ) { $action = 'deleted'; } else { $action = 'modified'; } $new_response[ $path ] = $action; endforeach; } return $new_response; } function get_uncommited_changes() { list( , $changes ) = $this->status(); return $changes; } function local_status() { list( $return, $response ) = $this->_call( 'status', '-s', '-b', '-u' ); if ( 0 !== $return ) { return array( '', array() ); } $new_response = array(); if ( ! empty( $response ) ) { $branch_status = array_shift( $response ); foreach ( $response as $idx => $line ) : unset( $index_status, $work_tree_status, $path, $new_path, $old_path ); if ( empty( $line ) ) { continue; } // ignore empty lines like the last item if ( '#' == $line[0] ) { continue; } // ignore branch status $index_status = substr( $line, 0, 1 ); $work_tree_status = substr( $line, 1, 1 ); $path = substr( $line, 3 ); $old_path = ''; $new_path = explode( '->', $path ); if ( ( 'R' === $index_status ) && ( ! empty( $new_path[1] ) ) ) { $old_path = trim( $new_path[0] ); $path = trim( $new_path[1] ); } $new_response[ $path ] = trim( $index_status . $work_tree_status . ' ' . $old_path ); endforeach; } return array( $branch_status, $new_response ); } function status( $local_only = false ) { list( $branch_status, $new_response ) = $this->local_status(); if ( $local_only ) { return array( $branch_status, $new_response ); } $behind_count = 0; $ahead_count = 0; if ( preg_match( '/## ([^.]+)\.+([^ ]+)/', $branch_status, $matches ) ) { $local_branch = $matches[1]; $remote_branch = $matches[2]; list( , $response ) = $this->_call( 'rev-list', "$local_branch..$remote_branch", '--count' ); $behind_count = (int)$response[0]; list( , $response ) = $this->_call( 'rev-list', "$remote_branch..$local_branch", '--count' ); $ahead_count = (int)$response[0]; } if ( $behind_count ) { list( , $response ) = $this->_call( 'diff', '-z', '--name-status', "$local_branch~$ahead_count", $remote_branch ); $response = explode( chr( 0 ), $response[0] ); array_pop( $response ); for ( $idx = 0 ; $idx < count( $response ) / 2 ; $idx++ ) { $file = $response[ $idx * 2 + 1 ]; $change = $response[ $idx * 2 ]; if ( ! isset( $new_response[ $file ] ) ) { $new_response[ $file ] = "r$change"; } } } return array( $branch_status, $new_response ); } /* * Checks if repo has uncommited changes * git status --porcelain */ function is_dirty() { $changes = $this->get_uncommited_changes(); return ! empty( $changes ); } /** * Return the last n commits */ function get_last_commits( $n = 20 ) { list( $return, $message ) = $this->_call( 'log', '-n', $n, '--pretty=format:%s' ); if ( 0 !== $return ) { return false; } list( $return, $response ) = $this->_call( 'log', '-n', $n, '--pretty=format:%h|%an|%ae|%ad|%cn|%ce|%cd' ); if ( 0 !== $return ) { return false; } foreach ( $response as $index => $value ) { $commit_info = explode( '|', $value ); $commits[ $commit_info[0] ] = array( 'subject' => $message[ $index ], 'author_name' => $commit_info[1], 'author_email' => $commit_info[2], 'author_date' => $commit_info[3], ); if ( $commit_info[1] != $commit_info[4] && $commit_info[2] != $commit_info[5] ) { $commits[ $commit_info[0] ]['committer_name'] = $commit_info[4]; $commits[ $commit_info[0] ]['committer_email'] = $commit_info[5]; $commits[ $commit_info[0] ]['committer_date'] = $commit_info[6]; } } return $commits; } public function set_gitignore( $content ) { file_put_contents( $this->repo_dir . '/.gitignore', $content ); return true; } public function get_gitignore() { return file_get_contents( $this->repo_dir . '/.gitignore' ); } /** * Remove files in .gitignore from version control */ function rm_cached( $path ) { list( $return, ) = $this->_call( 'rm', '--cached', $path ); return ( $return == 0 ); } function remove_wp_content_from_version_control() { $process = proc_open( 'rm -rf ' . ABSPATH . '/wp-content/.git', array( 0 => array( 'pipe', 'r' ), // stdin 1 => array( 'pipe', 'w' ), // stdout ), $pipes ); if ( is_resource( $process ) ) { fclose( $pipes[0] ); proc_close( $process ); return true; } return false; } } if ( ! defined( 'GIT_DIR' ) ) { define( 'GIT_DIR', dirname( WP_CONTENT_DIR ) ); } # global is needed here for wp-cli as it includes/exec files inside a function scope # this forces the context to really be global :\. global $git; $git = new Git_Wrapper( GIT_DIR );