@@ -14,6 +14,7 @@ import (
1414 "path/filepath"
1515 "runtime"
1616 "strings"
17+ "sync"
1718
1819 "github.com/MakeNowJust/heredoc"
1920 "github.com/cli/cli/v2/api"
@@ -103,146 +104,195 @@ func (m *Manager) Dispatch(args []string, stdin io.Reader, stdout, stderr io.Wri
103104
104105func (m * Manager ) List (includeMetadata bool ) []extensions.Extension {
105106 exts , _ := m .list (includeMetadata )
106- return exts
107+ r := make ([]extensions.Extension , len (exts ))
108+ for i , v := range exts {
109+ val := v
110+ r [i ] = & val
111+ }
112+ return r
107113}
108114
109- func (m * Manager ) list (includeMetadata bool ) ([]extensions. Extension , error ) {
115+ func (m * Manager ) list (includeMetadata bool ) ([]Extension , error ) {
110116 dir := m .installDir ()
111117 entries , err := ioutil .ReadDir (dir )
112118 if err != nil {
113119 return nil , err
114120 }
115121
116- var results []extensions. Extension
122+ var results []Extension
117123 for _ , f := range entries {
118124 if ! strings .HasPrefix (f .Name (), "gh-" ) {
119125 continue
120126 }
121- ext , err := m .parseExtensionDir (f , includeMetadata )
122- if err != nil {
123- return nil , err
127+ var ext Extension
128+ var err error
129+ if f .IsDir () {
130+ ext , err = m .parseExtensionDir (f )
131+ if err != nil {
132+ return nil , err
133+ }
134+ results = append (results , ext )
135+ } else {
136+ ext , err = m .parseExtensionFile (f )
137+ if err != nil {
138+ return nil , err
139+ }
140+ results = append (results , ext )
124141 }
125- results = append (results , ext )
142+ }
143+
144+ if includeMetadata {
145+ m .populateLatestVersions (results )
126146 }
127147
128148 return results , nil
129149}
130150
131- func (m * Manager ) parseExtensionDir (fi fs.FileInfo , includeMetadata bool ) (* Extension , error ) {
151+ func (m * Manager ) parseExtensionFile (fi fs.FileInfo ) (Extension , error ) {
152+ ext := Extension {isLocal : true }
153+ id := m .installDir ()
154+ exePath := filepath .Join (id , fi .Name (), fi .Name ())
155+ if ! isSymlink (fi .Mode ()) {
156+ // if this is a regular file, its contents is the local directory of the extension
157+ p , err := readPathFromFile (filepath .Join (id , fi .Name ()))
158+ if err != nil {
159+ return ext , err
160+ }
161+ exePath = filepath .Join (p , fi .Name ())
162+ }
163+ ext .path = exePath
164+ return ext , nil
165+ }
166+
167+ func (m * Manager ) parseExtensionDir (fi fs.FileInfo ) (Extension , error ) {
132168 id := m .installDir ()
133169 if _ , err := os .Stat (filepath .Join (id , fi .Name (), manifestName )); err == nil {
134- return m .parseBinaryExtensionDir (fi , includeMetadata )
170+ return m .parseBinaryExtensionDir (fi )
135171 }
136172
137- return m .parseGitExtensionDir (fi , includeMetadata )
173+ return m .parseGitExtensionDir (fi )
138174}
139175
140- func (m * Manager ) parseBinaryExtensionDir (fi fs.FileInfo , includeMetadata bool ) (* Extension , error ) {
176+ func (m * Manager ) parseBinaryExtensionDir (fi fs.FileInfo ) (Extension , error ) {
141177 id := m .installDir ()
142178 exePath := filepath .Join (id , fi .Name (), fi .Name ())
179+ ext := Extension {path : exePath , kind : BinaryKind }
143180 manifestPath := filepath .Join (id , fi .Name (), manifestName )
144181 manifest , err := os .ReadFile (manifestPath )
145182 if err != nil {
146- return nil , fmt .Errorf ("could not open %s for reading: %w" , manifestPath , err )
183+ return ext , fmt .Errorf ("could not open %s for reading: %w" , manifestPath , err )
147184 }
148-
149185 var bm binManifest
150186 err = yaml .Unmarshal (manifest , & bm )
151187 if err != nil {
152- return nil , fmt .Errorf ("could not parse %s: %w" , manifestPath , err )
188+ return ext , fmt .Errorf ("could not parse %s: %w" , manifestPath , err )
153189 }
154-
155190 repo := ghrepo .NewWithHost (bm .Owner , bm .Name , bm .Host )
156-
157- var remoteURL string
158- var updateAvailable bool
159-
160- if includeMetadata {
161- remoteURL = ghrepo .GenerateRepoURL (repo , "" )
162- var r * release
163- r , err = fetchLatestRelease (m .client , repo )
164- if err != nil {
165- return nil , fmt .Errorf ("failed to get release info for %s: %w" , ghrepo .FullName (repo ), err )
166- }
167- if bm .Tag != r .Tag {
168- updateAvailable = true
169- }
170- }
171-
172- return & Extension {
173- path : exePath ,
174- url : remoteURL ,
175- updateAvailable : updateAvailable ,
176- }, nil
191+ remoteURL := ghrepo .GenerateRepoURL (repo , "" )
192+ ext .url = remoteURL
193+ ext .currentVersion = bm .Tag
194+ return ext , nil
177195}
178196
179- func (m * Manager ) parseGitExtensionDir (fi fs.FileInfo , includeMetadata bool ) (* Extension , error ) {
180- // TODO untangle local from this since local might be binary or git
197+ func (m * Manager ) parseGitExtensionDir (fi fs.FileInfo ) (Extension , error ) {
181198 id := m .installDir ()
182- var remoteUrl string
183- updateAvailable := false
184- isLocal := false
185199 exePath := filepath .Join (id , fi .Name (), fi .Name ())
186- if fi .IsDir () {
187- if includeMetadata {
188- remoteUrl = m .getRemoteUrl (fi .Name ())
189- updateAvailable = m .checkUpdateAvailable (fi .Name ())
190- }
191- } else {
192- isLocal = true
193- if ! isSymlink (fi .Mode ()) {
194- // if this is a regular file, its contents is the local directory of the extension
195- p , err := readPathFromFile (filepath .Join (id , fi .Name ()))
196- if err != nil {
197- return nil , err
198- }
199- exePath = filepath .Join (p , fi .Name ())
200- }
201- }
202-
203- return & Extension {
204- path : exePath ,
205- url : remoteUrl ,
206- isLocal : isLocal ,
207- updateAvailable : updateAvailable ,
200+ remoteUrl := m .getRemoteUrl (fi .Name ())
201+ currentVersion := m .getCurrentVersion (fi .Name ())
202+ return Extension {
203+ path : exePath ,
204+ url : remoteUrl ,
205+ isLocal : false ,
206+ currentVersion : currentVersion ,
207+ kind : GitKind ,
208208 }, nil
209209}
210210
211- func (m * Manager ) getRemoteUrl (extension string ) string {
211+ // getCurrentVersion determines the current version for non-local git extensions.
212+ func (m * Manager ) getCurrentVersion (extension string ) string {
212213 gitExe , err := m .lookPath ("git" )
213214 if err != nil {
214215 return ""
215216 }
216217 dir := m .installDir ()
217218 gitDir := "--git-dir=" + filepath .Join (dir , extension , ".git" )
218- cmd := m .newCommand (gitExe , gitDir , "config " , "remote.origin.url " )
219- url , err := cmd .Output ()
219+ cmd := m .newCommand (gitExe , gitDir , "rev-parse " , "HEAD " )
220+ localSha , err := cmd .Output ()
220221 if err != nil {
221222 return ""
222223 }
223- return strings .TrimSpace (string ( url ))
224+ return string ( bytes .TrimSpace (localSha ))
224225}
225226
226- func (m * Manager ) checkUpdateAvailable (extension string ) bool {
227+ // getRemoteUrl determines the remote URL for non-local git extensions.
228+ func (m * Manager ) getRemoteUrl (extension string ) string {
227229 gitExe , err := m .lookPath ("git" )
228230 if err != nil {
229- return false
231+ return ""
230232 }
231233 dir := m .installDir ()
232234 gitDir := "--git-dir=" + filepath .Join (dir , extension , ".git" )
233- cmd := m .newCommand (gitExe , gitDir , "ls-remote " , "origin" , "HEAD " )
234- lsRemote , err := cmd .Output ()
235+ cmd := m .newCommand (gitExe , gitDir , "config " , "remote. origin.url " )
236+ url , err := cmd .Output ()
235237 if err != nil {
236- return false
238+ return ""
237239 }
238- remoteSha := bytes .SplitN (lsRemote , []byte ("\t " ), 2 )[0 ]
239- cmd = m .newCommand (gitExe , gitDir , "rev-parse" , "HEAD" )
240- localSha , err := cmd .Output ()
241- if err != nil {
242- return false
240+ return strings .TrimSpace (string (url ))
241+ }
242+
243+ func (m * Manager ) populateLatestVersions (exts []Extension ) {
244+ size := len (exts )
245+ type result struct {
246+ index int
247+ version string
248+ }
249+ ch := make (chan result , size )
250+ var wg sync.WaitGroup
251+ wg .Add (size )
252+ for idx , ext := range exts {
253+ go func (i int , e Extension ) {
254+ defer wg .Done ()
255+ version , _ := m .getLatestVersion (e )
256+ ch <- result {index : i , version : version }
257+ }(idx , ext )
258+ }
259+ wg .Wait ()
260+ close (ch )
261+ for r := range ch {
262+ ext := & exts [r .index ]
263+ ext .latestVersion = r .version
264+ }
265+ }
266+
267+ func (m * Manager ) getLatestVersion (ext Extension ) (string , error ) {
268+ if ext .isLocal {
269+ return "" , fmt .Errorf ("unable to get latest version for local extensions" )
270+ }
271+ if ext .kind == GitKind {
272+ gitExe , err := m .lookPath ("git" )
273+ if err != nil {
274+ return "" , err
275+ }
276+ extDir := filepath .Dir (ext .path )
277+ gitDir := "--git-dir=" + filepath .Join (extDir , ".git" )
278+ cmd := m .newCommand (gitExe , gitDir , "ls-remote" , "origin" , "HEAD" )
279+ lsRemote , err := cmd .Output ()
280+ if err != nil {
281+ return "" , err
282+ }
283+ remoteSha := bytes .SplitN (lsRemote , []byte ("\t " ), 2 )[0 ]
284+ return string (remoteSha ), nil
285+ } else {
286+ repo , err := ghrepo .FromFullName (ext .url )
287+ if err != nil {
288+ return "" , err
289+ }
290+ r , err := fetchLatestRelease (m .client , repo )
291+ if err != nil {
292+ return "" , err
293+ }
294+ return r .Tag , nil
243295 }
244- localSha = bytes .TrimSpace (localSha )
245- return ! bytes .Equal (remoteSha , localSha )
246296}
247297
248298func (m * Manager ) InstallLocal (dir string ) error {
0 commit comments