blob: 00df961cb6dff4cbc107230b63d2d006fa3d3e61 [file] [log] [blame]
Sergey Kolekonovba203982016-12-21 18:32:17 +04001package com.mirantis.mk
2
3/**
4 *
5 * Git functions
6 *
7 */
8
9/**
10 * Checkout single git repository
11 *
12 * @param path Directory to checkout repository to
13 * @param url Source Git repository URL
14 * @param branch Source Git repository branch
15 * @param credentialsId Credentials ID to use for source Git
Jakub Josef7dccebe2017-03-06 18:08:32 +010016 * @param poll Enable git polling (default true)
17 * @param timeout Set checkout timeout (default 10)
Jakub Josef61f29e62017-03-08 16:42:06 +010018 * @param depth Git depth param (default 0 means no depth)
Alexandr Lovtsove818e102019-07-29 14:45:01 +030019 * @param reference Git reference param to checkout (default empyt, i.e. no reference)
Sergey Kolekonovba203982016-12-21 18:32:17 +040020 */
Alexandr Lovtsove818e102019-07-29 14:45:01 +030021def checkoutGitRepository(path, url, branch, credentialsId = null, poll = true, timeout = 10, depth = 0, reference = ''){
Sergey Kolekonovba203982016-12-21 18:32:17 +040022 dir(path) {
Jakub Josef6fa8cb12017-03-06 18:20:08 +010023 checkout(
24 changelog:true,
25 poll: poll,
26 scm: [
27 $class: 'GitSCM',
28 branches: [[name: "*/${branch}"]],
29 doGenerateSubmoduleConfigurations: false,
30 extensions: [
Jakub Josef61f29e62017-03-08 16:42:06 +010031 [$class: 'CheckoutOption', timeout: timeout],
Alexandr Lovtsove818e102019-07-29 14:45:01 +030032 [$class: 'CloneOption', depth: depth, noTags: false, reference: reference, shallow: depth > 0, timeout: timeout]],
Jakub Josef6fa8cb12017-03-06 18:20:08 +010033 submoduleCfg: [],
34 userRemoteConfigs: [[url: url, credentialsId: credentialsId]]]
35 )
Sergey Kolekonovba203982016-12-21 18:32:17 +040036 sh(returnStdout: true, script: 'git rev-parse HEAD').trim()
37 }
38}
39
40/**
41 * Parse HEAD of current directory and return commit hash
42 */
43def getGitCommit() {
44 git_commit = sh (
45 script: 'git rev-parse HEAD',
46 returnStdout: true
47 ).trim()
48 return git_commit
49}
50
51/**
Ales Komarekfb7cbcb2017-02-24 14:02:03 +010052 * Change actual working branch of repo
53 *
54 * @param path Path to the git repository
55 * @param branch Branch desired to switch to
56 */
57def changeGitBranch(path, branch) {
58 dir(path) {
59 git_cmd = sh (
Leontii Istominb4f4ae12018-02-27 20:25:43 +010060 script: "git checkout ${branch}",
Ales Komarekfb7cbcb2017-02-24 14:02:03 +010061 returnStdout: true
62 ).trim()
63 }
64 return git_cmd
65}
66
67/**
Ales Komarekc3a8b972017-03-24 13:57:25 +010068 * Get remote URL
69 *
70 * @param name Name of remote (default any)
71 * @param type Type (fetch or push, default fetch)
72 */
73def getGitRemote(name = '', type = 'fetch') {
74 gitRemote = sh (
75 script: "git remote -v | grep '${name}' | grep ${type} | awk '{print \$2}' | head -1",
76 returnStdout: true
77 ).trim()
78 return gitRemote
79}
80
81/**
82 * Create new working branch for repo
83 *
84 * @param path Path to the git repository
85 * @param branch Branch desired to switch to
86 */
87def createGitBranch(path, branch) {
88 def git_cmd
89 dir(path) {
90 git_cmd = sh (
91 script: "git checkout -b ${branch}",
92 returnStdout: true
93 ).trim()
94 }
95 return git_cmd
96}
97
98/**
Ales Komarekfb7cbcb2017-02-24 14:02:03 +010099 * Commit changes to the git repo
100 *
101 * @param path Path to the git repository
102 * @param message A commit message
Denis Egorenkof4c45512019-03-04 15:53:36 +0400103 * @param global Use global config
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300104 * @param amend Whether to use "--amend" in commit command
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100105 */
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300106def commitGitChanges(path, message, gitEmail='jenkins@localhost', gitName='jenkins-slave', global=false, amend=false) {
Ales Komarekc3a8b972017-03-24 13:57:25 +0100107 def git_cmd
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300108 def gitOpts
Denis Egorenkof4c45512019-03-04 15:53:36 +0400109 def global_arg = ''
110 if (global) {
111 global_arg = '--global'
112 }
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300113 if (amend) {
114 gitOpts = '--amend'
115 } else {
116 gitOpts = ''
117 }
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100118 dir(path) {
Denis Egorenkof4c45512019-03-04 15:53:36 +0400119 sh "git config ${global_arg} user.email '${gitEmail}'"
120 sh "git config ${global_arg} user.name '${gitName}'"
Tomáš Kukráldf7bebc2017-03-27 15:12:43 +0200121
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100122 sh(
123 script: 'git add -A',
124 returnStdout: true
125 ).trim()
126 git_cmd = sh(
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300127 script: "git commit ${gitOpts} -m '${message}'",
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100128 returnStdout: true
129 ).trim()
130 }
131 return git_cmd
132}
133
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100134/**
135 * Push git changes to remote repo
136 *
Ales Komarekc3a8b972017-03-24 13:57:25 +0100137 * @param path Path to the local git repository
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100138 * @param branch Branch on the remote git repository
139 * @param remote Name of the remote repository
Ales Komarekc3a8b972017-03-24 13:57:25 +0100140 * @param credentialsId Credentials with write permissions
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100141 */
Ales Komarekc3a8b972017-03-24 13:57:25 +0100142def pushGitChanges(path, branch = 'master', remote = 'origin', credentialsId = null) {
143 def ssh = new com.mirantis.mk.Ssh()
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100144 dir(path) {
Ales Komarekc3a8b972017-03-24 13:57:25 +0100145 if (credentialsId == null) {
146 sh script: "git push ${remote} ${branch}"
147 }
148 else {
149 ssh.prepareSshAgentKey(credentialsId)
150 ssh.runSshAgentCommand("git push ${remote} ${branch}")
151 }
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100152 }
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100153}
154
Ales Komarekc3a8b972017-03-24 13:57:25 +0100155
Sergey Kolekonovba203982016-12-21 18:32:17 +0400156/**
Filip Pytloun49d66302017-03-06 10:26:22 +0100157 * Mirror git repository, merge target changes (downstream) on top of source
158 * (upstream) and push target or both if pushSource is true
159 *
160 * @param sourceUrl Source git repository
161 * @param targetUrl Target git repository
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400162 * @param credentialsId Credentials id to use for accessing target repositories
Filip Pytloun49d66302017-03-06 10:26:22 +0100163 * @param branches List or comma-separated string of branches to sync
164 * @param followTags Mirror tags
165 * @param pushSource Push back into source branch, resulting in 2-way sync
166 * @param pushSourceTags Push target tags into source or skip pushing tags
167 * @param gitEmail Email for creation of merge commits
168 * @param gitName Name for creation of merge commits
Sergey Kolekonovba203982016-12-21 18:32:17 +0400169 */
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400170def mirrorGit(sourceUrl, targetUrl, credentialsId, branches, followTags = false, pushSource = false, pushSourceTags = false, gitEmail = 'jenkins@localhost', gitName = 'Jenkins', sourceRemote = 'origin') {
Jakub Josef668dc2b2017-06-19 16:55:26 +0200171 def common = new com.mirantis.mk.Common()
172 def ssh = new com.mirantis.mk.Ssh()
Sergey Kolekonovba203982016-12-21 18:32:17 +0400173 if (branches instanceof String) {
174 branches = branches.tokenize(',')
175 }
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400176 // If both source and target repos are secured and accessible via http/https,
177 // we need to switch GIT_ASKPASS value when running git commands
178 def sourceAskPass
179 def targetAskPass
Sergey Kolekonovba203982016-12-21 18:32:17 +0400180
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400181 def sshCreds = common.getCredentialsById(credentialsId, 'sshKey') // True if found
182 if (sshCreds) {
183 ssh.prepareSshAgentKey(credentialsId)
184 ssh.ensureKnownHosts(targetUrl)
185 sh "git config user.name '${gitName}'"
186 } else {
187 withCredentials([[$class : 'UsernamePasswordMultiBinding',
188 credentialsId : credentialsId,
189 passwordVariable: 'GIT_PASSWORD',
190 usernameVariable: 'GIT_USERNAME']]) {
191 sh """
192 set +x
193 git config --global credential.${targetUrl}.username \${GIT_USERNAME}
194 echo "echo \${GIT_PASSWORD}" > ${WORKSPACE}/${credentialsId}_askpass.sh
195 chmod +x ${WORKSPACE}/${credentialsId}_askpass.sh
196 git config user.name \${GIT_USERNAME}
197 """
198 sourceAskPass = env.GIT_ASKPASS ?: ''
199 targetAskPass = "${WORKSPACE}/${credentialsId}_askpass.sh"
200 }
201 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100202 sh "git config user.email '${gitEmail}'"
Filip Pytloun49d66302017-03-06 10:26:22 +0100203
Jakub Josef1caa7ae2017-08-21 16:39:00 +0200204 def remoteExistence = sh(script: "git remote -v | grep ${TARGET_URL} | grep target", returnStatus: true)
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400205 if(remoteExistence == 0) {
206 // silently try to remove target
207 sh(script: "git remote remove target", returnStatus: true)
Jakub Josef1caa7ae2017-08-21 16:39:00 +0200208 }
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400209 sh("git remote add target ${TARGET_URL}")
210 if (sshCreds) {
211 ssh.agentSh "git remote update --prune"
212 } else {
213 env.GIT_ASKPASS = sourceAskPass
214 sh "git remote update ${sourceRemote} --prune"
215 env.GIT_ASKPASS = targetAskPass
216 sh "git remote update target --prune"
217 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100218
Sergey Kolekonovba203982016-12-21 18:32:17 +0400219 for (i=0; i < branches.size; i++) {
220 branch = branches[i]
Jakub Josef668dc2b2017-06-19 16:55:26 +0200221 sh "git branch | grep ${branch} || git checkout -b ${branch}"
222 def resetResult = sh(script: "git checkout ${branch} && git reset --hard origin/${branch}", returnStatus: true)
223 if(resetResult != 0){
224 common.warningMsg("Cannot reset to origin/${branch} for perform git mirror, trying to reset from target/${branch}")
225 resetResult = sh(script: "git checkout ${branch} && git reset --hard target/${branch}", returnStatus: true)
226 if(resetResult != 0){
227 throw new Exception("Cannot reset even to target/${branch}, git mirroring failed!")
228 }
229 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400230
Sergey Kolekonovba203982016-12-21 18:32:17 +0400231 sh "git ls-tree target/${branch} && git merge --no-edit --ff target/${branch} || echo 'Target repository is empty, skipping merge'"
232 followTagsArg = followTags ? "--follow-tags" : ""
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400233 if (sshCreds) {
234 ssh.agentSh "git push ${followTagsArg} target HEAD:${branch}"
235 } else {
236 sh "git push ${followTagsArg} target HEAD:${branch}"
237 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100238
239 if (pushSource == true) {
240 followTagsArg = followTags && pushSourceTags ? "--follow-tags" : ""
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400241 if (sshCreds) {
242 ssh.agentSh "git push ${followTagsArg} origin HEAD:${branch}"
243 } else {
244 sh "git push ${followTagsArg} origin HEAD:${branch}"
245 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100246 }
247 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100248 if (followTags == true) {
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400249 if (sshCreds) {
250 ssh.agentSh "git push -f target --tags"
251 } else {
252 sh "git push -f target --tags"
253 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100254
255 if (pushSourceTags == true) {
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400256 if (sshCreds) {
257 ssh.agentSh "git push -f origin --tags"
258 } else {
259 sh "git push -f origin --tags"
260 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100261 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400262 }
Jakub Josefecf8b452017-04-20 13:34:29 +0200263 sh "git remote rm target"
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400264 if (!sshCreds) {
265 sh "set +x; rm -f ${targetAskPass}"
266 sh "git config --global --unset credential.${targetUrl}.username"
267 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400268}
Martin Polreich765f7ba2019-03-12 16:39:25 +0100269
270
271/**
272 * Return all branches for the defined git repository that match the matcher.
273 *
274 * @param repoUrl URL of git repository
275 * @param branchMatcher matcher to filter out the branches (If '' or '*', returns all branches without filtering)
276 * @return branchesList list of branches
277 */
278
279def getBranchesForGitRepo(repoUrl, branchMatcher = ''){
280
281 if (branchMatcher.equals("*")) {
282 branchMatcher = ''
283 }
284 branchesList = sh (
285 script: "git ls-remote --heads ${repoUrl} | cut -f2 | grep -e '${branchMatcher}' | sed 's/refs\\/heads\\///g'",
286 returnStdout: true
287 ).trim()
288 return branchesList.tokenize('\n')
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300289}
290
Mykyta Karpin82437932019-06-06 14:08:18 +0300291/**
292 * Method for preparing a tag to be SemVer 2 compatible, and can handle next cases:
293 * - length of tag splitted by dots is more than 3
294 * - first part of splitted tag starts not from digit
295 * - length of tag is lower than 3
296 *
297 * @param tag String which contains a git tag from repository
298 * @return HashMap HashMap in the form: ['version': 'x.x.x', 'extra': 'x.x.x'], extra
299 * is added only if size of original tag splitted by dots is more than 3
300 */
301
302def prepareTag(tag){
303 def parts = tag.tokenize('.')
304 def res = [:]
305 // Handle case with tags like v1.1.1
306 parts[0] = parts[0].replaceFirst("[^\\d.]", '')
307 // handle different sizes of tags - 1.1.1.1 or 1.1.1.1rc1
308 if (parts.size() > 3){
309 res['extra'] = parts[3..-1].join('.')
310 } else if (parts.size() < 3){
311 (parts.size()..2).each {
312 parts[it] = '0'
313 }
314 }
315 res['version'] = "${parts[0]}.${parts[1]}.${parts[2]}"
316 return res
317}
318
319/**
320 * Method for incrementing SemVer 2 compatible version
321 *
322 * @param version String which contains main part of SemVer2 version - '2.1.0'
323 * @return string String conaining version with Patch part of version incremented by 1
324 */
325
326def incrementVersion(version){
327 def parts = checkVersion(version)
328 return "${parts[0]}.${parts[1]}.${parts[2].toInteger() + 1}"
329}
330
331/**
332 * Method for checking whether version is compatible with Sem Ver 2
333 *
334 * @param version String which contains main part of SemVer2 version - '2.1.0'
335 * @return list With 3 strings as result of splitting version by dots
336 */
337
338def checkVersion(version) {
339 def parts = version.tokenize('.')
340 if (parts.size() != 3 || !(parts[0] ==~ /^\d+/)) {
341 error "Bad version ${version}"
342 }
343 return parts
344}
345
346/**
347 * Method for constructing SemVer2 compatible version from tag in Git repository:
348 * - if current commit matches the last tag, last tag will be returned as version
349 * - if no tag found assuming no release was done, version will be 0.0.1 with pre release metadata
350 * - if tag found - patch part of version will be incremented and pre-release metadata will be added
351 *
352 *
353 * @param repoDir String which contains path to directory with git repository
354 * @param allowNonSemVer2 Bool whether to allow working with tags which aren't compatible
355 * with Sem Ver 2 (not in form X.Y.Z). if set to true tag will be
356* converted to Sem Ver 2 version e.g tag 1.1.1.1rc1 -> version 1.1.1-1rc1
357 * @return version String
358 */
359def getVersion(repoDir, allowNonSemVer2 = false) {
360 def common = new com.mirantis.mk.Common()
361 dir(repoDir){
362 def cmd = common.shCmdStatus('git describe --tags --first-parent --abbrev=0')
363 def tag_data = [:]
364 def last_tag = cmd['stdout'].trim()
365 def commits_since_tag
366 if (cmd['status'] != 0){
367 if (cmd['stderr'].contains('fatal: No names found, cannot describe anything')){
368 common.warningMsg('No parent tag found, using initial version 0.0.0')
369 tag_data['version'] = '0.0.0'
370 commits_since_tag = sh(script: 'git rev-list --count HEAD', returnStdout: true).trim()
371 } else {
372 error("Something went wrong, cannot find git information ${cmd['stderr']}")
373 }
374 } else {
375 tag_data['version'] = last_tag
376 commits_since_tag = sh(script: "git rev-list --count ${last_tag}..HEAD", returnStdout: true).trim()
377 }
378 try {
379 checkVersion(tag_data['version'])
380 } catch (Exception e) {
381 if (allowNonSemVer2){
382 common.errorMsg(
383 """Git tag isn't compatible with SemVer2, but allowNonSemVer2 is set.
384 Trying to convert git tag to Sem Ver 2 compatible version
385 ${e.message}""")
386 tag_data = prepareTag(tag_data['version'])
387 } else {
388 error("Git tag isn't compatible with SemVer2\n${e.message}")
389 }
390 }
391 // If current commit is exact match to the first parent tag than return it
392 def pre_release_meta = []
393 if (tag_data.get('extra')){
394 pre_release_meta.add(tag_data['extra'])
395 }
396 if (common.shCmdStatus('git describe --tags --first-parent --exact-match')['status'] == 0){
397 if (pre_release_meta){
398 return "${tag_data['version']}-${pre_release_meta[0]}"
399 } else {
400 return tag_data['version']
401 }
402 }
403 // If we away from last tag for some number of commits - add additional metadata and increment version
404 pre_release_meta.add(commits_since_tag)
405 def next_version = incrementVersion(tag_data['version'])
406 def commit_sha = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
407 return "${next_version}-${pre_release_meta.join('.')}-${commit_sha}"
408 }
409}
Mykyta Karpinf5b6c162019-08-08 14:28:15 +0300410
411
412/**
413 * Method for uploading a change request
414 *
415 * @param repo String which contains path to directory with git repository
416 * @param credentialsId Credentials id to use for accessing target repositories
417 * @param commit Id of commit which should be uploaded
418 * @param branch Name of the branch for uploading
419 * @param topic Topic of the change
420 *
421 */
422def pushForReview(repo, credentialsId, commit, branch, topic='', remote='origin') {
423 def common = new com.mirantis.mk.Common()
424 def ssh = new com.mirantis.mk.Ssh()
425 common.infoMsg("Uploading commit ${commit} to ${branch} for review...")
426
427 def pushArg = "${commit}:refs/for/${branch}"
428 def process = [:]
429 if (topic){
430 pushArg += '%topic=' + topic
431 }
432 dir(repo){
433 ssh.prepareSshAgentKey(credentialsId)
434 ssh.runSshAgentCommand("git push ${remote} ${pushArg}")
435 }
436}
437
438/**
439 * Generates a commit message with predefined or auto generate change id. If change
440 * id isn't provided, changeIdSeed and current sha of git head will be used in
441 * generation of commit change id.
442 *
443 * @param repo String which contains path to directory with git repository
444 * @param message Commit message main part
445 * @param changeId User defined change-id usually sha1 hash
446 * @param changeIdSeed Custom part of change id which can be added during change id generation
447 *
448 *
449 * @return commitMessage Multiline String with generated commit message
450 */
451def genCommitMessage(repo, message, changeId = '', changeIdSeed = ''){
452 def git = new com.mirantis.mk.Git()
453 def common = new com.mirantis.mk.Common()
454 def commitMessage
455 def id = changeId
456 def seed = changeIdSeed
457 if (!id) {
458 if (!seed){
459 seed = common.generateRandomHashString(32)
460 }
461 def head_sha
462 dir(repo){
463 head_sha = git.getGitCommit()
464 }
465 id = 'I' + sh(script: 'echo -n ' + seed + head_sha + ' | sha1sum | awk \'{print $1}\'', returnStdout: true)
466 }
467 commitMessage =
468 """${message}
469
470 |Change-Id: ${id}
471 """.stripMargin()
472
473 return commitMessage
474}