blob: 5743a61e832a575982bd071a5e635e992223642d [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)
Sergey Kolekonovba203982016-12-21 18:32:17 +040019 */
Jakub Josef61f29e62017-03-08 16:42:06 +010020def checkoutGitRepository(path, url, branch, credentialsId = null, poll = true, timeout = 10, depth = 0){
Sergey Kolekonovba203982016-12-21 18:32:17 +040021 dir(path) {
Jakub Josef6fa8cb12017-03-06 18:20:08 +010022 checkout(
23 changelog:true,
24 poll: poll,
25 scm: [
26 $class: 'GitSCM',
27 branches: [[name: "*/${branch}"]],
28 doGenerateSubmoduleConfigurations: false,
29 extensions: [
Jakub Josef61f29e62017-03-08 16:42:06 +010030 [$class: 'CheckoutOption', timeout: timeout],
Jakub Josef1589f9a2017-03-08 17:41:21 +010031 [$class: 'CloneOption', depth: depth, noTags: false, reference: '', shallow: depth > 0, timeout: timeout]],
Jakub Josef6fa8cb12017-03-06 18:20:08 +010032 submoduleCfg: [],
33 userRemoteConfigs: [[url: url, credentialsId: credentialsId]]]
34 )
Sergey Kolekonovba203982016-12-21 18:32:17 +040035 sh(returnStdout: true, script: 'git rev-parse HEAD').trim()
36 }
37}
38
39/**
40 * Parse HEAD of current directory and return commit hash
41 */
42def getGitCommit() {
43 git_commit = sh (
44 script: 'git rev-parse HEAD',
45 returnStdout: true
46 ).trim()
47 return git_commit
48}
49
50/**
Ales Komarekfb7cbcb2017-02-24 14:02:03 +010051 * Change actual working branch of repo
52 *
53 * @param path Path to the git repository
54 * @param branch Branch desired to switch to
55 */
56def changeGitBranch(path, branch) {
57 dir(path) {
58 git_cmd = sh (
Leontii Istominb4f4ae12018-02-27 20:25:43 +010059 script: "git checkout ${branch}",
Ales Komarekfb7cbcb2017-02-24 14:02:03 +010060 returnStdout: true
61 ).trim()
62 }
63 return git_cmd
64}
65
66/**
Ales Komarekc3a8b972017-03-24 13:57:25 +010067 * Get remote URL
68 *
69 * @param name Name of remote (default any)
70 * @param type Type (fetch or push, default fetch)
71 */
72def getGitRemote(name = '', type = 'fetch') {
73 gitRemote = sh (
74 script: "git remote -v | grep '${name}' | grep ${type} | awk '{print \$2}' | head -1",
75 returnStdout: true
76 ).trim()
77 return gitRemote
78}
79
80/**
81 * Create new working branch for repo
82 *
83 * @param path Path to the git repository
84 * @param branch Branch desired to switch to
85 */
86def createGitBranch(path, branch) {
87 def git_cmd
88 dir(path) {
89 git_cmd = sh (
90 script: "git checkout -b ${branch}",
91 returnStdout: true
92 ).trim()
93 }
94 return git_cmd
95}
96
97/**
Ales Komarekfb7cbcb2017-02-24 14:02:03 +010098 * Commit changes to the git repo
99 *
100 * @param path Path to the git repository
101 * @param message A commit message
Denis Egorenkof4c45512019-03-04 15:53:36 +0400102 * @param global Use global config
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300103 * @param amend Whether to use "--amend" in commit command
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100104 */
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300105def commitGitChanges(path, message, gitEmail='jenkins@localhost', gitName='jenkins-slave', global=false, amend=false) {
Ales Komarekc3a8b972017-03-24 13:57:25 +0100106 def git_cmd
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300107 def gitOpts
Denis Egorenkof4c45512019-03-04 15:53:36 +0400108 def global_arg = ''
109 if (global) {
110 global_arg = '--global'
111 }
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300112 if (amend) {
113 gitOpts = '--amend'
114 } else {
115 gitOpts = ''
116 }
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100117 dir(path) {
Denis Egorenkof4c45512019-03-04 15:53:36 +0400118 sh "git config ${global_arg} user.email '${gitEmail}'"
119 sh "git config ${global_arg} user.name '${gitName}'"
Tomáš Kukráldf7bebc2017-03-27 15:12:43 +0200120
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100121 sh(
122 script: 'git add -A',
123 returnStdout: true
124 ).trim()
125 git_cmd = sh(
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300126 script: "git commit ${gitOpts} -m '${message}'",
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100127 returnStdout: true
128 ).trim()
129 }
130 return git_cmd
131}
132
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100133/**
134 * Push git changes to remote repo
135 *
Ales Komarekc3a8b972017-03-24 13:57:25 +0100136 * @param path Path to the local git repository
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100137 * @param branch Branch on the remote git repository
138 * @param remote Name of the remote repository
Ales Komarekc3a8b972017-03-24 13:57:25 +0100139 * @param credentialsId Credentials with write permissions
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100140 */
Ales Komarekc3a8b972017-03-24 13:57:25 +0100141def pushGitChanges(path, branch = 'master', remote = 'origin', credentialsId = null) {
142 def ssh = new com.mirantis.mk.Ssh()
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100143 dir(path) {
Ales Komarekc3a8b972017-03-24 13:57:25 +0100144 if (credentialsId == null) {
145 sh script: "git push ${remote} ${branch}"
146 }
147 else {
148 ssh.prepareSshAgentKey(credentialsId)
149 ssh.runSshAgentCommand("git push ${remote} ${branch}")
150 }
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100151 }
Ales Komarekfb7cbcb2017-02-24 14:02:03 +0100152}
153
Ales Komarekc3a8b972017-03-24 13:57:25 +0100154
Sergey Kolekonovba203982016-12-21 18:32:17 +0400155/**
Filip Pytloun49d66302017-03-06 10:26:22 +0100156 * Mirror git repository, merge target changes (downstream) on top of source
157 * (upstream) and push target or both if pushSource is true
158 *
159 * @param sourceUrl Source git repository
160 * @param targetUrl Target git repository
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400161 * @param credentialsId Credentials id to use for accessing target repositories
Filip Pytloun49d66302017-03-06 10:26:22 +0100162 * @param branches List or comma-separated string of branches to sync
163 * @param followTags Mirror tags
164 * @param pushSource Push back into source branch, resulting in 2-way sync
165 * @param pushSourceTags Push target tags into source or skip pushing tags
166 * @param gitEmail Email for creation of merge commits
167 * @param gitName Name for creation of merge commits
Sergey Kolekonovba203982016-12-21 18:32:17 +0400168 */
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400169def 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 +0200170 def common = new com.mirantis.mk.Common()
171 def ssh = new com.mirantis.mk.Ssh()
Sergey Kolekonovba203982016-12-21 18:32:17 +0400172 if (branches instanceof String) {
173 branches = branches.tokenize(',')
174 }
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400175 // If both source and target repos are secured and accessible via http/https,
176 // we need to switch GIT_ASKPASS value when running git commands
177 def sourceAskPass
178 def targetAskPass
Sergey Kolekonovba203982016-12-21 18:32:17 +0400179
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400180 def sshCreds = common.getCredentialsById(credentialsId, 'sshKey') // True if found
181 if (sshCreds) {
182 ssh.prepareSshAgentKey(credentialsId)
183 ssh.ensureKnownHosts(targetUrl)
184 sh "git config user.name '${gitName}'"
185 } else {
186 withCredentials([[$class : 'UsernamePasswordMultiBinding',
187 credentialsId : credentialsId,
188 passwordVariable: 'GIT_PASSWORD',
189 usernameVariable: 'GIT_USERNAME']]) {
190 sh """
191 set +x
192 git config --global credential.${targetUrl}.username \${GIT_USERNAME}
193 echo "echo \${GIT_PASSWORD}" > ${WORKSPACE}/${credentialsId}_askpass.sh
194 chmod +x ${WORKSPACE}/${credentialsId}_askpass.sh
195 git config user.name \${GIT_USERNAME}
196 """
197 sourceAskPass = env.GIT_ASKPASS ?: ''
198 targetAskPass = "${WORKSPACE}/${credentialsId}_askpass.sh"
199 }
200 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100201 sh "git config user.email '${gitEmail}'"
Filip Pytloun49d66302017-03-06 10:26:22 +0100202
Jakub Josef1caa7ae2017-08-21 16:39:00 +0200203 def remoteExistence = sh(script: "git remote -v | grep ${TARGET_URL} | grep target", returnStatus: true)
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400204 if(remoteExistence == 0) {
205 // silently try to remove target
206 sh(script: "git remote remove target", returnStatus: true)
Jakub Josef1caa7ae2017-08-21 16:39:00 +0200207 }
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400208 sh("git remote add target ${TARGET_URL}")
209 if (sshCreds) {
210 ssh.agentSh "git remote update --prune"
211 } else {
212 env.GIT_ASKPASS = sourceAskPass
213 sh "git remote update ${sourceRemote} --prune"
214 env.GIT_ASKPASS = targetAskPass
215 sh "git remote update target --prune"
216 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100217
Sergey Kolekonovba203982016-12-21 18:32:17 +0400218 for (i=0; i < branches.size; i++) {
219 branch = branches[i]
Jakub Josef668dc2b2017-06-19 16:55:26 +0200220 sh "git branch | grep ${branch} || git checkout -b ${branch}"
221 def resetResult = sh(script: "git checkout ${branch} && git reset --hard origin/${branch}", returnStatus: true)
222 if(resetResult != 0){
223 common.warningMsg("Cannot reset to origin/${branch} for perform git mirror, trying to reset from target/${branch}")
224 resetResult = sh(script: "git checkout ${branch} && git reset --hard target/${branch}", returnStatus: true)
225 if(resetResult != 0){
226 throw new Exception("Cannot reset even to target/${branch}, git mirroring failed!")
227 }
228 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400229
Sergey Kolekonovba203982016-12-21 18:32:17 +0400230 sh "git ls-tree target/${branch} && git merge --no-edit --ff target/${branch} || echo 'Target repository is empty, skipping merge'"
231 followTagsArg = followTags ? "--follow-tags" : ""
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400232 if (sshCreds) {
233 ssh.agentSh "git push ${followTagsArg} target HEAD:${branch}"
234 } else {
235 sh "git push ${followTagsArg} target HEAD:${branch}"
236 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100237
238 if (pushSource == true) {
239 followTagsArg = followTags && pushSourceTags ? "--follow-tags" : ""
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400240 if (sshCreds) {
241 ssh.agentSh "git push ${followTagsArg} origin HEAD:${branch}"
242 } else {
243 sh "git push ${followTagsArg} origin HEAD:${branch}"
244 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100245 }
246 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100247 if (followTags == true) {
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400248 if (sshCreds) {
249 ssh.agentSh "git push -f target --tags"
250 } else {
251 sh "git push -f target --tags"
252 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100253
254 if (pushSourceTags == true) {
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400255 if (sshCreds) {
256 ssh.agentSh "git push -f origin --tags"
257 } else {
258 sh "git push -f origin --tags"
259 }
Filip Pytloun49d66302017-03-06 10:26:22 +0100260 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400261 }
Jakub Josefecf8b452017-04-20 13:34:29 +0200262 sh "git remote rm target"
Ivan Berezovskiycf269442019-07-18 16:15:26 +0400263 if (!sshCreds) {
264 sh "set +x; rm -f ${targetAskPass}"
265 sh "git config --global --unset credential.${targetUrl}.username"
266 }
Sergey Kolekonovba203982016-12-21 18:32:17 +0400267}
Martin Polreich765f7ba2019-03-12 16:39:25 +0100268
269
270/**
271 * Return all branches for the defined git repository that match the matcher.
272 *
273 * @param repoUrl URL of git repository
274 * @param branchMatcher matcher to filter out the branches (If '' or '*', returns all branches without filtering)
275 * @return branchesList list of branches
276 */
277
278def getBranchesForGitRepo(repoUrl, branchMatcher = ''){
279
280 if (branchMatcher.equals("*")) {
281 branchMatcher = ''
282 }
283 branchesList = sh (
284 script: "git ls-remote --heads ${repoUrl} | cut -f2 | grep -e '${branchMatcher}' | sed 's/refs\\/heads\\///g'",
285 returnStdout: true
286 ).trim()
287 return branchesList.tokenize('\n')
Oleksii Grudeva64e5b22019-06-11 11:21:02 +0300288}
289
Mykyta Karpin82437932019-06-06 14:08:18 +0300290/**
291 * Method for preparing a tag to be SemVer 2 compatible, and can handle next cases:
292 * - length of tag splitted by dots is more than 3
293 * - first part of splitted tag starts not from digit
294 * - length of tag is lower than 3
295 *
296 * @param tag String which contains a git tag from repository
297 * @return HashMap HashMap in the form: ['version': 'x.x.x', 'extra': 'x.x.x'], extra
298 * is added only if size of original tag splitted by dots is more than 3
299 */
300
301def prepareTag(tag){
302 def parts = tag.tokenize('.')
303 def res = [:]
304 // Handle case with tags like v1.1.1
305 parts[0] = parts[0].replaceFirst("[^\\d.]", '')
306 // handle different sizes of tags - 1.1.1.1 or 1.1.1.1rc1
307 if (parts.size() > 3){
308 res['extra'] = parts[3..-1].join('.')
309 } else if (parts.size() < 3){
310 (parts.size()..2).each {
311 parts[it] = '0'
312 }
313 }
314 res['version'] = "${parts[0]}.${parts[1]}.${parts[2]}"
315 return res
316}
317
318/**
319 * Method for incrementing SemVer 2 compatible version
320 *
321 * @param version String which contains main part of SemVer2 version - '2.1.0'
322 * @return string String conaining version with Patch part of version incremented by 1
323 */
324
325def incrementVersion(version){
326 def parts = checkVersion(version)
327 return "${parts[0]}.${parts[1]}.${parts[2].toInteger() + 1}"
328}
329
330/**
331 * Method for checking whether version is compatible with Sem Ver 2
332 *
333 * @param version String which contains main part of SemVer2 version - '2.1.0'
334 * @return list With 3 strings as result of splitting version by dots
335 */
336
337def checkVersion(version) {
338 def parts = version.tokenize('.')
339 if (parts.size() != 3 || !(parts[0] ==~ /^\d+/)) {
340 error "Bad version ${version}"
341 }
342 return parts
343}
344
345/**
346 * Method for constructing SemVer2 compatible version from tag in Git repository:
347 * - if current commit matches the last tag, last tag will be returned as version
348 * - if no tag found assuming no release was done, version will be 0.0.1 with pre release metadata
349 * - if tag found - patch part of version will be incremented and pre-release metadata will be added
350 *
351 *
352 * @param repoDir String which contains path to directory with git repository
353 * @param allowNonSemVer2 Bool whether to allow working with tags which aren't compatible
354 * with Sem Ver 2 (not in form X.Y.Z). if set to true tag will be
355* converted to Sem Ver 2 version e.g tag 1.1.1.1rc1 -> version 1.1.1-1rc1
356 * @return version String
357 */
358def getVersion(repoDir, allowNonSemVer2 = false) {
359 def common = new com.mirantis.mk.Common()
360 dir(repoDir){
361 def cmd = common.shCmdStatus('git describe --tags --first-parent --abbrev=0')
362 def tag_data = [:]
363 def last_tag = cmd['stdout'].trim()
364 def commits_since_tag
365 if (cmd['status'] != 0){
366 if (cmd['stderr'].contains('fatal: No names found, cannot describe anything')){
367 common.warningMsg('No parent tag found, using initial version 0.0.0')
368 tag_data['version'] = '0.0.0'
369 commits_since_tag = sh(script: 'git rev-list --count HEAD', returnStdout: true).trim()
370 } else {
371 error("Something went wrong, cannot find git information ${cmd['stderr']}")
372 }
373 } else {
374 tag_data['version'] = last_tag
375 commits_since_tag = sh(script: "git rev-list --count ${last_tag}..HEAD", returnStdout: true).trim()
376 }
377 try {
378 checkVersion(tag_data['version'])
379 } catch (Exception e) {
380 if (allowNonSemVer2){
381 common.errorMsg(
382 """Git tag isn't compatible with SemVer2, but allowNonSemVer2 is set.
383 Trying to convert git tag to Sem Ver 2 compatible version
384 ${e.message}""")
385 tag_data = prepareTag(tag_data['version'])
386 } else {
387 error("Git tag isn't compatible with SemVer2\n${e.message}")
388 }
389 }
390 // If current commit is exact match to the first parent tag than return it
391 def pre_release_meta = []
392 if (tag_data.get('extra')){
393 pre_release_meta.add(tag_data['extra'])
394 }
395 if (common.shCmdStatus('git describe --tags --first-parent --exact-match')['status'] == 0){
396 if (pre_release_meta){
397 return "${tag_data['version']}-${pre_release_meta[0]}"
398 } else {
399 return tag_data['version']
400 }
401 }
402 // If we away from last tag for some number of commits - add additional metadata and increment version
403 pre_release_meta.add(commits_since_tag)
404 def next_version = incrementVersion(tag_data['version'])
405 def commit_sha = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
406 return "${next_version}-${pre_release_meta.join('.')}-${commit_sha}"
407 }
408}