pymelos 0.1.3__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.
Files changed (57) hide show
  1. pymelos/__init__.py +63 -0
  2. pymelos/__main__.py +6 -0
  3. pymelos/cli/__init__.py +5 -0
  4. pymelos/cli/__main__.py +6 -0
  5. pymelos/cli/app.py +527 -0
  6. pymelos/cli/commands/__init__.py +1 -0
  7. pymelos/cli/commands/init.py +151 -0
  8. pymelos/commands/__init__.py +84 -0
  9. pymelos/commands/add.py +77 -0
  10. pymelos/commands/base.py +108 -0
  11. pymelos/commands/bootstrap.py +154 -0
  12. pymelos/commands/changed.py +161 -0
  13. pymelos/commands/clean.py +142 -0
  14. pymelos/commands/exec.py +116 -0
  15. pymelos/commands/list.py +128 -0
  16. pymelos/commands/release.py +258 -0
  17. pymelos/commands/run.py +160 -0
  18. pymelos/compat.py +14 -0
  19. pymelos/config/__init__.py +47 -0
  20. pymelos/config/loader.py +132 -0
  21. pymelos/config/schema.py +236 -0
  22. pymelos/errors.py +139 -0
  23. pymelos/execution/__init__.py +32 -0
  24. pymelos/execution/parallel.py +249 -0
  25. pymelos/execution/results.py +172 -0
  26. pymelos/execution/runner.py +171 -0
  27. pymelos/filters/__init__.py +27 -0
  28. pymelos/filters/chain.py +101 -0
  29. pymelos/filters/ignore.py +60 -0
  30. pymelos/filters/scope.py +90 -0
  31. pymelos/filters/since.py +98 -0
  32. pymelos/git/__init__.py +69 -0
  33. pymelos/git/changes.py +153 -0
  34. pymelos/git/commits.py +174 -0
  35. pymelos/git/repo.py +210 -0
  36. pymelos/git/tags.py +242 -0
  37. pymelos/py.typed +0 -0
  38. pymelos/types.py +16 -0
  39. pymelos/uv/__init__.py +44 -0
  40. pymelos/uv/client.py +167 -0
  41. pymelos/uv/publish.py +162 -0
  42. pymelos/uv/sync.py +168 -0
  43. pymelos/versioning/__init__.py +57 -0
  44. pymelos/versioning/changelog.py +189 -0
  45. pymelos/versioning/conventional.py +216 -0
  46. pymelos/versioning/semver.py +249 -0
  47. pymelos/versioning/updater.py +146 -0
  48. pymelos/workspace/__init__.py +33 -0
  49. pymelos/workspace/discovery.py +138 -0
  50. pymelos/workspace/graph.py +238 -0
  51. pymelos/workspace/package.py +191 -0
  52. pymelos/workspace/workspace.py +218 -0
  53. pymelos-0.1.3.dist-info/METADATA +106 -0
  54. pymelos-0.1.3.dist-info/RECORD +57 -0
  55. pymelos-0.1.3.dist-info/WHEEL +4 -0
  56. pymelos-0.1.3.dist-info/entry_points.txt +2 -0
  57. pymelos-0.1.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,98 @@
