diff --git a/main.go b/main.go index 4f81195..e48a23b 100644 --- a/main.go +++ b/main.go @@ -1,50 +1,18 @@ package main import ( - "crypto/sha1" _ "embed" - "encoding/hex" - "errors" + "encoding/json" "flag" "fmt" - "html/template" "io" - "io/fs" "log" "os" "path/filepath" - - "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/storage/memory" + "reflect" + "strings" ) -// CONFIG_FILE=".ht_git2html" -const configFile = ".config" - -/* -show_progress=1 -force_rebuild=0 -*/ -const showProgress = true -const forceRebuild = false - -// TODO: add log.Debug -/* - progress() - { - if test x"$show_progress" = x1 - then - echo "$@" - fi - } -*/ - -//go:embed config.tmpl -var cTmpl string - //go:embed repo.html.tmpl var rTmpl string @@ -55,12 +23,26 @@ var bTmpl string var iTmpl string type options struct { - project string - repo string - link string - branches string - quiet bool - force bool + name string + Project string `json:"project"` + Repo string `json:"repo"` + URL string `json:"url"` + Branches string `json:"branches"` + Quiet bool `json:"quiet"` + Force bool `json:"force"` +} + +// Helps store options into a JSON config file. +func (o *options) save(out string) { + bs, err := json.MarshalIndent(o, "", " ") + + if err != nil { + log.Fatalf("unable to encode config file: %v", err) + } + + if err := os.WriteFile(filepath.Join(out, o.name), bs, 0644); err != nil { + log.Fatalf("unable to save config file: %v", err) + } } func init() { @@ -70,116 +52,106 @@ func init() { fmt.Fprintln(flag.CommandLine.Output(), "usage:", os.Args[0], "[<options>] <path>") flag.PrintDefaults() } + + // Swap default logger timestamps for a custom prefix. + log.SetFlags(log.Lmsgprefix) + log.SetPrefix("jimmy: ") } -/* -usage() - { - echo "Usage $0 [-prlbq] TARGET" - echo "Generate static HTML pages in TARGET for the specified git repository." - echo - echo " -p Project's name" - echo " -r Repository to clone from." - echo " -l Public repository link, e.g., 'http://host.org/project.git'" - echo " -b List of branches to process (default: all)." - echo " -q Be quiet." - echo " -f Force rebuilding of all pages." - exit $1 - } -*/ func main() { - /* - while getopts ":p:r:l:b:qf" opt - do - case $opt in - p) - PROJECT=$OPTARG - ;; - r) - # Directory containing the repository. - REPOSITORY=$OPTARG - ;; - l) - PUBLIC_REPOSITORY=$OPTARG - ;; - b) - BRANCHES=$OPTARG - ;; - q) - show_progress=0 - ;; - f) - force_rebuild=1 - ;; - \?) - echo "Invalid option: -$OPTARG" >&2 - usage - ;; - esac - done - shift $(($OPTIND - 1)) - */ - opts := &options{} - - flag.StringVar(&opts.project, "p", "My project", "Project name") - flag.StringVar(&opts.repo, "r", "", "Target repo") - flag.StringVar(&opts.link, "l", "http://host.org/project.git", "Repo link") - flag.StringVar(&opts.branches, "b", "all", "Target branches") - flag.BoolVar(&opts.quiet, "q", false, "Be quiet") - flag.BoolVar(&opts.force, "f", false, "Force rebuilding of all pages") + opt := &options{ + name: ".jimmy.json", + } + + flag.StringVar(&opt.Project, "p", "Jimbo", "Project title") + flag.StringVar(&opt.Repo, "r", "", "Target repo") + flag.StringVar(&opt.URL, "u", "https://host.net/project.git", "Repo public URL") + // TODO: Allow for passing multiple values. + flag.StringVar(&opt.Branches, "b", "all", "Target branches") + flag.BoolVar(&opt.Quiet, "q", false, "Be quiet") + flag.BoolVar(&opt.Force, "f", false, "Force rebuilding of all pages") flag.Parse() - // Collect flags provided. Note these need to come before - // the target directory argument. + if opt.Quiet { + log.SetOutput(io.Discard) + } + + cwd, err := os.Getwd() + + if err != nil { + log.Fatalf("unable to get current working directory: %v", err) + } + + // Defaults to the current working directory if no argument present. + out := flag.Arg(0) + + // Make sure `out` is an absolute path. + if ok := filepath.IsAbs(out); !ok { + out = filepath.Join(cwd, out) + } + + // Attempt to read saved settings. + obs, err := os.ReadFile(filepath.Join(out, opt.name)) + + if err != nil { + log.Printf("unable to read config file: %v", err) + } + + // Create a separate options instance for reading config file values into. + // NOTE: Need dereference to safely copy. + cnf := *opt + + // If a config file exists and an option has not been set, override default to match. + if err := json.Unmarshal(obs, &cnf); err != nil { + log.Printf("unable to parse config file: %v", err) + } + + // Collect flags provided. flagset := make(map[string]bool) + // NOTE: These need to come before the output directory argument. flag.Visit(func(f *flag.Flag) { flagset[f.Name] = true }) - // TODO: Log these one by one unless quiet. - // log.Printf("+%v", opts) + ref := reflect.ValueOf(cnf) - // The repo flag is required, print usage and quit if none given or - // unless a single target directory is provided. - if !flagset["r"] || flag.NArg() != 1 { - flag.Usage() - os.Exit(1) - } + // Log current settings. + flag.VisitAll(func(f *flag.Flag) { + if !flagset[f.Name] { + // Attempt to override default settings where necessary. + v := ref.FieldByNameFunc(func(n string) bool { + return strings.HasPrefix(strings.ToLower(n), f.Name) + }) - target := flag.Arg(0) + // This has the nice side effect of magically overriding `opt` fields. + flag.Set(f.Name, v.String()) + } - // Make sure `target` is an absolute path. - if ok := filepath.IsAbs(target); !ok { - cwd, err := os.Getwd() + log.Printf("%v/%v: %v\n", f.Name, f.Usage, f.Value) + }) - if err != nil { - log.Fatalf("jimmy: unable to get current working directory %v", err) - } + // The repo flag is required at this point. + // NOTE: Being able to handle non-local repos would be nice. + if ok := filepath.IsAbs(opt.Repo); !ok { + flag.Usage() + os.Exit(1) + } - target = filepath.Join(cwd, target) + // Option considered repo-like if it contains a hidden `.git` dir. + if _, err := os.Stat(filepath.Join(opt.Repo, ".git")); os.IsNotExist(err) { + flag.Usage() + os.Exit(1) } - // Make sure `target` exists. - if err := os.MkdirAll(target, 0750); err != nil { - log.Fatalf("jimmy: unable to create target directory: %v", err) + // Make sure `out` exists. + if err := os.MkdirAll(out, 0750); err != nil { + log.Fatalf("unable to create output directory: %v", err) } - // Read the configuration file. - /* - if test -e "$TARGET/$CONFIG_FILE" - then - . "$TARGET/$CONFIG_FILE" - fi - */ - // TODO: Read config file - writeConfigFile(target, opts) - createDirectories(target, opts.force) - - // NOTE: I believe this check is too limiting and we should - // allow for cloning no local repos as well. Once the repo - // has been copied or downloaded, we should then check if it - // contains a hidden `.git` folder to verify true repo status? + // Save current settings for future use. + opt.save(out) + /* if test ! -d "$REPOSITORY" then @@ -187,10 +159,10 @@ func main() { exit 1 fi */ - setUpRepo(target, opts) - setGitConfig() + createDirectories(out, opt.Force) + setUpRepo(out, opt) - cleanBranches := cleanUpBranches(opts.branches) + cleanBranches := cleanUpBranches(opt.Branches) fetchBranches(cleanBranches) writeIndex() @@ -198,67 +170,6 @@ func main() { writeIndexFooter() } -func writeConfigFile(target string, opts *options) { - /* - # The output version - CURRENT_TEMPLATE="$(sha1sum "$0")" - if test "x$CURRENT_TEMPLATE" != "x$TEMPLATE" - then - progress "Rebuilding all pages as output template changed." - force_rebuild=1 - fi - TEMPLATE="$CURRENT_TEMPLATE" - */ - configTmpl := template.Must(template.New("default").Parse(cTmpl)) - - // TODO: Check file permissions are set to 0666. - // TODO: Read file if it exists. - outFile, err := os.Create(filepath.Join(target, configFile)) - - if err != nil { - log.Fatalf("jimmy: unable to create config file: %v", err) - } - - h := sha1.New() - - // (spike): why did we do this step? - if _, err := io.Copy(h, outFile); err != nil { - log.Fatal(err) - } - - /* - { - save() - { - # Prefer environment variables and arguments to the configuration file. - echo "$1=\"\${$1:-\"$2\"}\"" - } - save "PROJECT" "$PROJECT" - save "REPOSITORY" "$REPOSITORY" - save "PUBLIC_REPOSITORY" "$PUBLIC_REPOSITORY" - save "TARGET" "$TARGET" - save "BRANCHES" "$BRANCHES" - save "TEMPLATE" "$TEMPLATE" - } > "$TARGET/$CONFIG_FILE" - */ - configTmpl.Execute(outFile, struct { - Project string - Repository string - PublicRepository string - Target string - Branches string - // SHA1SUM - Template string - }{ - Project: opts.project, - Repository: opts.repo, - PublicRepository: opts.link, - Target: target, - Branches: opts.branches, - Template: hex.EncodeToString(h.Sum(nil)), - }) -} - func createDirectories(target string, force bool) { //# Ensure that some directories we need exist. /* @@ -292,196 +203,17 @@ func createDirectories(target string, force bool) { // Clear existing dirs if force true. if force && dir != "branches" { if err := os.RemoveAll(d); err != nil { - log.Printf("jimmy: unable to remove directory: %v", err) + log.Printf("unable to remove directory: %v", err) } } if err := os.MkdirAll(d, os.ModePerm); err != nil { - log.Printf("jimmy: unable to create directory: %v", err) - } - } -} - -func setUpRepo(target string, opts *options) { - mfs := memfs.New() - // Clones the given repository in memory, creating the remote, the local - // branches and fetching the objects, exactly as: - r, err := git.Clone(memory.NewStorage(), mfs, &git.CloneOptions{ - URL: opts.repo, - }) - - check(err) - - refs, _ := r.References() - refs.ForEach(func(ref *plumbing.Reference) error { - bc, err := r.Branch("refs/head/experimental") - - if err != nil { - return err - } - - log.Printf("reference: %v %v", ref.Type(), ref.Name()) - log.Printf("branch: %v", bc.Name) - - return nil - }) - - branches, err := r.Branches() - - check(err) - - // ... retrieves the branch pointed by HEAD - // ref, err := r.Head() - // - // check(err) - // - // cIter, err := r.Log(&git.LogOptions{From: ref.Hash()}) - // - // check(err) - - // err = cIter.ForEach(func(c *object.Commit) error { - // // log.Print(c) - // - // return nil - // }) - - branches.ForEach(func(b *plumbing.Reference) error { - log.Printf("branch: %v", b) - - return nil - }) - - check(err) - - var pathError *fs.PathError - repoPath := filepath.Join(target, "repository") - - _, err = os.Stat(repoPath) - - if errors.As(err, &pathError) { - localRepo, err := git.PlainClone(repoPath, false, &git.CloneOptions{ - URL: opts.repo, - SingleBranch: false, - NoCheckout: true, - // NOTE: This will screw things up if piping output to a file. - // Progress: os.Stdout, - }) - - commitObjects, err := localRepo.CommitObjects() - - if err != nil { - log.Printf("%v", err) - } - - var commitList []*object.Commit - - commitObjects.ForEach(func(c *object.Commit) error { - commitList = append(commitList, c) - - return nil - }) - - // it := template.Must(template.New("default").Parse(iTmpl)) - // - // cdata := struct { - // List []*object.Commit - // Title string - // }{ - // List: commitList, - // Title: opts.project, - // } - // - // if err := it.Execute(os.Stdout, cdata); err != nil { - // log.Fatalf("jimmy: unable to fill index template: %v", err) - // } - - localBranches, err := localRepo.Branches() - - if err != nil { - log.Printf("%v", err) - } - - var branchList []*plumbing.Reference - - localBranches.ForEach(func(b *plumbing.Reference) error { - branchList = append(branchList, b) - - return nil - }) - - rt := template.Must(template.New("default").Parse(rTmpl)) - - rconf, err := localRepo.Config() - - check(err) - - for _, b := range rconf.Branches { - log.Print(b.Name) - } - - log.Printf("config/branches: %v", rconf.Branches) - log.Printf("config/remotes: %v", rconf.Remotes) - - bdata := struct { - Description string - Link string - List []*plumbing.Reference - Title string - }{ - Description: "", - Link: opts.link, - List: branchList, - Title: opts.project, - } - - if err := rt.Execute(os.Stdout, bdata); err != nil { - log.Fatalf("jimmy: unable to fill home template: %v", err) + log.Printf("unable to create directory: %v", err) } - - // branch, err := localBranches.Next() - // - // log.Printf("branch: %v %s", branch.Name(), branch.String()) - // - // if err != nil { - // log.Printf("jimmy: failed to list branches: %v", err) - // } - // - // ref := plumbing.NewHashReference(branch.Name(), branch.Hash()) - // - // if err != nil { - // log.Printf("jimmy: failed to create ref: %v", err) - // } - - // workTree, err := localRepo.Worktree() - // - // if err != nil { - // log.Printf("jimmy: failed to open worktree: %v", err) - // } - // - // err = workTree.Checkout(&git.CheckoutOptions{ - // Hash: ref.Hash(), - // }) - // - // if err != nil { - // log.Printf("jimmy: failed to checkout detached HEAD: %v", err) - // } - // - // err = localRepo.Storer.RemoveReference(ref.Name()) - // - // if err != nil { - // log.Printf("jimmy: failed to delete branch: %v", err) - // } } } -// TODO: implement! -// NOTE: This may not be required, there is no merge calls anywhere. -func setGitConfig() { - /* - # git merge fails if there are not set. Fake them. - git config user.email "git2html@git2html" - git config user.name "git2html" - */ +func setUpRepo(target string, opt *options) { } // TODO: implement! @@ -930,9 +662,3 @@ func htmlFooter() { } */ } - -func check(err error) { - if err != nil { - log.Fatalf("jimmy: %v", err) - } -}
home › develop › c00da33 › f2a316d