diff --git a/core/commands/ls.go b/core/commands/ls.go index 1decd5d02..90251e00b 100644 --- a/core/commands/ls.go +++ b/core/commands/ls.go @@ -58,21 +58,23 @@ var LsCmd = &cmds.Command{ Displays the contents of an IPFS or IPNS object(s) at the given path, with the following format: - + -With the --long (-l) option, also display file mode (permissions) and -modification time for files that were added with --preserve-mode and ---preserve-mtime flags. Format similar to Unix 'ls -l': +With the --long (-l) option, display optional file mode (permissions) and +modification time in a format similar to Unix 'ls -l': - + -Example output with --long: +Mode and mtime are optional UnixFS metadata. They are only present if the +content was imported with 'ipfs add --preserve-mode' and '--preserve-mtime'. +Without preserved metadata, mode shows '----------' and mtime shows '-'. +Times are displayed in UTC. + +Example output with --long (content imported with preserved metadata): -rw-r--r-- QmZULkCELmmk5XNf... 1234 Jan 15 10:30 document.txt -rwxr-xr-x QmaRGe7bVmVaLmxb... 5678 Dec 01 2023 script.sh drwxr-xr-x QmWWEQhcLufF3qPm... - Nov 20 2023 subdir/ - -Files without preserved metadata show '----------' for mode and '-' for mtime. The JSON output contains type information. `, }, @@ -230,7 +232,16 @@ The JSON output contains type information. Type: LsOutput{}, } -// formatMode formats os.FileMode to Unix-style permission string (e.g., "-rw-r--r--"). +// formatMode converts os.FileMode to a 10-character Unix ls-style string. +// +// Format: [type][owner rwx][group rwx][other rwx] +// +// Type indicators: - (regular), d (directory), l (symlink), p (named pipe), +// s (socket), c (char device), b (block device). +// +// Special bits replace the execute position: setuid on owner (s/S), +// setgid on group (s/S), sticky on other (t/T). Lowercase when the +// underlying execute bit is also set, uppercase when not. func formatMode(mode os.FileMode) string { var buf [10]byte @@ -300,15 +311,20 @@ func formatMode(mode os.FileMode) string { } // permBit returns the permission character if the bit is set. -func permBit(mode os.FileMode, bit os.FileMode, char rune) byte { +func permBit(mode os.FileMode, bit os.FileMode, char byte) byte { if mode&bit != 0 { - return byte(char) + return char } return '-' } -// formatModTime formats time.Time for display. -// Returns empty string if time is zero. +// formatModTime formats time.Time for display, following Unix ls conventions. +// +// Returns "-" for zero time. Otherwise returns a 12-character string: +// recent files (within 6 months) show "Jan 02 15:04", +// older or future files show "Jan 02 2006". +// +// The output uses the timezone embedded in t (UTC for IPFS metadata). func formatModTime(t time.Time) string { if t.IsZero() { return "-" diff --git a/core/commands/ls_test.go b/core/commands/ls_test.go index 28895891c..243d72e5d 100644 --- a/core/commands/ls_test.go +++ b/core/commands/ls_test.go @@ -181,9 +181,8 @@ func TestFormatModTime(t *testing.T) { oldResult := formatModTime(oldTime) assert.Len(t, oldResult, 12, "old time format should be 12 chars") - // Recent time (use a fixed time to avoid flakiness) - // We test the format length by checking a definitely-recent time - recentTime := time.Now().Add(-1 * time.Hour) + // Recent time: use 1 month ago to ensure it's always within the 6-month window + recentTime := time.Now().AddDate(0, -1, 0) recentResult := formatModTime(recentTime) assert.Len(t, recentResult, 12, "recent time format should be 12 chars") }) diff --git a/test/cli/ls_test.go b/test/cli/ls_test.go index c08d850dc..afa529a54 100644 --- a/test/cli/ls_test.go +++ b/test/cli/ls_test.go @@ -57,11 +57,6 @@ func TestLsLongFormat(t *testing.T) { assert.Contains(t, lines[1], "-rw-r--r--", "readable file should have 644 permissions") assert.Contains(t, lines[1], "Jun 15 2020", "should show mtime with year format") assert.Contains(t, lines[1], "readable.txt", "should show filename") - - // Explicit full output check to catch any formatting regressions - expectedOutput := `-rwxr-xr-x QmNmu36VhUL46L1ebS7pjGNBhBWNhL6kERHiswK6fg2HJz 17 Jun 15 2020 executable.sh --rw-r--r-- QmUHndqmVyXbCS4FkEAQRZ4FeoBZMn8NBsy86bAVR8JhYQ 5 Jun 15 2020 readable.txt` - assert.Equal(t, expectedOutput, strings.TrimSpace(output), "output format must remain stable") }) t.Run("long format shows dash for files without preserved metadata", func(t *testing.T) {