Skip to content

Commit

Permalink
Refactor template script (#121)
Browse files Browse the repository at this point in the history
* initial refactor of create-template-assignment script

* handling various git and github states

* update cmdline args for mode

* removed debugging line

* update message for missing config

* debugging template update script

* updating tests for new scripts
  • Loading branch information
kcranston authored and Leah Wasser committed Nov 12, 2019
1 parent e67a057 commit 211fef9
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 102 deletions.
70 changes: 33 additions & 37 deletions abcclassroom/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,62 +417,58 @@ def author():
)


def assignment_template():
def new_template():
"""
Create a new assignment template repository: creates local directory,
copy / create required files, intialize as git repo, create remote repo
on GitHub, and push local repo to GitHub. Will open git editor to ask for
commit message.
copy / create required files, intialize as git repo, and (optionally) create remote repo
on GitHub and push local repo to GitHub. Will open git editor to ask for
commit message if custom message requested.
"""
parser = argparse.ArgumentParser(description=assignment_template.__doc__)
parser = argparse.ArgumentParser(description=new_template.__doc__)
parser.add_argument(
"assignment",
help="Name of assignment. Must match name in nbgrader release directory",
)
parser.add_argument(
"--custom-message",
action="store_true",
help="Use a custom commit message for git. Will open the default git text editor for entry. If not set, will use message 'Initial commit'.",
help="Use a custom commit message for git. Will open the default git text editor for entry (if not set, uses default message 'Initial commit').",
)
parser.add_argument(
"--local-only",
"--github",
action="store_true",
help="Create local template repository only; do not create GitHub repo or push to GitHub (default: False)",
help="Also perform the GitHub operations (create remote repo on GitHub and push to remote (by default, only does local repository setup)",
)
parser.add_argument(
"--mode",
choices=["delete", "fail", "merge"],
default="fail",
help="Action if template directory already exists. Choices are: delete = delete the directory and contents; fail = exit and let user delete or rename; merge = keep existing dir, overwrite existing files, add new files. Default is fail.",
help="Action if template directory already exists. Choices are: delete = delete contents before proceeding (except .git directory); merge = keep existing dir, overwrite existing files, add new files (Default = fail).",
)
args = parser.parse_args()

print("Loading configuration from config.yml")
config = cf.get_config()
template_dir = cf.get_config_option(config, "template_dir", True)
# organization = get_config_option(config,"organization",True)

# these are the steps to create the local git repository
assignment = args.assignment
template_repo_path = template.create_template_dir(
config, assignment, args.mode
template.new_update_template(args)


def update_template():
"""
Updates an existing assignment template repository: update / add new and changed files, then push local changes to GitHub. Will open git editor to ask for
commit message.
"""
parser = argparse.ArgumentParser(description=update_template.__doc__)
parser.add_argument(
"assignment",
help="Name of assignment. Must match name in nbgrader release directory",
)
print("repo path: {}".format(template_repo_path))
template.copy_assignment_files(config, template_repo_path, assignment)
template.create_extra_files(config, template_repo_path, assignment)
github.init_and_commit(template_repo_path, args.custom_message)

# now do the github things, unless we've been asked to only do local things
if not args.local_only:
organization = cf.get_config_option(config, "organization", True)
# get the name of the repo (the final dir in the path)
repo_name = os.path.basename(template_repo_path)
print("Creating repo {}".format(repo_name))
# create the remote repo on github and push the local repo
# (will print error and return if repo already exists)
github.create_repo(
organization,
repo_name,
template_repo_path,
cf.get_github_auth()["token"],
)
parser.add_argument(
"--mode",
choices=["delete", "merge"],
default="merge",
help="What to do with existing contents of template directory. Choices are: delete = remove contents before proceeding (leaving .git directory); merge = overwrite existing files add new files (Default = merge).",
)
args = parser.parse_args()
# now set the additional args (so that it matches the keys in add_template and we can use the same implementation
# methods)
setattr(args, "github", True)
setattr(args, "custom_message", True)
template.new_update_template(args)
7 changes: 3 additions & 4 deletions abcclassroom/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,9 @@ def get_config():
return config
except FileNotFoundError as err:
print(
"Oops! I can't seem to find a config.yml file in your course "
"directory. If you don't have a course directory and config file "
"setup yet, create one using abc-quickstart. You will need to edit"
"the file to ensure it contains the variables related to your course"
"Oops! I can't seem to find a config.yml file in this "
"directory. Are you in the top-level directory for the course? If you don't have a course directory and config file "
"setup yet, you can create one using abc-quickstart"
".\n"
)
sys.exit(1)
Expand Down
53 changes: 33 additions & 20 deletions abcclassroom/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,27 @@ def _call_git(*args, directory=None):
)
except subprocess.CalledProcessError as e:
err = e.stderr.decode("utf-8")
if err:
msg = err.split(":")[1].strip()
else:
msg = e.stdout.decode("utf-8")
raise RuntimeError(msg) from e
if not err:
err = e.stdout.decode("utf-8")
raise RuntimeError(err) from e

