blob: eb08e7dc31cdb03b5f12a00840c1c292c82cff79 [file] [log] [blame]
package com.mirantis.mk
/**
*
* Git functions
*
*/
/**
* Checkout single git repository
*
* @param path Directory to checkout repository to
* @param url Source Git repository URL
* @param branch Source Git repository branch
* @param credentialsId Credentials ID to use for source Git
* @param poll Enable git polling (default true)
* @param timeout Set checkout timeout (default 10)
* @param depth Git depth param (default 0 means no depth)
* @param reference Git reference param to checkout (default empyt, i.e. no reference)
* @param withWipeOut Enable workspace wipe before checkout
*/
def checkoutGitRepository(path, url, branch, credentialsId = null, poll = true, timeout = 10, depth = 0, reference = '', withWipeOut = false){
def branch_name = reference ? 'FETCH_HEAD' : "*/${branch}"
def scmExtensions = [
[$class: 'CheckoutOption', timeout: timeout],
[$class: 'CloneOption', depth: depth, noTags: false, shallow: depth > 0, timeout: timeout]
]
// wipe workspace before checkout
if (withWipeOut) {
scmExtensions.add([$class: 'WipeWorkspace'])
}
dir(path) {
checkout(
changelog:true,
poll: poll,
scm: [
$class: 'GitSCM',
branches: [[name: branch_name]],
doGenerateSubmoduleConfigurations: false,
extensions: scmExtensions,
submoduleCfg: [],
userRemoteConfigs: [[url: url, credentialsId: credentialsId, refspec: reference]]]
)
}
}
/**
* Parse HEAD of current directory and return commit hash
*/
def getGitCommit() {
git_commit = sh (
script: 'git rev-parse HEAD',
returnStdout: true
).trim()
return git_commit
}
/**
* Change actual working branch of repo
*
* @param path Path to the git repository
* @param branch Branch desired to switch to
*/
def changeGitBranch(path, branch) {
dir(path) {
git_cmd = sh (
script: "git checkout ${branch}",
returnStdout: true
).trim()
}
return git_cmd
}
/**
* Get remote URL
*
* @param name Name of remote (default any)
* @param type Type (fetch or push, default fetch)
*/
def getGitRemote(name = '', type = 'fetch') {
gitRemote = sh (
script: "git remote -v | grep '${name}' | grep ${type} | awk '{print \$2}' | head -1",
returnStdout: true
).trim()
return gitRemote
}
/**
* Get commit message for given commit reference
*/
def getGitCommitMessage(String path, String commitRef = 'HEAD') {
dir(path) {
commitMsg = sh (
script: "git log --format=%B -n 1 ${commitRef}",
returnStdout: true
).trim()
}
return commitMsg
}
/**
* Create new working branch for repo
*
* @param path Path to the git repository
* @param branch Branch desired to switch to
*/
def createGitBranch(path, branch) {
def git_cmd
dir(path) {
git_cmd = sh (
script: "git checkout -b ${branch}",
returnStdout: true
).trim()
}
return git_cmd
}
/**
* Commit changes to the git repo
*
* @param path Path to the git repository
* @param message A commit message
* @param global Use global config
* @param amend Whether to use "--amend" in commit command
*/
def commitGitChanges(path, message, gitEmail='jenkins@localhost', gitName='jenkins-slave', global=false, amend=false) {
def git_cmd
def gitOpts
def global_arg = ''
if (global) {
global_arg = '--global'
}
if (amend) {
gitOpts = '--amend'
} else {
gitOpts = ''
}
def gitEnv = [
"GIT_AUTHOR_NAME='${gitName}'",
"GIT_AUTHOR_EMAIL='${gitEmail}'",
"GIT_COMMITTER_NAME='${gitName}'",
"GIT_COMMITTER_EMAIL='${gitEmail}'",
]
dir(path) {
sh "git config ${global_arg} user.email '${gitEmail}'"
sh "git config ${global_arg} user.name '${gitName}'"
sh(
script: 'git add -A',
returnStdout: true
).trim()
withEnv(gitEnv) {
git_cmd = sh(
script: "git commit ${gitOpts} -m '${message}'",
returnStdout: true
).trim()
}
}
return git_cmd
}
/**
* Push git changes to remote repo
*
* @param path Path to the local git repository
* @param branch Branch on the remote git repository
* @param remote Name of the remote repository
* @param credentialsId Credentials with write permissions
*/
def pushGitChanges(path, branch = 'master', remote = 'origin', credentialsId = null) {
def ssh = new com.mirantis.mk.Ssh()
dir(path) {
if (credentialsId == null) {
sh script: "git push ${remote} ${branch}"
}
else {
ssh.prepareSshAgentKey(credentialsId)
ssh.runSshAgentCommand("git push ${remote} ${branch}")
}
}
}
/**
* Mirror git repository, merge target changes (downstream) on top of source
* (upstream) and push target or both if pushSource is true
*
* @param sourceUrl Source git repository
* @param targetUrl Target git repository
* @param credentialsId Credentials id to use for accessing target repositories
* @param branches List or comma-separated string of branches to sync
* @param followTags Mirror tags
* @param pushSource Push back into source branch, resulting in 2-way sync
* @param pushSourceTags Push target tags into source or skip pushing tags
* @param gitEmail Email for creation of merge commits
* @param gitName Name for creation of merge commits
*/
def mirrorGit(sourceUrl, targetUrl, credentialsId, branches, followTags = false, pushSource = false, pushSourceTags = false, gitEmail = 'jenkins@localhost', gitName = 'Jenkins', sourceRemote = 'origin') {
def common = new com.mirantis.mk.Common()
def ssh = new com.mirantis.mk.Ssh()
if (branches instanceof String) {
branches = branches.tokenize(',')
}
// If both source and target repos are secured and accessible via http/https,
// we need to switch GIT_ASKPASS value when running git commands
def sourceAskPass
def targetAskPass
def sshCreds = common.getCredentialsById(credentialsId, 'sshKey') // True if found
if (sshCreds) {
ssh.prepareSshAgentKey(credentialsId)
ssh.ensureKnownHosts(targetUrl)
sh "git config user.name '${gitName}'"
} else {
withCredentials([[$class : 'UsernamePasswordMultiBinding',
credentialsId : credentialsId,
passwordVariable: 'GIT_PASSWORD',
usernameVariable: 'GIT_USERNAME']]) {
sh """
set +x
git config --global credential.${targetUrl}.username \${GIT_USERNAME}
echo "echo \${GIT_PASSWORD}" > ${WORKSPACE}/${credentialsId}_askpass.sh
chmod +x ${WORKSPACE}/${credentialsId}_askpass.sh
git config user.name \${GIT_USERNAME}
"""
sourceAskPass = env.GIT_ASKPASS ?: ''
targetAskPass = "${WORKSPACE}/${credentialsId}_askpass.sh"
}
}
sh "git config user.email '${gitEmail}'"
def remoteExistence = sh(script: "git remote -v | grep ${TARGET_URL} | grep target", returnStatus: true)
if(remoteExistence == 0) {
// silently try to remove target
sh(script: "git remote remove target", returnStatus: true)
}
sh("git remote add target ${TARGET_URL}")
if (sshCreds) {
ssh.agentSh "git remote update --prune"
} else {
env.GIT_ASKPASS = sourceAskPass
sh "git remote update ${sourceRemote} --prune"
env.GIT_ASKPASS = targetAskPass
sh "git remote update target --prune"
}
for (i=0; i < branches.size; i++) {
branch = branches[i]
sh "git branch | grep ${branch} || git checkout -b ${branch}"
def resetResult = sh(script: "git checkout ${branch} && git reset --hard origin/${branch}", returnStatus: true)
if(resetResult != 0){
common.warningMsg("Cannot reset to origin/${branch} for perform git mirror, trying to reset from target/${branch}")
resetResult = sh(script: "git checkout ${branch} && git reset --hard target/${branch}", returnStatus: true)
if(resetResult != 0){
throw new Exception("Cannot reset even to target/${branch}, git mirroring failed!")
}
}
sh "git ls-tree target/${branch} && git merge --no-edit --ff target/${branch} || echo 'Target repository is empty, skipping merge'"
followTagsArg = followTags ? "--follow-tags" : ""
if (sshCreds) {
ssh.agentSh "git push ${followTagsArg} target HEAD:${branch}"
} else {
sh "git push ${followTagsArg} target HEAD:${branch}"
}
if (pushSource == true) {
followTagsArg = followTags && pushSourceTags ? "--follow-tags" : ""
if (sshCreds) {
ssh.agentSh "git push ${followTagsArg} origin HEAD:${branch}"
} else {
sh "git push ${followTagsArg} origin HEAD:${branch}"
}
}
}
if (followTags == true) {
if (sshCreds) {
ssh.agentSh "git push -f target --tags"
} else {
sh "git push -f target --tags"
}
if (pushSourceTags == true) {
if (sshCreds) {
ssh.agentSh "git push -f origin --tags"
} else {
sh "git push -f origin --tags"
}
}
}
sh "git remote rm target"
if (!sshCreds) {
sh "set +x; rm -f ${targetAskPass}"
sh "git config --global --unset credential.${targetUrl}.username"
}
}
/**
* Return all branches for the defined git repository that match the matcher.
*
* @param repoUrl URL of git repository
* @param branchMatcher matcher to filter out the branches (If '' or '*', returns all branches without filtering)
* @return branchesList list of branches
*/
def getBranchesForGitRepo(repoUrl, branchMatcher = ''){
if (branchMatcher.equals("*")) {
branchMatcher = ''
}
branchesList = sh (
script: "git ls-remote --heads ${repoUrl} | cut -f2 | grep -e '${branchMatcher}' | sed 's/refs\\/heads\\///g'",
returnStdout: true
).trim()
return branchesList.tokenize('\n')
}
/**
* Method for preparing a tag to be SemVer 2 compatible, and can handle next cases:
* - length of tag splitted by dots is more than 3
* - first part of splitted tag starts not from digit
* - length of tag is lower than 3
*
* @param tag String which contains a git tag from repository
* @return HashMap HashMap in the form: ['version': 'x.x.x', 'extra': 'x.x.x'], extra
* is added only if size of original tag splitted by dots is more than 3
*/
def prepareTag(tag){
def parts = tag.tokenize('.')
def res = [:]
// Handle case with tags like v1.1.1
parts[0] = parts[0].replaceFirst("[^\\d.]", '')
// handle different sizes of tags - 1.1.1.1 or 1.1.1.1rc1
if (parts.size() > 3){
res['extra'] = parts[3..-1].join('.')
} else if (parts.size() < 3){
(parts.size()..2).each {
parts[it] = '0'
}
}
res['version'] = "${parts[0]}.${parts[1]}.${parts[2]}"
return res
}
/**
* Method for incrementing SemVer 2 compatible version
*
* @param version String which contains main part of SemVer2 version - '2.1.0'
* @return string String conaining version with Patch part of version incremented by 1
*/
def incrementVersion(version){
def parts = checkVersion(version)
return "${parts[0]}.${parts[1]}.${parts[2].toInteger() + 1}"
}
/**
* Method for checking whether version is compatible with Sem Ver 2
*
* @param version String which contains main part of SemVer2 version - '2.1.0'
* @return list With 3 strings as result of splitting version by dots
*/
def checkVersion(version) {
def parts = version.tokenize('.')
if (parts.size() != 3 || !(parts[0] ==~ /^\d+/)) {
error "Bad version ${version}"
}
return parts
}
/**
* Method for constructing SemVer2 compatible version from tag in Git repository:
* - if current commit matches the last tag, last tag will be returned as version
* - if no tag found assuming no release was done, version will be 0.0.1 with pre release metadata
* - if tag found - patch part of version will be incremented and pre-release metadata will be added
*
*
* @param repoDir String which contains path to directory with git repository
* @param allowNonSemVer2 Bool whether to allow working with tags which aren't compatible
* with Sem Ver 2 (not in form X.Y.Z). if set to true tag will be
* converted to Sem Ver 2 version e.g tag 1.1.1.1rc1 -> version 1.1.1-1rc1
* @return version String
*/
def getVersion(repoDir, allowNonSemVer2 = false) {
def common = new com.mirantis.mk.Common()
dir(repoDir){
def cmd = common.shCmdStatus('git describe --tags --first-parent --abbrev=0')
def tag_data = [:]
def last_tag = cmd['stdout'].trim()
def commits_since_tag
if (cmd['status'] != 0){
if (cmd['stderr'].contains('fatal: No names found, cannot describe anything')){
common.warningMsg('No parent tag found, using initial version 0.0.0')
tag_data['version'] = '0.0.0'
commits_since_tag = sh(script: 'git rev-list --count HEAD', returnStdout: true).trim()
} else {
error("Something went wrong, cannot find git information ${cmd['stderr']}")
}
} else {
tag_data['version'] = last_tag
commits_since_tag = sh(script: "git rev-list --count ${last_tag}..HEAD", returnStdout: true).trim()
}
try {
checkVersion(tag_data['version'])
} catch (Exception e) {
if (allowNonSemVer2){
common.errorMsg(
"""Git tag isn't compatible with SemVer2, but allowNonSemVer2 is set.
Trying to convert git tag to Sem Ver 2 compatible version
${e.message}""")
tag_data = prepareTag(tag_data['version'])
} else {
error("Git tag isn't compatible with SemVer2\n${e.message}")
}
}
// If current commit is exact match to the first parent tag than return it
def pre_release_meta = []
if (tag_data.get('extra')){
pre_release_meta.add(tag_data['extra'])
}
if (common.shCmdStatus('git describe --tags --first-parent --exact-match')['status'] == 0){
if (pre_release_meta){
return "${tag_data['version']}-${pre_release_meta[0]}"
} else {
return tag_data['version']
}
}
// If we away from last tag for some number of commits - add additional metadata and increment version
pre_release_meta.add(commits_since_tag)
def next_version = incrementVersion(tag_data['version'])
def commit_sha = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
return "${next_version}-${pre_release_meta.join('.')}-${commit_sha}"
}
}
/**
* Method for uploading a change request
*
* @param repo String which contains path to directory with git repository
* @param credentialsId Credentials id to use for accessing target repositories
* @param commit Id of commit which should be uploaded
* @param branch Name of the branch for uploading
* @param topic Topic of the change
*
*/
def pushForReview(repo, credentialsId, commit, branch, topic='', remote='origin') {
def common = new com.mirantis.mk.Common()
def ssh = new com.mirantis.mk.Ssh()
common.infoMsg("Uploading commit ${commit} to ${branch} for review...")
def pushArg = "${commit}:refs/for/${branch}"
def process = [:]
if (topic){
pushArg += '%topic=' + topic
}
dir(repo){
ssh.prepareSshAgentKey(credentialsId)
ssh.runSshAgentCommand("git push ${remote} ${pushArg}")
}
}
/**
* Generates a commit message with predefined or auto generate change id. If change
* id isn't provided, changeIdSeed and current sha of git head will be used in
* generation of commit change id.
*
* @param repo String which contains path to directory with git repository
* @param message Commit message main part
* @param changeId User defined change-id usually sha1 hash
* @param changeIdSeed Custom part of change id which can be added during change id generation
*
*
* @return commitMessage Multiline String with generated commit message
*/
def genCommitMessage(repo, message, changeId = '', changeIdSeed = ''){
def git = new com.mirantis.mk.Git()
def common = new com.mirantis.mk.Common()
def commitMessage
def id = changeId
def seed = changeIdSeed
if (!id) {
if (!seed){
seed = common.generateRandomHashString(32)
}
def head_sha
dir(repo){
head_sha = git.getGitCommit()
}
id = 'I' + sh(script: 'echo -n ' + seed + head_sha + ' | sha1sum | awk \'{print $1}\'', returnStdout: true)
}
commitMessage =
"""${message}
|Change-Id: ${id}
""".stripMargin()
return commitMessage
}
/**
* Update (or create if cannot find) gerrit change request
*
* @param params Map of parameters to customize commit
* - gerritAuth A map containing information about Gerrit. Should include HOST, PORT and USER
* - credentialsId Jenkins credentials id for gerrit
* - repo Local directory with repository
* - comment Commit comment
* - change_id_seed Custom part of change id which can be added during change id generation
* - branch Name of the branch for uploading
* - topic Topic of the change
* - project Gerrit project to search in for gerrit change request
* - status Change request's status to search for
* - changeAuthorEmail Author's email of the change
* - changeAuthorName Author's name of the change
* - forceUpdate Whether to update change if no diff between local state and remote
* - gerritPatch Maps with patch information (result of gerrit.findGerritChange)
* - amend Do amend current patch
*/
def updateChangeRequest(Map params) {
def gerrit = new com.mirantis.mk.Gerrit()
def common = new com.mirantis.mk.Common()
def commitMessage
def creds = params['credentialsId']
def repo = params['repo']
def comment = params['comment']
def change_id_seed = params.get('change_id_seed', JOB_NAME)
def branch = params['branch']
def topic = params['topic']
def changeAuthorEmail = params['changeAuthorEmail']
def changeAuthorName = params['changeAuthorName']
def forceUpdate = params.get('forceUpdate', true)
def amend = params.get('amend', false)
def jsonChange = params.get('gerritPatch', [:])
def changeId = params.get('changeId', '')
def remote = params.get('remote', 'origin')
def commit
if (!jsonChange) {
def auth = params['gerritAuth']
def status = params.get('status', 'open')
def project = params['project']
def changeParams = ['owner': auth['USER'], 'status': status, 'project': project, 'branch': branch, 'topic': topic]
def gerritChange = gerrit.findGerritChange(creds, auth, changeParams, '--current-patch-set')
if (gerritChange) {
jsonChange = readJSON text: gerritChange
}
}
if (jsonChange) {
changeId = jsonChange['id']
if(!forceUpdate){
def ref = jsonChange['currentPatchSet']['ref']
def res
dir(repo){
sshagent (credentials: [creds]){
res = common.shCmdStatus("git fetch origin ${ref} && git diff --quiet --exit-code FETCH_HEAD")["status"]
}
}
if (res == 0){
common.infoMsg("Current patch set ${ref} is up to date, no need to update")
return
}
}
}
commitMessage = genCommitMessage(repo, comment, changeId, change_id_seed)
commitGitChanges(repo, commitMessage, changeAuthorEmail, changeAuthorName, false, amend)
dir(repo){
commit = getGitCommit()
}
pushForReview(repo, creds, commit, branch, topic, remote)
}
/**
* Create new working branch for repo from patch if it exists
*
* @param params Map of parameters to customize commit
* - gerritAuth A map containing information about Gerrit. Should include HOST, PORT and USER
* - credentialsId Jenkins credentials id for gerrit
* - repo Local directory with repository
* - branch Name of the branch for uploading
* - topic Topic of the change
* - project Gerrit project to search in for gerrit change request
* - status Change request's status to search for
*/
def createGitBranchFromRef(Map params) {
def gerrit = new com.mirantis.mk.Gerrit()
def common = new com.mirantis.mk.Common()
def auth = params['gerritAuth']
def creds = params['credentialsId']
def repo = params['repo']
def branch = params['branch']
def topic = params['topic']
def project = params['project']
def status = params.get('status', 'open')
def localBranch = "branch_${topic}"
def jsonChange = [:]
def changeParams = ['owner': auth['USER'], 'status': status, 'project': project, 'branch': branch, 'topic': topic]
def gerritChange = gerrit.findGerritChange(creds, auth, changeParams, '--current-patch-set')
if (gerritChange) {
jsonChange = readJSON text: gerritChange
def ref = jsonChange['currentPatchSet']['ref']
changeId = jsonChange['id']
dir(repo){
sshagent (credentials: [creds]){
common.shCmdStatus("git fetch origin ${ref} && git checkout -b ${localBranch} FETCH_HEAD")
}
}
}
else {
createGitBranch(repo, localBranch)
}
return jsonChange
}
/**
* Return array with branches that contains specific commit ref
*
* @param path Path to the git repository
* @param ref search ref
*/
def getBranchesContainsRef(String path, String ref) {
List result = []
dir(path) {
def gitResult = sh (
script: "git branch --all --contains ${ref}",
returnStdout: true
)
for (line in gitResult.split('\n')) {
if (! line.contains("HEAD detached")) {
result.add(line.trim().replaceAll('remotes/origin/', ''))
}
}
}
return result
}
/**
* Return array with branches that contains specific tag
*
* @param path Path to the git repository
* @param tag search tag
*/
def getBranchesContainsTag(String path, String tag) {
return getBranchesContainsRef(path, "tags/${tag}")
}