From b35f89f59574c8752f94eb84b1e0be48fae417fa Mon Sep 17 00:00:00 2001 From: Patrick DeVivo Date: Sun, 17 Jan 2021 18:55:01 -0500 Subject: [PATCH 1/7] rename files in gitqlite pkg, remove git_ prefix since it's redundant --- pkg/gitqlite/{git_branches.go => branches.go} | 0 pkg/gitqlite/{git_branches_test.go => branches_test.go} | 0 pkg/gitqlite/{git_file_iter.go => file_iter.go} | 0 pkg/gitqlite/{git_files.go => files.go} | 0 pkg/gitqlite/{git_files_test.go => files_test.go} | 0 pkg/gitqlite/{git_log.go => log.go} | 0 pkg/gitqlite/{git_log_cli.go => log_cli.go} | 0 pkg/gitqlite/{git_log_cli_test.go => log_cli_test.go} | 0 pkg/gitqlite/{git_log_test.go => log_test.go} | 0 pkg/gitqlite/{git_stats.go => stats.go} | 0 pkg/gitqlite/{git_stats_iter.go => stats_iter.go} | 0 pkg/gitqlite/{git_stats_test.go => stats_test.go} | 0 pkg/gitqlite/{git_tags.go => tags.go} | 0 pkg/gitqlite/{git_tags_test.go => tags_test.go} | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename pkg/gitqlite/{git_branches.go => branches.go} (100%) rename pkg/gitqlite/{git_branches_test.go => branches_test.go} (100%) rename pkg/gitqlite/{git_file_iter.go => file_iter.go} (100%) rename pkg/gitqlite/{git_files.go => files.go} (100%) rename pkg/gitqlite/{git_files_test.go => files_test.go} (100%) rename pkg/gitqlite/{git_log.go => log.go} (100%) rename pkg/gitqlite/{git_log_cli.go => log_cli.go} (100%) rename pkg/gitqlite/{git_log_cli_test.go => log_cli_test.go} (100%) rename pkg/gitqlite/{git_log_test.go => log_test.go} (100%) rename pkg/gitqlite/{git_stats.go => stats.go} (100%) rename pkg/gitqlite/{git_stats_iter.go => stats_iter.go} (100%) rename pkg/gitqlite/{git_stats_test.go => stats_test.go} (100%) rename pkg/gitqlite/{git_tags.go => tags.go} (100%) rename pkg/gitqlite/{git_tags_test.go => tags_test.go} (100%) diff --git a/pkg/gitqlite/git_branches.go b/pkg/gitqlite/branches.go similarity index 100% rename from pkg/gitqlite/git_branches.go rename to pkg/gitqlite/branches.go diff --git a/pkg/gitqlite/git_branches_test.go b/pkg/gitqlite/branches_test.go similarity index 100% rename from pkg/gitqlite/git_branches_test.go rename to pkg/gitqlite/branches_test.go diff --git a/pkg/gitqlite/git_file_iter.go b/pkg/gitqlite/file_iter.go similarity index 100% rename from pkg/gitqlite/git_file_iter.go rename to pkg/gitqlite/file_iter.go diff --git a/pkg/gitqlite/git_files.go b/pkg/gitqlite/files.go similarity index 100% rename from pkg/gitqlite/git_files.go rename to pkg/gitqlite/files.go diff --git a/pkg/gitqlite/git_files_test.go b/pkg/gitqlite/files_test.go similarity index 100% rename from pkg/gitqlite/git_files_test.go rename to pkg/gitqlite/files_test.go diff --git a/pkg/gitqlite/git_log.go b/pkg/gitqlite/log.go similarity index 100% rename from pkg/gitqlite/git_log.go rename to pkg/gitqlite/log.go diff --git a/pkg/gitqlite/git_log_cli.go b/pkg/gitqlite/log_cli.go similarity index 100% rename from pkg/gitqlite/git_log_cli.go rename to pkg/gitqlite/log_cli.go diff --git a/pkg/gitqlite/git_log_cli_test.go b/pkg/gitqlite/log_cli_test.go similarity index 100% rename from pkg/gitqlite/git_log_cli_test.go rename to pkg/gitqlite/log_cli_test.go diff --git a/pkg/gitqlite/git_log_test.go b/pkg/gitqlite/log_test.go similarity index 100% rename from pkg/gitqlite/git_log_test.go rename to pkg/gitqlite/log_test.go diff --git a/pkg/gitqlite/git_stats.go b/pkg/gitqlite/stats.go similarity index 100% rename from pkg/gitqlite/git_stats.go rename to pkg/gitqlite/stats.go diff --git a/pkg/gitqlite/git_stats_iter.go b/pkg/gitqlite/stats_iter.go similarity index 100% rename from pkg/gitqlite/git_stats_iter.go rename to pkg/gitqlite/stats_iter.go diff --git a/pkg/gitqlite/git_stats_test.go b/pkg/gitqlite/stats_test.go similarity index 100% rename from pkg/gitqlite/git_stats_test.go rename to pkg/gitqlite/stats_test.go diff --git a/pkg/gitqlite/git_tags.go b/pkg/gitqlite/tags.go similarity index 100% rename from pkg/gitqlite/git_tags.go rename to pkg/gitqlite/tags.go diff --git a/pkg/gitqlite/git_tags_test.go b/pkg/gitqlite/tags_test.go similarity index 100% rename from pkg/gitqlite/git_tags_test.go rename to pkg/gitqlite/tags_test.go From 5ef6ebd189cb3a53d549813f13e9d09fd66456ba Mon Sep 17 00:00:00 2001 From: Patrick DeVivo Date: Sun, 17 Jan 2021 19:19:12 -0500 Subject: [PATCH 2/7] add blame table implementation --- pkg/askgit/askgit.go | 12 ++- pkg/gitqlite/blame.go | 138 ++++++++++++++++++++++++++++++++++ pkg/gitqlite/blame_iter.go | 129 +++++++++++++++++++++++++++++++ pkg/gitqlite/blame_test.go | 53 +++++++++++++ pkg/gitqlite/gitqlite_test.go | 5 ++ 5 files changed, 330 insertions(+), 7 deletions(-) create mode 100644 pkg/gitqlite/blame.go create mode 100644 pkg/gitqlite/blame_iter.go create mode 100644 pkg/gitqlite/blame_test.go diff --git a/pkg/askgit/askgit.go b/pkg/askgit/askgit.go index 2586c730..59634bfa 100644 --- a/pkg/askgit/askgit.go +++ b/pkg/askgit/askgit.go @@ -153,6 +153,11 @@ func (a *AskGit) loadGitQLiteModules(conn *sqlite3.SQLiteConn) error { return err } + err = conn.CreateModule("blame", gitqlite.NewGitBlameModule(&gitqlite.GitBlameModuleOptions{RepoPath: a.RepoPath()})) + if err != nil { + return err + } + return nil } @@ -183,13 +188,6 @@ func (a *AskGit) loadGitHubModules(conn *sqlite3.SQLiteConn) error { if err != nil { return err } - err = conn.CreateModule("github_issues", ghqlite.NewIssuesModule(ghqlite.IssuesModuleOptions{ - Token: githubToken, - RateLimiter: rateLimiter, - })) - if err != nil { - return err - } return nil } diff --git a/pkg/gitqlite/blame.go b/pkg/gitqlite/blame.go new file mode 100644 index 00000000..002235ce --- /dev/null +++ b/pkg/gitqlite/blame.go @@ -0,0 +1,138 @@ +package gitqlite + +import ( + "fmt" + "io" + + git "github.com/libgit2/git2go/v31" + "github.com/mattn/go-sqlite3" +) + +type GitBlameModule struct { + options *GitBlameModuleOptions +} + +type GitBlameModuleOptions struct { + RepoPath string +} + +func NewGitBlameModule(options *GitBlameModuleOptions) *GitBlameModule { + return &GitBlameModule{options} +} + +type gitBlameTable struct { + repoPath string +} + +func (m *GitBlameModule) EponymousOnlyModule() {} + +func (m *GitBlameModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) { + err := c.DeclareVTab(fmt.Sprintf(` + CREATE TABLE %q ( + line_no INT, + path TEXT, + commit_id TEXT, + contents TEXT + )`, args[0])) + if err != nil { + return nil, err + } + + return &gitBlameTable{repoPath: m.options.RepoPath}, nil +} + +func (m *GitBlameModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) { + return m.Create(c, args) +} + +func (m *GitBlameModule) DestroyModule() {} + +func (v *gitBlameTable) Open() (sqlite3.VTabCursor, error) { + repo, err := git.OpenRepository(v.repoPath) + if err != nil { + return nil, err + } + + return &blameCursor{repo: repo}, nil + +} + +func (v *gitBlameTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy) (*sqlite3.IndexResult, error) { + // TODO this should actually be implemented! + dummy := make([]bool, len(cst)) + return &sqlite3.IndexResult{Used: dummy}, nil +} + +func (v *gitBlameTable) Disconnect() error { + return nil +} + +func (v *gitBlameTable) Destroy() error { return nil } + +type blameCursor struct { + repo *git.Repository + current *BlamedLine + iter *BlameIterator +} + +func (vc *blameCursor) Column(c *sqlite3.SQLiteContext, col int) error { + blamedLine := vc.current + switch col { + case 0: + c.ResultInt(blamedLine.Line) + case 1: + c.ResultText(blamedLine.File) + case 2: + c.ResultText(blamedLine.CommitID) + case 3: + c.ResultText(blamedLine.Content) + } + + return nil + +} + +func (vc *blameCursor) Filter(idxNum int, idxStr string, vals []interface{}) error { + iterator, err := NewBlameIterator(vc.repo) + if err != nil { + return err + } + + blamedLine, err := iterator.Next() + if err != nil { + if err == io.EOF { + vc.current = nil + return nil + } + return err + } + + vc.iter = iterator + vc.current = blamedLine + return nil +} + +func (vc *blameCursor) Next() error { + blamedLine, err := vc.iter.Next() + if err != nil { + if err == io.EOF { + vc.current = nil + return nil + } + return err + } + vc.current = blamedLine + return nil +} + +func (vc *blameCursor) EOF() bool { + return vc.current == nil +} + +func (vc *blameCursor) Rowid() (int64, error) { + return int64(0), nil +} + +func (vc *blameCursor) Close() error { + return nil +} diff --git a/pkg/gitqlite/blame_iter.go b/pkg/gitqlite/blame_iter.go new file mode 100644 index 00000000..96388e9d --- /dev/null +++ b/pkg/gitqlite/blame_iter.go @@ -0,0 +1,129 @@ +package gitqlite + +import ( + "strings" + + git "github.com/libgit2/git2go/v31" +) + +type BlameIterator struct { + repo *git.Repository + fileIter *commitFileIter + currentBlamedLines []*BlamedLine + currentBlamedLineIdx int +} + +type BlamedLine struct { + File string + Line int + CommitID string + Content string +} + +func NewBlameIterator(repo *git.Repository) (*BlameIterator, error) { + head, err := repo.Head() + if err != nil { + return nil, err + } + defer head.Free() + + // get a new iterator from repo and use the head commit + fileIter, err := NewCommitFileIter(repo, &commitFileIterOptions{head.Target().String()}) + if err != nil { + return nil, err + } + + return &BlameIterator{ + repo, + fileIter, + nil, + 0, + }, nil +} + +func (iter *BlameIterator) nextFile() error { + iter.currentBlamedLines = make([]*BlamedLine, 0) + + // grab the next file + file, err := iter.fileIter.Next() + if err != nil { + return err + } + defer file.Free() + + // blame the file + opts, err := git.DefaultBlameOptions() + if err != nil { + return err + } + blame, err := iter.repo.BlameFile(file.path+file.Name, &opts) + if err != nil { + return err + } + defer func() { + err := blame.Free() + if err != nil { + panic(err) + } + }() + + // store the lines of the file, used as we iterate over hunks + fileContents := file.Contents() + lines := strings.Split(string(fileContents), "\n") + + // iterate over the blame hunks + fileLine := 1 + for i := 0; i < blame.HunkCount(); i++ { + hunk, err := blame.HunkByIndex(i) + if err != nil { + return err + } + + // within a hunk, iterate over every line in the hunk + // creating and adding a new BlamedLine for each + for hunkLineOffset := 0; hunkLineOffset < int(hunk.LinesInHunk); hunkLineOffset++ { + // for every line of the hunk, create a BlamedLine + blamedLine := &BlamedLine{ + File: file.path + file.Name, + CommitID: hunk.OrigCommitId.String(), + Line: fileLine + hunkLineOffset, + Content: lines[i+hunkLineOffset], + } + // add it to the list for the current file + iter.currentBlamedLines = append(iter.currentBlamedLines, blamedLine) + // increment the file line by 1 + fileLine++ + } + } + iter.currentBlamedLineIdx = 0 + + return nil +} + +func (iter *BlameIterator) Next() (*BlamedLine, error) { + // if there's no currently blamed lines, grab the next file + if iter.currentBlamedLines == nil { + err := iter.nextFile() + if err != nil { + return nil, err + } + } + + // if we've exceeded the + if iter.currentBlamedLineIdx >= len(iter.currentBlamedLines) { + err := iter.nextFile() + if err != nil { + return nil, err + } + } + + // if there's no blamed lines + if len(iter.currentBlamedLines) == 0 { + return iter.Next() + } + + blamedLine := iter.currentBlamedLines[iter.currentBlamedLineIdx] + iter.currentBlamedLineIdx++ + + return blamedLine, nil +} diff --git a/pkg/gitqlite/blame_test.go b/pkg/gitqlite/blame_test.go new file mode 100644 index 00000000..65fd2b78 --- /dev/null +++ b/pkg/gitqlite/blame_test.go @@ -0,0 +1,53 @@ +package gitqlite + +import ( + "io" + "strconv" + "testing" +) + +func TestBlameDistinctFiles(t *testing.T) { + + rows, err := fixtureDB.Query("SELECT count(distinct path) from blame") + if err != nil { + t.Fatal(err) + } + defer rows.Close() + + _, contents, err := GetRowContents(rows) + if err != nil { + t.Fatal(err) + } + + gotFileCount, err := strconv.Atoi(contents[0][0]) + if err != nil { + t.Fatal(err) + } + + head, err := fixtureRepo.Head() + if err != nil { + t.Fatal(err) + } + defer head.Free() + + iter, err := NewCommitFileIter(fixtureRepo, &commitFileIterOptions{head.Target().String()}) + if err != nil { + t.Fatal(err) + } + + var expectedFileCount int + for { + _, err := iter.Next() + if err != nil { + if err == io.EOF { + break + } + t.Fatal(err) + } + expectedFileCount++ + } + + if gotFileCount != expectedFileCount { + t.Fatalf("expected %d distinct file paths in blame, got %d", expectedFileCount, gotFileCount) + } +} diff --git a/pkg/gitqlite/gitqlite_test.go b/pkg/gitqlite/gitqlite_test.go index 3c34a466..7bb6cfcb 100644 --- a/pkg/gitqlite/gitqlite_test.go +++ b/pkg/gitqlite/gitqlite_test.go @@ -112,6 +112,11 @@ func initFixtureDB(repoPath string) error { return err } + err = sqliteConn.CreateModule("blame", NewGitBlameModule(&GitBlameModuleOptions{RepoPath: repoPath})) + if err != nil { + return err + } + fixtureDB = db return nil } From 4e4dd4964756e8e1013bf30d1ce13fc41cca37cd Mon Sep 17 00:00:00 2001 From: vialeon Date: Mon, 18 Jan 2021 16:00:25 -0500 Subject: [PATCH 3/7] added contents testing --- pkg/gitqlite/blame_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkg/gitqlite/blame_test.go b/pkg/gitqlite/blame_test.go index 65fd2b78..7c0a0cf6 100644 --- a/pkg/gitqlite/blame_test.go +++ b/pkg/gitqlite/blame_test.go @@ -50,4 +50,25 @@ func TestBlameDistinctFiles(t *testing.T) { if gotFileCount != expectedFileCount { t.Fatalf("expected %d distinct file paths in blame, got %d", expectedFileCount, gotFileCount) } + iterator, err := NewBlameIterator(fixtureRepo) + if err != nil { + t.Fatal(err) + } + rows, err = fixtureDB.Query("SELECT contents from blame limit 100") + if err != nil { + t.Fatal(err) + } + _, lines, err := GetRowContents(rows) + if err != nil { + t.Fatal(err) + } + for _, line := range lines { + cont, err := iterator.Next() + if err != nil { + t.Fatal(err) + } + if !(line[0] == cont.Content) { + t.Fatalf("expected %s content in blame, got %s", cont.Content, line[0]) + } + } } From 284bb94a519abdc99324ffc1b6778b72a5b7d1ed Mon Sep 17 00:00:00 2001 From: vialeon Date: Mon, 18 Jan 2021 16:04:36 -0500 Subject: [PATCH 4/7] make it a different function --- pkg/gitqlite/blame_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/gitqlite/blame_test.go b/pkg/gitqlite/blame_test.go index 7c0a0cf6..b23bd78b 100644 --- a/pkg/gitqlite/blame_test.go +++ b/pkg/gitqlite/blame_test.go @@ -50,11 +50,14 @@ func TestBlameDistinctFiles(t *testing.T) { if gotFileCount != expectedFileCount { t.Fatalf("expected %d distinct file paths in blame, got %d", expectedFileCount, gotFileCount) } + +} +func TestBlameContents(t *testing.T) { iterator, err := NewBlameIterator(fixtureRepo) if err != nil { t.Fatal(err) } - rows, err = fixtureDB.Query("SELECT contents from blame limit 100") + rows, err := fixtureDB.Query("SELECT contents from blame limit 100") if err != nil { t.Fatal(err) } From a54fc82ce2408dc68e261a984cea59d090750ebe Mon Sep 17 00:00:00 2001 From: vialeon Date: Mon, 18 Jan 2021 16:07:33 -0500 Subject: [PATCH 5/7] add blame to the readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index cd8875da..07ea2363 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,18 @@ Similar to `git log`, the `commits` table includes all commits in the history of | parent_id | TEXT | | parent_count | INT | +##### `blame` + +Similar to `git blame`, the `blame` table includes the blame of all files in the current HEAD. + +| Column | Type | +|-----------------|----------| +| line_no | INT | +| path | TEXT | +| commit_id | TEXT | +| contents | TEXT | + + ##### `stats` | Column | Type | From 2361e3218fc272a02ed34fee5e950c1c937ede44 Mon Sep 17 00:00:00 2001 From: vialeon Date: Mon, 18 Jan 2021 16:12:14 -0500 Subject: [PATCH 6/7] add commit_id and filename testing --- pkg/gitqlite/blame_test.go | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pkg/gitqlite/blame_test.go b/pkg/gitqlite/blame_test.go index b23bd78b..868f06de 100644 --- a/pkg/gitqlite/blame_test.go +++ b/pkg/gitqlite/blame_test.go @@ -75,3 +75,49 @@ func TestBlameContents(t *testing.T) { } } } +func TestBlameCommitID(t *testing.T) { + iterator, err := NewBlameIterator(fixtureRepo) + if err != nil { + t.Fatal(err) + } + rows, err := fixtureDB.Query("SELECT commit_id from blame limit 100") + if err != nil { + t.Fatal(err) + } + _, lines, err := GetRowContents(rows) + if err != nil { + t.Fatal(err) + } + for _, line := range lines { + cont, err := iterator.Next() + if err != nil { + t.Fatal(err) + } + if !(line[0] == cont.CommitID) { + t.Fatalf("expected %s content in blame, got %s", cont.Content, line[0]) + } + } +} +func TestBlameFileNames(t *testing.T) { + iterator, err := NewBlameIterator(fixtureRepo) + if err != nil { + t.Fatal(err) + } + rows, err := fixtureDB.Query("SELECT path from blame limit 100") + if err != nil { + t.Fatal(err) + } + _, lines, err := GetRowContents(rows) + if err != nil { + t.Fatal(err) + } + for _, line := range lines { + cont, err := iterator.Next() + if err != nil { + t.Fatal(err) + } + if !(line[0] == cont.File) { + t.Fatalf("expected %s content in blame, got %s", cont.Content, line[0]) + } + } +} From ca4e34501bf50f72047bb81bea8a8fddaa33f2a3 Mon Sep 17 00:00:00 2001 From: Patrick DeVivo Date: Mon, 18 Jan 2021 18:24:37 -0500 Subject: [PATCH 7/7] update TOC and minor wording change --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 07ea2363..50f2efe0 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ More in-depth examples and documentation can be found below. - [Tables](#tables) - [Local Git Repository](#local-git-repository) - [`commits`](#commits) + - [`blame`](#blame) - [`stats`](#stats) - [`files`](#files) - [`branches`](#branches) @@ -150,7 +151,7 @@ Similar to `git log`, the `commits` table includes all commits in the history of ##### `blame` -Similar to `git blame`, the `blame` table includes the blame of all files in the current HEAD. +Similar to `git blame`, the `blame` table includes blame information for all files in the current HEAD. | Column | Type | |-----------------|----------|