This is a simple webhook handler that processes push events from GitHub and automatically closes pull requests that have been merged, but GitHub didn’t automatically detect them (i.e. commits has been rebased and/or squashed).
When you rebase commits (or just apply plain patches using git am
), git re-applies changes introduced in these commits on the HEAD of the branch you’re rebasing onto.
This means that the resulting commits have different SHA-1 hash and also different parents, so the relation to the original commits is lost.
Well, almost, you can still compare just content (diff) of the commits.
But if you also do some changes when rebasing (e.g. squash some commits, fix typos…), then diffs will not help you much.
Hopefully, there’s one thing that remains the same even when rebasing commits (if it’s done right) – author’s name, email and the date written. [2] It’s very unlikely that one person created two commits in the same repository at exactly the same time, so we can use it to find the original commit(s) and the corresponding pull request.
There may not be one-to-one mapping, e.g. when the committer has squashed some commits. We can say that the pull request will be closed when we find at least one commit that has been merged. This may be a problem, what if the committer applied just some of the changes introduced in the pull request? To minimize this issue, we can also compare set of the filenames that have been changed (i.e. added, modified, or deleted). It’s not bullet proof, but it may be sufficient.
You may ask, how the heck is this useful?
I created it for Alpine Linux, that uses GitHub just as a mirror and pull requests are always merged into the main repository as a set of patches using git am
.
It used to be quite often that someone had merged commits from a pull request, but forgot to close it.
Clone this repository and install dependencies:
git clone git@github.com:jirutka/github-pr-closer.git
cd github-pr-closer
pip install -r requirements.txt
-
Python 3.x
-
typing (needed only for Python older than 3.5)
Configuration is read from the file settings.ini.
You may change location of the settings file using the environment variable CONF_FILE
.
Each section of this file (except DEFAULT
) corresponds to one or more repositories.
The section name represents full name (slug) of a repository or regular expression matching full names of repositories you want to process.
Push events from repositories that do not match any section are ignored.
Each section may contain the following fields.
- branch_regex
-
A regular expression that specifies branches which should be processed. Push events from branches that don’t match this regular expression will be ignored. To match all branches, use
.*
. If you want to match branchesmaster
anddev
, use(master|dev)
. Default ismaster
. - cache_maxsize
-
Maximum number of pull requests' metadata to keep in LRU cache. This field may be declared only in the
DEFAULT
section. Default is 250. - close_comment
-
Text that will be added as a comment to a pull request being closed. It may contain placeholder
{committer}
that will be replaced by GitHub login (prefixed with@
) or name (if the login is not available) of the committer, and{commits}
that will be replaced by a list of hashes of the actually merged commits. This field is required. - github_token
-
Your GitHub token for communication with GitHub API. It must have scope
public_repo
. This field is required. - hook_secret
-
Webhook secret used by GitHub to sign the message. This field is required.
You can also declare default values for all repositories in special section named DEFAULT
.
There are multiple ways how to run it:
Simply execute the app.py
:
python3 app.py
Then you can access the app on http://localhost:8080.
The port and host may be changed using environment variables HTTP_PORT
and HTTP_HOST
.
If you have many micro-apps like this, it’s IMO kinda overkill to run each in a separate uWSGI process, isn’t it? It’s not so well known, but uWSGI allows to “mount” multiple application in a single uWSGI process and with a single socket.
[uwsgi]
plugins = python34
socket = /run/uwsgi/main.sock
chdir = /var/www/scripts
logger = file:/var/log/uwsgi/main.log
processes = 1
threads = 2
# map URI paths to applications
mount = /hooks/github-pr-closer=github-pr-closer/app.py
#mount = /other/foo=foo/app.py
manage-script-name = true
server {
listen 443 ssl;
server_name example.org;
ssl_certificate /etc/ssl/nginx/nginx.crt;
ssl_certificate_key /etc/ssl/nginx/nginx.key;
location /hooks/github-pr-closer {
uwsgi_pass unix:/run/uwsgi/main.sock;
include uwsgi_params;
}
}
This project is licensed under MIT License. For the full text of the license, see the LICENSE file.