Merge pull request #418 from anmol098/feat/gitpython_multiple_file_and_single_commits

Multiple files and single commits
This commit is contained in:
Alexander Sergeev
2023-03-12 21:29:01 +01:00
committed by GitHub
10 changed files with 146 additions and 97 deletions

View File

@@ -1,6 +1,7 @@
INPUT_WAKATIME_API_KEY=YOUR_WAKATIME_API_KEY
INPUT_GH_TOKEN=YOUR_GITHUB_TOKEN_KEY
INPUT_PUSH_BRANCH_NAME=main
INPUT_PULL_BRANCH_NAME=main
INPUT_SECTION_NAME=waka
INPUT_SHOW_TIMEZONE=True
INPUT_SHOW_PROJECTS=True
@@ -20,5 +21,6 @@ INPUT_SHOW_UPDATED_DATE=True
INPUT_UPDATED_DATE_FORMAT=%d/%m/%Y %H:%M:%S
INPUT_COMMIT_BY_ME=True
INPUT_COMMIT_MESSAGE=Updated with Dev Metrics
INPUT_COMMIT_SINGLE=True
INPUT_DEBUG_LOGGING=True
DEBUG_RUN=True

3
.gitignore vendored
View File

@@ -4,6 +4,9 @@
# Generated graph images:
assets/
# Cloned repo path:
repo/
# Library roots:
venv/

View File

@@ -47,6 +47,7 @@ lint: venv
clean:
@ # Clean all build files, including: libraries, package manager configs, docker images and containers
rm -rf venv
rm -rf repo
rm -rf assets
rm -f package*.json
docker rm -f waka-readme-stats 2>/dev/null || true

View File

@@ -17,6 +17,11 @@ inputs:
required: false
default: "waka"
PULL_BRANCH_NAME:
required: false
description: "The branch to get the readme from"
default: ""
PUSH_BRANCH_NAME:
required: false
description: "The branch to update the readme in"
@@ -112,6 +117,11 @@ inputs:
description: "Git commit custom email"
default: ""
COMMIT_SINGLE:
required: false
description: "Erase commit history on each commit"
default: "False"
LOCALE:
required: false
description: "Show stats in your own language"

View File

@@ -1,5 +1,6 @@
# GitHub integration modules:
PyGithub~=1.58
GitPython~=3.1
# Markdown visualization modules:
pytz~=2022.7

View File

@@ -5,10 +5,11 @@ import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
from manager_download import DownloadManager as DM
from manager_file import FileManager as FM
MAX_LANGUAGES = 5 # Number of top languages to add to chart, for each year quarter
GRAPH_PATH = "assets/bar_graph.png" # Chart saving path.
GRAPH_PATH = f"{FM.ASSETS_DIR}/bar_graph.png" # Chart saving path.
async def create_loc_graph(yearly_data: Dict, save_path: str):

View File

@@ -165,7 +165,7 @@ async def get_stats() -> str:
if EM.SHOW_PROFILE_VIEWS:
DBM.i("Adding profile views info...")
data = GHM.REPO.get_views_traffic(per="week")
data = GHM.REMOTE.get_views_traffic(per="week")
stats += f"![Profile Views](http://img.shields.io/badge/{quote(FM.t('Profile Views'))}-{data['count']}-blue)\n\n"
if EM.SHOW_LINES_OF_CODE:
@@ -185,7 +185,7 @@ async def get_stats() -> str:
if EM.SHOW_LOC_CHART:
await create_loc_graph(yearly_data, GRAPH_PATH)
stats += GHM.update_chart(GRAPH_PATH)
stats += f"**{FM.t('Timeline')}**\n\n{GHM.update_chart('Lines of Code', GRAPH_PATH)}"
if EM.SHOW_UPDATED_DATE:
DBM.i("Adding last updated time...")
@@ -207,11 +207,10 @@ async def main():
stats = await get_stats()
if not EM.DEBUG_RUN:
if GHM.update_readme(stats):
DBM.g("Readme updated!")
GHM.update_readme(stats)
GHM.commit_update()
else:
if GHM.set_github_output(stats):
DBM.g("Debug run, readme not updated. Check the latest comment for the generated stats.")
GHM.set_github_output(stats)
await DM.close_remote_resources()

View File

