You've already forked wakapi-readme-stats
Merge pull request #418 from anmol098/feat/gitpython_multiple_file_and_single_commits
Multiple files and single commits
This commit is contained in:
@@ -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
3
.gitignore
vendored
@@ -4,6 +4,9 @@
|
||||
# Generated graph images:
|
||||
assets/
|
||||
|
||||
# Cloned repo path:
|
||||
repo/
|
||||
|
||||
# Library roots:
|
||||
venv/
|
||||
|
||||
|
||||
1
Makefile
1
Makefile
@@ -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
|
||||
|
||||
10
action.yml
10
action.yml
@@ -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"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# GitHub integration modules:
|
||||
PyGithub~=1.58
|
||||
GitPython~=3.1
|
||||
|
||||
# Markdown visualization modules:
|
||||
pytz~=2022.7
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")}-{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()
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"\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\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"
|
||||
|
||||
Reference in New Issue
Block a user