ghclass
an R package for managing classes with GitHub
JSM 2019 · Denver
Colin Rundel
Univ of Edinburgh · Duke Univ
Goals:
Teach (enforce) version control and reproducible workflows
Encourage (enforce) collaboration
Embrace modern tools and methods
Goals:
Teach (enforce) version control and reproducible workflows
Encourage (enforce) collaboration
Embrace modern tools and methods
Organization:
Core toolkit: R + RStudio + RMarkdown
Spend 1 lecture on git / GitHub
GitHub setup:
1 organization / course
1 repo / (team | student) / assignment
Randomly assigned teams of 3-4 students / assignment
Signup for a GitHub Account
Get a GitHub personal access token (PAT)
Setup git and GitHub access in R
Signup for GitHub Education
Create a GitHub Organization for your class
Only Step 5 needs to be repeated for a new class!
ghclass
devtools::install_github("rundel/ghclass")library(ghclass)
ghclass
devtools::install_github("rundel/ghclass")library(ghclass)
Some design principals behind this package:
Functions are prefixed with org
, repo
, team
, github
or local_repo
to indicate what they operate on.
Functions are vectorized over their parameters (batch related operations).
Be verbose about what is happening, report failures but don't halt execution.
Most actions are non-destructive (backed by git), the handful of dangerous operations will warn you.
Follow the unix design philosophy, work towards simple & composable functions
We will be using ghclass-demo as our class organization, hopefully your Org has a slightly more informative name .
We will be using ghclass-demo as our class organization, hopefully your Org has a slightly more informative name .
(I find course#
-semester
works well, e.g. Sta323-Sp19
).
We will be using ghclass-demo as our class organization, hopefully your Org has a slightly more informative name .
(I find course#
-semester
works well, e.g. Sta323-Sp19
).
(roster = readr::read_csv("files/roster.csv"))
## # A tibble: 6 x 4## netid email github hw01 ## <chr> <chr> <chr> <chr> ## 1 za17 anya@school.edu ghclass-anya hw01-team01## 2 kb34 bruno@school.edu ghclass-bruno hw01-team01## 3 ac13 celine@school.edu ghclass-celine hw01-team02## 4 bd88 diego@school.edu ghclass-diego hw01-team02## 5 se01 elijah@school.edu ghclass-elijah hw01-team03## 6 df00 francis@school.edu ghclass-francis hw01-team03
github_test_token()
## ✔ Your github token is functioning correctly.
github_test_token()
## ✔ Your github token is functioning correctly.
usethis::git_sitrep()
## Git user## * Name: 'Colin Rundel'## * Email: 'rundel@gmail.com'## * Vaccinated: TRUE## usethis + git2r## * Default usethis protocol: <unset>## * git2r supports SSH: TRUE## * Credentials: '<user-provided git2r credential object with class cred_ssh_key>'## GitHub## * Personal access token: '<found in env var>'## * User: 'rundel'## * Name: 'Colin Rundel'## Repo## * Path: '/Users/rundel/Desktop/Presentations/.git'## * Local branch -> remote tracking branch: 'master' -> 'origin/master'## GitHub pull request readiness## * origin: rundel/Presentations, can push## * upstream: '<no such remote>'
org_invite(org = "ghclass-demo", user = roster$github)
## ✔ Invited user 'ghclass-anya' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-bruno' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-celine' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-diego' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-elijah' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-francis' to org 'ghclass-demo'.
org_invite(org = "ghclass-demo", user = roster$github)
## ✔ Invited user 'ghclass-anya' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-bruno' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-celine' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-diego' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-elijah' to org 'ghclass-demo'.## ✔ Invited user 'ghclass-francis' to org 'ghclass-demo'.
org_members("ghclass-demo", include_admins = FALSE)
## character(0)
org_pending_members("ghclass-demo")
## [1] "ghclass-anya" "ghclass-bruno" "ghclass-diego" "ghclass-elijah" ## [5] "ghclass-francis" "ghclass-celine"
org_members("ghclass-demo", include_admins = FALSE)
## [1] "ghclass-anya" "ghclass-bruno" "ghclass-celine" "ghclass-diego"
org_pending_members("ghclass-demo")
## [1] "ghclass-elijah" "ghclass-francis"
org_members("ghclass-demo", include_admins = FALSE)
## [1] "ghclass-anya" "ghclass-bruno" "ghclass-celine" "ghclass-diego"
org_pending_members("ghclass-demo")
## [1] "ghclass-elijah" "ghclass-francis"
org_members("ghclass-demo", include_admins = FALSE)
## [1] "ghclass-anya" "ghclass-bruno" "ghclass-celine" "ghclass-diego" ## [5] "ghclass-elijah" "ghclass-francis"
org_pending_members("ghclass-demo")
## character(0)
org_create_assignment(org = "ghclass-demo", repo = roster$hw01, user = roster$github, team = roster$hw01, source_repo = "Sta323-Sp19/hw1")
## ✔ Created repo 'ghclass-demo/hw01-team01'.## ✔ Created repo 'ghclass-demo/hw01-team02'.## ✔ Created repo 'ghclass-demo/hw01-team03'.## ✔ Created team 'hw01-team01' in org 'ghclass-demo'.## ✔ Created team 'hw01-team02' in org 'ghclass-demo'.## ✔ Created team 'hw01-team03' in org 'ghclass-demo'.## ✔ Added 'ghclass-anya' to team 'hw01-team01'.## ✔ Added 'ghclass-bruno' to team 'hw01-team01'.## ✔ Added 'ghclass-celine' to team 'hw01-team02'.## ✔ Added 'ghclass-diego' to team 'hw01-team02'.## ✔ Added 'ghclass-elijah' to team 'hw01-team03'.## ✔ Added 'ghclass-francis' to team 'hw01-team03'.## ✔ Added team 'hw01-team01' to repo 'ghclass-demo/hw01-team01'.## ✔ Added team 'hw01-team02' to repo 'ghclass-demo/hw01-team02'.## ✔ Added team 'hw01-team03' to repo 'ghclass-demo/hw01-team03'.## ✔ Cloned 'Sta323-Sp19/hw1'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team01'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team01'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team02'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team02'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team03'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team03'.## ✔ Removed local copy of 'Sta323-Sp19/hw1'
team_create("ghclass-demo", team = roster$hw01)
## ✔ Created team 'hw01-team01' in org 'ghclass-demo'.## ✔ Created team 'hw01-team02' in org 'ghclass-demo'.## ✔ Created team 'hw01-team03' in org 'ghclass-demo'.
team_invite("ghclass-demo", user = roster$github, team = roster$hw01)
## ✔ Added 'ghclass-anya' to team 'hw01-team01'.## ✔ Added 'ghclass-bruno' to team 'hw01-team01'.## ✔ Added 'ghclass-celine' to team 'hw01-team02'.## ✔ Added 'ghclass-diego' to team 'hw01-team02'.## ✔ Added 'ghclass-elijah' to team 'hw01-team03'.## ✔ Added 'ghclass-francis' to team 'hw01-team03'.
repo_create("ghclass-demo", name = roster$hw01)
## ✔ Created repo 'ghclass-demo/hw01-team01'.## ✔ Created repo 'ghclass-demo/hw01-team02'.## ✔ Created repo 'ghclass-demo/hw01-team03'.
repo_add_team(repo = org_repos("ghclass-demo", filter = "hw01-"), team = org_teams("ghclass-demo", filter = "hw01-"))
## ✔ Added team 'hw01-team01' to repo 'ghclass-demo/hw01-team01'.## ✔ Added team 'hw01-team02' to repo 'ghclass-demo/hw01-team02'.## ✔ Added team 'hw01-team03' to repo 'ghclass-demo/hw01-team03'.
repo_mirror(source_repo = "Sta323-Sp19/hw1", target_repo = org_repos("ghclass-demo", filter = "hw01-"))
## ✔ Cloned 'Sta323-Sp19/hw1'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team01'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team02'.## ✔ Pushed (mirror) 'hw1' to repo 'ghclass-demo/hw01-team03'.## ✔ Removed local copy of 'Sta323-Sp19/hw1'
org_create_assignment(org = "ghclass-demo", repo = paste0("mid1-", roster$github), user = roster$github, source_repo = "Sta323-Sp19/midterm1")
## ✔ Created repo 'ghclass-demo/mid1-ghclass-anya'.## ✔ Created repo 'ghclass-demo/mid1-ghclass-bruno'.## ✔ Created repo 'ghclass-demo/mid1-ghclass-celine'.## ✔ Created repo 'ghclass-demo/mid1-ghclass-diego'.## ✔ Created repo 'ghclass-demo/mid1-ghclass-elijah'.## ✔ Created repo 'ghclass-demo/mid1-ghclass-francis'.## ✔ Added user 'ghclass-anya' to repo 'ghclass-demo/mid1-ghclass-anya'.## ✔ Added user 'ghclass-bruno' to repo 'ghclass-demo/mid1-ghclass-bruno'.## ✔ Added user 'ghclass-celine' to repo 'ghclass-demo/mid1-ghclass-celine'.## ✔ Added user 'ghclass-diego' to repo 'ghclass-demo/mid1-ghclass-diego'.## ✔ Added user 'ghclass-elijah' to repo 'ghclass-demo/mid1-ghclass-elijah'.## ✔ Added user 'ghclass-francis' to repo 'ghclass-demo/mid1-ghclass-francis'.## ✔ Cloned 'Sta323-Sp19/midterm1'.## ✔ Pushed (mirror) 'midterm1' to repo 'ghclass-demo/mid1-ghclass-anya'.## ✔ Pushed (mirror) 'midterm1' to repo 'ghclass-demo/mid1-ghclass-bruno'.## ✔ Pushed (mirror) 'midterm1' to repo 'ghclass-demo/mid1-ghclass-celine'.## ✔ Pushed (mirror) 'midterm1' to repo 'ghclass-demo/mid1-ghclass-diego'.## ✔ Pushed (mirror) 'midterm1' to repo 'ghclass-demo/mid1-ghclass-elijah'.## ✔ Pushed (mirror) 'midterm1' to repo 'ghclass-demo/mid1-ghclass-francis'.## ✔ Removed local copy of 'Sta323-Sp19/midterm1'
repo_add_file(repo = org_repos("ghclass-demo", "hw01-"), file = "files/fizzbuzz.png")
## ✖ Failed to add file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team01': already exists.## If you want to force-add this file, re-run the command with `overwrite = TRUE`.## ✖ Failed to add file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team02': already exists.## If you want to force-add this file, re-run the command with `overwrite = TRUE`.## ✖ Failed to add file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team03': already exists.## If you want to force-add this file, re-run the command with `overwrite = TRUE`.
repo_add_file(repo = org_repos("ghclass-demo", "hw01-"), file = "files/fizzbuzz.png")
## ✖ Failed to add file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team01': already exists.## If you want to force-add this file, re-run the command with `overwrite = TRUE`.## ✖ Failed to add file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team02': already exists.## If you want to force-add this file, re-run the command with `overwrite = TRUE`.## ✖ Failed to add file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team03': already exists.## If you want to force-add this file, re-run the command with `overwrite = TRUE`.
repo_add_file(repo = org_repos("ghclass-demo", "hw01-"), file = "files/fizzbuzz.png", overwrite = TRUE)
## ✔ Added file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team01'.## ✔ Added file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team02'.## ✔ Added file 'fizzbuzz.png' to repo 'ghclass-demo/hw01-team03'.
repo_modify_file(repo = org_repos("ghclass-demo", "hw01-"), file = "README.md", pattern = "on Thursday 1/24/2019.", content = "on Wednesday 10/07/2019.")
## ✔ Modified file 'ghclass-demo/hw01-team01/README.md'.## ✔ Modified file 'ghclass-demo/hw01-team02/README.md'.## ✔ Modified file 'ghclass-demo/hw01-team03/README.md'.
repo_modify_file(repo = org_repos("ghclass-demo", "hw01-"), file = "README.md", pattern = "on Thursday 1/24/2019.", content = "on Wednesday 10/07/2019.")
## ✔ Modified file 'ghclass-demo/hw01-team01/README.md'.## ✔ Modified file 'ghclass-demo/hw01-team02/README.md'.## ✔ Modified file 'ghclass-demo/hw01-team03/README.md'.
local_repo_clone(repo = org_repos("ghclass-demo", "mid1-"), local_path = "mid1")
## ✔ Cloned 'ghclass-demo/mid1-ghclass-anya'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-bruno'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-celine'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-diego'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-elijah'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-francis'.
local_repo_clone(repo = org_repos("ghclass-demo", "mid1-"), local_path = "mid1")
## ✔ Cloned 'ghclass-demo/mid1-ghclass-anya'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-bruno'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-celine'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-diego'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-elijah'.## ✔ Cloned 'ghclass-demo/mid1-ghclass-francis'.
local_repo_rename(repo_dir = "mid1", pattern = roster$github, replacement = paste0(roster$netid, "-(\\1)"))
## ✔ Renaming 'mid1/mid1-ghclass-anya' to 'mid1/mid1-za17-(ghclass-anya)'.## ✔ Renaming 'mid1/mid1-ghclass-bruno' to 'mid1/mid1-kb34-(ghclass-bruno)'.## ✔ Renaming 'mid1/mid1-ghclass-celine' to 'mid1/mid1-ac13-(ghclass-celine)'.## ✔ Renaming 'mid1/mid1-ghclass-diego' to 'mid1/mid1-bd88-(ghclass-diego)'.## ✔ Renaming 'mid1/mid1-ghclass-elijah' to 'mid1/mid1-se01-(ghclass-elijah)'.## ✔ Renaming 'mid1/mid1-ghclass-francis' to 'mid1/mid1-df00-(ghclass-francis)'.
local_repo_rename(repo_dir = "mid1", pattern = roster$github, replacement = paste0(roster$netid, "-(\\1)"))
## ✔ Renaming 'mid1/mid1-ghclass-anya' to 'mid1/mid1-za17-(ghclass-anya)'.## ✔ Renaming 'mid1/mid1-ghclass-bruno' to 'mid1/mid1-kb34-(ghclass-bruno)'.## ✔ Renaming 'mid1/mid1-ghclass-celine' to 'mid1/mid1-ac13-(ghclass-celine)'.## ✔ Renaming 'mid1/mid1-ghclass-diego' to 'mid1/mid1-bd88-(ghclass-diego)'.## ✔ Renaming 'mid1/mid1-ghclass-elijah' to 'mid1/mid1-se01-(ghclass-elijah)'.## ✔ Renaming 'mid1/mid1-ghclass-francis' to 'mid1/mid1-df00-(ghclass-francis)'.
repo_style("ghclass-demo/hw01-team01", files = "*.Rmd", draft = TRUE)
## ✔ Created branch 'styler' from 'ghclass-demo/hw01-team01'.## ✔ Cloned 'ghclass-demo/hw01-team01@styler'.## ✔ Created pull request for 'ghclass-demo/hw01-team01 (master <= styler)'.
repo_style("ghclass-demo/hw01-team01", files = "*.Rmd", draft = TRUE)
## ✔ Created branch 'styler' from 'ghclass-demo/hw01-team01'.## ✔ Cloned 'ghclass-demo/hw01-team01@styler'.## ✔ Created pull request for 'ghclass-demo/hw01-team01 (master <= styler)'.
We will be submitting to CRAN in the next month
Active summer project adding functionality for peer review by Mine Cetinkaya-Rundel and Therese Anders
Support for GitHub actions for automated feedback (rundel/wercker replacement)
Support more workflows, if you use a GitHub based workflow for teaching that was not reflected here please get in touch.
Goals:
Teach (enforce) version control and reproducible workflows
Encourage (enforce) collaboration
Embrace modern tools and methods
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
Number + Return | Go to specific slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
t | Restart the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |