-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrcs-post-merge.py
executable file
·398 lines (311 loc) · 11.4 KB
/
rcs-post-merge.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
#! /usr/bin/env python
# # -*- coding: utf-8 -*
"""
rcs-keywords-post-merge
This module provides code to act as an event hook for the git
post-merge event. It detects which files have been changed
and forces the file to be checked back out within the
repository once the commit data is available.
"""
import sys
import os
import errno
import subprocess
import logging
__author__ = "David Rotthoff"
__email__ = "drotthoff@gmail.com"
__project__ = "git-rcs-keywords"
__version__ = "1.1.1-19"
__date__ = "2021-02-07 10:51:24"
__credits__ = []
__status__ = "Production"
# LOGGING_CONSOLE_LEVEL = None
# LOGGING_CONSOLE_LEVEL = logging.DEBUG
# LOGGING_CONSOLE_LEVEL = logging.INFO
# LOGGING_CONSOLE_LEVEL = logging.WARNING
LOGGING_CONSOLE_LEVEL = logging.ERROR
# LOGGING_CONSOLE_LEVEL = logging.CRITICAL
LOGGING_CONSOLE_MSG_FORMAT = \
'%(asctime)s:%(levelname)s:%(module)s:%(funcName)s:%(lineno)s: %(message)s'
LOGGING_CONSOLE_DATE_FORMAT = '%Y-%m-%d %H.%M.%S'
LOGGING_FILE_LEVEL = None
# LOGGING_FILE_LEVEL = logging.DEBUG
# LOGGING_FILE_LEVEL = logging.INFO
# LOGGING_FILE_LEVEL = logging.WARNING
# LOGGING_FILE_LEVEL = logging.ERROR
# LOGGING_FILE_LEVEL = logging.CRITICAL
LOGGING_FILE_MSG_FORMAT = LOGGING_CONSOLE_MSG_FORMAT
LOGGING_FILE_DATE_FORMAT = LOGGING_CONSOLE_DATE_FORMAT
# LOGGING_FILE_NAME = '.git-hook.post-merge.log'
LOGGING_FILE_NAME = '.git-hook.log'
# Conditionally map a time function for performance measurement
# depending on the version of Python used
if sys.version_info.major >= 3 and sys.version_info.minor >= 3:
from time import perf_counter as get_clock
else:
from time import clock as get_clock
def configure_logging():
"""Configure the logging service"""
# Configure the console logger
if LOGGING_CONSOLE_LEVEL:
console = logging.StreamHandler()
console.setLevel(LOGGING_CONSOLE_LEVEL)
console_formatter = logging.Formatter(
fmt=LOGGING_CONSOLE_MSG_FORMAT,
datefmt=LOGGING_CONSOLE_DATE_FORMAT,
)
console.setFormatter(console_formatter)
# Create an file based logger if a LOGGING_FILE_LEVEL is defined
if LOGGING_FILE_LEVEL:
logging.basicConfig(
level=LOGGING_FILE_LEVEL,
format=LOGGING_FILE_MSG_FORMAT,
datefmt=LOGGING_FILE_DATE_FORMAT,
filename=LOGGING_FILE_NAME,
)
# Basic logger configuration
if LOGGING_CONSOLE_LEVEL or LOGGING_FILE_LEVEL:
logger = logging.getLogger('')
if LOGGING_CONSOLE_LEVEL:
# Add the console logger to default logger
logger.addHandler(console)
def execute_cmd(cmd, cmd_source=None):
"""Execute the supplied program.
Arguments:
cmd -- string or list of strings of commands. A single string may
not contain spaces.
cmd_source -- The function requesting the program execution
Default value of None.
Returns:
Process stdout file handle
"""
start_time = get_clock()
logging.info('Entered function')
logging.debug('cmd: %s', cmd)
logging.debug('cmd_source: %s', cmd_source)
# Ensure there are no embedded spaces in a string command
if isinstance(cmd, str) and ' ' in cmd:
end_time = get_clock()
logging.error('Exiting - embedded space in command')
logging.info('Elapsed time: %f', (end_time - start_time))
exit(1)
# Execute the command
try:
cmd_handle = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(cmd_stdout, cmd_stderr) = cmd_handle.communicate()
if cmd_stderr:
for line in cmd_stderr.strip().decode("utf-8").splitlines():
logging.info("stderr line: %s", line)
# If the command fails, notify the user and exit immediately
except subprocess.CalledProcessError as err:
end_time = get_clock()
logging.info(
"Program %s called by %s failed! -- Exiting.",
cmd,
cmd_source,
exc_info=True
)
logging.error(
"Program %s call failed! -- Exiting.", cmd
)
logging.info('Elapsed time: %f', (end_time - start_time))
raise
except OSError as err:
end_time = get_clock()
logging.info(
"Program %s called by %s caused on OS error %s! -- Exiting.",
cmd,
cmd_source,
err.errno,
exc_info=True
)
logging.error(
"Program %s caused OS error %s! -- Exiting.",
cmd,
err.errno
)
logging.info('Elapsed time: %f', (end_time - start_time))
raise
end_time = get_clock()
logging.info('Elapsed time: %f', (end_time - start_time))
# Return from the function
return cmd_stdout
def check_for_cmd(cmd):
"""Make sure that a program necessary for using this script is
available.
Arguments:
cmd -- string or list of strings of commands. A single string may
not contain spaces.
Returns:
Nothing
"""
start_time = get_clock()
logging.info('Entered function')
logging.debug('cmd: %s', cmd)
# Ensure there are no embedded spaces in a string command
if isinstance(cmd, str) and ' ' in cmd:
end_time = get_clock()
logging.error('Exiting - embedded space in command')
logging.info('Elapsed time: %f', (end_time - start_time))
exit(1)
# Execute the command
execute_cmd(cmd=cmd, cmd_source='check_for_cmd')
end_time = get_clock()
logging.info('Elapsed time: %f', (end_time - start_time))
def git_ls_files():
"""Find files that are relevant based on all files for the
repository branch.
Arguments:
None
Returns:
A list of filenames.
"""
start_time = get_clock()
logging.debug('Entered function')
cmd = ['git', 'ls-files']
# Get a list of all files in the current repository branch
cmd_stdout = execute_cmd(cmd=cmd, cmd_source='git_ls_files')
end_time = get_clock()
logging.info('Elapsed time: %f', (end_time - start_time))
# Return from the function
return cmd_stdout
def get_modified_files():
"""Find files that were modified by the merge.
Arguments:
None
Returns:
A list of filenames.
"""
start_time = get_clock()
logging.debug('Entered function')
modified_file_list = []
cmd = ['git', 'diff-tree', 'ORIG_HEAD', 'HEAD', '--name-only', '-r',
'--diff-filter=ACMRT']
# Fetch the list of files modified by the last commit
cmd_stdout = execute_cmd(cmd=cmd, cmd_source='get_modified_files')
# Convert the stdout stream to a list of files
modified_file_list = cmd_stdout.decode('utf8').splitlines()
# Deal with unmodified repositories
if modified_file_list and modified_file_list[0] == 'clean':
end_time = get_clock()
logging.info('No modified files found')
logging.info('Elapsed time: %f', (end_time - start_time))
exit(0)
# Only return regular files.
modified_file_list = [i for i in modified_file_list if os.path.isfile(i)]
end_time = get_clock()
logging.debug('modified_file_list: %s', modified_file_list)
logging.info('Elapsed time: %f', (end_time - start_time))
# Return from the function
return modified_file_list
def remove_modified_files(files):
"""Filter the found files to eliminate any that have changes that have
not been checked in.
Arguments:
files - list of files to checkout
Returns:
A list of files to checkout that do not have pending changes.
"""
start_time = get_clock()
logging.info('Entered function')
logging.debug('files: %s', files)
cmd = ['git', 'status', '-s']
# Get the list of files that are modified but not checked in
cmd_stdout = execute_cmd(cmd=cmd, cmd_source='remove_modified_files')
# Convert the stream output to a list of output lines
modified_files_list = cmd_stdout.decode('utf8').splitlines()
logging.debug('modified files list: %s', modified_files_list)
# Deal with unmodified repositories
if not modified_files_list:
end_time = get_clock()
logging.info('No modified files found')
logging.debug('files: %s', files)
logging.info('Elapsed time: %f', (end_time - start_time))
return files
# Pull the file name (second field) of the output line and
# remove any double quotes
modified_files_list = [l.split(None, 1)[-1].strip('"')
for l in modified_files_list]
logging.debug('modified files list: %s', modified_files_list)
# Remove any modified files from the list of files to process
if modified_files_list:
files = [f for f in files if f not in modified_files_list]
end_time = get_clock()
logging.debug('cleaned files list: %s', files)
logging.info('Elapsed time: %f', (end_time - start_time))
# Return from the function
return files
def check_out_file(file_name):
"""Checkout file that was been modified by the latest merge.
Arguments:
file_name -- the file name to be checked out for smudging
Returns:
Nothing.
"""
start_time = get_clock()
logging.info('Entered function')
logging.debug('file_name: %s', file_name)
# Remove the file if it currently exists
try:
os.remove(file_name)
except OSError as err:
# Ignore a file not found error, it was being removed anyway
if err.errno != errno.ENOENT:
end_time = get_clock()
logging.info(
"File removal of %s caused on OS error %d! -- Exiting.",
file_name,
err.errno,
exc_info=True
)
logging.error(
"File removal %s caused OS error %d! -- Exiting.",
file_name,
err.errno
)
logging.info('Elapsed time: %f', (end_time - start_time))
exit(err.errno)
cmd = ['git', 'checkout', '-f', '%s' % file_name]
# Check out the file so that it is smudged
execute_cmd(cmd=cmd, cmd_source='check_out_files')
end_time = get_clock()
logging.info('Elapsed time: %f', (end_time - start_time))
def post_merge():
"""Main program.
Arguments:
None
Returns:
Nothing
"""
start_time = get_clock()
logging.info('Entered function')
# Check if git is available.
check_for_cmd(cmd=['git', '--version'])
# Get the list of modified files
files = get_modified_files()
logging.debug('Modified file list: %s', files)
# Filter the list of modified files to exclude those modified since
# the commit
files = remove_modified_files(files=files)
# Process the remaining file list
files_processed = 0
if files:
files.sort()
for file_name in files:
check_out_file(file_name=file_name)
files_processed += 1
sys.stderr.write('Smudged file %s\n' % file_name)
logging.info('Checked out file %s', file_name)
logging.debug('Files processed: %s', files_processed)
end_time = get_clock()
logging.info('Elapsed time: %f', (end_time - start_time))
# Execute the main function
if __name__ == '__main__':
configure_logging()
START_TIME = get_clock()
logging.debug('Entered module')
post_merge()
END_TIME = get_clock()
logging.info('Elapsed time: %f', (END_TIME - START_TIME))