Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 165 additions & 5 deletions scripts/sprint-overview
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ DISPLAY_SECTIONS = [
("closed-pull", "Closed Pull Requests"),
]

DISPLAY_COMPARISON_SECTIONS = [
('completed', 'Completed'),
('pending', 'Pending'),
('added', 'Added'),
('removed', 'Removed'),
('completed-original', 'Completed original items'),
('incomplete-original', 'Incomplete original items'),
('complete-added', 'Completed added items'),
('incomplete-added', 'Incompleted added items'),
]

DISPLAY_TEMPLATE = jinja2.Template(
trim_blocks=True,
source="""
Expand Down Expand Up @@ -75,6 +86,32 @@ DISPLAY_TEMPLATE = jinja2.Template(
)


DISPLAY_COMPARISON_TEMPLATE = jinja2.Template(
trim_blocks=True,
source="""
================================================================================
Sprint: {{ sprint_name }}
Total items: {{ count_base }} => {{ count_current }}
Total story points: {{ size_base }} => {{ size_current }}
Completed items: {{ count_completed }} vs {{ count_pending }}
Completed story points: {{ size_completed }} vs {{ size_pending }}
Added/removed items: {{ count_added }} vs {{ count_removed }}
Added/removed story points: {{ size_added }} vs {{ size_removed }}
================================================================================
{% for key, label in sections %}
{% if categories.get(key) %}

{{ label }} ({{ categories[key] | length }}) - {{ categories[key] | sum(attribute="safe_size") }} points
--------------------------------------------------------------------------------
{% for item in categories[key] %}
{{ item }}
{% endfor %}
{% endif %}
{% endfor %}
""", # noqa: E501
)


@dataclasses.dataclass
class Item:
"""
Expand All @@ -89,6 +126,10 @@ class Item:
url: str
title: str

@property
def safe_size(self) -> int:
return self.size or 0

def __str__(self) -> str:
size_str = f"[{self.size}]" if self.size is not None else "[-]"
identifier = f"{self.repo}#{self.id}"
Expand Down Expand Up @@ -232,6 +273,95 @@ def display_items(sprint_name: str, items: list[Item]) -> None:
)


def display_comparison(sprint_name: str, base: list[Item], current: list[Item]) -> None:
"""
Display difference between two sets of items in a readable format.

:param base: list of the "base", initial items, representing a past
state of the sprint.
:param items: list of the current items, representing the current
state of the sprint.
"""

base.sort(key=lambda x: x.id)

base_map = {item.id: item for item in base}
current_map = {item.id: item for item in current}

completed: list[Item] = []
pending: list[Item] = []
added: list[Item] = []
removed: list[Item] = []

for base_id, base_item in base_map.items():
current_item = current_map.pop(base_id, None)

if current_item is None:
removed.append(base_item)

elif current_item.status in ('closed', 'merged'):
completed.append(base_item)

else:
pending.append(base_item)

for current_item in current_map.values():
added.append(current_item)

if current_item.status in ('closed', 'merged'):
completed.append(current_item)

else:
pending.append(current_item)

def size(items: list[Item]) -> int:
return sum(item.size or 0 for item in items)

count_base, count_current, size_base, size_current = (
len(base),
len(current),
size(base),
size(current),
)
count_completed, count_pending, size_completed, size_pending = (
len(completed),
len(pending),
size(completed),
size(pending),
)
count_added, size_added = len(added), size(added)
count_removed, size_removed = len(removed), size(removed)

click.echo(
DISPLAY_COMPARISON_TEMPLATE.render(
sections=DISPLAY_COMPARISON_SECTIONS,
categories={
'completed': completed,
'pending': pending,
'added': added,
'removed': removed,
'completed-original': [item for item in completed if item not in added],
'incomplete-original': [item for item in pending if item not in added],
'complete-added': [item for item in added if item in completed],
'incomplete-added': [item for item in added if item not in completed],
},
sprint_name=sprint_name,
count_base=count_base,
count_current=count_current,
size_base=size_base,
size_current=size_current,
count_completed=count_completed,
count_pending=count_pending,
size_completed=size_completed,
size_pending=size_pending,
count_added=count_added,
size_added=size_added,
count_removed=count_removed,
size_removed=size_removed,
)
)


@click.command()
@click.option(
'--sprint',
Expand All @@ -245,20 +375,50 @@ def display_items(sprint_name: str, items: list[Item]) -> None:
is_flag=True,
help='Output in YAML format for machine-readable processing.',
)
def main(sprint: str, output_yaml: bool) -> None:
@click.option(
'--base',
'base_path',
default=None,
help='Instead of reporting, compare the current sprint against this saved state.',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a metavar with PATH? The default TEXT is a bit confusing:

  --base TEXT     Instead of reporting, compare the current sprint against
                  this saved state.
  --current TEXT  If set, compare --base against --current instead of the live
                  sprint state.

Similar below.

)
@click.option(
'--current',
'current_path',
default=None,
help='If set, compare --base against --current instead of the live sprint state.',
)
Comment thread
happz marked this conversation as resolved.
def main(
sprint: str,
output_yaml: bool,
base_path: Optional[str] = None,
current_path: Optional[str] = None,
) -> None:
"""
List all issues and pull requests in a GitHub Project sprint with story points.

Set the GITHUB_PERSONAL_ACCESS_TOKEN environment variable to avoid rate limits.
"""
items = fetch_sprint_items(sprint)

if not items:
click.echo(f"No items found in sprint '{sprint}'.")
return
if current_path is not None:
with open(current_path) as f:
items = YAML().load(f)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
items = YAML().load(f)
items = [Item(**item) for item in YAML().load(f)]

We need to unpack these too, right? Otherwise I get:

  File "/home/psss/git/tmt/./scripts/sprint-overview", line 289, in display_comparison
    current_map = {item.id: item for item in current}
                   ^^^^^^^
AttributeError: 'CommentedMap' object has no attribute 'id'


else:
items = fetch_sprint_items(sprint)

if not items:
click.echo(f"No items found in sprint '{sprint}'.")
return

if output_yaml:
YAML().dump([dataclasses.asdict(item) for item in items], sys.stdout)

elif base_path is not None:
with open(base_path) as f:
base = [Item(**item) for item in YAML().load(f)]

display_comparison(sprint, base, items)

else:
display_items(sprint, items)

Expand Down
Loading