@@ -17,7 +17,8 @@ class EnvironmentManager:
WAKATIME_API_KEY = environ["INPUT_WAKATIME_API_KEY"]
SECTION_NAME = getenv("INPUT_SECTION_NAME", "waka")
BRANCH_NAME = getenv("INPUT_PUSH_BRANCH_NAME", "")
PULL_BRANCH_NAME = getenv("INPUT_PULL_BRANCH_NAME", "")
PUSH_BRANCH_NAME = getenv("INPUT_PUSH_BRANCH_NAME", "")
SHOW_OS = getenv("INPUT_SHOW_OS", "False").lower() in _TRUTHY
SHOW_PROJECTS = getenv("INPUT_SHOW_PROJECTS", "True").lower() in _TRUTHY
@@ -38,6 +39,7 @@ class EnvironmentManager:
COMMIT_MESSAGE = getenv("INPUT_COMMIT_MESSAGE", "Updated with Dev Metrics")
COMMIT_USERNAME = getenv("INPUT_COMMIT_USERNAME", "")
COMMIT_EMAIL = getenv("INPUT_COMMIT_EMAIL", "")
COMMIT_SINGLE = getenv("INPUT_COMMIT_SINGLE", "").lower() in _TRUTHY
LOCALE = getenv("INPUT_LOCALE", "en")
UPDATED_DATE_FORMAT = getenv("INPUT_UPDATED_DATE_FORMAT", "%d/%m/%Y %H:%M:%S")

View File

