From 940112e696d8ad8c06ae020ee609d017914f4323 Mon Sep 17 00:00:00 2001
From: BoYanZh <boyanzh233@gmail.com>
Date: Thu, 17 Oct 2024 22:51:35 -0400
Subject: [PATCH] feat: file lock on git operation

---
 joint_teapot/app.py    | 137 ++++++++++++++++++++++-------------------
 joint_teapot/config.py |   6 +-
 requirements.txt       |   1 +
 3 files changed, 80 insertions(+), 64 deletions(-)

diff --git a/joint_teapot/app.py b/joint_teapot/app.py
index f3fc6a4..3899381 100644
--- a/joint_teapot/app.py
+++ b/joint_teapot/app.py
@@ -3,6 +3,7 @@ from datetime import datetime
 from pathlib import Path
 from typing import List
 
+from filelock import FileLock
 from git import Repo
 from typer import Argument, Option, Typer, echo
 
@@ -249,34 +250,40 @@ def joj3_scoreboard(
     logger.info(f"debug log to file: {settings.log_file_path}")
     if joj3.check_skipped(score_file_path, "skip-scoreboard"):
         return
-    repo_path = tea.pot.git.repo_clean_and_checkout(repo_name, "grading")
-    repo: Repo = tea.pot.git.get_repo(repo_name)
-    if "grading" not in repo.remote().refs:
-        logger.error(
-            '"grading" branch not found in remote, create and push it to origin first.'
+    lock = FileLock(
+        settings.joj3_lock_file_path, timeout=settings.joj3_lock_file_timeout
+    )
+    with lock.acquire():
+        repo_path = tea.pot.git.repo_clean_and_checkout(repo_name, "grading")
+        repo: Repo = tea.pot.git.get_repo(repo_name)
+        if "grading" not in repo.remote().refs:
+            logger.error(
+                '"grading" branch not found in remote, create and push it to origin first.'
+            )
+            return
+        if "grading" not in repo.branches:
+            logger.error('"grading" branch not found in local, create it first.')
+            return
+        repo.git.reset("--hard", "origin/grading")
+        joj3.generate_scoreboard(
+            score_file_path,
+            submitter,
+            os.path.join(repo_path, scoreboard_file_name),
+            exercise_name,
+        )
+        actions_link = (
+            f"https://{settings.gitea_domain_name}{settings.gitea_suffix}/"
+            + f"{settings.gitea_org_name}/{submitter_repo_name}/"
+            + f"actions/runs/{run_number}"
+        )
+        commit_message = (
+            f"joj3: update scoreboard by @{submitter} in "
+            + f"{settings.gitea_org_name}/{submitter_repo_name}@{commit_hash}\n\n"
+            + f"gitea actions link: {actions_link}"
+        )
+        tea.pot.git.add_commit_and_push(
+            repo_name, [scoreboard_file_name], commit_message
         )
-        return
-    if "grading" not in repo.branches:
-        logger.error('"grading" branch not found in local, create it first.')
-        return
-    repo.git.reset("--hard", "origin/grading")
-    joj3.generate_scoreboard(
-        score_file_path,
-        submitter,
-        os.path.join(repo_path, scoreboard_file_name),
-        exercise_name,
-    )
-    actions_link = (
-        f"https://{settings.gitea_domain_name}{settings.gitea_suffix}/"
-        + f"{settings.gitea_org_name}/{submitter_repo_name}/"
-        + f"actions/runs/{run_number}"
-    )
-    commit_message = (
-        f"joj3: update scoreboard by @{submitter} in "
-        + f"{settings.gitea_org_name}/{submitter_repo_name}@{commit_hash}\n\n"
-        + f"gitea actions link: {actions_link}"
-    )
-    tea.pot.git.add_commit_and_push(repo_name, [scoreboard_file_name], commit_message)
 
 
 @app.command(
@@ -318,43 +325,47 @@ def joj3_failed_table(
     logger.info(f"debug log to file: {settings.log_file_path}")
     if joj3.check_skipped(score_file_path, "skip-failed-table"):
         return
-    repo_path = tea.pot.git.repo_clean_and_checkout(repo_name, "grading")
-    repo: Repo = tea.pot.git.get_repo(repo_name)
-    if "grading" not in repo.remote().refs:
-        logger.error(
-            '"grading" branch not found in remote, create and push it to origin first.'
+    lock = FileLock(
+        settings.joj3_lock_file_path, timeout=settings.joj3_lock_file_timeout
+    )
+    with lock.acquire():
+        repo_path = tea.pot.git.repo_clean_and_checkout(repo_name, "grading")
+        repo: Repo = tea.pot.git.get_repo(repo_name)
+        if "grading" not in repo.remote().refs:
+            logger.error(
+                '"grading" branch not found in remote, create and push it to origin first.'
+            )
+            return
+        if "grading" not in repo.branches:
+            logger.error('"grading" branch not found in local, create it first.')
+            return
+        repo.git.reset("--hard", "origin/grading")
+        submitter_repo_link = (
+            f"https://{settings.gitea_domain_name}{settings.gitea_suffix}/"
+            + f"{settings.gitea_org_name}/{submitter_repo_name}"
+        )
+        actions_link = (
+            f"https://{settings.gitea_domain_name}{settings.gitea_suffix}/"
+            + f"{settings.gitea_org_name}/{submitter_repo_name}/"
+            + f"actions/runs/{run_number}"
+        )
+        joj3.generate_failed_table(
+            score_file_path,
+            submitter_repo_name,
+            submitter_repo_link,
+            os.path.join(repo_path, failed_table_file_name),
+            actions_link,
+        )
+        commit_message = (
+            f"joj3: update failed table by @{submitter} in "
+            + f"{settings.gitea_org_name}/{submitter_repo_name}@{commit_hash}\n\n"
+            + f"gitea actions link: {actions_link}"
+        )
+        tea.pot.git.add_commit_and_push(
+            repo_name,
+            [failed_table_file_name],
+            commit_message,
         )
-        return
-    if "grading" not in repo.branches:
-        logger.error('"grading" branch not found in local, create it first.')
-        return
-    repo.git.reset("--hard", "origin/grading")
-    submitter_repo_link = (
-        f"https://{settings.gitea_domain_name}{settings.gitea_suffix}/"
-        + f"{settings.gitea_org_name}/{submitter_repo_name}"
-    )
-    actions_link = (
-        f"https://{settings.gitea_domain_name}{settings.gitea_suffix}/"
-        + f"{settings.gitea_org_name}/{submitter_repo_name}/"
-        + f"actions/runs/{run_number}"
-    )
-    joj3.generate_failed_table(
-        score_file_path,
-        submitter_repo_name,
-        submitter_repo_link,
-        os.path.join(repo_path, failed_table_file_name),
-        actions_link,
-    )
-    commit_message = (
-        f"joj3: update failed table by @{submitter} in "
-        + f"{settings.gitea_org_name}/{submitter_repo_name}@{commit_hash}\n\n"
-        + f"gitea actions link: {actions_link}"
-    )
-    tea.pot.git.add_commit_and_push(
-        repo_name,
-        [failed_table_file_name],
-        commit_message,
-    )
 
 
 @app.command(
diff --git a/joint_teapot/config.py b/joint_teapot/config.py
index 027140e..2c0bbb1 100644
--- a/joint_teapot/config.py
+++ b/joint_teapot/config.py
@@ -35,9 +35,13 @@ class Settings(BaseSettings):
         "charlem",
     ]
 
-    # sid
+    # joj
     joj_sid: str = ""
 
+    # joj3
+    joj3_lock_file_path: str = ".git/teapot.lock"
+    joj3_lock_file_timeout: int = 30
+
     # log file
     log_file_path: str = "joint-teapot.log"
     stderr_log_level: str = "INFO"
diff --git a/requirements.txt b/requirements.txt
index d58f716..53d58bc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
 canvasapi>=2.2.0
 colorama>=0.4.6
+filelock>=3.14.0
 focs_gitea>=1.22.0
 GitPython>=3.1.18
 joj-submitter>=0.0.8