mkdocs-ultralytics-plugin 0.2.1__tar.gz → 0.2.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_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/PKG-INFO +1 -1
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/mkdocs_ultralytics_plugin.egg-info/PKG-INFO +1 -1
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/plugin/__init__.py +1 -1
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/plugin/main.py +21 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/plugin/postprocess.py +30 -7
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/plugin/processor.py +126 -28
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/plugin/utils.py +15 -46
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/LICENSE +0 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/README.md +0 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/mkdocs_ultralytics_plugin.egg-info/SOURCES.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/mkdocs_ultralytics_plugin.egg-info/dependency_links.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/mkdocs_ultralytics_plugin.egg-info/entry_points.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/mkdocs_ultralytics_plugin.egg-info/requires.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/mkdocs_ultralytics_plugin.egg-info/top_level.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/pyproject.toml +0 -0
- {mkdocs_ultralytics_plugin-0.2.1 → mkdocs_ultralytics_plugin-0.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs-ultralytics-plugin
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: An MkDocs plugin that provides Ultralytics Docs customizations at https://docs.ultralytics.com.
|
|
5
5
|
Author-email: Glenn Jocher <hello@ultralytics.com>
|
|
6
6
|
Maintainer-email: Ultralytics <hello@ultralytics.com>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs-ultralytics-plugin
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: An MkDocs plugin that provides Ultralytics Docs customizations at https://docs.ultralytics.com.
|
|
5
5
|
Author-email: Glenn Jocher <hello@ultralytics.com>
|
|
6
6
|
Maintainer-email: Ultralytics <hello@ultralytics.com>
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
5
7
|
from mkdocs.config import config_options
|
|
6
8
|
from mkdocs.plugins import BasePlugin
|
|
7
9
|
|
|
10
|
+
import plugin.processor as processor
|
|
8
11
|
from plugin.processor import process_html
|
|
9
12
|
|
|
10
13
|
|
|
@@ -26,6 +29,22 @@ class MetaPlugin(BasePlugin):
|
|
|
26
29
|
("add_copy_llm", config_options.Type(bool, default=True)),
|
|
27
30
|
)
|
|
28
31
|
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.git_repo_url = None
|
|
35
|
+
self.git_data = None
|
|
36
|
+
|
|
37
|
+
def on_config(self, config):
|
|
38
|
+
"""Prepare git metadata once for all pages if authors/JSON-LD are enabled."""
|
|
39
|
+
if not self.config.get("enabled", True):
|
|
40
|
+
return config
|
|
41
|
+
|
|
42
|
+
if self.config.get("add_authors") or self.config.get("add_json_ld"):
|
|
43
|
+
docs_dir = Path(config["docs_dir"])
|
|
44
|
+
md_files = [str(p) for p in docs_dir.rglob("*.md")] if docs_dir.exists() else []
|
|
45
|
+
self.git_repo_url, self.git_data = processor.build_git_map(md_files)
|
|
46
|
+
return config
|
|
47
|
+
|
|
29
48
|
def on_post_page(self, output: str, page, config) -> str:
|
|
30
49
|
"""Enhance HTML output by delegating to shared processor."""
|
|
31
50
|
if not self.config["enabled"]:
|
|
@@ -47,6 +66,8 @@ class MetaPlugin(BasePlugin):
|
|
|
47
66
|
page_url=page_url,
|
|
48
67
|
title=title,
|
|
49
68
|
src_path=page.file.abs_src_path,
|
|
69
|
+
git_data=self.git_data,
|
|
70
|
+
repo_url=self.git_repo_url,
|
|
50
71
|
default_image=self.config["default_image"],
|
|
51
72
|
default_author=self.config["default_author"],
|
|
52
73
|
keywords=keywords,
|
|
@@ -3,8 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
9
|
+
try:
|
|
10
|
+
from ultralytics.utils import TQDM # progress bars
|
|
11
|
+
except ImportError:
|
|
12
|
+
TQDM = None
|
|
13
|
+
|
|
14
|
+
import plugin.processor as processor
|
|
8
15
|
from plugin.processor import process_html
|
|
9
16
|
|
|
10
17
|
|
|
@@ -12,6 +19,8 @@ def process_html_file(
|
|
|
12
19
|
html_path: Path,
|
|
13
20
|
site_dir: Path,
|
|
14
21
|
md_index: dict[str, str],
|
|
22
|
+
git_data: dict[str, dict[str, str | dict]] | None,
|
|
23
|
+
repo_url: str | None,
|
|
15
24
|
site_url: str = "",
|
|
16
25
|
default_image: str | None = None,
|
|
17
26
|
default_author: str | None = None,
|
|
@@ -24,6 +33,7 @@ def process_html_file(
|
|
|
24
33
|
add_css: bool = True,
|
|
25
34
|
add_copy_llm: bool = True,
|
|
26
35
|
verbose: bool = False,
|
|
36
|
+
log: Callable[[str], None] | None = print,
|
|
27
37
|
) -> bool:
|
|
28
38
|
"""Process a single HTML file by delegating to shared processor.
|
|
29
39
|
|
|
@@ -35,8 +45,8 @@ def process_html_file(
|
|
|
35
45
|
try:
|
|
36
46
|
html = html_path.read_text(encoding="utf-8")
|
|
37
47
|
except (UnicodeDecodeError, FileNotFoundError) as e:
|
|
38
|
-
if verbose:
|
|
39
|
-
|
|
48
|
+
if verbose and log:
|
|
49
|
+
log(f"Error reading {html_path}: {e}")
|
|
40
50
|
return False
|
|
41
51
|
|
|
42
52
|
soup = BeautifulSoup(html, "html.parser")
|
|
@@ -65,6 +75,8 @@ def process_html_file(
|
|
|
65
75
|
page_url=page_url,
|
|
66
76
|
title=title,
|
|
67
77
|
src_path=src_path,
|
|
78
|
+
git_data=git_data,
|
|
79
|
+
repo_url=repo_url,
|
|
68
80
|
default_image=default_image,
|
|
69
81
|
default_author=default_author,
|
|
70
82
|
keywords=keywords,
|
|
@@ -81,12 +93,10 @@ def process_html_file(
|
|
|
81
93
|
# Write back
|
|
82
94
|
try:
|
|
83
95
|
html_path.write_text(processed_html, encoding="utf-8")
|
|
84
|
-
if verbose:
|
|
85
|
-
print(f"Processed: {html_path.relative_to(site_dir)}")
|
|
86
96
|
return True
|
|
87
97
|
except (OSError, PermissionError) as e:
|
|
88
|
-
if verbose:
|
|
89
|
-
|
|
98
|
+
if verbose and log:
|
|
99
|
+
log(f"Error writing {html_path}: {e}")
|
|
90
100
|
return False
|
|
91
101
|
|
|
92
102
|
|
|
@@ -129,11 +139,21 @@ def postprocess_site(
|
|
|
129
139
|
print(f"Processing {len(html_files)} HTML files in {site_dir}")
|
|
130
140
|
|
|
131
141
|
processed = 0
|
|
132
|
-
|
|
142
|
+
repo_url = None
|
|
143
|
+
git_data = None
|
|
144
|
+
if (add_authors or add_json_ld) and md_index:
|
|
145
|
+
repo_url, git_data = processor.build_git_map(list(md_index.values()))
|
|
146
|
+
|
|
147
|
+
progress = TQDM(html_files, desc="Postprocessing", unit="file", disable=not verbose) if TQDM else None
|
|
148
|
+
log_fn = (progress.write if verbose and progress else print) if verbose else None
|
|
149
|
+
iterator = progress if progress else html_files
|
|
150
|
+
for html_file in iterator:
|
|
133
151
|
success = process_html_file(
|
|
134
152
|
html_file,
|
|
135
153
|
site_dir,
|
|
136
154
|
md_index,
|
|
155
|
+
git_data,
|
|
156
|
+
repo_url,
|
|
137
157
|
site_url=site_url,
|
|
138
158
|
default_image=default_image,
|
|
139
159
|
default_author=default_author,
|
|
@@ -146,9 +166,12 @@ def postprocess_site(
|
|
|
146
166
|
add_css=add_css,
|
|
147
167
|
add_copy_llm=add_copy_llm,
|
|
148
168
|
verbose=verbose,
|
|
169
|
+
log=log_fn,
|
|
149
170
|
)
|
|
150
171
|
if success:
|
|
151
172
|
processed += 1
|
|
173
|
+
if progress:
|
|
174
|
+
progress.close()
|
|
152
175
|
|
|
153
176
|
print(f"✅ Postprocessing complete: {processed}/{len(html_files)} files processed")
|
|
154
177
|
|
|
@@ -27,37 +27,47 @@ COPY_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d
|
|
|
27
27
|
CHECK_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19L21 7l-1.41-1.41L9 16.17z"></path></svg>'
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def get_git_info(
|
|
31
|
-
|
|
30
|
+
def get_git_info(
|
|
31
|
+
file_path: str,
|
|
32
|
+
add_authors: bool = True,
|
|
33
|
+
default_author: str | None = None,
|
|
34
|
+
git_data: dict[str, dict[str, Any]] | None = None,
|
|
35
|
+
repo_url: str | None = None,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
"""Retrieve git information (dates + optional authors) from precomputed git data."""
|
|
32
38
|
file_path = str(Path(file_path).resolve())
|
|
33
39
|
git_info = {
|
|
34
40
|
"creation_date": DEFAULT_CREATION_DATE,
|
|
35
41
|
"last_modified_date": DEFAULT_MODIFIED_DATE,
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
creation_output = subprocess.check_output(
|
|
41
|
-
["git", "log", "--reverse", "--pretty=format:%ai", file_path]
|
|
42
|
-
).decode()
|
|
43
|
-
creation_date = creation_output.split("\n")[0] if creation_output else ""
|
|
44
|
-
last_modified_date = subprocess.check_output(["git", "log", "-1", "--pretty=format:%ai", file_path]).decode()
|
|
45
|
-
git_info.update(
|
|
46
|
-
{
|
|
47
|
-
"creation_date": creation_date or DEFAULT_CREATION_DATE,
|
|
48
|
-
"last_modified_date": last_modified_date or DEFAULT_MODIFIED_DATE,
|
|
49
|
-
}
|
|
50
|
-
)
|
|
44
|
+
if not git_data or file_path not in git_data:
|
|
45
|
+
return git_info
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
cached = git_data[file_path]
|
|
48
|
+
git_info.update(
|
|
49
|
+
{
|
|
50
|
+
"creation_date": cached.get("creation_date", DEFAULT_CREATION_DATE),
|
|
51
|
+
"last_modified_date": cached.get("last_modified_date", DEFAULT_MODIFIED_DATE),
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if add_authors and cached.get("emails"):
|
|
56
|
+
git_info["authors"] = sorted(
|
|
57
|
+
[
|
|
58
|
+
(
|
|
59
|
+
author,
|
|
60
|
+
info["url"],
|
|
61
|
+
info["changes"],
|
|
62
|
+
info["avatar"],
|
|
63
|
+
)
|
|
64
|
+
for author, info in get_github_usernames_from_file(
|
|
65
|
+
file_path, default_user=default_author, emails=cached["emails"], repo_url=repo_url
|
|
66
|
+
).items()
|
|
67
|
+
],
|
|
68
|
+
key=lambda x: x[2],
|
|
69
|
+
reverse=True,
|
|
70
|
+
)
|
|
61
71
|
|
|
62
72
|
return git_info
|
|
63
73
|
|
|
@@ -104,6 +114,90 @@ def insert_content(soup: BeautifulSoup, content_to_insert) -> None:
|
|
|
104
114
|
md_typeset.append(content_to_insert)
|
|
105
115
|
|
|
106
116
|
|
|
117
|
+
def build_git_map(file_paths: list[str] | list[Path]) -> tuple[str | None, dict[str, dict[str, Any]]]:
|
|
118
|
+
"""Build git metadata for provided files using a single git log pass."""
|
|
119
|
+
git_data: dict[str, dict[str, Any]] = {}
|
|
120
|
+
repo_url: str | None = None
|
|
121
|
+
|
|
122
|
+
if not file_paths:
|
|
123
|
+
return repo_url, git_data
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
repo_root = Path(
|
|
127
|
+
subprocess.check_output(["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL).decode().strip()
|
|
128
|
+
)
|
|
129
|
+
except subprocess.CalledProcessError:
|
|
130
|
+
return repo_url, git_data
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
github_repo_url = subprocess.check_output(
|
|
134
|
+
["git", "-C", str(repo_root), "config", "--get", "remote.origin.url"], stderr=subprocess.DEVNULL
|
|
135
|
+
).decode("utf-8")
|
|
136
|
+
github_repo_url = github_repo_url.strip()
|
|
137
|
+
if github_repo_url.endswith(".git"):
|
|
138
|
+
github_repo_url = github_repo_url[:-4]
|
|
139
|
+
if github_repo_url.startswith("git@"):
|
|
140
|
+
github_repo_url = "https://" + github_repo_url[4:].replace(":", "/")
|
|
141
|
+
repo_url = github_repo_url or None
|
|
142
|
+
except subprocess.CalledProcessError:
|
|
143
|
+
repo_url = None
|
|
144
|
+
|
|
145
|
+
rel_paths = []
|
|
146
|
+
for fp in file_paths:
|
|
147
|
+
path = Path(fp)
|
|
148
|
+
if path.exists():
|
|
149
|
+
try:
|
|
150
|
+
rel_paths.append(path.resolve().relative_to(repo_root))
|
|
151
|
+
except ValueError:
|
|
152
|
+
continue
|
|
153
|
+
if not rel_paths:
|
|
154
|
+
return repo_url, git_data
|
|
155
|
+
|
|
156
|
+
cmd = [
|
|
157
|
+
"git",
|
|
158
|
+
"-C",
|
|
159
|
+
str(repo_root),
|
|
160
|
+
"log",
|
|
161
|
+
"--name-only",
|
|
162
|
+
"--pretty=format:%ad\t%ae",
|
|
163
|
+
"--date=format:%Y-%m-%d %H:%M:%S %z",
|
|
164
|
+
"--",
|
|
165
|
+
*[str(p) for p in rel_paths],
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode().splitlines()
|
|
170
|
+
except subprocess.CalledProcessError:
|
|
171
|
+
return repo_url, git_data
|
|
172
|
+
|
|
173
|
+
current_date = None
|
|
174
|
+
current_email = None
|
|
175
|
+
for line in output:
|
|
176
|
+
if not line.strip():
|
|
177
|
+
continue
|
|
178
|
+
parts = line.split("\t")
|
|
179
|
+
if len(parts) == 2:
|
|
180
|
+
current_date, current_email = parts
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
if current_date and current_email:
|
|
184
|
+
abs_path = (repo_root / line.strip()).resolve()
|
|
185
|
+
key = str(abs_path)
|
|
186
|
+
entry = git_data.setdefault(
|
|
187
|
+
key,
|
|
188
|
+
{
|
|
189
|
+
"creation_date": current_date,
|
|
190
|
+
"last_modified_date": current_date,
|
|
191
|
+
"emails": {},
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
entry.setdefault("last_modified_date", current_date)
|
|
195
|
+
entry["creation_date"] = current_date
|
|
196
|
+
entry["emails"][current_email] = entry["emails"].get(current_email, 0) + 1
|
|
197
|
+
|
|
198
|
+
return repo_url, git_data
|
|
199
|
+
|
|
200
|
+
|
|
107
201
|
def get_css() -> str:
|
|
108
202
|
"""CSS for git info, share buttons, and copy button."""
|
|
109
203
|
return """
|
|
@@ -212,6 +306,8 @@ def process_html(
|
|
|
212
306
|
page_url: str,
|
|
213
307
|
title: str,
|
|
214
308
|
src_path: str | None = None,
|
|
309
|
+
git_data: dict[str, dict[str, Any]] | None = None,
|
|
310
|
+
repo_url: str | None = None,
|
|
215
311
|
default_image: str | None = None,
|
|
216
312
|
default_author: str | None = None,
|
|
217
313
|
keywords: str | None = None,
|
|
@@ -389,15 +485,17 @@ def process_html(
|
|
|
389
485
|
"""
|
|
390
486
|
soup.body.append(script)
|
|
391
487
|
|
|
392
|
-
# Initialize git info with defaults
|
|
488
|
+
# Initialize git info with defaults and only call git when needed (authors or JSON-LD)
|
|
393
489
|
git_info = {
|
|
394
490
|
"creation_date": DEFAULT_CREATION_DATE,
|
|
395
491
|
"last_modified_date": DEFAULT_MODIFIED_DATE,
|
|
396
492
|
}
|
|
493
|
+
needs_git = (add_authors or add_json_ld) and src_path
|
|
397
494
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
495
|
+
if needs_git:
|
|
496
|
+
git_info = get_git_info(
|
|
497
|
+
src_path, add_authors=add_authors, default_author=default_author, git_data=git_data, repo_url=repo_url
|
|
498
|
+
)
|
|
401
499
|
|
|
402
500
|
# Only render git footer if we have real git history (not placeholder defaults)
|
|
403
501
|
has_real_git_data = (
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import contextlib
|
|
6
5
|
import re
|
|
7
|
-
import subprocess
|
|
8
|
-
from collections import Counter
|
|
9
6
|
from datetime import datetime
|
|
10
7
|
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
11
9
|
|
|
12
10
|
import requests
|
|
13
11
|
import yaml # YAML is used for its readability and consistency with MkDocs ecosystem
|
|
@@ -141,8 +139,13 @@ def get_github_username_from_email(
|
|
|
141
139
|
return None, None
|
|
142
140
|
|
|
143
141
|
|
|
144
|
-
def get_github_usernames_from_file(
|
|
145
|
-
|
|
142
|
+
def get_github_usernames_from_file(
|
|
143
|
+
file_path: str,
|
|
144
|
+
default_user: str | None = None,
|
|
145
|
+
emails: dict[str, int] | None = None,
|
|
146
|
+
repo_url: str | None = None,
|
|
147
|
+
) -> dict[str, dict[str, Any]]:
|
|
148
|
+
"""Fetch GitHub usernames associated with a file using provided Git email counts.
|
|
146
149
|
|
|
147
150
|
Args:
|
|
148
151
|
file_path (str): The path to the file for which GitHub usernames are to be retrieved.
|
|
@@ -157,38 +160,13 @@ def get_github_usernames_from_file(file_path: str, default_user: str | None = No
|
|
|
157
160
|
- 'avatar' (str): The URL of the author's GitHub avatar.
|
|
158
161
|
|
|
159
162
|
Examples:
|
|
160
|
-
>>> print(get_github_usernames_from_file('mkdocs.yml'))
|
|
161
|
-
{'username1': {'email': 'user@example.com', 'url': 'https://github.com/username1', 'changes':
|
|
163
|
+
>>> print(get_github_usernames_from_file('mkdocs.yml', emails={'user@example.com': 2}))
|
|
164
|
+
{'username1': {'email': 'user@example.com', 'url': 'https://github.com/username1', 'changes': 2, 'avatar': '...'}}
|
|
162
165
|
"""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.decode("utf-8")
|
|
168
|
-
.split("\n")
|
|
169
|
-
)
|
|
170
|
-
emails = dict(Counter(authors_emails_log))
|
|
171
|
-
except subprocess.CalledProcessError:
|
|
172
|
-
emails = {} # Git not available or file not in git repo
|
|
173
|
-
|
|
174
|
-
# Fetch author emails using 'git blame'
|
|
175
|
-
with contextlib.suppress(Exception):
|
|
176
|
-
authors_emails_blame = (
|
|
177
|
-
subprocess.check_output(
|
|
178
|
-
["git", "blame", "--line-porcelain", Path(file_path).resolve()],
|
|
179
|
-
stderr=subprocess.DEVNULL,
|
|
180
|
-
)
|
|
181
|
-
.decode("utf-8")
|
|
182
|
-
.split("\n")
|
|
183
|
-
)
|
|
184
|
-
authors_emails_blame = [line.split(" ")[1] for line in authors_emails_blame if line.startswith("author-mail")]
|
|
185
|
-
authors_emails_blame = [email.strip("<>") for email in authors_emails_blame]
|
|
186
|
-
emails_blame = dict(Counter(authors_emails_blame))
|
|
187
|
-
|
|
188
|
-
# Merge the two email lists, adding any missing authors from 'git blame' as a 1-commit change
|
|
189
|
-
for email in emails_blame:
|
|
190
|
-
if email not in emails:
|
|
191
|
-
emails[email] = 1 # Only add new authors from 'git blame' with a 1-commit change
|
|
166
|
+
if emails is None:
|
|
167
|
+
emails = {}
|
|
168
|
+
else:
|
|
169
|
+
emails = dict(emails) # shallow copy to avoid mutating caller data
|
|
192
170
|
|
|
193
171
|
# If no git info found but default_user provided, use default_user
|
|
194
172
|
if not emails and default_user:
|
|
@@ -202,16 +180,7 @@ def get_github_usernames_from_file(file_path: str, default_user: str | None = No
|
|
|
202
180
|
else:
|
|
203
181
|
cache = {}
|
|
204
182
|
|
|
205
|
-
|
|
206
|
-
github_repo_url = (
|
|
207
|
-
subprocess.check_output(["git", "config", "--get", "remote.origin.url"]).decode("utf-8").strip()
|
|
208
|
-
)
|
|
209
|
-
if github_repo_url.endswith(".git"):
|
|
210
|
-
github_repo_url = github_repo_url[:-4]
|
|
211
|
-
if github_repo_url.startswith("git@"):
|
|
212
|
-
github_repo_url = "https://" + github_repo_url[4:].replace(":", "/")
|
|
213
|
-
except subprocess.CalledProcessError:
|
|
214
|
-
github_repo_url = "https://github.com/ultralytics/ultralytics" # Fallback URL
|
|
183
|
+
github_repo_url = repo_url or "https://github.com/ultralytics/ultralytics"
|
|
215
184
|
|
|
216
185
|
info = {}
|
|
217
186
|
for email, changes in emails.items():
|
|
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
|