Skip to content
Snippets Groups Projects
Commit 5963deac authored by Jigish Chauhan's avatar Jigish Chauhan
Browse files

add git deploy

parent 90af6ec1
No related branches found
No related tags found
3 merge requests!9Resolve #3374331 "Custom.js dependencies are",!8Resolve #3374306 "Drupal 10 compatibility",!5Resolve #3374310 "Hookhelp"
INTRODUCTION
------------
Git Deploy lets you develop on a live site and still satisfy version
requirements and get accurate results from the update status system. This makes
it easier to contribute to the projects you use.
Version information is added automatically when the Drupal packaging system
creates a release. If you check out a contributed project from the Drupal
repository with Git, it should not have any version information. Git Deploy gets
the missing version information from the project's Git log.
* Project page: https://www.drupal.org/project/git_deploy
* Issue queue: https://www.drupal.org/project/issues/git_deploy
REQUIREMENTS
------------
* Access to the git command.
* The ability for PHP to execute shell commands.
ALTERNATIVES
------------
A project’s dev release changes whenever a maintainer updates its branch. To
enforce consistency among sites using a dev release, you can lock them to the
same release by checking out a specific Git commit. You would not contribute
changes from these sites back to the project. Drush 8
(https://docs.drush.org/en/8.x/) can automatically add version information to
projects you check out with Git.
* The drush pm-download command with options --package-handler=git_drupalorg
--gitinfofile performs a Git clone and checkout and adds version information
to the project info file.
* The drush make command automatically adds version information to the project
info file without additional options.
Composer Deploy (https://www.drupal.org/project/composer_deploy) gets version
information from Composer metadata for projects installed with Composer.
name: 'Git Deploy'
description: 'Helps sites that are deployed directly from Git repositories.'
core: 8.x
core_version_requirement: ^8 || ^9
type: module
<?php
/**
* @file
* Checks that site can get version information with Git.
*/
/**
* Implements hook_requirements().
*/
function git_deploy_requirements($phase) {
$requirements = [];
if (in_array($phase, ['install', 'runtime'])) {
$message = t('Available');
$severity = REQUIREMENT_OK;
$description = '';
if (!function_exists('exec')) {
$message = t('Disabled exec()');
$description = t('The exec() PHP function is disabled. Git Deploy is unable to work without that.');
$severity = REQUIREMENT_ERROR;
}
elseif (!exec('git')) {
$message = t('Git not found');
$description = t('The git binary is not executable from PHP. Git Deploy is unable to work without that.');
$severity = REQUIREMENT_ERROR;
}
$requirements['git_deploy'] = [
'title' => t('Executable git binary'),
'value' => $message,
'severity' => $severity,
'description' => $description,
];
}
return $requirements;
}
<?php
/**
* @file
* Adds project, version and date information to projects checked out with Git.
*/
use Drupal\Core\Extension\Extension;
/**
* Null device.
*
* If /dev/null does not exist, assume we are on Windows.
*/
define('GIT_DEPLOY_ERROR_DUMP', file_exists('/dev/null') ? '/dev/null' : 'nul');
/**
* Implements hook_system_info_alter().
*/
function git_deploy_system_info_alter(array &$info, Extension $file, $type) {
// Use drupal_static() so we can free up memory from static variables later.
$projects = &drupal_static(__FUNCTION__ . ':projects', []);
$available = &drupal_static(__FUNCTION__ . ':available');
$update = &drupal_static(__FUNCTION__ . ':update', []);
// Core has hard-coded version numbers, so we need to verify them. Otherwise,
// a valid version number indicates that this project was not installed with
// Git.
if (empty($info['hidden']) && (empty($info['version']) || ($file->origin == 'core' ? strstr($info['version'], '-dev') == '-dev' : $info['version'] == \Drupal::VERSION || !preg_match('/^\d\..+\..+/', $info['version'])))) {
// Work around bug that causes Git to fail for users without home directory.
$home = getenv('HOME') === FALSE ? 'HOME=' . DRUPAL_ROOT . ' ' : '';
// Verify that we are in a Git repository. For core, also verify that this
// is really a Drupal repository.
$directory = exec($home . 'git -C ' . escapeshellarg($file->getPath()) . ' rev-parse --show-toplevel 2> ' . GIT_DEPLOY_ERROR_DUMP);
if (!empty($directory) && ($file->origin != 'core' || in_array($directory, [DRUPAL_ROOT, DRUPAL_ROOT . '/core']))) {
// Only check Git once per repository.
if (!isset($projects[$directory])) {
$projects[$directory] = [];
// Make sure Git operates in the right directory.
$git = $home . 'git -C ' . escapeshellarg($directory);
// Ensure file is in repository.
if (exec("$git ls-files " . escapeshellarg(str_replace("$directory/", '', realpath($file->getPathname()))) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP)) {
// Get upstream info.
$upstream = _git_deploy_get_upstream($git, $file->origin == 'core' ? ['8.*', '9.*'] : ['*.*']);
if ($file->origin == 'core') {
$project_name = 'drupal';
}
elseif (isset($upstream['remote'])) {
// Find the project name based on fetch URL.
$fetch_url = exec("$git config --get " . escapeshellarg("remote.$upstream[remote].url") . ' 2> ' . GIT_DEPLOY_ERROR_DUMP);
if (!empty($fetch_url)) {
$project_name = basename($fetch_url, '.git');
$projects[$directory]['project'] = $project_name;
}
}
// Set project datestamp.
if (isset($upstream['datestamp'])) {
$projects[$directory]['datestamp'] = $upstream['datestamp'];
// The '_info_file_ctime' should always get the latest value.
if (empty($info['_info_file_ctime'])) {
$projects[$directory]['_info_file_ctime'] = $upstream['datestamp'];
}
else {
$projects[$directory]['_info_file_ctime'] = max($info['_info_file_ctime'], $upstream['datestamp']);
}
}
// Set version from tag.
if (isset($upstream['tag'])) {
$projects[$directory]['version'] = $upstream['tag'];
}
// If tag was not found, set version from branch.
elseif (isset($upstream['branch'])) {
if ($upstream['branch'] != 'master') {
$projects[$directory]['version'] = "$upstream[branch]-dev";
}
if (\Drupal::moduleHandler()->moduleExists('update') && ($upstream['synced'] || $upstream['branch'] == 'master')) {
if (!isset($available)) {
// We only get this from the cache because an update status
// query is supposed to done only after processing all enabled
// modules and themes.
$available = \Drupal::keyValueExpirable('update_available_releases')
->getAll();
}
if (!empty($available[$project_name]['releases'])) {
if ($upstream['branch'] == 'master') {
// If there's available update_status data, we can use the
// version string the release node pointing to HEAD really
// has.
foreach ($available[$project_name]['releases'] as $release) {
if (isset($release['tag']) && $release['tag'] == 'HEAD') {
$projects[$directory]['version'] = $release['version'];
break;
}
}
}
if ($upstream['synced']) {
// This project's datestamp needs to be synced with upstream
// release.
$version = $projects[$directory]['version'];
if (!isset($available[$project_name]['releases'][$version]) || $projects[$directory]['_info_file_ctime'] > $available[$project_name]['last_fetch']) {
// We need to update available release data for this
// project.
$update[] = $project_name;
}
else {
_git_deploy_datestamp_sync($projects[$directory], $available[$project_name]['releases'][$version]);
}
}
}
}
}
}
}
$info = $projects[$directory] + $info;
}
}
}
/**
* Gets upstream info.
*
* @param string $git
* Git formatted for command line.
* @param string[] $patterns
* List of patterns for matching branch names, without trailing ".x". Must not
* include repository name. Also used to check for release tags.
*
* @return string[]
* Array with the following keys, if found:
* - branch: Best matching remote branch.
* - remote: Remote repository containing best matching branch.
* - tag: Release tag from last common commit in matching branch.
* - datestamp: Unix timestamp of last common commit.
*/
function _git_deploy_get_upstream($git, array $patterns = ['*']) {
$upstream = ['synced' => FALSE];
// Check that there are remote repositories.
exec("$git remote 2> " . GIT_DEPLOY_ERROR_DUMP, $remotes);
if (!empty($remotes)) {
// Get most recent tag.
$tag = exec("$git describe --tags --abbrev=0 --match " . implode(' --match ', substr_replace($patterns, '.*', array_map('strlen', $patterns))) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP);
// Get current commit.
$last_base = $head = exec("$git log -1 --pretty=format:%H 2> " . GIT_DEPLOY_ERROR_DUMP);
// Get tracked upstream branch.
$remote = exec("$git rev-parse --abbrev-ref @{upstream} 2> " . GIT_DEPLOY_ERROR_DUMP);
if ($remote !== '' && preg_match('/^.+\/(?:' . implode('|', str_replace(['.', '*'], ['\.', '\d+'], $patterns)) . ')\.x$/', $remote)) {
// Set remote.
list($upstream['branch'], $upstream['remote']) = array_reverse(explode('/', $remote));
// Find last common commit in both local and upstream.
$last_base = exec("$git merge-base HEAD " . escapeshellarg($remote) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP);
if (!empty($tag)) {
// See if most recent tag is in our branch.
exec("$git describe --tags --contains " . escapeshellarg($last_base) . ' --match ' . escapeshellarg($tag) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP, $output, $status);
}
if (empty($tag) || $status !== 0) {
// Compare with last remote commit for update status.
$upstream['synced'] = $last_base == exec("$git log -1 --pretty=format:%H " . escapeshellarg($remote) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP);
}
else {
$upstream['tag'] = $tag;
}
}
else {
// If local does not track an upstream branch, find best matching remote.
if (in_array('origin', $remotes)) {
// If origin exists, don't check any other remote repositories.
$upstream['remote'] = 'origin';
}
elseif ($remote !== '') {
// If we are tracking a remote repository, don't check any other remote
// repositories.
list(, $upstream['remote']) = array_reverse(explode('/', $remote));
}
if (isset($upstream['remote'])) {
$branch_patterns = substr_replace($patterns, "$upstream[remote]/", 0, 0);
}
else {
$upstream['remote'] = current($remotes);
$branch_patterns = substr_replace($patterns, '*/', 0, 0);
}
if (!empty($tag)) {
// See if we are on a tag.
exec("$git describe --tags --exact-match --match " . escapeshellarg($tag) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP, $output, $status);
}
// If we are not on a tag, find the best matching remote branch.
if (empty($tag) || $status !== 0) {
// Append ".x" to the branch patterns.
$branch_patterns = substr_replace($branch_patterns, '.x', array_map('strlen', $branch_patterns));
// Enclose branch patterns in quotes and join together.
$branch_pattern = implode(' ', array_map('escapeshellarg', $branch_patterns));
// List matching branches by version in descending order.
exec("$git branch -r --list $branch_pattern master 2> " . GIT_DEPLOY_ERROR_DUMP, $branches);
if (!empty($branches)) {
usort($branches, 'version_compare');
foreach (array_reverse(array_map('trim', $branches)) as $branch) {
$tip = exec("$git log -1 --pretty=format:%H " . escapeshellarg($branch) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP);
if ($tip == $head) {
// If remote branch matches local branch, it is the best match.
list($upstream['branch'], $upstream['remote']) = array_reverse(explode('/', $branch));
$last_base = $tip;
// Local history contains last remote commit.
$upstream['synced'] = TRUE;
break;
}
if (isset($upstream['branch'])) {
// Replace branch tip with last common commit.
$tip = exec("$git merge-base HEAD " . escapeshellarg($tip) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP);
if ($tip != $last_base) {
// See if this branch is older than last branch.
exec("$git merge-base --is-ancestor " . escapeshellarg($tip) . ' ' . escapeshellarg($last_base) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP, $output, $status);
}
if ($tip == $last_base || $status === 0) {
// Last remote branch was more recent. For performance, stop
// looking.
break;
}
// This remote branch is more recent.
list($upstream['branch'], $upstream['remote']) = array_reverse(explode('/', $branch));
$last_base = $tip;
}
else {
// Find most recent common commit.
list($upstream['branch'], $upstream['remote']) = array_reverse(explode('/', $branch));
$last_base = exec("$git merge-base HEAD " . escapeshellarg($tip) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP);
// Check for latest commit in local history.
$upstream['synced'] = $last_base == $tip;
}
}
if (!empty($tag)) {
// See if most recent tag is in our branch.
exec("$git describe --tags --contains " . escapeshellarg($last_base) . ' --match ' . escapeshellarg($tag) . ' 2> ' . GIT_DEPLOY_ERROR_DUMP, $output, $status);
if ($status === 0) {
$upstream['tag'] = $tag;
}
}
}
}
else {
$upstream['tag'] = $tag;
}
}
// Find the timestamp for the current commit.
$upstream['datestamp'] = exec("$git log -1 --pretty=format:%at " . escapeshellarg($last_base) . " 2> " . GIT_DEPLOY_ERROR_DUMP);
}
return $upstream;
}
/**
* Implements hook_update_projects_alter().
*/
function git_deploy_update_projects_alter(&$projects) {
// Get projects whose datestamps need to be synced with upstream releases.
$update = drupal_static('git_deploy_system_info_alter:update', []);
// Free up memory.
drupal_static_reset('git_deploy_system_info_alter:projects');
drupal_static_reset('git_deploy_system_info_alter:available');
drupal_static_reset('git_deploy_system_info_alter:update');
if (!empty($update)) {
// Fetch updated release data.
foreach ($update as $project_name) {
\Drupal::service('update.processor')
->processFetchTask($projects[$project_name]);
}
$available = \Drupal::keyValueExpirable('update_available_releases')
->getAll();
foreach ($update as $project_name) {
$project = &$projects[$project_name];
$version = $project['info']['version'];
if (isset($available[$project_name]['releases'][$version])) {
_git_deploy_datestamp_sync($project, $available[$project_name]['releases'][$version]);
}
}
}
}
/**
* Syncs the project datestamp to the release datestamp.
*
* @param array $project
* Project data. Datestamp will be synced if up to date with release.
* @param array $release
* Release data.
*/
function _git_deploy_datestamp_sync(array &$project, array $release) {
// We need to compare commit time to release time because our remote tracking
// branch could be out of date. Allow a 12 hour time difference between
// release and last commit, because dev releases are packaged only twice a
// day. Add a 100-second buffer to account for packaging time.
if ($project['datestamp'] + 43200 + 100 > $release['date']) {
// A dev release for the latest commit may be created later than an official
// release, so use release time only if it is later than the commit time.
$project['datestamp'] = max($release['date'], $project['datestamp']);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment