1
1
import { spawnSync } from "child_process" ;
2
- import { RepositoryType } from "../../models " ;
2
+ import type { Logger } from "../../utils " ;
3
3
import { BasePath } from "../utils/base-path" ;
4
4
5
5
const TEN_MEGABYTES : number = 1024 * 10000 ;
@@ -19,131 +19,43 @@ export const gitIsInstalled = git("--version").status === 0;
19
19
*/
20
20
export class Repository {
21
21
/**
22
- * The root path of this repository.
22
+ * The path of this repository on disk .
23
23
*/
24
24
path : string ;
25
25
26
26
/**
27
- * The name of the branch this repository is on right now .
27
+ * All files tracked by the repository.
28
28
*/
29
- branch : string ;
29
+ files = new Set < string > ( ) ;
30
30
31
31
/**
32
- * A list of all files tracked by the repository .
32
+ * The base url for link creation .
33
33
*/
34
- files : string [ ] = [ ] ;
34
+ baseUrl : string ;
35
35
36
36
/**
37
- * The user/organization name of this repository on GitHub.
37
+ * The anchor prefix used to select lines, usually `L`
38
38
*/
39
- user ?: string ;
40
-
41
- /**
42
- * The project name of this repository on GitHub.
43
- */
44
- project ?: string ;
45
-
46
- /**
47
- * The hostname for this GitHub/Bitbucket/.etc project.
48
- *
49
- * Defaults to: `github.com` (for normal, public GitHub instance projects)
50
- *
51
- * Can be the hostname for an enterprise version of GitHub, e.g. `github.acme.com`
52
- * (if found as a match in the list of git remotes).
53
- */
54
- hostname = "github.com" ;
55
-
56
- /**
57
- * Whether this is a GitHub, Bitbucket, or other type of repository.
58
- */
59
- type : RepositoryType = RepositoryType . GitHub ;
60
-
61
- private urlCache = new Map < string , string > ( ) ;
39
+ anchorPrefix : string ;
62
40
63
41
/**
64
42
* Create a new Repository instance.
65
43
*
66
44
* @param path The root path of the repository.
67
45
*/
68
- constructor ( path : string , gitRevision : string , repoLinks : string [ ] ) {
46
+ constructor ( path : string , baseUrl : string ) {
69
47
this . path = path ;
70
- this . branch = gitRevision || "master" ;
71
-
72
- for ( let i = 0 , c = repoLinks . length ; i < c ; i ++ ) {
73
- let match =
74
- / ( g i t h u b (? ! .u s ) (?: \. [ a - z ] + ) * \. [ a - z ] { 2 , } ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / . exec (
75
- repoLinks [ i ]
76
- ) ;
77
-
78
- // Github Enterprise
79
- if ( ! match ) {
80
- match = / ( \w + \. g i t h u b p r i v a t e .c o m ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / . exec (
81
- repoLinks [ i ]
82
- ) ;
83
- }
84
-
85
- // Github Enterprise
86
- if ( ! match ) {
87
- match = / ( \w + \. g h e .c o m ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / . exec ( repoLinks [ i ] ) ;
88
- }
89
-
90
- // Github Enterprise
91
- if ( ! match ) {
92
- match = / ( \w + \. g i t h u b .u s ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / . exec ( repoLinks [ i ] ) ;
93
- }
94
-
95
- if ( ! match ) {
96
- match = / ( b i t b u c k e t .o r g ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / . exec ( repoLinks [ i ] ) ;
97
- }
98
-
99
- if ( ! match ) {
100
- match = / ( g i t l a b .c o m ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / . exec ( repoLinks [ i ] ) ;
101
- }
102
-
103
- if ( match ) {
104
- this . hostname = match [ 1 ] ;
105
- this . user = match [ 2 ] ;
106
- this . project = match [ 3 ] ;
107
- if ( this . project . endsWith ( ".git" ) ) {
108
- this . project = this . project . slice ( 0 , - 4 ) ;
109
- }
110
- break ;
111
- }
112
- }
113
-
114
- if ( this . hostname . includes ( "bitbucket.org" ) ) {
115
- this . type = RepositoryType . Bitbucket ;
116
- } else if ( this . hostname . includes ( "gitlab.com" ) ) {
117
- this . type = RepositoryType . GitLab ;
118
- } else {
119
- this . type = RepositoryType . GitHub ;
120
- }
48
+ this . baseUrl = baseUrl ;
49
+ this . anchorPrefix = guessAnchorPrefix ( this . baseUrl ) ;
121
50
122
51
let out = git ( "-C" , path , "ls-files" ) ;
123
52
if ( out . status === 0 ) {
124
53
out . stdout . split ( "\n" ) . forEach ( ( file ) => {
125
54
if ( file !== "" ) {
126
- this . files . push ( BasePath . normalize ( path + "/" + file ) ) ;
55
+ this . files . add ( BasePath . normalize ( path + "/" + file ) ) ;
127
56
}
128
57
} ) ;
129
58
}
130
-
131
- if ( ! gitRevision ) {
132
- out = git ( "-C" , path , "rev-parse" , "--short" , "HEAD" ) ;
133
- if ( out . status === 0 ) {
134
- this . branch = out . stdout . replace ( "\n" , "" ) ;
135
- }
136
- }
137
- }
138
-
139
- /**
140
- * Check whether the given file is tracked by this repository.
141
- *
142
- * @param fileName The name of the file to test for.
143
- * @returns TRUE when the file is part of the repository, otherwise FALSE.
144
- */
145
- contains ( fileName : string ) : boolean {
146
- return this . files . includes ( fileName ) ;
147
59
}
148
60
149
61
/**
@@ -153,39 +65,15 @@ export class Repository {
153
65
* @returns A URL pointing to the web preview of the given file or undefined.
154
66
*/
155
67
getURL ( fileName : string ) : string | undefined {
156
- if ( this . urlCache . has ( fileName ) ) {
157
- return this . urlCache . get ( fileName ) ! ;
158
- }
159
-
160
- if ( ! this . user || ! this . project || ! this . contains ( fileName ) ) {
68
+ if ( ! this . files . has ( fileName ) ) {
161
69
return ;
162
70
}
163
71
164
- const url = [
165
- `https://${ this . hostname } ` ,
166
- this . user ,
167
- this . project ,
168
- this . type === RepositoryType . GitLab ? "-" : undefined ,
169
- this . type === RepositoryType . Bitbucket ? "src" : "blob" ,
170
- this . branch ,
171
- fileName . substring ( this . path . length + 1 ) ,
172
- ]
173
- . filter ( ( s ) => ! ! s )
174
- . join ( "/" ) ;
175
-
176
- this . urlCache . set ( fileName , url ) ;
177
- return url ;
72
+ return `${ this . baseUrl } /${ fileName . substring ( this . path . length + 1 ) } ` ;
178
73
}
179
74
180
75
getLineNumberAnchor ( lineNumber : number ) : string {
181
- switch ( this . type ) {
182
- default :
183
- case RepositoryType . GitHub :
184
- case RepositoryType . GitLab :
185
- return "L" + lineNumber ;
186
- case RepositoryType . Bitbucket :
187
- return "lines-" + lineNumber ;
188
- }
76
+ return `${ this . anchorPrefix } ${ lineNumber } ` ;
189
77
}
190
78
191
79
/**
@@ -200,19 +88,99 @@ export class Repository {
200
88
static tryCreateRepository (
201
89
path : string ,
202
90
gitRevision : string ,
203
- gitRemote : string
91
+ gitRemote : string ,
92
+ logger : Logger
204
93
) : Repository | undefined {
205
- const out = git ( "-C" , path , "rev-parse" , "--show-toplevel" ) ;
206
- const remotesOutput = git ( "-C" , path , "remote" , "get-url" , gitRemote ) ;
207
-
208
- if ( out . status !== 0 || remotesOutput . status !== 0 ) {
209
- return ;
94
+ const topLevel = git ( "-C" , path , "rev-parse" , "--show-toplevel" ) ;
95
+ if ( topLevel . status !== 0 ) return ;
96
+
97
+ gitRevision ||= git (
98
+ "-C" ,
99
+ path ,
100
+ "rev-parse" ,
101
+ "--short" ,
102
+ "HEAD"
103
+ ) . stdout . trim ( ) ;
104
+ if ( ! gitRevision ) return ; // Will only happen in a repo with no commits.
105
+
106
+ let baseUrl : string | undefined ;
107
+ if ( / ^ h t t p s ? : \/ \/ / . test ( gitRemote ) ) {
108
+ baseUrl = `${ gitRemote } /${ gitRevision } ` ;
109
+ } else {
110
+ const remotesOut = git ( "-C" , path , "remote" , "get-url" , gitRemote ) ;
111
+ if ( remotesOut . status === 0 ) {
112
+ baseUrl = guessBaseUrl (
113
+ gitRevision ,
114
+ remotesOut . stdout . split ( "\n" )
115
+ ) ;
116
+ } else {
117
+ logger . warn (
118
+ `The provided git remote "${ gitRemote } " was not valid. Source links will be broken.`
119
+ ) ;
120
+ }
210
121
}
211
122
123
+ if ( ! baseUrl ) return ;
124
+
212
125
return new Repository (
213
- BasePath . normalize ( out . stdout . replace ( "\n" , "" ) ) ,
214
- gitRevision ,
215
- remotesOutput . stdout . split ( "\n" )
126
+ BasePath . normalize ( topLevel . stdout . replace ( "\n" , "" ) ) ,
127
+ baseUrl
216
128
) ;
217
129
}
218
130
}
131
+
132
+ // Should have three capturing groups:
133
+ // 1. hostname
134
+ // 2. user
135
+ // 3. project
136
+ const repoExpressions = [
137
+ / ( g i t h u b (? ! .u s ) (?: \. [ a - z ] + ) * \. [ a - z ] { 2 , } ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / ,
138
+ / ( \w + \. g i t h u b p r i v a t e .c o m ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / , // GitHub enterprise
139
+ / ( \w + \. g h e .c o m ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / , // GitHub enterprise
140
+ / ( \w + \. g i t h u b .u s ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / , // GitHub enterprise
141
+ / ( b i t b u c k e t .o r g ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / ,
142
+ / ( g i t l a b .c o m ) [: / ] ( [ ^ / ] + ) \/ ( .* ) / ,
143
+ ] ;
144
+
145
+ export function guessBaseUrl (
146
+ gitRevision : string ,
147
+ remotes : string [ ]
148
+ ) : string | undefined {
149
+ let hostname = "" ;
150
+ let user = "" ;
151
+ let project = "" ;
152
+ outer: for ( const repoLink of remotes ) {
153
+ for ( const regex of repoExpressions ) {
154
+ const match = regex . exec ( repoLink ) ;
155
+ if ( match ) {
156
+ hostname = match [ 1 ] ;
157
+ user = match [ 2 ] ;
158
+ project = match [ 3 ] ;
159
+ break outer;
160
+ }
161
+ }
162
+ }
163
+
164
+ if ( ! hostname ) return ;
165
+
166
+ if ( project . endsWith ( ".git" ) ) {
167
+ project = project . slice ( 0 , - 4 ) ;
168
+ }
169
+
170
+ let sourcePath = "blob" ;
171
+ if ( hostname . includes ( "gitlab" ) ) {
172
+ sourcePath = "-/blob" ;
173
+ } else if ( hostname . includes ( "bitbucket" ) ) {
174
+ sourcePath = "src" ;
175
+ }
176
+
177
+ return `https://${ hostname } /${ user } /${ project } /${ sourcePath } /${ gitRevision } ` ;
178
+ }
179
+
180
+ function guessAnchorPrefix ( url : string ) {
181
+ if ( url . includes ( "bitbucket" ) ) {
182
+ return "lines-" ;
183
+ }
184
+
185
+ return "L" ;
186
+ }
0 commit comments