diff --git a/.github/workflows/sync-git-gui.yml b/.github/workflows/sync-git-gui.yml new file mode 100644 index 0000000..d9a2a7e --- /dev/null +++ b/.github/workflows/sync-git-gui.yml @@ -0,0 +1,149 @@ +name: sync-git-gui-branches + +on: + schedule: + - cron: '31 22 * * *' + workflow_dispatch: + +env: + SOURCE_REPOSITORY: prati0100/git-gui + TARGET_REPOSITORY: gitgitgadget/git + TARGET_REF_NAMESPACE: git-gui/ + +# We want to limit queuing to a single workflow run i.e. if there is already +# an active workflow run and a queued one, queue another one canceling the +# already queued one. +concurrency: + group: ${{ github.workflow }} + +jobs: + sync-git-gui-branches: + runs-on: ubuntu-latest + steps: + - name: check which refs need to be synchronized + uses: actions/github-script@v6 + id: check + with: + script: | + const sleep = async (milliseconds) => { + return new Promise(resolve => setTimeout(resolve, milliseconds)) + } + + const getRefs = async (repository, stripRefsPrefix) => { + let attemptCounter = 1 + for (;;) { + try { + const [owner, repo] = repository.split('/') + let { data } = await github.rest.git.listMatchingRefs({ + owner, + repo, + ref: 'heads/' + }) + + data = data.filter(e => { + if (!e.ref.startsWith('refs/heads/')) return false + e.name = e.ref.slice(11) + return true + }) + + if (stripRefsPrefix) { + data = data.filter(e => { + if (!e.name.startsWith(stripRefsPrefix)) return false + e.name = e.name.slice(stripRefsPrefix.length) + return true + }) + } + + return data + .sort((a, b) => a.ref.localeCompare(b.ref)) + } catch (e) { + if (e?.status !== 502) throw e + } + + if (++attemptCounter > 10) throw new Error('Giving up listing refs after 10 attempts') + + const seconds = attemptCounter * attemptCounter + 15 * Math.random() + core.info(`Encountered a Server Error; retrying in ${seconds} seconds`) + await sleep(1000 * seconds) + } + } + + const sourceRefs = await getRefs(process.env.SOURCE_REPOSITORY) + const targetRefs = await getRefs(process.env.TARGET_REPOSITORY, process.env.TARGET_REF_NAMESPACE) + + const targetPrefix = `refs/heads/${process.env.TARGET_REF_NAMESPACE}` + + const refspecs = [] + const toFetch = new Set() + for (let i = 0, j = 0; i < sourceRefs.length || j < targetRefs.length; ) { + const compare = i >= sourceRefs.length + ? +1 + : j >= targetRefs.length + ? -1 + : sourceRefs[i].name.localeCompare(targetRefs[j].name) + if (compare > 0) { + // no source ref => delete target ref + refspecs.push(`:${targetPrefix}${targetRefs[j].name}`) + j++ + } else if (compare < 0) { + // no corresponding target ref yet => push source ref (new) + const sha = sourceRefs[i].object.sha + toFetch.add(sha) + refspecs.push(`${sha}:${targetPrefix}${sourceRefs[i].name}`) + i++ + } else { + // the sourceRef's name matches the targetRef's + if (sourceRefs[i].object.sha !== targetRefs[j].object.sha) { + // target ref needs updating + const sha = sourceRefs[i].object.sha + toFetch.add(sha) + refspecs.push(`+${sha}:${targetPrefix}${sourceRefs[i].name}`) + } + i++ + j++ + } + } + + core.setOutput('refspec', refspecs.join(' ')) + targetRefs.forEach((e) => toFetch.delete(e.object.sha)) + core.setOutput('to-fetch', [...toFetch].join(' ')) + - name: obtain installation token + if: steps.check.outputs.refspec != '' + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + id: token + with: + app_id: ${{ secrets.GITGITGADGET_GITHUB_APP_ID }} + private_key: ${{ secrets.GITGITGADGET_GITHUB_APP_PRIVATE_KEY }} + repository: ${{ env.TARGET_REPOSITORY }} + - name: set authorization header + if: steps.check.outputs.refspec != '' + uses: actions/github-script@v6 + id: auth + with: + script: | + // Sadly, `git push` does not work with 'Authorization: Bearer ', therefore + // we have to use the `Basic` variant + const auth = Buffer.from('PAT:${{ steps.token.outputs.token }}').toString('base64') + core.setSecret(auth) + core.setOutput('header', `Authorization: Basic ${auth}`) + - name: sync + if: steps.check.outputs.refspec != '' + shell: bash + run: | + set -ex + git init --bare + + git remote add source "${{ github.server_url }}/$SOURCE_REPOSITORY" + # pretend to be a partial clone + git config remote.source.promisor true + git config remote.source.partialCloneFilter blob:none + + # fetch some commits + printf '%s' '${{ steps.check.outputs.to-fetch }}' | + xargs -d ' ' -r git fetch --depth 10000 source + rm -f .git/shallow + + # push the commits + printf '%s' '${{ steps.check.outputs.refspec }}' | + xargs -d ' ' -r git -c http.extraHeader='${{ steps.auth.outputs.header }}' \ + push "${{ github.server_url }}/$TARGET_REPOSITORY"