Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Rebase against github using ScriptRunner

Geobert Quach March 18, 2021

Hi,

I have forked a repository from github and imported it into our Bitbucket instance.

I edited one file and pushed to our Bitbucket repository.

I'm now trying to automate a rebase against the GitHub repository but have no knowledge at all on Groovy nor Bitbucket API. After hours of research, I have made this Custom Schedule Job inline script which does not work of course:

import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.repository.Repository
import com.atlassian.bitbucket.repository.RepositoryService
import com.atlassian.bitbucket.repository.RefService
import com.atlassian.bitbucket.repository.Branch
import com.atlassian.bitbucket.user.ApplicationUser
import com.atlassian.bitbucket.user.UserService
import com.atlassian.bitbucket.scm.git.command.GitRebaseCommandParameters
import com.atlassian.bitbucket.scm.git.command.GitExtendedCommandFactory

def repositoryService = ComponentLocator.getComponent(RepositoryService.class)
def refService = ComponentLocator.getComponent(RefService.class)
def cmdFactory = ComponentLocator.getComponent(GitExtendedCommandFactory.class)
def userService = ComponentLocator.getComponent(UserService.class)

Repository rep = repositoryService.getBySlug("RUST", "crates.io-mirror")
Branch master = refService.getDefaultBranch(rep)
ApplicationUser user = userService.findUserByEmail("the_committer@my_company.com")

def paramsBuilder = new GitRebaseCommandParameters.Builder(master, "https://github.com/rust-lang/crates.io-index.git")
paramsBuilder.dryRun(true).committer(user)

def rebase = cmdFactory.rebase(rep, paramsBuilder.build())
rebase.call()

This gives me the following error:

2021-03-18 15:27:06,990 ERROR [c.o.s.j.AbstractCustomScheduledJob]: *************************************************************************************
2021-03-18 15:27:06,995 ERROR [c.o.s.j.AbstractCustomScheduledJob]: Script job: 'Update crates.io mirror' failed
com.atlassian.bitbucket.validation.ArgumentValidationException: The specified upstream, 'https://github.com/rust-lang/crates.io-index.git', is not valid. The upstream must be specified as a full 40-character SHA-1.
at com.atlassian.stash.internal.scm.git.DefaultGitExtendedCommandFactory.rebase(DefaultGitExtendedCommandFactory.java:236)
at com.atlassian.plugin.util.ContextClassLoaderSettingInvocationHandler.invoke(ContextClassLoaderSettingInvocationHandler.java:26)
at com.sun.proxy.$Proxy438.rebase(Unknown Source)
at com.atlassian.bitbucket.scm.git.command.GitExtendedCommandFactory$rebase.call(Unknown Source)
at Script16.run(Script16.groovy:23)
at com.atlassian.stash.internal.scm.git.DefaultGitExtendedCommandFactory.rebase(DefaultGitExtendedCommandFactory.java:236)
at com.atlassian.plugin.util.ContextClassLoaderSettingInvocationHandler.invoke(ContextClassLoaderSettingInvocationHandler.java:26)
at com.sun.proxy.$Proxy438.rebase(Unknown Source)
at com.atlassian.bitbucket.scm.git.command.GitExtendedCommandFactory$rebase.call(Unknown Source)
at Script16.run(Script16.groovy:23)

So the upstream parameters is a SHA-1 string, so I guess I can't refer to a repository outside Bitbucket this way.

How can I do this rebase against a public GitHub repository?

1 answer

Hi @Geobert Quach 

I am afraid it is not possible to rebase a branch on a different repository.

My suggestion is, instead of importing a fork and commiting new work on it, to have separate branches to use for mirroring and development.

I have managed to achieve what you want to do using Scriptrunner's 'Mirror GitHub repositories', then fetching any changes into a buffer/proxy branch where I could rebase on.

In total, my bitbucket server had 2 repositories:

  • A repo that is created from the mirroring. You should not add more branches here
  • The repo that contains your development work. It contains 2 branches:
    • The branch were you will be commiting your changes. The script will be rebasing this branch on the public GitHub repository.
    • a proxy branch that will be used as a workaround, in order to run the rebase command. It will be copying over from the mirror. You should not push any changes here, but only have the script using it to fetch and rebase.

This is the script that worked for me:

import com.atlassian.bitbucket.i18n.I18nService
import com.atlassian.bitbucket.repository.RepositoryBranchesRequest
import com.atlassian.bitbucket.scm.CommandOutputHandler
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory
import com.atlassian.bitbucket.scm.git.command.fetch.GitFetchTagMode
import com.atlassian.bitbucket.util.Page
import com.atlassian.bitbucket.util.PageProvider
import com.atlassian.bitbucket.util.PageRequest
import com.atlassian.bitbucket.util.PagedIterable
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.repository.Repository
import com.atlassian.bitbucket.repository.RepositoryService
import com.atlassian.bitbucket.repository.RefService
import com.atlassian.bitbucket.repository.Branch
import com.atlassian.bitbucket.user.ApplicationUser
import com.atlassian.bitbucket.user.UserService
import com.atlassian.bitbucket.scm.git.command.GitRebaseCommandParameters
import com.atlassian.bitbucket.scm.git.command.GitExtendedCommandFactory
import com.atlassian.stash.internal.scm.git.command.updateref.UpdateRefCommandExitHandler
import com.atlassian.utils.process.ProcessException
import com.atlassian.utils.process.Watchdog
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl

def repositoryService = ComponentLocator.getComponent(RepositoryService)
def refService = ComponentLocator.getComponent(RefService)
def userService = ComponentLocator.getComponent(UserService)
def extendedCommandFactory = ComponentLocator.getComponent(GitExtendedCommandFactory)
def i18nService = ComponentLocator.getComponent(I18nService)

