Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
temp.txt
.DS_Store
__pycache__
test_patch_data.json
test_patch_data.json

# used for local testing
.env
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

![GitHub tag (with filter)](https://img.shields.io/github/v/tag/hestonhoffman/changed-lines)

This GitHub action returns the file names and modified lines of each file in a pull request. This is useful if you're running a custom linter against your files and you want to compare log lines against modified lines in your PR.
This GitHub action returns the file names and modified lines of each file in a pull request. This action is intended for custom linters, where you want to compare log lines against modified lines in your PR.

The action uses the patch data returned from the [Git API PR endpoint](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests-files) to collect the modified file names and lines. Changes in your PR are compared against the target branch and line numbers in the output are relative to the modified file. If the entire file is new, all lines appear in the output.


> [!NOTE]
> This action only works with pull requests.
Expand Down Expand Up @@ -86,4 +89,35 @@ Use the `api_url` input if you need to use a custom GitHub API URL. This is usef
run: echo ${{ steps.changed_lines.outputs.changed_files }}
```

Thanks to Jacob Tomlinson [for a helpful tutorial](https://jacobtomlinson.dev/posts/2019/creating-github-actions-in-python/).
Thanks to Jacob Tomlinson [for a helpful tutorial](https://jacobtomlinson.dev/posts/2019/creating-github-actions-in-python/).

## Local development

You can use the instructions below to load some environment variables so you can run the script locally. You'll need a GitHub personal access token with repo privileges.

If you want to run the script locally:
1. (Optional) Set up a virtual environment using your preferred method.
1. Install the python modules:
```shell
pip install -r requirements.txt
```
1. Make a copy of `example.env` and change the environment variables to point to a PR you want to test against:

**Note**: This file (`.env`) is ignored by git, but make sure you don't accidentally commit it somehow, as it stores your git token.

```shell
cp example.env .env
```
1. Source the environment variables:
```shell
source .env
```
1. Run the script:
```shell
python main.py
```
1. Check the generated text file for the results. The environment `GITHUB_OUTPUT` variable sets this to `temp.txt` by default.

## Deleted files and lines

Deleted files and lines don't show up in the output. This is intentional because the main use case for the action is to pass changed lines to a linter for GitHub command annotations. The GitHub API doesn't allow annotations on deleted lines.
7 changes: 7 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export INPUT_API_URL=https://api.github.com
export INPUT_TOKEN=your_token_here
export INPUT_BRANCH=some_branch
export INPUT_REPO=owner/repo
export INPUT_PR=99
export GITHUB_OUTPUT=temp.txt
export INPUT_DELIMITER=' '
35 changes: 18 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ def fetch_patch():
'X-GitHub-Api-Version': '2022-11-28',
'Authorization':f'Bearer {TOKEN}'
}
# Fetch the first page
# Fetch the first page with per_page=100
response = git_session.get(
f"{api_url}/repos/{repo}/pulls/{pr}/files", headers=headers
f"{api_url}/repos/{repo}/pulls/{pr}/files?per_page=100", headers=headers
)
files = response.json()
# Follow 'next' links to fetch the remaining pages
while 'next' in response.links:
response = git_session.get(response.links['next']['url'], headers=headers)
files.extend(response.json())

return files

def parse_patch_data(patch_data):
Expand All @@ -47,22 +48,22 @@ def parse_patch_data(patch_data):
# Some really big files don't have a patch key because GitHub
# returns a message in the PR that the file is too large to display
if entry['additions'] != 0 and 'patch' in entry:
patch_array = re.split('\n', entry['patch'])
# clean patch array
patch_array = [i for i in patch_array if i]
patch_array = re.split('\n', entry['patch'])
# clean patch array
patch_array = [i for i in patch_array if i]

for item in patch_array:
# Grabs hunk annotation and strips out added lines
if item.startswith('@@ -'):
if sublist:
line_array.append(sublist)
sublist = [re.sub(r'\s@@(.*)','',item.split('+')[1])]
# We don't need removed lines ('-')
elif not item.startswith('-') and not item == '\\ No newline at end of file':
sublist.append(item)
if sublist:
line_array.append(sublist)
final_dict[entry['filename']] = line_array
for item in patch_array:
# Grabs hunk annotation and strips out added lines
if item.startswith('@@ -'):
if sublist:
line_array.append(sublist)
sublist = [re.sub(r'\s@@(.*)','',item.split('+')[1])]
# We don't need removed lines ('-')
elif not item.startswith('-') and not item == '\\ No newline at end of file':
sublist.append(item)
if sublist:
line_array.append(sublist)
final_dict[entry['filename']] = line_array
return final_dict

def get_lines(line_dict):
Expand Down