|
package kiln
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
)
// Holds the bare minimum amount of information required to talk to a Kiln instance
type Client struct {
credentials *credential
}
// Stores kiln credentials in the Kiln configuration file
type credential struct {
// Base URL of Kiln instance
KilnUrl string `json:"kilnUrl"`
// User for whom this token applies
User string `json:"user"`
// Kiln API token
Token string `json:"token"`
}
type apiParams map[string]string
type Credentials map[string]map[string]string
func NewClient(kilnUrl *url.URL) *Client {
user := ""
if kilnUrl.User != nil {
user = kilnUrl.User.Username()
}
return &Client{&credential{KilnUrl: kilnUrl.String(), User: user}}
}
func (k *Client) LoadCredentials() bool {
if creds, err := loadCredentials(); err == nil {
if token, ok := creds[k.credentials.User][k.credentials.KilnUrl]; ok {
k.credentials.Token = token
return true
}
}
return false
}
func (k *Client) StoreCredentials() (err error) {
creds, _ := loadCredentials()
if _, ok := creds[k.credentials.User]; !ok {
creds[k.credentials.User] = make(map[string]string)
}
creds[k.credentials.User][k.credentials.KilnUrl] = k.credentials.Token
err = creds.storeCredentials()
return
}
func (k *Client) DeleteCredentials() (err error) {
creds, err := loadCredentials()
if err != nil {
return
}
if user, ok := creds[k.credentials.User]; ok {
delete(user, k.credentials.KilnUrl)
}
err = creds.storeCredentials()
return
}
// Logs a user into Kiln, returning true and storing their token in the
// Client if successful, and returning an error otherwise
func (k *Client) Logon() error {
login, password := requestUserCredentials()
resp, err := k.apiGet("Auth/Login", apiParams{"sUser": login, "sPassword": password})
if err != nil {
return fmt.Errorf("unable to contact Kiln: %v\n", err)
}
var errors ApiErrors
if err = json.Unmarshal(resp, &errors); err == nil {
if err, _ := errors["errors"]; len(err) > 0 {
return fmt.Errorf("failed: %v\n", err[0].Description)
}
}
if err = json.Unmarshal(resp, &k.credentials.Token); err != nil {
return fmt.Errorf("failed to parse token: %v", err)
}
return nil
}
// Makes sure the client has credentials, taking them through the logon
// sequence if not
func (k *Client) EnsureCredentials() error {
if k.credentials.Token == "" && !k.LoadCredentials() {
if err := k.Logon(); err != nil {
return err
}
k.StoreCredentials()
}
return nil
}
// Resolve a Git SHA
func (k *Client) ResolveSHA(commit string) (string, error) {
if out, err := exec.Command("git", "rev-parse", commit).CombinedOutput(); err == nil {
commit := strings.TrimSpace(string(out))
if strings.HasPrefix(commit, "fatal:") {
return "", fmt.Errorf("commit couldn't be resolved (try \"git fetch\" first)")
} else {
return commit, nil
}
}
return "", fmt.Errorf("commit couldn't be resolved (try \"git fetch\" first)")
}
// Browses the history tab of the repository
func (k *Client) BrowseHistory(repo string) error {
return browse(k.repoRoute(repo, ""))
}
// Browse the settings tab for the repository
func (k *Client) BrowseSettings(repo string) error {
return browse(k.repoRoute(repo, "Settings"))
}
// Browse the related tab for repository
func (k *Client) BrowseRelated(repo string) error {
return browse(k.repoRoute(repo, "Related"))
}
// Browse a commit, expanding out to the full SHA beforehand
func (k *Client) BrowseCommit(repo string, commit string) (err error) {
if commit, err = k.ResolveSHA(commit); err == nil {
err = browse(k.repoRoute(repo, "History/"+commit))
}
return
}
// Browse a file in Kiln
func (k *Client) BrowseFile(repo string, file string) error {
path, err := repoRelativePath(file)
if err != nil {
return err
}
return browse(k.repoRoute(repo, fmt.Sprintf("Files%v", path)))
}
// Browse an annotated file in Kiln
func (k *Client) BrowseAnnotatedFile(repo string, file string) error {
path, err := repoRelativePath(file)
if err != nil {
return err
}
return browse(k.repoRoute(repo, fmt.Sprintf("Files%v?view=annotate", path)))
}
// Browse a file in Kiln
func (k *Client) BrowseFileHistory(repo string, file string) error {
path, err := repoRelativePath(file)
if err != nil {
return err
}
return browse(k.repoRoute(repo, fmt.Sprintf("FileHistory%v", path)))
}
// Find the root of the Git repo
func GitRoot() (path string, err error) {
out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
if err != nil {
return
}
path = strings.TrimSpace(string(out))
if strings.HasPrefix(path, "fatal:") {
err = fmt.Errorf("unable to find root: %v", path)
}
return
}
// Opens a web browser in a cross-platform way
func browse(location string) error {
switch runtime.GOOS {
case "linux":
return exec.Command("xdg-open", location).Start()
case "windows":
return exec.Command("cmd", "/c", "start", location).Start()
case "darwin":
return exec.Command("open", location).Start()
default:
return fmt.Errorf("%v is an unsupported platform", runtime.GOOS)
}
}
// Change a relative or absolute path into a path relative to the repository root
func repoRelativePath(path string) (string, error) {
root, err := GitRoot()
if err != nil {
return "", err
}
absPath, err := filepath.Abs(path)
if err != nil {
return "", err
}
absPath = filepath.ToSlash(absPath)
return strings.TrimPrefix(absPath, root), nil
}
// Returns the full URL for relative Kiln URL
func (k *Client) kilnRoute(route string) string {
return strings.TrimRight(k.credentials.KilnUrl, "/") + "/" + strings.TrimLeft(route, "/")
}
// Returns the full URL for an API call in Kiln
func (k *Client) apiRoute(route string) string {
return k.kilnRoute("Api/1.0/" + strings.TrimLeft(route, "/"))
}
// Returns the full URL for a given API call or Kiln route
func (k *Client) repoRoute(repo string, action string) string {
return k.kilnRoute(fmt.Sprintf("Code/%v/%v", repo, action))
}
// Returns the body from an API call via HTTP GET
func (k *Client) apiGet(route string, params apiParams) ([]byte, error) {
return k.apiRequest(route, params, "GET")
}
// Returns the body from an API call via HTTP POST
func (k *Client) apiPost(route string, params apiParams) ([]byte, error) {
return k.apiRequest(route, params, "POST")
}
func (k *Client) apiRequest(route string, params apiParams, method string) ([]byte, error) {
v := url.Values{}
for key, value := range params {
v.Set(key, value)
}
if k.credentials.Token != "" {
v.Set("token", k.credentials.Token)
}
var resp *http.Response
var err error
if method == "GET" {
resp, err = http.Get(k.apiRoute(route) + "?" + v.Encode())
} else if method == "POST" {
resp, err = http.PostForm(k.apiRoute(route), v)
}
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
// Encode a path by Kiln's hex encoding
func hexEncoded(path string) string {
utf8 := []byte(path)
hexBytes := make([]string, len(utf8))
for idx, b := range utf8 {
hexBytes[idx] = fmt.Sprintf("%x", b)
}
return strings.Join(hexBytes, "")
}
// Request new credentials from the user, securely
func requestUserCredentials() (login, password string) {
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("Login: ")
scanner.Scan()
login = scanner.Text()
if strings.Trim(login, "\t ") == "" {
fmt.Println("Please enter your Kiln name or email address")
continue
}
password, _ = getPass("Password: ")
return
}
}
// Load any existing credentials from the user's credential store
func loadCredentials() (credentials Credentials, err error) {
credentials = make(Credentials)
path := filepath.Join(configDirectory(), "kiln_client.json")
fd, err := os.Open(path)
if err != nil {
return
}
defer fd.Close()
data, err := ioutil.ReadAll(fd)
if err != nil {
return
}
var creds []credential
if err = json.Unmarshal(data, &creds); err != nil {
return
}
for _, cred := range creds {
if _, ok := credentials[cred.User]; !ok {
credentials[cred.User] = make(map[string]string)
}
credentials[cred.User][cred.KilnUrl] = cred.Token
}
return
}
// Store all credentials in the credential store, overwriting any already present
func (credentials Credentials) storeCredentials() (err error) {
if err = os.MkdirAll(configDirectory(), 0700); err != nil {
return
}
path := filepath.Join(configDirectory(), "kiln_client.json")
fd, err := os.Create(path)
if err != nil {
return
}
defer fd.Close()
creds := make([]*credential, 0, 10)
for user, urls := range credentials {
for url, token := range urls {
creds = append(creds, &credential{KilnUrl: url, User: user, Token: token})
}
}
data, _ := json.Marshal(creds)
_, err = io.Copy(fd, bytes.NewReader(data))
return
}
// Finds the directory in which to store Kiln files. Platform-dependent.
func configDirectory() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("APPDATA"), "Kiln")
default:
usr, err := user.Current()
if err != nil {
panic("unable to determine current user")
}
return filepath.Join(usr.HomeDir, ".config", "kiln")
}
}
|
Loading...