#!/usr/bin/env python3 import requests from requests import get from requests.exceptions import ConnectTimeout, ConnectionError, HTTPError from requests.packages.urllib3.exceptions import InsecureRequestWarning from datetime import datetime from signal import signal, SIGINT, SIGTSTP, SIG_IGN from subprocess import run, PIPE from shlex import split from argparse import ArgumentParser from sys import exit RED = '\033[1;31m' GRE = '\033[1;32m' YEL = '\033[0;33m' BLU = '\033[1;34m' PUR = '\033[1;35m' CYA = '\033[1;36m' BOLD = '\033[1m' DEF = '\033[0m' BASE_URL = 'https://cse320.starkeffect.com:3000/submit' server_timeout = None def yn_prompt(): prompt = input('{}Continue? Please enter Y/n{}: '.format(PUR, DEF)) while prompt != 'Y' and prompt.lower() != 'n': prompt = input('{}Continue? Please enter Y/n{}: '.format(PUR, DEF)) if prompt != 'Y': print('{}Aborting...{}'.format(YEL, DEF)) exit(1) def first_api_call(): global server_timeout try: res = get(BASE_URL + '/timeout', params=None, timeout=15, verify=False) res_json = res.json() server_timeout = res_json['timeout'] except (ConnectionError, ConnectTimeout, HTTPError): print('Error connecting to the server. Try again. (FIRST_SERVER_CALL)') exit(1) def initial_message(): print('Only commits made from master or merged into master will be ', 'considered. If you would like to submit a commit from a different ', 'branch, please merge the branch into master first.') yn_prompt() def api_call(endpoint, query): global server_timeout try: res = get(BASE_URL + endpoint, params=query, timeout=server_timeout, verify=False) except (ConnectionError, ConnectTimeout, HTTPError): print('Error connecting to the server. Try again. ', '(SERVER_CONNECTION_ERROR)') exit(1) res_json = res.json() if 'errmsg' in res_json: print(res_json['errmsg']) exit(1) return res_json def validate_git_directory(): cmd = split('git remote show origin') proc = run(cmd, stdout=PIPE) if proc.returncode: exit(proc.returncode) # You are not expected to understand this return [line for line in proc.stdout.decode().split('\n') if line.strip().startswith('Push')][0].split('/')[-1].split('.')[0] def validate_commit(commit): # check current user branch branch_cmd = split('git rev-parse --abbrev-ref HEAD') branch_proc = run(branch_cmd, stdout=PIPE, stderr=PIPE) if branch_proc.returncode: print('{}Error{}: Uninitialized repository. Please initialize your repo.'.format(RED, DEF)) exit(branch_proc.returncode) current_branch = [line for line in branch_proc.stdout.decode().split('\n')][0] if not current_branch == 'master': print('{}Error{}: Run git submit from the master branch'.format(RED, DEF)) exit(1) # check branch commit was made from if not commit == 'master': if len(commit) < 8: print('{}Error{}: Use at least the first 8 characters of the commit hash.'.format(RED, DEF)) exit(1) commit_branch_cmd = split('git branch --contains {}'.format(commit)) commit_branch_proc = run(commit_branch_cmd, stdout=PIPE, stderr=PIPE) if commit_branch_proc.returncode: print('{}Error{}: The specified commit hash is invalid.'.format(RED, DEF)) exit(commit_branch_proc.returncode) branches = [branch for branch in commit_branch_proc.stdout.decode().split('\n')] on_master = False for branch in branches: branch_name = [word.strip() for word in branch.split('*')][-1] if branch_name == 'master': on_master = True if not on_master: print('{}Error{}: The submitted commit should be from master. Merge the branch with the commit into master.'.format(RED, DEF)) exit(1) cmd = "bash -c 'git log --branches=master* --not --remotes --pretty=oneline'" proc = run(cmd, stdout=PIPE, shell=True) if proc.returncode: print(proc.stdout) exit(proc.returncode) elif commit == 'master' and proc.stdout: print('{}Error{}: Push your commits to the remote using: git push'.format(RED, DEF)) exit(1) elif commit in proc.stdout.decode(): print('{}Error{}: Push your commits to the remote using: git push'.format(RED, DEF)) exit(1) def validate_tag(tag): response = api_call('/verify/{}'.format(tag), None) if 'err' in response: print('{}Error{}: {}'.format(RED, DEF, response['err'])) exit(1) if not response['valid']: print('{}Error{}: {}'.format(RED, DEF, response['msg'])) exit(1) def check_for_resubmission(args): status = api_call('/status', args) print('{}Submission Time{}: {}'.format(BLU, DEF, status['time'])) if status['submitted']: print('{}Resubmitting Homework{}: {} -- Are you {}sure{} you wish to resubmit ' '(this may result in lateday penalties)?'.format(YEL, DEF, CYA + args['tag'] + DEF, BOLD, DEF)) else: print('{}Submitting Homework{}: {} -- Are you {}sure{}? ' .format(YEL, DEF, CYA + args['tag'] + DEF, BOLD, DEF)) yn_prompt() return status['submitted'], status['time'] def confirm_repo_state(): run(split('git -c color.status=always status')) print('{}Current Repo State{}: Are you {}sure{} you have committed and pushed all the files needed ' '(such as .h files)?'.format(YEL, DEF, BOLD, DEF)) yn_prompt() def confirm_commit(commit): cmd = split('git --no-pager -c color.ui=always show {} --pretty=fuller --quiet'.format(commit)) proc = run(cmd) if proc.returncode: exit(proc.returncode) print('{}Submission Commit{}: Are you {}sure{} this is the commit you wish to submit?'.format(YEL, DEF, BOLD, DEF)) yn_prompt() def confirm_submission(tag): print('{}Confirm Submission{}: Are you {}sure{} you want to submit {}? Your previous submission (if any) will be ' '{}overwritten{}!'.format(YEL, DEF, BOLD, DEF, CYA + tag + DEF, BOLD, DEF)) yn_prompt() def trigger_submission(args): info = api_call('', args) return info['late'], info['lateDays'] def submission_info(**kwargs): run(split('git pull --quiet')) late = kwargs['late'] tag = kwargs['tag'] resubmit = kwargs['submit'] late_days = kwargs['days'] attempt_time = kwargs['time'] if late: print('{}Urgent{}: {} was overdue. You did not have enough late days remaining.'.format(RED, DEF, tag)) print('{}Info{}: You have {} lateday(s) remaining.'.format(BLU, DEF, late_days)) if resubmit: print('{}Alert{}: Your last submission will be taken into consideration.'.format(YEL, DEF)) else: print('{}Urgent{}: You will be given an automatic zero for this assignment. Please meet your Professor\n' 'after lecture or during office hours or please email us at cse320@cs.stonybrook.edu with\n' '{}[CSE320] - {} Overdue{} as the subject.'.format(RED, DEF, BLU, tag, DEF)) else: print('{}Success{}: {} submission successful. Your assignment was submitted on {}.' .format(GRE, DEF, tag, attempt_time)) print('{}Info{}: You have {} lateday(s) remaining.'.format(BLU, DEF, late_days)) print('Thank you for submitting your homework! We are working hard to get your grades out as soon as possible.') print('If you have any concerns please email us at cse320@cs.stonybrook.edu with {}[{}]{} in the subject.' .format(BLU, tag, DEF)) print('{}The CSE 320 Team{}'.format(PUR, DEF)) def main(arg_parser): requests.packages.urllib3.disable_warnings(InsecureRequestWarning) first_api_call() initial_message() net_id = validate_git_directory() args = arg_parser.parse_args() attempt_time = datetime.now() validate_tag(args.TAG) validate_commit(args.commit) resubmit, time = check_for_resubmission({'tag': args.TAG, 'repo': net_id, 'attemptTime': attempt_time.isoformat()}) confirm_repo_state() confirm_commit(args.commit) confirm_submission(args.TAG) signal(SIGINT, SIG_IGN) signal(SIGTSTP, SIG_IGN) late, late_days = trigger_submission({'tag': args.TAG, 'repo': net_id, 'commit': args.commit}) submission_info(tag=args.TAG, time=time, submit=resubmit, late=late, days=late_days) exit(0) class GitArgParser(ArgumentParser): def error(self, message): print('{}Error{}: {}'.format(RED, DEF, message)) self.print_help() exit(2) if __name__ == '__main__': parser = GitArgParser(prog='git submit', description='Submit your homework assignment using git.') parser.add_argument('TAG', type=str, help='The homework you wish to submit. Can have one of the following values: ' 'hw0, hw1, hw2, hw3, hw4, hw5') parser.add_argument('-c', dest='commit', type=str, required=False, default='master', help='Used if you wish to submit a commit that is not the latest. COMMIT is the SHA value of ' 'your commit.') main(parser)