@@ -472,31 +472,87 @@ async def cleanup_git_repo(self, repo: Dict[str, str]):
472472 return {}
473473
474474
475+ """
476+ `git ls-remote` is a Git command that queries the remote repository for
477+ references, specifically the SHA-1 hashes of commits at the HEADs of branches
478+ and tags. It's frequently utilized to view references without needing to
479+ perform a full clone or fetch.
480+
481+ When done over HTTP/HTTPS, `git ls-remote` follows these steps:
482+
483+ 1. **Send a GET request to `<repo URL>/info/refs?service=git-upload-pack`.**
484+
485+ `git-upload-pack` is the service that's responsible for providing packfiles
486+ to the client in response to fetch requests. This service also runs the `git
487+ upload-pack` command, gathering the objects necessary to complete a fetch.
488+
489+ 2. **Server will respond with a `text/plain` content type and a `001#
490+ service=git-upload-pack` header, followed by a list of references and
491+ capabilities.**
492+
493+ The payload consists of pkt-line (packet line) formatted data. Each line has
494+ a 4-byte length header, which includes the 4 bytes used for the length header
495+ itself. "0000" signals the end of the header.
496+
497+ The server lists all the HEADs of the branches and the tags of the repo,
498+ giving their SHA1 values and their fully-qualified names. After the `0000`,
499+ it also lists `capabilities` such as `multi_ack`, `thin-pack`, `ofs-delta`,
500+ etc.
501+
502+ 3. **Client parses the refs.**
503+
504+ Your client, or your code using `aiohttp` in this case, would need to parse
505+ the refs information to extract the SHA-1 hashes and the fully-qualified
506+ names of the branches and tags.
507+
508+ In short, when you run `git ls-remote` over HTTP, it makes a single HTTP GET
509+ request to the `/info/refs` endpoint of the repository you're querying, and
510+ parses the response to display the list of references in the remote repository.
511+
512+ Remember that this data contains null bytes and other binary data. Thus,
513+ manipulating it as a regular string might result in incorrect results. Use
514+ appropriate methods to deal with binary data.
515+ """
516+ import os
517+ import sys
518+ import json
519+ import base64
475520import asyncio
521+ import datetime
476522import dataclasses
477523from typing import List , Dict
478524
479525
480- async def git_ls_remote (repo_url ):
526+ async def git_ls_remote (session , repo_url ):
481527 import aiohttp
482528
483- async with aiohttp .ClientSession (trust_env = True ) as session :
484- async with session .get (
485- f"{ repo_url } /info/refs?service=git-upload-pack"
486- ) as response :
487- if response .status == 200 :
488- refs_info = await response .text ()
489- return parse_refs (refs_info )
529+ async with session .get (
530+ f"{ repo_url } /info/refs?service=git-upload-pack" ,
531+ ) as response :
532+ if response .status == 401 :
533+ raise Exception (
534+ repo_url
535+ + ": "
536+ + await response .text ()
537+ + ": "
538+ + json .dumps (dict (response .headers ), indent = 4 , sort_keys = True )
539+ )
540+ elif response .status == 200 :
541+ refs_info = await response .text ()
542+ return parse_refs (repo_url , refs_info )
490543
491544
492545@dataclasses .dataclass
493546class GitLsRemoteRefs :
547+ repo_url : str
494548 metadata : Dict [str , str ]
495549 capabilities : List [str ]
496550 refs : Dict [str , str ]
497551
498552
499- def parse_refs (refs_info ):
553+ def parse_refs (repo_url , refs_info ):
554+ if refs_info .count ("\n " ) < 2 :
555+ return
500556 header , HEAD = refs_info .split ("\n " , maxsplit = 1 )
501557 HEAD , lines = HEAD .split ("\x00 " , maxsplit = 1 )
502558 refs = {}
@@ -520,7 +576,57 @@ def parse_refs(refs_info):
520576 refs [ref ] = hash_ref [4 :]
521577 continue
522578 return GitLsRemoteRefs (
579+ repo_url = repo_url ,
523580 metadata = metadata ,
524581 capabilities = capabilities ,
525582 refs = refs ,
526583 )
584+
585+
586+ async def git_ls_remotes (repo_urls : List [str ], github_token : str = None ):
587+ import aiohttp
588+
589+ headers = None
590+ if github_token :
591+ basic_auth = base64 .b64encode (
592+ ("token:" + github_token ).encode ()
593+ ).decode ()
594+ headers = {"Authorization" : f"Basic { basic_auth } " }
595+
596+ async with aiohttp .ClientSession (
597+ trust_env = True ,
598+ headers = headers ,
599+ ) as session :
600+ async with asyncio .TaskGroup () as tg :
601+ for coro in asyncio .as_completed (
602+ [
603+ tg .create_task (git_ls_remote (session , repo_url ))
604+ for repo_url in repo_urls
605+ ]
606+ ):
607+ git_ls_remote_refs = await coro
608+ if git_ls_remote_refs :
609+ yield git_ls_remote_refs
610+
611+
612+ async def main ():
613+ print (
614+ json .dumps (
615+ {
616+ git_ls_remote_refs .repo_url : dataclasses .asdict (
617+ git_ls_remote_refs
618+ )
619+ async for git_ls_remote_refs in git_ls_remotes (
620+ list (
621+ sorted (list (set ([line .strip () for line in sys .stdin ])))
622+ ),
623+ github_token = os .environ .get ("GH_TOKEN" , None ),
624+ )
625+ },
626+ sort_keys = True ,
627+ )
628+ )
629+
630+
631+ if __name__ == "__main__" :
632+ asyncio .run (main ())
0 commit comments