|
package main
import (
"fmt"
"net/url"
"os"
"os/exec"
"strings"
"developers.kilnhg.com/Code/Kiln/Group/gitkiln/kiln"
)
const documentation = `
USAGE: git-kiln [<command> [args]]
git-kiln provides quick access to the Kiln repository associated with this
repository. To work, git-kiln must be run from within a Git repository
that either has a Kiln HTTP repository as the upstream remote, or has the
config value "kiln.url" set to an HTTP-accessible Kiln repository. The
latter is helpful if you access Kiln via SSH, or have an alternative
repository as your upstream remote.
Commands:
add-remote [--ssh] <repository> [<remote-alias>]
add the provided repository as a remote; if no local
alias is provided, then the name of the repository will
be used as the remote
annotate <file> [<file2> [...]]
annotate one or more files in Kiln
create-branch <branch-name>
create a branch repository in Kiln, which can be added
to your remotes via "gitkiln add-remote"
filehistory <file> [<file2> [...]]
show the history for one or more files in Kiln
hg-sha <sha> [<sha> [...]]
show the equivalent Mercurial SHAs corresponding to the
given Git SHAs
history
go to the history of this repository in Kiln
logout
log out of Kiln, disposing your authentication token
related
show related repositories in Kiln, including the
entire graph of which commits have and have not been
merged into branch repositories
settings
go to the settings tab for this repository in Kiln
show <commit> [<commit2> [...]]
go to the provided commits in Kiln. Commit IDs are expanded
locally, so "git-kiln show HEAD^" and similar commands will
work fine.
show-file <file> [<file2> [...]]
show the contents of one or more files in Kiln
version
show gitkiln's version number
If you provide no command, git-kiln will take you to the repository in
Kiln.
`
const kilnUrlSuffix = "kiln/"
func extractUrlsAndPaths(s string) (base *url.URL, path string, err error) {
out, err := exec.Command("git", "config", "kiln.url").Output()
if err == nil && len(out) > 0 {
base, err = url.Parse(strings.TrimSpace(string(out)))
} else {
base, err = url.Parse(s)
if err != nil || (base.Scheme != "http" && base.Scheme != "https") {
err = fmt.Errorf("must either have an HTTP remote or kiln.url specified in config")
return
}
}
components := strings.Split(base.Path, "/")
l := len(components)
components[l-1] = strings.TrimSuffix(components[l-1], ".git")
path = strings.Join(components[l-3:l], "/")
if strings.HasSuffix(base.Host, ".kilnhg.com") {
base.Path = ""
} else {
idx := strings.Index(base.Path, kilnUrlSuffix)
if idx < 0 {
err = fmt.Errorf("not sure where Kiln is based on %v", base)
return
}
base.Path = base.Path[0 : idx+len(kilnUrlSuffix)]
}
return
}
func isGitAvailable() bool {
if err := exec.Command("git", "version").Run(); err != nil {
return false
} else {
return true
}
}
func showHelp() {
fmt.Println(documentation)
}
func ensureArgs(n int, explanation string) bool {
if len(os.Args) < n {
fmt.Fprintln(os.Stderr, explanation)
showHelp()
return false
} else {
return true
}
}
func requireAuth(k *kiln.Client) {
if err := k.EnsureCredentials(); err != nil {
fmt.Fprintf(os.Stderr, "unable to logon: %v\n", err)
os.Exit(1)
}
}
func splitRepoPath(repoPath string) (project, group, repo string) {
parts := strings.Split(repoPath, "/")
repo = parts[len(parts)-1]
if len(parts) > 1 {
group = parts[len(parts)-2]
}
if len(parts) > 2 {
project = parts[len(parts)-3]
}
return
}
func findTargets(related []kiln.Repo, project, group, repo string) []kiln.Repo {
targets := make([]kiln.Repo, 0)
for _, r := range related {
if r.Slug == repo &&
(len(group) == 0 || r.GroupSlug == group) &&
(len(project) == 0 || r.ProjectSlug == project) {
targets = append(targets, r)
}
}
return targets
}
func dispatch(k *kiln.Client, repoPath string) {
var command string
if len(os.Args) == 1 {
command = "history"
} else {
command = os.Args[1]
}
switch command {
case "add-remote":
if len(os.Args) < 3 || len(os.Args) > 5 {
fmt.Fprintf(os.Stderr, "you must provide a repo to add as the remote")
showHelp()
return
}
requireAuth(k)
args := os.Args[2:len(os.Args)]
ssh := false
if args[0] == "--ssh" {
ssh = true
args = args[1:len(args)]
}
related, err := k.RelatedRepos(repoPath)
if err != nil {
fmt.Fprintf(os.Stderr, "unable to query related repos: %v\n", err)
return
}
project, group, repo := splitRepoPath(args[0])
targets := findTargets(related, project, group, repo)
if len(targets) == 0 {
fmt.Fprintf(os.Stderr, "nothing found; you must use exact matches\n")
} else if len(targets) > 1 {
fmt.Fprintf(os.Stderr, "too many matches; please provide more components\n")
fmt.Fprintf(os.Stderr, "Matches:\n")
for _, r := range targets {
fmt.Fprintf(os.Stderr, "\t%v/%v/%v\n", r.ProjectSlug, r.GroupSlug, r.Slug)
}
fmt.Fprintln(os.Stderr, "")
} else {
var remoteName string
if len(args) == 2 {
remoteName = args[1]
} else {
remoteName = repo
}
var remoteURL string
if ssh {
remoteURL = targets[0].GitSshUrl
} else {
remoteURL = targets[0].GitUrl
}
out, err := exec.Command("git", "remote", "add", remoteName, remoteURL).CombinedOutput()
if err != nil || strings.HasPrefix(string(out), "fatal: ") {
fmt.Fprintf(os.Stderr, "could not add remote: %s", out)
}
}
case "annotate":
if ensureArgs(3, "you must provide at least one file to annotate") {
for _, file := range os.Args[2:len(os.Args)] {
k.BrowseAnnotatedFile(repoPath, file)
}
}
case "create-branch":
if ensureArgs(3, "you must provide a branch name to create") {
requireAuth(k)
r, err := k.CreateBranch(repoPath, os.Args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "could not create branch: %v\n", err)
} else {
fmt.Printf("created: %v\n", r.GitUrl)
}
}
case "filehistory":
if ensureArgs(3, "you must provide at least one file to view") {
for _, file := range os.Args[2:len(os.Args)] {
k.BrowseFileHistory(repoPath, file)
}
}
case "help":
showHelp()
case "hg-sha":
if ensureArgs(3, "you must provide at least one commit SHA to lookup") {
requireAuth(k)
commits := make([]string, 0, len(os.Args)-2)
for _, commit := range os.Args[2:len(os.Args)] {
if fullSHA, err := k.ResolveSHA(commit); err != nil {
fmt.Fprintf(os.Stderr, "WARNING: Unable to resolve %v\n", commit)
} else {
commits = append(commits, fullSHA)
}
}
equivalents, err := k.MercurialEquivalents(repoPath, commits)
if err != nil {
fmt.Printf("Unable to look up Mercurial SHAs: %v\n", err)
} else {
for gitSHA, hgSHAs := range equivalents {
fmt.Printf("Git[%v]: Hg%v\n", gitSHA, hgSHAs)
}
}
}
case "history":
k.BrowseHistory(repoPath)
case "logout":
if k.LoadCredentials() {
k.DeleteCredentials()
}
case "related":
k.BrowseRelated(repoPath)
case "settings":
k.BrowseSettings(repoPath)
case "show":
if ensureArgs(3, "you must supply at least one commit SHA to show") {
for _, commit := range os.Args[2:len(os.Args)] {
if err := k.BrowseCommit(repoPath, commit); err != nil {
fmt.Fprintf(os.Stderr, "couldn't load commit: %v\n", err)
}
}
}
case "show-file":
if ensureArgs(3, "you must provide at least one file to view") {
for _, file := range os.Args[2:len(os.Args)] {
k.BrowseFile(repoPath, file)
}
}
case "version":
fmt.Println(version)
default:
showHelp()
}
}
func main() {
if !isGitAvailable() {
fmt.Fprintln(os.Stderr, "git not found")
os.Exit(1)
}
if _, err := kiln.GitRoot(); err != nil {
if len(os.Args) == 2 && os.Args[1] == "help" {
showHelp()
return
} else {
fmt.Fprintln(os.Stderr, "git-kiln must be run from within a Git repository; see \"gitkiln help\"")
os.Exit(1)
}
}
bytesOut, err := exec.Command("git", "ls-remote", "--get-url").Output()
repoUrl := strings.TrimSpace(string(bytesOut))
if err != nil || strings.HasPrefix(repoUrl, "fatal:") {
fmt.Fprintln(os.Stderr, "no remote found; set one with \"git remote add\"")
os.Exit(1)
}
kilnUrl, repoPath, err := extractUrlsAndPaths(repoUrl)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
k := kiln.NewClient(kilnUrl)
dispatch(k, repoPath)
}
|
Loading...