Skip to content

Commit b47f848

Browse files
[RA-3066] Add XCArchive and IPA artifact meta parsing logic (#241)
* Add XCArchive and IPA artifact meta parsing logic * Remove vendor folder and update mockery mock * Create named error for MacOS project not supported error * Add missing godoc * Fix nil pointer issue * Fix review items * Revert formatting * Revert formatting vol.2.
1 parent f876b48 commit b47f848

File tree

6 files changed

+288
-22
lines changed

6 files changed

+288
-22
lines changed

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ require (
66
github.com/bitrise-io/go-steputils v1.0.5
77
github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.18
88
github.com/bitrise-io/go-utils v1.0.12
9-
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.19
9+
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.23
1010
github.com/bitrise-io/go-xcode v1.0.18
1111
github.com/golang-jwt/jwt/v4 v4.5.0
1212
github.com/google/go-querystring v1.1.0
13-
github.com/hashicorp/go-retryablehttp v0.7.4
13+
github.com/hashicorp/go-retryablehttp v0.7.7
1414
github.com/hashicorp/go-version v1.6.0
1515
github.com/ryanuber/go-glob v1.0.0
16-
github.com/stretchr/testify v1.8.4
16+
github.com/stretchr/testify v1.9.0
1717
golang.org/x/text v0.12.0
1818
howett.net/plist v1.0.0
1919
)
@@ -26,9 +26,9 @@ require (
2626
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
2727
github.com/pkg/errors v0.9.1 // indirect
2828
github.com/pmezard/go-difflib v1.0.0 // indirect
29-
github.com/stretchr/objx v0.5.1 // indirect
29+
github.com/stretchr/objx v0.5.2 // indirect
3030
golang.org/x/crypto v0.12.0 // indirect
31-
golang.org/x/sys v0.11.0 // indirect
31+
golang.org/x/sys v0.22.0 // indirect
3232
golang.org/x/term v0.11.0 // indirect
3333
gopkg.in/yaml.v3 v3.0.1 // indirect
3434
)

go.sum

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.18/go.mod h1:/ueNOKnsjcUrlt8C
99
github.com/bitrise-io/go-utils v1.0.1/go.mod h1:ZY1DI+fEpZuFpO9szgDeICM4QbqoWVt0RSY3tRI1heY=
1010
github.com/bitrise-io/go-utils v1.0.12 h1:iJV1ZpyvSA0NCte/N6x+aIQ9TrNr5sIBlcJBf0dn1dE=
1111
github.com/bitrise-io/go-utils v1.0.12/go.mod h1:ZY1DI+fEpZuFpO9szgDeICM4QbqoWVt0RSY3tRI1heY=
12-
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.19 h1:55as5Iv0N4btuRP3YwRzN+BCMtKO210MnJ8mpxmeI7o=
13-
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.19/go.mod h1:Laih4ji980SQkRgdnMCH0g4u2GZI/5nnbqmYT9UfKFQ=
12+
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.23 h1:Dfh4nyZPuEtilBisidejqxBrkx9cWvbOUrpq8VEION0=
13+
github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.23/go.mod h1:3XUplo0dOWc3DqT2XA2SeHToDSg7+j1y1HTHibT2H68=
1414
github.com/bitrise-io/go-xcode v1.0.18 h1:guFywV/AwcZuexqIQkL1ixc3QThpbJvA4voa9MqvPto=
1515
github.com/bitrise-io/go-xcode v1.0.18/go.mod h1:9OwsvrhZ4A2JxHVoEY7CPcABAKA+OE7FQqFfBfvbFuY=
1616
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1717
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1818
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19+
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
1920
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
2021
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
2122
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
@@ -27,11 +28,11 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
2728
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
2829
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
2930
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
30-
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
3131
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
32+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
3233
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
33-
github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
34-
github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
34+
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
35+
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
3536
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
3637
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
3738
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -40,6 +41,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
4041
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
4142
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
4243
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
44+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
45+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
4346
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
4447
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
4548
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -48,18 +51,13 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB
4851
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
4952
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
5053
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
51-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
52-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
53-
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
54-
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
54+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
55+
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
5556
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
5657
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
5758
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
58-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
59-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
60-
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
61-
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
62-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
59+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
60+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
6361
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
6462
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
6563
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
@@ -68,8 +66,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
6866
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
6967
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7068
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
71-
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
72-
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
69+
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
70+
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
7371
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
7472
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
7573
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=

metaparser/ipa.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package metaparser
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/bitrise-io/go-xcode/v2/artifacts"
7+
"github.com/bitrise-io/go-xcode/v2/zip"
8+
)
9+
10+
// ParseIPAData ...
11+
func (m *Parser) ParseIPAData(pth string) (*ArtifactMetadata, error) {
12+
appInfo, provisioningInfo, err := m.readIPADeploymentMeta(pth)
13+
if err != nil {
14+
return nil, fmt.Errorf("failed to parse deployment info for %s: %w", pth, err)
15+
}
16+
17+
fileSize, err := m.fileManager.FileSizeInBytes(pth)
18+
if err != nil {
19+
m.logger.Warnf("Failed to get apk size, error: %s", err)
20+
}
21+
22+
return &ArtifactMetadata{
23+
AppInfo: appInfo,
24+
FileSizeBytes: fileSize,
25+
ProvisioningInfo: provisioningInfo,
26+
}, nil
27+
}
28+
29+
func (m *Parser) readIPADeploymentMeta(ipaPth string) (Info, ProvisionInfo, error) {
30+
reader, err := zip.NewDefaultReader(ipaPth, m.logger)
31+
if err != nil {
32+
return Info{}, ProvisionInfo{}, err
33+
}
34+
defer func() {
35+
if err := reader.Close(); err != nil {
36+
m.logger.Warnf("%s", err)
37+
}
38+
}()
39+
40+
ipaReader := artifacts.NewIPAReader(reader)
41+
infoPlist, err := ipaReader.AppInfoPlist()
42+
if err != nil {
43+
return Info{}, ProvisionInfo{}, fmt.Errorf("failed to unwrap Info.plist from ipa: %w", err)
44+
}
45+
46+
appTitle, _ := infoPlist.GetString("CFBundleName")
47+
bundleID, _ := infoPlist.GetString("CFBundleIdentifier")
48+
version, _ := infoPlist.GetString("CFBundleShortVersionString")
49+
buildNumber, _ := infoPlist.GetString("CFBundleVersion")
50+
minOSVersion, _ := infoPlist.GetString("MinimumOSVersion")
51+
deviceFamilyList, _ := infoPlist.GetUInt64Array("UIDeviceFamily")
52+
53+
appInfo := Info{
54+
AppTitle: appTitle,
55+
BundleID: bundleID,
56+
Version: version,
57+
BuildNumber: buildNumber,
58+
MinOSVersion: minOSVersion,
59+
DeviceFamilyList: deviceFamilyList,
60+
}
61+
62+
provisioningProfileInfo, err := ipaReader.ProvisioningProfileInfo()
63+
if err != nil {
64+
return Info{}, ProvisionInfo{}, fmt.Errorf("failed to read profile info from ipa: %w", err)
65+
}
66+
67+
provisioningInfo := ProvisionInfo{
68+
CreationDate: provisioningProfileInfo.CreationDate,
69+
ExpireDate: provisioningProfileInfo.ExpirationDate,
70+
DeviceUDIDList: provisioningProfileInfo.ProvisionedDevices,
71+
TeamName: provisioningProfileInfo.TeamName,
72+
ProfileName: provisioningProfileInfo.Name,
73+
ProvisionsAllDevices: provisioningProfileInfo.ProvisionsAllDevices,
74+
IPAExportMethod: provisioningProfileInfo.ExportType,
75+
}
76+
77+
return appInfo, provisioningInfo, nil
78+
}

