Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
emersonmello committed Jun 19, 2022
0 parents commit 75c67bb
Show file tree
Hide file tree
Showing 28 changed files with 1,676 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
on:
release:
types: [created]

jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
strategy:
matrix:
# build and publish in parallel: linux/386, linux/amd64, linux/arm64, darwin/amd64, darwin/arm64
goos: [linux, darwin]
goarch: ["386", amd64, arm64]
exclude:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v3
- uses: wangyoucao577/[email protected]
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
extra_files: LICENSE Readme.md
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work

### Go Patch ###
/vendor/
/Godeps/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Emerson Mello

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
159 changes: 159 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="images/logo-dark.png">
<img alt="Different logo for light and dark themes" src="images/logo.png">
</picture>

**claro** is a [GitHub Classroom](https://classroom.github.com) CLI for teachers

[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

# Overview

**claro** (**cla**ss**ro**om) is a cli tool that offers a simple interface that allows the teacher to clone all student repositories at once for grading and then send grades at once to all these repositories.

**claro** was inspired by the [Git-Gud-tool](https://github.com/NikolaiMagnussen/Git-Gud-tool) and it was created to make my life simpler as a teacher who relies on Github Classroom. It is suitable for scenarios where the teacher needs to manually grade each assignment and the Github Classroom autograding is not an option.

**claro** relies on [Github's REST API](https://docs.github.com/en/rest) because currently Github Classroom does not offer an API and it has features that are not present in [Github Classroom Assistant](https://classroom.github.com/assistant). The best one: it is a [CLI](https://clig.dev) :heart:!


**claro** provides:
- mass clone of Github repositories
- a template in Markdown for grading (it creates one file per repository)
- customization (commit message, grade sheet title, grading file, etc.)
- access to [Github Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) from operating system keyring (i.e. macOS Keychain, Gnome Keyring), environment var or claro's config file

## Requirements
- [git](https://git-scm.com/docs/git) command line application configured properly
```bash
git config --global user.email "[email protected]"
git config --global user.name "Your Name"
```
- [Git credential store](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage) configured properly
- See [section below](#authenticating-with-git-command-line-to-access-repositories-on-github)

## Download

- [Download Binaries for different platforms](https://github.com/emersonmello/claro/releases/latest)
- macOS
- darwin-amd64
- darwin-arm64
- Linux
- linux-386
- linux-amd64
- linux-arm64


## Usage

### Straight workflow

1. Create a Github Classroom assignment with a distinguish repository prefix
- Example: `2022-01-assignment-01`
2. Clone all repositories from a Github Classroom organization with a specific assignment prefix

![cloning](images/clone.gif)

3. Grade each student's work and write down the feedback in the respective Markdown file

![grading](images/grading.gif)

4. Push the grading

![pushing](images/push.gif)

## Other commands

![claro help message](images/claro.png)

### Pull students repositories

**claro** offers a `pull` command suitable for workflows where students make incremental deliveries to the same GitHub Classroom repository.

- Example:
```bash
claro pull 2022-01-assignment-01
```
### List all repositories which start with a specific prefix

Before cloning multiple repositories, you might want to check which repositories will be cloned with a specific prefix. **claro** offers the `list` command for that.

- Example:
```bash
claro list 2022-01-assignment-01
```

### Customize commit message, grading filename, grading string

The **claro** default strings are:

- **Grading filename:** `GRADING.md`
- It will be created and pushed to student repository
- **Grade string:** `Grade: `
- It will be inside grading file
- **Commit message:** `Graded project, the file containing the grade is in the root directory`
- It is the commit message
- **Grade sheet title** `Feedback`
- It will be inside grading file as title 1 (# Feedback)

You can change the values using **claro** `config` command. The new values will be stored in the **claro**'s config file (default `$HOME/.claro.env`).
- Examples:
- `claro config filename "Correcao.md"`
- `claro config grade "Nota: "`
- `claro config message "Correção finalizada, veja arquivo na raiz do repositório"`
- `claro config title "Comentários"`

## Storing claro's GitHub Personal Access Token in the OS keyring

**claro** only supports HTTPS remote URL (git over SSH is so [annoying](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github#authenticating-with-the-command-line)).

**claro** consumes [Github's REST API](https://docs.github.com/en/rest) to fetch the list of repositories (assignments) from a GitHub Classroom organization. So, **claro** uses a [GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token).
- **claro** will try to get GitHub Personal Access Token from: (1) operating system keyring; (2) environment var (`GH_TOKEN`); (3) **claro**'s config file (default `$HOME/.claro.env`)

You can inform the token whenever you clone repositories, or you can save the token in the operating system keyring (recommended) or in the **claro**'s config file. Please, have a look at [Authenticating with git command line to access repositories on GitHub](#authenticating-with-git-command-line-to-access-repositories-on-github).

- To save the token (**claro** will try to save to the OS keyring first and then to the config file.)
- `claro config token add`
- To delete the token from OS keyring
- `claro config token del`

## Authenticating with git command line to access repositories on GitHub



> Using an HTTPS remote URL has some advantages compared with using SSH. It's easier to set up than SSH, and usually works through strict firewalls and proxies. However, it also prompts you to enter your GitHub credentials every time you pull or push a repository. ([GitHub Docs](https://docs.github.com/en/get-started/getting-started-with-git/why-is-git-always-asking-for-my-password)).
To access repositories on GitHub from the command line application over HTTPS you must authenticate with a [GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github#authenticating-with-the-command-line).

You can avoid being prompted for your password (personal access token) by configuring Git to cache your credentials for you on [git credential storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). Git works with several credential helper:

- **Unsafe and simple way** (not recommended!)
- This method stores your password in plaintext on disk, protected only by filesystem permissions (default `$HOME/.git-credentials`)
- on Linux, macOS or Windows
```bash
git config --global credential.helper store
```
- The first time git will ask the username and password. Subsequent request will use the credentials from the store (default `$HOME/.git-credentials`).
- Example:
`https://username:[email protected]`
- **Safe with a little complexity**
- This method stores your password encrypted on disk in the operating system keyring service.
- You can use native operating system keyring
- Linux [Secret Service](https://specifications.freedesktop.org/secret-service/latest/) dbus interface, which is provided by [GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring)
- Use [Seahorse app](https://wiki.gnome.org/Apps/Seahorse) to create a default collection with `login` name
- Open `seahorse`; go to `file->new->password keyring`; when asked for a name, use: `login`

```bash
# ------------------------------------------------#
# on Ubuntu Linux 22.04 LTS
# ------------------------------------------------#
sudo apt-get install libsecret-1-0 libsecret-1-dev g++ make
cd /usr/share/doc/git/contrib/credential/libsecret && sudo make
git config --global credential.helper /usr/share/doc/git/contrib/credential/libsecret/git-credential-libsecret
# ------------------------------------------------#
```
- macOS Keychain (`/usr/bin/security`)
- `git config --global credential.helper osxkeychain`
- Multi-plataform (Linux, macOS or Windows)
- You can also use a credential helper like [Git Credential Manager](https://github.com/GitCredentialManager/git-credential-manager)
78 changes: 78 additions & 0 deletions cmd/clone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package cmd

import (
"fmt"
"os"

"github.com/emersonmello/claro/utils"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var OutputDir string

var cloneCmd = &cobra.Command{
Use: "clone <organization> <assignment repositories prefix>",
Short: "Clone all students assignment repositories in an organization",
Example: "claro clone ifsc-classroom 2022-01-assignment-01",
SilenceErrors: true,
Args: cobra.ExactArgs(2),

Run: func(cmd *cobra.Command, args []string) {

if OutputDir == "repository prefix" {
OutputDir = args[1]
}

if _, err := os.Stat(OutputDir); !os.IsNotExist(err) {
pterm.Error.Printf("The output directory '%s' already exists on the current directory!\n", OutputDir)
os.Exit(1)
}

repositories := utils.GetRepositoryList(args[0], args[1])

if len(repositories) <= 0 {
pterm.Warning.Printfln("Organization %s does not contains repositories with prefix %s\n", args[0], args[1])
os.Exit(0)
} else {
fmt.Printf("%d repositories were found\n", len(repositories))
}

if !utils.PromptUser("Would you like to proceed (Y/n)? ", "yes") {
os.Exit(0)
}

s, _ := pterm.DefaultSpinner.Start("Creating " + OutputDir + " directory")
if e := os.Mkdir(OutputDir, os.ModePerm); e != nil {
OutputDir = ""
s.Fail(e.Error())
os.Exit(1)
}
s.Success()

utils.CloneRepositories(OutputDir, repositories)

e := os.Chdir(OutputDir)
checkError(e)

s, _ = pterm.DefaultSpinner.Start("Creating Markdown files")
for _, r := range repositories {
f, e := os.Create("grade-" + r.Name + ".md")
if e != nil {
s.Fail(e.Error())
}
_, e = f.WriteString("# " + viper.GetString("grade_title") + "\n\n- ...\n- " + viper.GetString("grade_string") + " \n\n")
defer f.Close()
if e != nil {
s.Fail(e.Error())
}
}
s.Success()
},
}

func init() {
rootCmd.AddCommand(cloneCmd)
cloneCmd.Flags().StringVarP(&OutputDir, "output-directory", "o", "repository prefix", "Directory where the cloned repositories should be stored")
}
19 changes: 19 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Copyright © 2022 Emerson Mello
*/
package cmd

import (
"github.com/spf13/cobra"
)

// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
Short: "Configure claro's properties (github token, commit message, etc.)",
}

func init() {
rootCmd.AddCommand(configCmd)
}
25 changes: 25 additions & 0 deletions cmd/filename.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright © 2022 Emerson Mello
*/
package cmd

import (
"github.com/emersonmello/claro/utils"
"github.com/spf13/cobra"
)

// filenameCmd represents the filename command
var filenameCmd = &cobra.Command{
Use: "filename <filename.md>",
Example: "claro config filename \"GRADING.md\" ",
Short: "define the name of the file that will be created in the student repository containing the feedback",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
utils.WriteConfigFile("grade_filename", args[0], "Grade sheet filename has been set successfully!")
},
}

func init() {
configCmd.AddCommand(filenameCmd)
}
25 changes: 25 additions & 0 deletions cmd/grade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright © 2022 Emerson Mello
*/
package cmd

import (
"github.com/emersonmello/claro/utils"
"github.com/spf13/cobra"
)

// gradeCmd represents the grade command
var gradeCmd = &cobra.Command{
Use: "grade <\"string\">",
Example: "claro config grade \"Grade: \"",
Short: "define the grade string inserted in the file representing the grade sheet",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
utils.WriteConfigFile("grade_string", args[0], "Grade string has been set successfully!")
},
}

func init() {
configCmd.AddCommand(gradeCmd)
}
Loading

0 comments on commit 75c67bb

Please sign in to comment.