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 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
@@ -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
@@ -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
+ )