git-flow-envs 1.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git_flow/__init__.py +63 -0
- git_flow/changelog.py +59 -0
- git_flow/command/__init__.py +0 -0
- git_flow/command/base.py +236 -0
- git_flow/command/branch.py +117 -0
- git_flow/command/commit.py +62 -0
- git_flow/command/init.py +115 -0
- git_flow/command/merge.py +111 -0
- git_flow/command/new.py +56 -0
- git_flow/command/release.py +105 -0
- git_flow/command/tag.py +107 -0
- git_flow/git.py +179 -0
- git_flow/io.py +56 -0
- git_flow/main.py +90 -0
- git_flow/remote/base.py +75 -0
- git_flow/remote/bitbucket.py +47 -0
- git_flow/remote/github.py +21 -0
- git_flow_envs-1.14.0.dist-info/METADATA +101 -0
- git_flow_envs-1.14.0.dist-info/RECORD +22 -0
- git_flow_envs-1.14.0.dist-info/WHEEL +5 -0
- git_flow_envs-1.14.0.dist-info/entry_points.txt +2 -0
- git_flow_envs-1.14.0.dist-info/top_level.txt +1 -0
git_flow/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
GitFlowError = ValueError
|
|
2
|
+
|
|
3
|
+
BRANCH_TYPES = [
|
|
4
|
+
"feature",
|
|
5
|
+
"refactor",
|
|
6
|
+
"bugfix",
|
|
7
|
+
"hotfix",
|
|
8
|
+
"chore",
|
|
9
|
+
"docs",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
COMMIT_TYPES = [
|
|
13
|
+
"feature",
|
|
14
|
+
"bugfix",
|
|
15
|
+
"hotfix",
|
|
16
|
+
"refactor",
|
|
17
|
+
"perf",
|
|
18
|
+
"docs",
|
|
19
|
+
"style",
|
|
20
|
+
"test",
|
|
21
|
+
"wip",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
WIP_BRANCH_PREFIX = "wip/"
|
|
25
|
+
TRASH_BRANCH_PREFIX = "trash/"
|
|
26
|
+
|
|
27
|
+
# Commits con estos valores incrementan el componente correspondiente
|
|
28
|
+
SEMVER_MAJOR = 3
|
|
29
|
+
SEMVER_MINOR = 2
|
|
30
|
+
SEMVER_PATCH = 1
|
|
31
|
+
|
|
32
|
+
# Commits con estos tipos no alteran la versión
|
|
33
|
+
SEMVER_SKIP = 0
|
|
34
|
+
|
|
35
|
+
COMMIT_TYPE_INCREMENT = {
|
|
36
|
+
"feature": SEMVER_MINOR,
|
|
37
|
+
"bugfix": SEMVER_PATCH,
|
|
38
|
+
"hotfix": SEMVER_PATCH,
|
|
39
|
+
"refactor": SEMVER_SKIP,
|
|
40
|
+
"perf": SEMVER_SKIP,
|
|
41
|
+
"docs": SEMVER_SKIP,
|
|
42
|
+
"style": SEMVER_SKIP,
|
|
43
|
+
"test": SEMVER_SKIP,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
SUPPORTED_REMOTE_APIS = [
|
|
47
|
+
"bitbucket.org",
|
|
48
|
+
"github.com",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
REPOSITORY_TOKEN_FILENAME = ".repository-token"
|
|
52
|
+
|
|
53
|
+
COLOR_BLACK = "\033[30m"
|
|
54
|
+
COLOR_RED = "\033[31m"
|
|
55
|
+
COLOR_GREEN = "\033[32m"
|
|
56
|
+
COLOR_YELLOW = "\033[33m"
|
|
57
|
+
COLOR_BLUE = "\033[34m"
|
|
58
|
+
COLOR_5 = "\033[35m"
|
|
59
|
+
COLOR_6 = "\033[36m"
|
|
60
|
+
COLOR_7 = "\033[37m"
|
|
61
|
+
COLOR_BLACK_BOLD = "\033[1;30m"
|
|
62
|
+
COLOR_WHITE_BOLD = "\033[1;37m"
|
|
63
|
+
COLOR_RESET = "\033[0m"
|
git_flow/changelog.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from os.path import isfile
|
|
3
|
+
|
|
4
|
+
from git_flow.git import Git
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Changelog:
|
|
8
|
+
FILENAME = "CHANGELOG.md"
|
|
9
|
+
COMMIT_MESSAGE = "chore: bump version and update CHANGELOG.md [skip ci]"
|
|
10
|
+
|
|
11
|
+
def update(self, title: str, base: str, branch: str):
|
|
12
|
+
self._prepend(self.generate_entry(title, base, branch))
|
|
13
|
+
|
|
14
|
+
def generate_header(self, title):
|
|
15
|
+
now = datetime.now()
|
|
16
|
+
|
|
17
|
+
return f"""## {title} - {now.strftime("%F")}
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def generate_content(self, base: str, branch: str):
|
|
22
|
+
commits = Git(
|
|
23
|
+
"log", base + ".." + branch, first_parent=True, format="%h", merges=False
|
|
24
|
+
).lines()
|
|
25
|
+
content = ""
|
|
26
|
+
|
|
27
|
+
for commit in commits:
|
|
28
|
+
message = Git("show", commit, patch=False, format="%s").firstline()
|
|
29
|
+
email = Git("show", commit, patch=False, format="%ae").firstline()
|
|
30
|
+
username = email[: email.index("@")]
|
|
31
|
+
index = message.index(":")
|
|
32
|
+
commit_type = message[:index]
|
|
33
|
+
commit_message = message[index + 1 :]
|
|
34
|
+
|
|
35
|
+
content += f"- **{commit_type}**: {commit_message} [[{username}](mailto:{email})] ({commit})\n"
|
|
36
|
+
|
|
37
|
+
return content
|
|
38
|
+
|
|
39
|
+
def generate_footer(self):
|
|
40
|
+
return """
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def generate_entry(self, title: str, base: str, branch: str):
|
|
47
|
+
return (
|
|
48
|
+
self.generate_header(title)
|
|
49
|
+
+ self.generate_content(base, branch)
|
|
50
|
+
+ self.generate_footer()
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def _prepend(self, content: str):
|
|
54
|
+
if isfile(self.FILENAME):
|
|
55
|
+
with open(self.FILENAME, "r") as f:
|
|
56
|
+
content += f.read()
|
|
57
|
+
|
|
58
|
+
with open(self.FILENAME, "w") as f:
|
|
59
|
+
f.write(content)
|
|
File without changes
|
git_flow/command/base.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from argparse import Namespace, ArgumentParser
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from os.path import isfile
|
|
4
|
+
|
|
5
|
+
from git_flow import (
|
|
6
|
+
BRANCH_TYPES,
|
|
7
|
+
COLOR_BLUE,
|
|
8
|
+
COLOR_GREEN,
|
|
9
|
+
COLOR_RED,
|
|
10
|
+
COLOR_RESET,
|
|
11
|
+
COLOR_YELLOW,
|
|
12
|
+
REPOSITORY_TOKEN_FILENAME,
|
|
13
|
+
GitFlowError,
|
|
14
|
+
)
|
|
15
|
+
from git_flow.git import FLOWCONFIG_FILE, Git
|
|
16
|
+
from git_flow.remote.base import RemoteAPI
|
|
17
|
+
from git_flow.remote.bitbucket import BitbucketRemoteAPI
|
|
18
|
+
from git_flow.remote.github import GithubRemoteAPI
|
|
19
|
+
|
|
20
|
+
TYPE_SUCCESS = 0
|
|
21
|
+
TYPE_WARNING = 1
|
|
22
|
+
TYPE_ERROR = 2
|
|
23
|
+
TYPE_INFO = 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Command(ABC):
|
|
27
|
+
flowconfig: dict[str, str]
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def description(self) -> str:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def run(self, args: Namespace = Namespace()):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def init(self):
|
|
42
|
+
self.flowconfig = (
|
|
43
|
+
Git.get_config(FLOWCONFIG_FILE) if isfile(FLOWCONFIG_FILE) else {}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def success(self, msg: str):
|
|
47
|
+
self._print(TYPE_SUCCESS, msg)
|
|
48
|
+
|
|
49
|
+
def warning(self, msg: str):
|
|
50
|
+
self._print(TYPE_WARNING, msg)
|
|
51
|
+
|
|
52
|
+
def error(self, msg: str):
|
|
53
|
+
self._print(TYPE_ERROR, msg)
|
|
54
|
+
|
|
55
|
+
def info(self, msg: str):
|
|
56
|
+
self._print(TYPE_INFO, msg)
|
|
57
|
+
|
|
58
|
+
def prompt(
|
|
59
|
+
self,
|
|
60
|
+
prompt: str,
|
|
61
|
+
default: str | None = None,
|
|
62
|
+
persistent: bool = False,
|
|
63
|
+
strip: bool = True,
|
|
64
|
+
) -> str:
|
|
65
|
+
onetime = not persistent
|
|
66
|
+
|
|
67
|
+
while onetime or persistent:
|
|
68
|
+
if default:
|
|
69
|
+
prompt += f" [{default}]"
|
|
70
|
+
|
|
71
|
+
prompt += ": "
|
|
72
|
+
result = input(prompt)
|
|
73
|
+
result = result.strip() if strip else result
|
|
74
|
+
|
|
75
|
+
if result:
|
|
76
|
+
return result
|
|
77
|
+
elif default is not None:
|
|
78
|
+
return default
|
|
79
|
+
elif persistent:
|
|
80
|
+
self.error("Debe ingresar un valor no vacío.")
|
|
81
|
+
else:
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
def confirm(self, question: str, default: bool = True):
|
|
85
|
+
suffix = " [Y/n]: " if default else " [y/N]: "
|
|
86
|
+
answer = input(question + suffix)
|
|
87
|
+
|
|
88
|
+
return default if len(answer) == 0 else answer.startswith("y")
|
|
89
|
+
|
|
90
|
+
def choice(self, prompt: str, options: list[str]):
|
|
91
|
+
print(prompt)
|
|
92
|
+
|
|
93
|
+
for i, option in enumerate(options):
|
|
94
|
+
print(f"\t{i+1}. {option}")
|
|
95
|
+
|
|
96
|
+
selection = None
|
|
97
|
+
|
|
98
|
+
while selection is None:
|
|
99
|
+
answer = input(f"Seleccione una opción [1-{len(options)}] o escribala: ")
|
|
100
|
+
|
|
101
|
+
if answer.isdigit():
|
|
102
|
+
answer = int(answer)
|
|
103
|
+
|
|
104
|
+
if 1 <= answer and answer <= len(options):
|
|
105
|
+
selection = options[answer - 1]
|
|
106
|
+
|
|
107
|
+
if not self.confirm(
|
|
108
|
+
f"Seleccionó la opción {answer} ({selection}), ¿es correcto?"
|
|
109
|
+
):
|
|
110
|
+
selection = None
|
|
111
|
+
else:
|
|
112
|
+
self.error(f"La opción {answer} está fuera del rango permitido.")
|
|
113
|
+
elif answer in options:
|
|
114
|
+
selection = answer
|
|
115
|
+
else:
|
|
116
|
+
self.error(f"La opción '{answer}' es inválida.")
|
|
117
|
+
|
|
118
|
+
return selection
|
|
119
|
+
|
|
120
|
+
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
|
|
121
|
+
return parser
|
|
122
|
+
|
|
123
|
+
def ensure_initialized(self):
|
|
124
|
+
initialized = self.flowconfig["flow.initialized"] if self.flowconfig else None
|
|
125
|
+
|
|
126
|
+
if not initialized:
|
|
127
|
+
raise GitFlowError(
|
|
128
|
+
"El repositorio no fue inicializado, debe ejecutar el comando `init`."
|
|
129
|
+
)
|
|
130
|
+
elif initialized != "true":
|
|
131
|
+
raise GitFlowError(
|
|
132
|
+
f"El valor de `flow.initialized` ({initialized}) es inválido."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def ensure_right_branch(self):
|
|
136
|
+
branch = Git.get_current_branch()
|
|
137
|
+
|
|
138
|
+
if branch == "HEAD":
|
|
139
|
+
raise GitFlowError("No se encuentra parado sobre una rama.")
|
|
140
|
+
elif self.confirm(f"Se encuentra sobre la rama '{branch}'. ¿Es correcto?"):
|
|
141
|
+
return branch
|
|
142
|
+
else:
|
|
143
|
+
raise GitFlowError("Ejecución cancelada.")
|
|
144
|
+
|
|
145
|
+
def ensure_repository_token(self):
|
|
146
|
+
if not isfile(REPOSITORY_TOKEN_FILENAME):
|
|
147
|
+
raise GitFlowError(
|
|
148
|
+
"No existe un token para el repositorio en " + REPOSITORY_TOKEN_FILENAME
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
with open(REPOSITORY_TOKEN_FILENAME) as f:
|
|
152
|
+
return f.readline().strip()
|
|
153
|
+
|
|
154
|
+
def ensure_clean_worktree(self, has_remote: bool):
|
|
155
|
+
status = Git.status()
|
|
156
|
+
|
|
157
|
+
if not status:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
not_empty = any(map(lambda s: s.index != " " or s.worktree != " ", status))
|
|
161
|
+
|
|
162
|
+
if not_empty:
|
|
163
|
+
if not has_remote:
|
|
164
|
+
self.warning("Existen cambios en tu entorno de trabajo sin commitear.")
|
|
165
|
+
else:
|
|
166
|
+
raise GitFlowError("No se puede continuar con cambios pendientes.")
|
|
167
|
+
|
|
168
|
+
if not self.confirm("¿Desea continuar?", False):
|
|
169
|
+
raise GitFlowError("Ejecución abortada")
|
|
170
|
+
|
|
171
|
+
def get_branch_env_and_type(self, branch: str) -> tuple[str, str]:
|
|
172
|
+
components = branch.split("/")
|
|
173
|
+
target_branches = self.flowconfig["flow.branches"].split(",")
|
|
174
|
+
|
|
175
|
+
if len(components) == 2:
|
|
176
|
+
if components[0] not in BRANCH_TYPES:
|
|
177
|
+
raise GitFlowError(
|
|
178
|
+
f"Branch inválida, el tipo '{components[0]}' no es válido."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return (target_branches[0], components[0])
|
|
182
|
+
elif len(components) == 4:
|
|
183
|
+
target_branches = target_branches[1:]
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
components[0] != "release"
|
|
187
|
+
or components[1] not in target_branches
|
|
188
|
+
or components[2] not in BRANCH_TYPES
|
|
189
|
+
):
|
|
190
|
+
raise GitFlowError(
|
|
191
|
+
"Branch release inválido, debe tener el siguiente formato: "
|
|
192
|
+
"release/<env>/<type>/<name>, pero es: " + branch
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return (components[1], components[2])
|
|
196
|
+
else:
|
|
197
|
+
raise GitFlowError("Branch inválido: " + branch)
|
|
198
|
+
|
|
199
|
+
def get_remote_api(self, token: str) -> RemoteAPI:
|
|
200
|
+
if "flow.remote" not in self.flowconfig:
|
|
201
|
+
raise GitFlowError("El repositorio no tiene configurado un remoto.")
|
|
202
|
+
|
|
203
|
+
[host, repository] = RemoteAPI.parse(self.flowconfig["flow.remote"])
|
|
204
|
+
|
|
205
|
+
if host == "bitbucket.org":
|
|
206
|
+
return BitbucketRemoteAPI(repository, token)
|
|
207
|
+
elif host == "github.com":
|
|
208
|
+
return GithubRemoteAPI(repository, token)
|
|
209
|
+
else:
|
|
210
|
+
raise GitFlowError("El host del repositorio remoto es inválido")
|
|
211
|
+
|
|
212
|
+
def _print(self, type: int, msg: str):
|
|
213
|
+
print(self._get_color(type) + self._get_tag(type) + " " + msg + COLOR_RESET)
|
|
214
|
+
|
|
215
|
+
def _get_color(self, type: int) -> str:
|
|
216
|
+
if type == TYPE_SUCCESS:
|
|
217
|
+
return COLOR_GREEN
|
|
218
|
+
elif type == TYPE_WARNING:
|
|
219
|
+
return COLOR_YELLOW
|
|
220
|
+
elif type == TYPE_ERROR:
|
|
221
|
+
return COLOR_RED
|
|
222
|
+
elif type == TYPE_INFO:
|
|
223
|
+
return COLOR_BLUE
|
|
224
|
+
return ""
|
|
225
|
+
|
|
226
|
+
def _get_tag(self, type: int) -> str:
|
|
227
|
+
if type == TYPE_SUCCESS:
|
|
228
|
+
return "[success]"
|
|
229
|
+
elif type == TYPE_WARNING:
|
|
230
|
+
return "[warning]"
|
|
231
|
+
elif type == TYPE_ERROR:
|
|
232
|
+
return "[error]"
|
|
233
|
+
elif type == TYPE_INFO:
|
|
234
|
+
return "[info]"
|
|
235
|
+
|
|
236
|
+
return ""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from argparse import ArgumentParser, Namespace
|
|
2
|
+
from git_flow import COLOR_WHITE_BOLD, COLOR_GREEN, COLOR_RED, COLOR_RESET, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
|
|
3
|
+
from git_flow.command.base import Command
|
|
4
|
+
from git_flow.git import Git
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
BRANCH_FORMAT = "%(refname:short)"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BranchCommand(Command):
|
|
11
|
+
def name(self) -> str:
|
|
12
|
+
return "branch"
|
|
13
|
+
|
|
14
|
+
def description(self) -> str:
|
|
15
|
+
return """Lista ramas del repositorio, agrupandolas por entorno objetivo"""
|
|
16
|
+
|
|
17
|
+
def setup_parser(self, parser: ArgumentParser) -> ArgumentParser:
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"environment",
|
|
20
|
+
nargs="?",
|
|
21
|
+
help="Entorno de las ramas a listar. Por defecto, es el entorno actual.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--trash",
|
|
26
|
+
action="store_true",
|
|
27
|
+
help=f"Listar ramas en la 'papelera de reciclaje' (ramas que empiezan con '{TRASH_BRANCH_PREFIX}')"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--wip",
|
|
32
|
+
action="store_true",
|
|
33
|
+
help=f"Listar ramas 'en progreso' (ramas que empiezan con '{WIP_BRANCH_PREFIX}')"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--all",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help=f"Listar ramas de todos los entornos"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return parser
|
|
43
|
+
|
|
44
|
+
def run(self, args: Namespace = Namespace()):
|
|
45
|
+
self.ensure_initialized()
|
|
46
|
+
|
|
47
|
+
self.current_branch = Git.get_current_branch()
|
|
48
|
+
self.environments = self.flowconfig["flow.branches"].split(",")
|
|
49
|
+
|
|
50
|
+
if self.current_branch in self.environments:
|
|
51
|
+
current_environment = self.current_branch
|
|
52
|
+
else:
|
|
53
|
+
(current_environment, _) = self.get_branch_env_and_type(self.current_branch)
|
|
54
|
+
|
|
55
|
+
if args.trash:
|
|
56
|
+
self.show_branches(TRASH_BRANCH_PREFIX)
|
|
57
|
+
elif args.wip:
|
|
58
|
+
self.show_branches(WIP_BRANCH_PREFIX)
|
|
59
|
+
elif args.all:
|
|
60
|
+
self.show_envs()
|
|
61
|
+
self.show_all_branches()
|
|
62
|
+
elif args.environment is None:
|
|
63
|
+
self.show_envs()
|
|
64
|
+
self.show_env_branches(current_environment)
|
|
65
|
+
elif args.environment in self.environments:
|
|
66
|
+
self.show_envs()
|
|
67
|
+
self.show_env_branches(args.environment)
|
|
68
|
+
else:
|
|
69
|
+
raise GitFlowError(f"'{args.environment}' no es un entorno válido.")
|
|
70
|
+
|
|
71
|
+
def show_branches(self, prefix: str):
|
|
72
|
+
branches = self.get_branches(prefix)
|
|
73
|
+
|
|
74
|
+
if not branches:
|
|
75
|
+
print("No hay ramas a mostrar.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
for branch in branches:
|
|
79
|
+
prefix = (COLOR_RED + "*") if branch == self.current_branch else " "
|
|
80
|
+
print(prefix + " " + branch + COLOR_RESET)
|
|
81
|
+
|
|
82
|
+
def get_branches(self, prefix: str):
|
|
83
|
+
return Git("for-each-ref", "refs/heads/" + prefix, format=BRANCH_FORMAT).lines()
|
|
84
|
+
|
|
85
|
+
def show_all_branches(self):
|
|
86
|
+
for environment in self.environments:
|
|
87
|
+
self.show_env_branches(environment, len(self.environments) > 1)
|
|
88
|
+
|
|
89
|
+
trash = len(self.get_branches(TRASH_BRANCH_PREFIX))
|
|
90
|
+
|
|
91
|
+
if trash:
|
|
92
|
+
print()
|
|
93
|
+
print(COLOR_WHITE_BOLD + "Papelera: " + COLOR_RESET + str(trash) + " rama" + ("" if trash == 1 else "s"))
|
|
94
|
+
|
|
95
|
+
def show_envs(self):
|
|
96
|
+
print(COLOR_WHITE_BOLD + "Entornos" + COLOR_RESET)
|
|
97
|
+
for environment in self.environments:
|
|
98
|
+
prefix = (COLOR_GREEN + "*") if environment == self.current_branch else " "
|
|
99
|
+
print(prefix + " " + environment + COLOR_RESET)
|
|
100
|
+
print()
|
|
101
|
+
|
|
102
|
+
def show_env_branches(self, environment: str, show_env: bool = False):
|
|
103
|
+
is_first_env = environment == self.environments[0]
|
|
104
|
+
pattern = "refs/heads/" if is_first_env else "refs/heads/release/" + environment + "/"
|
|
105
|
+
pattern += "*/*"
|
|
106
|
+
exclude = ["refs/heads/" + TRASH_BRANCH_PREFIX, "refs/heads/release/"] if is_first_env else []
|
|
107
|
+
branches = Git("for-each-ref", pattern, exclude=exclude, format=BRANCH_FORMAT).lines()
|
|
108
|
+
|
|
109
|
+
print(COLOR_WHITE_BOLD + (environment if show_env else "Ramas") + COLOR_RESET)
|
|
110
|
+
|
|
111
|
+
if not branches:
|
|
112
|
+
print("No hay ramas a mostrar.")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
for branch in branches:
|
|
116
|
+
prefix = (COLOR_GREEN + "*") if branch == self.current_branch else " "
|
|
117
|
+
print(prefix + " " + branch + COLOR_RESET)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from argparse import Namespace
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from git_flow import COMMIT_TYPES, TRASH_BRANCH_PREFIX, WIP_BRANCH_PREFIX, GitFlowError
|
|
4
|
+
from git_flow.command.base import Command
|
|
5
|
+
from git_flow.git import Git
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommitCommand(Command):
|
|
9
|
+
def name(self) -> str:
|
|
10
|
+
return "commit"
|
|
11
|
+
|
|
12
|
+
def description(self) -> str:
|
|
13
|
+
return """Crea un commit siguiendo el formato de Conventional Commits"""
|
|
14
|
+
|
|
15
|
+
def run(self, args: Namespace = Namespace()):
|
|
16
|
+
self.ensure_initialized()
|
|
17
|
+
branch = self.ensure_right_branch()
|
|
18
|
+
|
|
19
|
+
if not self._has_files_staged():
|
|
20
|
+
self.warning("No hay cambios en el indice para commitear.")
|
|
21
|
+
if self.confirm("¿Desea agregar la carpeta actual?"):
|
|
22
|
+
Git("add", ".").exec(print="Agregando cambios")
|
|
23
|
+
else:
|
|
24
|
+
raise GitFlowError(
|
|
25
|
+
"Debe agregar algún cambio al indice para continuar."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
commit_type = self.choice("Tipo de commit", COMMIT_TYPES)
|
|
29
|
+
commit_message = self.prompt(
|
|
30
|
+
"Mensaje (máximo recomendado: 100 caracteres)", persistent=True
|
|
31
|
+
)
|
|
32
|
+
message = commit_type + ": " + commit_message
|
|
33
|
+
|
|
34
|
+
if branch.startswith(WIP_BRANCH_PREFIX):
|
|
35
|
+
original_branch = branch.removeprefix(WIP_BRANCH_PREFIX)
|
|
36
|
+
Git("commit", m=message).exec(print="Creando commit en rama WIP")
|
|
37
|
+
|
|
38
|
+
if commit_type != "wip":
|
|
39
|
+
suffix = datetime.now().strftime("%Y%m%dT%H%M")
|
|
40
|
+
Git("switch", original_branch).exec(print="Volviendo a rama original")
|
|
41
|
+
Git("merge", branch, squash=True).exec(print="Squasheando commits WIP en uno solo")
|
|
42
|
+
Git("commit", m=message).exec(print="Creando commit final de rama WIP")
|
|
43
|
+
Git("branch", branch, TRASH_BRANCH_PREFIX + branch + "/" + suffix, move=True).exec(print="Backup de rama WIP")
|
|
44
|
+
else:
|
|
45
|
+
if commit_type == "wip":
|
|
46
|
+
Git("switch", WIP_BRANCH_PREFIX + branch, create=True).exec(print="Creando nueva rama WIP")
|
|
47
|
+
|
|
48
|
+
Git("commit", m=message).exec(print="Creando commit")
|
|
49
|
+
|
|
50
|
+
def _has_files_staged(self):
|
|
51
|
+
status = Git.status()
|
|
52
|
+
|
|
53
|
+
if not status:
|
|
54
|
+
raise GitFlowError("No hay cambios para commitear.")
|
|
55
|
+
|
|
56
|
+
files_in_index = []
|
|
57
|
+
|
|
58
|
+
for s in status:
|
|
59
|
+
if s.worktree != "?" and s.worktree != " ":
|
|
60
|
+
files_in_index.append(s.file)
|
|
61
|
+
|
|
62
|
+
return len(files_in_index) > 0
|
git_flow/command/init.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from argparse import Namespace
|
|
2
|
+
from git_flow import GitFlowError
|
|
3
|
+
from git_flow.command.base import Command
|
|
4
|
+
from git_flow.git import FLOWCONFIG_FILE, Git
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InitCommand(Command):
|
|
8
|
+
def name(self) -> str:
|
|
9
|
+
return "init"
|
|
10
|
+
|
|
11
|
+
def description(self) -> str:
|
|
12
|
+
return """Inicializa el repositorio para utilizar git-flow"""
|
|
13
|
+
|
|
14
|
+
def run(self, args: Namespace = Namespace()):
|
|
15
|
+
self._ensure_is_repository()
|
|
16
|
+
self._ensure_not_already_initialized()
|
|
17
|
+
|
|
18
|
+
branches = self._setup_flow_branches()
|
|
19
|
+
remote = self._setup_flow_remote()
|
|
20
|
+
|
|
21
|
+
flowconfig = {
|
|
22
|
+
"flow.initialized": "true",
|
|
23
|
+
"flow.branches": ",".join(branches),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if remote:
|
|
27
|
+
flowconfig["flow.remote"] = remote
|
|
28
|
+
|
|
29
|
+
Git.set_config(flowconfig, FLOWCONFIG_FILE)
|
|
30
|
+
Git("add", FLOWCONFIG_FILE).exec()
|
|
31
|
+
Git("commit", message="feature: initialize git-flow").exec()
|
|
32
|
+
|
|
33
|
+
self._ensure_all_flow_branches_exist(branches, remote)
|
|
34
|
+
|
|
35
|
+
def _ensure_is_repository(self):
|
|
36
|
+
if not Git.is_repository():
|
|
37
|
+
self.warning("El directorio actual no es un repositorio.")
|
|
38
|
+
|
|
39
|
+
if not self.confirm("¿Desea inicializarlo?"):
|
|
40
|
+
raise GitFlowError(
|
|
41
|
+
"No se puede continuar sin inicializar el repositorio"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
Git("init").exec()
|
|
45
|
+
|
|
46
|
+
def _ensure_not_already_initialized(self):
|
|
47
|
+
if not self.flowconfig:
|
|
48
|
+
return
|
|
49
|
+
elif self.flowconfig.get("flow.initialized") == "true":
|
|
50
|
+
raise GitFlowError("El repositorio ya fue inicializado para usar git-flow")
|
|
51
|
+
else:
|
|
52
|
+
raise GitFlowError("Valor de 'flow.initialized' es inválido")
|
|
53
|
+
|
|
54
|
+
def _setup_flow_branches(self):
|
|
55
|
+
self.info(
|
|
56
|
+
"""Ingrese las ramas que representan los entornos de deploy del proyecto en
|
|
57
|
+
orden creciente de cercanía al entorno productivo, y separados por coma.
|
|
58
|
+
Por ejemplo: "dev,test,prod"."""
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
branches = list(
|
|
62
|
+
map(lambda b: b.strip(), self.prompt("Ramas", "main").split(","))
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not all(branches):
|
|
66
|
+
raise GitFlowError("No puede ingresar una rama vacia")
|
|
67
|
+
|
|
68
|
+
return branches
|
|
69
|
+
|
|
70
|
+
def _setup_flow_remote(self):
|
|
71
|
+
self.info(
|
|
72
|
+
"""Puede elegir o crear un repositorio remoto para generar automáticamente PRs"""
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
remote = None
|
|
76
|
+
|
|
77
|
+
if self.confirm("¿Configurar repositorio remoto?"):
|
|
78
|
+
remotes = Git("remote").lines(print="Listando remotos disponibles")
|
|
79
|
+
|
|
80
|
+
if not remotes:
|
|
81
|
+
self.info("No tiene ningún repositorio remoto, se creará uno.")
|
|
82
|
+
|
|
83
|
+
url = self.prompt(
|
|
84
|
+
"Ingrese la URL del repositorio (e.g. https://github.com/user/repo.git)",
|
|
85
|
+
persistent=True,
|
|
86
|
+
)
|
|
87
|
+
remote = "origin"
|
|
88
|
+
Git("remote", "add", remote, url).exec(print="Agregando remoto")
|
|
89
|
+
elif len(remotes) == 1:
|
|
90
|
+
remote = remotes[0]
|
|
91
|
+
else:
|
|
92
|
+
self.info("Tiene más de un remoto, seleccione el que va a utilizar.")
|
|
93
|
+
remote = self.choice("Remoto", remotes)
|
|
94
|
+
|
|
95
|
+
return remote
|
|
96
|
+
|
|
97
|
+
def _ensure_all_flow_branches_exist(self, branches: list[str], remote: str | None):
|
|
98
|
+
existing_branches = Git.get_branches()
|
|
99
|
+
|
|
100
|
+
if not all(map(lambda b: b in existing_branches, branches)):
|
|
101
|
+
self.warning("Algunas de las ramas de entornos expecificadas no existen.")
|
|
102
|
+
self.info("Debe indicar sobre que rama se crearán las ramas de entorno.")
|
|
103
|
+
|
|
104
|
+
target_branch = self.choice("Rama", existing_branches)
|
|
105
|
+
|
|
106
|
+
for branch in branches:
|
|
107
|
+
if branch not in existing_branches:
|
|
108
|
+
Git("branch", branch, target_branch).exec(
|
|
109
|
+
print="Creando rama inexistente"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if remote:
|
|
113
|
+
Git("push", remote, branch, set_upstream=True).exec(
|
|
114
|
+
print="Creando rama en remoto"
|
|
115
|
+
)
|