Skip to content

Commit cb0d8d9

Browse files
committed
Release v2.8.2: Add --since flag back to gx who (Go + Python)
1 parent cf66a60 commit cb0d8d9

4 files changed

Lines changed: 43 additions & 21 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.8.1
1+
2.8.2

cmd/who.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ func init() {
2121
RunE: runWho,
2222
}
2323
cmd.Flags().IntP("number", "n", 5, "Number of contributors to show")
24+
cmd.Flags().String("since", "", "Only consider contributions after this date (e.g., 6months, 2024-01-01)")
2425
cmd.Flags().Bool("no-limit", false, "Remove the 200-file cap for directory analysis")
2526
rootCmd.AddCommand(cmd)
2627
}
@@ -32,9 +33,10 @@ func runWho(cmd *cobra.Command, args []string) error {
3233
}
3334

3435
n, _ := cmd.Flags().GetInt("number")
36+
since, _ := cmd.Flags().GetString("since")
3537

3638
if len(args) == 0 {
37-
return whoRepo(n)
39+
return whoRepo(n, since)
3840
}
3941

4042
path := args[0]
@@ -45,9 +47,9 @@ func runWho(cmd *cobra.Command, args []string) error {
4547
}
4648
if info.IsDir() {
4749
noLimit, _ := cmd.Flags().GetBool("no-limit")
48-
return whoDir(path, n, noLimit)
50+
return whoDir(path, n, since, noLimit)
4951
}
50-
return whoFile(path, n)
52+
return whoFile(path, n, since)
5153
}
5254

5355
// --- File-level: git blame (fast, ~100ms per file) ---
@@ -59,10 +61,15 @@ type blameAuthor struct {
5961
lastDate string
6062
}
6163

