initial commit [141.14.129.234,mike]
This commit is contained in:
+646
@@ -0,0 +1,646 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
optRe = regexp.MustCompile(`^-(\w)$`)
|
||||
numRe = regexp.MustCompile(`^\d+$`)
|
||||
lsRepoRe = regexp.MustCompile(`git\s+git\s+\d+\s+(.*)\.git$`)
|
||||
lsArchRe = regexp.MustCompile(`git\s+git\s+\d+\s+(.*)\.git\.tar\.gz$`)
|
||||
gitDirRe = regexp.MustCompile(`^(.*)\.git$`)
|
||||
sanRe = regexp.MustCompile(`[,;:\\/='"|?><-]+`)
|
||||
wsRe = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
// requireRepo checks that a project with a .git is active, printing an error
|
||||
// otherwise. Returns true when it is safe to proceed.
|
||||
func requireRepo() bool {
|
||||
if PRJ == "" {
|
||||
errorln("no project selected")
|
||||
return false
|
||||
}
|
||||
if !isDir(DIR + "/.git") {
|
||||
errorln("error, no repository (.git) found")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// runCommand parses one command line and dispatches it. It returns false when
|
||||
// the shell should terminate (quit/exit).
|
||||
func runCommand(line string) bool {
|
||||
return runCommandDepth(line, 0)
|
||||
}
|
||||
|
||||
// runCommandDepth is runCommand with an alias-expansion recursion counter.
|
||||
func runCommandDepth(line string, depth int) bool {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// A leading '!' runs the rest of the line as a shell command. Nothing else
|
||||
// is forwarded to the shell any more.
|
||||
if strings.HasPrefix(line, "!") {
|
||||
if sh := strings.TrimSpace(line[1:]); sh != "" {
|
||||
forwardShell(sh)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Expand a user-defined alias in the first word, then re-dispatch. The
|
||||
// depth guard prevents runaway/cyclic expansion.
|
||||
if depth < maxAliasDepth {
|
||||
if f := strings.Fields(line); len(f) > 0 {
|
||||
if body, ok := aliases[f[0]]; ok && !isBuiltin(f[0]) {
|
||||
return runCommandDepth(expandAlias(body, f[1:]), depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
|
||||
// separate `-x` option flags from positional words.
|
||||
var words []string
|
||||
opt := map[string]bool{}
|
||||
for _, f := range fields {
|
||||
if m := optRe.FindStringSubmatch(f); m != nil {
|
||||
opt[m[1]] = true
|
||||
} else {
|
||||
words = append(words, f)
|
||||
}
|
||||
}
|
||||
|
||||
switch word(words, 0) {
|
||||
|
||||
case "": // empty line or options only — nothing to do
|
||||
|
||||
case "quit", "exit":
|
||||
return false
|
||||
|
||||
case "help":
|
||||
help()
|
||||
|
||||
case "alias": // list, show or define a command alias (persisted)
|
||||
handleAlias(line)
|
||||
|
||||
case "unalias": // remove a command alias
|
||||
handleUnalias(word(words, 1))
|
||||
|
||||
case "rescan": // refresh the cached server repository list
|
||||
rescanServer()
|
||||
fmt.Println("server repository list refreshed")
|
||||
|
||||
case "dist": // cp changed files to another directory/repository
|
||||
ddir := BASE + "/dist/" + PRJ
|
||||
if w := word(words, 1); w != "" {
|
||||
ddir = w
|
||||
}
|
||||
if !isDir(ddir) {
|
||||
errorln(fmt.Sprintf("dist path not found (%s)", ddir))
|
||||
break
|
||||
}
|
||||
n := 0
|
||||
out, _ := gitCapture(DIR, "ls-files")
|
||||
for _, f := range splitLines(out) {
|
||||
if f == "" || strings.HasPrefix(f, "private") {
|
||||
continue
|
||||
}
|
||||
src := filepath.Join(DIR, f)
|
||||
dst := filepath.Join(ddir, f)
|
||||
if !filesEqual(src, dst) {
|
||||
fmt.Println(f)
|
||||
if err := copyFile(src, dst); err != nil {
|
||||
fmt.Fprintln(os.Stderr, col(cRed, fmt.Sprintf("copy %s: %v", f, err)))
|
||||
continue
|
||||
}
|
||||
n++
|
||||
}
|
||||
}
|
||||
fmt.Printf("%d files copied to %s\n", n, ddir)
|
||||
|
||||
case "list": // list repositories on the git server
|
||||
path := "."
|
||||
if opt["a"] {
|
||||
path = "./archive"
|
||||
}
|
||||
pat := word(words, 1)
|
||||
lines, _ := sshOut("/bin/ls -ltr " + path)
|
||||
for _, ln := range lines {
|
||||
if pat != "" && !strings.Contains(strings.ToLower(ln), strings.ToLower(pat)) {
|
||||
continue
|
||||
}
|
||||
re := lsRepoRe
|
||||
if opt["a"] {
|
||||
re = lsArchRe
|
||||
}
|
||||
if m := re.FindStringSubmatch(ln); m != nil {
|
||||
fmt.Println(colorRepoLine(m[1]))
|
||||
}
|
||||
}
|
||||
|
||||
case "show": // show a repository's log directly on the server
|
||||
prj := PRJ
|
||||
if w := word(words, 1); w != "" {
|
||||
prj = w
|
||||
}
|
||||
found := 0
|
||||
lines, _ := sshOut("/bin/ls .")
|
||||
for _, ln := range lines {
|
||||
if strings.TrimSpace(ln) == prj+".git" {
|
||||
found++
|
||||
}
|
||||
}
|
||||
if found == 1 {
|
||||
logLines, _ := sshOut("cd " + cfg.GitPath + "/" + prj + ".git && git log --reverse --format='%h %ct %s'")
|
||||
repolog(logLines)
|
||||
} else {
|
||||
errorln("repository not found")
|
||||
}
|
||||
|
||||
case "log":
|
||||
if !requireRepo() {
|
||||
break
|
||||
}
|
||||
out, _ := gitCapture(DIR, "log", "--reverse", "--format=%h %ct %s")
|
||||
repolog(splitLines(out))
|
||||
|
||||
case "status": // short git status of the active project (-a: all projects)
|
||||
if opt["a"] {
|
||||
overviewAll()
|
||||
break
|
||||
}
|
||||
if !requireRepo() {
|
||||
break
|
||||
}
|
||||
git(DIR, "status", "-sb")
|
||||
|
||||
case "overview": // status summary of all projects under BASE
|
||||
overviewAll()
|
||||
|
||||
case "diff": // git diff of the active project (with optional args)
|
||||
if !requireRepo() {
|
||||
break
|
||||
}
|
||||
git(DIR, append([]string{"diff"}, fields[1:]...)...)
|
||||
|
||||
case "pull":
|
||||
if !requireRepo() {
|
||||
break
|
||||
}
|
||||
gitOK(DIR, "pull")
|
||||
|
||||
case "fetch":
|
||||
if !requireRepo() {
|
||||
break
|
||||
}
|
||||
gitOK(DIR, "fetch")
|
||||
|
||||
case "push": // commit everything and push to the server
|
||||
if PRJ == "" {
|
||||
errorln("no project selected")
|
||||
break
|
||||
}
|
||||
if fileExists(DIR + "/push.pl") {
|
||||
runInDir(DIR, "perl", DIR+"/push.pl")
|
||||
}
|
||||
if !isDir(DIR + "/.git") {
|
||||
errorln("error, no repository (.git) found")
|
||||
break
|
||||
}
|
||||
comment := strings.Join(fields[1:], " ")
|
||||
git(DIR, "add", "--all", ".")
|
||||
msg := strings.TrimSpace(fmt.Sprintf("[%s@%s] %s", USER, HOST, comment))
|
||||
git(DIR, "commit", "-m", msg) // may be "nothing to commit"; continue anyway
|
||||
if !gitOK(DIR, "push") {
|
||||
break
|
||||
}
|
||||
sshOK("touch " + cfg.GitPath + "/" + PRJ + ".git")
|
||||
if truthy(cfg.Mirror) && cfg.RemoteURL != "" && cfg.RemoteKey != "" {
|
||||
handlePushRemote("") // auto-mirror to the public server
|
||||
}
|
||||
|
||||
case "pushremote": // mirror the repo to a public git server via its API
|
||||
handlePushRemote(strings.Join(fields[1:], " "))
|
||||
|
||||
case "edit": // interactively edit the last N commits
|
||||
if !requireRepo() {
|
||||
break
|
||||
}
|
||||
num := "10"
|
||||
if w := word(words, 1); numRe.MatchString(w) {
|
||||
num = w
|
||||
}
|
||||
if !gitOK(DIR, "rebase", "-i", "HEAD~"+num) {
|
||||
break
|
||||
}
|
||||
if yesno("force-push rewritten history?", false) {
|
||||
gitOK(DIR, "push", "--force")
|
||||
}
|
||||
|
||||
case "archive": // snapshot the server-side repository into ./archive
|
||||
if PRJ == "" {
|
||||
errorln("no project selected")
|
||||
break
|
||||
}
|
||||
comment := sanitizeComment(strings.Join(fields[1:], " "))
|
||||
z := archiveStamp()
|
||||
name := PRJ + "_" + z
|
||||
if comment != "" {
|
||||
name = PRJ + "_" + z + "_" + comment
|
||||
}
|
||||
if !sshOK("cp -r " + PRJ + ".git archive/" + name + ".git") {
|
||||
break
|
||||
}
|
||||
if !sshOK("cd archive;tar cvzf " + name + ".git.tar.gz " + name + ".git") {
|
||||
break
|
||||
}
|
||||
sshOK("rm -rf archive/" + name + ".git")
|
||||
|
||||
case "init": // create a new repository from the current directory
|
||||
if PRJ == "" {
|
||||
errorln("no project selected")
|
||||
break
|
||||
}
|
||||
if fileExists(DIR + "/push.pl") {
|
||||
runInDir(DIR, "perl", DIR+"/push.pl")
|
||||
}
|
||||
if REPO == "" || yesno("overwrite existing repository?", false) {
|
||||
if !sshOK("rm -rf " + cfg.GitPath + "/" + PRJ + ".git") {
|
||||
break
|
||||
}
|
||||
if !sshOK("mkdir " + cfg.GitPath + "/" + PRJ + ".git;cd " + cfg.GitPath + "/" + PRJ + ".git;git --bare init") {
|
||||
break
|
||||
}
|
||||
gi := DIR + "/.gitignore"
|
||||
if !fileExists(gi) || yesno("overwrite existing .gitignore?", false) {
|
||||
os.WriteFile(gi, []byte(gitignore), 0644)
|
||||
}
|
||||
os.RemoveAll(DIR + "/.git")
|
||||
if !gitOK(DIR, "init") {
|
||||
break
|
||||
}
|
||||
if !gitOK(DIR, "remote", "add", "origin", URL+"/"+PRJ+".git/") {
|
||||
break
|
||||
}
|
||||
if !gitOK(DIR, "add", ".") {
|
||||
break
|
||||
}
|
||||
if !gitOK(DIR, "commit", "-m", fmt.Sprintf("initial commit [%s,%s]", IP, USER)) {
|
||||
break
|
||||
}
|
||||
gitOK(DIR, "push", "-u", "origin", "master")
|
||||
}
|
||||
|
||||
case "login": // open an interactive ssh session to the server
|
||||
runInDir("", "ssh", "-p", cfg.GitPort, cfg.GitUser+"@"+cfg.GitHost)
|
||||
|
||||
case "cd": // change the current project
|
||||
if strings.HasPrefix(word(words, 1), ".") {
|
||||
break // ignore filesystem-relative navigation
|
||||
}
|
||||
PRJ = ""
|
||||
if isDir(BASE + "/" + word(words, 1)) {
|
||||
PRJ = words[1]
|
||||
}
|
||||
|
||||
case "checkout":
|
||||
if !requireRepo() {
|
||||
break
|
||||
}
|
||||
gitOK(DIR, "checkout", word(words, 1))
|
||||
|
||||
case "clone": // clone a repository (or archive with -a) from the server
|
||||
prj := PRJ
|
||||
if w := word(words, 1); w != "" {
|
||||
prj = w
|
||||
}
|
||||
path := "."
|
||||
if opt["a"] {
|
||||
path = "./archive"
|
||||
}
|
||||
found := 0
|
||||
lines, _ := sshOut("/bin/ls " + path)
|
||||
for _, ln := range lines {
|
||||
t := strings.TrimSpace(ln)
|
||||
if opt["a"] {
|
||||
if t == prj+".git.tar.gz" {
|
||||
found++
|
||||
}
|
||||
} else if t == prj+".git" {
|
||||
found++
|
||||
}
|
||||
}
|
||||
if found != 1 {
|
||||
errorln("repository not found, try 'list [-a]'")
|
||||
break
|
||||
}
|
||||
if isDir(BASE + "/" + prj) {
|
||||
if yesno("overwrite existing directory?", false) {
|
||||
os.RemoveAll(BASE + "/" + prj)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !opt["a"] {
|
||||
if !gitOK(BASE, "clone", URL+"/"+prj+".git") {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if !sshOK("cd archive;tar xvzf " + prj + ".git.tar.gz") {
|
||||
break
|
||||
}
|
||||
if !gitOK(BASE, "clone", URL+"/archive/"+prj+".git") {
|
||||
break
|
||||
}
|
||||
sshOK("rm -rf archive/" + prj + ".git")
|
||||
}
|
||||
if isDir(BASE + "/" + prj) {
|
||||
PRJ = prj
|
||||
}
|
||||
|
||||
case "cloneall": // clone every repository found on the server
|
||||
path := "."
|
||||
if opt["a"] {
|
||||
path = "./archive"
|
||||
}
|
||||
lines, _ := sshOut("/bin/ls " + path)
|
||||
for _, ln := range lines {
|
||||
t := strings.TrimSpace(ln)
|
||||
if m := gitDirRe.FindStringSubmatch(t); m != nil {
|
||||
fmt.Printf("-%s-\n", m[1])
|
||||
gitOK(BASE, "clone", URL+"/"+m[1]+".git")
|
||||
}
|
||||
}
|
||||
|
||||
case "open", "view": // open project in Xcode / editor
|
||||
prj := PRJ
|
||||
if w := word(words, 1); w != "" {
|
||||
prj = w
|
||||
}
|
||||
d := BASE + "/" + prj
|
||||
if !isDir(d) {
|
||||
errorln("not found")
|
||||
break
|
||||
}
|
||||
xws, xprj := "", ""
|
||||
if entries, err := os.ReadDir(d); err == nil {
|
||||
for _, e := range entries {
|
||||
if strings.HasSuffix(e.Name(), ".xcworkspace") {
|
||||
xws = e.Name()
|
||||
}
|
||||
if strings.HasSuffix(e.Name(), ".xcodeproj") {
|
||||
xprj = e.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
if xws != "" && xprj != "" {
|
||||
xprj = "" // prefer the workspace
|
||||
}
|
||||
if xws != "" && isDir(d+"/"+xws) {
|
||||
runInDir(d, "open", xws)
|
||||
}
|
||||
if xprj != "" && isDir(d+"/"+xprj) {
|
||||
runInDir(d, "open", xprj)
|
||||
}
|
||||
if xws == "" && xprj == "" {
|
||||
editor := cfg.Editor
|
||||
if editor == "" {
|
||||
editor = "coda"
|
||||
}
|
||||
runInDir(d, editor, d)
|
||||
}
|
||||
if words[0] == "open" {
|
||||
PRJ = prj
|
||||
}
|
||||
|
||||
case "count": // count source lines in the project
|
||||
countLines(DIR)
|
||||
|
||||
case "tag": // manage tags
|
||||
sub := word(words, 1)
|
||||
switch {
|
||||
case sub == "add" && word(words, 2) != "":
|
||||
if gitOK(DIR, "tag", "-a", words[2], "-m", words[2]) {
|
||||
gitOK(DIR, "push", "origin", words[2])
|
||||
}
|
||||
case sub == "checkout" && word(words, 2) != "":
|
||||
gitOK(DIR, "checkout", "tags/"+words[2])
|
||||
case sub == "delete" && word(words, 2) != "":
|
||||
if gitOK(DIR, "tag", "-d", words[2]) {
|
||||
gitOK(DIR, "push", "origin", ":refs/tags/"+words[2])
|
||||
}
|
||||
case sub != "":
|
||||
errorln("unkown tag subcommand")
|
||||
default:
|
||||
git(DIR, "tag", "-l", "--format=%(taggerdate:short): %(refname:short)")
|
||||
}
|
||||
|
||||
default: // unknown command — no longer forwarded to the shell
|
||||
fmt.Println(col(cRed, "unknown command: "+words[0]) +
|
||||
col(cGray, " (prefix with '!' to run a shell command)"))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// repository log formatting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// repolog prints the formatted log for "hash<2sp>epoch<2sp>subject" lines.
|
||||
func repolog(lines []string) {
|
||||
fmt.Print(formatLog(lines, time.Now()))
|
||||
}
|
||||
|
||||
// formatLog renders the log, picking the most compact date column that still
|
||||
// disambiguates every entry. Returned as a string for testability.
|
||||
func formatLog(lines []string, now time.Time) string {
|
||||
days := [...]string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
|
||||
months := [...]string{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|
||||
|
||||
var full, short, tiny strings.Builder
|
||||
n, ns := 0, 0
|
||||
|
||||
for _, ln := range lines {
|
||||
if strings.TrimSpace(ln) == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(ln, " ", 3)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
hash := parts[0]
|
||||
epoch, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
subj := parts[2]
|
||||
t := time.Unix(epoch, 0)
|
||||
age := now.Sub(t)
|
||||
wd := int(t.Weekday())
|
||||
|
||||
var z, zs, zss string
|
||||
switch {
|
||||
case age < 24*time.Hour:
|
||||
z = fmt.Sprintf(" %02d:%02d", t.Hour(), t.Minute())
|
||||
zs = fmt.Sprintf(" %02d:%02d", t.Hour(), t.Minute())
|
||||
zss = fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute())
|
||||
case age < 7*24*time.Hour:
|
||||
z = fmt.Sprintf(" %s, %02d:%02d", days[wd], t.Hour(), t.Minute())
|
||||
zs = fmt.Sprintf("%s, %02d:%02d", days[wd], t.Hour(), t.Minute())
|
||||
zss = zs
|
||||
ns++
|
||||
default:
|
||||
z = fmt.Sprintf("%s, %02d %s %4d, %02d:%02d",
|
||||
days[wd], t.Day(), months[t.Month()-1], t.Year(), t.Hour(), t.Minute())
|
||||
zs, zss = z, z
|
||||
n++
|
||||
}
|
||||
|
||||
fmt.Fprintf(&full, "%s %s %s\n", col(cPurple, hash), col(cYellow, z), subj)
|
||||
fmt.Fprintf(&short, "%s %s %s\n", col(cPurple, hash), col(cYellow, zs), subj)
|
||||
fmt.Fprintf(&tiny, "%s %s %s\n", col(cPurple, hash), col(cYellow, zss), subj)
|
||||
}
|
||||
|
||||
switch {
|
||||
case n == 0 && ns == 0:
|
||||
return tiny.String()
|
||||
case n == 0 && ns > 0:
|
||||
return short.String()
|
||||
default:
|
||||
return full.String()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// line counting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// countLines prints the line total over all source files in the project.
|
||||
func countLines(dir string) {
|
||||
total, files := countSourceLines(dir)
|
||||
fmt.Printf("%d lines total in %d files in %s\n", total, files, PRJ)
|
||||
}
|
||||
|
||||
// countSourceLines walks dir and sums the lines of every source file, returning
|
||||
// the line and file totals. A "source file" is any text (non-binary) file;
|
||||
// hidden directories (.git, …) and hidden files are skipped.
|
||||
func countSourceLines(dir string) (lines, files int) {
|
||||
filepath.Walk(dir, func(p string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if fi.IsDir() {
|
||||
if p != dir && strings.HasPrefix(fi.Name(), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(fi.Name(), ".") || !isTextFile(p) {
|
||||
return nil
|
||||
}
|
||||
lines += countFileLines(p)
|
||||
files++
|
||||
return nil
|
||||
})
|
||||
return lines, files
|
||||
}
|
||||
|
||||
func countFileLines(path string) int {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return bytes.Count(data, []byte{'\n'})
|
||||
}
|
||||
|
||||
func isTextFile(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
buf := make([]byte, 512)
|
||||
n, _ := f.Read(buf)
|
||||
return !bytes.Contains(buf[:n], []byte{0})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// misc command helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func sanitizeComment(s string) string {
|
||||
s = sanRe.ReplaceAllString(s, " ")
|
||||
s = strings.TrimSpace(s)
|
||||
s = wsRe.ReplaceAllString(s, "_")
|
||||
return s
|
||||
}
|
||||
|
||||
// archiveStamp builds the DDMMYY.HHMM timestamp used for archive names.
|
||||
func archiveStamp() string {
|
||||
t := time.Now()
|
||||
return fmt.Sprintf("%02d%02d%02d.%02d%02d",
|
||||
t.Day(), int(t.Month()), t.Year()-2000, t.Hour(), t.Minute())
|
||||
}
|
||||
|
||||
const gitignore = `.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
._*
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
`
|
||||
|
||||
var helpItems = []struct{ cmd, desc string }{
|
||||
{"cd [project]", "change project"},
|
||||
{"open [project]", "open project"},
|
||||
{"init", "make new repository from current directory"},
|
||||
{"push [comment]", "push changes to git server"},
|
||||
{"pushremote [desc]", "mirror repo to a public server (gitea/github/gitlab) via API"},
|
||||
{"pull", "pull changes from git server"},
|
||||
{"fetch", "fetch changes from git server"},
|
||||
{"status [-a]", "short git status (-a: overview of all projects)"},
|
||||
{"overview", "status of all projects (dirty, ahead/behind)"},
|
||||
{"diff [args]", "show git diff"},
|
||||
{"edit [number]", "edit last [number] commits (default is 10)"},
|
||||
{"clone [-a] <repository>", "clone repository from git server (-a for archive)"},
|
||||
{"cloneall", "clone all repository from git server"},
|
||||
{"checkout [git options]", "forward checkout to git"},
|
||||
{"log", "show log"},
|
||||
{"archive [comment]", "archive current repository"},
|
||||
{"list [-a] [pattern]", "list repositories on git server (-a for archive)"},
|
||||
{"show <repository>", "show repository log on git server"},
|
||||
{"dist <path>", "cp changed files to other directory/repository"},
|
||||
{"login", "connect to git server"},
|
||||
{"count", "count lines in project"},
|
||||
{"tag", "show tags"},
|
||||
{"tag add <tag>", "add tag"},
|
||||
{"tag checkout <tag>", "checkout tag"},
|
||||
{"tag delete <tag>", "delete tag"},
|
||||
{"alias [name [cmd]]", "list, show or define an alias ($1..$N, $* args)"},
|
||||
{"unalias <name>", "remove an alias"},
|
||||
{"rescan", "refresh cached server repository list"},
|
||||
{"!<command>", "run <command> in the shell"},
|
||||
{"quit", "exit mgsh"},
|
||||
}
|
||||
|
||||
func help() {
|
||||
fmt.Println()
|
||||
fmt.Printf("%s v%s %s, builtin commands:\n\n",
|
||||
col(cBold+cWhite, "mgsh (git shell)"), col(cYellow, VERSION), INFO)
|
||||
for _, it := range helpItems {
|
||||
fmt.Printf(" %s%s\n", col(cGreen, padRight(it.cmd, 25)), it.desc)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
Reference in New Issue
Block a user