bl-odoo 0.2.7__py3-none-any.whl → 0.3.1__py3-none-any.whl
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.
- bl/__main__.py +13 -9
- bl/freezer.py +2 -4
- bl/spec_parser.py +47 -113
- bl/spec_processor.py +224 -212
- bl/types.py +86 -0
- bl/utils.py +6 -4
- {bl_odoo-0.2.7.dist-info → bl_odoo-0.3.1.dist-info}/METADATA +1 -1
- bl_odoo-0.3.1.dist-info/RECORD +13 -0
- bl_odoo-0.2.7.dist-info/RECORD +0 -12
- {bl_odoo-0.2.7.dist-info → bl_odoo-0.3.1.dist-info}/WHEEL +0 -0
- {bl_odoo-0.2.7.dist-info → bl_odoo-0.3.1.dist-info}/entry_points.txt +0 -0
- {bl_odoo-0.2.7.dist-info → bl_odoo-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {bl_odoo-0.2.7.dist-info → bl_odoo-0.3.1.dist-info}/top_level.txt +0 -0
bl/__main__.py
CHANGED
|
@@ -12,15 +12,19 @@ def run():
|
|
|
12
12
|
parser = argparse.ArgumentParser(
|
|
13
13
|
description="Process a project specification.", formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
14
14
|
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
parser.add_argument(
|
|
15
|
+
|
|
16
|
+
parent_parser = argparse.ArgumentParser(add_help=False)
|
|
17
|
+
parent_parser.add_argument(
|
|
19
18
|
"-c", "--config", type=Path, help="Path to the project specification file.", default="spec.yaml"
|
|
20
19
|
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
parent_parser.add_argument("-z", "--frozen", type=Path, help="Path to the frozen specification file.")
|
|
21
|
+
parent_parser.add_argument("-j", "--concurrency", type=int, default=28, help="Number of concurrent tasks.")
|
|
22
|
+
parent_parser.add_argument("-w", "--workdir", type=Path, help="Working directory. Defaults to config directory.")
|
|
23
|
+
|
|
24
|
+
sub = parser.add_subparsers(help="subcommand help", dest="command")
|
|
25
|
+
build = sub.add_parser("build", parents=[parent_parser], help="build help")
|
|
26
|
+
freeze = sub.add_parser("freeze", parents=[parent_parser], help="freeze help")
|
|
27
|
+
|
|
24
28
|
args = parser.parse_args()
|
|
25
29
|
|
|
26
30
|
project_spec = load_spec_file(args.config, args.frozen, args.workdir)
|
|
@@ -28,9 +32,9 @@ def run():
|
|
|
28
32
|
sys.exit(1)
|
|
29
33
|
|
|
30
34
|
try:
|
|
31
|
-
if args.freeze:
|
|
35
|
+
if args.command == "freeze":
|
|
32
36
|
asyncio.run(freeze_project(project_spec, args.freeze, concurrency=args.concurrency))
|
|
33
|
-
|
|
37
|
+
elif args.command == "build":
|
|
34
38
|
asyncio.run(process_project(project_spec, concurrency=args.concurrency))
|
|
35
39
|
except Exception:
|
|
36
40
|
sys.exit(1)
|
bl/freezer.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import yaml
|
|
3
|
-
from operator import countOf
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import TextIO
|
|
6
4
|
|
|
7
5
|
from rich.console import Console
|
|
8
6
|
from rich.live import Live
|
|
9
7
|
from rich.progress import BarColumn, MofNCompleteColumn, Progress, TaskID, TextColumn
|
|
10
|
-
from bl.
|
|
8
|
+
from bl.types import RepoInfo, ProjectSpec
|
|
11
9
|
from bl.utils import get_local_ref, get_module_path, run_git
|
|
12
10
|
|
|
13
11
|
console = Console()
|
|
@@ -18,7 +16,7 @@ async def freeze_spec(
|
|
|
18
16
|
progress: Progress,
|
|
19
17
|
task_id: TaskID,
|
|
20
18
|
module_name: str,
|
|
21
|
-
module_spec:
|
|
19
|
+
module_spec: RepoInfo,
|
|
22
20
|
workdir: Path,
|
|
23
21
|
):
|
|
24
22
|
result = {module_name: {}}
|
bl/spec_parser.py
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import warnings
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Dict, List, Optional
|
|
6
|
+
from typing import Any, Dict, List, Optional, Type
|
|
6
7
|
|
|
7
8
|
import yaml
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
class OriginType(Enum):
|
|
11
|
-
"""Type of origin reference."""
|
|
12
|
-
|
|
13
|
-
BRANCH = "branch"
|
|
14
|
-
PR = "pr"
|
|
15
|
-
REF = "ref"
|
|
10
|
+
from bl.types import RepoInfo, OriginType, ProjectSpec, RefspecInfo
|
|
16
11
|
|
|
17
12
|
|
|
18
13
|
def make_remote_merge_from_src(src: str) -> tuple[dict, list]:
|
|
@@ -52,60 +47,33 @@ def get_origin_type(origin_value: str) -> OriginType:
|
|
|
52
47
|
return OriginType.BRANCH
|
|
53
48
|
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
65
|
-
self.remote = remote
|
|
66
|
-
self.refspec = ref_str
|
|
67
|
-
""" The refspec string (branch name, PR ref, or commit hash). """
|
|
68
|
-
self.type = type
|
|
69
|
-
self.ref_name = ref_name
|
|
70
|
-
|
|
71
|
-
def __repr__(self) -> str:
|
|
72
|
-
return f"RefspecInfo(remote={self.remote!r}, origin={self.refspec!r}, type={self.type.value})"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class ModuleSpec:
|
|
76
|
-
"""Represents the specification for a set of modules."""
|
|
77
|
-
|
|
78
|
-
def __init__(
|
|
79
|
-
self,
|
|
80
|
-
modules: List[str],
|
|
81
|
-
remotes: Optional[Dict[str, str]] = {},
|
|
82
|
-
origins: Optional[List[RefspecInfo]] = [],
|
|
83
|
-
shell_commands: Optional[List[str]] = [],
|
|
84
|
-
patch_globs_to_apply: Optional[List[str]] = None,
|
|
85
|
-
target_folder: Optional[str] = None,
|
|
86
|
-
frozen_modules: Optional[Dict[str, Dict[str, str]]] = None,
|
|
87
|
-
):
|
|
88
|
-
self.modules = modules
|
|
89
|
-
self.remotes = remotes
|
|
90
|
-
self.refspec_info = origins
|
|
91
|
-
self.shell_commands = shell_commands
|
|
92
|
-
self.patch_globs_to_apply = patch_globs_to_apply
|
|
93
|
-
self.frozen_modules = frozen_modules
|
|
94
|
-
self.target_folder = None
|
|
50
|
+
def parse_remote_refspec_from_parts(parts: List[str], frozen_repo: Dict[str, Dict[str, str]]):
|
|
51
|
+
if len(parts) == 2:
|
|
52
|
+
parts.insert(1, "")
|
|
53
|
+
else:
|
|
54
|
+
warnings.warn(
|
|
55
|
+
"Deprecated src format: use <url> <sha> format for the src property",
|
|
56
|
+
DeprecationWarning,
|
|
57
|
+
)
|
|
58
|
+
remote_key, _, ref_spec = parts
|
|
59
|
+
ref_type = get_origin_type(ref_spec)
|
|
95
60
|
|
|
96
|
-
|
|
97
|
-
|
|
61
|
+
ref_name = None
|
|
62
|
+
remote_freezes = frozen_repo.get(remote_key, {})
|
|
98
63
|
|
|
64
|
+
if ref_spec in remote_freezes:
|
|
65
|
+
ref_type = OriginType.REF
|
|
66
|
+
ref_name = ref_spec
|
|
67
|
+
ref_spec = remote_freezes.get(ref_name)
|
|
99
68
|
|
|
100
|
-
|
|
101
|
-
"""Represents the overall project specification from the YAML file."""
|
|
69
|
+
return RefspecInfo(remote_key, ref_spec, ref_type, ref_name)
|
|
102
70
|
|
|
103
|
-
def __init__(self, specs: Dict[str, ModuleSpec], workdir: Path = Path(".")):
|
|
104
|
-
self.specs = specs
|
|
105
|
-
self.workdir = workdir
|
|
106
71
|
|
|
107
|
-
|
|
108
|
-
|
|
72
|
+
def get_with_syntax_check(name, data, key: str, type: Type):
|
|
73
|
+
result = data.get(key, type())
|
|
74
|
+
if not isinstance(result, type):
|
|
75
|
+
raise Exception(f"Key {key} not of proper syntax should be {str(type)} in {name} description")
|
|
76
|
+
return result
|
|
109
77
|
|
|
110
78
|
|
|
111
79
|
def load_spec_file(config: Path, frozen: Path, workdir: Path) -> Optional[ProjectSpec]:
|
|
@@ -123,6 +91,7 @@ def load_spec_file(config: Path, frozen: Path, workdir: Path) -> Optional[Projec
|
|
|
123
91
|
config = config.resolve()
|
|
124
92
|
# If the file is not in the current directory, check inside the odoo subdirectory
|
|
125
93
|
odoo_config = config.parent / "odoo" / config.name
|
|
94
|
+
# TODO(franz): should use rich console for prettiness
|
|
126
95
|
if not odoo_config.exists():
|
|
127
96
|
print(f"Error: Neither '{config}' nor '{odoo_config}' exists.")
|
|
128
97
|
return None
|
|
@@ -151,22 +120,21 @@ def load_spec_file(config: Path, frozen: Path, workdir: Path) -> Optional[Projec
|
|
|
151
120
|
except yaml.YAMLError as e:
|
|
152
121
|
print(f"Error parsing frozen YAML file '{frozen_path}': {e}")
|
|
153
122
|
|
|
154
|
-
|
|
155
|
-
for
|
|
156
|
-
modules =
|
|
157
|
-
src =
|
|
158
|
-
remotes =
|
|
159
|
-
merges =
|
|
160
|
-
shell_commands =
|
|
161
|
-
patch_globs_to_apply =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
123
|
+
repos: Dict[str, RepoInfo] = {}
|
|
124
|
+
for repo_name, repo_data in data.items():
|
|
125
|
+
modules = get_with_syntax_check(repo_name, repo_data, "modules", list)
|
|
126
|
+
src = get_with_syntax_check(repo_name, repo_data, "src", str)
|
|
127
|
+
remotes = get_with_syntax_check(repo_name, repo_data, "remotes", dict)
|
|
128
|
+
merges = get_with_syntax_check(repo_name, repo_data, "merges", list)
|
|
129
|
+
shell_commands = get_with_syntax_check(repo_name, repo_data, "shell_command_after", list)
|
|
130
|
+
patch_globs_to_apply = get_with_syntax_check(repo_name, repo_data, "patch_globs", list)
|
|
131
|
+
target_folder = get_with_syntax_check(repo_name, repo_data, "target_folder", str)
|
|
132
|
+
locales = get_with_syntax_check(repo_name, repo_data, "locales", list)
|
|
133
|
+
|
|
134
|
+
frozen_repo = frozen_mapping.get(repo_name, {})
|
|
167
135
|
|
|
168
136
|
# Parse merges into RefspecInfo objects
|
|
169
|
-
|
|
137
|
+
refspec_infos: List[RefspecInfo] = []
|
|
170
138
|
if src:
|
|
171
139
|
# If src is defined, create a remote and merge entry from it
|
|
172
140
|
src_remotes, src_merges = make_remote_merge_from_src(src)
|
|
@@ -175,51 +143,17 @@ def load_spec_file(config: Path, frozen: Path, workdir: Path) -> Optional[Projec
|
|
|
175
143
|
|
|
176
144
|
for merge_entry in merges:
|
|
177
145
|
parts = merge_entry.split(" ", 2)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
ref_type = get_origin_type(ref_spec)
|
|
183
|
-
|
|
184
|
-
ref_name = None
|
|
185
|
-
if frozen_for_section:
|
|
186
|
-
remote_freezes = frozen_for_section.get(remote_key) or {}
|
|
187
|
-
ref_name = ref_spec
|
|
188
|
-
ref_type = OriginType.REF
|
|
189
|
-
frozen_ref = remote_freezes.get(ref_spec)
|
|
190
|
-
ref_spec = frozen_ref or ref_spec
|
|
191
|
-
|
|
192
|
-
origins.append(
|
|
193
|
-
RefspecInfo(
|
|
194
|
-
remote_key,
|
|
195
|
-
ref_spec,
|
|
196
|
-
ref_type,
|
|
197
|
-
ref_name,
|
|
198
|
-
)
|
|
199
|
-
)
|
|
200
|
-
elif len(parts) == 3:
|
|
201
|
-
warnings.warn(
|
|
202
|
-
"Deprecated src format: use <url> <sha> format for the src property",
|
|
203
|
-
DeprecationWarning,
|
|
204
|
-
)
|
|
205
|
-
remote_key, _, ref_spec = parts
|
|
206
|
-
ref_type = get_origin_type(ref_spec)
|
|
207
|
-
|
|
208
|
-
ref_name = None
|
|
209
|
-
if frozen_for_section:
|
|
210
|
-
remote_freezes = frozen_for_section.get(remote_key) or {}
|
|
211
|
-
ref_name = ref_spec
|
|
212
|
-
ref_spec = remote_freezes.get(ref_spec)
|
|
213
|
-
|
|
214
|
-
origins.append(RefspecInfo(remote_key, ref_spec, ref_type, ref_name))
|
|
215
|
-
|
|
216
|
-
specs[section_name] = ModuleSpec(
|
|
146
|
+
refspec_info = parse_remote_refspec_from_parts(parts, frozen_repo)
|
|
147
|
+
refspec_infos.append(refspec_info)
|
|
148
|
+
|
|
149
|
+
repos[repo_name] = RepoInfo(
|
|
217
150
|
modules,
|
|
218
151
|
remotes,
|
|
219
|
-
|
|
152
|
+
refspec_infos,
|
|
220
153
|
shell_commands,
|
|
221
154
|
patch_globs_to_apply,
|
|
222
|
-
|
|
155
|
+
target_folder,
|
|
156
|
+
locales,
|
|
223
157
|
)
|
|
224
158
|
|
|
225
|
-
return ProjectSpec(
|
|
159
|
+
return ProjectSpec(repos, workdir)
|
bl/spec_processor.py
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import hashlib
|
|
3
|
-
from logging import root
|
|
4
2
|
import os
|
|
5
|
-
from posix import link
|
|
6
3
|
import warnings
|
|
7
|
-
import shutil
|
|
8
4
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
5
|
+
from typing import Dict, List
|
|
10
6
|
|
|
7
|
+
from rich import progress
|
|
11
8
|
from rich.console import Console
|
|
12
9
|
from rich.live import Live
|
|
13
10
|
from rich.progress import BarColumn, MofNCompleteColumn, Progress, SpinnerColumn, TaskID, TextColumn
|
|
@@ -16,10 +13,17 @@ from typing_extensions import deprecated
|
|
|
16
13
|
|
|
17
14
|
from bl.utils import english_env, get_local_ref, get_module_path, run_git
|
|
18
15
|
|
|
19
|
-
from .
|
|
16
|
+
from bl.types import CloneFlags, CloneInfo, OriginType, ProjectSpec, RefspecInfo, RepoInfo
|
|
20
17
|
|
|
21
18
|
console = Console()
|
|
22
19
|
|
|
20
|
+
# TODO(franz): it's a bit better now but better keep an eye on it
|
|
21
|
+
# TODO(franz): Error handling should be watch carefully because if
|
|
22
|
+
# we don't exit on some error code due to the fact that git resolve to
|
|
23
|
+
# the parent repo we could activate sparse checkout on a parent folder
|
|
24
|
+
# should probably make a function that handles the error in a unified manner
|
|
25
|
+
# and crash if the error is on a vital part of the process
|
|
26
|
+
|
|
23
27
|
|
|
24
28
|
def rich_warning(message, category, filename, lineno, file=None, line=None):
|
|
25
29
|
console.print(f"[yellow]Warning:[/] {category.__name__}: {message}\n[dim]{filename}:{lineno}[/]")
|
|
@@ -29,24 +33,44 @@ warnings.showwarning = rich_warning
|
|
|
29
33
|
warnings.simplefilter("default", DeprecationWarning)
|
|
30
34
|
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
def check_path_is_repo(module_path: Path):
|
|
37
|
+
# TODO(franz): add check for .git folder
|
|
38
|
+
return not module_path.exists() or not module_path.is_dir()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clone_info_from_repo(name: str, repo_info: RepoInfo):
|
|
42
|
+
flags = CloneFlags.SHALLOW if name == "odoo" or len(repo_info.refspec_info) == 1 else 0
|
|
43
|
+
flags |= CloneFlags.SPARSE if name != "odoo" or len(repo_info.locales) > 0 else 0
|
|
44
|
+
root_refspec_info = repo_info.refspec_info[0]
|
|
45
|
+
remote_url = repo_info.remotes.get(root_refspec_info.remote)
|
|
46
|
+
|
|
47
|
+
return CloneInfo(
|
|
48
|
+
remote_url,
|
|
49
|
+
flags,
|
|
50
|
+
root_refspec_info,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# for single branch we should clone shallow but for other we should clone deep
|
|
55
|
+
# this allows us to get merge-base to work and git can then merge by pulling the minimum
|
|
56
|
+
# amount of data
|
|
57
|
+
def create_clone_args(clone_info: CloneInfo) -> List[str]:
|
|
36
58
|
"""Creates git clone arguments based on the base origin."""
|
|
37
59
|
args = [
|
|
38
60
|
"clone",
|
|
39
61
|
"--filter=tree:0",
|
|
40
62
|
]
|
|
41
63
|
|
|
42
|
-
if
|
|
64
|
+
if clone_info.clone_flags & CloneFlags.SHALLOW:
|
|
43
65
|
args += [
|
|
44
66
|
"--depth",
|
|
45
67
|
"1",
|
|
46
68
|
]
|
|
47
|
-
|
|
69
|
+
if clone_info.clone_flags & CloneFlags.SPARSE:
|
|
48
70
|
args += ["--sparse"]
|
|
49
71
|
|
|
72
|
+
ref_spec_info = clone_info.root_refspec_info
|
|
73
|
+
|
|
50
74
|
if ref_spec_info.type == OriginType.REF:
|
|
51
75
|
args += [
|
|
52
76
|
"--revision",
|
|
@@ -61,7 +85,7 @@ def create_clone_args(name: str, ref_spec_info: RefspecInfo, remote_url: str, sh
|
|
|
61
85
|
]
|
|
62
86
|
|
|
63
87
|
args += [
|
|
64
|
-
|
|
88
|
+
clone_info.url,
|
|
65
89
|
]
|
|
66
90
|
|
|
67
91
|
return args
|
|
@@ -74,22 +98,37 @@ def normalize_merge_result(ret: int, out: str, err: str):
|
|
|
74
98
|
return ret, err
|
|
75
99
|
|
|
76
100
|
|
|
77
|
-
class
|
|
101
|
+
class RepoProcessor:
|
|
78
102
|
"""
|
|
79
103
|
Processes a ProjectSpec by concurrently cloning and merging modules.
|
|
80
104
|
"""
|
|
81
105
|
|
|
82
|
-
def __init__(
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
workdir: Path,
|
|
109
|
+
name: str,
|
|
110
|
+
semaphore: asyncio.Semaphore,
|
|
111
|
+
repo_info: RepoInfo,
|
|
112
|
+
progress: Progress,
|
|
113
|
+
count_progress: Progress,
|
|
114
|
+
count_task: TaskID,
|
|
115
|
+
concurrency: int,
|
|
116
|
+
):
|
|
83
117
|
self.workdir = workdir
|
|
118
|
+
self.name = name
|
|
119
|
+
self.semaphore = semaphore
|
|
120
|
+
self.repo_info = repo_info
|
|
121
|
+
self.progress = progress
|
|
122
|
+
self.count_progress = count_progress
|
|
123
|
+
self.count_task = count_task
|
|
84
124
|
self.concurrency = concurrency
|
|
85
|
-
self.semaphore = asyncio.Semaphore(concurrency)
|
|
86
125
|
|
|
87
126
|
@deprecated(
|
|
88
127
|
"run_shell_commands is deprecated if used to apply patches. Use patch_globs properties in spec.yaml instead."
|
|
89
128
|
)
|
|
90
|
-
async def run_shell_commands(self,
|
|
91
|
-
for cmd in
|
|
92
|
-
progress.update(task_id, status=f"Running shell command: {cmd}...")
|
|
129
|
+
async def run_shell_commands(self, repo_info: RepoInfo, module_path: Path) -> int:
|
|
130
|
+
for cmd in repo_info.shell_commands:
|
|
131
|
+
self.progress.update(self.task_id, status=f"Running shell command: {cmd}...")
|
|
93
132
|
proc = await asyncio.create_subprocess_shell(
|
|
94
133
|
cmd,
|
|
95
134
|
cwd=str(module_path),
|
|
@@ -102,128 +141,74 @@ class SpecProcessor:
|
|
|
102
141
|
# This is a sanity check because people usually put "git am" commands
|
|
103
142
|
# in shell_commands, so we abort any ongoing git am
|
|
104
143
|
await run_git("am", "--abort", cwd=str(module_path))
|
|
105
|
-
progress.update(
|
|
106
|
-
task_id,
|
|
144
|
+
self.progress.update(
|
|
145
|
+
self.task_id,
|
|
107
146
|
status=f"[red]Shell command failed: {cmd}\nError: {stderr.decode().strip()}",
|
|
108
147
|
)
|
|
109
148
|
return -1
|
|
110
149
|
return 0
|
|
111
150
|
|
|
112
|
-
async def fetch_local_ref(
|
|
113
|
-
self,
|
|
114
|
-
origin: RefspecInfo,
|
|
115
|
-
local_ref: str,
|
|
116
|
-
module_path: Path,
|
|
117
|
-
) -> tuple[int, str, str]:
|
|
118
|
-
return await run_git(
|
|
119
|
-
"fetch",
|
|
120
|
-
origin.remote,
|
|
121
|
-
f"{origin.refspec}:{local_ref}",
|
|
122
|
-
cwd=module_path,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
async def clone_base_repo_ref(
|
|
126
|
-
self, name: str, ref_spec_info: RefspecInfo, remote_url: str, module_path: Path, shallow: bool
|
|
127
|
-
) -> tuple[int, str, str]:
|
|
128
|
-
args = create_clone_args(name, ref_spec_info, remote_url, shallow)
|
|
129
|
-
|
|
130
|
-
ret, out, err = await run_git(
|
|
131
|
-
*args,
|
|
132
|
-
str(module_path),
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
# if it's a ref we need to manually create a base branch because we cannot
|
|
136
|
-
# merge in a detached head
|
|
137
|
-
local_ref = get_local_ref(ref_spec_info)
|
|
138
|
-
ret, out, err = await run_git(
|
|
139
|
-
"checkout",
|
|
140
|
-
"-b",
|
|
141
|
-
local_ref,
|
|
142
|
-
cwd=str(module_path),
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
return ret, out, err
|
|
146
|
-
|
|
147
|
-
async def try_merge(
|
|
148
|
-
self,
|
|
149
|
-
progress: Progress,
|
|
150
|
-
task_id: TaskID,
|
|
151
|
-
remote_url: str,
|
|
152
|
-
local_ref: str,
|
|
153
|
-
module_path: Path,
|
|
154
|
-
origin: RefspecInfo,
|
|
155
|
-
) -> tuple[int, str]:
|
|
156
|
-
# Merge
|
|
157
|
-
# I think the idea would be to not fetch shallow but fetch treeless and do a merge-base
|
|
158
|
-
# then fetch the required data and then merge
|
|
159
|
-
progress.update(task_id, status=f"Merging {local_ref}", advance=0.1)
|
|
160
|
-
ret, out, err = await run_git("merge", "--no-edit", local_ref, cwd=module_path)
|
|
161
|
-
ret, err = normalize_merge_result(ret, out, err)
|
|
162
|
-
|
|
163
|
-
if "CONFLICT" in err:
|
|
164
|
-
progress.update(task_id, status=f"[red]Merge conflict in {origin.refspec}: {err}")
|
|
165
|
-
# In case of conflict, we might want to abort the merge
|
|
166
|
-
await run_git("merge", "--abort", cwd=module_path)
|
|
167
|
-
return ret, err
|
|
168
|
-
|
|
169
151
|
async def setup_new_repo(
|
|
170
152
|
self,
|
|
171
|
-
|
|
172
|
-
task_id: TaskID,
|
|
173
|
-
spec: ModuleSpec,
|
|
174
|
-
name: str,
|
|
175
|
-
root_refspec_info: RefspecInfo,
|
|
176
|
-
remote_url: str,
|
|
153
|
+
clone_info: CloneInfo,
|
|
177
154
|
module_path: Path,
|
|
178
155
|
) -> int:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
156
|
+
root_refspec_info = clone_info.root_refspec_info
|
|
157
|
+
remote = root_refspec_info.remote
|
|
158
|
+
root_refspec = root_refspec_info.refspec
|
|
159
|
+
|
|
160
|
+
self.progress.update(
|
|
161
|
+
self.task_id,
|
|
162
|
+
status=(f"Cloning {remote}/{root_refspec}"),
|
|
182
163
|
)
|
|
183
164
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# User --revision for specific commit checkout if needed
|
|
187
|
-
shallow_clone = len(spec.refspec_info) == 1
|
|
188
|
-
ret, out, err = await self.clone_base_repo_ref(name, root_refspec_info, remote_url, module_path, shallow_clone)
|
|
165
|
+
clone_args = create_clone_args(clone_info)
|
|
166
|
+
ret, out, err = await run_git(*clone_args, module_path)
|
|
189
167
|
|
|
190
168
|
if ret != 0:
|
|
191
169
|
status_message = (
|
|
192
|
-
f"[red]Clone failed {root_refspec_info.remote}({
|
|
170
|
+
f"[red]Clone failed {root_refspec_info.remote}({clone_info.url})/{root_refspec_info.refspec}"
|
|
193
171
|
+ f" -> {module_path}:\n{err}"
|
|
194
172
|
)
|
|
195
|
-
progress.update(task_id, status=status_message)
|
|
173
|
+
self.progress.update(self.task_id, status=status_message)
|
|
196
174
|
return ret
|
|
197
175
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
176
|
+
local_ref = get_local_ref(root_refspec_info)
|
|
177
|
+
ret, out, err = await run_git("checkout", "-b", local_ref, cwd=module_path)
|
|
178
|
+
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
async def reset_repo_for_work(self, module_path: Path) -> int:
|
|
182
|
+
# TODO(franz): we should test if the folder is a git repo or not
|
|
183
|
+
|
|
201
184
|
ret, out, err = await run_git("status", "--porcelain", cwd=module_path)
|
|
202
185
|
|
|
203
186
|
if out != "":
|
|
204
|
-
progress.update(task_id, status=f"[red]Repo is dirty:\n{out}")
|
|
187
|
+
self.progress.update(self.task_id, status=f"[red]Repo is dirty:\n{out}")
|
|
188
|
+
return ret
|
|
189
|
+
if ret != 0:
|
|
190
|
+
self.progress.update(self.task_id, status="[red]Repo does not exist")
|
|
205
191
|
return ret
|
|
206
192
|
# Reset all the local origin to their remote origins
|
|
207
|
-
|
|
208
|
-
|
|
193
|
+
repo_info = self.repo_info
|
|
194
|
+
root_refspec_info = repo_info.refspec_info[0]
|
|
195
|
+
|
|
196
|
+
self.progress.update(
|
|
197
|
+
self.task_id,
|
|
209
198
|
status=(f"Resetting existing repository for {root_refspec_info.remote}/{root_refspec_info.refspec}"),
|
|
210
199
|
)
|
|
211
200
|
|
|
212
201
|
s_ret, s_out, s_err = await run_git("rev-parse", "--is-shallow-repository", cwd=module_path)
|
|
213
|
-
if len(
|
|
202
|
+
if len(repo_info.refspec_info) > 1 and s_out == "true":
|
|
214
203
|
await run_git("fetch", "--unshallow", cwd=module_path)
|
|
215
204
|
|
|
216
|
-
reset_target =
|
|
205
|
+
reset_target = get_local_ref(root_refspec_info)
|
|
217
206
|
ret, out, err = await run_git("reset", "--hard", reset_target, cwd=module_path)
|
|
218
207
|
if ret != 0:
|
|
219
|
-
progress.update(task_id, status=f"[red]Reset failed: {err}")
|
|
208
|
+
self.progress.update(self.task_id, status=f"[red]Reset failed: {err}")
|
|
220
209
|
return ret
|
|
221
210
|
|
|
222
|
-
|
|
223
|
-
local_ref = get_local_ref(refspec_info)
|
|
224
|
-
# This is probably the best thing but for now this works good enough
|
|
225
|
-
# TODO(franz): find something better
|
|
226
|
-
ret, out, err = await run_git("branch", "-d", local_ref, cwd=module_path)
|
|
211
|
+
return 0
|
|
227
212
|
|
|
228
213
|
def link_all_modules(self, module_list: List[str], module_path: Path) -> tuple[int, str]:
|
|
229
214
|
links_path = self.workdir / "links"
|
|
@@ -247,9 +232,7 @@ class SpecProcessor:
|
|
|
247
232
|
|
|
248
233
|
async def merge_spec_into_tree(
|
|
249
234
|
self,
|
|
250
|
-
|
|
251
|
-
task_id: TaskID,
|
|
252
|
-
spec: ModuleSpec,
|
|
235
|
+
spec: RepoInfo,
|
|
253
236
|
refspec_info: RefspecInfo,
|
|
254
237
|
root_refspec_info: RefspecInfo,
|
|
255
238
|
module_path: Path,
|
|
@@ -260,11 +243,23 @@ class SpecProcessor:
|
|
|
260
243
|
local_ref = get_local_ref(refspec_info)
|
|
261
244
|
remote_ref = refspec_info.refspec
|
|
262
245
|
|
|
263
|
-
|
|
246
|
+
# Merge
|
|
247
|
+
# I think the idea would be to not fetch shallow but fetch treeless and do a merge-base
|
|
248
|
+
# then fetch the required data and then merge
|
|
249
|
+
self.progress.update(self.task_id, status=f"Merging {local_ref}", advance=0.1)
|
|
250
|
+
ret, out, err = await run_git("merge", "--no-edit", local_ref, cwd=module_path)
|
|
251
|
+
ret, err = normalize_merge_result(ret, out, err)
|
|
252
|
+
|
|
253
|
+
if "CONFLICT" in err:
|
|
254
|
+
self.progress.update(self.task_id, status=f"[red]Merge conflict {local_ref} in {remote_ref}: {err}")
|
|
255
|
+
# In case of conflict, we might want to abort the merge
|
|
256
|
+
await run_git("merge", "--abort", cwd=module_path)
|
|
257
|
+
return ret, err
|
|
258
|
+
|
|
264
259
|
if ret != 0:
|
|
260
|
+
self.progress.update(self.task_id, status=f"[red]Merge error {local_ref} in {remote_ref}: {err}")
|
|
265
261
|
return ret, err
|
|
266
262
|
|
|
267
|
-
progress.advance(task_id)
|
|
268
263
|
return 0, ""
|
|
269
264
|
|
|
270
265
|
def get_refspec_by_remote(self, refspec_info_list: List[RefspecInfo]) -> Dict[str, List[RefspecInfo]]:
|
|
@@ -293,7 +288,7 @@ class SpecProcessor:
|
|
|
293
288
|
|
|
294
289
|
return ret, out, err
|
|
295
290
|
|
|
296
|
-
def filter_non_link_module(self, spec:
|
|
291
|
+
def filter_non_link_module(self, spec: RepoInfo):
|
|
297
292
|
result = []
|
|
298
293
|
base_path_links = self.workdir / "links"
|
|
299
294
|
for module in spec.modules:
|
|
@@ -307,153 +302,170 @@ class SpecProcessor:
|
|
|
307
302
|
)
|
|
308
303
|
return result
|
|
309
304
|
|
|
310
|
-
async def
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
"""
|
|
314
|
-
|
|
305
|
+
async def setup_odoo_sparse(self, module_spec: RepoInfo, module_path: Path):
|
|
306
|
+
list_modules = module_spec.modules
|
|
307
|
+
|
|
308
|
+
await run_git("sparse-checkout", "init", "--no-cone", cwd=module_path)
|
|
309
|
+
included_po = [f"{locale}.po" for locale in module_spec.locales]
|
|
310
|
+
included_modules = [f"/addons/{module}/*" for module in list_modules]
|
|
311
|
+
await run_git(
|
|
312
|
+
"sparse-checkout",
|
|
313
|
+
"set",
|
|
314
|
+
"/*",
|
|
315
|
+
"!/addons/*",
|
|
316
|
+
*included_modules,
|
|
317
|
+
"!*.po",
|
|
318
|
+
*included_po,
|
|
319
|
+
cwd=module_path,
|
|
320
|
+
)
|
|
315
321
|
|
|
316
|
-
|
|
322
|
+
async def setup_sparse_checkout(self, symlink_modules: List[str], module_path: Path):
|
|
323
|
+
# 2. Sparse Checkout setup
|
|
324
|
+
if self.name != "odoo":
|
|
325
|
+
self.progress.update(self.task_id, status="Configuring sparse checkout...")
|
|
326
|
+
await run_git("sparse-checkout", "init", "--cone", cwd=module_path)
|
|
327
|
+
if symlink_modules:
|
|
328
|
+
await run_git("sparse-checkout", "set", *self.repo_info.modules, cwd=module_path)
|
|
329
|
+
elif len(self.repo_info.locales) > 0:
|
|
330
|
+
# TODO(franz): We should still set sparse if there is no locales but there is a module list
|
|
331
|
+
self.progress.update(self.task_id, status="Configuring sparse odoo checkout...")
|
|
332
|
+
await self.setup_odoo_sparse(self.repo_info, module_path)
|
|
333
|
+
|
|
334
|
+
async def process_repo(self) -> int:
|
|
335
|
+
"""Processes a single ModuleSpec."""
|
|
336
|
+
symlink_modules = self.filter_non_link_module(self.repo_info)
|
|
337
|
+
module_path = get_module_path(self.workdir, self.name, self.repo_info)
|
|
317
338
|
|
|
318
339
|
async with self.semaphore:
|
|
319
|
-
task_id = progress.add_task(f"[cyan]{name}", status="Waiting...", total=total_steps)
|
|
320
340
|
try:
|
|
321
|
-
|
|
322
|
-
|
|
341
|
+
self.task_id = self.progress.add_task(
|
|
342
|
+
f"[cyan]{self.name}", status="Waiting...", total=len(self.repo_info.refspec_info) + 1
|
|
343
|
+
)
|
|
344
|
+
if not self.repo_info.refspec_info:
|
|
345
|
+
self.progress.update(self.task_id, status="[yellow]No origins defined", completed=1)
|
|
323
346
|
return -1
|
|
324
347
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if not module_path.exists() or not module_path.is_dir():
|
|
332
|
-
await self.setup_new_repo(progress, task_id, spec, name, root_refspec_info, remote_url, module_path)
|
|
348
|
+
# TODO(franz) the shallow and sparseness of repo should be unify
|
|
349
|
+
# so that we don't have all those stupid conditions
|
|
350
|
+
if check_path_is_repo(module_path):
|
|
351
|
+
clone_info = clone_info_from_repo(self.name, self.repo_info)
|
|
352
|
+
ret = await self.setup_new_repo(clone_info, module_path)
|
|
333
353
|
else:
|
|
334
|
-
await self.reset_repo_for_work(
|
|
335
|
-
|
|
336
|
-
if name != "odoo":
|
|
337
|
-
# We don't do sparse checkout for odoo because the odoo repo does not work at
|
|
338
|
-
# all like the other repos (modules are in addons/ and src/addons/) instead of
|
|
339
|
-
# at the root of the repo
|
|
354
|
+
ret = await self.reset_repo_for_work(module_path)
|
|
340
355
|
|
|
341
|
-
|
|
342
|
-
|
|
356
|
+
if ret != 0:
|
|
357
|
+
return -1
|
|
343
358
|
|
|
344
|
-
|
|
345
|
-
progress.update(task_id, status="Configuring sparse checkout...")
|
|
346
|
-
await run_git("sparse-checkout", "init", "--cone", cwd=module_path)
|
|
347
|
-
if symlink_modules:
|
|
348
|
-
await run_git("sparse-checkout", "set", *spec.modules, cwd=module_path)
|
|
359
|
+
await self.setup_sparse_checkout(symlink_modules, module_path)
|
|
349
360
|
|
|
350
361
|
checkout_target = "merged"
|
|
351
362
|
|
|
352
363
|
await run_git("checkout", "-b", checkout_target, cwd=module_path)
|
|
353
|
-
progress.advance(task_id)
|
|
364
|
+
self.progress.advance(self.task_id)
|
|
354
365
|
|
|
355
|
-
for remote, remote_url in
|
|
366
|
+
for remote, remote_url in self.repo_info.remotes.items():
|
|
356
367
|
await run_git("remote", "add", remote, remote_url, cwd=module_path)
|
|
357
368
|
await run_git("config", f"remote.{remote}.partialCloneFilter", "tree:0", cwd=module_path)
|
|
358
369
|
await run_git("config", f"remote.{remote}.promisor", "true", cwd=module_path)
|
|
359
370
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
refspec_by_remote: Dict[str, List[RefspecInfo]] = self.get_refspec_by_remote(spec.refspec_info[1:])
|
|
371
|
+
refspec_by_remote: Dict[str, List[RefspecInfo]] = self.get_refspec_by_remote(
|
|
372
|
+
self.repo_info.refspec_info
|
|
373
|
+
)
|
|
364
374
|
|
|
375
|
+
# TODO(franz): right now we fetch everything so when the repo is just cloned
|
|
376
|
+
# we fetch the base branch twice. Since we fetch with multi this is probably not
|
|
377
|
+
# a big issue but it could be better
|
|
365
378
|
for remote, refspec_list in refspec_by_remote.items():
|
|
366
|
-
progress.update(task_id, status=f"Fetching multi from {remote}")
|
|
379
|
+
self.progress.update(self.task_id, status=f"Fetching multi from {remote}")
|
|
367
380
|
await self.fetch_multi(remote, refspec_list, module_path)
|
|
368
381
|
|
|
369
382
|
# 4. Fetch and Merge remaining origins
|
|
370
|
-
for refspec_info in
|
|
383
|
+
for refspec_info in self.repo_info.refspec_info[1:]:
|
|
371
384
|
ret, err = await self.merge_spec_into_tree(
|
|
372
|
-
|
|
385
|
+
self.repo_info, refspec_info, self.repo_info.refspec_info[0], module_path
|
|
373
386
|
)
|
|
374
387
|
if ret != 0:
|
|
375
|
-
progress.update(task_id, status=f"[purple]Merge failed from {refspec_info.refspec}: {err}")
|
|
376
388
|
return -1
|
|
389
|
+
self.progress.advance(self.task_id)
|
|
377
390
|
|
|
378
|
-
if
|
|
379
|
-
ret = await self.run_shell_commands(
|
|
391
|
+
if self.repo_info.shell_commands:
|
|
392
|
+
ret = await self.run_shell_commands(self.repo_info, module_path)
|
|
380
393
|
if ret != 0:
|
|
381
394
|
return ret
|
|
382
395
|
|
|
383
|
-
if
|
|
384
|
-
for glob in
|
|
385
|
-
progress.update(task_id, status=f"Applying patches: {glob}...", advance=0.1)
|
|
396
|
+
if self.repo_info.patch_globs_to_apply:
|
|
397
|
+
for glob in self.repo_info.patch_globs_to_apply:
|
|
398
|
+
self.progress.update(self.task_id, status=f"Applying patches: {glob}...", advance=0.1)
|
|
386
399
|
ret, out, err = await run_git("am", glob, cwd=module_path)
|
|
387
400
|
if ret != 0:
|
|
388
401
|
await run_git("am", "--abort", cwd=module_path)
|
|
389
|
-
progress.update(task_id, status=f"[red]Applying patches failed: {err}")
|
|
402
|
+
self.progress.update(self.task_id, status=f"[red]Applying patches failed: {err}")
|
|
390
403
|
return ret
|
|
391
404
|
|
|
392
|
-
progress.update(task_id, status="Linking directory")
|
|
393
|
-
if name != "odoo":
|
|
405
|
+
self.progress.update(self.task_id, status="Linking directory")
|
|
406
|
+
if self.name != "odoo":
|
|
394
407
|
ret, err = self.link_all_modules(symlink_modules, module_path)
|
|
395
408
|
if ret != 0:
|
|
396
|
-
progress.update(task_id, status=f"[red]Could not link modules: {err}")
|
|
409
|
+
self.progress.update(self.task_id, status=f"[red]Could not link modules: {err}")
|
|
397
410
|
return ret
|
|
398
411
|
|
|
399
|
-
progress.update(task_id, status="[green]Complete")
|
|
400
|
-
progress.remove_task(task_id)
|
|
401
|
-
count_progress.advance(count_task)
|
|
412
|
+
self.progress.update(self.task_id, status="[green]Complete", advance=1)
|
|
413
|
+
self.progress.remove_task(self.task_id)
|
|
414
|
+
self.count_progress.advance(self.count_task)
|
|
402
415
|
|
|
403
416
|
except Exception as e:
|
|
404
|
-
progress.update(task_id, status=f"[red]Error: {str(e)}")
|
|
417
|
+
self.progress.update(self.task_id, status=f"[red]Error: {str(e)}")
|
|
418
|
+
raise e
|
|
405
419
|
return -1
|
|
406
420
|
|
|
407
421
|
return 0
|
|
408
422
|
|
|
409
|
-
async def process_project(self, project_spec: ProjectSpec) -> None:
|
|
410
|
-
"""Processes all modules in a ProjectSpec."""
|
|
411
|
-
(self.workdir / "external-src").mkdir(parents=True, exist_ok=True)
|
|
412
|
-
|
|
413
|
-
task_list_progress = Progress(
|
|
414
|
-
SpinnerColumn(),
|
|
415
|
-
TextColumn("[progress.description]{task.description}"),
|
|
416
|
-
BarColumn(),
|
|
417
|
-
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
418
|
-
TextColumn("{task.fields[status]}", table_column=Column(ratio=2)),
|
|
419
|
-
)
|
|
420
423
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
MofNCompleteColumn(),
|
|
425
|
-
)
|
|
426
|
-
count_task = task_count_progress.add_task("Processing Modules", total=len(project_spec.specs))
|
|
424
|
+
async def process_project(project_spec: ProjectSpec, concurrency: int) -> None:
|
|
425
|
+
"""Processes all modules in a ProjectSpec."""
|
|
426
|
+
(project_spec.workdir / "external-src").mkdir(parents=True, exist_ok=True)
|
|
427
427
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
428
|
+
task_list_progress = Progress(
|
|
429
|
+
SpinnerColumn(),
|
|
430
|
+
TextColumn("[progress.description]{task.description}"),
|
|
431
|
+
BarColumn(),
|
|
432
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
433
|
+
TextColumn("{task.fields[status]}", table_column=Column(ratio=2)),
|
|
434
|
+
)
|
|
435
435
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
spec,
|
|
443
|
-
task_list_progress,
|
|
444
|
-
task_count_progress,
|
|
445
|
-
count_task,
|
|
446
|
-
)
|
|
447
|
-
)
|
|
436
|
+
task_count_progress = Progress(
|
|
437
|
+
TextColumn("[progress.description]{task.description}"),
|
|
438
|
+
BarColumn(),
|
|
439
|
+
MofNCompleteColumn(),
|
|
440
|
+
)
|
|
441
|
+
count_task = task_count_progress.add_task("Processing Modules", total=len(project_spec.repos))
|
|
448
442
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
443
|
+
progress_table = Table.grid()
|
|
444
|
+
progress_table.add_row(
|
|
445
|
+
task_list_progress,
|
|
446
|
+
)
|
|
447
|
+
progress_table.add_row(
|
|
448
|
+
task_count_progress,
|
|
449
|
+
)
|
|
453
450
|
|
|
451
|
+
semaphore = asyncio.Semaphore(concurrency)
|
|
452
|
+
with Live(progress_table, console=console, refresh_per_second=10):
|
|
453
|
+
tasks = []
|
|
454
|
+
for name, repo_info in project_spec.repos.items():
|
|
455
|
+
total_steps = len(repo_info.refspec_info) + 1
|
|
456
|
+
repo_processor = RepoProcessor(
|
|
457
|
+
project_spec.workdir,
|
|
458
|
+
name,
|
|
459
|
+
semaphore,
|
|
460
|
+
repo_info,
|
|
461
|
+
task_list_progress,
|
|
462
|
+
task_count_progress,
|
|
463
|
+
count_task,
|
|
464
|
+
concurrency,
|
|
465
|
+
)
|
|
466
|
+
tasks.append(repo_processor.process_repo())
|
|
454
467
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return await processor.process_project(project_spec)
|
|
468
|
+
# this should error if a task crashes
|
|
469
|
+
return_codes = await asyncio.gather(*tasks)
|
|
470
|
+
if any(return_codes):
|
|
471
|
+
raise Exception()
|
bl/types.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum, IntEnum
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OriginType(Enum):
|
|
8
|
+
"""Type of origin reference."""
|
|
9
|
+
|
|
10
|
+
BRANCH = "branch"
|
|
11
|
+
PR = "pr"
|
|
12
|
+
REF = "ref"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Remote:
|
|
17
|
+
name: str
|
|
18
|
+
url: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CloneFlags(IntEnum):
|
|
22
|
+
SHALLOW = 1
|
|
23
|
+
SPARSE = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RefspecInfo:
|
|
27
|
+
"""A git refspec with its remote, type and optional frozen sha."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
remote: str,
|
|
32
|
+
ref_str: str,
|
|
33
|
+
type: OriginType,
|
|
34
|
+
ref_name: Optional[str],
|
|
35
|
+
):
|
|
36
|
+
self.remote = remote
|
|
37
|
+
self.refspec = ref_str
|
|
38
|
+
""" The refspec string (branch name, PR ref, or commit hash). """
|
|
39
|
+
self.type = type
|
|
40
|
+
self.ref_name = ref_name
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"RefspecInfo(remote={self.remote!r}, origin={self.refspec!r}, type={self.type.value})"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class CloneInfo:
|
|
48
|
+
url: str
|
|
49
|
+
clone_flags: int
|
|
50
|
+
root_refspec_info: RefspecInfo
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RepoInfo:
|
|
54
|
+
"""Represents the specification for a set of modules."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
modules: List[str],
|
|
59
|
+
remotes: List[str] = {},
|
|
60
|
+
refspecs: List[RefspecInfo] = [],
|
|
61
|
+
shell_commands: List[str] = [],
|
|
62
|
+
patch_globs_to_apply: List[str] = [],
|
|
63
|
+
target_folder: Optional[str] = None,
|
|
64
|
+
locales: List[str] = [],
|
|
65
|
+
):
|
|
66
|
+
self.modules = modules
|
|
67
|
+
self.remotes = remotes
|
|
68
|
+
self.refspec_info = refspecs
|
|
69
|
+
self.shell_commands = shell_commands
|
|
70
|
+
self.patch_globs_to_apply = patch_globs_to_apply
|
|
71
|
+
self.target_folder = target_folder
|
|
72
|
+
self.locales = locales
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
return f"ModuleSpec(modules={self.modules}, remotes={self.remotes}, origins={self.refspec_info})"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ProjectSpec:
|
|
79
|
+
"""Represents the overall project specification from the YAML file."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, repos: Dict[str, RepoInfo], workdir: Path = Path(".")):
|
|
82
|
+
self.repos = repos
|
|
83
|
+
self.workdir = workdir
|
|
84
|
+
|
|
85
|
+
def __repr__(self) -> str:
|
|
86
|
+
return f"ProjectSpec(specs={self.repos}, workdir={self.workdir})"
|
bl/utils.py
CHANGED
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
import warnings
|
|
7
|
-
from bl.
|
|
7
|
+
from bl.types import RepoInfo, OriginType, RefspecInfo
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
english_env = os.environ.copy()
|
|
@@ -12,16 +12,16 @@ english_env = os.environ.copy()
|
|
|
12
12
|
english_env["LANG"] = "en_US.UTF-8"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def get_module_path(workdir: Path, module_name: str, module_spec:
|
|
15
|
+
def get_module_path(workdir: Path, module_name: str, module_spec: RepoInfo) -> Path:
|
|
16
16
|
"""Returns the path to the module directory."""
|
|
17
|
-
if module_name == "odoo" and module_spec.target_folder
|
|
17
|
+
if module_name == "odoo" and not module_spec.target_folder:
|
|
18
18
|
warnings.warn(
|
|
19
19
|
"importing 'odoo' without a 'target_folder' "
|
|
20
20
|
+ "property is deprecated. Use target_folder: 'src/' in spec.yaml.",
|
|
21
21
|
DeprecationWarning,
|
|
22
22
|
)
|
|
23
23
|
return workdir / "src/"
|
|
24
|
-
elif module_spec.target_folder
|
|
24
|
+
elif module_spec.target_folder:
|
|
25
25
|
return workdir / module_spec.target_folder
|
|
26
26
|
else:
|
|
27
27
|
return workdir / "external-src" / module_name
|
|
@@ -36,6 +36,8 @@ async def run_git(*args: str, cwd: Optional[Path] = None) -> tuple[int, str, str
|
|
|
36
36
|
"""Executes a git command asynchronously."""
|
|
37
37
|
proc = await asyncio.create_subprocess_exec(
|
|
38
38
|
"git",
|
|
39
|
+
"--git-dir",
|
|
40
|
+
".git/",
|
|
39
41
|
*args,
|
|
40
42
|
stdout=asyncio.subprocess.PIPE,
|
|
41
43
|
stderr=asyncio.subprocess.PIPE,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
bl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
bl/__main__.py,sha256=ypl02Cl-9NF9823xjdHVQa3aA9lHtnKAJFrtwmP9vbc,1643
|
|
3
|
+
bl/freezer.py,sha256=wUYNd0zKU-0OGdIXSLJol-_xJmxSSkXvzV_VbF2HJyg,2512
|
|
4
|
+
bl/spec_parser.py,sha256=dVvaK4fhtut5_vI3sm7g5oGk0hc_vFAvDfnVDVTf_64,5457
|
|
5
|
+
bl/spec_processor.py,sha256=KsgNX-IAizLZ_22WcFKJAeCJKd98clDZb4qQ0kclqOs,17893
|
|
6
|
+
bl/types.py,sha256=h14FXDVCrYRxY7lYTEu8jhdrEHr1PvSNyZRIHm33CTk,2158
|
|
7
|
+
bl/utils.py,sha256=ZAdvXjNqDhICoZU8IZ5QQnmA5ILdYFbjaEeieyuo2GI,1622
|
|
8
|
+
bl_odoo-0.3.1.dist-info/licenses/LICENSE,sha256=GTVQl3vH6ht70wJXKC0yMT8CmXKHxv_YyO_utAgm7EA,1065
|
|
9
|
+
bl_odoo-0.3.1.dist-info/METADATA,sha256=6-dmdSbQqzfSmhA-wbIer6Pj36rtRretVUQ88TFSzIA,391
|
|
10
|
+
bl_odoo-0.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
bl_odoo-0.3.1.dist-info/entry_points.txt,sha256=fmdGhYYJlP-XByamgaZdM0bo3JK4LJFswU_Nilq6SSw,39
|
|
12
|
+
bl_odoo-0.3.1.dist-info/top_level.txt,sha256=1o4tN3wszdw7U5SnGgdF5P2sTYA0Schf0vKFy9_2D6A,3
|
|
13
|
+
bl_odoo-0.3.1.dist-info/RECORD,,
|
bl_odoo-0.2.7.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
bl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
bl/__main__.py,sha256=v1d-voJ7N1QBLGJJh8JdrTxXtzf0JFHQv4RUBxlCkcg,1428
|
|
3
|
-
bl/freezer.py,sha256=SXqfTXTys7tu-m94TftUjDLt7usNMkYoO10ZX62fRJg,2577
|
|
4
|
-
bl/spec_parser.py,sha256=23_J5qf-20uSrKOu3drT0u2I1c9UwOGvp2Y7Ql8K5GE,7461
|
|
5
|
-
bl/spec_processor.py,sha256=_acOuo_gfAZQEaSSDtZa3DAnTEBfGGVTke9wedS3Owo,17572
|
|
6
|
-
bl/utils.py,sha256=d6pmkwlMLU4jm94JMisd6LT31YJ_oyqgX50O3g5yzq4,1610
|
|
7
|
-
bl_odoo-0.2.7.dist-info/licenses/LICENSE,sha256=GTVQl3vH6ht70wJXKC0yMT8CmXKHxv_YyO_utAgm7EA,1065
|
|
8
|
-
bl_odoo-0.2.7.dist-info/METADATA,sha256=H2jetZWnqXRa3w-rnYHHRaBmBN44_w9kkZdSy9WuKgA,391
|
|
9
|
-
bl_odoo-0.2.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
-
bl_odoo-0.2.7.dist-info/entry_points.txt,sha256=fmdGhYYJlP-XByamgaZdM0bo3JK4LJFswU_Nilq6SSw,39
|
|
11
|
-
bl_odoo-0.2.7.dist-info/top_level.txt,sha256=1o4tN3wszdw7U5SnGgdF5P2sTYA0Schf0vKFy9_2D6A,3
|
|
12
|
-
bl_odoo-0.2.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|