Exercises for use in the GitStream interactive Git tutor.
- Creating a New Exercise
- Building the Exercises
- Configuration File Format
- Exercise Debugging Workflow
- Start by creating a new directory for the exercise in
src/exercises
. This will be the name of the repository that users clone.
The name should have the formatN-exerciseName
where N is a number used to order the exercises. Omitting theN-
will cause the exercise to be ignored by GitStream. - In the new exercise directory, create a configuration file,
conf.js
. - If your exercise requires additional files (perhaps for committing to the exercise
repo or comparing with files in the repo), create the
resources
subdirectory.
Great! Now you're ready to configure the exercise!
Add any necessary resources to the resources/
folder and then continue on to
populating the conf file.
When you are finished designing an exercise, build them by running
$ ./createx.js
Remember to either link your gitstream-exercises
directory into GitStream's or edit the
GitStream package.json
to point gitstream-exercises
to your own repo!
The following is a minimal template for the conf.js
file.
'use strict'
// place constants and static functions here
module.exports = {
// conf that applies to exercise (in general)
global: {
},
// description of server-side state machine
machine: {
startState: 'aDoneState', // name of the start state
aDoneState: null // null means done
},
// description of what the user sees in-browser
viewer: {
title: 'The title of the exercise',
steps: {
// HTML descriptions of the goal of each step
},
feedback: {
// HTML shown beneath the step when
// transitioning from one state to another
}
},
// optional: conf for custom generation of exercise repo
repo: {
// description of the commits to be made
}
}
- No specific constants are required. The
timeLimited
constant was previously required but has since been deprecated [January 2024].
startState:String
-
The name of the state (step) on which the exercise starts.
This state must be present in the machine
object.
Other than the startState
, keys are the names of states and their values can be one of the following:
String
- Steps into the named state.loopyState: 'loopyState'
is probably a bad idea.null
- Denotes a halt state. Stepping into a halt state causes the "Done!" step to be highlighted.Object
- The most common (and useful) option is an object mapping event names to callback functions performed when the event is triggered.
Ex.onReceive: function( repo, action, info, done )
For every event (except 404
and receive
), there are two options for registering callbacks.
The first and most common mode is as a listener and the second is as a handler.
The difference is that the handler is called synchronously (i.e. the exercise will not continue until the handler calls the provided callbacks or times out).
The key name in the state object and callback function signature is as follows:
onEventName: function( repo, action, info, done )
repo:String
- the name of the repo (e.g. nhynes/gitstream)action:String
- the name of the event that triggered this callback (which is usually known in advance)info:Object
- additional information associated with the action (see below)done:Function
- call with a state name or null to step into that state or with no arguments to remain in the current state without triggering a step
handleEventName: function( repo, action, info, gitDone, stepDone )
repo:String
- the name of the repoaction:String
- the name of the event that triggered this callbackinfo:Object
- additional information associated with the action (see below)gitDone:Function
- call with no arguments to allow the in-progress Git operation to complete or call withexitCode:Number
and optional feedback to be displayed in the terminal. Depending on the operation, a non-zero exit code will prevent its completion (see githooks)stepDone:Function
- accepts the same arguments asdone
, above, with a final positional argument passed to the viewer asstepOutput
.
Event | Description | info |
---|---|---|
Clone | Happens when a user clones the already-created repository. | |
PostCheckout* | Happens when a user checks out a ref |
prevHead:String - the ref of the prevous HEADnewHead:String - the ref of the new HEADchBranch:Boolean - whether the branche changed |
(Pre|Post)Commit* | Happens after the user enters a commit message but before the commit is recorded or after the commit. PreCommit works well as a handler since cancelling the event prevents the commit from being recorded. |
msg:String - the full text of the log message (may contain comments inserted by Git)
|
(Pre)Info | Happens when Git requests information about a remote repo. Notably, this occurs before a pull. | |
Merge | Happens after a non-conflicting merge | wasSquash:Boolean - whether the merge was a squash |
(Pre)Pull |
Happens before or after a pull (and before Merge).
This is another good place to use a handler.
Note that local Git may display unexpected error messages when the remote
changes between an info and the actual operation.
|
head:String - the requested HEAD |
(Pre)Push | Happens before or after a push (and before PreReceive). A handler here can stop a push from ever reaching the remote. |
last:String - The oldest commit in the pushhead:String - The newest commit in the pushbranch:String - The name of the pushed-to branch
|
(Pre)Rebase | Occurs before a rebase | (TODO: this should forward along Git-provided data) |
(Pre)Receive | Happens before or after the remote repo receives a push (and after the PrePush). This is another good place to use a handler, except if trying to make a commit that changes the location of the requested HEAD, which causes an "unable to lock" error. |
name:String - name of the updated refold:String - old SHA pointed to by the refnew:String - new SHA pointed to by the ref
|
* Updates the Shadowbranch, a ref containing the contents of the repository.
Note: tag events can also be detected, but it requires a fix that is in a seperate PR branch (let me know if you want it merged into master).
The this
of an event callback contains several helpful methods for interacting with
the repo and automating common tasks (see the docs).
title:String
- The title of the exercise visible on the main page and at the top of the exercise page.steps:Object
- An object mapping from step names (the same ones as in themachine
section). The halt states should not be included here.feedback:Object
- An object with keys that are the names states oronEnter
. The value is a function with the following signature:
function( stepOutput, cb )
wherestepOutput:Object
has keysprev
andnew
that contain the data provided to the callbacks of the previous and new (current) statescb:Function
can optionally be called with a string that is displayed under the newly-entered state
commits:Array
- an array of commit specs
Note: templating is done using Mustache syntax.
- There can only be one handler for a given event type and GitStream, itself, handles the 404 and Receive events.
- The monolithic conf files are split before being sent to the client and server.
The
machine
andrepo
sections are run on the server and can includerequire
calls, but theviewer
section can only contain environment-agnostic JS.
Additionally, the variables and functions placed at the top of the conf file must also be environment-agnostic because they are inlined into theviewer
file.
- Edit the exercise
- Build the exercises
- If you changed a
viewer
section, restart the GitStream server and view your changes viamake run