Skip to content
This repository was archived by the owner on Aug 25, 2024. It is now read-only.

Commit 715e6c2

Browse files
committed
feature: git: aiohttp ls-remotes
Signed-off-by: John Andersen <johnandersenpdx@gmail.com>
1 parent 6566ede commit 715e6c2

1 file changed

Lines changed: 115 additions & 9 deletions

File tree

feature/git/dffml_feature_git/feature/operations.py

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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
475520
import asyncio
521+
import datetime
476522
import dataclasses
477523
from 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
493546
class 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

Comments
 (0)