Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 136a6df

Browse files
authored
Sync script for translation repos (gatsbyjs#21185)
* add the create script * description of env * add reference to comment. * edit README * more information in README * update create script * add pulling logic * add maintainers to organization * add comment * add discord link * add more discord references * Move script explanations to the README * WIP sync script * add stuff * WIP sync * WIP sync * WIP sync * WIP sync * WIP outline of sync script * WIP outline of sync script * get closer to my goal * get closer to my goal * try to do a line counting function * try to do a line counting function * more templates * resolve conflicts * resolve conflicts function * resolve conflicts function * more work on sync script * get the thing to work hopefully * get it to work * get it to work * update stuff on sync * get rid of parse-diff * rever codesandbox * add FIXMEs * fix new repo cloning issue * more stuff * fix issues with deleted file conflicts * add documentation on sync behavior * no github comments * Create another pull request with non-conflicts * add FIXME * fix number issue * almost done! * todo * remember to switch to master branch * all * fix headings on sync guide * add more guide info * extraneous comment * information on the second pull request * add guide * remove fixme * add logging * more silencing * revert sync guide * update sync README * Update sync.js
1 parent 2435fb8 commit 136a6df

File tree

3 files changed

+265
-1
lines changed

3 files changed

+265
-1
lines changed

scripts/i18n/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ The script will take this info and:
5555
5656
This script should **only** be run by an admin of the GatsbyJS organization.
5757
58-
### `sync` (TODO)
58+
### `sync`
59+
60+
Usage:
61+
62+
```shell
63+
yarn run sync [language code]
64+
```
5965
6066
The `sync` script updates contents of the translation repository based on new changes to the repo. It can be run manually or through a bot.
67+
68+
When run, the script will:
69+
70+
- Pulls the latest version of `gatsby-i18n-source`.
71+
- Creates a "sync" pull request that updates all files that do not contain conflicts from the merge.
72+
- Creates a "conflicts" pull request that contains all merge conflicts, with instructions on how to resolve them.

scripts/i18n/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Scripts for gatsby internationalization",
55
"scripts": {
66
"create": "node ./create.js",
7+
"sync": "node ./sync.js",
78
"update-source": "node ./update-source.js"
89
},
910
"author": "Nat Alison",
@@ -14,6 +15,7 @@
1415
"dotenv": "^8.2.0",
1516
"js-yaml": "^3.13.1",
1617
"log4js": "^5.2.2",
18+
"node-fetch": "^2.6.0",
1719
"shelljs": "^0.8.3"
1820
}
1921
}

