mkdocs-document-dates 3.1.6__tar.gz → 3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mkdocs_document_dates-3.1.6/mkdocs_document_dates.egg-info → mkdocs_document_dates-3.2}/PKG-INFO +4 -5
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/README.md +3 -3
- mkdocs_document_dates-3.2/mkdocs_document_dates/cache_manager.py +126 -0
- mkdocs_document_dates-3.2/mkdocs_document_dates/hooks_installer.py +109 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/plugin.py +28 -95
- mkdocs_document_dates-3.2/mkdocs_document_dates/utils.py +147 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2/mkdocs_document_dates.egg-info}/PKG-INFO +4 -5
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates.egg-info/SOURCES.txt +1 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/setup.py +1 -2
- mkdocs_document_dates-3.1.6/mkdocs_document_dates/cache_manager.py +0 -226
- mkdocs_document_dates-3.1.6/mkdocs_document_dates/hooks_installer.py +0 -114
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/LICENSE +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/MANIFEST.in +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/__init__.py +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/hooks/pre-commit +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/config/user.config.css +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/config/user.config.js +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/core/core.css +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/core/core.js +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/core/timeago-load.js +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/core/timeago.full.min.js +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/core/timeago.min.js +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/ar.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/de.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/en.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/es.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/fr.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/ja.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/ko.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/ru.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/zh.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/languages/zh_TW.json +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/tippy/backdrop.css +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/tippy/light.css +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/tippy/material.css +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/tippy/popper.min.js +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/tippy/scale.css +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/tippy/shift-away.css +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/tippy/tippy.css +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/tippy/tippy.umd.min.js +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates.egg-info/dependency_links.txt +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates.egg-info/entry_points.txt +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates.egg-info/requires.txt +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates.egg-info/top_level.txt +0 -0
- {mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/setup.cfg +0 -0
{mkdocs_document_dates-3.1.6/mkdocs_document_dates.egg-info → mkdocs_document_dates-3.2}/PKG-INFO
RENAMED
@@ -1,13 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mkdocs-document-dates
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.2
|
4
4
|
Summary: An easy-to-use, lightweight MkDocs plugin for displaying the exact creation time, last modification time and author info of markdown documents.
|
5
5
|
Home-page: https://github.com/jaywhj/mkdocs-document-dates
|
6
6
|
Author: Aaron Wang
|
7
7
|
Author-email: aaronwqt@gmail.com
|
8
8
|
License: MIT
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
11
10
|
Classifier: Operating System :: OS Independent
|
12
11
|
Requires-Python: >=3.7
|
13
12
|
Description-Content-Type: text/markdown
|
@@ -118,9 +117,9 @@ email: e-name@gmail.com
|
|
118
117
|
|
119
118
|
## Customization
|
120
119
|
|
121
|
-
The plugin supports deep customization, such as **icon style,
|
120
|
+
The plugin supports deep customization, such as **icon style, theme color, font, animation, dividing line**, etc. Everything is customizable (I've already written the code, you just need to turn on the uncomment switch):
|
122
121
|
|
123
|
-
|
|
122
|
+
| Category: | Location: |
|
124
123
|
| :----------------------: | ---------------------------------------- |
|
125
124
|
| **Style & Theme** | `docs/assets/document_dates/user.config.css` |
|
126
125
|
| **Properties & Functions** | `docs/assets/document_dates/user.config.js` |
|
@@ -129,7 +128,7 @@ The plugin supports deep customization, such as **icon style, font style, theme
|
|
129
128
|
**Tip**: when `type: timeago` is set, timeago.js is enabled to render dynamic time, `timeago.min.js` only contains English and Chinese by default, if you need to load other languages, you can configure it as below (choose one):
|
130
129
|
|
131
130
|
- In `user.config.js`, refer to [the demo commented out](https://github.com/jaywhj/mkdocs-document-dates/blob/main/mkdocs_document_dates/static/config/user.config.js) at the bottom, translate it into your local language
|
132
|
-
- In `mkdocs.yml`,
|
131
|
+
- In `mkdocs.yml`, configure the full version of `timeago.full.min.js` to load all languages at once
|
133
132
|
```yaml
|
134
133
|
extra_javascript:
|
135
134
|
- assets/document_dates/core/timeago.full.min.js
|
@@ -91,9 +91,9 @@ email: e-name@gmail.com
|
|
91
91
|
|
92
92
|
## Customization
|
93
93
|
|
94
|
-
The plugin supports deep customization, such as **icon style,
|
94
|
+
The plugin supports deep customization, such as **icon style, theme color, font, animation, dividing line**, etc. Everything is customizable (I've already written the code, you just need to turn on the uncomment switch):
|
95
95
|
|
96
|
-
|
|
96
|
+
| Category: | Location: |
|
97
97
|
| :----------------------: | ---------------------------------------- |
|
98
98
|
| **Style & Theme** | `docs/assets/document_dates/user.config.css` |
|
99
99
|
| **Properties & Functions** | `docs/assets/document_dates/user.config.js` |
|
@@ -102,7 +102,7 @@ The plugin supports deep customization, such as **icon style, font style, theme
|
|
102
102
|
**Tip**: when `type: timeago` is set, timeago.js is enabled to render dynamic time, `timeago.min.js` only contains English and Chinese by default, if you need to load other languages, you can configure it as below (choose one):
|
103
103
|
|
104
104
|
- In `user.config.js`, refer to [the demo commented out](https://github.com/jaywhj/mkdocs-document-dates/blob/main/mkdocs_document_dates/static/config/user.config.js) at the bottom, translate it into your local language
|
105
|
-
- In `mkdocs.yml`,
|
105
|
+
- In `mkdocs.yml`, configure the full version of `timeago.full.min.js` to load all languages at once
|
106
106
|
```yaml
|
107
107
|
extra_javascript:
|
108
108
|
- assets/document_dates/core/timeago.full.min.js
|
@@ -0,0 +1,126 @@
|
|
1
|
+
import logging
|
2
|
+
import subprocess
|
3
|
+
from pathlib import Path
|
4
|
+
from .utils import read_json_cache, read_jsonl_cache, write_jsonl_cache, get_file_creation_time, get_git_first_commit_time
|
5
|
+
|
6
|
+
logger = logging.getLogger("mkdocs.plugins.document_dates")
|
7
|
+
logger.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
8
|
+
|
9
|
+
def find_mkdocs_projects():
|
10
|
+
projects = []
|
11
|
+
try:
|
12
|
+
git_root = Path(subprocess.check_output(
|
13
|
+
['git', 'rev-parse', '--show-toplevel'],
|
14
|
+
text=True, encoding='utf-8'
|
15
|
+
).strip())
|
16
|
+
|
17
|
+
# 遍历 git_root 及子目录, 寻找 mkdocs.yml 文件
|
18
|
+
for config_file in git_root.rglob('mkdocs.y*ml'):
|
19
|
+
if config_file.name.lower() in ('mkdocs.yml', 'mkdocs.yaml'):
|
20
|
+
projects.append(config_file.parent)
|
21
|
+
|
22
|
+
if not projects:
|
23
|
+
logger.warning("No MkDocs projects found in the repository")
|
24
|
+
except subprocess.CalledProcessError as e:
|
25
|
+
logger.error(f"Failed to find the Git repository root: {e}")
|
26
|
+
except Exception as e:
|
27
|
+
logger.error(f"Unexpected error while searching for MkDocs projects: {e}")
|
28
|
+
|
29
|
+
return projects
|
30
|
+
|
31
|
+
def setup_gitattributes(docs_dir: Path):
|
32
|
+
try:
|
33
|
+
gitattributes_path = docs_dir / '.gitattributes'
|
34
|
+
union_merge_line = ".dates_cache.jsonl merge=union"
|
35
|
+
# custom_merge_line = ".dates_cache.json merge=custom_json_merge"
|
36
|
+
content = gitattributes_path.read_text(encoding='utf-8') if gitattributes_path.exists() else ""
|
37
|
+
if union_merge_line not in content:
|
38
|
+
if content and not content.endswith('\n'):
|
39
|
+
content += '\n'
|
40
|
+
content += f"{union_merge_line}\n"
|
41
|
+
gitattributes_path.write_text(content, encoding='utf-8')
|
42
|
+
subprocess.run(["git", "add", str(gitattributes_path)], check=True)
|
43
|
+
logger.info(f"Updated .gitattributes file: {gitattributes_path}")
|
44
|
+
return True
|
45
|
+
except (IOError, OSError) as e:
|
46
|
+
logger.error(f"Failed to read/write .gitattributes file: {e}")
|
47
|
+
except Exception as e:
|
48
|
+
logger.error(f"Failed to add .gitattributes to git: {e}")
|
49
|
+
return False
|
50
|
+
|
51
|
+
def update_cache():
|
52
|
+
global_updated = False
|
53
|
+
|
54
|
+
for project_dir in find_mkdocs_projects():
|
55
|
+
try:
|
56
|
+
project_updated = False
|
57
|
+
|
58
|
+
docs_dir = project_dir / 'docs'
|
59
|
+
if not docs_dir.exists():
|
60
|
+
logger.error(f"Document directory does not exist: {docs_dir}")
|
61
|
+
continue
|
62
|
+
|
63
|
+
# 设置.gitattributes文件
|
64
|
+
global_updated = setup_gitattributes(docs_dir)
|
65
|
+
|
66
|
+
# 获取docs目录下已跟踪(tracked)的markdown文件
|
67
|
+
cmd = ["git", "ls-files", "*.md"]
|
68
|
+
result = subprocess.run(cmd, cwd=docs_dir, capture_output=True, text=True)
|
69
|
+
tracked_files = result.stdout.splitlines() if result.stdout else []
|
70
|
+
|
71
|
+
if not tracked_files:
|
72
|
+
logger.info(f"No tracked markdown files found in {docs_dir}")
|
73
|
+
continue
|
74
|
+
|
75
|
+
# 读取旧的JSON缓存文件(如果存在)
|
76
|
+
json_cache_file = docs_dir / '.dates_cache.json'
|
77
|
+
json_dates_cache = read_json_cache(json_cache_file)
|
78
|
+
|
79
|
+
# 读取新的JSONL缓存文件(如果存在)
|
80
|
+
jsonl_cache_file = docs_dir / '.dates_cache.jsonl'
|
81
|
+
jsonl_dates_cache = read_jsonl_cache(jsonl_cache_file)
|
82
|
+
|
83
|
+
# 根据 git已跟踪的文件来更新
|
84
|
+
for rel_path in tracked_files:
|
85
|
+
try:
|
86
|
+
# 如果文件已在JSONL缓存中,跳过
|
87
|
+
if rel_path in jsonl_dates_cache:
|
88
|
+
continue
|
89
|
+
|
90
|
+
full_path = docs_dir / rel_path
|
91
|
+
# 处理新文件或迁移旧JSON缓存
|
92
|
+
if rel_path in json_dates_cache:
|
93
|
+
jsonl_dates_cache[rel_path] = json_dates_cache[rel_path]
|
94
|
+
project_updated = True
|
95
|
+
elif full_path.exists():
|
96
|
+
created_time = get_file_creation_time(full_path)
|
97
|
+
if not jsonl_cache_file.exists() and not json_cache_file.exists():
|
98
|
+
git_time = get_git_first_commit_time(full_path)
|
99
|
+
if git_time:
|
100
|
+
created_time = min(created_time, git_time)
|
101
|
+
jsonl_dates_cache[rel_path] = {
|
102
|
+
"created": created_time.isoformat()
|
103
|
+
}
|
104
|
+
project_updated = True
|
105
|
+
except Exception as e:
|
106
|
+
logger.error(f"Error processing file {rel_path}: {e}")
|
107
|
+
continue
|
108
|
+
|
109
|
+
# 标记删除不再跟踪的文件
|
110
|
+
if len(jsonl_dates_cache) > len(tracked_files):
|
111
|
+
project_updated = True
|
112
|
+
|
113
|
+
# 如果有更新,写入JSONL缓存文件
|
114
|
+
if project_updated or not jsonl_cache_file.exists():
|
115
|
+
global_updated = write_jsonl_cache(jsonl_cache_file, jsonl_dates_cache, tracked_files)
|
116
|
+
except subprocess.CalledProcessError as e:
|
117
|
+
logger.error(f"Failed to execute git command: {e}")
|
118
|
+
continue
|
119
|
+
except Exception as e:
|
120
|
+
logger.error(f"Error processing project directory {project_dir}: {e}")
|
121
|
+
continue
|
122
|
+
return global_updated
|
123
|
+
|
124
|
+
|
125
|
+
if __name__ == "__main__":
|
126
|
+
update_cache()
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import logging
|
4
|
+
import subprocess
|
5
|
+
from pathlib import Path
|
6
|
+
import platform
|
7
|
+
|
8
|
+
logger = logging.getLogger("mkdocs.plugins.document_dates")
|
9
|
+
logger.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
10
|
+
|
11
|
+
def get_config_dir():
|
12
|
+
if platform.system().lower().startswith('win'):
|
13
|
+
return Path(os.getenv('APPDATA', str(Path.home() / 'AppData' / 'Roaming')))
|
14
|
+
else:
|
15
|
+
return Path.home() / '.config'
|
16
|
+
|
17
|
+
def check_python_version(interpreter):
|
18
|
+
try:
|
19
|
+
result = subprocess.run(
|
20
|
+
[interpreter, "-c", "import sys; print(sys.version_info >= (3, 7))"],
|
21
|
+
capture_output=True, text=True)
|
22
|
+
if result.returncode == 0 and result.stdout.strip().lower() == 'true':
|
23
|
+
return True
|
24
|
+
else:
|
25
|
+
logger.warning(f"Low python version, requires python_requires >=3.7")
|
26
|
+
except Exception as e:
|
27
|
+
logger.info(f"Failed to check {interpreter}: {str(e)}")
|
28
|
+
return False
|
29
|
+
|
30
|
+
def detect_python_interpreter():
|
31
|
+
# 检查可能的Python解释器
|
32
|
+
python_interpreters = ['python3', 'python']
|
33
|
+
for interpreter in python_interpreters:
|
34
|
+
if check_python_version(interpreter):
|
35
|
+
return f'#!/usr/bin/env {interpreter}'
|
36
|
+
|
37
|
+
# 如果都失败了,使用当前运行的Python解释器
|
38
|
+
return f'#!{sys.executable}'
|
39
|
+
|
40
|
+
def setup_hooks_directory():
|
41
|
+
config_dir = get_config_dir() / 'mkdocs-document-dates' / 'hooks'
|
42
|
+
try:
|
43
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
44
|
+
os.chmod(config_dir, 0o755)
|
45
|
+
return config_dir
|
46
|
+
except PermissionError:
|
47
|
+
logger.error(f"No permission to create directory: {config_dir}")
|
48
|
+
except Exception as e:
|
49
|
+
logger.error(f"Failed to create directory {config_dir}: {str(e)}")
|
50
|
+
return None
|
51
|
+
|
52
|
+
def install_hook_file(source_dir: Path, target_dir: Path):
|
53
|
+
try:
|
54
|
+
shebang = detect_python_interpreter()
|
55
|
+
for item in source_dir.iterdir():
|
56
|
+
# 跳过隐藏文件和目录
|
57
|
+
if item.name.startswith('.') or not item.is_file():
|
58
|
+
continue
|
59
|
+
# 添加 shebang 行
|
60
|
+
content = item.read_text(encoding='utf-8')
|
61
|
+
if content.startswith('#!'):
|
62
|
+
content = shebang + os.linesep + content[content.find('\n')+1:]
|
63
|
+
else:
|
64
|
+
content = shebang + os.linesep + content
|
65
|
+
|
66
|
+
target_hook_path = target_dir / item.name
|
67
|
+
target_hook_path.write_text(content, encoding='utf-8')
|
68
|
+
os.chmod(target_hook_path, 0o755)
|
69
|
+
|
70
|
+
return True
|
71
|
+
except Exception as e:
|
72
|
+
logger.error(f"Failed to create hook file {target_hook_path}: {str(e)}")
|
73
|
+
return False
|
74
|
+
|
75
|
+
def configure_git_hooks(hooks_dir):
|
76
|
+
try:
|
77
|
+
# 配置自定义合并驱动
|
78
|
+
# script_path = hooks_dir / 'json_merge_driver.py'
|
79
|
+
# subprocess.run(['git', 'config', '--global', 'merge.custom_json_merge.name', 'Custom JSON merge driver'], check=True)
|
80
|
+
# subprocess.run(['git', 'config', '--global', 'merge.custom_json_merge.driver', f'"{sys.executable}" "{script_path}" %O %A %B'], check=True)
|
81
|
+
|
82
|
+
subprocess.run(['git', 'config', '--global', 'core.hooksPath', str(hooks_dir)], check=True)
|
83
|
+
logger.info(f"Git hooks successfully installed in: {hooks_dir}")
|
84
|
+
return True
|
85
|
+
except Exception:
|
86
|
+
logger.warning("Git not detected, using plugin in a no-Git environment")
|
87
|
+
return False
|
88
|
+
|
89
|
+
def install():
|
90
|
+
try:
|
91
|
+
# 创建hooks目录
|
92
|
+
target_dir = setup_hooks_directory()
|
93
|
+
if not target_dir:
|
94
|
+
return False
|
95
|
+
|
96
|
+
# 安装hook文件
|
97
|
+
source_dir = Path(__file__).parent / 'hooks'
|
98
|
+
if not install_hook_file(source_dir, target_dir):
|
99
|
+
return False
|
100
|
+
|
101
|
+
# 配置git hooks路径
|
102
|
+
return configure_git_hooks(target_dir)
|
103
|
+
|
104
|
+
except Exception as e:
|
105
|
+
logger.error(f"Unexpected error during hooks installation: {str(e)}")
|
106
|
+
return False
|
107
|
+
|
108
|
+
if __name__ == '__main__':
|
109
|
+
install()
|
@@ -2,36 +2,15 @@ import os
|
|
2
2
|
import json
|
3
3
|
import shutil
|
4
4
|
import logging
|
5
|
-
import subprocess
|
6
5
|
from datetime import datetime
|
7
6
|
from pathlib import Path
|
8
7
|
from mkdocs.plugins import BasePlugin
|
9
8
|
from mkdocs.config import config_options
|
10
|
-
from .
|
11
|
-
|
12
|
-
|
13
|
-
logging.
|
14
|
-
|
15
|
-
format='%(levelname)s: %(message)s'
|
16
|
-
)
|
17
|
-
|
18
|
-
|
19
|
-
class Author:
|
20
|
-
def __init__(self, name="", email="", **kwargs):
|
21
|
-
self.name = name
|
22
|
-
self.email = email
|
23
|
-
# 扩展属性
|
24
|
-
self.attributes = kwargs
|
25
|
-
|
26
|
-
def __getattr__(self, name):
|
27
|
-
return self.attributes.get(name)
|
28
|
-
|
29
|
-
def to_dict(self):
|
30
|
-
return {
|
31
|
-
'name': self.name,
|
32
|
-
'email': self.email,
|
33
|
-
**self.attributes
|
34
|
-
}
|
9
|
+
from mkdocs.structure.pages import Page
|
10
|
+
from .utils import Author, read_json_cache, read_jsonl_cache, check_git_repo, get_file_creation_time, get_git_first_commit_time, get_git_authors
|
11
|
+
|
12
|
+
logger = logging.getLogger("mkdocs.plugins.document_dates")
|
13
|
+
logger.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
35
14
|
|
36
15
|
|
37
16
|
class DocumentDatesPlugin(BasePlugin):
|
@@ -66,12 +45,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
66
45
|
self.config['locale'] = 'en'
|
67
46
|
|
68
47
|
# 检查是否为 Git 仓库
|
69
|
-
|
70
|
-
check_git = subprocess.run(['git', 'rev-parse', '--is-inside-work-tree'], capture_output=True, text=True)
|
71
|
-
if check_git.returncode == 0:
|
72
|
-
self.is_git_repo = True
|
73
|
-
except Exception as e:
|
74
|
-
logging.info(f"Not a Git repository: {str(e)}")
|
48
|
+
self.is_git_repo = check_git_repo()
|
75
49
|
|
76
50
|
docs_dir_path = Path(config['docs_dir'])
|
77
51
|
|
@@ -158,7 +132,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
158
132
|
|
159
133
|
return config
|
160
134
|
|
161
|
-
def on_page_markdown(self, markdown, page, config, files):
|
135
|
+
def on_page_markdown(self, markdown, page: Page, config, files):
|
162
136
|
# 获取相对路径,src_uri 总是以"/"分隔
|
163
137
|
rel_path = getattr(page.file, 'src_uri', None)
|
164
138
|
if not rel_path:
|
@@ -194,7 +168,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
194
168
|
return self._insert_date_info(markdown, info_html)
|
195
169
|
|
196
170
|
|
197
|
-
def _load_translation(self, docs_dir_path):
|
171
|
+
def _load_translation(self, docs_dir_path: Path):
|
198
172
|
# 内置语言文件目录
|
199
173
|
builtin_dir = Path(__file__).parent / 'static' / 'languages'
|
200
174
|
# 用户自定义语言文件目录
|
@@ -214,16 +188,16 @@ class DocumentDatesPlugin(BasePlugin):
|
|
214
188
|
en_json = builtin_dir / 'en.json'
|
215
189
|
shutil.copy2(en_json, custom_en_json)
|
216
190
|
|
217
|
-
def _load_lang_file(self, lang_dir):
|
191
|
+
def _load_lang_file(self, lang_dir: Path):
|
218
192
|
try:
|
219
193
|
locale_file = lang_dir / f"{self.config['locale']}.json"
|
220
194
|
if locale_file.exists():
|
221
195
|
with locale_file.open('r', encoding='utf-8') as f:
|
222
196
|
self.translation = json.load(f)
|
223
197
|
except json.JSONDecodeError as e:
|
224
|
-
|
198
|
+
logger.error(f"Invalid JSON format in language file {locale_file}: {str(e)}")
|
225
199
|
except Exception as e:
|
226
|
-
|
200
|
+
logger.error(f"Error loading language file {locale_file}: {str(e)}")
|
227
201
|
|
228
202
|
|
229
203
|
def _is_excluded(self, rel_path):
|
@@ -233,7 +207,7 @@ class DocumentDatesPlugin(BasePlugin):
|
|
233
207
|
return True
|
234
208
|
return False
|
235
209
|
|
236
|
-
def _matches_exclude_pattern(self, rel_path, pattern):
|
210
|
+
def _matches_exclude_pattern(self, rel_path: str, pattern: str):
|
237
211
|
if '*' not in pattern:
|
238
212
|
return rel_path == pattern
|
239
213
|
else:
|
@@ -268,19 +242,9 @@ class DocumentDatesPlugin(BasePlugin):
|
|
268
242
|
return fs_time
|
269
243
|
|
270
244
|
def _get_file_modification_time(self, file_path):
|
271
|
-
"""
|
272
245
|
# 从git获取最后修改时间
|
273
|
-
if self.is_git_repo:
|
274
|
-
|
275
|
-
# 获取文件最后修改时间
|
276
|
-
cmd = f'git log -1 --format="%aI" --date=iso -- "{file_path}"'
|
277
|
-
process = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
278
|
-
if process.returncode == 0 and process.stdout.strip():
|
279
|
-
git_time = process.stdout.strip()
|
280
|
-
return datetime.fromisoformat(git_time).replace(tzinfo=None)
|
281
|
-
except Exception as e:
|
282
|
-
logging.warning(f"Failed to get git modification time: {str(e)}")
|
283
|
-
"""
|
246
|
+
# if self.is_git_repo:
|
247
|
+
# return get_git_last_commit_time(file_path)
|
284
248
|
|
285
249
|
# 从文件系统获取最后修改时间
|
286
250
|
stat = os.stat(file_path)
|
@@ -295,9 +259,10 @@ class DocumentDatesPlugin(BasePlugin):
|
|
295
259
|
if authors:
|
296
260
|
return authors
|
297
261
|
# 2. git author
|
298
|
-
|
299
|
-
|
300
|
-
|
262
|
+
if self.is_git_repo:
|
263
|
+
authors = get_git_authors(file_path)
|
264
|
+
if authors:
|
265
|
+
return authors
|
301
266
|
# 3. site_author 或 PC username
|
302
267
|
return [Author(name=config.get('site_author') or Path.home().name)]
|
303
268
|
|
@@ -336,43 +301,18 @@ class DocumentDatesPlugin(BasePlugin):
|
|
336
301
|
name = email.partition('@')[0]
|
337
302
|
return [Author(name=name, email=email)]
|
338
303
|
except Exception as e:
|
339
|
-
|
340
|
-
return None
|
341
|
-
|
342
|
-
def _get_git_authors(self, file_path):
|
343
|
-
if not self.is_git_repo:
|
344
|
-
return None
|
345
|
-
try:
|
346
|
-
# 获取作者信息(为了兼容性,不采用管道命令,在 python 中处理去重)
|
347
|
-
# git_log_cmd = f'git log --format="%an|%ae" -- {file_path} | sort | uniq'
|
348
|
-
# git_log_cmd = f'git log --format="%an|%ae" -- {file_path} | grep -vE "bot|noreply|ci|github-actions|dependabot|renovate" | sort | uniq'
|
349
|
-
git_log_cmd = ['git', 'log', '--format=%an|%ae', '--', file_path]
|
350
|
-
git_log_result = subprocess.run(git_log_cmd, capture_output=True, text=True, check=False)
|
351
|
-
if git_log_result.returncode != 0 or not git_log_result.stdout.strip():
|
352
|
-
return None
|
353
|
-
|
354
|
-
# 使用字典去重和存储作者
|
355
|
-
authors_map = {}
|
356
|
-
for line in git_log_result.stdout.splitlines():
|
357
|
-
if not line.strip() or line in authors_map:
|
358
|
-
continue
|
359
|
-
name, email = line.split('|')
|
360
|
-
authors_map[line] = Author(name=name, email=email)
|
361
|
-
|
362
|
-
return list(authors_map.values()) or None
|
363
|
-
except Exception as e:
|
364
|
-
logging.warning(f"Failed to get git author info: {str(e)}")
|
304
|
+
logger.warning(f"Error processing author meta: {e}")
|
365
305
|
return None
|
366
306
|
|
367
307
|
|
368
|
-
def _get_formatted_date(self, date):
|
308
|
+
def _get_formatted_date(self, date: datetime):
|
369
309
|
if self.config['type'] == 'timeago':
|
370
310
|
return ""
|
371
311
|
elif self.config['type'] == 'datetime':
|
372
312
|
return date.strftime(f"{self.config['date_format']} {self.config['time_format']}")
|
373
313
|
return date.strftime(self.config['date_format'])
|
374
314
|
|
375
|
-
def _generate_html_info(self, created, modified, authors=None):
|
315
|
+
def _generate_html_info(self, created: datetime, modified: datetime, authors=None):
|
376
316
|
html = ""
|
377
317
|
try:
|
378
318
|
locale = 'zh_CN' if self.config['locale'] == 'zh' else self.config['locale']
|
@@ -405,7 +345,6 @@ class DocumentDatesPlugin(BasePlugin):
|
|
405
345
|
# 多个作者的情况
|
406
346
|
authors_info = ', '.join(a.name for a in authors if a.name)
|
407
347
|
authors_tooltip = ', '.join(f'<a href="mailto:{a.email}">{a.name}</a>' if a.email else a.name for a in authors)
|
408
|
-
|
409
348
|
html += (
|
410
349
|
f"<span data-tippy-content='{self.translation.get('authors', 'Authors')}: {authors_tooltip}'>"
|
411
350
|
f"<span class='material-icons' data-icon='doc_authors'></span>"
|
@@ -416,21 +355,15 @@ class DocumentDatesPlugin(BasePlugin):
|
|
416
355
|
html += f"</div></div>"
|
417
356
|
|
418
357
|
except Exception as e:
|
419
|
-
|
358
|
+
logger.warning(f"Error generating HTML info: {e}")
|
420
359
|
return html
|
421
360
|
|
422
361
|
|
423
|
-
def _insert_date_info(self, markdown, date_info):
|
424
|
-
if not markdown.strip():
|
425
|
-
return markdown
|
426
|
-
|
362
|
+
def _insert_date_info(self, markdown: str, date_info: str):
|
427
363
|
if self.config['position'] == 'top':
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
lines.insert(0, date_info)
|
434
|
-
return '\n'.join(lines)
|
435
|
-
return f"{date_info}\n{markdown}"
|
364
|
+
before, _, after = markdown.lstrip().partition('\n')
|
365
|
+
if before.startswith('# '):
|
366
|
+
return f"{before}\n{date_info}\n{after}"
|
367
|
+
else:
|
368
|
+
return f"{date_info}\n{markdown}"
|
436
369
|
return f"{markdown}\n\n{date_info}"
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import os
|
2
|
+
import platform
|
3
|
+
import json
|
4
|
+
import logging
|
5
|
+
import subprocess
|
6
|
+
from pathlib import Path
|
7
|
+
from datetime import datetime
|
8
|
+
|
9
|
+
logger = logging.getLogger("mkdocs.plugins.document_dates")
|
10
|
+
logger.setLevel(logging.WARNING) # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
11
|
+
|
12
|
+
class Author:
|
13
|
+
def __init__(self, name="", email="", **kwargs):
|
14
|
+
self.name = name
|
15
|
+
self.email = email
|
16
|
+
# 扩展属性
|
17
|
+
self.attributes = kwargs
|
18
|
+
|
19
|
+
def __getattr__(self, name):
|
20
|
+
return self.attributes.get(name)
|
21
|
+
|
22
|
+
def to_dict(self):
|
23
|
+
return {
|
24
|
+
'name': self.name,
|
25
|
+
'email': self.email,
|
26
|
+
**self.attributes
|
27
|
+
}
|
28
|
+
|
29
|
+
|
30
|
+
def get_file_creation_time(file_path):
|
31
|
+
try:
|
32
|
+
stat = os.stat(file_path)
|
33
|
+
system = platform.system().lower()
|
34
|
+
if system.startswith('win'): # Windows
|
35
|
+
return datetime.fromtimestamp(stat.st_ctime)
|
36
|
+
elif system == 'darwin': # macOS
|
37
|
+
try:
|
38
|
+
return datetime.fromtimestamp(stat.st_birthtime)
|
39
|
+
except AttributeError:
|
40
|
+
return datetime.fromtimestamp(stat.st_ctime)
|
41
|
+
else: # Linux, 没有创建时间,使用修改时间
|
42
|
+
return datetime.fromtimestamp(stat.st_mtime)
|
43
|
+
except (OSError, ValueError) as e:
|
44
|
+
logger.error(f"Failed to get file creation time for {file_path}: {e}")
|
45
|
+
return datetime.now()
|
46
|
+
|
47
|
+
def check_git_repo():
|
48
|
+
try:
|
49
|
+
check_git = subprocess.run(['git', 'rev-parse', '--is-inside-work-tree'], capture_output=True, text=True)
|
50
|
+
if check_git.returncode == 0:
|
51
|
+
return True
|
52
|
+
except Exception as e:
|
53
|
+
logger.info(f"Not a Git repository: {str(e)}")
|
54
|
+
return False
|
55
|
+
|
56
|
+
def get_git_first_commit_time(file_path):
|
57
|
+
try:
|
58
|
+
# git log --reverse --format="%aI" -- {file_path} | head -n 1
|
59
|
+
cmd_list = ['git', 'log', '--reverse', '--format=%aI', '--', file_path]
|
60
|
+
process = subprocess.run(cmd_list, capture_output=True, text=True)
|
61
|
+
if process.returncode == 0 and process.stdout.strip():
|
62
|
+
first_line = process.stdout.partition('\n')[0].strip()
|
63
|
+
return datetime.fromisoformat(first_line).replace(tzinfo=None)
|
64
|
+
except Exception as e:
|
65
|
+
logger.info(f"Error getting git first commit time for {file_path}: {e}")
|
66
|
+
return None
|
67
|
+
|
68
|
+
def get_git_last_commit_time(file_path):
|
69
|
+
try:
|
70
|
+
cmd_list = ['git', 'log', '-1', '--format=%aI', '--', file_path]
|
71
|
+
process = subprocess.run(cmd_list, capture_output=True, text=True)
|
72
|
+
if process.returncode == 0 and process.stdout.strip():
|
73
|
+
git_time = process.stdout.strip()
|
74
|
+
return datetime.fromisoformat(git_time).replace(tzinfo=None)
|
75
|
+
except Exception as e:
|
76
|
+
logger.info(f"Error getting git last commit time for {file_path}: {e}")
|
77
|
+
return None
|
78
|
+
|
79
|
+
def get_git_authors(file_path):
|
80
|
+
try:
|
81
|
+
# 为了兼容性,不采用管道命令,在 python 中处理去重
|
82
|
+
# git log --format="%an|%ae" -- {file_path} | sort | uniq
|
83
|
+
# git log --format="%an|%ae" -- {file_path} | grep -vE "bot|noreply|ci|github-actions|dependabot|renovate" | sort | uniq
|
84
|
+
cmd_list = ['git', 'log', '--format=%an|%ae', '--', file_path]
|
85
|
+
process = subprocess.run(cmd_list, capture_output=True, text=True)
|
86
|
+
if process.returncode == 0 and process.stdout.strip():
|
87
|
+
# 使用字典去重和存储作者
|
88
|
+
authors_map = {}
|
89
|
+
for line in process.stdout.splitlines():
|
90
|
+
if not line.strip() or line in authors_map:
|
91
|
+
continue
|
92
|
+
name, email = line.split('|')
|
93
|
+
authors_map[line] = Author(name=name, email=email)
|
94
|
+
return list(authors_map.values()) or None
|
95
|
+
except Exception as e:
|
96
|
+
logger.warning(f"Failed to get git author info: {str(e)}")
|
97
|
+
return None
|
98
|
+
|
99
|
+
def read_json_cache(cache_file: Path):
|
100
|
+
dates_cache = {}
|
101
|
+
if cache_file.exists():
|
102
|
+
try:
|
103
|
+
with open(cache_file, "r", encoding='utf-8') as f:
|
104
|
+
dates_cache = json.load(f)
|
105
|
+
except (IOError, json.JSONDecodeError) as e:
|
106
|
+
logger.warning(f"Error reading from '.dates_cache.json': {str(e)}")
|
107
|
+
return dates_cache
|
108
|
+
|
109
|
+
def read_jsonl_cache(jsonl_file: Path):
|
110
|
+
dates_cache = {}
|
111
|
+
if jsonl_file.exists():
|
112
|
+
try:
|
113
|
+
with open(jsonl_file, "r", encoding='utf-8') as f:
|
114
|
+
for line in f:
|
115
|
+
try:
|
116
|
+
entry = json.loads(line.strip())
|
117
|
+
if entry and isinstance(entry, dict) and len(entry) == 1:
|
118
|
+
file_path, file_info = next(iter(entry.items()))
|
119
|
+
dates_cache[file_path] = file_info
|
120
|
+
except (json.JSONDecodeError, StopIteration) as e:
|
121
|
+
logger.warning(f"Skipping invalid JSONL line: {e}")
|
122
|
+
except IOError as e:
|
123
|
+
logger.warning(f"Error reading from '.dates_cache.jsonl': {str(e)}")
|
124
|
+
return dates_cache
|
125
|
+
|
126
|
+
def write_jsonl_cache(jsonl_file: Path, dates_cache, tracked_files):
|
127
|
+
try:
|
128
|
+
# 使用临时文件写入,然后替换原文件,避免写入过程中的问题
|
129
|
+
temp_file = jsonl_file.with_suffix('.jsonl.tmp')
|
130
|
+
with open(temp_file, "w", encoding='utf-8') as f:
|
131
|
+
for file_path in tracked_files:
|
132
|
+
if file_path in dates_cache:
|
133
|
+
entry = {file_path: dates_cache[file_path]}
|
134
|
+
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
135
|
+
|
136
|
+
# 替换原文件
|
137
|
+
temp_file.replace(jsonl_file)
|
138
|
+
|
139
|
+
# 将文件添加到git
|
140
|
+
subprocess.run(["git", "add", str(jsonl_file)], check=True)
|
141
|
+
logger.info(f"Successfully updated JSONL cache file: {jsonl_file}")
|
142
|
+
return True
|
143
|
+
except (IOError, json.JSONDecodeError) as e:
|
144
|
+
logger.error(f"Failed to write JSONL cache file {jsonl_file}: {e}")
|
145
|
+
except Exception as e:
|
146
|
+
logger.error(f"Failed to add JSONL cache file to git: {e}")
|
147
|
+
return False
|
{mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2/mkdocs_document_dates.egg-info}/PKG-INFO
RENAMED
@@ -1,13 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mkdocs-document-dates
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.2
|
4
4
|
Summary: An easy-to-use, lightweight MkDocs plugin for displaying the exact creation time, last modification time and author info of markdown documents.
|
5
5
|
Home-page: https://github.com/jaywhj/mkdocs-document-dates
|
6
6
|
Author: Aaron Wang
|
7
7
|
Author-email: aaronwqt@gmail.com
|
8
8
|
License: MIT
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
11
10
|
Classifier: Operating System :: OS Independent
|
12
11
|
Requires-Python: >=3.7
|
13
12
|
Description-Content-Type: text/markdown
|
@@ -118,9 +117,9 @@ email: e-name@gmail.com
|
|
118
117
|
|
119
118
|
## Customization
|
120
119
|
|
121
|
-
The plugin supports deep customization, such as **icon style,
|
120
|
+
The plugin supports deep customization, such as **icon style, theme color, font, animation, dividing line**, etc. Everything is customizable (I've already written the code, you just need to turn on the uncomment switch):
|
122
121
|
|
123
|
-
|
|
122
|
+
| Category: | Location: |
|
124
123
|
| :----------------------: | ---------------------------------------- |
|
125
124
|
| **Style & Theme** | `docs/assets/document_dates/user.config.css` |
|
126
125
|
| **Properties & Functions** | `docs/assets/document_dates/user.config.js` |
|
@@ -129,7 +128,7 @@ The plugin supports deep customization, such as **icon style, font style, theme
|
|
129
128
|
**Tip**: when `type: timeago` is set, timeago.js is enabled to render dynamic time, `timeago.min.js` only contains English and Chinese by default, if you need to load other languages, you can configure it as below (choose one):
|
130
129
|
|
131
130
|
- In `user.config.js`, refer to [the demo commented out](https://github.com/jaywhj/mkdocs-document-dates/blob/main/mkdocs_document_dates/static/config/user.config.js) at the bottom, translate it into your local language
|
132
|
-
- In `mkdocs.yml`,
|
131
|
+
- In `mkdocs.yml`, configure the full version of `timeago.full.min.js` to load all languages at once
|
133
132
|
```yaml
|
134
133
|
extra_javascript:
|
135
134
|
- assets/document_dates/core/timeago.full.min.js
|
{mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates.egg-info/SOURCES.txt
RENAMED
@@ -6,6 +6,7 @@ mkdocs_document_dates/__init__.py
|
|
6
6
|
mkdocs_document_dates/cache_manager.py
|
7
7
|
mkdocs_document_dates/hooks_installer.py
|
8
8
|
mkdocs_document_dates/plugin.py
|
9
|
+
mkdocs_document_dates/utils.py
|
9
10
|
mkdocs_document_dates.egg-info/PKG-INFO
|
10
11
|
mkdocs_document_dates.egg-info/SOURCES.txt
|
11
12
|
mkdocs_document_dates.egg-info/dependency_links.txt
|
@@ -28,7 +28,7 @@ try:
|
|
28
28
|
except FileNotFoundError:
|
29
29
|
long_description = "An easy-to-use, lightweight MkDocs plugin for displaying the exact creation time, last modification time and author info of markdown documents."
|
30
30
|
|
31
|
-
VERSION = '3.
|
31
|
+
VERSION = '3.2'
|
32
32
|
|
33
33
|
setup(
|
34
34
|
name="mkdocs-document-dates",
|
@@ -46,7 +46,6 @@ setup(
|
|
46
46
|
],
|
47
47
|
classifiers=[
|
48
48
|
"Programming Language :: Python :: 3",
|
49
|
-
"License :: OSI Approved :: MIT License",
|
50
49
|
"Operating System :: OS Independent",
|
51
50
|
],
|
52
51
|
cmdclass={
|
@@ -1,226 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import json
|
3
|
-
import logging
|
4
|
-
import platform
|
5
|
-
import subprocess
|
6
|
-
from datetime import datetime
|
7
|
-
from pathlib import Path
|
8
|
-
|
9
|
-
# 配置日志等级 (INFO WARNING ERROR)
|
10
|
-
logging.basicConfig(
|
11
|
-
level=logging.WARNING,
|
12
|
-
format='%(levelname)s: %(message)s'
|
13
|
-
)
|
14
|
-
|
15
|
-
def find_mkdocs_projects():
|
16
|
-
projects = []
|
17
|
-
try:
|
18
|
-
git_root = Path(subprocess.check_output(
|
19
|
-
['git', 'rev-parse', '--show-toplevel'],
|
20
|
-
text=True, encoding='utf-8'
|
21
|
-
).strip())
|
22
|
-
|
23
|
-
# 遍历 git_root 及子目录, 寻找 mkdocs.yml 文件
|
24
|
-
for config_file in git_root.rglob('mkdocs.y*ml'):
|
25
|
-
if config_file.name.lower() in ('mkdocs.yml', 'mkdocs.yaml'):
|
26
|
-
projects.append(config_file.parent)
|
27
|
-
|
28
|
-
if not projects:
|
29
|
-
logging.warning("No MkDocs projects found in the repository")
|
30
|
-
except subprocess.CalledProcessError as e:
|
31
|
-
logging.error(f"Failed to find the Git repository root: {e}")
|
32
|
-
except Exception as e:
|
33
|
-
logging.error(f"Unexpected error while searching for MkDocs projects: {e}")
|
34
|
-
|
35
|
-
return projects
|
36
|
-
|
37
|
-
def get_file_creation_time(file_path):
|
38
|
-
try:
|
39
|
-
stat = os.stat(file_path)
|
40
|
-
system = platform.system().lower()
|
41
|
-
if system.startswith('win'): # Windows
|
42
|
-
return datetime.fromtimestamp(stat.st_ctime)
|
43
|
-
elif system == 'darwin': # macOS
|
44
|
-
try:
|
45
|
-
return datetime.fromtimestamp(stat.st_birthtime)
|
46
|
-
except AttributeError:
|
47
|
-
return datetime.fromtimestamp(stat.st_ctime)
|
48
|
-
else: # Linux, 没有创建时间,使用修改时间
|
49
|
-
return datetime.fromtimestamp(stat.st_mtime)
|
50
|
-
except (OSError, ValueError) as e:
|
51
|
-
logging.error(f"Failed to get file creation time for {file_path}: {e}")
|
52
|
-
return datetime.now()
|
53
|
-
|
54
|
-
def get_git_first_commit_time(file_path):
|
55
|
-
try:
|
56
|
-
# git log --reverse --format="%aI" --date=iso -- {file_path} | head -n 1
|
57
|
-
result = subprocess.run(['git', 'log', '--reverse', '--format=%aI', '--', file_path], capture_output=True, text=True)
|
58
|
-
if result.returncode == 0:
|
59
|
-
first_line = result.stdout.partition('\n')[0].strip()
|
60
|
-
if first_line:
|
61
|
-
return datetime.fromisoformat(first_line).replace(tzinfo=None)
|
62
|
-
except Exception as e:
|
63
|
-
logging.info(f"Error getting git first commit time for {file_path}: {e}")
|
64
|
-
return None
|
65
|
-
|
66
|
-
def setup_gitattributes(docs_dir):
|
67
|
-
updated = False
|
68
|
-
try:
|
69
|
-
gitattributes_path = docs_dir / '.gitattributes'
|
70
|
-
union_config_line = ".dates_cache.jsonl merge=union"
|
71
|
-
if gitattributes_path.exists():
|
72
|
-
with open(gitattributes_path, 'r', encoding='utf-8') as f:
|
73
|
-
content = f.read()
|
74
|
-
|
75
|
-
if union_config_line not in content:
|
76
|
-
with open(gitattributes_path, 'a', encoding='utf-8') as f:
|
77
|
-
f.write(f"\n{union_config_line}\n")
|
78
|
-
updated = True
|
79
|
-
else:
|
80
|
-
with open(gitattributes_path, 'w', encoding='utf-8') as f:
|
81
|
-
f.write(f"{union_config_line}\n")
|
82
|
-
updated = True
|
83
|
-
|
84
|
-
if updated:
|
85
|
-
subprocess.run(["git", "add", str(gitattributes_path)], check=True)
|
86
|
-
logging.info(f"Updated .gitattributes file: {gitattributes_path}")
|
87
|
-
except (IOError, OSError) as e:
|
88
|
-
logging.error(f"Failed to read/write .gitattributes file: {e}")
|
89
|
-
except Exception as e:
|
90
|
-
logging.error(f"Failed to add .gitattributes to git: {e}")
|
91
|
-
|
92
|
-
return updated
|
93
|
-
|
94
|
-
def read_json_cache(cache_file):
|
95
|
-
dates_cache = {}
|
96
|
-
if cache_file.exists():
|
97
|
-
try:
|
98
|
-
with open(cache_file, "r", encoding='utf-8') as f:
|
99
|
-
dates_cache = json.load(f)
|
100
|
-
except (IOError, json.JSONDecodeError) as e:
|
101
|
-
logging.warning(f"Error reading from '.dates_cache.json': {str(e)}")
|
102
|
-
return dates_cache
|
103
|
-
|
104
|
-
def read_jsonl_cache(jsonl_file):
|
105
|
-
dates_cache = {}
|
106
|
-
if jsonl_file.exists():
|
107
|
-
try:
|
108
|
-
with open(jsonl_file, "r", encoding='utf-8') as f:
|
109
|
-
for line in f:
|
110
|
-
try:
|
111
|
-
entry = json.loads(line.strip())
|
112
|
-
if entry and isinstance(entry, dict) and len(entry) == 1:
|
113
|
-
file_path, file_info = next(iter(entry.items()))
|
114
|
-
dates_cache[file_path] = file_info
|
115
|
-
except (json.JSONDecodeError, StopIteration) as e:
|
116
|
-
logging.warning(f"Skipping invalid JSONL line: {e}")
|
117
|
-
except IOError as e:
|
118
|
-
logging.warning(f"Error reading from '.dates_cache.jsonl': {str(e)}")
|
119
|
-
return dates_cache
|
120
|
-
|
121
|
-
def write_jsonl_cache(jsonl_file, dates_cache, tracked_files):
|
122
|
-
try:
|
123
|
-
# 使用临时文件写入,然后替换原文件,避免写入过程中的问题
|
124
|
-
temp_file = jsonl_file.with_suffix('.jsonl.tmp')
|
125
|
-
with open(temp_file, "w", encoding='utf-8') as f:
|
126
|
-
for file_path in tracked_files:
|
127
|
-
if file_path in dates_cache:
|
128
|
-
entry = {file_path: dates_cache[file_path]}
|
129
|
-
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
130
|
-
|
131
|
-
# 替换原文件
|
132
|
-
temp_file.replace(jsonl_file)
|
133
|
-
|
134
|
-
# 将文件添加到git
|
135
|
-
subprocess.run(["git", "add", str(jsonl_file)], check=True)
|
136
|
-
logging.info(f"Successfully updated JSONL cache file: {jsonl_file}")
|
137
|
-
return True
|
138
|
-
except (IOError, json.JSONDecodeError) as e:
|
139
|
-
logging.error(f"Failed to write JSONL cache file {jsonl_file}: {e}")
|
140
|
-
return False
|
141
|
-
except subprocess.CalledProcessError as e:
|
142
|
-
logging.error(f"Failed to add JSONL cache file to git: {e}")
|
143
|
-
return False
|
144
|
-
|
145
|
-
def update_cache():
|
146
|
-
global_updated = False
|
147
|
-
|
148
|
-
for project_dir in find_mkdocs_projects():
|
149
|
-
try:
|
150
|
-
project_updated = False
|
151
|
-
|
152
|
-
docs_dir = project_dir / 'docs'
|
153
|
-
if not docs_dir.exists():
|
154
|
-
logging.error(f"Document directory does not exist: {docs_dir}")
|
155
|
-
continue
|
156
|
-
|
157
|
-
# 设置.gitattributes文件
|
158
|
-
gitattributes_updated = setup_gitattributes(docs_dir)
|
159
|
-
if gitattributes_updated:
|
160
|
-
global_updated = True
|
161
|
-
|
162
|
-
# 获取docs目录下已跟踪(tracked)的markdown文件
|
163
|
-
cmd = ["git", "ls-files", "*.md"]
|
164
|
-
result = subprocess.run(cmd, cwd=docs_dir, capture_output=True, text=True, check=True)
|
165
|
-
tracked_files = result.stdout.splitlines() if result.stdout else []
|
166
|
-
|
167
|
-
if not tracked_files:
|
168
|
-
logging.info(f"No tracked markdown files found in {docs_dir}")
|
169
|
-
continue
|
170
|
-
|
171
|
-
# 读取旧的JSON缓存文件(如果存在)
|
172
|
-
json_cache_file = docs_dir / '.dates_cache.json'
|
173
|
-
json_dates_cache = read_json_cache(json_cache_file)
|
174
|
-
|
175
|
-
# 读取新的JSONL缓存文件(如果存在)
|
176
|
-
jsonl_cache_file = docs_dir / '.dates_cache.jsonl'
|
177
|
-
jsonl_dates_cache = read_jsonl_cache(jsonl_cache_file)
|
178
|
-
|
179
|
-
# 根据 git已跟踪的文件来更新
|
180
|
-
for rel_path in tracked_files:
|
181
|
-
try:
|
182
|
-
# 如果文件已在JSONL缓存中,跳过
|
183
|
-
if rel_path in jsonl_dates_cache:
|
184
|
-
continue
|
185
|
-
|
186
|
-
full_path = docs_dir / rel_path
|
187
|
-
# 处理新文件或迁移旧JSON缓存
|
188
|
-
if rel_path in json_dates_cache:
|
189
|
-
jsonl_dates_cache[rel_path] = json_dates_cache[rel_path]
|
190
|
-
project_updated = True
|
191
|
-
elif full_path.exists():
|
192
|
-
created_time = get_file_creation_time(full_path)
|
193
|
-
if not jsonl_cache_file.exists() and not json_cache_file.exists():
|
194
|
-
git_time = get_git_first_commit_time(full_path)
|
195
|
-
if git_time:
|
196
|
-
created_time = min(created_time, git_time)
|
197
|
-
jsonl_dates_cache[rel_path] = {
|
198
|
-
"created": created_time.isoformat()
|
199
|
-
}
|
200
|
-
project_updated = True
|
201
|
-
except Exception as e:
|
202
|
-
logging.error(f"Error processing file {rel_path}: {e}")
|
203
|
-
continue
|
204
|
-
|
205
|
-
# 标记删除不再跟踪的文件
|
206
|
-
files_to_remove = set(jsonl_dates_cache.keys()) - set(tracked_files)
|
207
|
-
if files_to_remove:
|
208
|
-
project_updated = True
|
209
|
-
logging.info(f"Removing {len(files_to_remove)} untracked files from cache")
|
210
|
-
|
211
|
-
# 如果有更新,写入JSONL缓存文件
|
212
|
-
if project_updated or not jsonl_cache_file.exists():
|
213
|
-
if write_jsonl_cache(jsonl_cache_file, jsonl_dates_cache, tracked_files):
|
214
|
-
global_updated = True
|
215
|
-
except subprocess.CalledProcessError as e:
|
216
|
-
logging.error(f"Failed to execute git command: {e}")
|
217
|
-
continue
|
218
|
-
except Exception as e:
|
219
|
-
logging.error(f"Error processing project directory {project_dir}: {e}")
|
220
|
-
continue
|
221
|
-
|
222
|
-
return global_updated
|
223
|
-
|
224
|
-
|
225
|
-
if __name__ == "__main__":
|
226
|
-
update_cache()
|
@@ -1,114 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import sys
|
3
|
-
import logging
|
4
|
-
import subprocess
|
5
|
-
from pathlib import Path
|
6
|
-
import platform
|
7
|
-
|
8
|
-
# 配置日志等级 (INFO WARNING ERROR)
|
9
|
-
logging.basicConfig(
|
10
|
-
level=logging.WARNING,
|
11
|
-
format='%(levelname)s: %(message)s'
|
12
|
-
)
|
13
|
-
|
14
|
-
def get_config_dir():
|
15
|
-
if platform.system().lower().startswith('win'):
|
16
|
-
return Path(os.getenv('APPDATA', str(Path.home() / 'AppData' / 'Roaming')))
|
17
|
-
else:
|
18
|
-
return Path.home() / '.config'
|
19
|
-
|
20
|
-
def check_python_version(interpreter):
|
21
|
-
try:
|
22
|
-
result = subprocess.run(
|
23
|
-
[interpreter, "-c", "import sys; print(sys.version_info >= (3, 7))"],
|
24
|
-
capture_output=True, text=True, check=False
|
25
|
-
)
|
26
|
-
if result.returncode == 0 and result.stdout.strip().lower() == 'true':
|
27
|
-
return True
|
28
|
-
else:
|
29
|
-
logging.warning(f"Low python version, requires python_requires >=3.7")
|
30
|
-
return False
|
31
|
-
except Exception as e:
|
32
|
-
logging.debug(f"Failed to check {interpreter}: {str(e)}")
|
33
|
-
return False
|
34
|
-
|
35
|
-
def detect_python_interpreter():
|
36
|
-
# 检查可能的Python解释器
|
37
|
-
python_interpreters = ['python3', 'python']
|
38
|
-
|
39
|
-
for interpreter in python_interpreters:
|
40
|
-
if check_python_version(interpreter):
|
41
|
-
return f'#!/usr/bin/env {interpreter}'
|
42
|
-
|
43
|
-
# 如果都失败了,使用当前运行的Python解释器
|
44
|
-
return f'#!{sys.executable}'
|
45
|
-
|
46
|
-
def setup_hooks_directory():
|
47
|
-
config_dir = get_config_dir() / 'mkdocs-document-dates' / 'hooks'
|
48
|
-
try:
|
49
|
-
config_dir.mkdir(parents=True, exist_ok=True)
|
50
|
-
os.chmod(config_dir, 0o755)
|
51
|
-
return config_dir
|
52
|
-
except PermissionError:
|
53
|
-
logging.error(f"No permission to create directory: {config_dir}")
|
54
|
-
return None
|
55
|
-
except Exception as e:
|
56
|
-
logging.error(f"Failed to create directory {config_dir}: {str(e)}")
|
57
|
-
return None
|
58
|
-
|
59
|
-
def install_hook_file(source_hook, target_dir):
|
60
|
-
target_hook_path = target_dir / source_hook.name
|
61
|
-
try:
|
62
|
-
# 读取并更新hook文件内容
|
63
|
-
with open(source_hook, 'r', encoding='utf-8') as f_in:
|
64
|
-
content = f_in.read()
|
65
|
-
|
66
|
-
# 更新shebang行
|
67
|
-
shebang = detect_python_interpreter()
|
68
|
-
if content.startswith('#!'):
|
69
|
-
content = shebang + os.linesep + content[content.find('\n'):]
|
70
|
-
else:
|
71
|
-
content = shebang + os.linesep + content
|
72
|
-
|
73
|
-
# 写入并设置权限
|
74
|
-
with open(target_hook_path, 'w', encoding='utf-8') as f_out:
|
75
|
-
f_out.write(content)
|
76
|
-
os.chmod(target_hook_path, 0o755)
|
77
|
-
return True
|
78
|
-
except Exception as e:
|
79
|
-
logging.error(f"Failed to create hook file {target_hook_path}: {str(e)}")
|
80
|
-
return False
|
81
|
-
|
82
|
-
def configure_git_hooks(hooks_dir):
|
83
|
-
try:
|
84
|
-
subprocess.run(
|
85
|
-
['git', 'config', '--global', 'core.hooksPath', str(hooks_dir)],
|
86
|
-
check=True, capture_output=True, encoding='utf-8'
|
87
|
-
)
|
88
|
-
logging.info(f"Git hooks successfully installed in: {hooks_dir}")
|
89
|
-
return True
|
90
|
-
except Exception:
|
91
|
-
logging.warning("Git not detected, failed to set git hooks path")
|
92
|
-
return False
|
93
|
-
|
94
|
-
def install():
|
95
|
-
try:
|
96
|
-
# 创建hooks目录
|
97
|
-
hooks_dir = setup_hooks_directory()
|
98
|
-
if not hooks_dir:
|
99
|
-
return False
|
100
|
-
|
101
|
-
# 安装hook文件
|
102
|
-
source_hook = Path(__file__).parent / 'hooks' / 'pre-commit'
|
103
|
-
if not install_hook_file(source_hook, hooks_dir):
|
104
|
-
return False
|
105
|
-
|
106
|
-
# 配置git hooks路径
|
107
|
-
return configure_git_hooks(hooks_dir)
|
108
|
-
|
109
|
-
except Exception as e:
|
110
|
-
logging.error(f"Unexpected error during hooks installation: {str(e)}")
|
111
|
-
return False
|
112
|
-
|
113
|
-
if __name__ == '__main__':
|
114
|
-
install()
|
File without changes
|
File without changes
|
File without changes
|
{mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/hooks/pre-commit
RENAMED
File without changes
|
File without changes
|
File without changes
|
{mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/core/core.css
RENAMED
File without changes
|
{mkdocs_document_dates-3.1.6 → mkdocs_document_dates-3.2}/mkdocs_document_dates/static/core/core.js
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|