Skip to content

Commit 80b8272

Browse files
authored
Merge pull request #3 from gitgitgadget/sync-git-gui
Add a scheduled workflow to synchronize Git GUI's branches
2 parents fa784e9 + eddbced commit 80b8272

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

.github/workflows/sync-git-gui.yml

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
name: sync-git-gui-branches
2+
3+
on:
4+
schedule:
5+
- cron: '31 22 * * *'
6+
workflow_dispatch:
7+
8+
env:
9+
SOURCE_REPOSITORY: prati0100/git-gui
10+
TARGET_REPOSITORY: gitgitgadget/git
11+
TARGET_REF_NAMESPACE: git-gui/
12+
13+
# We want to limit queuing to a single workflow run i.e. if there is already
14+
# an active workflow run and a queued one, queue another one canceling the
15+
# already queued one.
16+
concurrency:
17+
group: ${{ github.workflow }}
18+
19+
jobs:
20+
sync-git-gui-branches:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: check which refs need to be synchronized
24+
uses: actions/github-script@v6
25+
id: check
26+
with:
27+
script: |
28+
const sleep = async (milliseconds) => {
29+
return new Promise(resolve => setTimeout(resolve, milliseconds))
30+
}
31+
32+
const getRefs = async (repository, stripRefsPrefix) => {
33+
let attemptCounter = 1
34+
for (;;) {
35+
try {
36+
const [owner, repo] = repository.split('/')
37+
let { data } = await github.rest.git.listMatchingRefs({
38+
owner,
39+
repo,
40+
ref: 'heads/'
41+
})
42+
43+
data = data.filter(e => {
44+
if (!e.ref.startsWith('refs/heads/')) return false
45+
e.name = e.ref.slice(11)
46+
return true
47+
})
48+
49+
if (stripRefsPrefix) {
50+
data = data.filter(e => {
51+
if (!e.name.startsWith(stripRefsPrefix)) return false
52+
e.name = e.name.slice(stripRefsPrefix.length)
53+
return true
54+
})
55+
}
56+
57+
return data
58+
.sort((a, b) => a.ref.localeCompare(b.ref))
59+
} catch (e) {
60+
if (e?.status !== 502) throw e
61+
}
62+
63+
if (++attemptCounter > 10) throw new Error('Giving up listing refs after 10 attempts')
64+
65+
const seconds = attemptCounter * attemptCounter + 15 * Math.random()
66+
core.info(`Encountered a Server Error; retrying in ${seconds} seconds`)
67+
await sleep(1000 * seconds)
68+
}
69+
}
70+
71+
const sourceRefs = await getRefs(process.env.SOURCE_REPOSITORY)
72+
const targetRefs = await getRefs(process.env.TARGET_REPOSITORY, process.env.TARGET_REF_NAMESPACE)
73+
74+
const targetPrefix = `refs/heads/${process.env.TARGET_REF_NAMESPACE}`
75+
76+
const refspecs = []
77+
const toFetch = new Set()
78+
for (let i = 0, j = 0; i < sourceRefs.length || j < targetRefs.length; ) {
79+
const compare = i >= sourceRefs.length
80+
? +1
81+
: j >= targetRefs.length
82+
? -1
83+
: sourceRefs[i].name.localeCompare(targetRefs[j].name)
84+
if (compare > 0) {
85+
// no source ref => delete target ref
86+
refspecs.push(`:${targetPrefix}${targetRefs[j].name}`)
87+
j++
88+
} else if (compare < 0) {
89+
// no corresponding target ref yet => push source ref (new)
90+
const sha = sourceRefs[i].object.sha
91+
toFetch.add(sha)
92+
refspecs.push(`${sha}:${targetPrefix}${sourceRefs[i].name}`)
93+
i++
94+
} else {
95+
// the sourceRef's name matches the targetRef's
96+
if (sourceRefs[i].object.sha !== targetRefs[j].object.sha) {
97+
// target ref needs updating
98+
const sha = sourceRefs[i].object.sha
99+
toFetch.add(sha)
100+
refspecs.push(`+${sha}:${targetPrefix}${sourceRefs[i].name}`)
101+
}
102+
i++
103+
j++
104+
}
105+
}
106+
107+
core.setOutput('refspec', refspecs.join(' '))
108+
targetRefs.forEach((e) => toFetch.delete(e.object.sha))
109+
core.setOutput('to-fetch', [...toFetch].join(' '))
110+
- name: obtain installation token
111+
if: steps.check.outputs.refspec != ''
112+
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
113+
id: token
114+
with:
115+
app_id: ${{ secrets.GITGITGADGET_GITHUB_APP_ID }}
116+
private_key: ${{ secrets.GITGITGADGET_GITHUB_APP_PRIVATE_KEY }}
117+
repository: ${{ env.TARGET_REPOSITORY }}
118+
- name: set authorization header
119+
if: steps.check.outputs.refspec != ''
120+
uses: actions/github-script@v6
121+
id: auth
122+
with:
123+
script: |
124+
// Sadly, `git push` does not work with 'Authorization: Bearer <PAT>', therefore
125+
// we have to use the `Basic` variant
126+
const auth = Buffer.from('PAT:${{ steps.token.outputs.token }}').toString('base64')
127+
core.setSecret(auth)
128+
core.setOutput('header', `Authorization: Basic ${auth}`)
129+
- name: sync
130+
if: steps.check.outputs.refspec != ''
131+
shell: bash
132+
run: |
133+
set -ex
134+
git init --bare
135+
136+
git remote add source "${{ github.server_url }}/$SOURCE_REPOSITORY"
137+
# pretend to be a partial clone
138+
git config remote.source.promisor true
139+
git config remote.source.partialCloneFilter blob:none
140+
141+
# fetch some commits
142+
printf '%s' '${{ steps.check.outputs.to-fetch }}' |
143+
xargs -d ' ' -r git fetch --depth 10000 source
144+
rm -f .git/shallow
145+
146+
# push the commits
147+
printf '%s' '${{ steps.check.outputs.refspec }}' |
148+
xargs -d ' ' -r git -c http.extraHeader='${{ steps.auth.outputs.header }}' \
149+
push "${{ github.server_url }}/$TARGET_REPOSITORY"

0 commit comments

Comments
 (0)