isValidGitRepo()) {
drupal_set_message(t('The repository %name at @root is not a valid Git bare repository.', array('%name' => $repository->name, '@root' => $repository->root)), 'error');
return FALSE;
}
$root = escapeshellcmd($repository->root);
putenv("GIT_DIR=$root");
if ($repository->locked != 0) {
drupal_set_message(t('This repository is locked, there is already a fetch in progress. If this is not the case, press the clear lock button.'), 'error');
return FALSE;
}
$repository->updateLock();
$repository->update();
// 1. Process branches
// Fetch branches from the repo and load them from the db.
$branches_in_repo = $repository->fetchBranches();
$branches_in_db = $repository->loadBranches(array(), array(), array('may cache' => FALSE));
$branches_in_db_by_name = array();
foreach ($branches_in_db as $branch) { // Branches get keyed on id; key on name instead.
$branches_in_db_by_name[$branch->name] = $branch;
}
unset($branches_in_db);
// Determine whether we've got branch changes to make.
$branches_new = array_diff_key($branches_in_repo, $branches_in_db_by_name);
$branches_deleted = array_diff_key($branches_in_db_by_name, $branches_in_repo);
// Insert new branches in the repository. Later all commits in these new
// branches will be updated.
// Unfortunately we can't say anything about the branch author at this time.
// The post-update hook could do this, though.
// We also can't insert a VCOperation which adds the branch, because
// we don't know anything about the branch. This is all stuff a hook could
// figure out.
foreach($branches_new as $branch) {
$branch->insert();
}
// reload branches, after they was inserted
$branches_in_db = $repository->loadBranches();
$branches_in_db_by_name = array();
foreach ($branches_in_db as $branch) {
$branches_in_db_by_name[$branch->name] = $branch;
}
// Deleted branches are removed, commits in them are not!
foreach($branches_deleted as $branch) {
$branch->delete();
}
// 2. Process commits
// Fetch commits from the repo and load them from the db.
$commits_in_repo_hashes = $repository->fetchCommits();
$commits_in_db = $repository->loadCommits(array(), array(), array('may cache' => FALSE));
$commits_in_db_hashes = array();
foreach ($commits_in_db as $commit) {
$commits_in_db_hashes[] = $commit->revision;
}
unset($commits_in_db);
$commits_new = array_diff($commits_in_repo_hashes, $commits_in_db_hashes);
// Insert new commits in the database.
foreach ($commits_new as $hash) {
$command = "show --numstat --summary --pretty=format:\"%H%n%P%n%an%n%ae%n%cn%n%ce%n%ct%n%s%n%b%nENDOFOUTPUTGITMESSAGEHERE\" " . escapeshellarg($hash);
$output = _versioncontrol_git_log_exec($command);
_versioncontrol_git_log_parse_and_insert_commit($repository, $output, $branches_in_db_by_name);
}
// 3. Process tags
// Insert new tags in the database.
$tags_in_repo = $repository->fetchTags();
$tags_in_db = $repository->loadTags();
$tags_in_db_by_name = array();
foreach ($tags_in_db as $tag) {
$tags_in_db_by_name[$tag->name] = $tag;
}
unset($tags_in_db);
// Deleting tags is *not* supported. Read the manual if you want to know why...
// Check for new tags.
$tags_new = array_diff_key($tags_in_repo, $tags_in_db_by_name);
if (!empty($tags_new)) {
_versioncontrol_git_log_process_tags($repository, $tags_new);
}
// Update repository updated field. Displayed on administration interface for documentation purposes.
$repository->updated = time();
$repository->updateLock(0);
$repository->update();
return TRUE;
}
/// All general functions
/**
* Execute a Git command using the root context and the command to be executed.
* @param string $command Command to execute.
* @return mixed Logged output from the command in either array of file pointer form.
*/
function _versioncontrol_git_log_exec($command) {
$logs = array();
$git_bin = variable_get('versioncontrol_git_binary_path', 'git');
if ($errors = _versioncontrol_git_binary_check_path($git_bin)) {
watchdog('versioncontrol_git', '!errors', array('!errors' => implode(' ', $errors)), WATCHDOG_ERROR);
return array();
}
exec(escapeshellcmd("$git_bin $command"), $logs);
array_unshift($logs, ''); // FIXME doing it this way is just wrong.
reset($logs); // Reset the array pointer, so that we can use next().
return $logs;
}
/// All commit related function
/**
* Returns an array of all branches a given commit is in.
* @param string $revision
* @param array $branch_label_list
* @return VersioncontrolBranch
*/
function _versioncontrol_git_log_get_branches_of_commit($revision, $branch_label_list) {
$exec = 'branch --contains ' . escapeshellarg($revision);
$logs = _versioncontrol_git_log_exec($exec);
$branches = array();
while (($line = next($logs)) !== FALSE) {
$line = trim($line);
if($line[0] == '*') {
$line = substr($line, 2);
}
$branches[] = $branch_label_list[$line];
}
return $branches;
}
/**
* This function returns all commits in the repository
* @param $repository
* @return array An array of strings with all commit id's in it
*/
function _versioncontrol_git_log_get_commits_in_repo($repository) {
$logs = _versioncontrol_git_log_exec("rev-list --all");
$commits = array();
while (($line = next($logs)) !== FALSE) {
$commits[] = trim($line);
}
return $commits;
}
/**
* This function returns all commits in the given branch.
* @param VersioncontrolBranch $branch
* @return array An array of strings with all commit id's in it
*/
function _versioncontrol_git_log_get_commits_in_label($label) {
$logs = _versioncontrol_git_log_exec("rev-list " . escapeshellarg($label->name) . " --");
$commits = array();
while (($line = next($logs)) !== FALSE) {
$commits[] = trim($line);
}
return $commits;
}
/**
* A function to fill in the source_item for a specific VersioncontrolItem.
*
* Now VCS API assumes there is only one source item, so merges can not be
* tracked propertly there, and we are neither tracking on git backend for
* now.
* For merges we are choosing the first parent git-log show.
*
* @param VersioncontrolItem &$item
* @param array $parents The parent commit(s)
* @return none
*/
function _versioncontrol_git_fill_source_item(&$item, $parents, $inc_data) {
$data = array(
'type' => VERSIONCONTROL_ITEM_FILE,
'repository' => $inc_data['repository'],
'path' => $item->path,
);
$path_stripped = substr($item->path, 1);
// using -5 to let detect merges until 4 parents, merging more than 4 parents in one operation is insane!
// use also --first-parent to retrieve only one parent for the current support of VCS API
$cmd = 'log --first-parent --follow --pretty=format:"%H" -5 ' . escapeshellarg($item->revision) . ' -- ' . escapeshellarg($path_stripped);
$prev_revisions = _versioncontrol_git_log_exec($cmd);
next($prev_revisions); // grab our hash out
if (($parent_hash = next($prev_revisions)) !== FALSE) { // get the first parent hash
$data['revision'] = trim($parent_hash);
// just fill an object from scratch
$source_item = new VersioncontrolGitItem($item->getBackend());
$source_item->build($data);
$item->setSourceItem($source_item);
}
//TODO unify the way to fail
}
/**
* Takes parts of the output of git log and returns all affected OperationItems for a commit.
* @param array $logs
* @param string $line
* @param string $revision
* @param array $parents The parent commit(s)
* @param bool $merge
* @return array All items affected by a commit.
*/
function _versioncontrol_git_parse_items(&$logs, &$line, $data, $parents) {
$op_items = array();
// Parse the diffstat for the changed files.
do {
if (!preg_match('/^(\S+)' . "\t" . '(\S+)' . "\t" . '(.+)$/', $line, $matches)) {
break;
}
$path = '/'. $matches[3];
$op_items[$path] = new VersioncontrolGitItem($data['backend']);
$data['path'] = $path;
$op_items[$path]->build($data);
unset($data['path']);
if (is_numeric($matches[1]) && is_numeric($matches[2])) {
$op_items[$path]->line_changes_added = $matches[1];
$op_items[$path]->line_changes_removed = $matches[2];
}
// extract blob
$command = 'ls-tree -r ' . escapeshellarg($data['revision']) . ' ' . escapeshellarg($matches[3]);
$lstree_lines = _versioncontrol_git_log_exec($command);
$blob_hash = _versioncontrol_git_parse_item_blob($lstree_lines);
$op_items[$path]->blob_hash = $blob_hash;
} while (($line = next($logs)) !== FALSE);
// Parse file actions.
do {
if (!preg_match('/^ (\S+) (\S+) (\S+) (.+)$/', $line, $matches)) {
break;
}
// We also can get 'mode' here if someone changes the file permissions.
if ($matches[1] == 'create') {
$op_items['/'. $matches[4]]->action = VERSIONCONTROL_ACTION_ADDED;
// extract blob
$command = 'ls-tree -r ' . escapeshellarg($data['revision']) . ' ' . escapeshellarg($matches[4]);
$lstree_lines = _versioncontrol_git_log_exec($command);
$blob_hash = _versioncontrol_git_parse_item_blob($lstree_lines);
$op_items['/'. $matches[4]]->blob_hash = $blob_hash;
}
else if ($matches[1] == 'delete') {
$op_items['/'. $matches[4]]->action = VERSIONCONTROL_ACTION_DELETED;
$op_items['/'. $matches[4]]->type = VERSIONCONTROL_ITEM_FILE_DELETED;
}
} while (($line = next($logs)) !== FALSE);
// Fill in the source_items for non-added items
foreach ($op_items as $path => &$item) {
if ($item->action != VERSIONCONTROL_ACTION_ADDED) {
_versioncontrol_git_fill_source_item($item, $parents, $data);
}
}
return $op_items;
}
/**
* Parse ls-tree with one commit hash and one item.
*/
function _versioncontrol_git_parse_item_blob($lines) {
$line = next($lines);
// output: SP SP