mirror of https://github.com/zeldaret/tp.git
546 lines
18 KiB
Python
546 lines
18 KiB
Python
import yaml
|
|
|
|
from .issue import *
|
|
from .field import *
|
|
from typing import Optional
|
|
|
|
class Project:
|
|
def __eq__(self, other):
|
|
if isinstance(other, Label):
|
|
return self.title == other.name and self.id == other.id
|
|
return False
|
|
|
|
def __hash__(self):
|
|
return hash((self.title, self.id))
|
|
|
|
def __init__(self, id=None, title=None, number=None, items=None, items_to_attach=None, status_field=None, data=None):
|
|
if data is not None:
|
|
self.id = None
|
|
self.title = data.get("project").get('title', 'MISSING_TITLE')
|
|
self.number = None
|
|
self.items = None
|
|
self.items_to_attach = None
|
|
self.status_field = None
|
|
else:
|
|
self.id = id
|
|
self.title = title
|
|
self.number = number
|
|
self.items = items
|
|
self.items_to_attach = items_to_attach
|
|
self.status_field = status_field
|
|
|
|
@staticmethod
|
|
def get_all_from_yaml(data) -> list['Project']:
|
|
ret_projects = []
|
|
issues_dict = {issue['file_path']: issue['id'] for issue in StateFile.data["issues"]}
|
|
|
|
for d in data:
|
|
items = []
|
|
|
|
for tu, _, file_path in get_translation_units(d):
|
|
if file_path in issues_dict:
|
|
LOG.debug(f'Issue {tu} found in state file.')
|
|
LOG.debug(f'Adding ID {issues_dict[file_path]} to items.')
|
|
|
|
items.append({
|
|
"issue_id": issues_dict[file_path]
|
|
})
|
|
else:
|
|
LOG.error(f'Issue {tu} not found in state file. Please run ./tp github-sync-issues first.')
|
|
sys.exit(1)
|
|
|
|
project = Project(
|
|
id=None,
|
|
title=d['project']['title'],
|
|
number=None,
|
|
items=[],
|
|
items_to_attach=items
|
|
)
|
|
|
|
ret_projects.append(project)
|
|
return ret_projects
|
|
|
|
@staticmethod
|
|
def get_all_from_github() -> list['Project']:
|
|
LOG.debug(f'Getting projects on {RepoInfo.owner.name}/{RepoInfo.name}')
|
|
query = '''
|
|
query ($owner: String!, $repo: String!, $cursor: String) {
|
|
repository(owner: $owner, name: $repo) {
|
|
projectsV2(first: 20) {
|
|
nodes {
|
|
id
|
|
title
|
|
number
|
|
|
|
items(first: 100, after: $cursor) {
|
|
pageInfo {
|
|
endCursor
|
|
hasNextPage
|
|
}
|
|
edges {
|
|
cursor
|
|
node {
|
|
id
|
|
content {
|
|
... on Issue {
|
|
id
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
variables = {
|
|
"owner": RepoInfo.owner.name,
|
|
"repo": RepoInfo.name,
|
|
"cursor": None,
|
|
}
|
|
|
|
ret_projects = []
|
|
while True:
|
|
data = GraphQLClient.get_instance().make_request(query, variables)
|
|
if data:
|
|
projects = data['data']['repository']['projectsV2']['nodes']
|
|
LOG.debug(f'Projects retrieved: {projects}')
|
|
|
|
for project in projects:
|
|
items = []
|
|
|
|
for edge in project['items']['edges']:
|
|
LOG.debug(f'Item: {edge}')
|
|
item_id = edge['node']['id']
|
|
issue_id = edge['node']['content']['id']
|
|
|
|
item = ProjectItem(
|
|
id=item_id,
|
|
issue_id = issue_id,
|
|
)
|
|
|
|
items.append(item)
|
|
|
|
ret_project = Project(
|
|
id=project['id'],
|
|
title=project['title'],
|
|
number=project['number'],
|
|
items=items
|
|
)
|
|
|
|
ret_projects.append(ret_project)
|
|
|
|
# Check if there are more items to fetch
|
|
if len(projects) == 0 or not data['data']['repository']['projectsV2']['nodes'][0]['items']['pageInfo']['hasNextPage']:
|
|
break
|
|
|
|
# Update the cursor to the last item's cursor for the next fetch
|
|
variables['cursor'] = data['data']['repository']['projectsV2']['nodes'][0]['items']['pageInfo']['endCursor']
|
|
else:
|
|
LOG.warning("No projects found!")
|
|
break
|
|
|
|
return ret_projects
|
|
|
|
|
|
@staticmethod
|
|
def get_project_by_name(project_name: str) -> Optional['Project']:
|
|
all_projects = Project.get_all_from_github()
|
|
if all_projects:
|
|
for project in all_projects:
|
|
if project.title == project_name:
|
|
return project
|
|
else:
|
|
LOG.warning(f'No projects found in {RepoInfo.owner.name}/{RepoInfo.name}')
|
|
return None
|
|
|
|
@staticmethod
|
|
def delete_all():
|
|
LOG.debug(f'Deleting all projects in {RepoInfo.owner.name}/{RepoInfo.name}')
|
|
project_state = StateFile.data["projects"]
|
|
|
|
if project_state is not None and len(project_state) > 0:
|
|
for project in project_state.copy():
|
|
Project(
|
|
id=project["id"],
|
|
title=project["title"]
|
|
).delete()
|
|
else:
|
|
LOG.warning(f'No projects found in state file. Nothing to delete.')
|
|
return
|
|
|
|
def create(self) -> None:
|
|
owner_id = RepoInfo.owner.id
|
|
repo_id = RepoInfo.id
|
|
|
|
if not owner_id or not repo_id:
|
|
return
|
|
|
|
LOG.debug(f'Creating Github project {self.title}')
|
|
|
|
mutation = '''
|
|
mutation ($ownerId: ID!, $repoId: ID!, $projectName: String!) {
|
|
createProjectV2(input: { ownerId: $ownerId, repositoryId: $repoId, title: $projectName }) {
|
|
projectV2 {
|
|
id
|
|
number
|
|
title
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
variables = {
|
|
"ownerId": owner_id,
|
|
"repoId": repo_id,
|
|
"projectName": self.title
|
|
}
|
|
|
|
data = GraphQLClient.get_instance().make_request(mutation, variables)
|
|
if data:
|
|
self.id = data['data']['createProjectV2']['projectV2']['id']
|
|
self.number = data['data']['createProjectV2']['projectV2']['number']
|
|
self.status_field = Field.get_status_field(self.id)
|
|
self.write_state_to_file()
|
|
self.set_public()
|
|
|
|
LOG.info(f"Successfully created project '{self.title}' with ID {self.id} and number {self.number}")
|
|
else:
|
|
LOG.error(f'Failed to create project {self.title}')
|
|
sys.exit(1)
|
|
|
|
def check_and_create(self) -> None:
|
|
projects = StateFile.data.get('projects')
|
|
|
|
if projects is None:
|
|
project_dict = {}
|
|
else:
|
|
project_dict = {project['title']: project for project in projects}
|
|
|
|
if self.title in project_dict:
|
|
LOG.info(f'Project {self.title} already exists')
|
|
|
|
|
|
self.id = project_dict[self.title]["id"]
|
|
self.number = project_dict[self.title]["number"]
|
|
self.items = project_dict[self.title]["items"]
|
|
self.status_field = project_dict[self.title]["status_field"]
|
|
|
|
missing_issue_ids = [item['issue_id'] for item in self.items_to_attach if item['issue_id'] not in {item['issue_id'] for item in self.items}]
|
|
|
|
LOG.info(f'Attaching missing issues to project {self.title}')
|
|
if len(missing_issue_ids) > 0:
|
|
for id in missing_issue_ids:
|
|
self.attach_issue(id)
|
|
else:
|
|
LOG.info(f'All issues already attached to project {self.title}')
|
|
else:
|
|
LOG.info(f'Creating missing project {self.title}')
|
|
|
|
self.create()
|
|
for item in self.items_to_attach:
|
|
self.attach_issue(item["issue_id"])
|
|
|
|
def attach_issue(self, issue_id) -> None:
|
|
LOG.debug(f'Attaching issue {issue_id} to project {self.title}')
|
|
mutation = """
|
|
mutation AddProjectV2ItemById($input: AddProjectV2ItemByIdInput!) {
|
|
addProjectV2ItemById(input: $input) {
|
|
clientMutationId
|
|
|
|
item {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
variables = {
|
|
"input": {
|
|
"projectId": self.id,
|
|
"contentId": issue_id,
|
|
"clientMutationId": "UNIQUE_ID"
|
|
}
|
|
}
|
|
|
|
data = GraphQLClient.get_instance().make_request(mutation, variables)
|
|
if data:
|
|
LOG.debug(f'Issue {issue_id} attached to project {self.title}')
|
|
item_id = data['data']['addProjectV2ItemById']['item']['id']
|
|
self.items.append({
|
|
"issue_id": issue_id,
|
|
"item_id": item_id
|
|
})
|
|
self.write_state_to_file()
|
|
else:
|
|
LOG.error(f'Failed to attach issue {issue_id} to project {self.title}')
|
|
sys.exit(1)
|
|
|
|
def get_item_id_from_issue(self, issue: Issue) -> str:
|
|
LOG.debug(f'Getting item ID for issue {issue.title} in project {self.title}')
|
|
|
|
query = """
|
|
query ($projectId: ID!, $issueId: ID!) {
|
|
projectV2Item(projectId: $projectId, contentId: $issueId) {
|
|
id
|
|
}
|
|
}
|
|
"""
|
|
|
|
variables = {
|
|
"projectId": self.id,
|
|
"issueId": issue.id
|
|
}
|
|
|
|
data = GraphQLClient.get_instance().make_request(query, variables)
|
|
if data:
|
|
LOG.debug(f'Got item ID for issue {issue.title} in project {self.title}')
|
|
return data['data']['projectV2Item']['id']
|
|
else:
|
|
LOG.error(f'Failed to get item ID for issue {issue.title} in project {self.title}')
|
|
sys.exit(1)
|
|
|
|
def delete(self) -> None:
|
|
query = '''
|
|
mutation ($projectId: ID!) {
|
|
deleteProjectV2(input: {
|
|
projectId: $projectId
|
|
}) {
|
|
clientMutationId
|
|
}
|
|
}
|
|
'''
|
|
|
|
variables = {
|
|
"projectId": self.id
|
|
}
|
|
|
|
data = GraphQLClient.get_instance().make_request(query, variables)
|
|
if data:
|
|
self.write_state_to_file(delete=True)
|
|
LOG.info(f'Successfully deleted project {self.title}.')
|
|
|
|
else:
|
|
LOG.error(f'Failed to delete project {self.title}')
|
|
sys.exit(1)
|
|
|
|
def set_public(self) -> None:
|
|
query = '''
|
|
mutation UpdateProjectVisibility($input: UpdateProjectV2Input!) {
|
|
updateProjectV2(input: $input) {
|
|
projectV2 {
|
|
id
|
|
title
|
|
public
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
|
|
variables = {
|
|
"input": {
|
|
"projectId": self.id,
|
|
"public": True
|
|
}
|
|
}
|
|
|
|
data = GraphQLClient.get_instance().make_request(query, variables)
|
|
if data:
|
|
LOG.info(f'Successfully set project {self.title} to public.')
|
|
else:
|
|
LOG.error(f'Failed to set project {self.title} to public.')
|
|
sys.exit(1)
|
|
|
|
def set_status_for_item(self, item_id: str, status: str) -> None:
|
|
query = '''
|
|
mutation updateProjectV2ItemFieldValue($input: UpdateProjectV2ItemFieldValueInput!) {
|
|
updateProjectV2ItemFieldValue(input: $input) {
|
|
projectV2Item {
|
|
databaseId
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
|
|
options = self.status_field.options
|
|
option = next((option for option in options if option.name.lower() == status.lower()), None)
|
|
|
|
variables = {
|
|
"input": {
|
|
"projectId": self.id,
|
|
"itemId": item_id,
|
|
"fieldId": self.status_field.id,
|
|
"value": {
|
|
"singleSelectOptionId": option.id
|
|
}
|
|
}
|
|
}
|
|
|
|
data = GraphQLClient.get_instance().make_request(query, variables)
|
|
if data:
|
|
LOG.info(f'Successfully set status for item {item_id} to {status}')
|
|
else:
|
|
LOG.error(f'Failed to set status for item {item_id} to {status}')
|
|
sys.exit(1)
|
|
|
|
def set_id(self) -> None:
|
|
LOG.debug(f'Getting ID for project {self.title}')
|
|
|
|
query = '''
|
|
query ($owner: String!, $name: String!, $projectName: String!) {
|
|
repository(owner: $owner, name: $name) {
|
|
projectsV2(query: $projectName, first: 1) {
|
|
nodes {
|
|
id
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
|
|
variables = {
|
|
"owner": RepoInfo.owner.name,
|
|
"name": RepoInfo.name,
|
|
"projectName": self.title
|
|
}
|
|
|
|
data = GraphQLClient.get_instance().make_request(query, variables)
|
|
if data:
|
|
projects = data['data']['repository']['projectsV2']['nodes']
|
|
for project in projects:
|
|
if project['title'] == self.title:
|
|
project_id = project['id']
|
|
LOG.info(f'Project ID for {self.title}: {project_id}')
|
|
self.id = project_id
|
|
else:
|
|
LOG.critical(f'No project found with title {self.title}')
|
|
sys.exit(1)
|
|
else:
|
|
LOG.critical(f'Query failed.')
|
|
sys.exit(1)
|
|
|
|
def set_items(self) -> None:
|
|
query = '''
|
|
query ($owner: String!, $repo: String!, $cursor: String) {
|
|
repository(owner: $owner, name: $repo) {
|
|
projectsV2(first: 20) {
|
|
nodes {
|
|
items(first: 100, after: $cursor) {
|
|
pageInfo {
|
|
endCursor
|
|
hasNextPage
|
|
}
|
|
edges {
|
|
cursor
|
|
node {
|
|
id
|
|
content {
|
|
... on Issue {
|
|
id
|
|
title
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
variables = {
|
|
"owner": RepoInfo.owner.name,
|
|
"repo": RepoInfo.name,
|
|
"cursor": None,
|
|
}
|
|
|
|
self.items = []
|
|
while True:
|
|
data = GraphQLClient.get_instance().make_request(query, variables)
|
|
|
|
if data:
|
|
projects = data['data']['repository']['projectsV2']['nodes']
|
|
|
|
for project in projects:
|
|
for edge in project['items']['edges']:
|
|
item_id = edge['node']['id']
|
|
issue_id = edge['node']['content']['id']
|
|
issue_title = edge['node']['content']['title']
|
|
|
|
item = {
|
|
'id': item_id,
|
|
'issue_id': issue_id,
|
|
'issue_title': issue_title
|
|
}
|
|
|
|
self.items.append(item)
|
|
|
|
# Check if there are more items to fetch
|
|
if not data['data']['repository']['projectsV2']['nodes'][0]['items']['pageInfo']['hasNextPage']:
|
|
break
|
|
|
|
# Update the cursor to the last item's cursor for the next fetch
|
|
variables['cursor'] = data['data']['repository']['projectsV2']['nodes'][0]['items']['pageInfo']['endCursor']
|
|
else:
|
|
break
|
|
|
|
def write_state_to_file(self, delete: bool = False):
|
|
state = {
|
|
"title": self.title,
|
|
"id": self.id,
|
|
"number": self.number,
|
|
"items": self.items,
|
|
"status_field": self.status_field
|
|
}
|
|
|
|
curr_state_projects = StateFile.data.get("projects", None)
|
|
if curr_state_projects is not None:
|
|
for i, project in enumerate(curr_state_projects):
|
|
if project['id'] == self.id:
|
|
if delete:
|
|
del StateFile.data['projects'][i]
|
|
break
|
|
else:
|
|
StateFile.data['projects'][i] = state
|
|
break
|
|
else:
|
|
StateFile.data['projects'].append((state))
|
|
else:
|
|
StateFile.data['projects'] = [state]
|
|
|
|
|
|
with open("tools/pjstate.yml", 'w') as f:
|
|
yaml.safe_dump(StateFile.data, f)
|
|
|
|
# Custom representer for Option
|
|
def option_representer(dumper, data):
|
|
return dumper.represent_mapping('!Option', {
|
|
'id': data.id,
|
|
'name': data.name
|
|
})
|
|
|
|
# Custom constructor for Option
|
|
def option_constructor(loader, node):
|
|
values = loader.construct_mapping(node)
|
|
return Option(values['id'], values['name'])
|
|
|
|
# Custom representer for Field
|
|
def field_representer(dumper, data):
|
|
return dumper.represent_mapping('!Field', {
|
|
'id': data.id,
|
|
'name': data.name,
|
|
'options': data.options
|
|
})
|
|
|
|
# Custom constructor for Field
|
|
def field_constructor(loader, node):
|
|
values = loader.construct_mapping(node)
|
|
return Field(values['id'], values['name'], values['options'])
|
|
|
|
# Register the custom representers and constructors with SafeDumper
|
|
yaml.add_representer(Option, option_representer, Dumper=yaml.SafeDumper)
|
|
yaml.add_constructor('!Option', option_constructor, Loader=yaml.SafeLoader)
|
|
yaml.add_representer(Field, field_representer, Dumper=yaml.SafeDumper)
|
|
yaml.add_constructor('!Field', field_constructor, Loader=yaml.SafeLoader) |