mud-git 0.0.post1.dev1__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.
mud/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from mud import utils, settings
4
+ from mud.app import App
5
+
6
+
7
+ def run():
8
+ try:
9
+ utils.settings = settings.Settings(utils.SETTINGS_FILE_NAME)
10
+
11
+ app = App()
12
+ app.run()
13
+ except KeyboardInterrupt:
14
+ utils.print_error('Stopped by user.', 0)
15
+
16
+
17
+ if __name__ == '__main__':
18
+ run()
mud/app.py ADDED
@@ -0,0 +1,269 @@
1
+ import os
2
+ import sys
3
+ import asyncio
4
+ import argparse
5
+ import subprocess
6
+
7
+ from argparse import ArgumentParser
8
+
9
+ from mud import config
10
+ from mud import utils
11
+ from mud.runner import Runner
12
+ from mud.styles import *
13
+ from mud.commands import *
14
+ from mud.utils import glyphs
15
+
16
+
17
+ class App:
18
+ def __init__(self):
19
+ self.cmd_runner = None
20
+ self.command = None
21
+ self.config = None
22
+ self.parser = self._create_parser()
23
+
24
+ @staticmethod
25
+ def _create_parser() -> ArgumentParser:
26
+ parser = argparse.ArgumentParser(description=f'{BOLD}mud{RESET} allows you to run commands in multiple repositories.')
27
+ subparsers = parser.add_subparsers(dest='command')
28
+
29
+ subparsers.add_parser(LOG[0], aliases=LOG[1:], help='Displays log of latest commit messages for all repositories in a table view.')
30
+ subparsers.add_parser(INFO[0], aliases=INFO[1:], help='Displays branch divergence and working directory changes')
31
+ subparsers.add_parser(INIT[0], aliases=INIT[1:], help=f'Initializes the {BOLD}.mudconfig{RESET} and adds all repositories in this directory to {BOLD}.mudconfig{RESET}.')
32
+ subparsers.add_parser(TAGS[0], aliases=TAGS[1:], help='Displays git tags in repositories.')
33
+ subparsers.add_parser(LABELS[0], aliases=LABELS[1:], help='Displays mud labels across repositories.')
34
+ subparsers.add_parser(STATUS[0], aliases=STATUS[1:], help='Displays working directory changes.')
35
+ subparsers.add_parser(BRANCHES[0], aliases=BRANCHES[1:], help='Displays all branches in repositories.')
36
+ subparsers.add_parser(REMOTE_BRANCHES[0], aliases=REMOTE_BRANCHES[1:], help='Displays all remote branches in repositories.')
37
+ subparsers.add_parser(CONFIGURE[0], aliases=CONFIGURE[1:], help='Runs the interactive configuration wizard.')
38
+
39
+ add_parser = subparsers.add_parser(ADD[0], aliases=ADD[1:], help='Adds repository or labels an existing repository.')
40
+ add_parser.add_argument('label', help='The label to add (optional).', nargs='?', default='', type=str)
41
+ add_parser.add_argument('path', help='Repository to add (optional).', nargs='?', type=str)
42
+
43
+ remove_parser = subparsers.add_parser(REMOVE[0], aliases=REMOVE[1:], help='Removes repository or removes the label from an existing repository.')
44
+ remove_parser.add_argument('label', help='Label to remove from repository (optional).', nargs='?', default='', type=str)
45
+ remove_parser.add_argument('path', help='Repository to remove (optional).', nargs='?', type=str)
46
+
47
+ parser.add_argument(*COMMAND_ATTR, metavar='COMMAND', nargs='?', default='', type=str, help=f'Explicit command argument. Use this when you want to run a command that has a special characters.')
48
+ parser.add_argument(*TABLE_ATTR, metavar='TABLE', nargs='?', default='', type=str, help=f'Switches table view, runs in table view it is disabled in {BOLD}.mudsettings{RESET}.')
49
+ parser.add_argument(*LABEL_PREFIX, metavar='LABEL', nargs='?', default='', type=str, help='Includes repositories with provided label.')
50
+ parser.add_argument(*NOT_LABEL_PREFIX, metavar='NOT_LABEL', nargs='?', default='', type=str, help=f'Excludes repositories with provided label..')
51
+ parser.add_argument(*BRANCH_PREFIX, metavar='BRANCH', nargs='?', default='', type=str, help='Includes repositories on a provided branch.')
52
+ parser.add_argument(*NOT_BRANCH_PREFIX, metavar='NOT_BRANCH', nargs='?', default='', type=str, help='Excludes repositories on a provided branch.')
53
+ parser.add_argument(*MODIFIED_ATTR, action='store_true', help='Filters modified repositories.')
54
+ parser.add_argument(*DIVERGED_ATTR, action='store_true', help='Filters repositories with diverged branches.')
55
+ parser.add_argument(*ASYNC_ATTR, action='store_true', help='Switches asynchronous run feature.')
56
+ parser.add_argument(SET_GLOBAL[0], help=f'Sets {BOLD}.mudconfig{RESET} in the current repository as your fallback {BOLD}.mudconfig{RESET}.', action='store_true')
57
+ parser.add_argument('catch_all', nargs='*', help='Type any commands to execute among repositories.')
58
+ return parser
59
+
60
+ def run(self) -> None:
61
+ # Displays default help message
62
+ if len(sys.argv) == 1 or sys.argv[1] in HELP:
63
+ utils.info()
64
+ print()
65
+ self.parser.print_help()
66
+ return
67
+ # Sets global repository in .mudsettings
68
+ if sys.argv[1] in SET_GLOBAL:
69
+ config_path = os.path.join(os.getcwd(), utils.CONFIG_FILE_NAME)
70
+ if os.path.exists(config_path):
71
+ utils.settings.config.set('mud', 'config_path', config_path)
72
+ utils.settings.save()
73
+ print(f'Current {BOLD}.mudconfig{RESET} set as a global configuration.')
74
+ return
75
+ # Runs configuration wizard
76
+ elif sys.argv[1] in CONFIGURE:
77
+ utils.configure()
78
+ return
79
+
80
+ current_directory = os.getcwd()
81
+ self.config = config.Config()
82
+
83
+ # Discovers repositories in current directory
84
+ if sys.argv[1] in INIT:
85
+ self.init(self.parser.parse_args())
86
+ return
87
+
88
+ self.config.find()
89
+ self._filter_with_arguments()
90
+
91
+ self.cmd_runner = Runner(self.config)
92
+ # Handling commands
93
+ if len(sys.argv) > 1 and sys.argv[1] in [cmd for group in COMMANDS for cmd in group]:
94
+ args = self.parser.parse_args()
95
+ if args.command in INIT:
96
+ os.chdir(current_directory)
97
+ self.init(args)
98
+ elif args.command in ADD:
99
+ self.add(args)
100
+ elif args.command in REMOVE:
101
+ self.remove(args)
102
+ else:
103
+ if len(self.repos) == 0:
104
+ utils.print_error('No repositories are matching this filter.', 1)
105
+ return
106
+ if args.command in INFO:
107
+ self.cmd_runner.info(self.repos)
108
+ elif args.command in LOG:
109
+ self.cmd_runner.log(self.repos)
110
+ elif args.command in REMOTE_BRANCHES:
111
+ self.cmd_runner.remote_branches(self.repos)
112
+ elif args.command in BRANCHES:
113
+ self.cmd_runner.branches(self.repos)
114
+ elif args.command in LABELS:
115
+ self.cmd_runner.labels(self.repos)
116
+ elif args.command in TAGS:
117
+ self.cmd_runner.tags(self.repos)
118
+ elif args.command in STATUS:
119
+ self.cmd_runner.status(self.repos)
120
+ # Handling subcommands
121
+ else:
122
+ del sys.argv[0]
123
+ if self.command is None:
124
+ if len(sys.argv) == 0:
125
+ self.parser.print_help()
126
+ return
127
+ self.command = ' '.join(sys.argv)
128
+ self._parse_aliases()
129
+ try:
130
+ if self.run_async:
131
+ if self.table:
132
+ asyncio.run(self.cmd_runner.run_async_table_view(self.repos.keys(), self.command))
133
+ else:
134
+ asyncio.run(self.cmd_runner.run_async(self.repos.keys(), self.command))
135
+ else:
136
+ self.cmd_runner.run_ordered(self.repos.keys(), self.command)
137
+ except Exception as exception:
138
+ utils.print_error(f'Invalid command. {exception}', 2)
139
+
140
+ def init(self, args) -> None:
141
+ table = utils.get_table(['Path', 'Status'])
142
+ self.config.data = {}
143
+ index = 0
144
+ directories = [d for d in os.listdir('.') if os.path.isdir(d) and os.path.isdir(os.path.join(d, '.git'))]
145
+ for directory in directories:
146
+ if directory in self.config.paths():
147
+ continue
148
+ self.config.add_label(directory, getattr(args, 'label', ''))
149
+ index += 1
150
+ table.add_row([f'{DIM}{directory}{RESET}', f'{GREEN}{glyphs("added")}{RESET}'])
151
+ if index == 0:
152
+ utils.print_error('No git repositories were found in this directory.', 3)
153
+ return
154
+ self.config.save(utils.CONFIG_FILE_NAME)
155
+ utils.print_table(table)
156
+
157
+ def add(self, args) -> None:
158
+ self.config.add_label(args.path, args.label)
159
+ self.config.save(utils.CONFIG_FILE_NAME)
160
+
161
+ def remove(self, args) -> None:
162
+ if args.path:
163
+ self.config.remove_label(args.path, args.label)
164
+ elif args.label:
165
+ self.config.remove_path(args.label)
166
+ else:
167
+ utils.print_error(f'Invalid input. Please provide a value to remove.', 4)
168
+ self.config.save(utils.CONFIG_FILE_NAME)
169
+
170
+ # Filter out repositories if user provided filters
171
+ def _filter_with_arguments(self) -> None:
172
+ self.repos = self.config.data
173
+ self.table = utils.settings.config['mud'].getboolean('run_table', fallback=True)
174
+ self.run_async = utils.settings.config['mud'].getboolean('run_async', fallback=True)
175
+
176
+ for path, labels in self.config.filter_label('ignore', self.config.data).items():
177
+ del self.repos[path]
178
+ include_labels = []
179
+ exclude_labels = []
180
+ include_branches = []
181
+ exclude_branches = []
182
+ modified = False
183
+ diverged = False
184
+ index = 1
185
+ while index < len(sys.argv):
186
+ arg = sys.argv[index]
187
+ if not arg.startswith('-'):
188
+ break
189
+ if any(arg.startswith(prefix) for prefix in LABEL_PREFIX):
190
+ include_labels.append(arg.split('=', 1)[1])
191
+ elif any(arg.startswith(prefix) for prefix in NOT_LABEL_PREFIX):
192
+ exclude_labels.append(arg.split('=', 1)[1])
193
+ elif any(arg.startswith(prefix) for prefix in BRANCH_PREFIX):
194
+ include_branches.append(arg.split('=', 1)[1])
195
+ elif any(arg.startswith(prefix) for prefix in NOT_BRANCH_PREFIX):
196
+ exclude_branches.append(arg.split('=', 1)[1])
197
+ elif arg in MODIFIED_ATTR:
198
+ modified = True
199
+ elif arg in DIVERGED_ATTR:
200
+ diverged = True
201
+ elif arg in TABLE_ATTR:
202
+ self.table = not self.table
203
+ elif arg in ASYNC_ATTR:
204
+ self.run_async = not self.run_async
205
+ elif any(arg.startswith(prefix) for prefix in COMMAND_ATTR):
206
+ self.command = arg.split('=', 1)[1]
207
+ else:
208
+ index += 1
209
+ continue
210
+ del sys.argv[index]
211
+ directory = os.getcwd()
212
+ to_delete = []
213
+
214
+ for repo, labels in self.repos.items():
215
+ abs_path = os.path.join(directory, repo)
216
+
217
+ if not os.path.isdir(abs_path):
218
+ utils.print_error(f'Invalid path {BOLD}{repo}{RESET}.', 12, False)
219
+ to_delete.append(repo)
220
+ continue
221
+ elif not os.path.isdir(os.path.join(abs_path, '.git')):
222
+ utils.print_error(f'{BOLD}.git{RESET} directory not found at target "{repo}".', 13, False)
223
+ to_delete.append(repo)
224
+ continue
225
+
226
+ os.chdir(abs_path)
227
+ delete = False
228
+
229
+ if any(include_labels) and not any(item in include_labels for item in labels):
230
+ delete = True
231
+ if any(exclude_labels) and any(item in exclude_labels for item in labels):
232
+ delete = True
233
+
234
+ if not delete:
235
+ try:
236
+ branch = subprocess.check_output('git rev-parse --abbrev-ref HEAD', shell=True, text=True, stderr=subprocess.DEVNULL).splitlines()[0]
237
+ except subprocess.CalledProcessError:
238
+ branch = 'NA'
239
+ if any(include_branches) and branch not in include_branches:
240
+ delete = True
241
+ if any(exclude_branches) and branch in exclude_branches:
242
+ delete = True
243
+
244
+ if not delete and modified:
245
+ status_output = subprocess.check_output('git status --porcelain', shell=True, stderr=subprocess.DEVNULL)
246
+ if not status_output:
247
+ delete = True
248
+
249
+ if not delete and diverged:
250
+ branch_status = subprocess.check_output('git status --branch --porcelain', shell=True, text=True).splitlines()
251
+ if not any('ahead' in line or 'behind' in line for line in branch_status if line.startswith('##')):
252
+ delete = True
253
+
254
+ if delete:
255
+ to_delete.append(repo)
256
+
257
+ for repo in to_delete:
258
+ del self.repos[repo]
259
+
260
+ os.chdir(directory)
261
+
262
+ def _parse_aliases(self) -> None:
263
+ if utils.settings.alias_settings is None:
264
+ return
265
+ for alias, command in dict(utils.settings.alias_settings).items():
266
+ args = self.command.split(' ')
267
+ if args[0] == alias:
268
+ del args[0]
269
+ self.command = ' '.join(command.split(' ') + args)
mud/commands.py ADDED
@@ -0,0 +1,27 @@
1
+ # Commands
2
+ ADD = ['add', 'a']
3
+ REMOVE = ['remove', 'rm']
4
+ LOG = ['log', 'l']
5
+ INFO = ['info', 'i']
6
+ INIT = ['init']
7
+ TAGS = ['tags', 'tag', 't']
8
+ LABELS = ['labels', 'lb']
9
+ STATUS = ['status', 'st']
10
+ BRANCHES = ['branch', 'branches', 'br']
11
+ REMOTE_BRANCHES = ['remote-branch', 'remote-branches', 'rbr']
12
+ HELP = ['help', '--help', '-h']
13
+ CONFIGURE = ['configure', 'config']
14
+ SET_GLOBAL = ['--set-global']
15
+
16
+ COMMANDS = [ADD, REMOVE, LOG, INFO, INIT, TAGS, LABELS, STATUS, BRANCHES, REMOTE_BRANCHES, HELP, CONFIGURE, SET_GLOBAL]
17
+
18
+ # Filters
19
+ ASYNC_ATTR = '-a', '--async'
20
+ TABLE_ATTR = '-t', '--table'
21
+ COMMAND_ATTR = '-c', '--command'
22
+ MODIFIED_ATTR = '-m', '--modified'
23
+ DIVERGED_ATTR = '-d', '--diverged'
24
+ LABEL_PREFIX = '-l=', '--label='
25
+ NOT_LABEL_PREFIX = '-nl=', '--not-label='
26
+ BRANCH_PREFIX = '-b=', '--branch='
27
+ NOT_BRANCH_PREFIX = '-nb=', '--not-branch='
mud/config.py ADDED
@@ -0,0 +1,99 @@
1
+ import os
2
+ import re
3
+ import csv
4
+
5
+ from typing import List, Dict
6
+
7
+ from mud import utils
8
+ from mud.styles import *
9
+
10
+
11
+ class Config:
12
+ def __init__(self):
13
+ self.data = {}
14
+
15
+ def save(self, file_path: str) -> None:
16
+ def _filter_labels(label: str):
17
+ return bool(re.match(r'^\w+$', label))
18
+
19
+ with open(file_path, 'w', newline='') as tsvfile:
20
+ writer = csv.writer(tsvfile, delimiter='\t')
21
+
22
+ for path, labels in self.data.items():
23
+ valid_labels = [label for label in labels if _filter_labels(label)]
24
+ formatted_labels = ','.join(valid_labels) if valid_labels else ''
25
+ writer.writerow([path, formatted_labels])
26
+
27
+ def find(self) -> None:
28
+ if os.path.exists(utils.CONFIG_FILE_NAME):
29
+ self.load(utils.CONFIG_FILE_NAME)
30
+ return
31
+
32
+ directory = os.getcwd()
33
+ current_path = directory
34
+ while os.path.dirname(current_path) != current_path:
35
+ os.chdir(current_path)
36
+ if os.path.exists(utils.CONFIG_FILE_NAME):
37
+ self.load(utils.CONFIG_FILE_NAME)
38
+ return utils.CONFIG_FILE_NAME
39
+ current_path = os.path.dirname(current_path)
40
+
41
+ if utils.settings.mud_settings['config_path'] != '' and os.path.exists(
42
+ utils.settings.mud_settings['config_path']):
43
+ directory = os.path.dirname(utils.settings.mud_settings['config_path'])
44
+ os.chdir(directory)
45
+ os.environ['PWD'] = directory
46
+ self.load(utils.CONFIG_FILE_NAME)
47
+ return
48
+
49
+ utils.print_error(f'{BOLD}.mudconfig{RESET} file was not found. Type `mud init` to create configuration file.', 11)
50
+ return
51
+
52
+ def load(self, file_path: str) -> None:
53
+ self.data = {}
54
+ with open(file_path, 'r') as tsvfile:
55
+ reader = csv.reader(tsvfile, delimiter='\t')
56
+ for row in reader:
57
+ path = row[0]
58
+
59
+ if path.startswith('~'):
60
+ path = os.path.expanduser(path)
61
+
62
+ labels = [label.strip() for label in row[1].split(',') if len(row) > 1 and label.strip()] if len(row) > 1 else []
63
+ self.data[path] = labels
64
+
65
+ def paths(self) -> List[str]:
66
+ return list(self.data.keys())
67
+
68
+ def filter_label(self, label: str, repos: Dict[str, List[str]] = None) -> Dict[str, List[str]]:
69
+ if repos is None:
70
+ repos = self.data
71
+ if label == '':
72
+ return repos
73
+ result = {}
74
+ for path, labels in repos.items():
75
+ if label in labels:
76
+ result[path] = labels
77
+ return result
78
+
79
+ def add_label(self, path: str, label: str) -> None:
80
+ if path is None:
81
+ path = label
82
+ label = None
83
+ if not os.path.isdir(path):
84
+ utils.print_error(f'Invalid path {BOLD}{path}{RESET}. Remember that path should be relative.', 14)
85
+ return
86
+ if path not in self.data:
87
+ self.data[path] = []
88
+ if label is not None and label not in self.data[path]:
89
+ self.data[path].append(label)
90
+
91
+ def remove_path(self, path: str) -> None:
92
+ if path in self.data:
93
+ del self.data[path]
94
+
95
+ def remove_label(self, path: str, label: str) -> None:
96
+ if path in self.data and label in self.data[path]:
97
+ self.data[path].remove(label)
98
+ if not self.data[path]:
99
+ del self.data[path]