gitflow-manager 1.0.1__tar.gz

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.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitflow_manager
3
+ Version: 1.0.1
4
+ Summary: This package contains the Anacision Git Manager.
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: gitpython>=3.1.24
8
+ Requires-Dist: pystache>=0.6.0
9
+ Requires-Dist: requests>=2.27.1
10
+ Requires-Dist: toml>=0.10.2
11
+
12
+ # Anacision Gitflow Manager
13
+
14
+ ## Description
15
+
16
+ The gitflow manager is a tool that helps maintaining clean versioning and documentation in code projects.
17
+ By using the gitflow manager one can make sure to stick to the
18
+ [GitFlow](https://nvie.com/posts/a-successful-git-branching-model) rules. Furthermore, it helps maintain a good
19
+ changelog and documentation.
20
+ Whenever you want to start a new branch or merging it back to dev/main run the `gfm` command.
21
+ It uses a dialog to define the type of new branch (feature, bugfix, hotfix, release) or merge option as well as setting
22
+ the required changelog messages.
23
+ It then automatically updates version numbers, checks out corresponding branches and commits/merges according to the
24
+ GitFlow.
25
+ Changes are also pushed to the Git Host via ssh or https and for protected branches merge requests are initialized.
26
+ Currently, the gitflow manager supports Gitlab and Bitbucket as hosting platforms.
27
+
28
+ ### Versioning
29
+
30
+ - Utilize the gitflow manager in projects to make sure that you easily stick to these rules:
31
+ - Versions are defined in the format *Major.Minor.Hotfix*. New versions are created when opening the hotfix or release
32
+ branch.
33
+ - The hotfix branch is only allowed to be started from main branch, to increase the Hotfix version digit only and to
34
+ be merged into main (+ dev thereafter).
35
+ - The release branch is only allowed to be started from dev, to increase the minor or major version digit and to be
36
+ merged into main (+ dev thereafter). In case of a new major version, the minor and hotfix digit is reset to 0. In case
37
+ of a new minor version, the hotfix digit is reset to 0.
38
+ - The bugfix and feature branches are only allowed to be started from dev, increase no version number and are merged
39
+ back to dev.
40
+ - All new branches need a description in the change log, which has separate "Added", "Changed" and "Fixed" sections.
41
+
42
+ ### Supported branches with gitflow manager
43
+
44
+ - **main**: Permanent, stable, (normally) protected branch used for deployment. Each commit has a new version. Merge
45
+ requests only come from release or hotfix branch.
46
+ - **dev**: Permanent development branch. Gets merge requests from feature, hotfix and release branch.
47
+ - **release/vX.X.X**: Release branch. Branched off from dev branch with new minor or major version. When the branch is
48
+ finished, it is merged into main and dev branch. Intermediate merges into dev are allowed, too. Merges from dev into
49
+ release branch are not allowed.
50
+ - **feature/xxxxxx**: For features to be developed. Branched off from dev branch and will be merged back into dev after
51
+ finishing feature.
52
+ - **bugfix/xxxxxx**: For bugs to be fixed. Branched off from dev branch and will be merged back into dev after
53
+ finishing the fix.
54
+ - **hotfix/vX.X.X**: Hotfix branch for fixes from deployed code. Branched off from main branch with new hotfix
55
+ version. When done, is merged into main and dev branch.
56
+
57
+ ### Tagging
58
+
59
+ - Currently, tagging is not done automatically. You can configure yourself a CI pipeline, that does the job for you.
60
+
61
+ ## Getting started
62
+
63
+ ### Installation
64
+
65
+ 1. Install gitflow manager with pip: `pip install gitflow-manager`
66
+ 2. Restart terminal
67
+
68
+ ### Usage
69
+
70
+ - When using the first time, run `gfm --init` in the root of the project.
71
+ - Subsequently, just type `gfm` each time you need to branch or merge (or add change log information), and follow the
72
+ dialog
@@ -0,0 +1,61 @@
1
+ # Anacision Gitflow Manager
2
+
3
+ ## Description
4
+
5
+ The gitflow manager is a tool that helps maintaining clean versioning and documentation in code projects.
6
+ By using the gitflow manager one can make sure to stick to the
7
+ [GitFlow](https://nvie.com/posts/a-successful-git-branching-model) rules. Furthermore, it helps maintain a good
8
+ changelog and documentation.
9
+ Whenever you want to start a new branch or merging it back to dev/main run the `gfm` command.
10
+ It uses a dialog to define the type of new branch (feature, bugfix, hotfix, release) or merge option as well as setting
11
+ the required changelog messages.
12
+ It then automatically updates version numbers, checks out corresponding branches and commits/merges according to the
13
+ GitFlow.
14
+ Changes are also pushed to the Git Host via ssh or https and for protected branches merge requests are initialized.
15
+ Currently, the gitflow manager supports Gitlab and Bitbucket as hosting platforms.
16
+
17
+ ### Versioning
18
+
19
+ - Utilize the gitflow manager in projects to make sure that you easily stick to these rules:
20
+ - Versions are defined in the format *Major.Minor.Hotfix*. New versions are created when opening the hotfix or release
21
+ branch.
22
+ - The hotfix branch is only allowed to be started from main branch, to increase the Hotfix version digit only and to
23
+ be merged into main (+ dev thereafter).
24
+ - The release branch is only allowed to be started from dev, to increase the minor or major version digit and to be
25
+ merged into main (+ dev thereafter). In case of a new major version, the minor and hotfix digit is reset to 0. In case
26
+ of a new minor version, the hotfix digit is reset to 0.
27
+ - The bugfix and feature branches are only allowed to be started from dev, increase no version number and are merged
28
+ back to dev.
29
+ - All new branches need a description in the change log, which has separate "Added", "Changed" and "Fixed" sections.
30
+
31
+ ### Supported branches with gitflow manager
32
+
33
+ - **main**: Permanent, stable, (normally) protected branch used for deployment. Each commit has a new version. Merge
34
+ requests only come from release or hotfix branch.
35
+ - **dev**: Permanent development branch. Gets merge requests from feature, hotfix and release branch.
36
+ - **release/vX.X.X**: Release branch. Branched off from dev branch with new minor or major version. When the branch is
37
+ finished, it is merged into main and dev branch. Intermediate merges into dev are allowed, too. Merges from dev into
38
+ release branch are not allowed.
39
+ - **feature/xxxxxx**: For features to be developed. Branched off from dev branch and will be merged back into dev after
40
+ finishing feature.
41
+ - **bugfix/xxxxxx**: For bugs to be fixed. Branched off from dev branch and will be merged back into dev after
42
+ finishing the fix.
43
+ - **hotfix/vX.X.X**: Hotfix branch for fixes from deployed code. Branched off from main branch with new hotfix
44
+ version. When done, is merged into main and dev branch.
45
+
46
+ ### Tagging
47
+
48
+ - Currently, tagging is not done automatically. You can configure yourself a CI pipeline, that does the job for you.
49
+
50
+ ## Getting started
51
+
52
+ ### Installation
53
+
54
+ 1. Install gitflow manager with pip: `pip install gitflow-manager`
55
+ 2. Restart terminal
56
+
57
+ ### Usage
58
+
59
+ - When using the first time, run `gfm --init` in the root of the project.
60
+ - Subsequently, just type `gfm` each time you need to branch or merge (or add change log information), and follow the
61
+ dialog
@@ -0,0 +1 @@
1
+ __version__="1.0.1"
@@ -0,0 +1,280 @@
1
+ import json
2
+ import os
3
+ from copy import deepcopy
4
+ from datetime import datetime
5
+ from enum import Enum
6
+
7
+ import pystache
8
+
9
+
10
+ class Sections(Enum):
11
+ """
12
+ Enumerator to define possible change log sections
13
+ """
14
+
15
+ added = "Added"
16
+ changed = "Changed"
17
+ fixed = "Fixed"
18
+
19
+
20
+ class ChangeLog:
21
+ """
22
+ A class to create a formatted markdown change log from a mustache template and the changes as json
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ file: str = "docs/source/change_log.rst",
28
+ template_file: str = "docs/source/change_log.tpl",
29
+ compare_url=None,
30
+ version_prefix: str = "",
31
+ ):
32
+ """
33
+
34
+ :param file: File name of the change log file. Has to have the rst ending
35
+ :param template_file: File name of the change log template. Has to be in mustache format
36
+ :param compare_url: Git url that is used to compare commits/branches with each other. Mostly ends with /compare/
37
+ :param version_prefix: Optional prefix that will be added to the version string before creating the log data
38
+ """
39
+ assert file.endswith(".rst"), 'Change log file name has to end with ".rst"'
40
+ self.file_name = file
41
+ self.data = {}
42
+ if not os.path.isfile(self.file_name.replace(".rst", ".json")):
43
+ self.create_empty_json()
44
+ if not os.path.isfile(template_file):
45
+ self.create_default_tpl(template_file)
46
+ self.read_json()
47
+ self.compare_url = compare_url
48
+ self.version_prefix = version_prefix
49
+ with open(template_file) as fh:
50
+ self.template = fh.read()
51
+ self.renderer = pystache.Renderer()
52
+
53
+ @staticmethod
54
+ def create_default_tpl(template_file):
55
+ with open(template_file, "w") as fh:
56
+ fh.write(
57
+ "{{#general}}\n"
58
+ "{{{title}}}\n"
59
+ "------------------\n"
60
+ "{{{description}}}\n"
61
+ "{{/general}}\n\n"
62
+ "{{#versions}}\n"
63
+ "{{{version}}}\n"
64
+ "-------------\n"
65
+ "**Release Date:** {{{date}}}\n\n"
66
+ "{{#sections}}\n"
67
+ "{{{label}}}\n"
68
+ "~~~~~~~~~~~~~\n"
69
+ "{{#entries}}\n"
70
+ "- {{{message}}} [{{{author}}}]\n"
71
+ "{{/entries}}\n\n"
72
+ "{{/sections}}\n"
73
+ "{{/versions}}\n\n"
74
+ "Changes comparison\n"
75
+ "------------------\n"
76
+ "{{#version_comparison}}\n"
77
+ "- **[{{version}}]**: `<{{{url}}}>`_\n"
78
+ "{{/version_comparison}}\n"
79
+ )
80
+
81
+ def create_empty_json(self):
82
+ json_path = self.file_name.replace(".rst", ".json")
83
+ os.makedirs(os.path.dirname(json_path), exist_ok=True)
84
+ with open(json_path, "w") as fh:
85
+ json.dump({"versions": []}, fh, indent=4)
86
+
87
+ def read_json(self):
88
+ """
89
+ Read JSON data from a file and ensure version info is sorted in descending date order.
90
+
91
+ """
92
+ with open(self.file_name.replace(".rst", ".json")) as fh:
93
+ data = json.load(fh)
94
+ # assert version info sorted in descending date order
95
+ data["versions"] = sorted(
96
+ data["versions"],
97
+ key=lambda x: x["date"] if x["date"] != "" else "Z",
98
+ reverse=True,
99
+ )
100
+ self.data = data
101
+
102
+ def write_json(self):
103
+ with open(self.file_name.replace(".rst", ".json"), "w") as fh:
104
+ json.dump(self.data, fh, indent=4)
105
+
106
+ def get_version(self, version: str) -> None | dict:
107
+ """
108
+ Returns a copy of the change log for the requested version
109
+
110
+ :param version: The requested version as string
111
+ :return: The version's change log as dict
112
+ """
113
+ for version_dict in self.data["versions"]:
114
+ if version_dict["version"] == self.version_prefix + version:
115
+ return deepcopy(version_dict)
116
+ return None
117
+
118
+ @staticmethod
119
+ def ask_for_changes(user: str, sections: list[Sections], app_specific_changes, app_specific_text) -> list:
120
+ """
121
+ This method asks interactively for changes and returns a section list ready to be handed over to ChangeLog
122
+
123
+ :param user: Name of the user, who did the changes. Should be the git user name if possible
124
+ :param sections: A list of sections that should be added
125
+ :param app_specific_changes:
126
+ :param app_specific_text:
127
+ :return: A list of sections and changes to add into ChangeLog.
128
+ """
129
+ section_list = []
130
+ for section in sections:
131
+ assert isinstance(section, Sections), 'List entries have to be values from the Enum "Sections"!'
132
+ sec_string = section.value
133
+ entries = []
134
+ while True:
135
+ if not app_specific_changes:
136
+ answer = input(
137
+ f"Any (more) changes to document for section \n'{sec_string}'\n? "
138
+ f"(Type the change or enter nothing for continuing)\n"
139
+ )
140
+ else:
141
+ app_specific_text = app_specific_text.format(sec_string)
142
+ answer = input(app_specific_text)
143
+ if answer == "":
144
+ break
145
+ else:
146
+ entries.append({"author": user, "message": answer})
147
+ if entries:
148
+ section_list.append({"label": sec_string, "entries": entries})
149
+ return section_list
150
+
151
+ @staticmethod
152
+ def _add_sections(version_dict: dict, sections: dict) -> dict:
153
+ """
154
+ Add sections and their entries to an existing version dictionary.
155
+
156
+ :param version_dict: The versions as a dictionary to which sections and entries will be added.
157
+ :param sections: A dictionary containing sections and their entries to be added to the versions.
158
+ :return: The updated version dictionary with sections and entries.
159
+ """
160
+ label_dict = {}
161
+ for n, section in enumerate(version_dict["sections"]):
162
+ label_dict[section["label"]] = n
163
+ for section in sections:
164
+ label = section["label"]
165
+ if label not in label_dict:
166
+ # create section
167
+ version_dict["sections"].append({"label": label, "entries": []})
168
+ label_dict[label] = len(version_dict["sections"]) - 1
169
+ # add all messages of unrel_dicts section to this section
170
+ version_dict["sections"][label_dict[label]]["entries"].extend(section["entries"])
171
+ return version_dict
172
+
173
+ def create_new_version(self, new_version: str, new_sections=None):
174
+ """
175
+ Adds a new version to the change log
176
+
177
+ :param new_version: String of the new version, such as v0.5.2 . If the changes are still for an unreleased \
178
+ state, use None!
179
+ :param new_sections: A formatted list of new_sections, obtained from method ask_for_changes
180
+ """
181
+ if new_version is None or new_version=="Unreleased":
182
+ new_version = "Unreleased"
183
+ else:
184
+ new_version = self.version_prefix + new_version
185
+ date_ = datetime.now().strftime("%Y-%m-%d %H:%M") if new_version != "Unreleased" else ""
186
+
187
+ found = False
188
+ # search for existing version with same string (forbidden) or Unreleased tag (will be moved into new version)
189
+ for version_dict in self.data["versions"]:
190
+ # remember, self.data is a dict (i.e. mutable) so all changes directly apply to it
191
+ if version_dict["version"] == new_version and new_version != "Unreleased":
192
+ raise Exception(
193
+ f"Version {new_version} already exists! Use method add_to_version if you want to add "
194
+ f"sections to an existing version!"
195
+ )
196
+ elif version_dict["version"] == "Unreleased":
197
+ found = True
198
+ version_dict["version"] = new_version
199
+ version_dict["date"] = date_
200
+ if new_sections is not None:
201
+ # add new sections. No need to get result as dicts are mutable
202
+ self._add_sections(version_dict, new_sections)
203
+
204
+ if not found:
205
+ if new_sections is None:
206
+ raise Exception(
207
+ "No entry found for Unreleased version and no new version information added. "
208
+ "Adding empty new version is not allowed!"
209
+ )
210
+ else:
211
+ self.data["versions"].insert(0, {"version": new_version, "date": date_, "sections": new_sections})
212
+
213
+ # update the json file
214
+ self.write_json()
215
+
216
+ def add_to_version(self, version: str | None, new_sections):
217
+ """
218
+ Adds change logs to an existing version
219
+
220
+ :param version: String of the version to add changes to, such as v0.5.2 . If the changes are still for an \
221
+ unreleased state, use None!
222
+ :param new_sections: A formatted list of new_sections, obtained from method ask_for_changes
223
+ """
224
+ if version is None or version=="Unreleased":
225
+ version = "Unreleased"
226
+ else:
227
+ version = self.version_prefix + version
228
+ found = False
229
+ for version_dict in self.data["versions"]:
230
+ # remember, self.data is a dict (i.e. mutable) so all changes directly apply to it
231
+ if version_dict["version"] == version:
232
+ found = True
233
+ # add new sections. No need to get result as dicts are mutable
234
+ self._add_sections(version_dict, new_sections)
235
+
236
+ if not found:
237
+ if version == "Unreleased":
238
+ self.create_new_version(new_version=version, new_sections=new_sections)
239
+ else:
240
+ raise Exception(
241
+ f"No entry found for version {version}! If you want to create a new version, use method "
242
+ "create_new_version!"
243
+ )
244
+
245
+ # update the json file
246
+ self.write_json()
247
+
248
+ def _add_branch_comparison(self, data: dict):
249
+ """
250
+ Add a list of comparisons between commits/branches in data dictionary.
251
+ :param data: The dictionary to which the comparison info will be added, also contains the version info
252
+ :return: The updated data dictionary with branch comparison information.
253
+ """
254
+ if self.compare_url is not None:
255
+ if "version_comparison" in data:
256
+ data.pop("version_comparison")
257
+ # assumes the dict is ordered by date!!
258
+ newer_version = None
259
+ comparison_list = []
260
+ for version_dict in data["versions"]:
261
+ if newer_version is None:
262
+ newer_version = version_dict["version"]
263
+ else:
264
+ comparison_list.append(
265
+ {
266
+ "version": newer_version,
267
+ "url": f"{self.compare_url}/diff?targetBranch=tags/{version_dict['version']}&sourceBranch="
268
+ f"{'tags/'+newer_version if newer_version != 'Unreleased' else 'heads/dev'}",
269
+ }
270
+ )
271
+ data["version_comparison"] = comparison_list
272
+ return data
273
+
274
+ def write_log(self):
275
+ """
276
+ Write changes to the ChangeLog file.
277
+ """
278
+ extended_data = self._add_branch_comparison(self.data)
279
+ with open(self.file_name, "w") as fh:
280
+ fh.write(self.renderer.render(self.template, extended_data))
@@ -0,0 +1,2 @@
1
+ class GitFlowManagerError(Exception):
2
+ pass
@@ -0,0 +1,35 @@
1
+ import toml
2
+
3
+
4
+ def read_file(path: str):
5
+ """Read a file and return the content.
6
+
7
+ :param path: _description_
8
+ :type path: str
9
+ """
10
+ with open(path, encoding="utf8") as file:
11
+ file_content = file.read()
12
+ return file_content
13
+
14
+
15
+ def write_to_file(path: str, content):
16
+ """Write given content to the path.
17
+
18
+ :param path: _description_
19
+ :type path: str
20
+ :param content: _description_
21
+ :type content: _type_
22
+ """
23
+ with open(path, "w", encoding="utf8") as fhh:
24
+ fhh.write(content)
25
+
26
+
27
+ def read_toml(path):
28
+ with open(path, encoding="utf8") as file:
29
+ content = toml.load(file)
30
+ return content
31
+
32
+
33
+ def write_to_toml(path: str, content):
34
+ with open(path, "w", encoding="utf8") as file:
35
+ toml.dump(content, file)