metaparser/metaparser.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package metaparser
2+
3+
import (
4+
"time"
5+
6+
"github.com/bitrise-io/go-utils/v2/fileutil"
7+
"github.com/bitrise-io/go-utils/v2/log"
8+
"github.com/bitrise-io/go-xcode/exportoptions"
9+
)
10+
11+
// ArtifactMetadata ...
12+
type ArtifactMetadata struct {
13+
AppInfo Info `json:"app_info"`
14+
FileSizeBytes int64 `json:"file_size_bytes"`
15+
ProvisioningInfo ProvisionInfo `json:"provisioning_info,omitempty"`
16+
Scheme string `json:"scheme,omitempty"`
17+
}
18+
19+
// Info ...
20+
type Info struct {
21+
AppTitle string `json:"app_title"`
22+
BundleID string `json:"bundle_id"`
23+
Version string `json:"version"`
24+
BuildNumber string `json:"build_number"`
25+
MinOSVersion string `json:"min_OS_version"`
26+
DeviceFamilyList []uint64 `json:"device_family_list"`
27+
}
28+
29+
// ProvisionInfo ...
30+
type ProvisionInfo struct {
31+
CreationDate time.Time `json:"creation_date"`
32+
ExpireDate time.Time `json:"expire_date"`
33+
DeviceUDIDList []string `json:"device_UDID_list"`
34+
TeamName string `json:"team_name"`
35+
ProfileName string `json:"profile_name"`
36+
ProvisionsAllDevices bool `json:"provisions_all_devices"`
37+
IPAExportMethod exportoptions.Method `json:"ipa_export_method"`
38+
}
39+
40+
// Parser ...
41+
type Parser struct {
42+
logger log.Logger
43+
fileManager fileutil.FileManager
44+
}
45+
46+
// New ...
47+
func New(logger log.Logger, fileManager fileutil.FileManager) *Parser {
48+
return &Parser{
49+
logger: logger,
50+
fileManager: fileManager,
51+
}
52+
}

