@@ -10,6 +10,20 @@ import getScope from '../util/get-scope';
10
10
import getCommandFlags from '../util/get-command-flags' ;
11
11
import { getPkgName , getCommandName } from '../util/pkg-name' ;
12
12
import Client from '../util/client' ;
13
+ import validatePaths from '../util/validate-paths' ;
14
+ import { ensureLink } from '../util/ensure-link' ;
15
+ import { parseGitConfig , pluckRemoteUrl } from '../util/create-git-meta' ;
16
+ import {
17
+ connectGitProvider ,
18
+ disconnectGitProvider ,
19
+ formatProvider ,
20
+ parseRepoUrl ,
21
+ } from '../util/projects/connect-git-provider' ;
22
+ import { join } from 'path' ;
23
+ import { Team , User } from '../types' ;
24
+ import confirm from '../util/input/confirm' ;
25
+ import { Output } from '../util/output' ;
26
+ import link from '../util/output/link' ;
13
27
14
28
const e = encodeURIComponent ;
15
29
@@ -20,6 +34,7 @@ const help = () => {
20
34
${ chalk . dim ( 'Commands:' ) }
21
35
22
36
ls Show all projects in the selected team/user
37
+ connect Connect a Git provider to your project
23
38
add [name] Add a new project
24
39
rm [name] Remove a project
25
40
@@ -54,6 +69,7 @@ const main = async (client: Client) => {
54
69
argv = getArgs ( client . argv . slice ( 2 ) , {
55
70
'--next' : Number ,
56
71
'-N' : '--next' ,
72
+ '--yes' : Boolean ,
57
73
} ) ;
58
74
} catch ( error ) {
59
75
handleError ( error ) ;
@@ -71,10 +87,10 @@ const main = async (client: Client) => {
71
87
72
88
const { output } = client ;
73
89
74
- let contextName = null ;
90
+ let scope = null ;
75
91
76
92
try {
77
- ( { contextName } = await getScope ( client ) ) ;
93
+ scope = await getScope ( client ) ;
78
94
} catch ( err ) {
79
95
if ( err . code === 'NOT_AUTHORIZED' || err . code === 'TEAM_DELETED' ) {
80
96
output . error ( err . message ) ;
@@ -84,17 +100,12 @@ const main = async (client: Client) => {
84
100
throw err ;
85
101
}
86
102
87
- try {
88
- await run ( { client, contextName } ) ;
89
- } catch ( err ) {
90
- handleError ( err ) ;
91
- exit ( 1 ) ;
92
- }
103
+ return await run ( { client, scope } ) ;
93
104
} ;
94
105
95
106
export default async ( client : Client ) => {
96
107
try {
97
- await main ( client ) ;
108
+ return await main ( client ) ;
98
109
} catch ( err ) {
99
110
handleError ( err ) ;
100
111
process . exit ( 1 ) ;
@@ -103,16 +114,148 @@ export default async (client: Client) => {
103
114
104
115
async function run ( {
105
116
client,
106
- contextName ,
117
+ scope ,
107
118
} : {
108
119
client : Client ;
109
- contextName : string ;
120
+ scope : {
121
+ contextName : string ;
122
+ team : Team | null ;
123
+ user : User ;
124
+ } ;
110
125
} ) {
111
126
const { output } = client ;
127
+ const { contextName, team } = scope ;
112
128
const args = argv . _ . slice ( 1 ) ;
113
129
114
130
const start = Date . now ( ) ;
115
131
132
+ if ( subcommand === 'connect' ) {
133
+ const yes = Boolean ( argv [ '--yes' ] ) ;
134
+ if ( args . length !== 0 ) {
135
+ output . error (
136
+ `Invalid number of arguments. Usage: ${ chalk . cyan (
137
+ `${ getCommandName ( 'project connect' ) } `
138
+ ) } `
139
+ ) ;
140
+ return exit ( 2 ) ;
141
+ }
142
+
143
+ let paths = [ process . cwd ( ) ] ;
144
+
145
+ const validate = await validatePaths ( client , paths ) ;
146
+ if ( ! validate . valid ) {
147
+ return validate . exitCode ;
148
+ }
149
+ const { path } = validate ;
150
+
151
+ const linkedProject = await ensureLink (
152
+ 'project connect' ,
153
+ client ,
154
+ path ,
155
+ yes
156
+ ) ;
157
+ if ( typeof linkedProject === 'number' ) {
158
+ return linkedProject ;
159
+ }
160
+
161
+ const { project, org } = linkedProject ;
162
+ const gitProviderLink = project . link ;
163
+
164
+ client . config . currentTeam = org . type === 'team' ? org . id : undefined ;
165
+
166
+ // get project from .git
167
+ const gitConfigPath = join ( path , '.git/config' ) ;
168
+ const gitConfig = await parseGitConfig ( gitConfigPath , output ) ;
169
+ if ( ! gitConfig ) {
170
+ output . error (
171
+ `No local git repo found. Run ${ chalk . cyan (
172
+ '`git clone <url>`'
173
+ ) } to clone a remote Git repository first.`
174
+ ) ;
175
+ return 1 ;
176
+ }
177
+ const remoteUrl = pluckRemoteUrl ( gitConfig ) ;
178
+ if ( ! remoteUrl ) {
179
+ output . error (
180
+ `No remote origin URL found in your Git config. Make sure you've connected your local Git repo to a Git provider first.`
181
+ ) ;
182
+ return 1 ;
183
+ }
184
+ const parsedUrl = parseRepoUrl ( remoteUrl ) ;
185
+ if ( ! parsedUrl ) {
186
+ output . error (
187
+ `Failed to parse Git repo data from the following remote URL in your Git config: ${ link (
188
+ remoteUrl
189
+ ) } `
190
+ ) ;
191
+ return 1 ;
192
+ }
193
+ const { provider, org : gitOrg , repo } = parsedUrl ;
194
+ const repoPath = `${ gitOrg } /${ repo } ` ;
195
+ let connectedRepoPath ;
196
+
197
+ if ( ! gitProviderLink ) {
198
+ const connect = await connectGitProvider (
199
+ client ,
200
+ team ,
201
+ project . id ,
202
+ provider ,
203
+ repoPath
204
+ ) ;
205
+ if ( typeof connect === 'number' ) {
206
+ return connect ;
207
+ }
208
+ } else {
209
+ const connectedProvider = gitProviderLink . type ;
210
+ const connectedOrg = gitProviderLink . org ;
211
+ const connectedRepo = gitProviderLink . repo ;
212
+ connectedRepoPath = `${ connectedOrg } /${ connectedRepo } ` ;
213
+
214
+ const isSameRepo =
215
+ connectedProvider === provider &&
216
+ connectedOrg === gitOrg &&
217
+ connectedRepo === repo ;
218
+ if ( isSameRepo ) {
219
+ output . log (
220
+ `${ chalk . cyan (
221
+ connectedRepoPath
222
+ ) } is already connected to your project.`
223
+ ) ;
224
+ return 1 ;
225
+ }
226
+
227
+ const shouldReplaceRepo = await confirmRepoConnect (
228
+ client ,
229
+ output ,
230
+ yes ,
231
+ connectedRepoPath
232
+ ) ;
233
+ if ( ! shouldReplaceRepo ) {
234
+ return 0 ;
235
+ }
236
+
237
+ await disconnectGitProvider ( client , team , project . id ) ;
238
+ const connect = await connectGitProvider (
239
+ client ,
240
+ team ,
241
+ project . id ,
242
+ provider ,
243
+ repoPath
244
+ ) ;
245
+ if ( typeof connect === 'number' ) {
246
+ return connect ;
247
+ }
248
+ }
249
+
250
+ output . log (
251
+ `Connected ${ formatProvider ( provider ) } repository ${ chalk . cyan (
252
+ repoPath
253
+ ) } !`
254
+ ) ;
255
+
256
+ return 0 ;
257
+ }
258
+
116
259
if ( subcommand === 'ls' || subcommand === 'list' ) {
117
260
if ( args . length !== 0 ) {
118
261
console . error (
@@ -271,7 +414,7 @@ async function run({
271
414
return ;
272
415
}
273
416
274
- console . error ( error ( 'Please specify a valid subcommand: ls | add | rm' ) ) ;
417
+ output . error ( 'Please specify a valid subcommand: ls | connect | add | rm' ) ;
275
418
help ( ) ;
276
419
exit ( 2 ) ;
277
420
}
@@ -281,6 +424,28 @@ process.on('uncaughtException', err => {
281
424
exit ( 1 ) ;
282
425
} ) ;
283
426
427
+ async function confirmRepoConnect (
428
+ client : Client ,
429
+ output : Output ,
430
+ yes : boolean ,
431
+ connectedRepoPath : string
432
+ ) {
433
+ let shouldReplaceProject = yes ;
434
+ if ( ! shouldReplaceProject ) {
435
+ shouldReplaceProject = await confirm (
436
+ client ,
437
+ `Looks like you already have a repository connected: ${ chalk . cyan (
438
+ connectedRepoPath
439
+ ) } . Do you want to replace it?`,
440
+ true
441
+ ) ;
442
+ if ( ! shouldReplaceProject ) {
443
+ output . log ( `Aborted. Repo not connected.` ) ;
444
+ }
445
+ }
446
+ return shouldReplaceProject ;
447
+ }
448
+
284
449
function readConfirmation ( projectName : string ) {
285
450
return new Promise ( resolve => {
286
451
process . stdout . write (
0 commit comments