odoo-version-manager 0.0.10__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Marc Wimmer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.1
2
+ Name: odoo-version-manager
3
+ Version: 0.0.10
4
+ Summary: Manage Versions of module along all odoo version
5
+ Home-page: https://github.com/odoo-module-version-manager.git
6
+ Author: Marc-Christian Wimmer
7
+ Author-email: marc@zebroo.de
8
+ License: MIT
9
+ Requires-Python: >=3.9.0
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+
13
+
14
+ # odoo-version-manager
15
+
16
+ Helps handling updates on main branch of a module to be deployed to all sub branches.
17
+ It uses github workflows to accomplish this.
18
+
19
+ ## installing
20
+
21
+ ```bash
22
+ pipx install odoo-version-manager
23
+ odoo-version-manager completion -x
24
+ ```
25
+
26
+ # usage
27
+
28
+ ## initial setup
29
+
30
+ - create a repository like an OCA repository with some modules on branch **main**
31
+ - decide which version the main branch is for example 16.0
32
+
33
+ ```
34
+ odoo-version-manager setup 16.0
35
+ ```
36
+
37
+ ## set another odoo version for main branch
38
+
39
+ If you move on and main branch becomes odoo version 18.0 instead of 16.0 do the following:
40
+
41
+ ```
42
+ git checkout main
43
+ git reset --hard origin/18.0
44
+ odoo-version-manager setup 18.0
45
+ ```
@@ -0,0 +1,32 @@
1
+ # odoo-version-manager
2
+
3
+ Helps handling updates on main branch of a module to be deployed to all sub branches.
4
+ It uses github workflows to accomplish this.
5
+
6
+ ## installing
7
+
8
+ ```bash
9
+ pipx install odoo-version-manager
10
+ odoo-version-manager completion -x
11
+ ```
12
+
13
+ # usage
14
+
15
+ ## initial setup
16
+
17
+ - create a repository like an OCA repository with some modules on branch **main**
18
+ - decide which version the main branch is for example 16.0
19
+
20
+ ```
21
+ odoo-version-manager setup 16.0
22
+ ```
23
+
24
+ ## set another odoo version for main branch
25
+
26
+ If you move on and main branch becomes odoo version 18.0 instead of 16.0 do the following:
27
+
28
+ ```
29
+ git checkout main
30
+ git reset --hard origin/18.0
31
+ odoo-version-manager setup 18.0
32
+ ```
@@ -0,0 +1,50 @@
1
+ import click
2
+ import subprocess
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from .config import pass_config
7
+ from .config import Config
8
+ import shellingham
9
+ from click_default_group import DefaultGroup
10
+
11
+ global_data = {
12
+ 'config': None
13
+ }
14
+
15
+ @click.group()
16
+ @pass_config
17
+ def cli(config):
18
+ global_data['config'] = config
19
+
20
+
21
+
22
+ @cli.command()
23
+ def install_completion():
24
+ def setup_for_shell_generic(shell, shell_call):
25
+ path = Path(f"/etc/{shell}_completion.d")
26
+ NAME = shell_call.upper().replace("-", "_")
27
+ completion = subprocess.check_output([sys.argv[0]], env={f"_{NAME}_COMPLETE": f"{shell}_source"}, shell=True)
28
+ if path.exists():
29
+ if os.access(path, os.W_OK):
30
+ (path / shell_call).write_bytes(completion)
31
+ return
32
+
33
+ if not (path / shell_call).exists():
34
+ rc = Path(os.path.expanduser("~")) / f'.{shell}rc'
35
+ if not rc.exists():
36
+ return
37
+ complete_file = rc.parent / f'.{shell_call}-completion.sh'
38
+ complete_file.write_bytes(completion)
39
+ if complete_file.name not in rc.read_text():
40
+ content = rc.read_text()
41
+ content += '\nsource ~/' + complete_file.name
42
+ rc.write_text(content)
43
+
44
+ name = Path(sys.argv[0]).name
45
+ setup_for_shell_generic(shellingham.detect_shell()[0], name)
46
+ sys.exit(0)
47
+
48
+ from . import odoo_version_manager
49
+ from . import gitcommands
50
+ from . import repo
@@ -0,0 +1,86 @@
1
+ import atexit
2
+ import click
3
+ import logging
4
+ from pathlib import Path
5
+ import sys
6
+ import json
7
+ import os
8
+ from contextlib import contextmanager
9
+ import configparser
10
+ from paramiko.config import SSHConfig
11
+ import subprocess
12
+
13
+ BASE_PATH = Path(os.path.expanduser("~/.fetch_latest_file.d"))
14
+ SSH_CONFIG = Path(os.path.expanduser("~/.ssh/config"))
15
+
16
+ class Config(object):
17
+ def __init__(self):
18
+ super().__init__()
19
+ self.sources = {}
20
+ self.source = None
21
+ if BASE_PATH.exists():
22
+ for file in BASE_PATH.glob("*"):
23
+ if file.name.startswith('.'):
24
+ continue
25
+ for section, config in self.parse_file(file):
26
+ self.sources[section] = config
27
+
28
+ def cleanup():
29
+ pass
30
+
31
+ atexit.register(cleanup)
32
+
33
+ def parse_file(self, file):
34
+ config = configparser.ConfigParser()
35
+ config.read(file)
36
+ for section in config.sections():
37
+ yield (section, config[section])
38
+
39
+ @contextmanager
40
+ def shell(self):
41
+ config = self.get_source()
42
+
43
+ def execute(cmd):
44
+ output = subprocess.check_output([
45
+ "ssh", config['host'],
46
+ ] + cmd)
47
+ output = output.decode('utf-8').split("\n")
48
+ return output
49
+
50
+ yield config, execute
51
+
52
+ def add(self, filename, host, username, path, regex, destination):
53
+ path = BASE_PATH / Path(filename).name
54
+ config = configparser.ConfigParser()
55
+ BASE_PATH.mkdir(exist_ok=True, parents=True)
56
+ if path.exists():
57
+ config.read(path)
58
+ config[self.source] = {
59
+ "host": host,
60
+ "path": path,
61
+ "regex": regex,
62
+ "destination": destination,
63
+ }
64
+ if username:
65
+ config[self.source]['username'] = username
66
+ with open(path, 'w') as configfile:
67
+ config.write(configfile)
68
+
69
+ def get_source(self):
70
+ if not self.source:
71
+ raise Exception("Please define a source first!")
72
+ return self.sources[self.source]
73
+
74
+ def setup_logging(self):
75
+ FORMAT = '[%(levelname)s] %(asctime)s %(message)s'
76
+ formatter = logging.Formatter(FORMAT)
77
+ logging.basicConfig(format=FORMAT)
78
+ self.logger = logging.getLogger('') # root handler
79
+ self.logger.setLevel(self.log_level)
80
+
81
+ stdout_handler = logging.StreamHandler(sys.stdout)
82
+ self.logger.addHandler(stdout_handler)
83
+ stdout_handler.setFormatter(formatter)
84
+
85
+
86
+ pass_config = click.make_pass_decorator(Config, ensure=True)
@@ -0,0 +1,5 @@
1
+ gitcmd = ["git", "-c", "protocol.file.allow=always"]
2
+ odoo_versions = [11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0]
3
+
4
+ github_workflow_file = ".github/workflows/deploy_patches.yml"
5
+ version_behind_main_branch = ".github/version_behind_main_branch"
@@ -0,0 +1,82 @@
1
+ import os
2
+ import time
3
+ import errno
4
+
5
+
6
+ class FileLockException(Exception):
7
+ pass
8
+
9
+
10
+ class FileLock(object):
11
+ """A file locking mechanism that has context-manager support so
12
+ you can use it in a with statement. This should be relatively cross
13
+ compatible as it doesn't rely on msvcrt or fcntl for the locking.
14
+ """
15
+
16
+ def __init__(self, file_name, timeout=10, delay=0.05):
17
+ """Prepare the file locker. Specify the file to lock and optionally
18
+ the maximum timeout and the delay between each attempt to lock.
19
+ """
20
+ if timeout is not None and delay is None:
21
+ raise ValueError("If timeout is not None, then delay must not be None.")
22
+ self.is_locked = False
23
+ self.lockfile = os.path.join(os.getcwd(), "%s.lock" % file_name)
24
+ self.file_name = file_name
25
+ self.timeout = timeout
26
+ self.delay = delay
27
+
28
+ def acquire(self):
29
+ """Acquire the lock, if possible. If the lock is in use, it check again
30
+ every `wait` seconds. It does this until it either gets the lock or
31
+ exceeds `timeout` number of seconds, in which case it throws
32
+ an exception.
33
+ """
34
+ start_time = time.time()
35
+ while True:
36
+ try:
37
+ self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
38
+ self.is_locked = True # moved to ensure tag only when locked
39
+ break
40
+ except OSError as e:
41
+ if e.errno != errno.EEXIST:
42
+ raise
43
+ if self.timeout is None:
44
+ raise FileLockException(
45
+ "Could not acquire lock on {}".format(self.file_name)
46
+ )
47
+ if (time.time() - start_time) >= self.timeout:
48
+ raise FileLockException("Timeout occured.")
49
+ time.sleep(self.delay)
50
+
51
+ # self.is_locked = True
52
+
53
+ def release(self):
54
+ """Get rid of the lock by deleting the lockfile.
55
+ When working in a `with` statement, this gets automatically
56
+ called at the end.
57
+ """
58
+ if self.is_locked:
59
+ os.close(self.fd)
60
+ os.unlink(self.lockfile)
61
+ self.is_locked = False
62
+
63
+ def __enter__(self):
64
+ """Activated when used in the with statement.
65
+ Should automatically acquire a lock to be used in the with block.
66
+ """
67
+ if not self.is_locked:
68
+ self.acquire()
69
+ return self
70
+
71
+ def __exit__(self, type, value, traceback):
72
+ """Activated at the end of the with statement.
73
+ It automatically releases the lock if it isn't locked.
74
+ """
75
+ if self.is_locked:
76
+ self.release()
77
+
78
+ def __del__(self):
79
+ """Make sure that the FileLock instance doesn't leave a lockfile
80
+ lying around.
81
+ """
82
+ self.release()
@@ -0,0 +1,165 @@
1
+ import os
2
+ from pathlib import Path
3
+ from .tools import yieldlist, X, wait_git_lock
4
+ from .consts import gitcmd as git
5
+
6
+
7
+ class GitCommands(object):
8
+ def __init__(self, path=None):
9
+ self.path = Path(path or os.getcwd())
10
+ self.path_absolute = self.path.absolute()
11
+
12
+ @property
13
+ def configdir(self):
14
+ from .repo import Repo
15
+
16
+ stop_at = Repo(self.path_absolute).root_repo
17
+ here = self.path_absolute
18
+ while True:
19
+ default = here / ".git"
20
+ if default.exists() and default.is_dir():
21
+ return default
22
+ if default.is_file():
23
+ path = default.read_text().strip().split("gitdir:")[1].strip()
24
+ return (here / path).resolve()
25
+
26
+ if here == stop_at:
27
+ break
28
+ here = here.parent
29
+ raise Exception("Config dir not found")
30
+
31
+ def X(self, *params, allow_error=False, env=None, output=None):
32
+ if output is None:
33
+ output = False
34
+ with wait_git_lock(self.path_absolute):
35
+ kwparams = {
36
+ "output": output,
37
+ "allow_error": allow_error,
38
+ "env": env,
39
+ }
40
+ if self.path.exists():
41
+ # case not existing at recreating cache dir e.g.
42
+ kwparams["cwd"] = self.path
43
+ return X(*params, **kwparams)
44
+
45
+ def out(self, *params, allow_error=False, env=None):
46
+ return X(*params, output=True, cwd=self.path, allow_error=allow_error, env=env)
47
+
48
+ def _parse_git_status(self):
49
+ for line in X(
50
+ *(
51
+ git
52
+ + [
53
+ "status",
54
+ "--porcelain",
55
+ "--untracked-files=all",
56
+ ]
57
+ ),
58
+ cwd=self.path_absolute,
59
+ output=True,
60
+ ).splitlines():
61
+ # splits: A asdas
62
+ # M asdasd
63
+ # M asdsad
64
+ # ?? asasdasd
65
+ modifier = line[:2]
66
+ path = line.strip().split(" ", 1)[1].strip()
67
+ if path.startswith(".."):
68
+ continue
69
+
70
+ yield modifier, Path(path)
71
+
72
+ @property
73
+ @yieldlist
74
+ def staged_files(self):
75
+ for modifier, path in self._parse_git_status():
76
+ if modifier[0] in ["A", "M", "D"]:
77
+ yield path
78
+
79
+ @property
80
+ @yieldlist
81
+ def dirty_existing_files(self):
82
+ for modifier, path in self._parse_git_status():
83
+ if modifier[0] == "M" or modifier[1] == "M" or modifier[1] == "D":
84
+ yield path
85
+
86
+ @property
87
+ @yieldlist
88
+ def all_dirty_files(self):
89
+ return self.untracked_files + self.dirty_existing_files
90
+
91
+ @property
92
+ @yieldlist
93
+ def all_dirty_files_absolute(self):
94
+ res = self.untracked_files + self.dirty_existing_files
95
+ res = list(map(lambda x: self.path_absolute / x, res))
96
+ return res
97
+
98
+ @property
99
+ @yieldlist
100
+ def untracked_files(self):
101
+ for modifier, path in self._parse_git_status():
102
+ if modifier == "??" or modifier[0] == "A":
103
+ yield path
104
+
105
+ @property
106
+ @yieldlist
107
+ def untracked_files_absolute(self):
108
+ for file in self.untracked_files:
109
+ yield self.path_absolute / file
110
+
111
+ @property
112
+ def dirty(self):
113
+ return bool(list(self._parse_git_status()))
114
+
115
+ def is_submodule(self, path):
116
+ path = self._combine(path)
117
+ for line in X(
118
+ *(git + ["submodule", "status"]), output=True, cwd=self.path_absolute
119
+ ).splitlines():
120
+ line = line.strip()
121
+ _, _path, _ = line.split(" ", 2)
122
+ if _path == str(path.relative_to(self.path_absolute)):
123
+ return path
124
+
125
+ def _combine(self, path):
126
+ """
127
+ Makes a new path
128
+ """
129
+ path = self.path / path
130
+ path.relative_to(self.path)
131
+ return path
132
+
133
+ def output_status(self):
134
+ self.X(*(git + ["status"]))
135
+
136
+ def get_all_branches(self):
137
+ res = list(
138
+ map(
139
+ lambda x: x.strip(),
140
+ self.out(
141
+ *(git + ["for-each-ref", "--format=%(refname:short)", "refs/heads"])
142
+ ).splitlines(),
143
+ )
144
+ )
145
+ return res
146
+
147
+ @property
148
+ def dirty(self):
149
+ files = []
150
+ for modifier, path in self._parse_git_status():
151
+ if str(path) == "gimera.yml":
152
+ continue
153
+ files.append(path)
154
+ return bool(files)
155
+
156
+ def simple_commit_all(self, msg="."):
157
+ self.X(*(git + ["add", "."]))
158
+ self.X(*(git + ["commit", "--allow-empty", "-am", msg]))
159
+
160
+ @property
161
+ def hex(self):
162
+ return self.out(*(git + ["log", "-n", "1", "--pretty=%H"]))
163
+
164
+ def checkout(self, ref, force=False):
165
+ self.X(*(git + ["checkout", "-f" if force else None, ref]))