metaparser/xcarchive.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package metaparser
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/bitrise-io/go-xcode/v2/artifacts"
8+
"github.com/bitrise-io/go-xcode/v2/zip"
9+
)
10+
11+
// MacOSProjectIsNotSupported ...
12+
var MacOSProjectIsNotSupported = errors.New("macOS project is not supported")
13+
14+
// ParseXCArchiveData ...
15+
func (m *Parser) ParseXCArchiveData(pth string) (*ArtifactMetadata, error) {
16+
17+
appInfo, scheme, err := m.readXCArchiveDeploymentMeta(pth)
18+
if err != nil {
19+
return &ArtifactMetadata{
20+
AppInfo: appInfo,
21+
Scheme: scheme,
22+
}, fmt.Errorf("failed to parse deployment info for %s: %w", pth, err)
23+
}
24+
25+
fileSize, err := m.fileManager.FileSizeInBytes(pth)
26+
if err != nil {
27+
m.logger.Warnf("Failed to get apk size, error: %s", err)
28+
}
29+
30+
return &ArtifactMetadata{
31+
AppInfo: appInfo,
32+
FileSizeBytes: fileSize,
33+
Scheme: scheme,
34+
}, nil
35+
}
36+
37+
func (m *Parser) readXCArchiveDeploymentMeta(pth string) (Info, string, error) {
38+
reader, err := zip.NewDefaultReader(pth, m.logger)
39+
if err != nil {
40+
return Info{}, "", err
41+
}
42+
defer func() {
43+
if err := reader.Close(); err != nil {
44+
m.logger.Warnf("%s", err)
45+
}
46+
}()
47+
48+
xcarchiveReader := artifacts.NewXCArchiveReader(reader)
49+
isMacos := xcarchiveReader.IsMacOS()
50+
if isMacos {
51+
return Info{}, "", MacOSProjectIsNotSupported
52+
}
53+
archiveInfoPlist, err := xcarchiveReader.InfoPlist()
54+
if err != nil {
55+
return Info{}, "", fmt.Errorf("failed to unwrap Info.plist from xcarchive: %w", err)
56+
}
57+
58+
iosXCArchiveReader := artifacts.NewIOSXCArchiveReader(reader)
59+
appInfoPlist, err := iosXCArchiveReader.AppInfoPlist()
60+
if err != nil {
61+
return Info{}, "", fmt.Errorf("failed to unwrap application Info.plist from xcarchive: %w", err)
62+
}
63+
64+
appTitle, _ := appInfoPlist.GetString("CFBundleName")
65+
bundleID, _ := appInfoPlist.GetString("CFBundleIdentifier")
66+
version, _ := appInfoPlist.GetString("CFBundleShortVersionString")
67+
buildNumber, _ := appInfoPlist.GetString("CFBundleVersion")
68+
minOSVersion, _ := appInfoPlist.GetString("MinimumOSVersion")
69+
deviceFamilyList, _ := appInfoPlist.GetUInt64Array("UIDeviceFamily")
70+
scheme, _ := archiveInfoPlist.GetString("SchemeName")
71+
72+
appInfo := Info{
73+
AppTitle: appTitle,
74+
BundleID: bundleID,
75+
Version: version,
76+
BuildNumber: buildNumber,
77+
MinOSVersion: minOSVersion,
78+
DeviceFamilyList: deviceFamilyList,
79+
}
80+
81+
return appInfo, scheme, nil
82+
}

0 commit comments

Comments
 (0)