62-
func whoFile(path string, n int) error {
64+
func whoFile(path string, n int, since string) error {
6365
sp := ui.StartSpinner(fmt.Sprintf("Analyzing %s...", path))
6466

65-
out, err := git.Run("blame", "--line-porcelain", path)
67+
blameArgs := []string{"blame", "--line-porcelain"}
68+
if since != "" {
69+
blameArgs = append(blameArgs, "--since", since)
70+
}
71+
blameArgs = append(blameArgs, path)
72+
out, err := git.Run(blameArgs...)
6673
if err != nil {
6774
sp.Stop()
6875
ui.PrintError(fmt.Sprintf("Failed to blame %s: %s", path, err))
@@ -145,7 +152,7 @@ func whoFile(path string, n int) error {
145152

146153
// --- Directory-level: concurrent blame across files ---
147154

148-
func whoDir(dir string, n int, noLimit bool) error {
155+
func whoDir(dir string, n int, since string, noLimit bool) error {
149156
sp := ui.StartSpinner(fmt.Sprintf("Analyzing %s...", dir))
150157

151158
filesOut, err := git.Run("ls-files", dir)
@@ -192,7 +199,12 @@ func whoDir(dir string, n int, noLimit bool) error {
192199
sem <- struct{}{}
193200
defer func() { <-sem }()
194201

195-
out := git.RunUnchecked("blame", "--line-porcelain", f)
202+
bArgs := []string{"blame", "--line-porcelain"}
203+
if since != "" {
204+
bArgs = append(bArgs, "--since", since)
205+
}
206+
bArgs = append(bArgs, f)
207+
out := git.RunUnchecked(bArgs...)
196208
counts := map[string]int{}
197209
emails := map[string]string{}
198210
var name, email string
@@ -284,10 +296,14 @@ func whoDir(dir string, n int, noLimit bool) error {
284296

285297
// --- Repo-level: git shortlog (fast, commits only) ---
286298

287-
func whoRepo(n int) error {
299+
func whoRepo(n int, since string) error {
288300
sp := ui.StartSpinner("Analyzing contributors...")
289301

290-
out, err := git.Run("shortlog", "-sne", "HEAD")
302+
args := []string{"shortlog", "-sne", "HEAD"}
303+
if since != "" {
304+
args = append(args, "--since="+since)
305+
}
306+
out, err := git.Run(args...)
291307
sp.Stop()
292308
if err != nil || out == "" {
293309
ui.PrintInfo("No contributors found.")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "gx-git"
7-
version = "2.8.1"
7+
version = "2.8.2"
88
description = "Git Productivity Toolkit"
99
readme = "README.md"
1010
license = {text = "MIT"}

src/gx/commands/who.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
from gx.utils.git import GitError, ensure_git_repo, run_git, time_ago
1515

1616

17-
def _blame_file(filepath: str, cwd: str | None = None) -> dict[str, dict]:
17+
def _blame_file(filepath: str, cwd: str | None = None, since: str | None = None) -> dict[str, dict]:
1818
"""Run git blame on a file and return per-author stats.
1919
2020
Returns {author: {"lines": int, "email": str, "last_ts": str}}.
2121
"""
22-
args = ["blame", "--line-porcelain", filepath]
22+
args = ["blame", "--line-porcelain"]
23+
if since:
24+
args.extend(["--since", since])
25+
args.append(filepath)
2326
try:
2427
output = run_git(args, cwd=cwd)
2528
except GitError:
@@ -70,9 +73,11 @@ def _get_tracked_files(directory: str, cwd: str | None = None) -> list[str]:
7073
return output.splitlines()
7174

7275

73-
def _repo_level(n: int) -> None:
76+
def _repo_level(n: int, since: str | None = None) -> None:
7477
"""Show top contributors to the entire repo (fast, shortlog only)."""
7578
args = ["shortlog", "-sne", "HEAD"]
79+
if since:
80+
args.extend(["--since", since])
7681
output = run_git(args)
7782
if not output:
7883
print_info("No contributors found.")
@@ -107,13 +112,13 @@ def _repo_level(n: int) -> None:
107112
)
108113

109114

110-
def _file_level(filepath: str, n: int) -> None:
115+
def _file_level(filepath: str, n: int, since: str | None = None) -> None:
111116
"""Show who knows a specific file best (blame-based)."""
112117
if not os.path.exists(filepath):
113118
print_error(f"File not found: {filepath}")
114119
raise typer.Exit(1)
115120

116-
stats = _blame_file(filepath)
121+
stats = _blame_file(filepath, since=since)
117122
if not stats:
118123
print_info(f"No blame data for {filepath}")
119124
return
@@ -135,7 +140,7 @@ def _file_level(filepath: str, n: int) -> None:
135140
)
136141

137142

138-
def _dir_level(directory: str, n: int, no_limit: bool) -> None:
143+
def _dir_level(directory: str, n: int, since: str | None = None, no_limit: bool = False) -> None:
139144
"""Show who knows a directory best (concurrent blame)."""
140145
files = _get_tracked_files(directory)
141146
if not files:
@@ -162,7 +167,7 @@ def _dir_level(directory: str, n: int, no_limit: bool) -> None:
162167
task = progress.add_task(f"Analyzing {len(files)} files...", total=len(files))
163168

164169
def blame_one(f: str) -> tuple[str, dict[str, dict]]:
165-
return f, _blame_file(f)
170+
return f, _blame_file(f, since=since)
166171

167172
with ThreadPoolExecutor(max_workers=8) as pool:
168173
futures = {pool.submit(blame_one, f): f for f in files}
@@ -200,6 +205,7 @@ def blame_one(f: str) -> tuple[str, dict[str, dict]]:
200205
def who(
201206
path: str = typer.Argument(None, help="File or directory to analyze (omit for repo-level)."),
202207
n: int = typer.Option(DEFAULT_WHO_LIMIT, "-n", "--number", help="Number of contributors to show."),
208+
since: str = typer.Option(None, "--since", help="Only consider contributions after this date."),
203209
no_limit: bool = typer.Option(False, "--no-limit", help="Remove file cap for directory analysis."),
204210
) -> None:
205211
"""Show who knows a file, directory, or repo best."""
@@ -210,8 +216,8 @@ def who(
210216
raise typer.Exit(1)
211217

212218
if path is None:
213-
_repo_level(n)
219+
_repo_level(n, since)
214220
elif os.path.isdir(path):
215-
_dir_level(path, n, no_limit)
221+
_dir_level(path, n, since, no_limit)
216222
else:
217-
_file_level(path, n)
223+
_file_level(path, n, since)

0 commit comments

Comments
 (0)