Skip to content
Snippets Groups Projects
  • Richard Antalik's avatar
    2efd9ff4
    Add GPL license block · 2efd9ff4
    Richard Antalik authored
    I have received concern about legality of modifying code without license.
    
    I am not a lawyer, but I can add license that makes it clear that it is
    OK to modify this file.
    2efd9ff4
    History
    Add GPL license block
    Richard Antalik authored
    I have received concern about legality of modifying code without license.
    
    I am not a lawyer, but I can add license that makes it clear that it is
    OK to modify this file.
weekly_report.py 10.33 KiB
#!/usr/bin/env python3

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

import datetime
import ast

# https://github.com/disqus/python-phabricator
from phabricator import Phabricator
phab = Phabricator()  # This will use your ~/.arcrc file
phab.update_interfaces()
me = phab.user.whoami()


class PhabTransaction:
    def __init__(self, story_data, time_start, time_end):
        # Unparsed entries:

        # For paging if necessary, but you can increase query limit.
        self.entry_id = int(story_data['chronologicalKey'])

        self.timestamp = int(story_data['epoch'])
        self.author = story_data['authorPHID']
        self.text = story_data['text']
        # Parsed entries:
        self.object_type = None
        self.object_name = None
        self.object_title = None
        self.action = None
        self.action_effect = ""

        # Parse only transactions from me
        if self.author != me['phid']:
            return None

        # Parse only transactions with date in requested range
        if self.timestamp < time_start or self.timestamp > time_end:
            return None

        # Parse object type (PHID-TASK, PHID-DREV, PHID-CMIT)
        self.object_type = story_data['objectPHID'][:9]

        # Parse `text` var
        # Typical strings are:
        # Richard Antalik (ISS) added a comment to T75265: Movie Clip Editor not loading full image sequences.
        # Richard Antalik (ISS) created D7318: Cleanup: Change callback suffix to `_fn` in sequencer.
        # Richard Antalik (ISS) committed rB188ccfb0dd67: Fix T74662: Prefetching causes random crashes (authored by Richard Antalik (ISS)).
        text_unparsed = story_data['text']

        # Chop off author part
        start = text_unparsed.find(')') + 2
        text_unparsed = text_unparsed[start:]

        # Parse object name (Txxxx, Dxxxx, commit hash).
        if self.object_type == "PHID-TASK":
            start = text_unparsed.find('T')
            end = text_unparsed.find(':')
            self.object_name = text_unparsed[start:end]
        if self.object_type == "PHID-DREV":
            start = text_unparsed.find('D')
            end = text_unparsed.find(':')
            self.object_name = text_unparsed[start:end]
        if self.object_type == "PHID-CMIT":
            start = text_unparsed.find('rB')
            end = text_unparsed.find(':')
            self.object_name = text_unparsed[start:end]
        if self.object_type == "PHID-PSTE":
            start = text_unparsed.find('P')
            end = text_unparsed.find(':')
            self.object_name = text_unparsed[start:end]
        if self.object_type == "PHID-USER":
            self.object_name = "XXX-profile-edit"

        # Parse action - like "added a comment to", "created", ...
        end = start - 1
        self.action = text_unparsed[:end]

        # Parse object title
        start = text_unparsed.find(':') + 2
        end = len(text_unparsed)
        if self.object_type == "PHID-CMIT":
            end = text_unparsed.find('(authored by') - 1
        self.object_title = text_unparsed[start:end]

        # Parse "effect" of action for example action "closed" may have effect of "Invalid" or "Resolved"
        start = text_unparsed.rfind('"', 0, len(text_unparsed) - 3)
        if start != -1:
            self.action_effect = text_unparsed[start + 1:-2]

        if self.object_name is None or self.object_type is None or self.action is None:
            raise ValueError('Can not parse transaction. \n', story_data['text'])

    # Nice for debug
    def to_list(self):
        return [
            self.entry_id,
            self.timestamp,
            self.author,
            self.object_type,
            self.object_name,
            self.action,
            self.action_effect,
            self.object_title,
            self.text,
        ]


def add_to_catalog(catalog, transaction):
    key = transaction.object_name
    if key not in catalog:
        catalog[key] = []
    catalog[key].append(transaction)


