Skip to content

Commit 120922e

Browse files
doriableemcfarlane
andauthored
Add --all flag for buf export (#3886)
Co-authored-by: Edward McFarlane <[email protected]>
1 parent 3f756d2 commit 120922e

File tree

16 files changed

+254
-1
lines changed

16 files changed

+254
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## [Unreleased]
44

5-
- No changes yet.
5+
- Add `buf export --all` flag to include non-proto source files.
66

77
## [v1.55.1] - 2025-06-17
88

private/buf/cmd/buf/buf_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2819,6 +2819,122 @@ func TestExportProtoFileRefWithPathFlag(t *testing.T) {
28192819
)
28202820
}
28212821

2822+
func TestExportAllSourceFilesV1Module(t *testing.T) {
2823+
t.Parallel()
2824+
tempDir := t.TempDir()
2825+
testRunStdout(
2826+
t,
2827+
nil,
2828+
0,
2829+
``,
2830+
"export",
2831+
"--all",
2832+
"-o",
2833+
tempDir,
2834+
filepath.Join("testdata", "export", "proto"),
2835+
)
2836+
readWriteBucket, err := storageos.NewProvider().NewReadWriteBucket(tempDir)
2837+
require.NoError(t, err)
2838+
storagetesting.AssertPaths(
2839+
t,
2840+
readWriteBucket,
2841+
"",
2842+
"LICENSE",
2843+
"README.md",
2844+
"request.proto",
2845+
"rpc.proto",
2846+
)
2847+
}
2848+
2849+
func TestExportAllSourceFilesV1Workspace(t *testing.T) {
2850+
t.Parallel()
2851+
tempDir := t.TempDir()
2852+
testRunStdout(
2853+
t,
2854+
nil,
2855+
0,
2856+
``,
2857+
"export",
2858+
"--all",
2859+
"-o",
2860+
tempDir,
2861+
filepath.Join("testdata", "export"),
2862+
)
2863+
readWriteBucket, err := storageos.NewProvider().NewReadWriteBucket(tempDir)
2864+
require.NoError(t, err)
2865+
storagetesting.AssertPaths(
2866+
t,
2867+
readWriteBucket,
2868+
"",
2869+
"LICENSE.request",
2870+
"LICENSE.rpc",
2871+
"README.another.md",
2872+
"README.rpc.md",
2873+
"another.proto",
2874+
"request.proto",
2875+
"rpc.proto",
2876+
"unimported.proto",
2877+
)
2878+
}
2879+
2880+
func TestExportAllSourceFilesV2Module(t *testing.T) {
2881+
t.Parallel()
2882+
tempDir := t.TempDir()
2883+
testRunStdout(
2884+
t,
2885+
nil,
2886+
0,
2887+
``,
2888+
"export",
2889+
"--all",
2890+
"-o",
2891+
tempDir,
2892+
filepath.Join("testdata", "workspace", "success", "v2", "export", "proto"),
2893+
)
2894+
readWriteBucket, err := storageos.NewProvider().NewReadWriteBucket(tempDir)
2895+
require.NoError(t, err)
2896+
storagetesting.AssertPaths(
2897+
t,
2898+
readWriteBucket,
2899+
"",
2900+
"LICENSE",
2901+
"README.md",
2902+
"request.proto",
2903+
"rpc.proto",
2904+
)
2905+
}
2906+
2907+
func TestExportAllSourceFilesV2Workspace(t *testing.T) {
2908+
t.Parallel()
2909+
tempDir := t.TempDir()
2910+
testRunStdout(
2911+
t,
2912+
nil,
2913+
0,
2914+
``,
2915+
"export",
2916+
"--all",
2917+
"-o",
2918+
tempDir,
2919+
filepath.Join("testdata", "workspace", "success", "v2", "export"),
2920+
)
2921+
readWriteBucket, err := storageos.NewProvider().NewReadWriteBucket(tempDir)
2922+
require.NoError(t, err)
2923+
storagetesting.AssertPaths(
2924+
t,
2925+
readWriteBucket,
2926+
"",
2927+
"LICENSE.request",
2928+
"LICENSE.rpc",
2929+
"README.another.md",
2930+
"README.rpc.md",
2931+
"another.proto",
2932+
"request.proto",
2933+
"rpc.proto",
2934+
"unimported.proto",
2935+
)
2936+
}
2937+
28222938
func TestBuildWithPaths(t *testing.T) {
28232939
t.Parallel()
28242940
testRunStdout(t, nil, 0, ``, "build", filepath.Join("testdata", "paths"), "--path", filepath.Join("testdata", "paths", "a", "v3"), "--exclude-path", filepath.Join("testdata", "paths", "a", "v3", "foo"))

private/buf/cmd/buf/command/export/export.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ package export
1717
import (
1818
"context"
1919
"errors"
20+
"fmt"
2021
"io/fs"
2122
"os"
23+
"strings"
2224

2325
"buf.build/go/app/appcmd"
2426
"buf.build/go/app/appext"
2527
"github.com/bufbuild/buf/private/buf/bufcli"
2628
"github.com/bufbuild/buf/private/buf/bufctl"
2729
"github.com/bufbuild/buf/private/bufpkg/bufmodule"
2830
"github.com/bufbuild/buf/private/gen/data/datawkt"
31+
"github.com/bufbuild/buf/private/pkg/normalpath"
2932
"github.com/bufbuild/buf/private/pkg/storage"
3033
"github.com/bufbuild/buf/private/pkg/storage/storageos"
3134
"github.com/bufbuild/buf/private/pkg/syserror"
@@ -40,6 +43,7 @@ const (
4043
configFlagName = "config"
4144
excludePathsFlagName = "exclude-path"
4245
disableSymlinksFlagName = "disable-symlinks"
46+
allFlagName = "all"
4347
)
4448

4549
// NewCommand returns a new Command.
@@ -92,6 +96,7 @@ type flags struct {
9296
Config string
9397
ExcludePaths []string
9498
DisableSymlinks bool
99+
All bool
95100

96101
// special
97102
InputHashtag string
@@ -121,6 +126,12 @@ func (f *flags) Bind(flagSet *pflag.FlagSet) {
121126
"",
122127
`The buf.yaml file or data to use for configuration`,
123128
)
129+
flagSet.BoolVar(
130+
&f.All,
131+
allFlagName,
132+
false,
133+
`When set, include any available documentation and license files for the exported input. If the input has more than one module, then the documentation and license file names will be suffixed with the module name.`,
134+
)
124135
}
125136

126137
func run(
@@ -165,6 +176,55 @@ func run(
165176
return err
166177
}
167178

179+
// If the --all flag is set, then we need to pull the non-proto source files, documentation
180+
// and license files, for the input, if available.
181+
// We only add non-proto source files for target module(s).
182+
//
183+
// If the input has more than one target module (e.g. a workspace Git input), then we set
184+
// an identifier on the file path.
185+
// See [getNonProtoFilePath] docs for details on how that is set.
186+
if flags.All {
187+
seenModuleNamesForDocs := map[string]int{}
188+
seenModuleNamesForLicense := map[string]int{}
189+
targetModules := bufmodule.ModuleSetTargetModules(workspace)
190+
for _, module := range targetModules {
191+
docFile, err := bufmodule.GetDocFile(ctx, module)
192+
// If the file is not found, then we ignore it.
193+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
194+
return err
195+
}
196+
if docFile != nil {
197+
docFilePath := docFile.Path()
198+
if len(targetModules) > 1 {
199+
docFilePath = getNonProtoFilePath(docFilePath, module, seenModuleNamesForDocs)
200+
}
201+
if err := storage.CopyReader(ctx, readWriteBucket, docFile, docFilePath); err != nil {
202+
return errors.Join(err, docFile.Close())
203+
}
204+
if err := docFile.Close(); err != nil {
205+
return err
206+
}
207+
}
208+
licenseFile, err := bufmodule.GetLicenseFile(ctx, module)
209+
// If the file is not found, then we ignore it.
210+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
211+
return err
212+
}
213+
if licenseFile != nil {
214+
licenseFilePath := licenseFile.Path()
215+
if len(targetModules) > 1 {
216+
licenseFilePath = getNonProtoFilePath(licenseFilePath, module, seenModuleNamesForLicense)
217+
}
218+
if err := storage.CopyReader(ctx, readWriteBucket, licenseFile, licenseFilePath); err != nil {
219+
return errors.Join(err, licenseFile.Close())
220+
}
221+
if err := licenseFile.Close(); err != nil {
222+
return err
223+
}
224+
}
225+
}
226+
}
227+
168228
// In the case where we are excluding imports, we are allowing users to specify an input
169229
// that may not have resolved imports (https://github.com/bufbuild/buf/issues/3002).
170230
// Thus we do not need to build the image, and instead we can return the non-import files
@@ -226,3 +286,37 @@ func run(
226286
}
227287
return nil
228288
}
289+
290+
// This is a helper function that returns the path non-proto source files should be written
291+
// to if the --all flag has been set.
292+
//
293+
// This sets an identifier for the module using the module name, [bufparse.FullName.Name()]
294+
// if available, and if not, we use [module.OpaqueID()].
295+
//
296+
// e.g. README.foo.md, README.bar.md
297+
//
298+
// If a module name is repeated, e.g. acme/foo and bufbuild/foo both have the module name
299+
// "foo", then we use an incrementing integer based on the order they are seen in the workspace.
300+
//
301+
// e.g. README.foo.md, README.foo.2.md
302+
func getNonProtoFilePath(
303+
path string,
304+
module bufmodule.Module,
305+
seenModuleNamesForPath map[string]int,
306+
) string {
307+
moduleIdentifier := module.OpaqueID()
308+
if module.FullName() != nil {
309+
moduleIdentifier = module.FullName().Name()
310+
seenModuleNamesForPath[module.FullName().Name()]++
311+
count := seenModuleNamesForPath[module.FullName().Name()]
312+
if count > 1 {
313+
moduleIdentifier = fmt.Sprintf("%s.%d", module.FullName().Name(), count)
314+
}
315+
}
316+
return fmt.Sprintf(
317+
"%s.%s%s",
318+
strings.TrimSuffix(path, normalpath.Ext(path)),
319+
moduleIdentifier,
320+
normalpath.Ext(path),
321+
)
322+
}

private/buf/cmd/buf/testdata/export/another/proto/README.md

Whitespace-only changes.

private/buf/cmd/buf/testdata/export/other/proto/LICENSE

Whitespace-only changes.

private/buf/cmd/buf/testdata/export/proto/LICENSE

Whitespace-only changes.

private/buf/cmd/buf/testdata/export/proto/README.md

Whitespace-only changes.

private/buf/cmd/buf/testdata/workspace/success/v2/export/another/proto/README.md

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
syntax = "proto3";
2+
3+
package another;
4+
5+
message Request {
6+
string name = 1;
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: v2
2+
modules:
3+
- path: another/proto
4+
name: bufbuild.test/workspace/another
5+
- path: other/proto
6+
name: bufbuild.test/workspace/request
7+
- path: proto
8+
name: bufbuild.test/workspace/rpc

0 commit comments

Comments
 (0)