Skip to content

Commit 733e35c

Browse files
babakksCopilot
andauthored
feat: add repo read-file and repo read-dir (cli#13580)
Signed-off-by: Babak K. Shandiz <babakks@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a6012ac commit 733e35c

12 files changed

Lines changed: 2535 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# List directory contents of a repository without cloning.
2+
3+
# Use gh as a credential helper
4+
exec gh auth setup-git
5+
6+
# Create a private repo with a default branch
7+
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --private --add-readme
8+
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
9+
10+
# Clone the repo and add a docs dir plus an executable script
11+
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
12+
cd $SCRIPT_NAME-$RANDOM_STRING
13+
mkdir docs
14+
mkdir script
15+
cp $WORK/guide.md docs/guide.md
16+
cp $WORK/run.sh script/run.sh
17+
chmod 755 script/run.sh
18+
exec git add -A
19+
exec git commit -m 'Add docs and script'
20+
exec git push origin main
21+
22+
# Add a v2 branch with an extra file in docs, to exercise --ref
23+
exec git checkout -b v2
24+
cp $WORK/extra.md docs/extra.md
25+
exec git add -A
26+
exec git commit -m 'Add docs/extra.md on v2'
27+
exec git push -u origin v2
28+
exec git checkout main
29+
30+
# List the repository root. Non-TTY output is tab-separated as type, name,
31+
# octal mode, and byte size. Directories report mode 040000 and size 0; the
32+
# auto-generated README has a non-deterministic size, so match it as digits.
33+
exec gh repo read-dir
34+
stdout '^file\tREADME\.md\t100644\t\d+$'
35+
stdout '^dir\tdocs\t040000\t0$'
36+
stdout '^dir\tscript\t040000\t0$'
37+
38+
# List a subdirectory
39+
exec gh repo read-dir docs
40+
stdout '^file\tguide\.md\t100644\t19$'
41+
! stdout 'extra\.md'
42+
43+
# Executable files are reported as a file with octal mode 100755
44+
exec gh repo read-dir script --json name,type,modeOctal --jq '.entries[] | .name + " " + .type + " " + .modeOctal'
45+
stdout '^run\.sh file 100755$'
46+
47+
# JSON output lists directory entries
48+
exec gh repo read-dir --json name,type --jq '.entries[] | select(.type=="dir") | .name'
49+
stdout '^docs$'
50+
stdout '^script$'
51+
52+
# A ref can select a different tree
53+
exec gh repo read-dir docs --ref v2
54+
stdout 'guide\.md'
55+
stdout 'extra\.md'
56+
57+
# Error: the path points to a file, not a directory
58+
! exec gh repo read-dir README.md
59+
stderr 'is a file, not a directory'
60+
61+
# Error: the path does not exist
62+
! exec gh repo read-dir does-not-exist
63+
stderr 'could not find'
64+
65+
-- guide.md --
66+
This is the guide.
67+
-- extra.md --
68+
Extra file on v2.
69+
-- run.sh --
70+
#!/bin/bash
71+
echo "hello"
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Read files from a repository without cloning, in several modes.
2+
3+
# Use gh as a credential helper
4+
exec gh auth setup-git
5+
6+
# Create a private repo with a default branch
7+
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --private --add-readme
8+
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
9+
10+
# Clone the repo and add a text file under a subdirectory
11+
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
12+
cd $SCRIPT_NAME-$RANDOM_STRING
13+
mkdir docs
14+
cp $WORK/guide.md docs/guide.md
15+
exec git add -A
16+
exec git commit -m 'Add docs/guide.md'
17+
exec git push origin main
18+
19+
# Add a v2 branch where the same file has different content, to exercise --ref
20+
exec git checkout -b v2
21+
cp $WORK/guide-v2.md docs/guide.md
22+
exec git add -A
23+
exec git commit -m 'Update docs/guide.md on v2'
24+
exec git push -u origin v2
25+
exec git checkout main
26+
27+
# Create a small binary file (PNG signature) via the Contents API
28+
exec gh api -X PUT repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING/contents/assets/logo.png -f message='Add binary' -f content=iVBORw0KGgo=
29+
30+
# Create a text file containing ANSI terminal escape sequences via the Contents API
31+
exec gh api -X PUT repos/$ORG/$SCRIPT_NAME-$RANDOM_STRING/contents/ansi.txt -f message='Add ansi' -f content=G1szMW1oZWxsbxtbMG0K
32+
33+
# Read a file from the default branch: raw content goes to stdout
34+
exec gh repo read-file docs/guide.md
35+
cmp stdout $WORK/guide.md
36+
37+
# Read the same file at a specific ref
38+
exec gh repo read-file docs/guide.md --ref v2
39+
cmp stdout $WORK/guide-v2.md
40+
41+
# Save a file to disk, and confirm --clobber is required to overwrite
42+
exec gh repo read-file docs/guide.md --output out.txt
43+
cmp out.txt $WORK/guide.md
44+
! exec gh repo read-file docs/guide.md --output out.txt
45+
stderr 'already exists'
46+
exec gh repo read-file docs/guide.md --output out.txt --clobber
47+
cmp out.txt $WORK/guide.md
48+
49+
# Save into a directory: a trailing separator writes under the remote basename
50+
exec gh repo read-file docs/guide.md --output download-dir/
51+
cmp download-dir/guide.md $WORK/guide.md
52+
53+
# Save to an explicit path inside a directory, creating it as needed
54+
exec gh repo read-file docs/guide.md --output download-dir/out.txt
55+
cmp download-dir/out.txt $WORK/guide.md
56+
57+
# JSON output exposes file metadata
58+
exec gh repo read-file docs/guide.md --json name,path,size,type --jq '.name'
59+
stdout '^guide\.md$'
60+
61+
# Read a binary file: the raw bytes written to stdout match the saved copy,
62+
# and the base64 content is exposed via --json
63+
exec gh repo read-file assets/logo.png --output downloaded.png
64+
exists downloaded.png
65+
exec gh repo read-file assets/logo.png
66+
cmp stdout downloaded.png
67+
exec gh repo read-file assets/logo.png --json size,type,content --jq '.content'
68+
stdout '^iVBORw0KGgo=$'
69+
70+
# A file with terminal escape sequences is refused by default, but readable
71+
# with --allow-escape-sequences
72+
! exec gh repo read-file ansi.txt
73+
stderr 'terminal escape sequences'
74+
exec gh repo read-file ansi.txt --allow-escape-sequences
75+
stdout 'hello'
76+
77+
# Error: the path is a directory
78+
! exec gh repo read-file docs
79+
stderr 'is a directory'
80+
81+
# Error: the path does not exist
82+
! exec gh repo read-file does/not/exist.md
83+
stderr 'Not Found'
84+
85+
-- guide.md --
86+
Hello from the default branch!
87+
-- guide-v2.md --
88+
Hello from the v2 branch!

internal/text/text.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,26 @@ func FormatSlice(values []string, lineLength uint, indent uint, prependWith stri
149149
}
150150
return builder.String()
151151
}
152+
153+
// FormatSize formats a byte count using binary units (B, KB, MB, GB, TB, PB).
154+
// Values below a kilobyte are shown as whole bytes; larger values are shown with
155+
// one decimal place of precision.
156+
func FormatSize(n int64) string {
157+
const unit = 1024
158+
if n < unit {
159+
return fmt.Sprintf("%d B", n)
160+
}
161+
162+
units := []string{"KB", "MB", "GB", "TB", "PB"}
163+
164+
// Stop at the largest known unit so an out-of-range index can never occur,
165+
// even for byte counts beyond a petabyte.
166+
div, exp := int64(unit), 0
167+
for v := n / unit; v >= unit && exp < len(units)-1; v /= unit {
168+
div *= unit
169+
exp++
170+
}
171+
172+
value := float64(n) / float64(div)
173+
return fmt.Sprintf("%.1f %s", value, units[exp])
174+
}

