diff --git a/README.md b/README.md index b7e7a553..099af6fe 100644 --- a/README.md +++ b/README.md @@ -495,6 +495,8 @@ gitWithCreds 'https://your.repo' // Implicitly passed credentials ### Changes to remote repository * `git.push('master')` - pushes origin +* `git.pushAndPullOnFailure('refspec')` - pushes and pulls if push failed e.g. because local and remote have diverged, + then tries pushing again * `pushGitHubPagesBranch('folderToPush', 'commit Message')` - Commits and pushes a folder to the `gh-pages` branch of the current repo. Can be used to conveniently deliver websites. See https://pages.github.com. Note: * Uses the name and email of the last committer as author and committer. diff --git a/src/com/cloudogu/ces/cesbuildlib/Git.groovy b/src/com/cloudogu/ces/cesbuildlib/Git.groovy index ea474ef0..3fd6d389 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Git.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Git.groovy @@ -291,21 +291,54 @@ class Git implements Serializable { * * @param refSpec branch or tag name */ - void push(String refSpec) { - executeGitWithCredentials "push origin ${refSpec}" + void push(String refSpec = '') { + refSpec = addOriginWhenMissing(refSpec) + executeGitWithCredentials "push ${refSpec}" } /** * Pulls to local from remote repo. * * @param refSpec branch or tag name + * @param authorName + * @param authorEmail */ void pull(String refSpec = '', String authorName = commitAuthorName, String authorEmail = commitAuthorEmail) { + refSpec = addOriginWhenMissing(refSpec) withAuthorAndEmail(authorName, authorEmail) { executeGitWithCredentials "pull ${refSpec}" } } + /** + * Pushes local to remote repo. Additionally pulls, if push has failed. + * + * @param refSpec branch or tag name + * @param authorName + * @param authorEmail + */ + void pushAndPullOnFailure(String refSpec = '', String authorName = commitAuthorName, String authorEmail = commitAuthorEmail) { + refSpec = addOriginWhenMissing(refSpec) + executeGitWithCredentials("push ${refSpec}") { + script.echo "Got error, trying to pull first" + pull(refSpec, authorName, authorEmail) + } + } + + /** + * Method exists purely because of downward compatibility. Adding remote to pushes and pulls is preferred, + * but historically git push always added `origin` implicitly. + * + */ + private static String addOriginWhenMissing(String refSpec) { + // if refspec contains more than 1 argument e.g. `upstream master` + if(!refSpec || refSpec.trim().split(' ').length > 1 || refSpec.trim() == 'origin') { + return refSpec + } + + return 'origin ' + refSpec + } + /** * Commits and pushes a folder to the gh-pages branch of the current repo. * Can be used to conveniently deliver websites. See https://pages.github.com/ @@ -338,28 +371,30 @@ class Git implements Serializable { * This method executes the git command with a bash function as credential helper, * which return username and password from jenkins credentials. * - * If the script failes with exit code 128, this will retry the call up to the + * If the script failes with exit code > 0, this will retry the call up to the * configured max retries before failing. * * @param args git arguments + * @param closure closure to execute after first retry */ - protected void executeGitWithCredentials(String args) { + protected void executeGitWithCredentials(String args, Closure executeBeforeRetry = {}) { if (credentials) { script.withCredentials([script.usernamePassword(credentialsId: credentials, passwordVariable: 'GIT_AUTH_PSW', usernameVariable: 'GIT_AUTH_USR')]) { - def pushResultCode = 128 + def gitResultCode = 1 def retryCount = 0 - while (pushResultCode == 128 && retryCount < maxRetries) { + while (gitResultCode > 0 && retryCount < maxRetries) { if (retryCount > 0) { - script.echo "Got error code ${pushResultCode} - retrying in ${retryTimeout} ms ..." + script.echo "Got error code ${gitResultCode} - retrying in ${retryTimeout} ms ..." sleep(retryTimeout) + executeBeforeRetry.call() } ++retryCount - pushResultCode = script.sh returnStatus: true, script: "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" ${args}" - pushResultCode = pushResultCode as int + gitResultCode = script.sh returnStatus: true, script: "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" ${args}" + gitResultCode = gitResultCode as int } - if (pushResultCode != 0) { - script.error "Unable to execute git call. Retried ${retryCount} times. Last error code: ${pushResultCode}" + if (gitResultCode != 0) { + script.error "Unable to execute git call. Retried ${retryCount} times. Last error code: ${gitResultCode}" } } } else { diff --git a/test/com/cloudogu/ces/cesbuildlib/GitTest.groovy b/test/com/cloudogu/ces/cesbuildlib/GitTest.groovy index 6faf6385..29eee72b 100644 --- a/test/com/cloudogu/ces/cesbuildlib/GitTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/GitTest.groovy @@ -221,7 +221,7 @@ class GitTest { } @Test - void pull() { + void "pull with empty refspec"() { def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull' scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, 0) scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') @@ -240,7 +240,45 @@ class GitTest { } @Test - void 'pull with refspec'() { + void "pull origin"() { + def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull origin' + scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, 0) + scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') + git = new Git(scriptMock, 'creds') + + git.pull('origin') + + def actualWithEnv = scriptMock.actualWithEnvAsMap() + assert actualWithEnv['GIT_AUTHOR_NAME'] == 'User Name' + assert actualWithEnv['GIT_COMMITTER_NAME'] == 'User Name' + assert actualWithEnv['GIT_AUTHOR_EMAIL'] == 'user.name@doma.in' + assert actualWithEnv['GIT_COMMITTER_EMAIL'] == 'user.name@doma.in' + + assert scriptMock.actualShMapArgs.size() == 3 + assert scriptMock.actualShMapArgs.get(2).trim() == expectedGitCommandWithCredentials + } + + @Test + void 'pull master'() { + def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull origin master' + scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, 0) + scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') + git = new Git(scriptMock, 'creds') + + git.pull 'master' + + def actualWithEnv = scriptMock.actualWithEnvAsMap() + assert actualWithEnv['GIT_AUTHOR_NAME'] == 'User Name' + assert actualWithEnv['GIT_COMMITTER_NAME'] == 'User Name' + assert actualWithEnv['GIT_AUTHOR_EMAIL'] == 'user.name@doma.in' + assert actualWithEnv['GIT_COMMITTER_EMAIL'] == 'user.name@doma.in' + + assert scriptMock.actualShMapArgs.size() == 3 + assert scriptMock.actualShMapArgs.get(2).trim() == expectedGitCommandWithCredentials + } + + @Test + void 'pull origin master'() { def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull origin master' scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, 0) scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') @@ -258,6 +296,25 @@ class GitTest { assert scriptMock.actualShMapArgs.get(2).trim() == expectedGitCommandWithCredentials } + @Test + void 'pull upstream master'() { + def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull upstream master' + scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, 0) + scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') + git = new Git(scriptMock, 'creds') + + git.pull 'upstream master' + + def actualWithEnv = scriptMock.actualWithEnvAsMap() + assert actualWithEnv['GIT_AUTHOR_NAME'] == 'User Name' + assert actualWithEnv['GIT_COMMITTER_NAME'] == 'User Name' + assert actualWithEnv['GIT_AUTHOR_EMAIL'] == 'user.name@doma.in' + assert actualWithEnv['GIT_COMMITTER_EMAIL'] == 'user.name@doma.in' + + assert scriptMock.actualShMapArgs.size() == 3 + assert scriptMock.actualShMapArgs.get(2).trim() == expectedGitCommandWithCredentials + } + @Test void checkout() { git.checkout("master") @@ -310,6 +367,38 @@ class GitTest { git.push('master') } + @Test + void "push with empty refspec"() { + git.push() + + assert scriptMock.actualShStringArgs.size() == 1 + assert scriptMock.actualShStringArgs.get(0).trim() == 'git push' + } + + @Test + void "push origin"() { + git.push('origin') + + assert scriptMock.actualShStringArgs.size() == 1 + assert scriptMock.actualShStringArgs.get(0) == 'git push origin' + } + + @Test + void "push origin master"() { + git.push('origin master') + + assert scriptMock.actualShStringArgs.size() == 1 + assert scriptMock.actualShStringArgs.get(0) == 'git push origin master' + } + + @Test + void "push upstream master"() { + git.push('upstream master') + + assert scriptMock.actualShStringArgs.size() == 1 + assert scriptMock.actualShStringArgs.get(0) == 'git push upstream master' + } + @Test void pushNonHttps() { scriptMock.expectedShRetValueForScript.put('git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push origin master', 0) @@ -333,6 +422,98 @@ class GitTest { assert scriptMock.actualShMapArgs.get(2) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push origin master' } + @Test + void "pushAndPullOnFailure with empty refspec"() { + def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push' + scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, [1, 0]) + scriptMock.expectedShRetValueForScript.put('git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull', 0) + scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') + git = new Git(scriptMock, 'creds') + + git.retryTimeout = 1 + git.pushAndPullOnFailure() + + def actualWithEnv = scriptMock.actualWithEnvAsMap() + assert actualWithEnv['GIT_AUTHOR_NAME'] == 'User Name' + assert actualWithEnv['GIT_COMMITTER_NAME'] == 'User Name' + assert actualWithEnv['GIT_AUTHOR_EMAIL'] == 'user.name@doma.in' + assert actualWithEnv['GIT_COMMITTER_EMAIL'] == 'user.name@doma.in' + + assert scriptMock.actualShMapArgs.size() == 5 + assert scriptMock.actualShMapArgs.get(2).trim() == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push' + assert scriptMock.actualShMapArgs.get(3).trim() == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull' + assert scriptMock.actualShMapArgs.get(4).trim() == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push' + } + + @Test + void "pushAndPullOnFailure master"() { + def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push origin master' + scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, [1, 0]) + scriptMock.expectedShRetValueForScript.put('git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull origin master', 0) + scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') + git = new Git(scriptMock, 'creds') + + git.retryTimeout = 1 + git.pushAndPullOnFailure('master') + + def actualWithEnv = scriptMock.actualWithEnvAsMap() + assert actualWithEnv['GIT_AUTHOR_NAME'] == 'User Name' + assert actualWithEnv['GIT_COMMITTER_NAME'] == 'User Name' + assert actualWithEnv['GIT_AUTHOR_EMAIL'] == 'user.name@doma.in' + assert actualWithEnv['GIT_COMMITTER_EMAIL'] == 'user.name@doma.in' + + assert scriptMock.actualShMapArgs.size() == 5 + assert scriptMock.actualShMapArgs.get(2) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push origin master' + assert scriptMock.actualShMapArgs.get(3) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull origin master' + assert scriptMock.actualShMapArgs.get(4) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push origin master' + } + + @Test + void "pushAndPullOnFailure origin master"() { + def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push origin master' + scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, [1, 0]) + scriptMock.expectedShRetValueForScript.put('git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull origin master', 0) + scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') + git = new Git(scriptMock, 'creds') + + git.retryTimeout = 1 + git.pushAndPullOnFailure('origin master') + + def actualWithEnv = scriptMock.actualWithEnvAsMap() + assert actualWithEnv['GIT_AUTHOR_NAME'] == 'User Name' + assert actualWithEnv['GIT_COMMITTER_NAME'] == 'User Name' + assert actualWithEnv['GIT_AUTHOR_EMAIL'] == 'user.name@doma.in' + assert actualWithEnv['GIT_COMMITTER_EMAIL'] == 'user.name@doma.in' + + assert scriptMock.actualShMapArgs.size() == 5 + assert scriptMock.actualShMapArgs.get(2) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push origin master' + assert scriptMock.actualShMapArgs.get(3) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull origin master' + assert scriptMock.actualShMapArgs.get(4) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push origin master' + } + + @Test + void "pushAndPullOnFailure upstream master"() { + def expectedGitCommandWithCredentials = 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push upstream master' + scriptMock.expectedShRetValueForScript.put(expectedGitCommandWithCredentials, [1, 0]) + scriptMock.expectedShRetValueForScript.put('git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull upstream master', 0) + scriptMock.expectedShRetValueForScript.put('git --no-pager show -s --format=\'%an <%ae>\' HEAD', 'User Name ') + git = new Git(scriptMock, 'creds') + + git.retryTimeout = 1 + git.pushAndPullOnFailure('upstream master') + + def actualWithEnv = scriptMock.actualWithEnvAsMap() + assert actualWithEnv['GIT_AUTHOR_NAME'] == 'User Name' + assert actualWithEnv['GIT_COMMITTER_NAME'] == 'User Name' + assert actualWithEnv['GIT_AUTHOR_EMAIL'] == 'user.name@doma.in' + assert actualWithEnv['GIT_COMMITTER_EMAIL'] == 'user.name@doma.in' + + assert scriptMock.actualShMapArgs.size() == 5 + assert scriptMock.actualShMapArgs.get(2) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push upstream master' + assert scriptMock.actualShMapArgs.get(3) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" pull upstream master' + assert scriptMock.actualShMapArgs.get(4) == 'git -c credential.helper="!f() { echo username=\'$GIT_AUTH_USR\'; echo password=\'$GIT_AUTH_PSW\'; }; f" push upstream master' + } + @Test void pushNoCredentials() { git.push('master')