diff --git a/.gitignore b/.gitignore index 894a44c..3b37a32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# My credentials +creds.json + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -102,3 +105,6 @@ venv.bak/ # mypy .mypy_cache/ + +.idea/ +*.iml diff --git a/column_mover.py b/column_mover.py new file mode 100644 index 0000000..ee33a98 --- /dev/null +++ b/column_mover.py @@ -0,0 +1,72 @@ +from lib import Client + + +class Checker(): + def __init__(self, work, client): + self.work = work + self.client = client + + def phid_check(self, phid): + if self.work.get('projects'): + return self.phid_check_project( + phid, [ + self.client.lookupPhid( + '#' + i) for i in self.work['projects']]) + if self.work.get('status'): + return self.phid_check_status(phid, self.work['status']) + return False + + def phid_check_project(self, phid, project_phids): + taskDetails = self.client.taskDetails(phid) + for project_phid in project_phids: + if project_phid in taskDetails['projectPHIDs']: + return True + return False + + def phid_check_status(self, phid, statuses): + taskDetails = self.client.taskDetails(phid) + return taskDetails['statusName'] in statuses + + +client = Client.newFromCreds() + +work = [{'from': ['incoming'], + 'project': 'Wikidata', + 'to': 'in progress', + 'projects': ['wikidata-campsite-iteration-∞', + 'RL_Module_Terminators_Trailblazing', + 'wikidata-bridge-sprint-8']}, + {'from': ['Incoming'], + 'project': 'User-Ladsgroup', + 'to': 'In progress', + 'projects': ['wikidata-campsite-iteration-∞', + 'RL_Module_Terminators_Trailblazing']}, + {'from': ['In progress', + 'Incoming'], + 'project': 'User-Ladsgroup', + 'to': 'Done', + 'status': ['Resolved']}, + {'from': ['Unsorted'], + 'project': 'user-dannys712', + 'to': 'Global Watchlist', + 'projects': ['DannyS712-Global_watchlist.js']}, + ] +for case in work: + gen = client.getTasksWithProject(client.lookupPhid('#' + case['project'])) + checker = Checker(case, client) + columns = client.getColumns(client.lookupPhid('#' + case['project'])) + mapping = {} + for column in columns['data']: + mapping[column['fields']['name']] = column['phid'] + for phid in gen: + if checker.phid_check(phid): + project_phid = client.lookupPhid('#' + case['project']) + currentColumnName = client.getTaskColumns( + phid)['boards'][project_phid]['columns'][0]['name'] + if currentColumnName not in case['from']: + continue + try: + print(phid) + client.moveColumns(phid, mapping[case['to']]) + except KeyboardInterrupt: + continue diff --git a/lib.py b/lib.py new file mode 100644 index 0000000..985dee5 --- /dev/null +++ b/lib.py @@ -0,0 +1,123 @@ +import json +import time + +import requests + + +class Client(object): + """Phabricator client""" + + def __init__(self, url, username, key): + self.url = url + self.username = username + self.column_cache = {} + self.phid_cache = {} + self.session = { + 'token': key, + } + + @classmethod + def newFromCreds(cls): + with open('creds.json', 'r') as f: + creds = json.loads(f.read()) + return cls(*creds) + + def post(self, path, data): + data['__conduit__'] = self.session + r = requests.post('%s/api/%s' % (self.url, path), data={ + 'params': json.dumps(data), + 'output': 'json', + }) + resp = r.json() + if resp['error_code'] is not None: + raise Exception(resp['error_info']) + return resp['result'] + + def lookupPhid(self, label): + """Lookup information on a Phab object by name.""" + if not self.phid_cache.get(label): + r = self.post('phid.lookup', {'names': [label]}) + if label in r and 'phid' in r[label]: + obj = r[label]['phid'] + self.phid_cache[label] = obj + else: + raise Exception('No object found for %s' % label) + return self.phid_cache[label] + + def getColumns(self, project_phid): + if not self.column_cache.get(project_phid): + self.column_cache[project_phid] = self.post( + 'project.column.search', { + "constraints": { + "projects": [project_phid]}}) + return self.column_cache[project_phid] + + def moveColumns(self, task_phid, to_column): + self.post('maniphest.edit', { + 'objectIdentifier': task_phid, + 'transactions': [{ + 'type': 'column', + 'value': [to_column], + }] + }) + + def taskDetails(self, phid): + """Lookup details of a Maniphest task.""" + r = self.post('maniphest.query', {'phids': [phid]}) + if phid in r: + return r[phid] + raise Exception('No task found for phid %s' % phid) + + def getTransactions(self, phid): + r = self.post('transaction.search', {'objectIdentifier': phid}) + if 'data' in r: + return r['data'] + raise Exception('No transaction found for phid %s' % phid) + + def removeProject(self, project_phid, task): + return self.removeProjectByPhid(project_phid, self.lookupPhid(task)) + + def removeProjectByPhid(self, project_phid, task_phid): + self.post('maniphest.edit', { + 'objectIdentifier': task_phid, + 'transactions': [{ + 'type': 'projects.remove', + 'value': [project_phid], + }] + }) + + def getTasksWithProject(self, project_phid, continue_=None): + r = self._getTasksWithProjectContinue(project_phid, continue_) + cursor = r['cursor'] + for case in r['data']: + if case['type'] != 'TASK': + continue + yield case['phid'] + if cursor.get('after'): + for case in self.getTasksWithProject( + project_phid, cursor['after']): + yield case + + def _getTasksWithProjectContinue(self, project_phid, continue_=None): + params = { + 'limit': 100, + 'constraints': { + 'projects': [project_phid], + "modifiedStart": int(time.time() - 3600) + } + } + if continue_: + params['after'] = continue_ + return self.post('maniphest.search', params) + + def getTaskColumns(self, phid): + params = { + "attachments": { + "columns": {"boards": {"columns": True}} + }, + "constraints": { + "phids": [phid] + } + } + return self.post('maniphest.search', params)[ + 'data'][0]['attachments']['columns'] diff --git a/patchforreview_remover.py b/patchforreview_remover.py new file mode 100644 index 0000000..11337d7 --- /dev/null +++ b/patchforreview_remover.py @@ -0,0 +1,70 @@ +import re +import time +from collections import defaultdict + +from lib import Client + + +class Checker(): + def __init__(self, gerrit_bot_phid, project_patch_for_review_phid, client): + self.gerrit_bot_phid = gerrit_bot_phid + self.project_patch_for_review_phid = project_patch_for_review_phid + self.client = client + + def check(self, t_id): + phid = self.client.lookupPhid(t_id) + return self.phid_check(phid) + + def phid_check(self, phid): + gerrit_bot_actions = [] + for transaction in self.client.getTransactions(phid): + if 'https://github.com/' in str(transaction): + return False + if transaction['authorPHID'] == self.gerrit_bot_phid: + gerrit_bot_actions.append(transaction) + else: + if transaction['type'] == 'projects': + check = self.project_patch_for_review_phid in str( + transaction['fields']) + add_check = "'add'" in str(transaction['fields']) + if check and add_check: + return False + + gerrit_patch_status = defaultdict(list) + for case in gerrit_bot_actions: + if case['type'] != 'comment': + continue + + if len(case['comments']) != 1: + return False + raw_comment = case['comments'][0]['content']['raw'] + gerrit_patch_id = re.findall( + r'https\:\/\/gerrit\.wikimedia\.org\/r\/(\d+)', raw_comment)[0] + merged = re.findall( + r'Change \d+ (?:\*\*merged\*\*|abandoned) by ', + raw_comment) + + gerrit_patch_status[gerrit_patch_id].append(not(bool(merged))) + + for patch in gerrit_patch_status: + if gerrit_patch_status[patch] != [False, True]: + return False + return True + + +client = Client.newFromCreds() + +project_patch_for_review_phid = 'PHID-PROJ-onnxucoedheq3jevknyr' +checker = Checker( + 'PHID-USER-idceizaw6elwiwm5xshb', + project_patch_for_review_phid, + client) +gen = client.getTasksWithProject(project_patch_for_review_phid) +for phid in gen: + if checker.phid_check(phid): + print(client.taskDetails(phid)['id']) + try: + client.removeProjectByPhid(project_patch_for_review_phid, phid) + except BaseException: + continue + time.sleep(10)