diff --git a/config.yaml b/config.yaml index 2c11f7c..d8f4d00 100644 --- a/config.yaml +++ b/config.yaml @@ -21,6 +21,20 @@ repo: - master - main + # How to generate commit diffs? Valid values are "system" and "go" (default). + # + # "system" uses system git to generate diff. It's faster and its output is + # familiar. + # + # "go" uses pure-Go diff implementation. Slower, but absolutely stable output + # as it does not use system git installation. Its output has unfamiliar hunk + # (chunk header's context line.) Unfortunately, its performance is significantly + # worse than system git. For large diffs such as auto-generated files, it can + # take 10x~ times compared to system git. + # + # Default value is "go" for backwards compatibility. + diff: go + # Runtime directories. dirs: # [optional] diff --git a/config/config.go b/config/config.go index e1e87bd..6f7f848 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,10 @@ type Config struct { MainBranch []string `yaml:"mainBranch"` Ignore []string `yaml:"ignore,omitempty"` Unlisted []string `yaml:"unlisted,omitempty"` + + // Diff method. "system" uses system git and "go" uses go-git and go-diff. + // Default is "go". + Diff string `yaml:"diff"` } `yaml:"repo"` Dirs struct { Templates string `yaml:"templates"` @@ -67,6 +71,7 @@ func NewWithDefaults() *Config { "README", "README.txt", "README.md", "README.adoc", "readme", "readme.txt", "readme.md", "readme.adoc", } + c.Repo.Diff = "go" c.Meta.Title = "Repositories" c.StaticDirRevision = staticDirRevision @@ -103,6 +108,13 @@ func (c *Config) Resolve(cwd string) error { return err } + switch c.Repo.Diff { + case "go": + case "system": + default: + return fmt.Errorf("\"%s\" is not a valid repo.diff value", c.Repo.Diff) + } + // Override templates dir if c.Dirs.Templates != "" { if c.Dirs.Templates, err = resolvePath(c.Dirs.Templates, basePath); err != nil { diff --git a/git/git.go b/git/git.go index 5b83b0c..2eb7aa5 100644 --- a/git/git.go +++ b/git/git.go @@ -19,6 +19,9 @@ import ( type GitRepo struct { r *git.Repository h plumbing.Hash + + // Repository path + path string } type TagList struct { @@ -84,7 +87,9 @@ func (self *TagList) Less(i, j int) bool { func Open(path string, ref string) (*GitRepo, error) { var err error - g := GitRepo{} + g := GitRepo{ + path: path, + } g.r, err = git.PlainOpen(path) if err != nil { return nil, fmt.Errorf("opening %s: %w", path, err) diff --git a/git/systemdiff.go b/git/systemdiff.go new file mode 100644 index 0000000..3646010 --- /dev/null +++ b/git/systemdiff.go @@ -0,0 +1,105 @@ +package git + +import ( + "bytes" + "fmt" + "log" + "os/exec" + "strings" + + "github.com/bluekeyes/go-gitdiff/gitdiff" + "github.com/go-git/go-git/v5/plumbing/object" + + "github.com/pocka/legit/git/exe" +) + +// SystemDiff generates diff lines using system git. +func (g *GitRepo) SystemDiff() (*NiceDiff, error) { + c, err := g.r.CommitObject(g.h) + if err != nil { + return nil, fmt.Errorf("commit object: %w", err) + } + + var parent *object.Commit + if parent, err = c.Parent(0); err != nil { + if err != object.ErrParentNotFound { + return nil, fmt.Errorf("unable to read parent of %s: %w", c.Hash, err) + } + } + + nd := NiceDiff{} + nd.Commit = c + nd.Parent = parent + if parent == nil { + nd.Parent = &object.Commit{} + } + + diffs, err := g.getSystemGitDiff(c) + if err == nil { + nd.Files = diffs + + for _, d := range diffs { + for _, tf := range d.TextFragments { + for _, l := range tf.Lines { + switch l.Op { + case gitdiff.OpAdd: + nd.Stat.Insertions += 1 + case gitdiff.OpDelete: + nd.Stat.Deletions += 1 + } + } + } + } + + nd.Stat.FilesChanged = len(diffs) + } else { + log.Printf("diff generation failed: %s", err) + } + + return &nd, nil +} + +func (g *GitRepo) getSystemGitDiff(commit *object.Commit) ([]*gitdiff.File, error) { + cmd := exec.Command(exe.GitBin(), []string{ + // repo.scanPath is trusted directory, thus g.path (inside repo.scanPath) is + // trusted too. + // https://git-scm.com/docs/git-config#Documentation/git-config.txt-safedirectory + "-c", fmt.Sprintf("safe.directory=%s", g.path), + "-C", g.path, + "show", commit.Hash.String(), + "--no-ext-diff", + }...) + + cmd.Env = []string{ + // By default, system git tries to read from various places. + // We want deterministic, configuration independent behavior here. + "GIT_CONFIG_GLOBAL=/dev/null", + "GIT_CONFIG_SYSTEM=/dev/null", + "GIT_CONFIG_NOSYSTEM=1", + fmt.Sprintf("GIT_CEILING_DIRECTORIES=%s", g.path), + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("stdout pipe init failure: %w", err) + } + defer stdout.Close() + stderr := strings.Builder{} + cmd.Stderr = &stderr + cmd.Stdin = &bytes.Reader{} + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("unable to start system git-diff: %w", err) + } + + diffs, _, err := gitdiff.Parse(stdout) + if err != nil { + return nil, fmt.Errorf("unable to parse git diff: %w", err) + } + + if err := cmd.Wait(); err != nil { + log.Print(stderr.String()) + return nil, fmt.Errorf("unable to run system git-diff: %w", err) + } + + return diffs, nil +} diff --git a/main.go b/main.go index 30ae40f..2214ba8 100644 --- a/main.go +++ b/main.go @@ -25,12 +25,14 @@ func main() { var port uint var scanPath string var compileTemplatesOnRequest bool + var repoDiff string var staticDirOverride string var templatesDirOverride string flag.StringVar(&cfg, "config", "", "path to config file") flag.StringVar(&host, "server.host", "", "override server.host config") flag.UintVar(&port, "server.port", 0, "override server.port config") flag.StringVar(&scanPath, "repo.scanPath", "", "override repo.scanPath config") + flag.StringVar(&repoDiff, "repo.diff", "", "override repo.diff config") flag.BoolVar(&compileTemplatesOnRequest, "compileTemplatesOnRequest", false, "override compileTemplatesOnRequest config") flag.StringVar(&staticDirOverride, "dirs.static", "", "override dirs.static config") flag.StringVar(&templatesDirOverride, "dirs.templates", "", "override dirs.templates config") @@ -61,6 +63,10 @@ func main() { c.Repo.ScanPath = scanPath } + if repoDiff != "" { + c.Repo.Diff = repoDiff + } + if compileTemplatesOnRequest { c.CompileTemplatesOnRequest = true } diff --git a/routes/routes.go b/routes/routes.go index c78617b..6aae134 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -7,6 +7,8 @@ import ( "net/http" "path/filepath" "strings" + + "github.com/pocka/legit/git" ) func (d *deps) serveRepoTree(w http.ResponseWriter, r *http.Request) { @@ -101,7 +103,12 @@ func (d *deps) serveDiff(w http.ResponseWriter, r *http.Request) { return } - diff, err := gr.Diff() + var diff *git.NiceDiff + if d.c.Repo.Diff == "system" { + diff, err = gr.SystemDiff() + } else { + diff, err = gr.Diff() + } if err != nil { d.write500(w, r) log.Println(err)