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

Add support for rebase merging #164

Merged
merged 18 commits into from
Apr 26, 2019
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ You can configure Release Drafter using the following key in your `.github/relea
| `no-changes-template` | Optional | The template to use for when there’s no changes. Default: `"* No changes"`. |
| `branches` | Optional | The branches to listen for configuration updates to `.github/release-drafter.yml` and for merge commits. Useful if you want to test the app on a pull request branch. Default is the repository’s default branch. |
| `categories` | Optional | Categorize pull requests using labels. Refer to [Categorize Pull Requests](#categorize-pull-requests) to learn more about this option. |
| `exclude-lables` | Optional | Exclude pull requests using labels. Refer to [Exclude Pull Requests](#exclude-pull-requests) to learn more about this option. |
| `exclude-labels` | Optional | Exclude pull requests using labels. Refer to [Exclude Pull Requests](#exclude-pull-requests) to learn more about this option. |

Release Drafter also supports [Probot Config](https://github.com/probot/probot-config), if you want to store your configuration files in a central repository. This allows you to share configurations between projects, and create a organization-wide configuration file by creating a repository named `.github` with the file `.github/release-drafter.yml`.

Expand Down Expand Up @@ -133,10 +133,10 @@ With the `exclude-labels` option you can exclude pull requests from the release

```yml
exclude-labels:
- release
- skip-changelog
```

Pull requests with the label "release" will now be excluded from the release draft.
Pull requests with the label "skip-changelog" will now be excluded from the release draft.

## Projects that don't use Semantic Versioning

Expand Down
14 changes: 11 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const getConfig = require('probot-config')
const { isTriggerableBranch } = require('./lib/triggerable-branch')
const { findReleases, generateReleaseInfo } = require('./lib/releases')
const { findCommits, findPullRequests } = require('./lib/commits')
const { findCommitsWithAssociatedPullRequests } = require('./lib/commits')
const log = require('./lib/log')

const configName = 'release-drafter.yml'
Expand Down Expand Up @@ -33,8 +33,16 @@ module.exports = app => {
}

const { draftRelease, lastRelease } = await findReleases({ app, context })
const commits = await findCommits({ app, context, branch, lastRelease })
const mergedPullRequests = await findPullRequests({ app, context, commits })
const {
commits,
pullRequests: mergedPullRequests
} = await findCommitsWithAssociatedPullRequests({
app,
context,
branch,
lastRelease
})

const releaseInfo = generateReleaseInfo({
commits,
config,
Expand Down
147 changes: 84 additions & 63 deletions lib/commits.js
Original file line number Diff line number Diff line change
@@ -1,78 +1,99 @@
const log = require('./log')
const paginate = require('./pagination')
const _ = require('lodash')
Copy link
Member

Choose a reason for hiding this comment

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

Would it not be better only to require what you need?

Copy link
Member Author

Choose a reason for hiding this comment

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

Performance wise it shouldn't really matter I think? I like the convenience of having all of Lodash under a single char 😅


module.exports.findCommits = async ({ app, context, branch, lastRelease }) => {
const findCommitsWithAssociatedPullRequestsQuery = /* GraphQL */ `
query findCommitsWithAssociatedPullRequests(
$name: String!
$owner: String!
$branch: String!
$since: GitTimestamp
$after: String
) {
repository(name: $name, owner: $owner) {
ref(qualifiedName: $branch) {
target {
... on Commit {
history(first: 100, since: $since, after: $after) {
totalCount
pageInfo {
hasNextPage
endCursor
}
nodes {
id
message
author {
name
user {
login
}
}
associatedPullRequests(first: 5) {
nodes {
title
number
author {
login
}
labels(first: 10) {
nodes {
name
}
}
}
}
}
}
}
}
}
}
}
`

module.exports.findCommitsWithAssociatedPullRequests = async ({
app,
context,
branch,
lastRelease
}) => {
const { owner, repo } = context.repo()
const variables = { name: repo, owner, branch }
const dataPath = ['repository', 'ref', 'target', 'history']

let data
if (lastRelease) {
log({
app,
context,
message: `Comparing commits ${lastRelease.tag_name}..${branch}`
message: `Fetching all commits for branch ${branch} since ${
lastRelease.published_at
Copy link
Member

Choose a reason for hiding this comment

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

looks funky

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah looks a bit weird, it happens because the max line length is reached. There's not much I can do about it except increasing the line length 😬. I personally don't really mind though.

}`
})
// Only supports up to 250 commits
return context.github.repos
.compareCommits(
context.repo({
base: lastRelease.tag_name,
head: branch
})
)
.then(res => res.data.commits)

data = await paginate(
context.github.graphql,
findCommitsWithAssociatedPullRequestsQuery,
{ ...variables, since: lastRelease.published_at },
dataPath
)
} else {
log({ app, context, message: `Fetching all commits for branch ${branch}` })
return context.github.paginate(
context.github.repos.listCommits.endpoint.merge(
context.repo({
sha: branch,
per_page: 100
})
)
)
}
}

module.exports.extractPullRequestNumber = commit => {
// There are two types of GitHub pull request merges, normal and squashed.
// Normal ones look like 'Merge pull request #123'
// Squashed ones have multiple lines, first one looks like 'Some changes (#123)'
const match = commit.commit.message
.split('\n')[0]
.match(/\(#(\d+)\)$|^Merge pull request #(\d+)/)
return match && (match[1] || match[2])
}

const findPullRequest = ({ context, number }) => {
return (
context.github.pullRequests
.get(context.repo({ number: number }))
.then(res => res.data)
// We ignore any problems, in case the PR number pulled out of the commits
// are bonkers
.catch(() => false)
)
}

module.exports.findPullRequests = ({ app, context, commits }) => {
if (commits.length === 0) {
return Promise.resolve([])
data = await paginate(
context.github.graphql,
findCommitsWithAssociatedPullRequestsQuery,
variables,
dataPath
)
}

const pullRequestNumbers = commits
.map(module.exports.extractPullRequestNumber)
.filter(number => number)

log({
app,
context,
message: `Found pull request numbers: ${pullRequestNumbers.join(', ')}`
})

const pullRequestPromises = pullRequestNumbers.map(number =>
findPullRequest({ context, number })
const commits = _.get(data, [...dataPath, 'nodes'])
const pullRequests = _.uniqBy(
_.flatten(commits.map(commit => commit.associatedPullRequests.nodes)),
'number'
)

return (
Promise.all(pullRequestPromises)
// Filter out PR lookups that failed
.then(prs => prs.filter(pr => pr))
.catch(() => [])
)
return { commits, pullRequests }
}
51 changes: 51 additions & 0 deletions lib/pagination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const _ = require('lodash')

/**
* Utility function to paginate a GraphQL function using Relay-style cursor pagination.
*
* @param {Function} queryFn - function used to query the GraphQL API
* @param {string} query - GraphQL query, must include `nodes` and `pageInfo` fields for the field that will be paginated
* @param {Object} variables
* @param {string[]} paginatePath - path to field to paginate
*/
async function paginate(queryFn, query, variables, paginatePath) {
const nodesPath = [...paginatePath, 'nodes']
const pageInfoPath = [...paginatePath, 'pageInfo']
const endCursorPath = [...pageInfoPath, 'endCursor']
const hasNextPagePath = [...pageInfoPath, 'hasNextPage']
const hasNextPage = data => _.get(data, hasNextPagePath)

let data = await queryFn(query, variables)

if (!_.has(data, nodesPath)) {
throw new Error(
"Data doesn't contain `nodes` field. Make sure the `paginatePath` is set to the field you wish to paginate and that the query includes the `nodes` field."
)
}

if (
!_.has(data, pageInfoPath) ||
!_.has(data, endCursorPath) ||
!_.has(data, hasNextPagePath)
) {
throw new Error(
"Data doesn't contain `pageInfo` field with `endCursor` and `hasNextPage` fields. Make sure the `paginatePath` is set to the field you wish to paginate and that the query includes the `pageInfo` field."
)
}

while (hasNextPage(data)) {
const newData = await queryFn(query, {
...variables,
after: _.get(data, [...pageInfoPath, 'endCursor'])
})
const newNodes = _.get(newData, nodesPath)
const newPageInfo = _.get(newData, pageInfoPath)

_.set(data, pageInfoPath, newPageInfo)
_.update(data, nodesPath, d => d.concat(newNodes))
}

return data
}

module.exports = paginate
21 changes: 10 additions & 11 deletions lib/releases.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ const contributorsSentence = ({ commits, pullRequests }) => {
const contributors = new Set()

commits.forEach(commit => {
if (commit.author) {
contributors.add(`@${commit.author.login}`)
if (commit.author.user) {
contributors.add(`@${commit.author.user.login}`)
} else {
contributors.add(commit.commit.author.name)
contributors.add(commit.author.name)
}
})

pullRequests.forEach(pullRequest => {
contributors.add(`@${pullRequest.user.login}`)
contributors.add(`@${pullRequest.author.login}`)
})

const sortedContributors = Array.from(contributors).sort()
Expand Down Expand Up @@ -93,17 +93,16 @@ const getCategoriesConfig = ({ config }) => {
const categorizePullRequests = (pullRequests, categories, excludeLabels) => {
return pullRequests.reduce(
(acc, pullRequest) => {
if (
pullRequest.labels.some(label => excludeLabels.includes(label.name))
) {
const labels = pullRequest.labels.nodes
if (labels.some(label => excludeLabels.includes(label.name))) {
return acc
} else if (
pullRequest.labels.length === 0 ||
!pullRequest.labels.some(label => categories.includes(label.name))
labels.length === 0 ||
!labels.some(label => categories.includes(label.name))
) {
acc[UNCATEGORIZED].push(pullRequest)
} else {
pullRequest.labels.forEach(label => {
labels.forEach(label => {
if (!acc[label.name]) acc[label.name] = []

acc[label.name].push(pullRequest)
Expand Down Expand Up @@ -147,7 +146,7 @@ const generateChangeLog = (mergedPullRequests, config) => {
template(config['change-template'], {
$TITLE: pullRequest.title,
$NUMBER: pullRequest.number,
$AUTHOR: pullRequest.user.login
$AUTHOR: pullRequest.author.login
})
)
.join('\n')
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"dependencies": {
"compare-versions": "3.4.0",
"lodash": "4.17.11",
"probot": "9.2.8",
"probot-config": "1.0.1",
"request": "2.88.0",
Expand Down
29 changes: 0 additions & 29 deletions test/commits.test.js

This file was deleted.