-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
391 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
name: Test Copy Commit | ||
|
||
on: | ||
pull_request: | ||
push: | ||
branches: | ||
- master | ||
|
||
concurrency: | ||
group: ${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
copy-commit: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout Copy Commit Action | ||
uses: actions/checkout@v3 | ||
- name: Configure Git | ||
run: | | ||
git config --global user.email "user@host.domain" | ||
git config --global user.name "Test User" | ||
- name: Create Commit for No-Args Test | ||
run: | | ||
echo "no-args-1" > ${GITHUB_SHA}-no-args-1 | ||
echo "no-args-2" > ${GITHUB_SHA}-no-args-2 | ||
git add ${GITHUB_SHA}-no-args-1 ${GITHUB_SHA}-no-args-2 | ||
git commit -m "test no-args" | ||
- name: Test Copy Commit with No Args | ||
uses: ./ | ||
env: | ||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | ||
with: | ||
destination: 'jeffreyc/copy-commit-to-another-repo-test' | ||
- name: Create Commit for Include Test | ||
run: | | ||
echo "include-1" > ${GITHUB_SHA}-include-1 | ||
echo "include-2" > ${GITHUB_SHA}-include-2 | ||
echo "include-3" > ${GITHUB_SHA}-include-3 | ||
git add ${GITHUB_SHA}-include-1 ${GITHUB_SHA}-include-2 ${GITHUB_SHA}-include-3 | ||
git commit -m "test include" | ||
- name: Test Copy Commit with Include | ||
uses: ./ | ||
env: | ||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | ||
with: | ||
destination: 'jeffreyc/copy-commit-to-another-repo-test' | ||
include: "${{ github.sha }}-include-1,${{ github.sha }}-include-2" | ||
- name: Create Commit for Exclude Test | ||
run: | | ||
echo "exclude-1" > ${GITHUB_SHA}-exclude-1 | ||
echo "exclude-2" > ${GITHUB_SHA}-exclude-2 | ||
echo "exclude-3" > ${GITHUB_SHA}-exclude-3 | ||
git add ${GITHUB_SHA}-exclude-1 ${GITHUB_SHA}-exclude-2 ${GITHUB_SHA}-exclude-3 | ||
git commit -m "test exclude" | ||
- name: Test Copy Commit with Exclude | ||
uses: ./ | ||
env: | ||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | ||
with: | ||
destination: 'jeffreyc/copy-commit-to-another-repo-test' | ||
exclude: "${{ github.sha }}-exclude-1,${{ github.sha }}-exclude-3" | ||
- name: Create Commit for Include-and-Exclude Test | ||
run: | | ||
echo "include-and-exclude-1" > ${GITHUB_SHA}-include-and-exclude-1 | ||
echo "include-and-exclude-2" > ${GITHUB_SHA}-include-and-exclude-2 | ||
echo "include-and-exclude-3" > ${GITHUB_SHA}-include-and-exclude-3 | ||
git add ${GITHUB_SHA}-include-and-exclude-1 ${GITHUB_SHA}-include-and-exclude-2 ${GITHUB_SHA}-include-and-exclude-3 | ||
git commit -m "test include and exclude" | ||
- name: Test Copy Commit with Include and Exclude | ||
uses: ./ | ||
env: | ||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | ||
with: | ||
destination: 'jeffreyc/copy-commit-to-another-repo-test' | ||
exclude: "${{ github.sha }}-include-and-exclude-1,${{ github.sha }}-include-and-exclude-3" | ||
include: "${{ github.sha }}-include-and-exclude-1,${{ github.sha }}-include-and-exclude-2" | ||
# - name: Create Commit for Branch Test | ||
# run: | | ||
# echo "branch-1" > ${GITHUB_SHA}-branch-1 | ||
# git add ${GITHUB_SHA}-branch-1 | ||
# git commit -m "test branch" | ||
# - name: Test Copy Commit with Branch | ||
# uses: ./ | ||
# env: | ||
# PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | ||
# with: | ||
# destination: 'jeffreyc/copy-commit-to-another-repo-test' | ||
# branch: "TODO" | ||
- name: Checkout Test Destination | ||
uses: actions/checkout@v3 | ||
with: | ||
fetch-depth: 5 | ||
path: test | ||
repository: 'jeffreyc/copy-commit-to-another-repo-test' | ||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | ||
- name: Verify Commits | ||
run: python test_action.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.idea | ||
__pycache__ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Changelog | ||
|
||
All notable changes to this project will be documented in this file. | ||
|
||
The format is based on [Keep a Changelog](https://keepachangelog.com/), | ||
and this project adheres to [Semantic Versioning](https://semver.org/). | ||
|
||
## [Unreleased] | ||
|
||
## [1.0.0] - 2023-04-11 | ||
|
||
* Initial Release | ||
|
||
[unreleased]: https://github.com/jeffreyc/copy-commit-to-another-repo/compare/v1.0.0...HEAD | ||
[1.0.0]: https://github.com/jeffreyc/copy-commit-to-another-repo/releases/tag/v1.0.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
FROM python:alpine | ||
|
||
RUN apk update && \ | ||
apk upgrade && \ | ||
apk add git | ||
|
||
ADD copy_commit.py /copy_commit.py | ||
|
||
ENTRYPOINT ["python", "/copy_commit.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
Copyright 2023 Jeff Cousens | ||
|
||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
|
||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# copy-commit-to-another-repo | ||
|
||
`copy-commit-to-another-repo` is a GitHub Action that copies commits from the current repository to another repository. | ||
The intent is to enable keeping two isolated repositories in sync; _e.g._, you have `work` and `home` repositories for your dotfiles, and you want to be able to update either, keeping shared files in sync, including commit messages, while not syncing everything. | ||
|
||
## Configuration | ||
|
||
### Environment | ||
|
||
- `PERSONAL_ACCESS_TOKEN` - this needs to be set under `Settings` -> `Secrets and variables` -> `Actions` -> `New repository secret` on the source repository. | ||
Create a token under `Settings` -> `Developer settings` -> `Personal access tokens` -> `Tokens (classic)` or `Fine-grained tokens`. | ||
The token will need `repo` permissions (classic) or `Repository permissions` -> `Contents` permissions (fine-grained). | ||
|
||
### Inputs | ||
|
||
- `destination` - the repository to copy commits to. | ||
- `branch` [optional] - the branch to copy commits to. If unspecified, the default branch for `destination` will be used. | ||
- `include` [optional] - a comma-separated list of [regular expressions](https://docs.python.org/3/howto/regex.html) to match against; e.g., `hello,world?`. | ||
If specified, only changes to files that match a pattern in `include` will be copied. | ||
Both `include` and `exclude` may be specified. | ||
If neither `include` nor `exclude` are specified, all changes will be copied. | ||
- `exclude` [optional] - a comma-separated list of [regular expressions](https://docs.python.org/3/howto/regex.html) to match against; e.g., `hello,world?`. | ||
If specified, all changes will be copied except changes to files that match a pattern in `exclude`. | ||
Both `include` and `exclude` may be specified. | ||
If neither `include` nor `exclude` are specified, all changes will be copied. | ||
|
||
## Usage | ||
|
||
```yaml | ||
name: Copy Commit | ||
|
||
on: push | ||
|
||
jobs: | ||
copy-commit: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v3 | ||
with: | ||
fetch-depth: 2 | ||
- name: Copy Commit | ||
uses: jeffreyc/copy-commit-to-another-repo@v1.0.0 | ||
env: | ||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | ||
with: | ||
include: 'hello,world?' | ||
exclude: '"world,"' | ||
destination: 'jeffreyc/hello-copy' | ||
branch: 'staging' | ||
``` | ||
_n.b._, you must specify `fetch-depth: 2` for the `Checkout` action, else git will be unable to determine what has changed. | ||
|
||
## License | ||
|
||
`copy-commit-to-another-repo` is released under the [BSD 3-Clause License](LICENSE.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
name: 'Copy commits to another repo' | ||
author: 'Jeff Cousens' | ||
description: 'Copies all or part of a commit to another repository' | ||
inputs: | ||
include: | ||
description: 'Comma-separated list of regular expressions to include' | ||
required: false | ||
exclude: | ||
description: 'Comma-separated list of regular expressions to exclude' | ||
required: false | ||
destination: | ||
description: 'Destination repository' | ||
required: true | ||
branch: | ||
description: "Branch to push changes to in the destination repository" | ||
required: false | ||
runs: | ||
using: 'docker' | ||
image: 'Dockerfile' | ||
args: | ||
- ${{ inputs.include }} | ||
- ${{ inputs.exclude }} | ||
- ${{ inputs.destination }} | ||
- ${{ inputs.branch }} | ||
branding: | ||
icon: 'copy' | ||
color: 'white' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
#!/usr/bin/env python | ||
|
||
import csv | ||
import io | ||
import logging | ||
import os | ||
import re | ||
import subprocess | ||
import sys | ||
import tempfile | ||
from typing import Optional, Pattern | ||
|
||
|
||
class CopyCommit: | ||
def __init__(self, log_level: int = logging.DEBUG): | ||
self.logger = self.get_logger(log_level) | ||
self.cwd = os.environ["GITHUB_WORKSPACE"] | ||
|
||
@staticmethod | ||
def get_logger(log_level: int = logging.DEBUG) -> logging.Logger: | ||
logger = logging.getLogger("copy-commit-to-another-repo") | ||
handler = logging.StreamHandler() | ||
logger.addHandler(handler) | ||
logger.setLevel(log_level) | ||
return logger | ||
|
||
@staticmethod | ||
def match(item: str, patterns: list[Pattern[str]]) -> bool: | ||
if [i for i in [p.match(item) for p in patterns] if i is not None]: | ||
return True | ||
return False | ||
|
||
def parse_csv(self, to_parse: str) -> list[str]: | ||
f = io.StringIO(to_parse) | ||
reader = csv.reader(f, delimiter=",") | ||
return (list(reader) or [[]])[0] | ||
|
||
def require(self, var: str, name: str) -> str: | ||
if not os.environ[var]: | ||
self.logger.critical(f"{name} must be specified") | ||
sys.exit(1) | ||
return os.environ[var].strip() | ||
|
||
def run(self, cmd: str, cwd: Optional[str] = None) -> str: | ||
self.logger.info(f"Running `{cmd}` in `{cwd or self.cwd}`") | ||
try: | ||
ret = subprocess.check_output( | ||
cmd, stderr=subprocess.STDOUT, shell=True, cwd=cwd | ||
).decode("ascii") | ||
except subprocess.CalledProcessError as e: | ||
self.logger.critical( | ||
"Exception on process, rc=", e.returncode, "output=", e.output | ||
) | ||
raise | ||
self.logger.info(ret) | ||
return ret | ||
|
||
def main(self) -> None: | ||
token = self.require("PERSONAL_ACCESS_TOKEN", "PERSONAL_ACCESS_TOKEN") | ||
destination = self.require("INPUT_DESTINATION", "destination") | ||
|
||
self.run(f"git config --global --add safe.directory {self.cwd}") | ||
|
||
username = self.run("git log --pretty=format:%an -1") | ||
self.run(f'git config --global user.name "{username}"') | ||
|
||
email = self.run("git log --pretty=format:%ae -1") | ||
self.run(f'git config --global user.email "{email}"') | ||
|
||
excluded = [ | ||
re.compile(pattern) | ||
for pattern in self.parse_csv(os.environ["INPUT_EXCLUDE"]) | ||
if pattern | ||
] | ||
self.logger.debug(f"excluded: {excluded}") | ||
included = [ | ||
re.compile(pattern) | ||
for pattern in self.parse_csv(os.environ["INPUT_INCLUDE"]) | ||
if pattern | ||
] | ||
self.logger.debug(f"included: {included}") | ||
|
||
with tempfile.TemporaryDirectory() as tmpdir: | ||
if os.environ["INPUT_BRANCH"]: | ||
branch = os.environ["INPUT_BRANCH"] | ||
self.run( | ||
f'git clone --single-branch --branch {branch} "https://x-access-token:{token}@github.com/{destination}.git" "{tmpdir}"' | ||
) | ||
else: | ||
self.run( | ||
f'git clone --single-branch "https://x-access-token:{token}@github.com/{destination}.git" "{tmpdir}"' | ||
) | ||
|
||
modified = self.run( | ||
f"git diff-tree --no-commit-id --name-only HEAD -r" | ||
).split() | ||
|
||
self.logger.debug(f"modified: {modified}") | ||
|
||
keep = [] | ||
for item in modified: | ||
if (not included or self.match(item, included)) and ( | ||
not excluded or not self.match(item, excluded) | ||
): | ||
keep.append(item) | ||
|
||
self.logger.debug(f"keep: {keep}") | ||
|
||
if keep: | ||
keep = " ".join(keep) | ||
self.run( | ||
f"git --git-dir={self.cwd}/.git format-patch -k -1 --stdout HEAD -- {keep} | git am -3 -k", | ||
tmpdir, | ||
) | ||
self.run("git log -2", tmpdir) | ||
self.run("git push -u origin", tmpdir) | ||
else: | ||
self.logger.info( | ||
"All files excluded or no files included, nothing to apply." | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
cc = CopyCommit() | ||
cc.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
#!/usr/bin/env python | ||
|
||
import logging | ||
import os | ||
import unittest | ||
|
||
import copy_commit | ||
|
||
|
||
class TestAction(unittest.TestCase): | ||
def setUp(self): | ||
self.cc = copy_commit.CopyCommit(logging.CRITICAL) | ||
self.repo_path = os.path.join(os.environ["GITHUB_WORKSPACE"], "test") | ||
self.sha = os.environ["GITHUB_SHA"] | ||
|
||
def get_changed_files(self, rev): | ||
return self.cc.run( | ||
f"git diff-tree --no-commit-id --name-only {rev} -r", self.repo_path | ||
).split() | ||
|
||
def test_branch(self): | ||
# HEAD..HEAD~1 on <branch> | ||
pass | ||
|
||
def test_exclude(self): | ||
changed = self.get_changed_files("HEAD~1..HEAD~2") | ||
self.assertIn(f"{self.sha}-exclude-2", changed) | ||
|
||
def test_include(self): | ||
changed = self.get_changed_files("HEAD~2..HEAD~3") | ||
self.assertIn(f"{self.sha}-include-1", changed) | ||
self.assertIn(f"{self.sha}-include-2", changed) | ||
|
||
def test_include_and_exclude(self): | ||
changed = self.get_changed_files("HEAD..HEAD~1") | ||
self.assertIn(f"{self.sha}-include-and-exclude-2", changed) | ||
|
||
def test_no_args(self): | ||
changed = self.get_changed_files("HEAD~3..HEAD~4") | ||
self.assertIn(f"{self.sha}-no-args-1", changed) | ||
self.assertIn(f"{self.sha}-no-args-2", changed) | ||
author = self.cc.run("git log --format='%an <%ae>' -1 HEAD~3", self.repo_path).strip() | ||
self.assertEqual("Test User <user@host.domain>", author) | ||
raw_body = self.cc.run("git log --format='%B' -1 HEAD~3", self.repo_path).strip() | ||
self.assertEqual("test no-args", raw_body) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |