Skip to content

Commit 899c636

Browse files
authored
🐛 Create Commit tree using GitHub API (#321)
Fixes #246
1 parent 3023dac commit 899c636

File tree

4 files changed

+94
-74
lines changed

4 files changed

+94
-74
lines changed

dist/index.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as core from '@actions/core'
22
import * as yaml from 'js-yaml'
3-
import * as fs from 'fs-extra'
3+
import fs from 'fs-extra'
44
import * as path from 'path'
55
import { getInput } from 'action-input-parser'
66

src/git.js

+89-69
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as github from '@actions/github'
44
import { GitHub, getOctokitOptions } from '@actions/github/lib/utils.js'
55
import { throttling } from '@octokit/plugin-throttling'
66
import * as path from 'path'
7-
import * as fs from 'fs/promises'
87

98
import config from './config.js'
109

@@ -200,13 +199,6 @@ export default class Git {
200199
}
201200
}
202201

203-
async getBlobBase64Content(file) {
204-
const fileRelativePath = path.join(this.workingDir, file)
205-
const fileContent = await fs.readFile(fileRelativePath)
206-
207-
return fileContent.toString('base64')
208-
}
209-
210202
async getLastCommitSha() {
211203
this.lastCommitSha = await execCmd(
212204
`git rev-parse HEAD`,
@@ -243,56 +235,70 @@ export default class Git {
243235
}
244236

245237
// Returns a git tree parsed for the specified commit sha
246-
async getTree(commitSha) {
247-
const output = await execCmd(
248-
`git ls-tree -r --full-tree ${ commitSha }`,
238+
async getTreeId(commitSha) {
239+
core.debug(`Getting treeId for commit ${ commitSha }`)
240+
const output = (await execCmd(
241+
`git cat-file -p ${ commitSha }`,
249242
this.workingDir
250-
)
243+
)).split('\n')
251244

252-
const tree = []
253-
for (const treeObject of output.split('\n')) {
254-
const [ mode, type, sha ] = treeObject.split(/\s/)
255-
const file = treeObject.split('\t')[1]
256-
257-
const treeEntry = {
258-
mode,
259-
type,
260-
sha,
261-
path: file
262-
}
263-
264-
tree.push(treeEntry)
265-
}
245+
const commitHeaders = output.slice(0, output.findIndex((e) => e === ''))
246+
const tree = commitHeaders.find((e) => e.startsWith('tree')).replace('tree ', '')
266247

267248
return tree
268249
}
269250

270-
// Creates the blob objects in GitHub for the files that are not in the previous commit only
271-
async createGithubBlobs(commitSha) {
272-
core.debug('Creating missing blobs on GitHub')
273-
const [ previousTree, tree ] = await Promise.all([ this.getTree(`${ commitSha }~1`), this.getTree(commitSha) ])
274-
const promisesGithubCreateBlobs = []
275-
276-
for (const treeEntry of tree) {
277-
// If the current treeEntry are in the previous tree, that means that the blob is uploaded and it doesn't need to be uploaded to GitHub again.
278-
if (previousTree.findIndex((entry) => entry.sha === treeEntry.sha) !== -1) {
279-
continue
280-
}
281-
282-
const base64Content = await this.getBlobBase64Content(treeEntry.path)
251+
async getTreeDiff(referenceTreeId, differenceTreeId) {
252+
const output = await execCmd(
253+
`git diff-tree ${ referenceTreeId } ${ differenceTreeId } -r`,
254+
this.workingDir
255+
)
283256

284-
// Creates the blob. We don't need to store the response because the local sha is the same and we can use it to reference the blob
285-
const githubCreateBlobRequest = this.github.git.createBlob({
286-
owner: this.repo.user,
287-
repo: this.repo.name,
288-
content: base64Content,
289-
encoding: 'base64'
257+
const diff = []
258+
for (const line of output.split('\n')) {
259+
const splitted = line
260+
.replace(/^:/, '')
261+
.replace('\t', ' ')
262+
.split(' ')
263+
264+
const [
265+
newMode,
266+
previousMode,
267+
newBlob,
268+
previousBlob,
269+
change,
270+
path
271+
] = splitted
272+
273+
diff.push({
274+
newMode,
275+
previousMode,
276+
newBlob,
277+
previousBlob,
278+
change,
279+
path
290280
})
291-
promisesGithubCreateBlobs.push(githubCreateBlobRequest)
292281
}
293282

294-
// Wait for all the file uploads to be completed
295-
await Promise.all(promisesGithubCreateBlobs)
283+
return diff
284+
}
285+
286+
// Creates the blob objects in GitHub for the files that are not in the previous commit only
287+
async uploadGitHubBlob(blob) {
288+
core.debug(`Uploading GitHub Blob for blob ${ blob }`)
289+
const fileContent = await execCmd(
290+
`git cat-file -p ${ blob }`,
291+
this.workingDir,
292+
false
293+
)
294+
295+
// Creates the blob. We don't need to store the response because the local sha is the same and we can use it to reference the blob
296+
return this.github.git.createBlob({
297+
owner: this.repo.user,
298+
repo: this.repo.name,
299+
content: Buffer.from(fileContent).toString('base64'),
300+
encoding: 'base64'
301+
})
296302
}
297303

298304
// Gets the commit list in chronological order
@@ -313,25 +319,10 @@ export default class Git {
313319
)
314320
}
315321

316-
// Returns an array of objects with the git tree and the commit, one entry for each pending commit to push
317-
async getCommitsDataToPush() {
318-
const commitsToPush = await this.getCommitsToPush()
319-
320-
const commitsData = []
321-
for (const commitSha of commitsToPush) {
322-
const [ commitMessage, tree ] = await Promise.all([ this.getCommitMessage(commitSha), this.getTree(commitSha), this.createGithubBlobs(commitSha) ])
323-
const commitData = {
324-
commitMessage,
325-
tree
326-
}
327-
commitsData.push(commitData)
328-
}
329-
return commitsData
330-
}
331-
332322
// A wrapper for running all the flow to generate all the pending commits using the GitHub API
333323
async createGithubVerifiedCommits() {
334-
const commitsData = await this.getCommitsDataToPush()
324+
core.debug(`Creating Commits using GitHub API`)
325+
const commits = await this.getCommitsToPush()
335326

336327
if (SKIP_PR === false) {
337328
// Creates the PR branch if doesn't exists
@@ -350,8 +341,8 @@ export default class Git {
350341
}
351342
}
352343

353-
for (const commitData of commitsData) {
354-
await this.createGithubTreeAndCommit(commitData.tree, commitData.commitMessage)
344+
for (const commit of commits) {
345+
await this.createGithubCommit(commit)
355346
}
356347

357348
core.debug(`Updating branch ${ SKIP_PR === false ? this.prBranch : this.baseBranch } ref`)
@@ -502,14 +493,43 @@ export default class Git {
502493
})
503494
}
504495

505-
async createGithubTreeAndCommit(tree, commitMessage) {
496+
async createGithubCommit(commitSha) {
497+
const [ treeId, parentTreeId, commitMessage ] = await Promise.all([
498+
this.getTreeId(`${ commitSha }`),
499+
this.getTreeId(`${ commitSha }~1`),
500+
this.getCommitMessage(commitSha)
501+
])
502+
503+
const treeDiff = await this.getTreeDiff(treeId, parentTreeId)
504+
core.debug(`Uploading the blobs to GitHub`)
505+
const blobsToCreate = treeDiff
506+
.filter((e) => e.newMode !== '000000') // Do not upload the blob if it is being removed
507+
508+
await Promise.all(blobsToCreate.map((e) => this.uploadGitHubBlob(e.newBlob)))
506509
core.debug(`Creating a GitHub tree`)
510+
const tree = treeDiff.map((e) => {
511+
if (e.newMode === '000000') { // Set the sha to null to remove the file
512+
e.newMode = e.previousMode
513+
e.newBlob = null
514+
}
515+
516+
const entry = {
517+
path: e.path,
518+
mode: e.newMode,
519+
type: 'blob',
520+
sha: e.newBlob
521+
}
522+
523+
return entry
524+
})
525+
507526
let treeSha
508527
try {
509528
const request = await this.github.git.createTree({
510529
owner: this.repo.user,
511530
repo: this.repo.name,
512-
tree
531+
tree,
532+
base_tree: parentTreeId
513533
})
514534
treeSha = request.data.sha
515535
} catch (error) {

src/helpers.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import * as fs from 'fs-extra'
1+
import fs from 'fs-extra'
22
import readfiles from 'node-readfiles'
33
import { exec } from 'child_process'
44
import * as core from '@actions/core'
55
import * as path from 'path'
6-
import * as nunjucks from 'nunjucks'
6+
import nunjucks from 'nunjucks'
77

88
nunjucks.configure({ autoescape: true, trimBlocks: true, lstripBlocks: true })
99

0 commit comments

Comments
 (0)