Skip to content

Commit bf68599

Browse files
authored
chore: use copyLibraryFiles overe copyLibrary (#1941)
CopyLibrary function does not override the existing files. Updating the test about symbolic links. We should support symbolic links in the library repositories. Fixes #1832.
1 parent fb75149 commit bf68599

File tree

4 files changed

+178
-144
lines changed

4 files changed

+178
-144
lines changed

internal/librarian/command.go

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,14 @@ func cleanAndCopyLibrary(state *config.LibrarianState, repoDir, libraryID, outpu
265265
return fmt.Errorf("failed to clean library, %s: %w", library.ID, err)
266266
}
267267

268-
return copyLibrary(repoDir, outputDir, library)
268+
return copyLibraryFiles(state, repoDir, libraryID, outputDir)
269269
}
270270

271+
// copyLibraryFiles copies the files in state.SourceRoots relative to the src folder to the dest
272+
// folder. It overwrites any existing files.
273+
// If there's no files in the library's SourceRoots under the src directory, no copy will happen.
274+
// If a file is being copied to the library's SourceRoots in the dest folder but the folder does
275+
// not exist, the copy fails.
271276
func copyLibraryFiles(state *config.LibrarianState, dest, libraryID, src string) error {
272277
library := findLibraryByID(state, libraryID)
273278
if library == nil {
@@ -322,20 +327,6 @@ func getDirectoryFilenames(dir string) ([]string, error) {
322327
return fileNames, nil
323328
}
324329

325-
// copyLibrary copies library file from src to dst.
326-
func copyLibrary(dst, src string, library *config.LibraryState) error {
327-
slog.Info("Copying library", "id", library.ID, "destination", dst, "source", src)
328-
for _, srcRoot := range library.SourceRoots {
329-
dstPath := filepath.Join(dst, srcRoot)
330-
srcPath := filepath.Join(src, srcRoot)
331-
if err := os.CopyFS(dstPath, os.DirFS(srcPath)); err != nil {
332-
return fmt.Errorf("failed to copy %s to %s: %w", library.ID, dstPath, err)
333-
}
334-
}
335-
336-
return nil
337-
}
338-
339330
// commitAndPush creates a commit and push request to GitHub for the generated changes.
340331
// It uses the GitHub client to create a PR with the specified branch, title, and
341332
// description to the repository.
@@ -424,16 +415,35 @@ func createPRBody(info *commitInfo) (string, error) {
424415
}
425416

426417
func copyFile(dst, src string) (err error) {
427-
sourceFile, err := os.Open(src)
418+
lstat, err := os.Lstat(src)
428419
if err != nil {
429-
return fmt.Errorf("failed to open file: %q: %w", src, err)
420+
return fmt.Errorf("failed to lstat file: %q: %w", src, err)
430421
}
431-
defer sourceFile.Close()
432422

433423
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
434-
return fmt.Errorf("failed to make directory: %s", src)
424+
return fmt.Errorf("failed to make directory for %q: %w", dst, err)
435425
}
436426

427+
if lstat.Mode()&os.ModeSymlink == os.ModeSymlink {
428+
linkTarget, err := os.Readlink(src)
429+
if err != nil {
430+
return fmt.Errorf("failed to read link: %q: %w", src, err)
431+
}
432+
// Remove existing file at dst if it exists. os.Symlink will fail otherwise.
433+
if _, err := os.Lstat(dst); err == nil {
434+
if err := os.Remove(dst); err != nil {
435+
return fmt.Errorf("failed to remove existing file at destination: %q: %w", dst, err)
436+
}
437+
}
438+
return os.Symlink(linkTarget, dst)
439+
}
440+
441+
sourceFile, err := os.Open(src)
442+
if err != nil {
443+
return fmt.Errorf("failed to open file: %q: %w", src, err)
444+
}
445+
defer sourceFile.Close()
446+
437447
destinationFile, err := os.Create(dst)
438448
if err != nil {
439449
return fmt.Errorf("failed to create file: %s", dst)

internal/librarian/command_test.go

Lines changed: 81 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ func TestCleanAndCopyLibrary(t *testing.T) {
321321
errContains: "failed to clean library",
322322
},
323323
{
324-
name: "copy fails on symlink",
324+
name: "copy should not fail on symlink",
325325
libraryID: "some-library",
326326
state: &config.LibrarianState{
327327
Libraries: []*config.LibraryState{
@@ -335,13 +335,11 @@ func TestCleanAndCopyLibrary(t *testing.T) {
335335
},
336336
repo: newTestGitRepo(t),
337337
setup: func(t *testing.T, repoDir, outputDir string) {
338-
// Create a symlink in the output directory to trigger an error.
338+
// Create a symlink in the output directory
339339
if err := os.Symlink("target", filepath.Join(outputDir, "symlink")); err != nil {
340340
t.Fatalf("os.Symlink() = %v", err)
341341
}
342342
},
343-
wantErr: true,
344-
errContains: "failed to copy",
345343
},
346344
{
347345
name: "empty RemoveRegex defaults to source root",
@@ -532,120 +530,6 @@ func TestCleanAndCopyLibrary(t *testing.T) {
532530
}
533531
}
534532

535-
func TestCopyOneLibrary(t *testing.T) {
536-
t.Parallel()
537-
// Create files in src directory.
538-
setup := func(src string, files []string) {
539-
for _, relPath := range files {
540-
fullPath := filepath.Join(src, relPath)
541-
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
542-
t.Error(err)
543-
}
544-
545-
if _, err := os.Create(fullPath); err != nil {
546-
t.Error(err)
547-
}
548-
}
549-
}
550-
for _, test := range []struct {
551-
name string
552-
dst string
553-
src string
554-
library *config.LibraryState
555-
filesToCreate []string
556-
wantFiles []string
557-
skipFiles []string
558-
wantErr bool
559-
wantErrMsg string
560-
}{
561-
{
562-
name: "copied a library",
563-
dst: filepath.Join(t.TempDir(), "dst"),
564-
src: filepath.Join(t.TempDir(), "src"),
565-
library: &config.LibraryState{
566-
ID: "example-library",
567-
SourceRoots: []string{
568-
"a/path",
569-
"another/path",
570-
},
571-
},
572-
filesToCreate: []string{
573-
"a/path/example.txt",
574-
"another/path/example.txt",
575-
"skipped/path/example.txt",
576-
},
577-
wantFiles: []string{
578-
"a/path/example.txt",
579-
"another/path/example.txt",
580-
},
581-
skipFiles: []string{
582-
"skipped/path/example.txt",
583-
},
584-
},
585-
{
586-
name: "invalid src",
587-
dst: os.TempDir(),
588-
src: "/invalid-path",
589-
library: &config.LibraryState{
590-
ID: "example-library",
591-
SourceRoots: []string{
592-
"a-library/path",
593-
},
594-
},
595-
wantErr: true,
596-
wantErrMsg: "failed to copy",
597-
},
598-
{
599-
name: "invalid dst",
600-
dst: "/invalid-path",
601-
src: os.TempDir(),
602-
library: &config.LibraryState{
603-
ID: "example-library",
604-
SourceRoots: []string{
605-
"a-library/path",
606-
},
607-
},
608-
wantErr: true,
609-
wantErrMsg: "failed to copy",
610-
},
611-
} {
612-
t.Run(test.name, func(t *testing.T) {
613-
if !test.wantErr {
614-
setup(test.src, test.filesToCreate)
615-
}
616-
err := copyLibrary(test.dst, test.src, test.library)
617-
if test.wantErr {
618-
if err == nil {
619-
t.Errorf("copyOneLibrary() shoud fail")
620-
}
621-
622-
if !strings.Contains(err.Error(), test.wantErrMsg) {
623-
t.Errorf("want error message: %s, got: %s", test.wantErrMsg, err.Error())
624-
}
625-
626-
return
627-
}
628-
if err != nil {
629-
t.Errorf("failed to run copyOneLibrary(): %s", err.Error())
630-
}
631-
632-
for _, file := range test.wantFiles {
633-
fullPath := filepath.Join(test.dst, file)
634-
if _, err := os.Stat(fullPath); err != nil {
635-
t.Errorf("file %s is not copied to %s", file, test.dst)
636-
}
637-
}
638-
639-
for _, file := range test.skipFiles {
640-
fullPath := filepath.Join(test.dst, file)
641-
if _, err := os.Stat(fullPath); !os.IsNotExist(err) {
642-
t.Errorf("file %s should not be copied to %s", file, test.dst)
643-
}
644-
}
645-
})
646-
}
647-
}
648-
649533
func TestClean(t *testing.T) {
650534
t.Parallel()
651535
for _, test := range []struct {
@@ -1592,11 +1476,34 @@ func TestCopyLibraryFiles(t *testing.T) {
15921476
libraryID string
15931477
state *config.LibrarianState
15941478
filesToCreate []string
1479+
setup func(t *testing.T, outputDir string)
1480+
verify func(t *testing.T, repoDir string)
15951481
wantFiles []string
15961482
skipFiles []string
15971483
wantErr bool
15981484
wantErrMsg string
15991485
}{
1486+
{
1487+
repoDir: "/invalid-dst-path",
1488+
name: "invalid dst",
1489+
outputDir: t.TempDir(),
1490+
libraryID: "example-library",
1491+
state: &config.LibrarianState{
1492+
Libraries: []*config.LibraryState{
1493+
{
1494+
ID: "example-library",
1495+
SourceRoots: []string{
1496+
"a-library/path",
1497+
},
1498+
},
1499+
},
1500+
},
1501+
filesToCreate: []string{
1502+
"a-library/path/example.txt",
1503+
},
1504+
wantErr: true,
1505+
wantErrMsg: "failed to make directory",
1506+
},
16001507
{
16011508
name: "copy library files",
16021509
repoDir: filepath.Join(t.TempDir(), "dst"),
@@ -1626,6 +1533,51 @@ func TestCopyLibraryFiles(t *testing.T) {
16261533
"skipped/path/example.txt",
16271534
},
16281535
},
1536+
{
1537+
name: "copy library files with symbolic link",
1538+
repoDir: filepath.Join(t.TempDir(), "dst"),
1539+
outputDir: filepath.Join(t.TempDir(), "src"),
1540+
libraryID: "example-library",
1541+
state: &config.LibrarianState{
1542+
Libraries: []*config.LibraryState{
1543+
{
1544+
ID: "example-library",
1545+
SourceRoots: []string{
1546+
"a/path",
1547+
},
1548+
},
1549+
},
1550+
},
1551+
filesToCreate: []string{
1552+
"a/path/target.txt",
1553+
},
1554+
setup: func(t *testing.T, outputDir string) {
1555+
if err := os.Symlink("target.txt", filepath.Join(outputDir, "a/path", "link.txt")); err != nil {
1556+
t.Fatalf("failed to create symlink: %v", err)
1557+
}
1558+
},
1559+
wantFiles: []string{
1560+
"a/path/target.txt",
1561+
"a/path/link.txt",
1562+
},
1563+
verify: func(t *testing.T, repoDir string) {
1564+
linkPath := filepath.Join(repoDir, "a/path", "link.txt")
1565+
info, err := os.Lstat(linkPath)
1566+
if err != nil {
1567+
t.Fatalf("failed to lstat symlink: %v", err)
1568+
}
1569+
if info.Mode()&os.ModeSymlink == 0 {
1570+
t.Errorf("copied file is not a symlink")
1571+
}
1572+
target, err := os.Readlink(linkPath)
1573+
if err != nil {
1574+
t.Fatalf("failed to readlink: %v", err)
1575+
}
1576+
if target != "target.txt" {
1577+
t.Errorf("symlink target is incorrect: got %q, want %q", target, "target.txt")
1578+
}
1579+
},
1580+
},
16291581
{
16301582
name: "library not found",
16311583
repoDir: filepath.Join(t.TempDir(), "dst"),
@@ -1670,16 +1622,20 @@ func TestCopyLibraryFiles(t *testing.T) {
16701622
},
16711623
} {
16721624
t.Run(test.name, func(t *testing.T) {
1673-
if !test.wantErr {
1625+
if len(test.filesToCreate) > 0 {
16741626
setup(test.outputDir, test.filesToCreate)
16751627
}
1628+
if test.setup != nil {
1629+
test.setup(t, test.outputDir)
1630+
}
16761631
err := copyLibraryFiles(test.state, test.repoDir, test.libraryID, test.outputDir)
16771632
if test.wantErr {
16781633
if err == nil {
16791634
t.Errorf("copyLibraryFiles() shoud fail")
1635+
return
16801636
}
1681-
1682-
if !strings.Contains(err.Error(), test.wantErrMsg) {
1637+
e := err.Error()
1638+
if !strings.Contains(e, test.wantErrMsg) {
16831639
t.Errorf("want error message: %s, got: %s", test.wantErrMsg, err.Error())
16841640
}
16851641

@@ -1702,6 +1658,9 @@ func TestCopyLibraryFiles(t *testing.T) {
17021658
t.Errorf("file %s should not be copied to %s", file, test.repoDir)
17031659
}
17041660
}
1661+
if test.verify != nil {
1662+
test.verify(t, test.repoDir)
1663+
}
17051664
})
17061665
}
17071666
}
@@ -1720,7 +1679,7 @@ func TestCopyFile(t *testing.T) {
17201679
name: "invalid src",
17211680
src: "/invalid-path/example.txt",
17221681
wantErr: true,
1723-
wantErrMsg: "failed to open file",
1682+
wantErrMsg: "failed to lstat file",
17241683
},
17251684
{
17261685
name: "invalid dst path",

internal/librarian/release_init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func (r *initRunner) runInitCommand(ctx context.Context, outputDir string) error
159159
if err := r.updateLibrary(library); err != nil {
160160
return err
161161
}
162-
if err := copyLibrary(dst, src, library); err != nil {
162+
if err := copyLibraryFiles(r.state, dst, library.ID, src); err != nil {
163163
return err
164164
}
165165

@@ -170,7 +170,7 @@ func (r *initRunner) runInitCommand(ctx context.Context, outputDir string) error
170170
if err := r.updateLibrary(library); err != nil {
171171
return err
172172
}
173-
if err := copyLibrary(dst, src, library); err != nil {
173+
if err := copyLibraryFiles(r.state, dst, library.ID, src); err != nil {
174174
return err
175175
}
176176
}

0 commit comments

Comments
 (0)