aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorflu0r1ne <flu0r1ne@flu0r1ne.net>2023-08-31 16:53:13 -0500
committerflu0r1ne <flu0r1ne@flu0r1ne.net>2023-08-31 16:53:13 -0500
commit92500aca58dc812e4e5eb9d18213b06369d627a6 (patch)
treee666fa713aa7741b3e11685bc5c44b0e1d7f5f1a
downloadautomirror-92500aca58dc812e4e5eb9d18213b06369d627a6.tar.xz
automirror-92500aca58dc812e4e5eb9d18213b06369d627a6.zip
Add script & readmeHEADmain
-rw-r--r--LICENSE7
-rw-r--r--README.md205
-rw-r--r--post-receive190
3 files changed, 402 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e3cd62c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+Copyright © 2023 Flu0r1ne
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7a4e89e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,205 @@
+# AutoMirror
+## Purpose
+
+AutoMirror is a Git post-receive hook designed to automatically mirror a self-hosted Git repository
+to a remote location when a push to the original repository occurs. The utility is especially useful
+for mirroring repositories to standard cloud platforms like [GitHub](https://github.com). AutoMirror
+was designed to facilitate the publication of specific releases or branches to publicly accessible
+repositories to supports standard fork and pull-request workflows for newcomers. However, pull requests
+should be manually applied and pushed to the original remote. AutoMirror is activated after a `git receive`
+operation in the source repository, enabling the mirroring of specific Git references (`refs`) such
+as entire repositories, specific branches, or tags. Additionally, when provided with a GitHub Access
+Token, it can create GitHub repositories and configure remotes automatically.
+
+## Scope and Limitations
+
+- AutoMirror requires that the user executing `git-receive-pack` has push access to the specified remote repository. The utility is compatible with all standard Git protocols (`ssh`, `git`, `https`, `file`, and `ftps`). `ssh` keys or other automatic authentication methods should be configured in advance.
+
+- Installation of a Git `post-receive` hook is necessary for using AutoMirror.
+
+- AutoMirror is limited to creating repositories on GitHub. For other Git providers, repository setup must be done manually. Contributions through pull requests are welcome.
+
+## Installation
+
+### Dependencies
+
+AutoMirror is developed for systems with Python 3.x and the `python3-requests` package. The required dependencies can be installed using the following distro-specific commands:
+
+For Ubuntu/Debian:
+
+```bash
+sudo apt install python3 python3-requests git
+```
+
+For Arch Linux:
+
+```bash
+sudo pacman -S python git python-requests
+```
+
+### Installing a Key File
+
+Automatic authentication is required for ssh. The standard method to accomplish this is to setup SSH key for pushing to the remote repository and to specify the key should be used in the `~/.ssh/config` file:
+
+```bash
+Match host github.com
+ IdentityFile ~/.ssh/github-mirror
+```
+
+### Installing in a Single Repository
+
+To activate AutoMirror for a single repository, copy the `post-receive` script into the repository's hooks directory:
+
+```bash
+cp post-receive /some/repo.git/hooks/post-receive
+chmod +x /some/repo.git/hooks/post-receive
+```
+
+### Global Installation
+
+For a global installation, the following steps are required (which are compatible with newer Git versions):
+
+```bash
+mkdir -p /etc/git/hooks
+git config --global core.hooksPath /etc/git/hooks
+cp post-receive /etc/git/hooks/post-receive
+chmod +x /etc/git/hooks/post-receive
+```
+
+### Gitolite Installation
+
+For Gitolite installations:
+
+```bash
+cp post-receive ~/.gitolite/hooks/common/post-receive
+gitolite setup --hooks
+chmod +x ~/.gitolite/hooks/common/post-receive
+```
+
+Append the following keys to the `GIT_CONFIG_KEYS` variable in `/etc/gitolite3/gitolite.rc`:
+
+```bash
+automirror.refs automirror.remote automirror.gh-create automirror.gh-desc automirror.gh-homepage
+```
+
+Per-repository AutoMirror settings can be configured through `git config` entries in the `gitolite.conf` file:
+
+```bash
+repo example
+ config automirror.remote ssh://git.example.com:/example
+```
+
+## Configuration
+
+### Remote and Refs
+
+A remote must be specified to enable mirroring. When this setting is configured through `automirror.remote`,
+AutoMirror mirrors the repository to the specified remote following a `git receive` operation. Optionally,
+you can specify which `refs` to mirror using `automirror.refs`:
+
+```bash
+git config --local --set automirror.remote <REMOTE_URL>
+git config --local --set automirror.refs <REF_SPEC>
+```
+
+The `<REMOTE_URL>` is the URL of the Git remote repository to which updates will be pushed. The
+`<REF_SPEC>` is a set of refs that will be synchronized. If `automirror.refs` is not set, the remote
+symbolic ref "HEAD" will be mirrored. By default, HEAD is set to `git config --get init.defaultbranch`
+which is typically either `master` or `main`.
+
+For example, `<REF_SPEC>` could be:
+
+```bash
+refs/heads/* # all heads
+refs/tags/* # all tags
+refs/{heads,tags}/* # all heads and tags
+refs/heads/main,refs/heads/release # main and release branches
+refs/tags/*,refs/heads/main # all tags and main branch
+```
+
+### GitHub Integration
+
+GitHub integration is optional but offers automated repository creation via GitHub's API. If you set
+`automirror.gh-create` to true and leave `automirror.remote` unspecified, AutoMirror will automatically
+create a GitHub repository. This feature requires AutoMirror to have access to a GitHub personal access
+token stored in `automirror.gh-token-file`. It's important to secure this file with restricted permissions
+(`chmod 600 <file>`).
+
+```bash
+git config --local --add automirror.gh-create true
+```
+
+After the repository is successfully created, the value of `automirror.remote` is automatically set
+to the repository's `ssh_url`.
+
+#### Configuring the Repository Description and Homepage
+
+You can optionally specify the repository description and homepage. Use the `automirror.gh-desc` and
+`automirror.gh-homepage` settings, respectively. These strings utilize Python's `string.format` method,
+allowing you to use `{repo_name}` as a placeholder in your templates.
+
+```bash
+git config --global --add automirror.gh-desc '{repo_name} Mirror - This is an automatically maintained mirror of https://git.flu0r1ne.net/{repo_name}.'
+git config --global --add automirror.gh-homepage 'https://git.flu0r1ne.net/{repo_name}/'
+```
+
+#### Obtaining and Configuring a Personal Access Token
+
+For this feature to work, a personal access token from GitHub is required. This token must have read
+and write access to the Administration scope, as well as read access to Metadata. You can obtain such
+a token as follows:
+
+1. Navigate to GitHub and go to `Settings -> Developer settings -> Personal access tokens -> Fine-grained tokens`.
+2. Click `Generate new token`.
+3. In the account permissions section, enable read and write access to Administration and read access to Metadata.
+4. Generate and copy the token.
+
+Once obtained, store this token in a file and set its permissions. You can use `cat` to echo the token
+without it being added to your `bash` history. Press `Ctrl-D` to exit:
+
+```bash
+cat </dev/stdin >/path/to/token_file
+chmod 600 /path/to/token_file
+```
+
+Finally, configure AutoMirror to use this token file:
+
+```bash
+git config --global --add automirror.gh-token-file '/path/to/token_file'
+```
+
+This completes the setup for GitHub integration.
+
+GitHub integration is optional. If `automirror.gh-create` is set to true and `automirror.remote`
+is not defined, AutoMirror will create the GitHub repository using the GitHub API. The API requires
+a personal access token stored in `automirror.gh-token-file`, which should have restricted permissions
+(`chmod 600 file`).
+
+For optional GitHub integration:
+
+```bash
+git config --local --add automirror.gh-create true
+```
+
+The description and homepage templates:
+
+```bash
+git config --global --add automirror.gh-desc '{repo_name} Mirror - This is an automatically maintained mirror of https://git.flu0r1ne.net/{repo_name}.'
+git config --global --add automirror.gh-homepage 'https://git.flu0r1ne.net/{repo_name}/'
+```
+
+## Example Usage
+
+To mirror all heads and tags from a local repository to a remote repository:
+
+1. Configure the remote URL:
+ ```bash
+ git config --local --set automirror.remote ssh://git.example.com:/example
+ ```
+
+2. Specify the refs to mirror:
+ ```bash
+ git config --local --set automirror.refs "refs/{heads,tags}/*"
+ ```
+
+For additional queries or issues, consult the project documentation or contact the maintainers.
diff --git a/post-receive b/post-receive
new file mode 100644
index 0000000..5a0ac2c
--- /dev/null
+++ b/post-receive
@@ -0,0 +1,190 @@
+#!/usr/bin/python3
+
+import sys
+import subprocess
+from typing import List, Tuple, Set, Optional, Dict, Any
+import os
+import json
+import requests
+from pathlib import Path
+
+def die(*args: Any, **kwargs: Any) -> None:
+ """
+ Function to print to stderr and then exit with status code 1.
+
+ Parameters:
+ *args (Any): Variable length argument list.
+ **kwargs (Any): Arbitrary keyword arguments.
+ """
+ print(*args, file=sys.stderr, **kwargs)
+ exit(1)
+
+def get_git_config(key: str) -> str:
+ """
+ Retrieve the git configuration value for a given key.
+
+ Parameters:
+ key (str): The git configuration key to retrieve.
+
+ Returns:
+ str: The value of the git configuration key.
+ """
+ try:
+ value = subprocess.check_output(['git', 'config', '--get', key], text=True).strip()
+ return value
+ except subprocess.CalledProcessError:
+ return ''
+
+def read_token_from_file(file_path: str) -> str:
+ """
+ Read a token string from a file.
+
+ Parameters:
+ file_path (str): The path to the file containing the token.
+
+ Returns:
+ str: The token string read from the file.
+ """
+ try:
+ with open(file_path, 'r') as f:
+ return f.read().strip()
+ except FileNotFoundError:
+ die(f"Token file {file_path} not found.")
+
+def create_github_repo(token: str, repo_name: str, description: str, homepage: str, private: bool) -> Optional[str]:
+ """
+ Create a GitHub repository using the provided details.
+
+ Parameters:
+ token (str): The GitHub access token.
+ repo_name (str): The name of the repository to create.
+ description (str): The description of the repository.
+ homepage (str): The homepage URL for the repository.
+ private (bool): Whether the repository should be private.
+
+ Returns:
+ Optional[str]: The SSH URL of the created repository, or None if creation fails.
+ """
+ headers = {
+ 'Accept': 'application/vnd.github+json',
+ 'Authorization': f'Bearer {token}',
+ 'X-GitHub-Api-Version': '2022-11-28'
+ }
+ payload = {
+ 'name': repo_name,
+ 'description': description,
+ 'homepage': homepage,
+ 'private': private,
+ 'is_template': False # Setting this to false as it's a mirror repo
+ }
+ url = 'https://api.github.com/user/repos'
+ response = requests.post(url, headers=headers, json=payload)
+ response_data: Dict[str, Any] = json.loads(response.text)
+
+ if response.status_code != 201:
+ error_message = response_data.get('message', 'Unknown error')
+ die(f"Failed to create GitHub repo: {error_message}")
+
+ return response_data.get('ssh_url')
+
+def configure_github_repo() -> str:
+ """
+ Configure a GitHub repository by retrieving token and other configuration settings from git config.
+
+ Returns:
+ str: The SSH URL of the configured repository.
+ """
+ token_file = get_git_config('automirror.gh-token-file')
+ if not token_file:
+ die("Token file not specified for GitHub integration.")
+
+ token = read_token_from_file(token_file)
+ repo_name = os.environ.get('GL_REPO', '') # Retrieve the repository name from the environment variable
+ if not repo_name:
+ die("GL_REPO environment variable not set.")
+
+ description_template = get_git_config('automirror.gh-desc')
+ homepage_template = get_git_config('automirror.gh-homepage')
+ is_private_str = get_git_config('automirror.private')
+ is_private = is_private_str.lower() == 'true' if is_private_str else False
+
+ description = description_template.format(repo_name=repo_name)
+ homepage = homepage_template.format(repo_name=repo_name)
+
+ ssh_url = create_github_repo(token, repo_name, description, homepage, is_private)
+ if ssh_url:
+ subprocess.run(['git', 'config', '--local', '--add', 'automirror.remote', ssh_url])
+
+ return ssh_url
+
+def resolve_refs(ref_patterns: List[str]) -> Set[str]:
+ """
+ Resolve ref patterns into actual git refs.
+
+ Parameters:
+ ref_patterns (List[str]): The ref patterns to resolve.
+
+ Returns:
+ Set[str]: The set of resolved git refs.
+ """
+ # Use git for-each-ref to expand the ref pattern
+ resolve_refs = subprocess.check_output(
+ ['git', 'for-each-ref', "--format=%(refname)"] + ref_patterns, text=True
+ ).strip().split('\n')
+
+ return set(resolve_refs)
+
+def head_ref() -> str:
+ """
+ Retrieve the symbolic reference for the HEAD in the git repository.
+
+ Returns:
+ str: The HEAD ref in the repository.
+ """
+ ref = subprocess.check_output(['git', 'symbolic-ref', 'HEAD'], text=True)
+ return ref.strip()
+
+def git_push(remote_url: str, ref_spec: List[str]) -> None:
+ """
+ Push to the remote git repository.
+
+ Parameters:
+ remote_url (str): The URL of the remote repository.
+ ref_spec (List[str]): The ref specs to push.
+ """
+ ref_spec_args = [f'+{ref}:{ref}' for ref in ref_spec]
+ subprocess.run(['git', 'push'] + [remote_url] + ref_spec_args, check=True)
+
+def main() -> None:
+ """
+ Main function to execute the git automirror process.
+ """
+ remote_url = get_git_config('automirror.remote')
+ gh_create = get_git_config('automirror.gh-create') == 'true'
+
+ if gh_create and not remote_url:
+ remote_url = configure_github_repo()
+ elif not remote_url:
+ exit(0)
+
+ ref_spec_config = get_git_config('automirror.refs')
+ ref_spec_patterns = ref_spec_config.split(',') if ref_spec_config else [ head_ref() ]
+ resolved_ref_spec = resolve_refs(ref_spec_patterns)
+
+ # Collect updated refs from stdin
+ updated_refs = []
+ for line in sys.stdin:
+ old_sha1, new_sha1, refname = line.strip().split()
+ updated_refs.append(refname)
+
+ # Check if any of the updated refs are in the configured ref_spec
+ refs_to_push = [ref for ref in updated_refs if ref in resolved_ref_spec]
+
+ print('[post-receive] pushing refs to remote:', refs_to_push)
+
+ if refs_to_push:
+ git_push(remote_url, refs_to_push)
+
+if __name__ == '__main__':
+ main()
+