@@ -20,6 +20,7 @@ class FileManager:
Stores localization in dictionary.
"""
ASSETS_DIR = "assets"
_LOCALIZATION: Dict[str, str] = dict()
@staticmethod
@@ -53,7 +54,7 @@ class FileManager:
:param append: True for appending to file, false for rewriting.
:param assets: True for saving to 'assets' directory, false otherwise.
"""
name = join("assets", name) if assets else name
name = join(FileManager.ASSETS_DIR, name) if assets else name
with open(name, "a" if append else "w", encoding="utf-8") as file:
file.write(content)
@@ -67,7 +68,7 @@ class FileManager:
:param assets: True for saving to 'assets' directory, false otherwise.
:returns: File cache contents if content is None, None otherwise.
"""
name = join("assets", name) if assets else name
name = join(FileManager.ASSETS_DIR, name) if assets else name
if content is None and not isfile(name):
return None

View File

@@ -1,10 +1,13 @@
from base64 import b64decode, b64encode
from os import environ
from base64 import b64encode
from os import environ, makedirs
from os.path import dirname, join
from random import choice
from re import sub
from shutil import copy, rmtree
from string import ascii_letters
from github import Github, AuthenticatedUser, Repository, ContentFile, InputGitAuthor, UnknownObjectException
from git import Repo, Actor
from github import Github, AuthenticatedUser, Repository
from manager_environment import EnvironmentManager as EM
from manager_file import FileManager as FM
@@ -17,14 +20,17 @@ def init_github_manager():
Current user, user readme repo and readme file are downloaded.
"""
GitHubManager.prepare_github_env()
DBM.i(f"Current user: {GitHubManager.USER.login}")
DBM.i(f"Current user: {GitHubManager.USER.login}.")
class GitHubManager:
USER: AuthenticatedUser
REPO: Repository
_README: ContentFile
_README_CONTENTS: str
REPO: Repo
REMOTE: Repository
_REMOTE_NAME: str
_REMOTE_PATH: str
_SINGLE_COMMIT_BRANCH = "latest_branch"
_START_COMMENT = f"<!--START_SECTION:{EM.SECTION_NAME}-->"
_END_COMMENT = f"<!--END_SECTION:{EM.SECTION_NAME}-->"
@@ -36,28 +42,27 @@ class GitHubManager:
Download and store for future use:
- Current GitHub user.
- Named repo of the user [username]/[username].
- README.md file of this repo.
- Parsed contents of the file.
- Clone of the named repo.
"""
github = Github(EM.GH_TOKEN)
clone_path = "repo"
GitHubManager.USER = github.get_user()
GitHubManager.REPO = github.get_repo(f"{GitHubManager.USER.login}/{GitHubManager.USER.login}")
GitHubManager._README = GitHubManager.REPO.get_readme()
GitHubManager._README_CONTENTS = str(b64decode(GitHubManager._README.content), "utf-8")
rmtree(clone_path, ignore_errors=True)
GitHubManager._REMOTE_NAME = f"{GitHubManager.USER.login}/{GitHubManager.USER.login}"
GitHubManager._REPO_PATH = f"https://{EM.GH_TOKEN}@github.com/{GitHubManager._REMOTE_NAME}.git"
GitHubManager.REMOTE = github.get_repo(GitHubManager._REMOTE_NAME)
GitHubManager.REPO = Repo.clone_from(GitHubManager._REPO_PATH, to_path=clone_path)
if EM.COMMIT_SINGLE:
GitHubManager.REPO.git.checkout(GitHubManager.branch(EM.PULL_BRANCH_NAME))
GitHubManager.REPO.git.checkout("--orphan", GitHubManager._SINGLE_COMMIT_BRANCH)
else:
GitHubManager.REPO.git.checkout(GitHubManager.branch(EM.PUSH_BRANCH_NAME))
@staticmethod
def _generate_new_readme(stats: str) -> str:
"""
Generates new README.md file, inserts its contents between start and end tags.
:param stats: contents to insert.
:returns: new README.md string.
"""
readme_stats = f"{GitHubManager._START_COMMENT}\n{stats}\n{GitHubManager._END_COMMENT}"
return sub(GitHubManager._README_REGEX, readme_stats, GitHubManager._README_CONTENTS)
@staticmethod
def _get_author() -> InputGitAuthor:
def _get_author() -> Actor:
"""
Gets GitHub commit author specified by environmental variables.
It is the user himself or a 'readme-bot'.
@@ -65,94 +70,118 @@ class GitHubManager:
:returns: Commit author.
"""
if EM.COMMIT_BY_ME:
return InputGitAuthor(GitHubManager.USER.login or EM.COMMIT_USERNAME, GitHubManager.USER.email or EM.COMMIT_EMAIL)
return Actor(EM.COMMIT_USERNAME or GitHubManager.USER.login, EM.COMMIT_EMAIL or GitHubManager.USER.email)
else:
return InputGitAuthor(EM.COMMIT_USERNAME or "readme-bot", EM.COMMIT_EMAIL or "41898282+github-actions[bot]@users.noreply.github.com")
return Actor(EM.COMMIT_USERNAME or "readme-bot", EM.COMMIT_EMAIL or "41898282+github-actions[bot]@users.noreply.github.com")
@staticmethod
def branch() -> str:
def branch(requested_branch: str) -> str:
"""
Gets name of branch to commit to specified by environmental variables.
It is the default branch (regularly, 'main' or 'master') or a branch specified by user.
Gets requested branch name or the default branch name if requested branch wasn't found.
The default branch name is regularly, 'main' or 'master'.
:param requested_branch: Requested branch name.
:returns: Commit author.
"""
return GitHubManager.REPO.default_branch if EM.BRANCH_NAME == "" else EM.BRANCH_NAME
return GitHubManager.REMOTE.default_branch if requested_branch == "" else requested_branch
@staticmethod
def update_readme(stats: str) -> bool:
def _copy_file_and_add_to_repo(src_path: str):
"""
Copies file to repository folder, creating path if needed and adds file to git.
The copied file relative to repository root path will be equal the source file relative to work directory path.
:param src_path: Source file path.
"""
dst_path = join(GitHubManager.REPO.working_tree_dir, src_path)
makedirs(dirname(src_path), exist_ok=True)
copy(dst_path, src_path)
GitHubManager.REPO.git.add(dst_path)
@staticmethod
def update_readme(stats: str):
"""
Updates readme with given data if necessary.
Uses commit author, commit message and branch name specified by environmental variables.
:param stats: Readme stats to be pushed.
:returns: whether the README.md file was updated or not.
"""
DBM.i("Updating README...")
new_readme = GitHubManager._generate_new_readme(stats)
if new_readme != GitHubManager._README_CONTENTS:
GitHubManager.REPO.update_file(
path=GitHubManager._README.path,
message=EM.COMMIT_MESSAGE,
content=new_readme,
sha=GitHubManager._README.sha,
branch=GitHubManager.branch(),
committer=GitHubManager._get_author(),
)
DBM.g("README updated!")
return True
else:
DBM.w("README update not needed!")
return False
readme_path = join(GitHubManager.REPO.working_tree_dir, GitHubManager.REMOTE.get_readme().path)
with open(readme_path, "r") as readme_file:
readme_contents = readme_file.read()
readme_stats = f"{GitHubManager._START_COMMENT}\n{stats}\n{GitHubManager._END_COMMENT}"
new_readme = sub(GitHubManager._README_REGEX, readme_stats, readme_contents)
with open(readme_path, "w") as readme_file:
readme_file.write(new_readme)
GitHubManager.REPO.git.add(readme_path)
DBM.g("README updated!")
@staticmethod
def set_github_output(stats: str) -> bool:
def update_chart(name: str, path: str) -> str:
"""
Outputs readme data as current action output instead of committing it.
Updates a chart.
Inlines data into readme if in debug mode, commits otherwise.
Uses commit author, commit message and branch name specified by environmental variables.
param stats: Readme stats to be outputted.
:param name: Name of the chart to update.
:param path: Path of the chart to update.
:returns: String to add to README file.
"""
output = str()
DBM.i(f"Updating {name} chart...")
if not EM.DEBUG_RUN:
DBM.i("\tAdding chart to repo...")
GitHubManager._copy_file_and_add_to_repo(path)
chart_path = f"https://raw.githubusercontent.com/{GitHubManager._REMOTE_NAME}/{GitHubManager.branch(EM.PUSH_BRANCH_NAME)}/{path}"
output += f"![{name} chart]({chart_path})\n\n"
else:
DBM.i("\tInlining chart...")
hint = "You can use [this website](https://codebeautify.org/base64-to-image-converter) to view the generated base64 image."
with open(path, "rb") as input_file:
output += f"{hint}\n```\ndata:image/png;base64,{b64encode(input_file.read()).decode('utf-8')}\n```\n\n"
return output
@staticmethod
def commit_update():
"""
Commit update data to repository.
"""
actor = GitHubManager._get_author()
DBM.i("Committing files to repo...")
GitHubManager.REPO.index.commit(EM.COMMIT_MESSAGE, author=actor, committer=actor)
if EM.COMMIT_SINGLE:
DBM.i("Pushing files to repo as a single commit...")
refspec = f"{GitHubManager._SINGLE_COMMIT_BRANCH}:{GitHubManager.branch(EM.PUSH_BRANCH_NAME)}"
headers = GitHubManager.REPO.remotes.origin.push(force=True, refspec=refspec)
else:
DBM.i("Pushing files to repo...")
headers = GitHubManager.REPO.remotes.origin.push()
if len(headers) == 0:
DBM.i(f"Repository push error: {headers}!")
else:
DBM.i("Repository synchronized!")
@staticmethod
def set_github_output(stats: str):
"""
Output readme data as current action output instead of committing it.
:param stats: String representation of stats to output.
"""
DBM.i("Setting README contents as action output...")
if "GITHUB_OUTPUT" not in environ.keys():
DBM.p("Not in GitHub environment, not setting action output!")
return False
return
else:
DBM.i("Outputting readme contents, check the latest comment for the generated stats.")
prefix = "README stats current output:"
eol = "".join(choice(ascii_letters) for _ in range(10))
FM.write_file(environ["GITHUB_OUTPUT"], f"README_CONTENT<<{eol}\n{prefix}\n\n{stats}\n{eol}\n", append=True)
DBM.g("Action output set!")
return True
@staticmethod
def update_chart(chart_path: str) -> str:
"""
Updates lines of code chart.
Inlines data into readme if in debug mode, commits otherwise.
Uses commit author, commit message and branch name specified by environmental variables.
:param chart_path: path to saved lines of code chart.
:returns: string to add to README file.
"""
DBM.i("Updating lines of code chart...")
with open(chart_path, "rb") as input_file:
data = input_file.read()
if not EM.DEBUG_RUN:
DBM.i("Pushing chart to repo...")
try:
contents = GitHubManager.REPO.get_contents(chart_path)
GitHubManager.REPO.update_file(contents.path, "Charts Updated", data, contents.sha, committer=GitHubManager._get_author())
DBM.g("Lines of code chart updated!")
except UnknownObjectException:
GitHubManager.REPO.create_file(chart_path, "Charts Added", data, committer=GitHubManager._get_author())
DBM.g("Lines of code chart created!")
chart_path = f"https://raw.githubusercontent.com/{GitHubManager.USER.login}/{GitHubManager.USER.login}/{GitHubManager.branch()}/{chart_path}"
return f"**{FM.t('Timeline')}**\n\n![Lines of Code chart]({chart_path})\n\n"
else:
DBM.i("Inlining chart...")
hint = "You can use [this website](https://codebeautify.org/base64-to-image-converter) to view the generated base64 image."
return f"{hint}\n```\ndata:image/png;base64,{b64encode(data).decode('utf-8')}\n```\n\n"