Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 52 additions & 13 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"time"
)

const VERSION = "6.2.0"
const VERSION = "6.2.1"

// HistoryLimit is the maximum number of commits to review; we'll exit if we
// find all files before it; if a file was more than this many commits ago we
Expand All @@ -41,6 +41,7 @@ type File struct {
hash string // full hash
shortHash string // Git's abbreviated hash (length varies by repo size)
lastModified string
commitTime int64 // unix timestamp of the commit for precise sorting
message string
isDir bool
isExe bool
Expand Down Expand Up @@ -321,6 +322,11 @@ OPTIONS
filters out unmodified tracked files and shows only files with
status indicators (modified, added, deleted, renamed, etc.)

-s, --sort
Sort files by last git modification date, most recent first.
Files without a date (ignored, untracked, .git) are placed at
the bottom in alphabetical order.

--debug
Print debug timing information to stderr, including which file
took the longest to find in git history and warnings for files
Expand All @@ -340,6 +346,7 @@ func run() int {
monoHash := false
nerdFont := false
changedOnly := false
sortByDate := false
debug := false
var formatColumns []Column
for len(argv) > 0 {
Expand All @@ -360,6 +367,9 @@ func run() int {
} else if argv[0] == "--changed-only" || argv[0] == "-c" {
changedOnly = true
argv = argv[1:]
} else if argv[0] == "--sort" || argv[0] == "-s" {
sortByDate = true
argv = argv[1:]
} else if strings.HasPrefix(argv[0], "--diffWidth") {
if len(argv) == 1 {
if strings.Contains(argv[0], "=") {
Expand Down Expand Up @@ -523,10 +533,12 @@ func run() int {
}
}

// Sort files by name
slices.SortFunc(files, func(a, b *File) int {
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
})
// Sort files
if sortByDate {
sortFilesByDate(files)
} else {
sortFilesByName(files)
}

// Use default columns if not specified
if len(formatColumns) == 0 {
Expand Down Expand Up @@ -728,6 +740,33 @@ func makeDiffGraph(file *File, width int) string {
RESET)
}

// sortFilesByDate sorts files by commit timestamp descending (most recent first).
// Files without a timestamp (commitTime == 0) go to the bottom, sorted alphabetically.
func sortFilesByDate(files []*File) {
slices.SortStableFunc(files, func(a, b *File) int {
if a.commitTime == 0 && b.commitTime == 0 {
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
}
if a.commitTime == 0 {
return 1
}
if b.commitTime == 0 {
return -1
}
if b.commitTime != a.commitTime {
return int(b.commitTime - a.commitTime)
}
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
})
}

// sortFilesByName sorts files alphabetically by name (case-insensitive).
func sortFilesByName(files []*File) {
slices.SortFunc(files, func(a, b *File) int {
return strings.Compare(strings.ToLower(a.Name()), strings.ToLower(b.Name()))
})
}

func showColumns(out io.Writer, maxWidth int, files []*File, rctx *RenderContext, columns []Column) {
// Calculate max widths for each column
colWidths := calculateColumnWidths(files, columns, rctx)
Expand Down Expand Up @@ -1076,8 +1115,7 @@ func parseGitLog(files []*File, timer *DebugTimer) error {
cmd := exec.Command("git", "-c", "core.fsmonitor=false", "log",
"--name-only",
"--relative",
"--date=format:%Y-%m-%d",
"--format=%H%x00%h%x00%ad%x00%aN%x00%aE%x00%s%x00",
"--format=%H%x00%h%x00%aN%x00%aE%x00%s%x00%at%x00",
"-n", HistoryLimit,
"HEAD", "--", ".")

Expand All @@ -1094,10 +1132,10 @@ func parseGitLog(files []*File, timer *DebugTimer) error {
var currentCommit struct {
hash string
shortHash string
date string
author string
authorEmail string
message string
timestamp int64
}

// Track lines since last find for early exit. This counts lines of output
Expand All @@ -1121,10 +1159,10 @@ func parseGitLog(files []*File, timer *DebugTimer) error {
if len(parts) >= 6 {
currentCommit.hash = parts[0]
currentCommit.shortHash = parts[1]
currentCommit.date = parts[2]
currentCommit.author = parts[3]
currentCommit.authorEmail = parts[4]
currentCommit.message = parts[5]
currentCommit.author = parts[2]
currentCommit.authorEmail = parts[3]
currentCommit.message = parts[4]
currentCommit.timestamp, _ = strconv.ParseInt(parts[5], 10, 64)
}
} else if currentCommit.hash != "" {
// This is a filename line
Expand All @@ -1151,7 +1189,8 @@ func parseGitLog(files []*File, timer *DebugTimer) error {
// Found it! Populate the file info
file.hash = currentCommit.hash
file.shortHash = currentCommit.shortHash
file.lastModified = currentCommit.date
file.commitTime = currentCommit.timestamp
file.lastModified = time.Unix(currentCommit.timestamp, 0).UTC().Format("2006-01-02")
file.author = currentCommit.author
file.authorEmail = currentCommit.authorEmail
file.message = currentCommit.message
Expand Down
77 changes: 77 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1405,3 +1405,80 @@ func TestSubdirectoryTrackedFiles(t *testing.T) {
t.Errorf("node_modules should be marked as ignored (I), got %q", status)
}
}

func TestSortFilesByDate(t *testing.T) {
files := []*File{
{name: "old.go", commitTime: 1000},
{name: "new.go", commitTime: 3000},
{name: "mid.go", commitTime: 2000},
{name: "ignored.go", status: "I", commitTime: 0},
{name: "untracked.go", status: "??", commitTime: 0},
{name: ".git", status: "*", commitTime: 0},
}

sortFilesByDate(files)

// Files with timestamps should be sorted descending
expectedOrder := []string{"new.go", "mid.go", "old.go", ".git", "ignored.go", "untracked.go"}
for i, expected := range expectedOrder {
if files[i].Name() != expected {
t.Errorf("position %d: got %q, want %q", i, files[i].Name(), expected)
}
}
}

func TestSortFilesByDateSameTimestamp(t *testing.T) {
// When timestamps are equal, files should be sorted alphabetically
files := []*File{
{name: "zebra.go", commitTime: 1000},
{name: "alpha.go", commitTime: 1000},
{name: "middle.go", commitTime: 1000},
}

sortFilesByDate(files)

expectedOrder := []string{"alpha.go", "middle.go", "zebra.go"}
for i, expected := range expectedOrder {
if files[i].Name() != expected {
t.Errorf("position %d: got %q, want %q", i, files[i].Name(), expected)
}
}
}

func TestSortFilesByDateUndatedAtBottom(t *testing.T) {
// All undated files should appear after all dated files
files := []*File{
{name: "untracked.go", status: "??", commitTime: 0},
{name: "dated.go", commitTime: 500},
{name: "ignored.go", status: "I", commitTime: 0},
{name: "also-dated.go", commitTime: 100},
}

sortFilesByDate(files)

// Dated files first (descending), then undated (alphabetical)
expectedOrder := []string{"dated.go", "also-dated.go", "ignored.go", "untracked.go"}
for i, expected := range expectedOrder {
if files[i].Name() != expected {
t.Errorf("position %d: got %q, want %q", i, files[i].Name(), expected)
}
}
}

func TestSortFilesByName(t *testing.T) {
files := []*File{
{name: "Zebra.go"},
{name: "alpha.go"},
{name: "Beta.go"},
}

sortFilesByName(files)

// Case-insensitive alphabetical
expectedOrder := []string{"alpha.go", "Beta.go", "Zebra.go"}
for i, expected := range expectedOrder {
if files[i].Name() != expected {
t.Errorf("position %d: got %q, want %q", i, files[i].Name(), expected)
}
}
}