11

I am a lecturer writing coding problems for students. What I want to do is give the students the boilerplate code with placeholders for the functions which students are to complete. I will give the students access to a private github repo to clone this.

However, I also want a version of the codebase, complete with sample solutions. Obviously I don't want the students to have access to the solution (until the assignment is over).

I have thought about branches, but AFAIK, I can't keep one branch private.

Maybe I could fork the project into another private repo, but am unsure how I might keep the projects in snyc (apart from the file which contains the solution).

Is there a workflow for this situation?

  • 1
    I dont think so. But what you cold do: delcare interfaces for alle the methods that are to be implemented. In your student-public repo, create classes implementing those interfaces with the empty method bodies. Maintain the solutions in a separate private repo. This does not entirely solve your synchronization problem but it reduces it to the scope of the tasks. – marstato Sep 14 '16 at 11:42
  • have you looked in to using the github API to control access to branches? – user223083 Sep 14 '16 at 20:15
8

What could be quite doable:

  • Create 2 repositories: student and teacher.
  • Clone them to your machine (can be done with Github client)
  • You work only in teacher, never touch student.

So your directory structure is 2 cloned git repo's:

  • /student (with a .git folder)
  • /teacher (with a .git folder)

You put markers around the "private" code in comments for your language, example javascript below. The markers indicate where the private code starts and ends.

function sum(a, b) {
  // -----------------------START
  return a + b; // so this is what you expect from the student
  // -----------------------END
}

console.log(sum(1,1)); // I expect 2 as a result of your homework

Then make a simple script on your local machine:

files.forEach((fileContent, fileName) => {
  let newFileContent = '';
  let public = true;
  fileContent.forEach((line) => {
    switch(line) {
      case '// -----------------------START':
        public = false;
        break;
      case '// -----------------------END':
        public = true;
        break;
      default:
        if(public) {
          newFileContent = newFileContent + line + "\n";
        }
    }
  });
  writeFile('../student/' + fileName, newFileContent);
});

It will: take all your files and copy the contents to /student (overwriting) without the private marked parts of the code. If you want you can insert empty lines there but that might give a hint about what kind of solution your expect.

It's untested example code, so likely you have to do some debugging.

Now only thing you have to do is commit and push in the student repository when you are happy about the output. That can be done in one click when using the GitHub client (so you can do a quick visual review) or just do it manually on command line.

The student repo is an output repository only so it will always stay up-to-date, it's clear to the students what's changed by looking at the commits (because they only show changes) and it's simple to handle.

One step further would be to create a git commit-hook which auto-runs your script.

Edit: See you made an edit to your post:

Obviously I don't want the students to have access to the solution (until the assignment is over).

I suspect it's clear but to be complete: Just remove the tags around the finished exercise will publish the answer the same way as you would do for normal updates to the exercises.

  • was hoping I could do this with some git voodoo, however your solution is very practical. – Ken Sep 14 '16 at 13:32
  • @Ken was thinking about that too but it's a bit wrong tool for the wrong job. Git does merge, update etc but in general it's not the idea to select code. It good at keeping your codebase consistent on multiple machines. So that's why I though up another solution. What I also like about this approach is that it minimises risk and labour so it's easy to keep up with it. And, in the end, you should write your commit message to student repo by hand anyway to give a good example to your students ;) – Luc Franken Sep 14 '16 at 14:00
  • To help git keep track of changes you could make a student branch in your teacher repo, run the script when merging (or merging by hand removing anything between the markers). Then sync the student branch locally and push it to the student repo instead of the teacher origin. This way git would be in better shape to track changes and have history properly forwarded from one repo to the next. Best of both worlds. I have not tried this mind you but I dont see why it would not work. – Newtopian Sep 14 '16 at 14:41
  • 1
    I like this except for the idea of removing the start end tags. Better to mangle them by adding the word "solution". – candied_orange Sep 14 '16 at 16:10
  • @CandiedOrange that's also a nice one, agree on that. Solution would also allow some different formatting and it clearly differentiates between forgotten tags and the real decision that the solution should be published. @ newtopian: I was thinking about that but I did not see enough advantages. Also I decided to see the student output as a totally different kind of code. It's not the real source so I decided not to. What I would do with branches in teacher repo is for example: Work on the tasks for next semester. When you are ready you merge them to master and then run the script. – Luc Franken Sep 14 '16 at 19:15
6

You could

  • Create a public GitHub repostory were you commit the boilerplate code
  • Fork this repository as a private GitHub repostory
  • Solve the assignments in the forked repository
  • Merge each solution into the public repository when the assignment is done

This is how I would implement this workflow:

  • Create a public repostory assignments hosted on GitHub. Add the boilerplate code for the assignments. E.g. for each assignment you introduce a new sub-directory containing the boilerplate code of the assignment.
  • Create a new private repository assignments-solved on GitHub. Clone the assignments repo on your machine, and push it to the assignments-solved repo (essentially fork your own repository as a private copy): git clone https://github.com/[user]/assignments assignments-solved cd assignments-solved git remote set-url origin https://github.com/[user]/assignments-solved git push origin master git push --all
  • Add the assignments-solved repo as remote to the assignments repo: cd assignments # change to the assignments repo on your machine git remote add solutions https://github.com/[user]/assignments-solved
  • Implement each assignment in the assignments-solved repository. Make sure that each commit contains only changes from one assignment.
  • You might want to create a solved branch in the assignments repo, so that the original assignments are not altered: cd assignments # change to the assignments repo on your machine git branch -b solutions git push -u origin
  • When you want to publish a solution into the assignments, fetch the solved remote and cherry-pick the commits containing the solutions. cd assignments # change to the assignments repo on your machine git checkout solved git fetch solutions git cherry-pick [commithash] Where [commithash] contains the commit of your solution.

