package main
import (
"flag"
"fmt"
"io"
"os"
)
// version is the current version of the CLI.
var version string
// Cli represents the command-line interface.
type Cli struct {
OutStream, ErrStream io.Writer
}
// CliArgs holds the parsed command-line arguments.
type CliArgs struct {
ShowHelp bool
ShowVersion bool
Templates []string
}
// ParseArgs parses command-line arguments.
func ParseArgs(args []string) (CliArgs, error) {
var cliArgs CliArgs
flags := flag.NewFlagSet("gh-dot-tmpl", flag.ContinueOnError)
flags.BoolVar(&cliArgs.ShowHelp, "h", false, "Show help message")
flags.BoolVar(&cliArgs.ShowHelp, "help", false, "Show help message")
flags.BoolVar(&cliArgs.ShowVersion, "v", false, "Show version")
flags.BoolVar(&cliArgs.ShowVersion, "version", false, "Show version")
if err := flags.Parse(args); err != nil {
// nolint: wrapcheck
return cliArgs, err
}
cliArgs.Templates = flags.Args()
return cliArgs, nil
}
// Run parses command-line arguments and executes the appropriate action.
func (cli *Cli) Run() int {
cliArgs, err := ParseArgs(os.Args[1:])
if err != nil {
fmt.Fprintln(cli.ErrStream, err)
return 1
}
if cliArgs.ShowHelp {
cli.usage()
return 0
}
if cliArgs.ShowVersion {
fmt.Fprintf(cli.OutStream, "gh-dot-tmpl version %s\n", version)
return 0
}
if len(cliArgs.Templates) == 0 {
fmt.Fprintf(cli.ErrStream, "Error: No template names provided\n")
cli.usage()
return 1
}
templateName := cliArgs.Templates
err = Generate(templateName)
if err != nil {
fmt.Fprintf(cli.ErrStream, "Error: %s\n", err)
return 1
}
return 0
}
// usage prints the help message.
func (cli *Cli) usage() {
fmt.Fprintf(cli.OutStream, `Usage: gh-dot-tmpl [options] [template_name...]
Options:
-h, --help Show help message
-v, --version Show version
Arguments:
template_name... Names of the templates to process
`)
}
package main
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// Config struct represents the configuration file structure.
type Config struct {
Templates map[string]TemplateConfig `yaml:"templates"`
}
// TemplateConfig represents the mapping of template files to generated files.
type TemplateConfig struct {
TemplateFile string `yaml:"template_file"`
OutputFile string `yaml:"output_file"`
}
// LoadConfig reads the configuration file and unmarshals it into a Config struct.
func LoadConfig(configPath string) (*Config, error) {
file, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("unable to open config file: %w", err)
}
defer file.Close()
var config Config
decoder := yaml.NewDecoder(file)
if err := decoder.Decode(&config); err != nil {
return nil, fmt.Errorf("unable to decode config file: %w", err)
}
return &config, nil
}
// GetConfigPath returns the path to the configuration file.
func GetConfigPath() string {
configDir := filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "gh-dot-tmpl")
if os.Getenv("XDG_CONFIG_HOME") == "" {
configDir = filepath.Join(os.Getenv("HOME"), ".config", "gh-dot-tmpl")
}
return filepath.Join(configDir, "config.yaml")
}
package main
import (
"fmt"
"os"
)
func Generate(templates []string) error {
if !IsGitRepository() {
return fmt.Errorf("not a git repository")
}
gitRoot, err := GetGitRoot()
if err != nil {
return err
}
if err := os.Chdir(gitRoot); err != nil {
return fmt.Errorf("failed to change directory to git root: %w", err)
}
user, repo, err := GetGithubUserRepo()
if err != nil {
return err
}
configPath := GetConfigPath()
config, err := LoadConfig(configPath)
if err != nil {
return err
}
for _, template := range templates {
if err := processTemplate(config, template, user, repo); err != nil {
return err
}
}
return nil
}
func processTemplate(config *Config, template, user, repo string) error {
tempPath, err := GetTemplatePath(config, template)
if err != nil {
return err
}
outputFile := config.Templates[template].OutputFile
if err := GenerateFileFromTemplate(tempPath, outputFile, user, repo); err != nil {
return err
}
return nil
}
package main
import (
"errors"
"os/exec"
"strings"
)
// Number of parts expected in the GitHub URL.
const expectedGithubURLParts = 2
// IsGitRepository checks if the current directory is a git repository.
func IsGitRepository() bool {
cmd := exec.Command("git", "rev-parse", "--is-inside-work-tree")
cmd.Stderr = nil
err := cmd.Run()
return err == nil
}
// GetGitRoot returns the root path of the git repository.
func GetGitRoot() (string, error) {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
output, err := cmd.Output()
if err != nil {
return "", errors.New("not a git repository")
}
return strings.TrimSpace(string(output)), nil
}
// GetGithubUserRepo returns the GitHub username and repository name.
func GetGithubUserRepo() (string, string, error) {
cmd := exec.Command("git", "config", "--get", "remote.origin.url")
output, err := cmd.Output()
if err != nil {
return "", "", errors.New("unable to get remote origin URL")
}
url := strings.TrimSpace(string(output))
url = strings.TrimPrefix(url, "https://github.com/")
url = strings.TrimSuffix(url, ".git")
parts := strings.Split(url, "/")
if len(parts) != expectedGithubURLParts {
return "", "", errors.New("invalid GitHub URL")
}
return parts[0], parts[1], nil
}
package main
import (
"os"
)
func main() {
cli := &Cli{os.Stdout, os.Stderr}
os.Exit(cli.Run())
}
package main
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
)
// lookupUser is a variable for user lookup function, so it can be mocked in tests.
var lookupUser = user.Lookup
func ExpandTilde(pth string) (string, error) {
if pth == "" {
return "", fmt.Errorf("No Path provided")
}
if strings.HasPrefix(pth, "~") {
pth = strings.TrimPrefix(pth, "~")
if len(pth) == 0 || strings.HasPrefix(pth, "/") {
return os.ExpandEnv("$HOME" + pth), nil
}
splitPth := strings.Split(pth, "/")
username := splitPth[0]
usr, err := lookupUser(username)
if err != nil {
return "", err
}
pathInUsrHome := strings.Join(splitPth[1:], "/")
return filepath.Join(usr.HomeDir, pathInUsrHome), nil
}
return pth, nil
}
package main
import (
"bytes"
"fmt"
"os"
"text/template"
)
// TemplateData holds the data to be inserted into the template.
type TemplateData struct {
Username string
Repository string
}
const permission = 0o600
// GenerateFileFromTemplate generates a file from a template with the provided data.
func GenerateFileFromTemplate(templatePath, outputPath, username, repository string) error {
data := TemplateData{
Username: username,
Repository: repository,
}
tmpl, err := template.ParseFiles(templatePath)
if err != nil {
return fmt.Errorf("failed to parse template file: %w", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
if err := os.WriteFile(outputPath, buf.Bytes(), permission); err != nil {
return fmt.Errorf("failed to write output file: %w", err)
}
return nil
}
// GetTemplatePath returns the full path of a template file based on the config directory.
func GetTemplatePath(config *Config, templateName string) (string, error) {
templatePath, err := ExpandTilde(config.Templates[templateName].TemplateFile)
if err != nil {
return "", err
}
return templatePath, nil
}