Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Create Commit tree using GitHub API #321

Merged
merged 1 commit into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as core from '@actions/core'
import * as yaml from 'js-yaml'
import * as fs from 'fs-extra'
import fs from 'fs-extra'
import * as path from 'path'
import { getInput } from 'action-input-parser'

Expand Down
158 changes: 89 additions & 69 deletions src/git.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as github from '@actions/github'
import { GitHub, getOctokitOptions } from '@actions/github/lib/utils.js'
import { throttling } from '@octokit/plugin-throttling'
import * as path from 'path'
import * as fs from 'fs/promises'

import config from './config.js'

Expand Down Expand Up @@ -200,13 +199,6 @@ export default class Git {
}
}

async getBlobBase64Content(file) {
const fileRelativePath = path.join(this.workingDir, file)
const fileContent = await fs.readFile(fileRelativePath)

return fileContent.toString('base64')
}

async getLastCommitSha() {
this.lastCommitSha = await execCmd(
`git rev-parse HEAD`,
Expand Down Expand Up @@ -243,56 +235,70 @@ export default class Git {
}

// Returns a git tree parsed for the specified commit sha
async getTree(commitSha) {
const output = await execCmd(
`git ls-tree -r --full-tree ${ commitSha }`,
async getTreeId(commitSha) {
core.debug(`Getting treeId for commit ${ commitSha }`)
const output = (await execCmd(
`git cat-file -p ${ commitSha }`,
this.workingDir
)
)).split('\n')

const tree = []
for (const treeObject of output.split('\n')) {
const [ mode, type, sha ] = treeObject.split(/\s/)
const file = treeObject.split('\t')[1]

const treeEntry = {
mode,
type,
sha,
path: file
}

tree.push(treeEntry)
}
const commitHeaders = output.slice(0, output.findIndex((e) => e === ''))
const tree = commitHeaders.find((e) => e.startsWith('tree')).replace('tree ', '')

return tree
}

// Creates the blob objects in GitHub for the files that are not in the previous commit only
async createGithubBlobs(commitSha) {
core.debug('Creating missing blobs on GitHub')
const [ previousTree, tree ] = await Promise.all([ this.getTree(`${ commitSha }~1`), this.getTree(commitSha) ])
const promisesGithubCreateBlobs = []

for (const treeEntry of tree) {
// 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.
if (previousTree.findIndex((entry) => entry.sha === treeEntry.sha) !== -1) {
continue
}

const base64Content = await this.getBlobBase64Content(treeEntry.path)
async getTreeDiff(referenceTreeId, differenceTreeId) {
const output = await execCmd(
`git diff-tree ${ referenceTreeId } ${ differenceTreeId } -r`,
this.workingDir
)

// 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
const githubCreateBlobRequest = this.github.git.createBlob({
owner: this.repo.user,
repo: this.repo.name,
content: base64Content,
encoding: 'base64'
const diff = []
for (const line of output.split('\n')) {
const splitted = line
.replace(/^:/, '')
.replace('\t', ' ')
.split(' ')

const [
newMode,
previousMode,
newBlob,
previousBlob,
change,
path
] = splitted

diff.push({
newMode,
previousMode,
newBlob,
previousBlob,
change,
path
})
promisesGithubCreateBlobs.push(githubCreateBlobRequest)
}

// Wait for all the file uploads to be completed
await Promise.all(promisesGithubCreateBlobs)
return diff
}

// Creates the blob objects in GitHub for the files that are not in the previous commit only
async uploadGitHubBlob(blob) {
core.debug(`Uploading GitHub Blob for blob ${ blob }`)
const fileContent = await execCmd(
`git cat-file -p ${ blob }`,
this.workingDir,
false
)

// 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi,
We have integrated this PR as it solves a couple of issues for us, however - while I can't find documentation on it - we have observed a behavior (that happens rarely) where the local sha is different from the sha returned by github api. I know it makes no sense given git internals, not sure what exactly happens here, probably some transformation on the content/filename by the API.

So I would amend the code to use the remote sha

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @sthuck.

When did you find the problem? I found the same problem that you described after publishing the pull request, but I solved it (or I thought I did at least) the day 11/09. I forced pushed the commit to my branch replacing the previous commit.

The problem was caused by my usage of the ExecCmd function which by default trims the output of the command. In this case, I use it to get the content of a blob in the local git, which is a problem for the files is a problem for files which end with empty lines. I solved it passing the parameter to disable the trimming of the command (alvarezfr@0fad1b1#diff-30246b15f18bb39311579208583260447efd5ee017c25172393ad4505bb8c5a4R292). Based on my tests this solves the issue.

return this.github.git.createBlob({
owner: this.repo.user,
repo: this.repo.name,
content: Buffer.from(fileContent).toString('base64'),
encoding: 'base64'
})
}

// Gets the commit list in chronological order
Expand All @@ -313,25 +319,10 @@ export default class Git {
)
}

// Returns an array of objects with the git tree and the commit, one entry for each pending commit to push
async getCommitsDataToPush() {
const commitsToPush = await this.getCommitsToPush()

const commitsData = []
for (const commitSha of commitsToPush) {
const [ commitMessage, tree ] = await Promise.all([ this.getCommitMessage(commitSha), this.getTree(commitSha), this.createGithubBlobs(commitSha) ])
const commitData = {
commitMessage,
tree
}
commitsData.push(commitData)
}
return commitsData
}

// A wrapper for running all the flow to generate all the pending commits using the GitHub API
async createGithubVerifiedCommits() {
const commitsData = await this.getCommitsDataToPush()
core.debug(`Creating Commits using GitHub API`)
const commits = await this.getCommitsToPush()

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

for (const commitData of commitsData) {
await this.createGithubTreeAndCommit(commitData.tree, commitData.commitMessage)
for (const commit of commits) {
await this.createGithubCommit(commit)
}

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

async createGithubTreeAndCommit(tree, commitMessage) {
async createGithubCommit(commitSha) {
const [ treeId, parentTreeId, commitMessage ] = await Promise.all([
this.getTreeId(`${ commitSha }`),
this.getTreeId(`${ commitSha }~1`),
this.getCommitMessage(commitSha)
])

const treeDiff = await this.getTreeDiff(treeId, parentTreeId)
core.debug(`Uploading the blobs to GitHub`)
const blobsToCreate = treeDiff
.filter((e) => e.newMode !== '000000') // Do not upload the blob if it is being removed

await Promise.all(blobsToCreate.map((e) => this.uploadGitHubBlob(e.newBlob)))
core.debug(`Creating a GitHub tree`)
const tree = treeDiff.map((e) => {
if (e.newMode === '000000') { // Set the sha to null to remove the file
e.newMode = e.previousMode
e.newBlob = null
}

const entry = {
path: e.path,
mode: e.newMode,
type: 'blob',
sha: e.newBlob
}

return entry
})

let treeSha
try {
const request = await this.github.git.createTree({
owner: this.repo.user,
repo: this.repo.name,
tree
tree,
base_tree: parentTreeId
})
treeSha = request.data.sha
} catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions src/helpers.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as fs from 'fs-extra'
import fs from 'fs-extra'
import readfiles from 'node-readfiles'
import { exec } from 'child_process'
import * as core from '@actions/core'
import * as path from 'path'
import * as nunjucks from 'nunjucks'
import nunjucks from 'nunjucks'

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

Expand Down