internal/text/text_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package text
22

33
import (
4+
"math"
45
"testing"
56
"time"
67

@@ -181,3 +182,33 @@ func TestDisplayURL(t *testing.T) {
181182
})
182183
}
183184
}
185+
186+
func TestFormatSize(t *testing.T) {
187+
tests := []struct {
188+
n int64
189+
want string
190+
}{
191+
{0, "0 B"},
192+
{1, "1 B"},
193+
{512, "512 B"},
194+
{1023, "1023 B"},
195+
{1024, "1.0 KB"},
196+
{1536, "1.5 KB"},
197+
{2048, "2.0 KB"},
198+
{10240, "10.0 KB"},
199+
{524288, "512.0 KB"},
200+
{1048576, "1.0 MB"},
201+
{1572864, "1.5 MB"},
202+
{5242880, "5.0 MB"},
203+
{1073741824, "1.0 GB"},
204+
{1610612736, "1.5 GB"},
205+
{1099511627776, "1.0 TB"},
206+
{1125899906842624, "1.0 PB"},
207+
{1152921504606846976, "1024.0 PB"}, // 1 EB clamps to the largest known unit
208+
{math.MaxInt64, "8192.0 PB"}, // maximum int never indexes past PB
209+
}
210+
211+
for _, tt := range tests {
212+
assert.Equal(t, tt.want, FormatSize(tt.n))
213+
}
214+
}

0 commit comments

Comments
 (0)