You might be also able to implement the workflow by implementing each assignment in a separate branch of the assignments-solved repo and then creating a pull-request in the assignments repo. But I am not sure if this will work in GitHub, as the assignments-solved repo is not a real fork.

  • I've successfully used a similar method to separate a programming test from submitted answers. In my case, the submitted solutions are added to individual branches of a private clone, and never merged back to the public repo. It has the added benefit of letting me see what version of the test each candidate has solved, as it evolves over time. – axl Sep 19 '16 at 0:40
0

I just can to propose you an utility intended to .gitignore-ing and encrypt files in your repository. Workflow is slightly hard to use, but it make your files' encrypted counterparts avaliable at the working copy along with other non-secret files, which allows to track them by git as usual.

#!/bin/bash

set -o errexit
set -o pipefail
set -o nounset

version=1
OPTIND=1
verbose=0
mode="add"
recurse=()
files=()

while getopts ":vaslr:" opt
do
    case "$opt" in
        \?) echo "error: invalid option: -$OPTARG" >&2 ; exit 1
            ;;
        :)  echo "error: option -$OPTARG requires an argument" >&2 ; exit 1
            ;;
        v)  let "verbose++" ; echo "verbosity increased"
            ;;
        a)  mode="add"
            ;;
        s)  mode="save"
            ;;
        l)  mode="load"
            ;;
        r)  recurse+=("$OPTARG")
            ;;
    esac
done
shift $((OPTIND-1))
if [[ "${#recurse[@]}" != 0 ]] 
then
    for pattern in "${recurse[@]}" 
    do
        while IFS= read -d $'\0' -r file
        do
            files+=("$file")
        done < <(find . -name "$pattern" -type f -print0)
    done
else
    files=("$@")
fi

[[ "${#files[@]}" != 0 ]] || { echo "list of files to process is empty" >&2 ; exit 1 ; }

if [[ $mode == "add" ]]
then
    for file in "${files[@]}"
    do
        [[ -e $file ]] && cp "$file" "${file}.bak" || touch "$file"
        sshare_file="${file}.sshare"
        [[ -e $sshare_file ]] || { echo "$version" > "$sshare_file" ; git add --intent-to-add "$sshare_file" ; echo "$file" >> .gitignore ; echo "${file}.bak" >> .gitignore ; git add .gitignore ; }
    done
    exit 0
fi
tmp_dir=`mktemp --tmpdir -d sshare.XXXX`
read -r -s -p "enter password to $mode tracked files:" sshare_password && echo ;
for file in "${files[@]}"
do
    [[ ! -e $file ]] && touch "$file" || cp "$file" "${file}.bak"
    sshare_file="${file}.sshare"
    [[ -r $sshare_file ]] || { echo "warning: can't read file '$sshare_file' (file '$file' skipped)" >&2 ; continue ; }
    file_version=$(head -1 "$sshare_file")
    [[ "$file_version" == $version ]] || { echo "warning: version '$file_version' of '$sshare_file' file differs from version '$version' of script (file '$file' skipped)" >&2 ; continue ; }
    tmp_file="$tmp_dir/$file"
    mkdir -p "$(dirname "$tmp_file")"
    > "$tmp_file"
    line_number=0
    while IFS= read -r line
    do
        let "line_number++" || :
        [[ -n $line ]] || { echo "warning: empty line encountered at #$line_number in file '$sshare_file' (ignored)" >&2 ; continue ; }
        echo "$line" | openssl enc -d -A -base64 -aes256 -k "$sshare_password" | gunzip --to-stdout --force | patch "$tmp_file" --normal --quiet
    done < <(tail --lines=+2 "$sshare_file")
    if [[ $mode == "load" ]]
    then
        cp -f "$tmp_file" . || { echo "warning: can't write to file '$file' (file '$file' skipped)" >&2 ; continue ; }
    elif [[ $mode == "save" ]]
    then
        chunk=$(diff "$tmp_file" "$file" || :)
        [[ -n $chunk ]] || { echo "nothing to comit since last edit for file '$file'" ; continue ; }
        [[ -w $sshare_file ]] || { echo "warning: can't update sshare database '$sshare_file' (file '$file' skipped)" ; continue ; }
        echo "$chunk" | gzip --stdout | openssl enc -e -A -base64 -aes256 -k "$sshare_password" >> "$sshare_file"
        echo >> "$sshare_file"
        echo "changes encrypted for file '$file'"
    fi
done

To create secret file with filename a.txt type sshare -a a.txt. Utility create file a.txt and file added to .gitignore. Then it create encrypted "database" counterpart a.txt.sshare by adding .sshare extension to filename.

Then you can fill a.txt with some text. To save its state right before git commit type sshare -s a.txt, then utility prompt you for password to encrypt new state of the file a.txt. Then utilty using this password adds encrypted diff between previous and current state of file a.txt to the end of a.txt.sshare file.

After fetch/pull repository with encrypted files you should run sshare utility for each file using -l ("load") key. In this case utility decrypt *.sshare files to text files untracked by git in the working copy.

You may use different passwords for each secret file.

The utility allows git to track changes efficiently (diff of .sshare files is simply one line).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.