// A bitbucket project that contains both repositories (mirror and dev).
String PROJECT_KEY = 'MIR'

// the repo that contains the branch to be rebased.
String TARGET_REPO_NAME = 'develop'

// The branch that you want to use for development and rebase occasionally.
String REBASED_BRANCH_NAME = 'main'

// We cannot rebase from a different repository. Will use a buffer or proxy branch, in the same repo. We will fetch any new commits from the mirror here, and rebase on this one
// This branch should be read only for other users, as we should not push other commits here
String BUFFER_BRANCH_NAME = 'mirror'

// Another repo, set to mirror a github repo
String MIRROR_REPO_NAME = 'test'

// The name of the branch in the mirror repo that we originally want to rebase on
String MIRROR_BRANCH_NAME = 'main'

/**

Overview of branches and actions

SERVER | REPO | BRANCH
-------------------------------
GitHub | MIRROR_REPO_NAME | MIRROR_BRANCH_NAME
↓ Scriptrunner will be mirroring this into bitbucket (Use 'Mirror GitHub Repositories' to set it up)

BitBucket | MIRROR_REPO_NAME | MIRROR_BRANCH_NAME
↓ This script will fetch any new changes from the mirror, into the repo that we want to rebase

BitBucket | TARGET_REPO_NAME | BUFFER_BRANCH_NAME
↓ This script will rebase REBASED_BRANCH_NAME on BUFFER_BRANCH_NAME

BitBucket | TARGET_REPO_NAME | REBASED_BRANCH_NAME

*/
class FetchOutputHandler implements CommandOutputHandler<Void> {

@Override
Void getOutput() { return null }

@Override
void process(InputStream output) throws ProcessException {}

@Override
void complete() throws ProcessException {}

@Override
void setWatchdog(Watchdog watchdog) {}
}

Branch getBranch(RefService refService, Repository repository, String branchName) {
def repositoryBranchesRequest = new RepositoryBranchesRequest.Builder(repository).build()
new PagedIterable<Branch>(new PageProvider<Branch>() {
@Override
Page<Branch> get(PageRequest pageRequest) {
refService.getBranches(repositoryBranchesRequest, pageRequest) as Page<Branch>
}
}, 1000).iterator().find { Branch it -> it.displayId == branchName }
}

Repository targetRep = repositoryService.getBySlug(PROJECT_KEY, TARGET_REPO_NAME)
assert targetRep : 'target repository not found'

Repository mirrorRep = repositoryService.getBySlug(PROJECT_KEY, MIRROR_REPO_NAME)
assert mirrorRep : "source repository not found"

Branch targetBranch = getBranch(refService, targetRep, REBASED_BRANCH_NAME)
//if you want to rebase the default branch, use the next line instead, for better performance
//Branch targetBranch = get refService.getDefaultBranch(targetRep)
log.debug("Will rebase $targetBranch.displayId ($targetBranch.id) in $targetRep.project.key / $targetRep.slug ")

Branch bufferBranch = getBranch(refService, targetRep, BUFFER_BRANCH_NAME)
assert bufferBranch : 'buffer branch not found'
log.debug("on to buffer branch $bufferBranch.displayId ($bufferBranch.id) in $targetRep.project.key / $targetRep.slug")

Branch mirrorBranch = getBranch(refService, mirrorRep, MIRROR_BRANCH_NAME)
assert mirrorBranch : 'source branch not found'
log.debug("Mirror branch is $mirrorBranch.displayId ($mirrorBranch.id) in $mirrorRep.project.key / $mirrorRep.slug")

def latestCommit = targetBranch.latestCommit
log.debug("Latest commit in target branch is $latestCommit")

def latestMirrorCommit = mirrorBranch.latestCommit
log.debug("Latest commit in mirror branch is $latestMirrorCommit")

ApplicationUser user = userService.findUserByNameOrEmail("admin")
assert user : "user not found"

ApplicationUser committer = userService.findUserByNameOrEmail("admin")
GitCommandBuilderFactory gitCommandBuilderFactory = ScriptRunnerImpl.getOsgiService(GitCommandBuilderFactory)

// Fetching any new commits from the MIRROR_BRANCH
def fetchCommand = gitCommandBuilderFactory.builder(targetRep).fetch().repository(mirrorRep)
.refspec(mirrorBranch.id)
.tags(GitFetchTagMode.NO_TAGS)
.build(new FetchOutputHandler())
fetchCommand.setTimeout(60)
fetchCommand.call()

// Updating refs to complete pulling the changes into BUFFER_BRANCH
def updateRefCommand = gitCommandBuilderFactory.builder(targetRep).updateRef()
.set(bufferBranch.id, mirrorBranch.latestCommit)
.author(user) //for reflogs
.exitHandler(new UpdateRefCommandExitHandler(i18nService, targetRep))
.oldValue(bufferBranch.latestCommit)
.build(new FetchOutputHandler())
//optionally set a timeout in seconds
updateRefCommand.setTimeout(60)
updateRefCommand.call()

// Rebasing REBASED_BRANCH on to BUFFER_BRANCH
def rebaseCommand = extendedCommandFactory.rebase(targetRep,
new GitRebaseCommandParameters.Builder(targetBranch, latestMirrorCommit)
.committer(committer)
.commitRequired(true)
.upstreamRepository(targetRep)
.build())
//optionally set a timeout in seconds
rebaseCommand.setTimeout(60)
rebaseCommand.call()

I hope this helps. I appreciate this setup might be more complicated than expected, but this was the best way I could find around the problems and limitations that I mentioned above.

Please let us know if you have any further questions or issues.

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events