+ - 0:00:00
Notes for current slide
Notes for next slide

ghclass

an R package for managing classes with GitHub

JSM 2019 · Denver

Colin Rundel

Univ of Edinburgh · Duke Univ

1 / 27

Reproducible Assignments

Goals:

  • Teach (enforce) version control and reproducible workflows

  • Encourage (enforce) collaboration

  • Embrace modern tools and methods

2 / 27

Reproducible Assignments

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

2 / 27

Setup

  1. Signup for a GitHub Account

    github.com

  2. Get a GitHub personal access token (PAT)

    github.com/settings/tokens

  3. Setup git and GitHub access in R

    usethis setup vignette &
    happy git with R

  4. Signup for GitHub Education

    education.github.com/benefits

  5. Create a GitHub Organization for your class

    github.com/organizations/new


Only Step 5 needs to be repeated for a new class!

3 / 27

Introduction to ghclass

devtools::install_github("rundel/ghclass")
library(ghclass)
4 / 27

Introduction to ghclass

devtools::install_github("rundel/ghclass")
library(ghclass)


Some design principals behind this package:

  1. Functions are prefixed with org, repo, team, github or local_repo to indicate what they operate on.

  2. Functions are vectorized over their parameters (batch related operations).

  3. Be verbose about what is happening, report failures but don't halt execution.

  4. Most actions are non-destructive (backed by git), the handful of dangerous operations will warn you.

  5. Follow the unix design philosophy, work towards simple & composable functions

4 / 27

Org & Roster

We will be using ghclass-demo as our class organization, hopefully your Org has a slightly more informative name .

5 / 27

Org & Roster

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).

5 / 27

Org & Roster

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
5 / 27
6 / 27

Checking your git / GitHub config

github_test_token()
## Your github token is functioning correctly.
7 / 27

Checking your git / GitHub config

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>'
7 / 27

Inviting Students

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'.
8 / 27

Inviting Students

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"
8 / 27
9 / 27
10 / 27

A few days later ...

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"
11 / 27

A few days later ...

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"


several emails and a week later ...

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)
11 / 27
12 / 27

Team Assignment

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'
13 / 27
14 / 27
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'
15 / 27

Individual Assignment

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'
16 / 27
17 / 27

What about GitHub Classroom?

18 / 27

Making Changes

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`.
19 / 27

Making Changes

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'.
19 / 27
20 / 27

Modify a file

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'.
21 / 27

Modify a file

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'.

21 / 27

Collect student work

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'.
22 / 27

Collect student work

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'.

22 / 27

Organize student work

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)'.
23 / 27

Organize student work

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)'.

23 / 27

Feedback?

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)'.
24 / 27

Feedback?

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)'.

24 / 27
25 / 27

Future Work

  • 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.

26 / 27

Reproducible Assignments

Goals:

  • Teach (enforce) version control and reproducible workflows

  • Encourage (enforce) collaboration

  • Embrace modern tools and methods

2 / 27
Paused

Help

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