Skip to content

Commit 211fef9

Browse files
kcranstonLeah Wasser
authored and
Leah Wasser
committed
Refactor template script (#121)
* 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
1 parent e67a057 commit 211fef9

File tree

6 files changed

+179
-102
lines changed

6 files changed

+179
-102
lines changed

abcclassroom/__main__.py

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -417,62 +417,58 @@ def author():
417417
)
418418

419419

420-
def assignment_template():
420+
def new_template():
421421
"""
422422
Create a new assignment template repository: creates local directory,
423-
copy / create required files, intialize as git repo, create remote repo
424-
on GitHub, and push local repo to GitHub. Will open git editor to ask for
425-
commit message.
423+
copy / create required files, intialize as git repo, and (optionally) create remote repo
424+
on GitHub and push local repo to GitHub. Will open git editor to ask for
425+
commit message if custom message requested.
426426
"""
427-
parser = argparse.ArgumentParser(description=assignment_template.__doc__)
427+
parser = argparse.ArgumentParser(description=new_template.__doc__)
428428
parser.add_argument(
429429
"assignment",
430430
help="Name of assignment. Must match name in nbgrader release directory",
431431
)
432432
parser.add_argument(
433433
"--custom-message",
434434
action="store_true",
435-
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'.",
435+
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').",
436436
)
437437
parser.add_argument(
438-
"--local-only",
438+
"--github",
439439
action="store_true",
440-
help="Create local template repository only; do not create GitHub repo or push to GitHub (default: False)",
440+
help="Also perform the GitHub operations (create remote repo on GitHub and push to remote (by default, only does local repository setup)",
441441
)
442442
parser.add_argument(
443443
"--mode",
444444
choices=["delete", "fail", "merge"],
445445
default="fail",
446-
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.",
446+
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).",
447447
)
448448
args = parser.parse_args()
449449

450-
print("Loading configuration from config.yml")
451-
config = cf.get_config()
452-
template_dir = cf.get_config_option(config, "template_dir", True)
453-
# organization = get_config_option(config,"organization",True)
454-
455-
# these are the steps to create the local git repository
456-
assignment = args.assignment
457-
template_repo_path = template.create_template_dir(
458-
config, assignment, args.mode
450+
template.new_update_template(args)
451+
452+
453+
def update_template():
454+
"""
455+
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
456+
commit message.
457+
"""
458+
parser = argparse.ArgumentParser(description=update_template.__doc__)
459+
parser.add_argument(
460+
"assignment",
461+
help="Name of assignment. Must match name in nbgrader release directory",
459462
)
460-
print("repo path: {}".format(template_repo_path))
461-
template.copy_assignment_files(config, template_repo_path, assignment)
462-
template.create_extra_files(config, template_repo_path, assignment)
463-
github.init_and_commit(template_repo_path, args.custom_message)
464-
465-
# now do the github things, unless we've been asked to only do local things
466-
if not args.local_only:
467-
organization = cf.get_config_option(config, "organization", True)
468-
# get the name of the repo (the final dir in the path)
469-
repo_name = os.path.basename(template_repo_path)
470-
print("Creating repo {}".format(repo_name))
471-
# create the remote repo on github and push the local repo
472-
# (will print error and return if repo already exists)
473-
github.create_repo(
474-
organization,
475-
repo_name,
476-
template_repo_path,
477-
cf.get_github_auth()["token"],
478-
)
463+
parser.add_argument(
464+
"--mode",
465+
choices=["delete", "merge"],
466+
default="merge",
467+
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).",
468+
)
469+
args = parser.parse_args()
470+
# now set the additional args (so that it matches the keys in add_template and we can use the same implementation
471+
# methods)
472+
setattr(args, "github", True)
473+
setattr(args, "custom_message", True)
474+
template.new_update_template(args)

abcclassroom/config.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,9 @@ def get_config():
6464
return config
6565
except FileNotFoundError as err:
6666
print(
67-
"Oops! I can't seem to find a config.yml file in your course "
68-
"directory. If you don't have a course directory and config file "
69-
"setup yet, create one using abc-quickstart. You will need to edit"
70-
"the file to ensure it contains the variables related to your course"
67+
"Oops! I can't seem to find a config.yml file in this "
68+
"directory. Are you in the top-level directory for the course? If you don't have a course directory and config file "
69+
"setup yet, you can create one using abc-quickstart"
7170
".\n"
7271
)
7372
sys.exit(1)

abcclassroom/github.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,27 @@ def _call_git(*args, directory=None):
2828
)
2929
except subprocess.CalledProcessError as e:
3030
err = e.stderr.decode("utf-8")
31-
if err:
32-
msg = err.split(":")[1].strip()
33-
else:
34-
msg = e.stdout.decode("utf-8")
35-
raise RuntimeError(msg) from e
31+
if not err:
32+
err = e.stdout.decode("utf-8")
33+
raise RuntimeError(err) from e
3634

3735
return ret
3836

3937

38+
def remote_repo_exists(org, repository, token=None):
39+
"""Check if the remote repository exists for the assignment.
40+
"""
41+
42+
try:
43+
g = gh3.login(token=token)
44+
g.repository(org, repository)
45+
46+
except Exception as e:
47+
return False
48+
49+
return True
50+
51+
4052
def check_student_repo_exists(org, course, student, token=None):
4153
"""Check if the student has a repository for the course.
4254
@@ -132,10 +144,8 @@ def create_pr(org, repository, branch, message, token):
132144
repo.create_pull(title, "master", branch, msg)
133145

134146

135-
def create_repo(org, repository, directory, token):
136-
"""Create a repository in the provided GitHub organization, adds that
137-
repo as a remote to the local repo in directory, and pushes the
138-
directory.
147+
def create_repo(org, repository, token):
148+
"""Create a repository in the provided GitHub organization.
139149
"""
140150
github_obj = gh3.login(token=token)
141151
organization = github_obj.organization(org)
@@ -152,17 +162,13 @@ def create_repo(org, repository, directory, token):
152162
org, repository
153163
)
154164
)
155-
print("Not adding remote to local repo or pushing to github.")
156-
return
157-
158-
_call_git(
159-
"remote",
160-
"add",
161-
"origin",
162-
"https://{}@github.com/{}/{}".format(token, org, repository),
163-
directory=directory,
165+
166+
167+
def add_remote(directory, organization, remote_repo, token):
168+
remote_url = "https://{}@github.com/{}/{}".format(
169+
token, organization, remote_repo
164170
)
165-
push_to_github(directory, "master")
171+
_call_git("remote", "add", "origin", remote_url, directory=directory)
166172

167173

168174
def repo_changed(directory):
@@ -223,11 +229,18 @@ def init_and_commit(directory, custom_message=False):
223229
print("Empty commit message, exiting.")
224230
sys.exit(1)
225231
commit_all_changes(directory, message)
232+
else:
233+
print("No changes to local repository.")
226234

227235

228236
def push_to_github(directory, branch):
229237
"""Push `branch` back to GitHub"""
230-
_call_git("push", "--set-upstream", "origin", branch, directory=directory)
238+
try:
239+
_call_git(
240+
"push", "--set-upstream", "origin", branch, directory=directory
241+
)
242+
except RuntimeError as e:
243+
raise e
231244

232245

233246
def git_init(directory):

abcclassroom/template.py

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,76 @@
66
import os
77
import sys
88
import shutil
9+
from pathlib import Path
910

1011
from . import config as cf
1112
from . import github
1213
from . import utils
1314

1415

16+
def new_update_template(args):
17+
"""
18+
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).
19+
"""
20+
print("Loading configuration from config.yml")
21+
config = cf.get_config()
22+
template_dir = cf.get_config_option(config, "template_dir", True)
23+
24+
# create the local git repository
25+
assignment = args.assignment
26+
template_repo_path = create_template_dir(config, assignment, args.mode)
27+
print("repo path: {}".format(template_repo_path))
28+
copy_assignment_files(config, template_repo_path, assignment)
29+
create_extra_files(config, template_repo_path, assignment)
30+
github.init_and_commit(template_repo_path, args.custom_message)
31+
32+
# optional github steps
33+
if args.github:
34+
organization = cf.get_config_option(config, "organization", True)
35+
repo_name = os.path.basename(template_repo_path)
36+
token = cf.get_github_auth()["token"]
37+
38+
create_or_update_remote(
39+
template_repo_path, organization, repo_name, token
40+
)
41+
42+
43+
def create_or_update_remote(
44+
template_repo_path, organization, repo_name, token
45+
):
46+
remote_exists = github.remote_repo_exists(organization, repo_name, token)
47+
if not remote_exists:
48+
print("Creating remote repo {}".format(repo_name))
49+
# create the remote repo on github and push the local repo
50+
# (will print error and return if repo already exists)
51+
github.create_repo(organization, repo_name, token)
52+
53+
try:
54+
github.add_remote(template_repo_path, organization, repo_name, token)
55+
except RuntimeError as e:
56+
print("Remote already added to local repository")
57+
pass
58+
59+
print("Pushing any changes to remote repository on GitHub")
60+
try:
61+
github.push_to_github(template_repo_path, "master")
62+
except RuntimeError as e:
63+
print(
64+
"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:"
65+
)
66+
print(e)
67+
68+
1569
def create_template_dir(config, assignment, mode="fail"):
1670
"""
1771
Creates a new directory in template_dir that will become the
18-
template repository for the assignment.
72+
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.
1973
"""
2074
course_dir = cf.get_config_option(config, "course_directory", True)
21-
template_dir = cf.get_config_option(config, "template_dir", True)
22-
parent_path = utils.get_abspath(template_dir, course_dir)
75+
template_parent_dir = cf.get_config_option(config, "template_dir", True)
76+
parent_path = utils.get_abspath(template_parent_dir, course_dir)
2377

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

4498
repo_name = course_name + "-" + assignment + "-template"
45-
template_path = os.path.join(parent_path, repo_name)
46-
dir_exists = os.path.exists(template_path)
99+
template_path = Path(parent_path, repo_name)
100+
dir_exists = template_path.is_dir()
47101
if not dir_exists:
48-
os.mkdir(template_path)
102+
template_path.mkdir()
49103
print("Creating new template repo at {}".format(template_path))
50104
else:
51105
if mode == "fail":
52106
print(
53-
"Directory {} already exists; re-run with '--mode merge' or --mode delete', or delete / move directory before re-running".format(
107+
"Directory {} already exists; re-run with '--mode merge' or '--mode delete', or delete / move directory before re-running".format(
54108
template_path
55109
)
56110
)
57111
sys.exit(1)
58112
elif mode == "merge":
59113
print(
60-
"Template directory {} already exists but mode is 'merge'; will keep directory but overwrite existing files with same names".format(
114+
"Template directory {} already exists; will keep directory but overwrite existing files with same names".format(
61115
template_path
62116
)
63117
)
64118
else:
65119
# mode == delete
66120
print(
67-
"Deleting existing directory and contents at {} and creating new empty directory with same name.".format(
121+
"Template directory {} already exists; deleting existing files but keeping .git directory, if exists.".format(
68122
template_path
69123
)
70124
)
71-
shutil.rmtree(template_path)
72-
os.mkdir(template_path)
125+
# temporarily move the .git dir to the parent of the template_path
126+
gitdir = Path(template_path, ".git")
127+
if gitdir.exists():
128+
target = Path(Path(template_path).parent, ".tempgit")
129+
gitdir.replace(target)
130+
131+
# remove template_path and re-create with same name
132+
shutil.rmtree(template_path)
133+
Path(template_path).mkdir()
134+
135+
# and then move the .git dir back
136+
target.replace(gitdir)
137+
73138
return template_path
74139

75140

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

0 commit comments

Comments
 (0)