-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgit.py
139 lines (114 loc) · 3.95 KB
/
git.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Optional
@dataclass
class RepoSize:
value: float
unit: str
@classmethod
def from_dict(cls, data: dict[str, Any]):
return cls(
value=float(data['value']),
unit=data['unit'],
)
@dataclass
class Branch:
local_branch: str
remote_branch: Optional[str] = None
is_sync: Optional[bool] = None
@classmethod
def from_dict(cls, data: dict[str, Any]):
return cls(
local_branch=data['local_branch'],
remote_branch=data['remote_branch'],
is_sync=data['is_sync'],
)
@dataclass
class Repo:
name: str
is_clean: Optional[bool]
size: Optional[RepoSize]
current_branch: Optional[Branch]
remotes: Optional[dict[str, str]]
ignore: bool = False
@classmethod
def from_dict(cls, data: dict[str, Any]):
return cls(
name=data['name'],
is_clean=data['is_clean'],
size=RepoSize.from_dict(data['size']),
current_branch=Branch.from_dict(data['current_branch']),
remotes=data['remotes'],
ignore=data.get('ignore', False)
)
Runnable = Callable[[str], tuple[str, int]]
class Git:
def __init__(self, run: Runnable):
self.run = run
def is_git_repo(self):
output, code = self.run('git rev-parse --is-inside-work-tree')
if _failed(code):
return False
return output.strip() == 'true'
def is_clean(self) -> Optional[bool]:
output, code = self.run('git status')
if _failed(code):
return None
return 'nothing to commit, working tree clean' in output
def get_repo_size(self) -> Optional[RepoSize]:
_, code1 = self.run('git gc')
output, code2 = self.run('git count-objects -vH')
if _failed(code1) or _failed(code2):
return None
output_list = output.split()
key_index = output_list.index('size-pack:')
val_index = key_index + 1
unit_index = val_index + 1
return RepoSize(value=float(output_list[val_index]), unit=output_list[unit_index])
def get_current_branch(self) -> Optional[Branch]:
output, code = self.run('git status -sb')
if _failed(code):
return None
if 'No commits yet' in output:
return None
tokens = output.split()
branch_pair_index = tokens.index('##') + 1
branch_pair = tokens[branch_pair_index]
pair_split_index = branch_pair.find('...')
if pair_split_index == -1:
return Branch(local_branch=branch_pair)
local_branch = branch_pair[:pair_split_index]
remote_branch = branch_pair[pair_split_index+3:]
is_synced = 'ahead' not in output and 'behind' not in output
return Branch(local_branch, remote_branch, is_synced)
def get_remotes(self) -> Optional[dict[str, str]]:
output, code = self.run('git remote -v')
if _failed(code):
return None
remotes_str = output.strip()
if not remotes_str:
return None
remotes = {}
for name, url, type in [remote.split() for remote in remotes_str.split('\n')]:
if name not in remotes:
remotes[name] = {}
type = type[1:-1]
remotes[name][type] = url
return remotes
def clone_repo(self, repo_name, repo_url) -> tuple[bool, str]:
output, code = self.run(f'git clone {repo_url} {repo_name}')
if _failed(code):
return False, output.strip()
return True, ''
def _failed(code):
return code != 0
def parse_repos(json: Any):
all_dicts = all(map(lambda item: isinstance(item, dict), json))
if not all_dicts:
return False, []
repos: list[Repo] = []
try:
repos = [Repo.from_dict(item) for item in json]
except KeyError:
return False, []
return True, repos