1
+ """Git-based package filtering (--since)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from pymelos.workspace.package import Package
10
+ from pymelos.workspace.workspace import Workspace
11
+
12
+
13
+ def get_changed_files(
14
+ root: Path,
15
+ since: str,
16
+ *,
17
+ include_untracked: bool = True,
18
+ ) -> set[Path]:
19
+ """Get files changed since a git reference.
20
+
21
+ Args:
22
+ root: Repository root.
23
+ since: Git reference (branch, tag, commit).
24
+ include_untracked: Include untracked files.
25
+
26
+ Returns:
27
+ Set of changed file paths (relative to root).
28
+ """
29
+ from pymelos.git import get_changed_files_since
30
+
31
+ return get_changed_files_since(root, since, include_untracked=include_untracked)
32
+
33
+
34
+ def get_changed_packages(
35
+ workspace: Workspace,
36
+ since: str,
37
+ *,
38
+ include_dependents: bool = False,
39
+ ) -> list[Package]:
40
+ """Get packages that have changed since a git reference.
41
+
42
+ Args:
43
+ workspace: Workspace instance.
44
+ since: Git reference.
45
+ include_dependents: Also include packages that depend on changed packages.
46
+
47
+ Returns:
48
+ List of changed packages.
49
+ """
50
+ changed_files = get_changed_files(workspace.root, since)
51
+
52
+ # Map changed files to packages
53
+ changed_packages: list[Package] = []
54
+
55
+ for package in workspace.packages.values():
56
+ # Check if any changed file is within this package
57
+ for changed_file in changed_files:
58
+ abs_changed = workspace.root / changed_file
59
+ try:
60
+ abs_changed.relative_to(package.path)
61
+ if package not in changed_packages:
62
+ changed_packages.append(package)
63
+ break
64
+ except ValueError:
65
+ continue
66
+
67
+ if include_dependents:
68
+ affected = workspace.get_affected_packages(changed_packages)
69
+ return affected
70
+
71
+ return changed_packages
72
+
73
+
74
+ def filter_by_since(
75
+ packages: list[Package],
76
+ workspace: Workspace,
77
+ since: str | None,
78
+ *,
79
+ include_dependents: bool = False,
80
+ ) -> list[Package]:
81
+ """Filter packages to only those changed since a git reference.
82
+
83
+ Args:
84
+ packages: List of packages to filter.
85
+ workspace: Workspace instance.
86
+ since: Git reference.
87
+ include_dependents: Also include packages that depend on changed packages.
88
+
89
+ Returns:
90
+ Filtered list of packages.
91
+ """
92
+ if not since:
93
+ return packages
94
+
95
+ changed = get_changed_packages(workspace, since, include_dependents=include_dependents)
96
+ changed_names = {p.name for p in changed}
97
+
98
+ return [p for p in packages if p.name in changed_names]
@@ -0,0 +1,69 @@
1
+ """Git operations."""
2
+
3
+ from pymelos.git.changes import (
4
+ get_changed_files_since,
5
+ get_commits_since,
6
+ get_files_in_commit,
7
+ get_merge_base,
8
+ is_ancestor,
9
+ )
10
+ from pymelos.git.commits import (
11
+ Commit,
12
+ get_commit,
13
+ get_commits,
14
+ get_commits_affecting_path,
15
+ )
16
+ from pymelos.git.repo import (
17
+ get_current_branch,
18
+ get_current_commit,
19
+ get_default_branch,
20
+ get_repo_root,
21
+ is_clean,
22
+ is_git_repo,
23
+ run_git_command,
24
+ run_git_command_async,
25
+ )
26
+ from pymelos.git.tags import (
27
+ Tag,
28
+ create_tag,
29
+ delete_tag,
30
+ get_latest_package_tag,
31
+ get_latest_tag,
32
+ get_package_tags,
33
+ get_tags_for_commit,
34
+ list_tags,
35
+ parse_version_from_tag,
36
+ )
37
+
38
+ __all__ = [
39
+ # Repo
40
+ "is_git_repo",
41
+ "get_repo_root",
42
+ "run_git_command",
43
+ "run_git_command_async",
44
+ "get_current_branch",
45
+ "get_current_commit",
46
+ "is_clean",
47
+ "get_default_branch",
48
+ # Changes
49
+ "get_changed_files_since",
50
+ "get_files_in_commit",
51
+ "get_commits_since",
52
+ "is_ancestor",
53
+ "get_merge_base",
54
+ # Commits
55
+ "Commit",
56
+ "get_commits",
57
+ "get_commit",
58
+ "get_commits_affecting_path",
59
+ # Tags
60
+ "Tag",
61
+ "list_tags",
62
+ "get_latest_tag",
63
+ "get_tags_for_commit",
64
+ "create_tag",
65
+ "delete_tag",
66
+ "parse_version_from_tag",
67
+ "get_package_tags",
68
+ "get_latest_package_tag",
69
+ ]
pymelos/git/changes.py ADDED
@@ -0,0 +1,153 @@
1
+ """Git change detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from pymelos.git.repo import run_git_command
8
+
9
+
10
+ def get_changed_files_since(
11
+ cwd: Path,
12
+ since: str,
13
+ *,
14
+ include_untracked: bool = True,
15
+ ) -> set[Path]:
16
+ """Get files changed since a git reference.
17
+
18
+ Args:
19
+ cwd: Working directory (repository root).
20
+ since: Git reference (branch, tag, commit SHA).
21
+ include_untracked: Include untracked files.
22
+
23
+ Returns:
24
+ Set of changed file paths (relative to cwd).
25
+ """
26
+ changed: set[Path] = set()
27
+
28
+ # Get files changed between since ref and HEAD
29
+ result = run_git_command(
30
+ ["diff", "--name-only", f"{since}...HEAD"],
31
+ cwd=cwd,
32
+ check=False,
33
+ )
34
+ if result.returncode == 0:
35
+ for line in result.stdout.strip().split("\n"):
36
+ if line:
37
+ changed.add(Path(line))
38
+
39
+ # Get staged changes
40
+ result = run_git_command(
41
+ ["diff", "--name-only", "--cached"],
42
+ cwd=cwd,
43
+ check=False,
44
+ )
45
+ if result.returncode == 0:
46
+ for line in result.stdout.strip().split("\n"):
47
+ if line:
48
+ changed.add(Path(line))
49
+
50
+ # Get unstaged changes
51
+ result = run_git_command(
52
+ ["diff", "--name-only"],
53
+ cwd=cwd,
54
+ check=False,
55
+ )
56
+ if result.returncode == 0:
57
+ for line in result.stdout.strip().split("\n"):
58
+ if line:
59
+ changed.add(Path(line))
60
+
61
+ # Get untracked files
62
+ if include_untracked:
63
+ result = run_git_command(
64
+ ["ls-files", "--others", "--exclude-standard"],
65
+ cwd=cwd,
66
+ check=False,
67
+ )
68
+ if result.returncode == 0:
69
+ for line in result.stdout.strip().split("\n"):
70
+ if line:
71
+ changed.add(Path(line))
72
+
73
+ return changed
74
+
75
+
76
+ def get_files_in_commit(cwd: Path, commit: str) -> set[Path]:
77
+ """Get files changed in a specific commit.
78
+
79
+ Args:
80
+ cwd: Working directory.
81
+ commit: Commit SHA.
82
+
83
+ Returns:
84
+ Set of file paths changed in the commit.
85
+ """
86
+ result = run_git_command(
87
+ ["diff-tree", "--no-commit-id", "--name-only", "-r", commit],
88
+ cwd=cwd,
89
+ )
90
+ files: set[Path] = set()
91
+ for line in result.stdout.strip().split("\n"):
92
+ if line:
93
+ files.add(Path(line))
94
+ return files
95
+
96
+
97
+ def get_commits_since(
98
+ cwd: Path,
99
+ since: str,
100
+ *,
101
+ path: Path | None = None,
102
+ ) -> list[str]:
103
+ """Get commit SHAs since a reference.
104
+
105
+ Args:
106
+ cwd: Working directory.
107
+ since: Git reference.
108
+ path: Optional path to filter commits.
109
+
110
+ Returns:
111
+ List of commit SHAs (newest first).
112
+ """
113
+ args = ["log", "--format=%H", f"{since}..HEAD"]
114
+ if path:
115
+ args.extend(["--", str(path)])
116
+
117
+ result = run_git_command(args, cwd=cwd)
118
+ commits = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
119
+ return commits
120
+
121
+
122
+ def is_ancestor(cwd: Path, commit: str, ancestor: str) -> bool:
123
+ """Check if ancestor is an ancestor of commit.
124
+
125
+ Args:
126
+ cwd: Working directory.
127
+ commit: Commit to check.
128
+ ancestor: Potential ancestor.
129
+
130
+ Returns:
131
+ True if ancestor is an ancestor of commit.
132
+ """
133
+ result = run_git_command(
134
+ ["merge-base", "--is-ancestor", ancestor, commit],
135
+ cwd=cwd,
136
+ check=False,
137
+ )
138
+ return result.returncode == 0
139
+
140
+
141
+ def get_merge_base(cwd: Path, ref1: str, ref2: str) -> str:
142
+ """Get the merge base between two refs.
143
+
144
+ Args:
145
+ cwd: Working directory.
146
+ ref1: First reference.
147
+ ref2: Second reference.
148
+
149
+ Returns:
150
+ Merge base commit SHA.
151
+ """
152
+ result = run_git_command(["merge-base", ref1, ref2], cwd=cwd)
153
+ return result.stdout.strip()
pymelos/git/commits.py ADDED
@@ -0,0 +1,174 @@
1
+ """Git commit parsing and analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from pymelos.git.repo import run_git_command
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class Commit:
13
+ """Represents a git commit.
14
+
15
+ Attributes:
16
+ sha: Full commit SHA.
17
+ short_sha: Abbreviated commit SHA.
18
+ message: Full commit message.
19
+ subject: First line of commit message.
20
+ body: Commit message body (after first line).
21
+ author_name: Commit author name.
22
+ author_email: Commit author email.
23
+ timestamp: Unix timestamp of commit.
24
+ """
25
+
26
+ sha: str
27
+ short_sha: str
28
+ message: str
29
+ author_name: str
30
+ author_email: str
31
+ timestamp: int
32
+
33
+ @property
34
+ def subject(self) -> str:
35
+ """First line of commit message."""
36
+ return self.message.split("\n", 1)[0]
37
+
38
+ @property
39
+ def body(self) -> str | None:
40
+ """Commit message body (after first line)."""
41
+ parts = self.message.split("\n", 1)
42
+ if len(parts) > 1:
43
+ return parts[1].strip()
44
+ return None
45
+
46
+
47
+ # Format for git log output (fields separated by special delimiter)
48
+ # Uses record separator (%x1e) at end to handle bodies with newlines
49
+ LOG_FORMAT = "%H%x00%h%x00%s%x00%b%x00%an%x00%ae%x00%ct%x1e"
50
+ FIELD_SEPARATOR = "\x00"
51
+ COMMIT_SEPARATOR = "\x1e" # Record separator
52
+
53
+
54
+ def parse_commit_line(line: str) -> Commit | None:
55
+ """Parse a single commit from git log output.
56
+
57
+ Args:
58
+ line: Output line from git log with LOG_FORMAT.
59
+
60
+ Returns:
61
+ Parsed Commit or None if parsing fails.
62
+ """
63
+ parts = line.split(FIELD_SEPARATOR)
64
+ if len(parts) < 7:
65
+ return None
66
+
67
+ sha, short_sha, subject, body, author_name, author_email, timestamp_str = parts[:7]
68
+
69
+ try:
70
+ timestamp = int(timestamp_str)
71
+ except ValueError:
72
+ timestamp = 0
73
+
74
+ # Combine subject and body for full message
75
+ message = subject
76
+ if body.strip():
77
+ message = f"{subject}\n\n{body}"
78
+
79
+ return Commit(
80
+ sha=sha,
81
+ short_sha=short_sha,
82
+ message=message,
83
+ author_name=author_name,
84
+ author_email=author_email,
85
+ timestamp=timestamp,
86
+ )
87
+
88
+
89
+ def get_commits(
90
+ cwd: Path,
91
+ since: str | None = None,
92
+ until: str | None = None,
93
+ path: Path | None = None,
94
+ limit: int | None = None,
95
+ ) -> list[Commit]:
96
+ """Get commits from the repository.
97
+
98
+ Args:
99
+ cwd: Working directory.
100
+ since: Start reference (exclusive).
101
+ until: End reference (inclusive). Defaults to HEAD.
102
+ path: Filter to commits affecting this path.
103
+ limit: Maximum number of commits.
104
+
105
+ Returns:
106
+ List of commits (newest first).
107
+ """
108
+ args = ["log", f"--format={LOG_FORMAT}"]
109
+
110
+ if limit:
111
+ args.append(f"-n{limit}")
112
+
113
+ if since and until:
114
+ args.append(f"{since}..{until}")
115
+ elif since:
116
+ args.append(f"{since}..HEAD")
117
+ elif until:
118
+ args.append(until)
119
+
120
+ if path:
121
+ args.extend(["--", str(path)])
122
+
123
+ result = run_git_command(args, cwd=cwd)
124
+
125
+ commits: list[Commit] = []
126
+ # Split by record separator to handle bodies with newlines
127
+ for record in result.stdout.split(COMMIT_SEPARATOR):
128
+ record = record.strip()
129
+ if record:
130
+ commit = parse_commit_line(record)
131
+ if commit:
132
+ commits.append(commit)
133
+
134
+ return commits
135
+
136
+
137
+ def get_commit(cwd: Path, ref: str) -> Commit | None:
138
+ """Get a single commit by reference.
139
+
140
+ Args:
141
+ cwd: Working directory.
142
+ ref: Git reference (SHA, branch, tag).
143
+
144
+ Returns:
145
+ Commit or None if not found.
146
+ """
147
+ args = ["log", "-1", f"--format={LOG_FORMAT}", ref]
148
+
149
+ result = run_git_command(args, cwd=cwd, check=False)
150
+ if result.returncode != 0:
151
+ return None
152
+
153
+ line = result.stdout.strip()
154
+ if line:
155
+ return parse_commit_line(line)
156
+ return None
157
+
158
+
159
+ def get_commits_affecting_path(
160
+ cwd: Path,
161
+ path: Path,
162
+ since: str | None = None,
163
+ ) -> list[Commit]:
164
+ """Get commits that affected a specific path.
165
+
166
+ Args:
167
+ cwd: Working directory.
168
+ path: Path to filter by.
169
+ since: Start reference (exclusive).
170
+
171
+ Returns:
172
+ List of commits affecting the path.
173
+ """
174
+ return get_commits(cwd, since=since, path=path)
pymelos/git/repo.py ADDED
@@ -0,0 +1,210 @@
1
+ """Git repository abstraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from pymelos.errors import GitError
10
+
11
+
12
+ def is_git_repo(path: Path) -> bool:
13
+ """Check if path is inside a git repository.
14
+
15
+ Args:
16
+ path: Path to check.
17
+
18
+ Returns:
19
+ True if path is inside a git repository.
20
+ """
21
+ try:
22
+ result = subprocess.run(
23
+ ["git", "rev-parse", "--git-dir"],
24
+ cwd=path,
25
+ capture_output=True,
26
+ text=True,
27
+ check=False,
28
+ )
29
+ return result.returncode == 0
30
+ except FileNotFoundError:
31
+ return False
32
+
33
+
34
+ def get_repo_root(path: Path) -> Path:
35
+ """Get the root directory of the git repository.
36
+
37
+ Args:
38
+ path: Path inside the repository.
39
+
40
+ Returns:
41
+ Path to repository root.
42
+
43
+ Raises:
44
+ GitError: If not inside a git repository.
45
+ """
46
+ try:
47
+ result = subprocess.run(
48
+ ["git", "rev-parse", "--show-toplevel"],
49
+ cwd=path,
50
+ capture_output=True,
51
+ text=True,
52
+ check=True,
53
+ )
54
+ return Path(result.stdout.strip())
55
+ except subprocess.CalledProcessError as e:
56
+ raise GitError(
57
+ "Not inside a git repository",
58
+ command="git rev-parse --show-toplevel",
59
+ ) from e
60
+ except FileNotFoundError as e:
61
+ raise GitError("Git is not installed") from e
62
+
63
+
64
+ def run_git_command(
65
+ args: list[str],
66
+ cwd: Path | None = None,
67
+ *,
68
+ check: bool = True,
69
+ ) -> subprocess.CompletedProcess[str]:
70
+ """Run a git command synchronously.
71
+
72
+ Args:
73
+ args: Git command arguments (without 'git').
74
+ cwd: Working directory.
75
+ check: Raise on non-zero exit code.
76
+
77
+ Returns:
78
+ Completed process result.
79
+
80
+ Raises:
81
+ GitError: If command fails and check is True.
82
+ """
83
+ cmd = ["git"] + args
84
+
85
+ try:
86
+ result = subprocess.run(
87
+ cmd,
88
+ cwd=cwd,
89
+ capture_output=True,
90
+ text=True,
91
+ check=False,
92
+ )
93
+ if check and result.returncode != 0:
94
+ raise GitError(
95
+ result.stderr.strip() or f"Command failed with exit code {result.returncode}",
96
+ command=" ".join(cmd),
97
+ )
98
+ return result
99
+ except FileNotFoundError as e:
100
+ raise GitError("Git is not installed") from e
101
+
102
+
103
+ async def run_git_command_async(
104
+ args: list[str],
105
+ cwd: Path | None = None,
106
+ *,
107
+ check: bool = True,
108
+ ) -> tuple[int, str, str]:
109
+ """Run a git command asynchronously.
110
+
111
+ Args:
112
+ args: Git command arguments (without 'git').
113
+ cwd: Working directory.
114
+ check: Raise on non-zero exit code.
115
+
116
+ Returns:
117
+ Tuple of (exit_code, stdout, stderr).
118
+
119
+ Raises:
120
+ GitError: If command fails and check is True.
121
+ """
122
+ cmd = ["git"] + args
123
+
124
+ try:
125
+ process = await asyncio.create_subprocess_exec(
126
+ *cmd,
127
+ cwd=cwd,
128
+ stdout=asyncio.subprocess.PIPE,
129
+ stderr=asyncio.subprocess.PIPE,
130
+ )
131
+ stdout_bytes, stderr_bytes = await process.communicate()
132
+ stdout = stdout_bytes.decode("utf-8", errors="replace")
133
+ stderr = stderr_bytes.decode("utf-8", errors="replace")
134
+
135
+ if check and process.returncode != 0:
136
+ raise GitError(
137
+ stderr.strip() or f"Command failed with exit code {process.returncode}",
138
+ command=" ".join(cmd),
139
+ )
140
+
141
+ return process.returncode or 0, stdout, stderr
142
+ except FileNotFoundError as e:
143
+ raise GitError("Git is not installed") from e
144
+
145
+
146
+ def get_current_branch(cwd: Path | None = None) -> str:
147
+ """Get the current git branch name.
148
+
149
+ Args:
150
+ cwd: Working directory.
151
+
152
+ Returns:
153
+ Current branch name.
154
+ """
155
+ result = run_git_command(["rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd)
156
+ return result.stdout.strip()
157
+
158
+
159
+ def get_current_commit(cwd: Path | None = None) -> str:
160
+ """Get the current commit SHA.
161
+
162
+ Args:
163
+ cwd: Working directory.
164
+
165
+ Returns:
166
+ Current commit SHA.
167
+ """
168
+ result = run_git_command(["rev-parse", "HEAD"], cwd=cwd)
169
+ return result.stdout.strip()
170
+
171
+
172
+ def is_clean(cwd: Path | None = None) -> bool:
173
+ """Check if the working directory is clean (no uncommitted changes).
174
+
175
+ Args:
176
+ cwd: Working directory.
177
+
178
+ Returns:
179
+ True if working directory is clean.
180
+ """
181
+ result = run_git_command(["status", "--porcelain"], cwd=cwd, check=False)
182
+ return not result.stdout.strip()
183
+
184
+
185
+ def get_default_branch(cwd: Path | None = None) -> str:
186
+ """Get the default branch name (main or master).
187
+
188
+ Args:
189
+ cwd: Working directory.
190
+
191
+ Returns:
192
+ Default branch name.
193
+ """
194
+ # Try to get from remote
195
+ result = run_git_command(
196
+ ["symbolic-ref", "refs/remotes/origin/HEAD"],
197
+ cwd=cwd,
198
+ check=False,
199
+ )
200
+ if result.returncode == 0:
201
+ # refs/remotes/origin/main -> main
202
+ return result.stdout.strip().split("/")[-1]
203
+
204
+ # Check if main exists
205
+ result = run_git_command(["branch", "--list", "main"], cwd=cwd, check=False)
206
+ if result.stdout.strip():
207
+ return "main"
208
+
209
+ # Default to master
210
+ return "master"