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')