def report_personal_weekly_get(time_start, time_end):
    result = phab.feed.query(
        filterPHIDs=[me['phid']],
        limit=500,
        view="text"
    )

    # Strip junk from result, so it can be parsed
    result = str(result)[9:-1]
    # Parse and convert to dict
    stories = ast.literal_eval(result)
    transactions = []

    for story in stories:
        data = stories[story]
        transaction = PhabTransaction(data, time_start, time_end)
        transactions.append(transaction)

    # Make catalogs - per-category view
    tasks = {}
    review = {}
    created_diffs = {}
    commits = {}
    confirmed = {}
    resolved = {}
    invalid = {}
    duplicate = {}
    needs_info = {}
    needs_reproduce = {}

    # Total number of actions.
    task_txn_count = 0

    for transaction in transactions:
        # Skip unparsed.
        if transaction.object_type is None:
            continue

        if transaction.object_type == 'PHID-TASK':
            add_to_catalog(tasks, transaction)
            task_txn_count += 1
        if transaction.object_type == 'PHID-DREV':
            add_to_catalog(review, transaction)  # Own diffs removed lower in code.
            if transaction.action == 'created':
                add_to_catalog(created_diffs, transaction)
        if transaction.object_type == 'PHID-CMIT' and transaction.action == 'committed':
            add_to_catalog(commits, transaction)
        if transaction.action_effect == 'Confirmed':
            add_to_catalog(confirmed, transaction)
        if transaction.action == 'closed' and transaction.action_effect == 'Resolved':
            add_to_catalog(resolved, transaction)
        if transaction.action == 'closed' and transaction.action_effect == 'Invalid':
            add_to_catalog(invalid, transaction)
        if transaction.action == 'merged task':
            add_to_catalog(duplicate, transaction)
        if transaction.action_effect == 'Needs Information from User':
            add_to_catalog(needs_info, transaction)
        if transaction.action_effect == 'Needs Developer to Reproduce':
            add_to_catalog(needs_reproduce, transaction)

    # Get all own diffs
    constraints = {
        "authorPHIDs": [me['userName']],
    }
    result = phab.differential.revision.search(constraints=constraints)
    data = result["data"]

    # Remove own diffs from review "catalog"
    for diff in data:
        diff_id = 'D' + str(diff['id'])
        review.pop(diff_id, None)

    # Get open own diffs
    result = phab.differential.revision.search(queryKey="open", constraints=constraints)
    data = result["data"]

    accepteds = [diff for diff in data if diff['fields']['status']['name'] == "Accepted"]
    needs_reviews = [diff for diff in data if diff['fields']['status']['name'] == "Needs Review"]
    needs_revisions = [diff for diff in data if diff['fields']['status']['name'] == "Needs Revision"]
    changes_planneds = [diff for diff in data if diff['fields']['status']['name'] == "Changes Planned"]
    sum_diffs = len(accepteds) + len(needs_reviews) + len(needs_revisions) + len(changes_planneds)

    # Print triaging stats
    print("\'\'\'Involved in %s reports:\'\'\'" % len(tasks))
    print("* Confirmed: %s" % len(confirmed))
    print("* Closed as Resolved: %s" % len(resolved))
    print("* Closed as Invalid: %s" % len(invalid))
    print("* Closed as Duplicate: %s" % len(duplicate))
    print("* Needs Info from User: %s" % len(needs_info))
    print("* Needs Developer to Reproduce: %s" % len(needs_reproduce))
    print("* Actions total: %s" % task_txn_count)
    print()

    # Print review stats
    print("'''Review: %s'''" % len(review))
    for key in review:
        txn = review[key][0]
        print("* %s [https://developer.blender.org/%s %s]" % (txn.text, txn.object_name, txn.object_name))
    print()

    # Print created diffs
    print("'''Created patches: %s'''" % len(created_diffs))
    for key in created_diffs:
        txn = created_diffs[key][0]
        print("* %s [https://developer.blender.org/%s %s]" % (txn.object_title, txn.object_name, txn.object_name))
    print()

    # Print open diffs
    print("\'\'\'Own solutions in review: %s\'\'\'" % sum_diffs)
    for diff in sorted(data, key=lambda i: i['fields']['status']['name']):
        if diff["fields"]["status"]['closed']:
            continue
        diff_title = diff["fields"]["title"]
        diff_status = diff["fields"]["status"]['name']
        print("* %s: %s [https://developer.blender.org/D%s D%s]" % (diff_status, diff_title, diff['id'], diff['id']))
    print()

    # Print commits
    print("'''Commits:'''")
    for key in commits:
        txn = commits[key][0]
        print("* %s {{GitCommit|%s}}" % (txn.object_title, txn.object_name))


def main():
    #end_date = datetime.datetime(2020, 3, 14)
    end_date = datetime.datetime.now()
    weekday = end_date.weekday()

    # Assuming I am lazy and making this at last moment or even later in worst case
    if weekday < 2:
        time_delta = 7 + weekday
        start_date = end_date - datetime.timedelta(days=time_delta, hours=end_date.hour)
        end_date -= datetime.timedelta(days=weekday, hours=end_date.hour)
    else:
        time_delta = weekday
        start_date = end_date - datetime.timedelta(days=time_delta, hours=end_date.hour)

    # Ensure friday :)
    firday = start_date + datetime.timedelta(days=4)
    week = start_date.isocalendar()[1]
    start_date_str = start_date.strftime('%b %d')
    end_date_str = firday.strftime('%b %d')

    print("== Week %d (%s - %s) ==\n\n" % (week, start_date_str, end_date_str))
    report_personal_weekly_get(int(start_date.timestamp()), int(end_date.timestamp()))


if __name__ == "__main__":
    main()

    # wait for input to close window
    input()