scripts/i18n/sync.js

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
const log4js = require(`log4js`)
2+
const shell = require(`shelljs`)
3+
const { graphql } = require(`@octokit/graphql`)
4+
let logger = log4js.getLogger(`sync`)
5+
6+
require(`dotenv`).config()
7+
8+
const host = `https://github.com`
9+
const cacheDir = `.cache`
10+
const owner = `gatsbyjs`
11+
const repoBase = `gatsby`
12+
// Repo to be used as basis for translations
13+
const sourceRepo = `gatsby-i18n-source`
14+
15+
const sourceRepoUrl = `${host}/${owner}/${sourceRepo}`
16+
const sourceRepoGitUrl = `${sourceRepoUrl}.git`
17+
18+
// get the git short hash
19+
function getShortHash(hash) {
20+
return hash.substr(0, 7)
21+
}
22+
23+
function cloneOrUpdateRepo(repoName, repoUrl) {
24+
if (shell.ls(repoName).code !== 0) {
25+
logger.debug(`cloning ${repoName}`)
26+
shell.exec(`git clone ${repoUrl}`)
27+
shell.cd(repoName)
28+
} else {
29+
// if the repo already exists, pull from it
30+
shell.cd(repoName)
31+
shell.exec(`git checkout master`)
32+
shell.exec(`git pull origin master`)
33+
}
34+
}
35+
36+
async function getRepository(owner, name) {
37+
const { repository } = await graphql(
38+
`
39+
query($owner: String!, $name: String!) {
40+
repository(owner: $owner, name: $name) {
41+
id
42+
}
43+
}
44+
`,
45+
{
46+
headers: {
47+
authorization: `token ${process.env.GITHUB_ADMIN_AUTH_TOKEN}`,
48+
},
49+
owner,
50+
name,
51+
}
52+
)
53+
return repository
54+
}
55+
async function createPullRequest(input) {
56+
const { createPullRequest } = await graphql(
57+
`
58+
mutation($input: CreatePullRequestInput!) {
59+
createPullRequest(input: $input) {
60+
pullRequest {
61+
id
62+
number
63+
}
64+
}
65+
}
66+
`,
67+
{
68+
headers: {
69+
authorization: `token ${process.env.GITHUB_BOT_AUTH_TOKEN}`,
70+
accept: `application/vnd.github.shadow-cat-preview+json`,
71+
},
72+
input,
73+
}
74+
)
75+
return createPullRequest.pullRequest
76+
}
77+
78+
function conflictPRBody(conflictFiles, comparisonUrl, prNumber) {
79+
return `
80+
Sync conflicts with the source repo. Please update the translations based on updated source content.
81+
82+
For more information on how to resolve sync conflicts, check out the [guide for syncing translations](https://gatsbyjs.org/contributing/translation/sync-guide/).
83+
84+
<details ${conflictFiles.length <= 10 ? `open` : ``}>
85+
<summary>The following ${
86+
conflictFiles.length
87+
} files have conflicts:</summary><br />
88+
89+
${conflictFiles.map(file => `* [ ] ${file}`).join(`\n`)}
90+
</details>
91+
92+
Once all the commits have been fixed, mark this pull request as "Ready for review" and merge it in!
93+
94+
See all changes since the last sync here:
95+
96+
${comparisonUrl}
97+
98+
NOTE: Do **NOT** squash-merge this pull request. The sync script requires a ref to the source repo in order to work correctly.
99+
100+
## Related PRs
101+
102+
#${prNumber} PR for syncing non-conflicting files
103+
`
104+
}
105+
106+
function syncPRBody() {
107+
return `
108+
Sync all non-conflicting files with the source repo. This PR contains all updates that do not cause any conflicts and can be merged immediately.
109+
110+
NOTE: Do *NOT* squash-merge this pull request. The sync script requires a ref to the source repo in order to work correctly.
111+
`
112+
}
113+
114+
async function syncTranslationRepo(code) {
115+
logger = log4js.getLogger(`sync:` + code)
116+
logger.level = `info`
117+
const transRepoName = `${repoBase}-${code}`
118+
const transRepoUrl = `${host}/${owner}/${transRepoName}`
119+
if (shell.cd(cacheDir).code !== 0) {
120+
logger.debug(`creating ${cacheDir}`)
121+
shell.mkdir(cacheDir)
122+
shell.cd(cacheDir)
123+
}
124+
cloneOrUpdateRepo(transRepoName, transRepoUrl)
125+
126+
shell.exec(`git remote add source ${sourceRepoGitUrl}`)
127+
shell.exec(`git fetch source master`)
128+
129+
// TODO don't run the sync script if there is a current PR from the bot
130+
131+
// TODO exit early if this fails
132+
// Compare these changes
133+
const baseHash = shell
134+
.exec(`git merge-base origin/master source/master`, { silent: true })
135+
.stdout.replace(`\n`, ``)
136+
const shortBaseHash = getShortHash(baseHash)
137+
138+
const hash = shell
139+
.exec(`git rev-parse source/master`, { silent: true })
140+
.stdout.replace(`\n`, ``)
141+
const shortHash = getShortHash(hash)
142+
143+
logger.info(`Syncing with source (no conflicts)...`)
144+
const syncBranch = `sync-${shortHash}`
145+
if (shell.exec(`git checkout ${syncBranch}`, { silent: true }).code !== 0) {
146+
shell.exec(`git checkout -b ${syncBranch}`)
147+
}
148+
shell.exec(`git pull source master --no-edit --strategy-option ours`, {
149+
silent: true,
150+
})
151+
152+
// Remove files that are deleted by upstream
153+
// https://stackoverflow.com/a/54232519
154+
shell.exec(`git diff --name-only --diff-filter=U | xargs git rm`)
155+
shell.exec(`git ci --no-edit`)
156+
157+
shell.exec(`git push -u origin ${syncBranch}`)
158+
159+
const repository = await getRepository(owner, transRepoName)
160+
161+
logger.info(`Creating sync pull request`)
162+
// TODO if there is already an existing PR, don't create a new one and exit early
163+
const { number: syncPRNumber } = await createPullRequest({
164+
repositoryId: repository.id,
165+
baseRefName: `master`,
166+
headRefName: syncBranch,
167+
title: `(sync) Sync with ${sourceRepo} @ ${shortHash}`,
168+
body: syncPRBody(),
169+
maintainerCanModify: true,
170+
})
171+
172+
// if we successfully publish the PR, pull again and create a new PR --
173+
shell.exec(`git checkout master`)
174+
175+
const comparisonUrl = `${sourceRepoUrl}/compare/${shortBaseHash}..${shortHash}`
176+
177+
// Check out a new branch
178+
logger.info(`Finding conflicts with source...`)
179+
const conflictBranch = `conflicts-${shortHash}`
180+
if (
181+
shell.exec(`git checkout ${conflictBranch}`, { silent: true }).code !== 0
182+
) {
183+
shell.exec(`git checkout -b ${conflictBranch}`)
184+
}
185+
186+
// pull from the source
187+
const output = shell.exec(`git pull source master`, { silent: true }).stdout
188+
if (output.includes(`Already up to date.`)) {
189+
logger.info(`We are already up to date with source.`)
190+
process.exit(0)
191+
}
192+
const lines = output.split(`\n`)
193+
194+
// find all merge conflicts
195+
const conflictLines = lines.filter(line =>
196+
line.startsWith(`CONFLICT (content)`)
197+
)
198+
199+
// If no conflicts, exit early
200+
if (conflictLines.length === 0) {
201+
logger.info(`No conflicting files found. Exiting...`)
202+
process.exit(0)
203+
}
204+
205+
// Message is of the form:
206+
// CONFLICT (content): Merge conflict in {file path}
207+
const conflictFiles = conflictLines.map(line =>
208+
line.substr(line.lastIndexOf(` `) + 1)
209+
)
210+
// Do a soft reset and unstage non-conflicted files
211+
shell.exec(`git reset`, { silent: true })
212+
213+
// Add all the conflicts as-is
214+
shell.exec(`git add ${conflictFiles.join(` `)}`)
215+
216+
const removedLines = lines.filter(line =>
217+
line.startsWith(`CONFLICT (modify/delete)`)
218+
)
219+
// Deleted message format:
220+
// CONFLICT (modify/delete): {file path} deleted in {hash} and modified in HEAD. Version HEAD of {file path} left in tree.
221+
const removedFiles = removedLines.map(
222+
line => line.replace(`CONFLICT (modify/delete): `, ``).split(` `)[0]
223+
)
224+
if (removedFiles.length > 0) {
225+
shell.exec(`git rm ${removedFiles.join(` `)}`, { silent: true })
226+
}
227+
228+
// clean out the rest of the changed files
229+
shell.exec(`git checkout -- .`)
230+
shell.exec(`git clean -fd`, { silent: true })
231+
232+
// Commit the conflicts into the new branch and push it
233+
shell.exec(`git commit -m "Commit git conflicts"`, { silent: true })
234+
shell.exec(`git push -u origin ${conflictBranch}`)
235+
236+
logger.info(`Creating conflicts pull request`)
237+
// TODO assign codeowners as reviewers
238+
await createPullRequest({
239+
repositoryId: repository.id,
240+
baseRefName: `master`,
241+
headRefName: conflictBranch,
242+
title: `(sync) Resolve conflicts with ${sourceRepo} @ ${shortHash}`,
243+
body: conflictPRBody(conflictFiles, comparisonUrl, syncPRNumber),
244+
maintainerCanModify: true,
245+
draft: true,
246+
})
247+
}
248+
249+
const [langCode] = process.argv.slice(2)
250+
syncTranslationRepo(langCode)

0 commit comments

Comments
 (0)