return ret


def remote_repo_exists(org, repository, token=None):
"""Check if the remote repository exists for the assignment.
"""

try:
g = gh3.login(token=token)
g.repository(org, repository)

except Exception as e:
return False

return True


def check_student_repo_exists(org, course, student, token=None):
"""Check if the student has a repository for the course.
Expand Down Expand Up @@ -132,10 +144,8 @@ def create_pr(org, repository, branch, message, token):
repo.create_pull(title, "master", branch, msg)


def create_repo(org, repository, directory, token):
"""Create a repository in the provided GitHub organization, adds that
repo as a remote to the local repo in directory, and pushes the
directory.
def create_repo(org, repository, token):
"""Create a repository in the provided GitHub organization.
"""
github_obj = gh3.login(token=token)
organization = github_obj.organization(org)
Expand All @@ -152,17 +162,13 @@ def create_repo(org, repository, directory, token):
org, repository
)
)
print("Not adding remote to local repo or pushing to github.")
return

_call_git(
"remote",
"add",
"origin",
"https://{}@github.com/{}/{}".format(token, org, repository),
directory=directory,


def add_remote(directory, organization, remote_repo, token):
remote_url = "https://{}@github.com/{}/{}".format(
token, organization, remote_repo
)
push_to_github(directory, "master")
_call_git("remote", "add", "origin", remote_url, directory=directory)


def repo_changed(directory):
Expand Down Expand Up @@ -223,11 +229,18 @@ def init_and_commit(directory, custom_message=False):
print("Empty commit message, exiting.")
sys.exit(1)
commit_all_changes(directory, message)
else:
print("No changes to local repository.")


def push_to_github(directory, branch):
"""Push `branch` back to GitHub"""
_call_git("push", "--set-upstream", "origin", branch, directory=directory)
try:
_call_git(
"push", "--set-upstream", "origin", branch, directory=directory
)
except RuntimeError as e:
raise e


def git_init(directory):
Expand Down
90 changes: 78 additions & 12 deletions abcclassroom/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,76 @@
import os
import sys
import shutil
from pathlib import Path

from . import config as cf
from . import github
from . import utils


def new_update_template(args):
"""
Creates or updates an assignment template repository. Implementation of both the new_template and update_template console scripts (which perform the same basic functions but with different command line arguments and defaults).
"""
print("Loading configuration from config.yml")
config = cf.get_config()
template_dir = cf.get_config_option(config, "template_dir", True)

# create the local git repository
assignment = args.assignment
template_repo_path = create_template_dir(config, assignment, args.mode)
print("repo path: {}".format(template_repo_path))
copy_assignment_files(config, template_repo_path, assignment)
create_extra_files(config, template_repo_path, assignment)
github.init_and_commit(template_repo_path, args.custom_message)

# optional github steps
if args.github:
organization = cf.get_config_option(config, "organization", True)
repo_name = os.path.basename(template_repo_path)
token = cf.get_github_auth()["token"]

create_or_update_remote(
template_repo_path, organization, repo_name, token
)


def create_or_update_remote(
template_repo_path, organization, repo_name, token
):
remote_exists = github.remote_repo_exists(organization, repo_name, token)
if not remote_exists:
print("Creating remote repo {}".format(repo_name))
# create the remote repo on github and push the local repo
# (will print error and return if repo already exists)
github.create_repo(organization, repo_name, token)

try:
github.add_remote(template_repo_path, organization, repo_name, token)
except RuntimeError as e:
print("Remote already added to local repository")
pass

print("Pushing any changes to remote repository on GitHub")
try:
github.push_to_github(template_repo_path, "master")
except RuntimeError as e:
print(
"Push to github failed. This is usually because there are changes on the remote that you do not have locally. Here is the github error:"
)
print(e)


def create_template_dir(config, assignment, mode="fail"):
"""
Creates a new directory in template_dir that will become the
template repository for the assignment.
template repository for the assignment. If directory exists and mode is merge, do nothing. If directory exists and mode is delete, remove contents but leave .git directory.
"""
course_dir = cf.get_config_option(config, "course_directory", True)
template_dir = cf.get_config_option(config, "template_dir", True)
parent_path = utils.get_abspath(template_dir, course_dir)
template_parent_dir = cf.get_config_option(config, "template_dir", True)
parent_path = utils.get_abspath(template_parent_dir, course_dir)

# check that course_dir/template_dir exists, and create it if it does not
# check that parent directory for templates exists, and create it if it does not
if not os.path.isdir(parent_path):
print(
"Creating new directory for template repos at {}".format(
Expand All @@ -42,34 +96,45 @@ def create_template_dir(config, assignment, mode="fail"):
sys.exit(1)

repo_name = course_name + "-" + assignment + "-template"
template_path = os.path.join(parent_path, repo_name)
dir_exists = os.path.exists(template_path)
template_path = Path(parent_path, repo_name)
dir_exists = template_path.is_dir()
if not dir_exists:
os.mkdir(template_path)
template_path.mkdir()
print("Creating new template repo at {}".format(template_path))
else:
if mode == "fail":
print(
"Directory {} already exists; re-run with '--mode merge' or --mode delete', or delete / move directory before re-running".format(
"Directory {} already exists; re-run with '--mode merge' or '--mode delete', or delete / move directory before re-running".format(
template_path
)
)
sys.exit(1)
elif mode == "merge":
print(
"Template directory {} already exists but mode is 'merge'; will keep directory but overwrite existing files with same names".format(
"Template directory {} already exists; will keep directory but overwrite existing files with same names".format(
template_path
)
)
else:
# mode == delete
print(
"Deleting existing directory and contents at {} and creating new empty directory with same name.".format(
"Template directory {} already exists; deleting existing files but keeping .git directory, if exists.".format(
template_path
)
)
shutil.rmtree(template_path)
os.mkdir(template_path)
# temporarily move the .git dir to the parent of the template_path
gitdir = Path(template_path, ".git")
if gitdir.exists():
target = Path(Path(template_path).parent, ".tempgit")
gitdir.replace(target)

# remove template_path and re-create with same name
shutil.rmtree(template_path)
Path(template_path).mkdir()

# and then move the .git dir back
target.replace(gitdir)

return template_path


Expand All @@ -96,6 +161,7 @@ def copy_assignment_files(config, template_repo, assignment):
for file in all_files:
fpath = os.path.join(release_dir, file)
print("copying {} to {}".format(fpath, template_repo))
# overwrites if fpath exists in template_repo
shutil.copy(fpath, template_repo)
nfiles += 1
print("Copied {} files".format(nfiles))
Expand Down
Loading

0 comments on commit 211fef9

Please sign in to comment.