pax_global_header00006660000000000000000000000064146456750650014534gustar00rootroot0000000000000052 comment=6cae65fd6d144e221446e27d7596184deb1e3d34 gitmap-1.6.0/000077500000000000000000000000001464567506500130215ustar00rootroot00000000000000gitmap-1.6.0/.github/000077500000000000000000000000001464567506500143615ustar00rootroot00000000000000gitmap-1.6.0/.github/FUNDING.yml000066400000000000000000000000151464567506500161720ustar00rootroot00000000000000github: [bep]gitmap-1.6.0/.github/workflows/000077500000000000000000000000001464567506500164165ustar00rootroot00000000000000gitmap-1.6.0/.github/workflows/test.yml000066400000000000000000000017331464567506500201240ustar00rootroot00000000000000on: push: branches: [master] pull_request: name: Test jobs: test: strategy: matrix: go-version: [1.21.x, 1.22.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest shell: bash - name: Update PATH run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH shell: bash - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Fmt if: matrix.platform != 'windows-latest' # :( run: "diff <(gofmt -d .) <(printf '')" shell: bash - name: Vet run: go vet ./... - name: Staticcheck run: staticcheck ./... - name: Test run: go test -race ./... gitmap-1.6.0/.gitignore000066400000000000000000000004511464567506500150110ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof cover.out bench.txt bench2.txt gitmap-1.6.0/LICENSE000066400000000000000000000020771464567506500140340ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Bjørn Erik Pedersen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gitmap-1.6.0/README.md000066400000000000000000000015411464567506500143010ustar00rootroot00000000000000# GitMap [![GoDoc](https://godoc.org/github.com/bep/gitmap?status.svg)](https://godoc.org/github.com/bep/gitmap) [![Build Status](https://travis-ci.org/bep/gitmap.svg)](https://travis-ci.org/bep/gitmap) [![Build status](https://ci.appveyor.com/api/projects/status/c8tu1wdoa4j7q81g?svg=true)](https://ci.appveyor.com/project/bjornerik/gitmap) [![Go Report Card](https://goreportcard.com/badge/github.com/bep/gitmap)](https://goreportcard.com/report/github.com/bep/gitmap) A fairly fast way to create a map from all the filenames to info objects for a given revision of a Git repo. This library uses `os/exec` to talk to Git. There are faster ways to do this by using some Go Git-lib or C bindings, but that adds dependencies I really don't want or need. If some `git log kung fu master` out there have suggestions for improvements, please open an issue or a PR. gitmap-1.6.0/gitmap.go000066400000000000000000000114251464567506500146340ustar00rootroot00000000000000// Copyright 2024 Bjørn Erik Pedersen . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package gitmap import ( "bytes" "errors" "fmt" "io" "os/exec" "path/filepath" "strings" "time" ) var ( // will be modified during tests gitExec string ErrGitNotFound = errors.New("git executable not found in $PATH") ) type GitRepo struct { // TopLevelAbsPath contains the absolute path of the top-level directory. // This is similar to the answer from "git rev-parse --show-toplevel" // except symbolic link is not followed on non-Windows platforms. // Note that this follows Git's way of handling paths, so expect to get forward slashes, // even on Windows. TopLevelAbsPath string // The files in this Git repository. Files GitMap } // GitMap maps filenames to Git revision information. type GitMap map[string]*GitInfo // GitInfo holds information about a Git commit. type GitInfo struct { Hash string `json:"hash"` // Commit hash AbbreviatedHash string `json:"abbreviatedHash"` // Abbreviated commit hash Subject string `json:"subject"` // The commit message's subject/title line AuthorName string `json:"authorName"` // The author name, respecting .mailmap AuthorEmail string `json:"authorEmail"` // The author email address, respecting .mailmap AuthorDate time.Time `json:"authorDate"` // The author date CommitDate time.Time `json:"commitDate"` // The commit date Body string `json:"body"` // The commit message body } // Runner is an interface for running Git commands, // as implemented buy *exec.Cmd. type Runner interface { Run() error } // Options for the Map function type Options struct { Repository string // Path to the repository to map Revision string // Use blank or HEAD for the currently active revision GetGitCommandFunc func(stdout, stderr io.Writer, args ...string) (Runner, error) } // Map creates a GitRepo with a file map from the given options. func Map(opts Options) (*GitRepo, error) { if opts.GetGitCommandFunc == nil { opts.GetGitCommandFunc = func(stdout, stderr io.Writer, args ...string) (Runner, error) { cmd := exec.Command(gitExec, args...) cmd.Stdout = stdout cmd.Stderr = stderr return cmd, nil } } m := make(GitMap) // First get the top level repo path absRepoPath, err := filepath.Abs(opts.Repository) if err != nil { return nil, err } out, err := git(opts, "-C", opts.Repository, "rev-parse", "--show-cdup") if err != nil { return nil, err } cdUp := strings.TrimSpace(string(out)) topLevelPath := filepath.ToSlash(filepath.Join(absRepoPath, cdUp)) gitLogArgs := strings.Fields(fmt.Sprintf( `--name-only --no-merges --format=format:%%x1e%%H%%x1f%%h%%x1f%%s%%x1f%%aN%%x1f%%aE%%x1f%%ai%%x1f%%ci%%x1f%%b%%x1d %s`, opts.Revision, )) gitLogArgs = append([]string{"-c", "diff.renames=0", "-c", "log.showSignature=0", "-C", opts.Repository, "log"}, gitLogArgs...) out, err = git(opts, gitLogArgs...) if err != nil { return nil, err } entriesStr := strings.Trim(out, "\n\x1e'") entries := strings.Split(entriesStr, "\x1e") for _, e := range entries { lines := strings.Split(e, "\x1d") gitInfo, err := toGitInfo(lines[0]) if err != nil { return nil, err } filenames := strings.Split(lines[1], "\n") for _, filename := range filenames { filename := strings.TrimSpace(filename) if filename == "" { continue } if _, ok := m[filename]; !ok { m[filename] = gitInfo } } } return &GitRepo{Files: m, TopLevelAbsPath: topLevelPath}, nil } func git(opts Options, args ...string) (string, error) { var outBuff bytes.Buffer var errBuff bytes.Buffer cmd, err := opts.GetGitCommandFunc(&outBuff, &errBuff, args...) if err != nil { return "", err } err = cmd.Run() if err != nil { if ee, ok := err.(*exec.Error); ok { if ee.Err == exec.ErrNotFound { return "", ErrGitNotFound } } return "", errors.New(strings.TrimSpace(errBuff.String())) } return outBuff.String(), nil } func toGitInfo(entry string) (*GitInfo, error) { items := strings.Split(entry, "\x1f") if len(items) == 7 { items = append(items, "") } authorDate, err := time.Parse("2006-01-02 15:04:05 -0700", items[5]) if err != nil { return nil, err } commitDate, err := time.Parse("2006-01-02 15:04:05 -0700", items[6]) if err != nil { return nil, err } return &GitInfo{ Hash: items[0], AbbreviatedHash: items[1], Subject: items[2], AuthorName: items[3], AuthorEmail: items[4], AuthorDate: authorDate, CommitDate: commitDate, Body: strings.TrimSpace(items[7]), }, nil } func init() { initDefaults() } func initDefaults() { gitExec = "git" } gitmap-1.6.0/gitmap_test.go000066400000000000000000000133601464567506500156730ustar00rootroot00000000000000// Copyright 2024 Bjørn Erik Pedersen . // // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package gitmap import ( "encoding/json" "os" "strings" "testing" ) var ( revision = "7d46b653c9674510d808815c4c92c7dc10bedc16" repository string ) func init() { var err error if repository, err = os.Getwd(); err != nil { panic(err) } } func TestMap(t *testing.T) { var ( gm GitMap gr *GitRepo err error ) if gr, err = Map(Options{Repository: repository, Revision: revision}); err != nil { t.Fatal(err) } gm = gr.Files if len(gm) != 11 { t.Fatalf("Wrong number of files, got %d, expected %d", len(gm), 9) } assertFile(t, gm, "testfiles/d1/d1.txt", "39120eb", "39120eb28a2f8a0312f9b45f91b6abb687b7fd3c", "2016-07-20", "2016-07-20", ) assertFile(t, gm, "testfiles/d2/d2.txt", "39120eb", "39120eb28a2f8a0312f9b45f91b6abb687b7fd3c", "2016-07-20", "2016-07-20", ) assertFile(t, gm, "testfiles/amended.txt", "7d46b65", "7d46b653c9674510d808815c4c92c7dc10bedc16", "2019-05-23", "2019-05-25", ) assertFile(t, gm, "README.md", "0b830e4", "0b830e458446fdb774b1688af9b402acf388d6ab", "2016-07-22", "2016-07-22", ) } func assertFile( t *testing.T, gm GitMap, filename, expectedAbbreviatedHash, expectedHash, expectedAuthorDate, expectedCommitDate string, ) { var ( gi *GitInfo ok bool ) if gi, ok = gm[filename]; !ok { t.Fatal(filename) } if gi.AbbreviatedHash != expectedAbbreviatedHash || gi.Hash != expectedHash { t.Error("Invalid tree hash, file", filename, "abbreviated:", gi.AbbreviatedHash, "full:", gi.Hash, gi.Subject) } if gi.AuthorName != "Bjørn Erik Pedersen" && gi.AuthorName != "Michael Stapelberg" { t.Error("These commits are mine! Got", gi.AuthorName, "and", gi.AuthorEmail) } if gi.AuthorEmail != "bjorn.erik.pedersen@gmail.com" && gi.AuthorEmail != "stapelberg@google.com" { t.Error("These commits are mine! Got", gi.AuthorName, "and", gi.AuthorEmail) } if got, want := gi.AuthorDate.Format("2006-01-02"), expectedAuthorDate; got != want { t.Errorf("%s: unexpected author date: got %v, want %v", filename, got, want) } if got, want := gi.CommitDate.Format("2006-01-02"), expectedCommitDate; got != want { t.Errorf("%s: unexpected commit date: got %v, want %v", filename, got, want) } } func TestCommitMessage(t *testing.T) { var ( gm GitMap gr *GitRepo err error ) if gr, err = Map(Options{Repository: repository, Revision: "HEAD"}); err != nil { t.Fatal(err) } gm = gr.Files assertMessage( t, gm, "testfiles/d1/d1.txt", "Change the test files", "To trigger a test variant.", ) assertMessage( t, gm, "testfiles/r3.txt", "Edit testfiles/r3.txt", "Multiline\n\ncommit body.", ) assertMessage( t, gm, "testfiles/amended.txt", "Add testfile with different author/commit date", "", ) } func assertMessage( t *testing.T, gm GitMap, filename, expectedSubject, expectedBody string, ) { t.Helper() var ( gi *GitInfo ok bool ) if gi, ok = gm[filename]; !ok { t.Fatal(filename) } if gi.Subject != expectedSubject { t.Fatalf("Incorrect commit subject. Expected:\n%q\nGot:\n%q", expectedSubject, gi.Subject) } if gi.Body != expectedBody { t.Fatalf("Incorrect commit body. Expected:\n%q\nGot:\n%q", expectedBody, gi.Body) } } func TestActiveRevision(t *testing.T) { var ( gm GitMap gr *GitRepo err error ) if gr, err = Map(Options{Repository: repository, Revision: "HEAD"}); err != nil { t.Fatal(err) } gm = gr.Files if len(gm) < 10 { t.Fatalf("Wrong number of files, got %d, expected at least %d", len(gm), 10) } if len(gm) < 10 { t.Fatalf("Wrong number of files, got %d, expected at least %d", len(gm), 10) } } func TestGitExecutableNotFound(t *testing.T) { defer initDefaults() gitExec = "thisShouldHopefullyNotExistOnPath" gi, err := Map(Options{Repository: repository, Revision: revision}) if err != ErrGitNotFound || gi != nil { t.Fatal("Invalid error handling") } } func TestEncodeJSON(t *testing.T) { var ( gm GitMap gr *GitRepo gi *GitInfo err error ok bool filename = "README.md" ) if gr, err = Map(Options{Repository: repository, Revision: revision}); err != nil { t.Fatal(err) } gm = gr.Files if gi, ok = gm[filename]; !ok { t.Fatal(filename) } b, err := json.Marshal(&gi) if err != nil { t.Fatal(err) } s := string(b) if s != `{"hash":"0b830e458446fdb774b1688af9b402acf388d6ab","abbreviatedHash":"0b830e4","subject":"Add some more to README","authorName":"Bjørn Erik Pedersen","authorEmail":"bjorn.erik.pedersen@gmail.com","authorDate":"2016-07-22T21:40:27+02:00","commitDate":"2016-07-22T21:40:27+02:00","body":""}` { t.Errorf("JSON marshal error: \n%s", s) } } func TestGitRevisionNotFound(t *testing.T) { gi, err := Map(Options{Repository: repository, Revision: "adfasdfasdf"}) // TODO(bep) improve error handling. if err == nil || gi != nil { t.Fatal("Invalid error handling", err) } } func TestGitRepoNotFound(t *testing.T) { gi, err := Map(Options{Repository: "adfasdfasdf", Revision: revision}) // TODO(bep) improve error handling. if err == nil || gi != nil { t.Fatal("Invalid error handling", err) } } func TestTopLevelAbsPath(t *testing.T) { var ( gr *GitRepo err error ) if gr, err = Map(Options{Repository: repository, Revision: revision}); err != nil { t.Fatal(err) } expected := "/gitmap" if !strings.HasSuffix(gr.TopLevelAbsPath, expected) { t.Fatalf("Expected to end with %q got %q", expected, gr.TopLevelAbsPath) } } func BenchmarkMap(b *testing.B) { for i := 0; i < b.N; i++ { _, err := Map(Options{Repository: repository, Revision: revision}) if err != nil { b.Fatalf("Got error: %s", err) } } } gitmap-1.6.0/go.mod000066400000000000000000000000461464567506500141270ustar00rootroot00000000000000module github.com/bep/gitmap go 1.18 gitmap-1.6.0/go.sum000066400000000000000000000000001464567506500141420ustar00rootroot00000000000000gitmap-1.6.0/testfiles/000077500000000000000000000000001464567506500150235ustar00rootroot00000000000000gitmap-1.6.0/testfiles/amended.txt000066400000000000000000000000351464567506500171570ustar00rootroot00000000000000different author/commit date gitmap-1.6.0/testfiles/d1/000077500000000000000000000000001464567506500153275ustar00rootroot00000000000000gitmap-1.6.0/testfiles/d1/d1.txt000066400000000000000000000000141464567506500163670ustar00rootroot00000000000000d1-changed2 gitmap-1.6.0/testfiles/d2/000077500000000000000000000000001464567506500153305ustar00rootroot00000000000000gitmap-1.6.0/testfiles/d2/d2.txt000066400000000000000000000000131464567506500163700ustar00rootroot00000000000000d2-changed gitmap-1.6.0/testfiles/r1.txt000066400000000000000000000000061464567506500161020ustar00rootroot00000000000000test1 gitmap-1.6.0/testfiles/r2.txt000066400000000000000000000000031464567506500161000ustar00rootroot00000000000000r2 gitmap-1.6.0/testfiles/r3.txt000066400000000000000000000000061464567506500161040ustar00rootroot00000000000000Edit1.