diff --git a/cmd/params/params.go b/cmd/params/params.go index 7538f223..cc3f59c5 100644 --- a/cmd/params/params.go +++ b/cmd/params/params.go @@ -29,10 +29,13 @@ import ( "golang.org/x/net/http/httpproxy" ) -const errMsgTemplate = "invalid url %q. Must be in format" + - "'https://user:pass@host:port' or " + - "'socks5://user:pass@host:port' or " + - "'domain\\\\user:pass.'" +const ( + defaultReadAPIKeyTimeoutSecs = 2 + errMsgTemplate = "invalid url %q. Must be in format" + + "'https://user:pass@host:port' or " + + "'socks5://user:pass@host:port' or " + + "'domain\\\\user:pass.'" +) var ( // nolint @@ -668,7 +671,7 @@ func readAPIKeyFromCommand(cmdStr string) (string, error) { cmdName := cmdParts[0] cmdArgs := cmdParts[1:] - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), defaultReadAPIKeyTimeoutSecs*time.Second) defer cancel() cmd := exec.CommandContext(ctx, cmdName, cmdArgs...) // nolint:gosec diff --git a/pkg/git/git.go b/pkg/git/git.go new file mode 100644 index 00000000..a07a0658 --- /dev/null +++ b/pkg/git/git.go @@ -0,0 +1,115 @@ +package git + +import ( + "context" + "fmt" + "os" + "os/exec" + "regexp" + "strconv" + "time" + + "github.com/wakatime/wakatime-cli/pkg/log" +) + +const defaultCountLinesChangedTimeoutSecs = 2 + +var gitLinesChangedRegex = regexp.MustCompile(`^(?P\d+)\s*(?P\d+)\s*(?s).*$`) + +type ( + // Git is an interface to git. + Git interface { + CountLinesChanged() (*int, *int, error) + } + + // Client is a git client. + Client struct { + filepath string + GitCmd func(args ...string) (string, error) + } +) + +// New creates a new git client. +func New(filepath string) *Client { + return &Client{ + filepath: filepath, + GitCmd: gitCmdFn, + } +} + +// gitCmdFn runs a git command with the specified env vars and returns its output or errors. +func gitCmdFn(args ...string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultCountLinesChangedTimeoutSecs*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "git", args...) + cmd.Stderr = os.Stderr + + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to execute git command: %s", err) + } + + return string(out), nil +} + +// CountLinesChanged counts the number of lines added and removed in a file. +func (c *Client) CountLinesChanged() (*int, *int, error) { + if !fileExists(c.filepath) { + return nil, nil, nil + } + + out, err := c.GitCmd("diff", "--numstat", c.filepath) + if err != nil { + return nil, nil, fmt.Errorf("failed to count lines changed: %s", err) + } + + if out == "" { + // Maybe it's staged, try with --cached. + out, err = c.GitCmd("diff", "--numstat", "--cached", c.filepath) + if err != nil { + return nil, nil, fmt.Errorf("failed to count lines changed: %s", err) + } + } + + if out == "" { + return nil, nil, nil + } + + match := gitLinesChangedRegex.FindStringSubmatch(out) + paramsMap := make(map[string]string) + + for i, name := range gitLinesChangedRegex.SubexpNames() { + if i > 0 && i <= len(match) { + paramsMap[name] = match[i] + } + } + + if len(paramsMap) == 0 { + log.Debugf("failed to parse git diff output: %s", out) + + return nil, nil, nil + } + + var added, removed *int + + if val, ok := paramsMap["added"]; ok { + if v, err := strconv.Atoi(val); err == nil { + added = &v + } + } + + if val, ok := paramsMap["removed"]; ok { + if v, err := strconv.Atoi(val); err == nil { + removed = &v + } + } + + return added, removed, nil +} + +// fileExists checks if a file or directory exist. +func fileExists(fp string) bool { + _, err := os.Stat(fp) + return err == nil || os.IsExist(err) +} diff --git a/pkg/git/git_test.go b/pkg/git/git_test.go new file mode 100644 index 00000000..497d4724 --- /dev/null +++ b/pkg/git/git_test.go @@ -0,0 +1,124 @@ +package git_test + +import ( + "errors" + "testing" + + "github.com/wakatime/wakatime-cli/pkg/git" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCountLinesChanged(t *testing.T) { + gc := git.New("testdata/main.go") + gc.GitCmd = func(args ...string) (string, error) { + assert.Equal(t, args, []string{"diff", "--numstat", "testdata/main.go"}) + + return "4 1 testdata/main.go", nil + } + + added, removed, err := gc.CountLinesChanged() + require.NoError(t, err) + + require.NotNil(t, added) + require.NotNil(t, removed) + + assert.Equal(t, 4, *added) + assert.Equal(t, 1, *removed) +} + +func TestCountLinesChanged_Err(t *testing.T) { + gc := git.New("testdata/main.go") + gc.GitCmd = func(args ...string) (string, error) { + assert.Equal(t, args, []string{"diff", "--numstat", "testdata/main.go"}) + + return "", errors.New("some error") + } + + added, removed, err := gc.CountLinesChanged() + assert.EqualError(t, err, "failed to count lines changed: some error") + + assert.Nil(t, added) + assert.Nil(t, removed) +} + +func TestCountLinesChanged_Staged(t *testing.T) { + gc := git.New("testdata/main.go") + + var numCalls int + + gc.GitCmd = func(args ...string) (string, error) { + numCalls++ + + switch numCalls { + case 1: + assert.Equal(t, args, []string{"diff", "--numstat", "testdata/main.go"}) + case 2: + assert.Equal(t, args, []string{"diff", "--numstat", "--cached", "testdata/main.go"}) + + return "4 1 testdata/main.go", nil + } + + return "", nil + } + + added, removed, err := gc.CountLinesChanged() + assert.NoError(t, err) + + require.NotNil(t, added) + require.NotNil(t, removed) + + assert.Equal(t, 4, *added) + assert.Equal(t, 1, *removed) +} + +func TestCountLinesChanged_MissingFile(t *testing.T) { + gc := git.New("/tmp/missing-file") + + added, removed, err := gc.CountLinesChanged() + assert.NoError(t, err) + + assert.Nil(t, added) + assert.Nil(t, removed) +} + +func TestCountLinesChanged_NoOutput(t *testing.T) { + gc := git.New("testdata/main.go") + + var numCalls int + + gc.GitCmd = func(args ...string) (string, error) { + numCalls++ + + switch numCalls { + case 1: + assert.Equal(t, args, []string{"diff", "--numstat", "testdata/main.go"}) + case 2: + assert.Equal(t, args, []string{"diff", "--numstat", "--cached", "testdata/main.go"}) + } + + return "", nil + } + + added, removed, err := gc.CountLinesChanged() + assert.NoError(t, err) + + assert.Nil(t, added) + assert.Nil(t, removed) +} + +func TestCountLinesChanged_MalformedOutput(t *testing.T) { + gc := git.New("testdata/main.go") + gc.GitCmd = func(args ...string) (string, error) { + assert.Equal(t, args, []string{"diff", "--numstat", "testdata/main.go"}) + + return "malformed output", nil + } + + added, removed, err := gc.CountLinesChanged() + assert.NoError(t, err) + + assert.Nil(t, added) + assert.Nil(t, removed) +} diff --git a/pkg/git/testdata/main.go b/pkg/git/testdata/main.go new file mode 100644 index 00000000..6454500a --- /dev/null +++ b/pkg/git/testdata/main.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +func main() { + sum := 1 + 2 + + fmt.Printf("sum: %d\n", sum) +} diff --git a/pkg/heartbeat/heartbeat.go b/pkg/heartbeat/heartbeat.go index a29031e5..f2282a86 100644 --- a/pkg/heartbeat/heartbeat.go +++ b/pkg/heartbeat/heartbeat.go @@ -33,6 +33,8 @@ type Heartbeat struct { LanguageAlternate string `json:"-"` LineNumber *int `json:"lineno,omitempty"` Lines *int `json:"lines,omitempty"` + LinesAdded *int `json:"lines_added,omitempty"` + LinesRemoved *int `json:"lines_removed,omitempty"` LocalFile string `json:"-"` LocalFileNeedsCleanup bool `json:"-"` Project *string `json:"project,omitempty"` diff --git a/pkg/project/git.go b/pkg/project/git.go index 1449bdb3..741411ec 100644 --- a/pkg/project/git.go +++ b/pkg/project/git.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" + "github.com/wakatime/wakatime-cli/pkg/git" "github.com/wakatime/wakatime-cli/pkg/log" "github.com/wakatime/wakatime-cli/pkg/regex" ) @@ -13,6 +14,8 @@ import ( type Git struct { // Filepath contains the entity path. Filepath string + // git contains the git client. + GitClient git.Git // ProjectFromGitRemote when enabled uses the git remote as the project name instead of local git folder. ProjectFromGitRemote bool // SubmoduleDisabledPatterns will be matched against the submodule path and if matching, will skip it. @@ -48,16 +51,23 @@ func (g Git) Detect() (Result, bool, error) { branch, err := findGitBranch(filepath.Join(gitdirSubmodule, "HEAD")) if err != nil { log.Errorf( - "error finding branch from %q: %s", + "failed to find branch from %q: %s", filepath.Join(filepath.Dir(gitdirSubmodule), "HEAD"), err, ) } + added, removed, err := g.GitClient.CountLinesChanged() + if err != nil { + log.Errorf("failed to count lines changed: %s", err) + } + return Result{ - Project: project, - Branch: branch, - Folder: filepath.Dir(gitdirSubmodule), + Project: project, + Branch: branch, + Folder: filepath.Dir(gitdirSubmodule), + LinesAdded: added, + LinesRemoved: removed, }, true, nil } @@ -70,14 +80,14 @@ func (g Git) Detect() (Result, bool, error) { // Find for gitdir path gitdir, err := findGitdir(dotGit) if err != nil { - return Result{}, false, fmt.Errorf("error finding gitdir: %s", err) + return Result{}, false, fmt.Errorf("failed to find gitdir: %s", err) } // Commonly .git file is present when it's a worktree // Find for commondir file commondir, ok, err := findCommondir(gitdir) if err != nil { - return Result{}, false, fmt.Errorf("error finding commondir: %s", err) + return Result{}, false, fmt.Errorf("failed to find commondir: %s", err) } // we found a commondir file so this is a worktree @@ -87,16 +97,23 @@ func (g Git) Detect() (Result, bool, error) { branch, err := findGitBranch(filepath.Join(gitdir, "HEAD")) if err != nil { log.Errorf( - "error finding branch from %q: %s", + "failed to find branch from %q: %s", filepath.Join(filepath.Dir(dotGit), "HEAD"), err, ) } + added, removed, err := g.GitClient.CountLinesChanged() + if err != nil { + log.Errorf("failed to count lines changed: %s", err) + } + return Result{ - Project: project, - Branch: branch, - Folder: filepath.Dir(commondir), + Project: project, + Branch: branch, + Folder: filepath.Dir(commondir), + LinesAdded: added, + LinesRemoved: removed, }, true, nil } @@ -107,16 +124,23 @@ func (g Git) Detect() (Result, bool, error) { branch, err := findGitBranch(filepath.Join(gitdir, "HEAD")) if err != nil { log.Errorf( - "error finding branch from %q: %s", + "failed to find branch from %q: %s", filepath.Join(filepath.Dir(gitdir), "HEAD"), err, ) } + added, removed, err := g.GitClient.CountLinesChanged() + if err != nil { + log.Errorf("failed to count lines changed: %s", err) + } + return Result{ - Project: project, - Branch: branch, - Folder: filepath.Join(gitdir, ".."), + Project: project, + Branch: branch, + Folder: filepath.Join(gitdir, ".."), + LinesAdded: added, + LinesRemoved: removed, }, true, nil } @@ -125,23 +149,30 @@ func (g Git) Detect() (Result, bool, error) { if ok { gitDir := filepath.Dir(gitConfigFile) + projectDir := filepath.Join(gitDir, "..") + project := projectOrRemote(filepath.Base(projectDir), g.ProjectFromGitRemote, gitDir) branch, err := findGitBranch(filepath.Join(gitDir, "HEAD")) if err != nil { log.Errorf( - "error finding branch from %q: %s", + "failed to find branch from %q: %s", filepath.Join(gitDir, "HEAD"), err, ) } - project := projectOrRemote(filepath.Base(projectDir), g.ProjectFromGitRemote, gitDir) + added, removed, err := g.GitClient.CountLinesChanged() + if err != nil { + log.Errorf("failed to count lines changed: %s", err) + } return Result{ - Project: project, - Branch: branch, - Folder: projectDir, + Project: project, + Branch: branch, + Folder: projectDir, + LinesAdded: added, + LinesRemoved: removed, }, true, nil } diff --git a/pkg/project/git_test.go b/pkg/project/git_test.go index b67d263d..010158dd 100644 --- a/pkg/project/git_test.go +++ b/pkg/project/git_test.go @@ -8,6 +8,8 @@ import ( "runtime" "testing" + "github.com/wakatime/wakatime-cli/pkg/git" + "github.com/wakatime/wakatime-cli/pkg/heartbeat" "github.com/wakatime/wakatime-cli/pkg/project" "github.com/wakatime/wakatime-cli/pkg/regex" "github.com/wakatime/wakatime-cli/pkg/windows" @@ -18,36 +20,55 @@ import ( ) func TestGit_Detect(t *testing.T) { - fp := setupTestGitBasic(t) + dir := setupTestGitBasic(t) + fp := filepath.Join(dir, "wakatime-cli/src/pkg/file.go") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + assert.Equal(t, args, []string{"diff", "--numstat", fp}) + + return "12 5 pkg/file.go", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "wakatime-cli/src/pkg/file.go"), + Filepath: filepath.Join(fp, "wakatime-cli/src/pkg/file.go"), + GitClient: gc, } result, detected, err := g.Detect() require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ - Project: "wakatime-cli", - Branch: "master", - Folder: result.Folder, + Project: "wakatime-cli", + Branch: "master", + Folder: result.Folder, + LinesAdded: heartbeat.PointerTo(12), + LinesRemoved: heartbeat.PointerTo(5), }, result) } func TestGit_Detect_BranchWithSlash(t *testing.T) { - fp := setupTestGitBasicBranchWithSlash(t) + dir := setupTestGitBasicBranchWithSlash(t) + + fp := filepath.Join(dir, "wakatime-cli/src/pkg/file.go") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "wakatime-cli/src/pkg/file.go"), + Filepath: fp, + GitClient: gc, } result, detected, err := g.Detect() require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "wakatime-cli", Branch: "feature/detection", @@ -56,17 +77,25 @@ func TestGit_Detect_BranchWithSlash(t *testing.T) { } func TestGit_Detect_DetachedHead(t *testing.T) { - fp := setupTestGitBasicDetachedHead(t) + dir := setupTestGitBasicDetachedHead(t) + + fp := filepath.Join(dir, "wakatime-cli/src/pkg/file.go") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "wakatime-cli/src/pkg/file.go"), + Filepath: fp, + GitClient: gc, } result, detected, err := g.Detect() require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "wakatime-cli", Branch: "", @@ -75,37 +104,43 @@ func TestGit_Detect_DetachedHead(t *testing.T) { } func TestGit_Detect_GitConfigFile_File(t *testing.T) { - fp := setupTestGitFile(t) + dir := setupTestGitFile(t) tests := map[string]struct { Filepath string Project string }{ "main repo": { - Filepath: filepath.Join(fp, "wakatime-cli/src/pkg/file.go"), + Filepath: filepath.Join(dir, "wakatime-cli/src/pkg/file.go"), Project: "wakatime-cli", }, "relative path": { - Filepath: filepath.Join(fp, "feed/src/pkg/file.go"), + Filepath: filepath.Join(dir, "feed/src/pkg/file.go"), Project: "feed", }, "absolute path": { - Filepath: filepath.Join(fp, "mobile/src/pkg/file.go"), + Filepath: filepath.Join(dir, "mobile/src/pkg/file.go"), Project: "mobile", }, } for name, test := range tests { t.Run(name, func(t *testing.T) { + gc := git.New(test.Filepath) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } + g := project.Git{ - Filepath: test.Filepath, + Filepath: test.Filepath, + GitClient: gc, } result, detected, err := g.Detect() require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: test.Project, Branch: "feature/list-elements", @@ -116,17 +151,25 @@ func TestGit_Detect_GitConfigFile_File(t *testing.T) { } func TestGit_Detect_Worktree(t *testing.T) { - fp := setupTestGitWorktree(t) + dir := setupTestGitWorktree(t) + + fp := filepath.Join(dir, "api/src/pkg/file.go") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "api/src/pkg/file.go"), + Filepath: fp, + GitClient: gc, } result, detected, err := g.Detect() require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "wakatime-cli", Branch: "feature/api", @@ -135,10 +178,18 @@ func TestGit_Detect_Worktree(t *testing.T) { } func TestGit_Detect_WorktreeGitRemote(t *testing.T) { - fp := setupTestGitWorktree(t) + dir := setupTestGitWorktree(t) + + fp := filepath.Join(dir, "api/src/pkg/file.go") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "api/src/pkg/file.go"), + Filepath: fp, + GitClient: gc, ProjectFromGitRemote: true, } @@ -146,7 +197,7 @@ func TestGit_Detect_WorktreeGitRemote(t *testing.T) { require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "wakatime/wakatime-cli", Branch: "feature/api", @@ -155,10 +206,18 @@ func TestGit_Detect_WorktreeGitRemote(t *testing.T) { } func TestGit_Detect_Submodule(t *testing.T) { - fp := setupTestGitSubmodule(t) + dir := setupTestGitSubmodule(t) + + fp := filepath.Join(dir, "wakatime-cli/lib/billing/src/lib/lib.cpp") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "wakatime-cli/lib/billing/src/lib/lib.cpp"), + Filepath: fp, + GitClient: gc, SubmoduleDisabledPatterns: []regex.Regex{regexp.MustCompile("not_matching")}, } @@ -166,7 +225,7 @@ func TestGit_Detect_Submodule(t *testing.T) { require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "billing", Branch: "master", @@ -175,10 +234,18 @@ func TestGit_Detect_Submodule(t *testing.T) { } func TestGit_Detect_SubmoduleDisabled(t *testing.T) { - fp := setupTestGitSubmodule(t) + dir := setupTestGitSubmodule(t) + + fp := filepath.Join(dir, "wakatime-cli/lib/billing/src/lib/lib.cpp") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "wakatime-cli/lib/billing/src/lib/lib.cpp"), + Filepath: fp, + GitClient: gc, SubmoduleDisabledPatterns: []regex.Regex{regexp.MustCompile(".*billing.*")}, } @@ -186,7 +253,7 @@ func TestGit_Detect_SubmoduleDisabled(t *testing.T) { require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "wakatime-cli", Branch: "feature/billing", @@ -195,10 +262,18 @@ func TestGit_Detect_SubmoduleDisabled(t *testing.T) { } func TestGit_Detect_SubmoduleProjectMap_NotMatch(t *testing.T) { - fp := setupTestGitSubmodule(t) + dir := setupTestGitSubmodule(t) + + fp := filepath.Join(dir, "wakatime-cli/lib/billing/src/lib/lib.cpp") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "wakatime-cli/lib/billing/src/lib/lib.cpp"), + Filepath: fp, + GitClient: gc, SubmoduleProjectMapPatterns: []project.MapPattern{ { Name: "my-project-1", @@ -211,7 +286,7 @@ func TestGit_Detect_SubmoduleProjectMap_NotMatch(t *testing.T) { require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "billing", Branch: "master", @@ -220,10 +295,18 @@ func TestGit_Detect_SubmoduleProjectMap_NotMatch(t *testing.T) { } func TestGit_Detect_SubmoduleProjectMap(t *testing.T) { - fp := setupTestGitSubmodule(t) + dir := setupTestGitSubmodule(t) + + fp := filepath.Join(dir, "wakatime-cli/lib/billing/src/lib/lib.cpp") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "wakatime-cli/lib/billing/src/lib/lib.cpp"), + Filepath: fp, + GitClient: gc, SubmoduleProjectMapPatterns: []project.MapPattern{ { Name: "my-project-1", @@ -236,7 +319,7 @@ func TestGit_Detect_SubmoduleProjectMap(t *testing.T) { require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "my-project-1", Branch: "master", @@ -245,10 +328,18 @@ func TestGit_Detect_SubmoduleProjectMap(t *testing.T) { } func TestGit_Detect_SubmoduleGitRemote(t *testing.T) { - fp := setupTestGitSubmodule(t) + dir := setupTestGitSubmodule(t) + + fp := filepath.Join(dir, "wakatime-cli/lib/billing/src/lib/lib.cpp") + + gc := git.New(fp) + gc.GitCmd = func(args ...string) (string, error) { + return "", nil + } g := project.Git{ - Filepath: filepath.Join(fp, "wakatime-cli/lib/billing/src/lib/lib.cpp"), + Filepath: fp, + GitClient: gc, ProjectFromGitRemote: true, SubmoduleDisabledPatterns: []regex.Regex{regexp.MustCompile("not_matching")}, } @@ -257,7 +348,7 @@ func TestGit_Detect_SubmoduleGitRemote(t *testing.T) { require.NoError(t, err) assert.True(t, detected) - assert.Contains(t, result.Folder, filepath.Join(fp, "wakatime-cli")) + assert.Contains(t, result.Folder, filepath.Join(dir, "wakatime-cli")) assert.Equal(t, project.Result{ Project: "wakatime/billing", Branch: "master", @@ -265,7 +356,7 @@ func TestGit_Detect_SubmoduleGitRemote(t *testing.T) { }, result) } -func setupTestGitBasic(t *testing.T) (fp string) { +func setupTestGitBasic(t *testing.T) string { tmpDir := t.TempDir() tmpDir, err := realpath.Realpath(tmpDir) @@ -292,7 +383,7 @@ func setupTestGitBasic(t *testing.T) (fp string) { return tmpDir } -func setupTestGitBasicBranchWithSlash(t *testing.T) (fp string) { +func setupTestGitBasicBranchWithSlash(t *testing.T) string { tmpDir := t.TempDir() tmpDir, err := realpath.Realpath(tmpDir) @@ -319,7 +410,7 @@ func setupTestGitBasicBranchWithSlash(t *testing.T) (fp string) { return tmpDir } -func setupTestGitBasicDetachedHead(t *testing.T) (fp string) { +func setupTestGitBasicDetachedHead(t *testing.T) string { tmpDir := t.TempDir() tmpDir, err := realpath.Realpath(tmpDir) @@ -346,7 +437,7 @@ func setupTestGitBasicDetachedHead(t *testing.T) (fp string) { return tmpDir } -func setupTestGitFile(t *testing.T) (fp string) { +func setupTestGitFile(t *testing.T) string { tmpDir := t.TempDir() tmpDir, err := realpath.Realpath(tmpDir) @@ -406,7 +497,7 @@ func setupTestGitFile(t *testing.T) (fp string) { return tmpDir } -func setupTestGitWorktree(t *testing.T) (fp string) { +func setupTestGitWorktree(t *testing.T) string { tmpDir := t.TempDir() tmpDir, err := realpath.Realpath(tmpDir) @@ -456,7 +547,7 @@ func setupTestGitWorktree(t *testing.T) (fp string) { return tmpDir } -func setupTestGitSubmodule(t *testing.T) (fp string) { +func setupTestGitSubmodule(t *testing.T) string { tmpDir := t.TempDir() tmpDir, err := realpath.Realpath(tmpDir) diff --git a/pkg/project/project.go b/pkg/project/project.go index 7160535b..bc68a301 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/wakatime/wakatime-cli/pkg/git" "github.com/wakatime/wakatime-cli/pkg/heartbeat" "github.com/wakatime/wakatime-cli/pkg/log" "github.com/wakatime/wakatime-cli/pkg/regex" @@ -97,9 +98,11 @@ type ( // Result contains the result of Detect(). Result struct { - Project string - Branch string - Folder string + Branch string + Folder string + LinesAdded *int + LinesRemoved *int + Project string } // Config contains project detection configurations. @@ -171,6 +174,8 @@ func WithDetection(config Config) heartbeat.HandleOption { result.Project = firstNonEmptyString(result.Project, revControlResult.Project) result.Branch = firstNonEmptyString(result.Branch, revControlResult.Branch) result.Folder = firstNonEmptyString(result.Folder, revControlResult.Folder) + result.LinesAdded = revControlResult.LinesAdded + result.LinesRemoved = revControlResult.LinesRemoved } // fourth, use alternate project @@ -215,6 +220,8 @@ func WithDetection(config Config) heartbeat.HandleOption { hh[n].Project = &result.Project hh[n].Branch = &result.Branch hh[n].ProjectPath = result.Folder + hh[n].LinesAdded = result.LinesAdded + hh[n].LinesRemoved = result.LinesRemoved } return next(hh) @@ -271,6 +278,7 @@ func DetectWithRevControl( var revControlPlugins = []Detecter{ Git{ Filepath: arg.Filepath, + GitClient: git.New(arg.Filepath), ProjectFromGitRemote: projectFromGitRemote, SubmoduleDisabledPatterns: submoduleDisabledPatterns, SubmoduleProjectMapPatterns: submoduleProjectMapPatterns, @@ -297,9 +305,11 @@ func DetectWithRevControl( if detected { return Result{ - Project: result.Project, - Branch: result.Branch, - Folder: result.Folder, + Project: result.Project, + Branch: result.Branch, + Folder: result.Folder, + LinesAdded: result.LinesAdded, + LinesRemoved: result.LinesRemoved, } } }