11import { spawnSync } from "child_process" ;
2- import { RepositoryType } from "../../models " ;
2+ import type { Logger } from "../../utils " ;
33import { BasePath } from "../utils/base-path" ;
44
55const TEN_MEGABYTES : number = 1024 * 10000 ;
@@ -19,131 +19,43 @@ export const gitIsInstalled = git("--version").status === 0;
1919 */
2020export class Repository {
2121 /**
22- * The root path of this repository.
22+ * The path of this repository on disk .
2323 */
2424 path : string ;
2525
2626 /**
27- * The name of the branch this repository is on right now .
27+ * All files tracked by the repository.
2828 */
29- branch : string ;
29+ files = new Set < string > ( ) ;
3030
3131 /**
32- * A list of all files tracked by the repository .
32+ * The base url for link creation .
3333 */
34- files : string [ ] = [ ] ;
34+ baseUrl : string ;
3535
3636 /**
37- * The user/organization name of this repository on GitHub.
37+ * The anchor prefix used to select lines, usually `L`
3838 */
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 ;
6240
6341 /**
6442 * Create a new Repository instance.
6543 *
6644 * @param path The root path of the repository.
6745 */
68- constructor ( path : string , gitRevision : string , repoLinks : string [ ] ) {
46+ constructor ( path : string , baseUrl : string ) {
6947 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 ) ;
12150
12251 let out = git ( "-C" , path , "ls-files" ) ;
12352 if ( out . status === 0 ) {
12453 out . stdout . split ( "\n" ) . forEach ( ( file ) => {
12554 if ( file !== "" ) {
126- this . files . push ( BasePath . normalize ( path + "/" + file ) ) ;
55+ this . files . add ( BasePath . normalize ( path + "/" + file ) ) ;
12756 }
12857 } ) ;
12958 }
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 ) ;
14759 }
14860
14961 /**
@@ -153,39 +65,15 @@ export class Repository {
15365 * @returns A URL pointing to the web preview of the given file or undefined.
15466 */
15567 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 ) ) {
16169 return ;
16270 }
16371
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 ) } ` ;
17873 }
17974
18075 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 } ` ;
18977 }
19078
19179 /**
@@ -200,19 +88,99 @@ export class Repository {
20088 static tryCreateRepository (
20189 path : string ,
20290 gitRevision : string ,
203- gitRemote : string
91+ gitRemote : string ,
92+ logger : Logger
20493 ) : 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+ }
210121 }
211122
123+ if ( ! baseUrl ) return ;
124+
212125 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
216128 ) ;
217129 }
218130}
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