-
Notifications
You must be signed in to change notification settings - Fork 60
/
gitFunctions.py
369 lines (318 loc) · 14 KB
/
gitFunctions.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
import os
import re
from datetime import datetime
import configuration
import shell
import shouter
class Initializer:
def __init__(self):
config = configuration.get()
self.repoName = config.gitRepoName
self.clonedRepoName = config.clonedGitRepoName
self.author = config.user
@staticmethod
def createignore():
git_ignore = ".gitignore"
if not os.path.exists(git_ignore):
with open(git_ignore, "w") as ignore:
ignore.write(".jazz5" + '\n')
ignore.write(".metadata" + '\n')
ignore.write(".jazzShed" + '\n')
config = configuration.get()
if len(config.ignoredirectories) > 0:
ignore.write('\n' + "# directories" + '\n')
for directory in config.ignoredirectories:
ignore.write('/' + directory + '\n')
ignore.write('\n')
shell.execute("git add " + git_ignore)
shell.execute("git commit -m %s -q" % shell.quote("Add .gitignore"))
@staticmethod
def createattributes():
"""
create a .gitattributes file (if so specified and not yet present)
"""
config = configuration.get()
if len(config.gitattributes) > 0:
gitattribues = ".gitattributes"
if not os.path.exists(gitattribues):
with open(gitattribues, "w") as attributes:
for line in config.gitattributes:
attributes.write(line + '\n')
shell.execute("git add " + gitattribues)
shell.execute("git commit -m %s -q" % shell.quote("Add .gitattributes"))
def initalize(self):
self.createrepo()
self.preparerepo()
@staticmethod
def preparerepo():
Initializer.setgitconfigs()
Initializer.createignore()
Initializer.createattributes()
def createrepo(self):
shell.execute("git init --bare " + self.repoName)
shouter.shout("Repository was created in " + os.getcwd())
shell.execute("git clone " + self.repoName)
os.chdir(self.clonedRepoName)
@staticmethod
def setgitconfigs():
shell.execute("git config push.default current")
shell.execute("git config core.ignorecase false") # should be the default anyway
shouter.shout("Set core.ignorecase to false")
@staticmethod
def initialcommit():
shouter.shout("Initial git add")
shell.execute("git add -A", os.devnull)
shouter.shout("Finished initial git add, starting commit")
shell.execute("git commit -m %s -q" % shell.quote("Initial Commit"))
shouter.shout("Finished initial commit")
class Commiter:
commitcounter = 0
isattachedtoaworkitemregex = re.compile("^\d*:.*-")
findignorepatternregex = re.compile("\{([^\{\}]*)\}")
@staticmethod
def addandcommit(changeentry):
Commiter.handleignore()
Commiter.replaceauthor(changeentry.author, changeentry.email)
shell.execute("git add -A")
Commiter.handle_captitalization_filename_changes()
shell.execute(Commiter.getcommitcommand(changeentry))
Commiter.commitcounter += 1
if Commiter.commitcounter is 30:
shouter.shout("30 Commits happend, push current branch to avoid out of memory")
Commiter.pushbranch("")
Commiter.commitcounter = 0
shouter.shout("Commited change in local git repository")
@staticmethod
def handle_captitalization_filename_changes():
sandbox = os.path.join(configuration.get().workDirectory, configuration.get().clonedGitRepoName)
lines = shell.getoutput("git status -z", stripped=False)
for newfilerelativepath in Commiter.splitoutputofgitstatusz(lines, "A "):
directoryofnewfile = os.path.dirname(os.path.join(sandbox, newfilerelativepath))
newfilename = os.path.basename(newfilerelativepath)
cwd = os.getcwd()
os.chdir(directoryofnewfile)
files = shell.getoutput("git ls-files")
for previousFileName in files:
was_same_file_name = newfilename.lower() == previousFileName.lower()
file_was_renamed = newfilename != previousFileName
if was_same_file_name and file_was_renamed:
shell.execute("git rm --cached %s" % previousFileName)
os.chdir(cwd)
@staticmethod
def getcommitcommand(changeentry):
comment = Commiter.getcommentwithprefix(changeentry.comment)
return "git commit -m %s --date %s --author=%s" \
% (shell.quote(comment), shell.quote(changeentry.date), changeentry.getgitauthor())
@staticmethod
def getcommentwithprefix(comment):
prefix = configuration.get().commitmessageprefix
if prefix and Commiter.isattachedtoaworkitemregex.match(comment):
return prefix + comment
return comment
@staticmethod
def replaceauthor(author, email):
shell.execute("git config --replace-all user.name " + shell.quote(author))
if not email:
email = Commiter.defaultemail(author)
shell.execute("git config --replace-all user.email " + email)
@staticmethod
def defaultemail(author):
if not author:
name = "default"
else:
haspoint = False
index = 0
name = ""
for c in author:
if c.isalnum() or c == "_":
name += c
else:
if index > 0 and not haspoint:
name += "."
haspoint = True
else:
name += "_"
index += 1
return name.lower() + "@rtc.to"
@staticmethod
def checkbranchname(branchname):
exitcode = shell.execute("git check-ref-format --normalize refs/heads/" + branchname)
if exitcode is 0:
return True
else:
return False
@staticmethod
def branch(branchname):
branchexist = shell.execute("git show-ref --verify --quiet refs/heads/" + branchname)
if branchexist is 0:
Commiter.checkout(branchname)
else:
shell.execute("git checkout -b " + branchname)
@staticmethod
def pushbranch(branchname, force=False):
if branchname:
shouter.shout("Push of branch " + branchname)
if force:
return shell.execute("git push -f origin " + branchname)
else:
return shell.execute("git push origin " + branchname)
@staticmethod
def pushmaster():
Commiter.pushbranch("master")
@staticmethod
def checkout(branchname):
shell.execute("git checkout " + branchname)
@staticmethod
def renamebranch(oldname, newname):
return shell.execute("git branch -m %s %s" % (oldname, newname))
@staticmethod
def copybranch(existingbranchname, newbranchname):
return shell.execute("git branch %s %s" % (newbranchname, existingbranchname))
@staticmethod
def promotebranchtomaster(branchname):
master = "master"
masterrename = Commiter.renamebranch(master, "masterRenamedAt_" + datetime.now().strftime('%Y%m%d_%H%M%S'))
copybranch = Commiter.copybranch(branchname, master)
if masterrename is 0 and copybranch is 0:
return Commiter.pushbranch(master, True)
else:
shouter.shout("Branch %s couldnt get renamed to master, please do that on your own" % branchname)
return 1 # branch couldnt get renamed
@staticmethod
def get_untracked_statuszlines():
return shell.getoutput("git status --untracked-files=all -z", stripped=False)
@staticmethod
def handleignore():
"""
check untracked files and handle both global and local ignores
"""
repositoryfiles = Commiter.splitoutputofgitstatusz(Commiter.get_untracked_statuszlines())
Commiter.ignoreextensions(repositoryfiles)
Commiter.ignorejazzignore(repositoryfiles)
@staticmethod
def ignoreextensions(repositoryfiles):
"""
add files with extensions to be ignored to the global .gitignore
"""
ignorefileextensions = configuration.get().ignorefileextensions
if len(ignorefileextensions) > 0:
Commiter.ignore(ExtensionFilter.match(repositoryfiles, ignorefileextensions))
@staticmethod
def ignore(filelines):
"""
append the file lines to the toplevel .gitignore
:param filelines: a list of newline terminated file names to be ignored
"""
if len(filelines) > 0:
with open(".gitignore", "a") as ignore:
ignore.writelines(filelines)
@staticmethod
def splitoutputofgitstatusz(lines, filterprefix=None):
"""
Split the output of 'git status -z' into single files
:param lines: the unstripped output line(s) from the command
:param filterprefix: if given, only the files of those entries matching the prefix will be returned
:return: a list of repository files with status changes
"""
repositoryfiles = []
for line in lines: # expect exactly one line
entries = line.split(sep='\x00') # ascii 0 is the delimiter
for entry in entries:
if len(entry) > 0:
if not filterprefix or entry.startswith(filterprefix):
start = entry.find(' ')
if 0 <= start <= 2:
repositoryfile = entry[3:] # output is formatted
else:
repositoryfile = entry # file on a single line (e.g. rename continuation)
repositoryfiles.append(repositoryfile)
return repositoryfiles
@staticmethod
def translatejazzignore(jazzignorelines):
"""
translate the lines of a local .jazzignore file into the lines of a local .gitignore file
:param jazzignorelines: the input lines
:return: the .gitignore lines
"""
recursive = False
gitignorelines = []
for line in jazzignorelines:
if not line.startswith("#"):
line = line.strip()
if line.startswith("core.ignore"):
gitignorelines.append('\n')
recursive = line.startswith("core.ignore.recursive")
for foundpattern in Commiter.findignorepatternregex.findall(line):
gitignoreline = foundpattern + '\n'
if not recursive:
gitignoreline = '/' + gitignoreline # forward, not os.sep
gitignorelines.append(gitignoreline)
return gitignorelines
@staticmethod
def restore_shed_gitignore(statuszlines):
"""
If a force reload of the RTC workspace sheds .gitignore files away, we need to restore them.
In this case they are marked as deletions from git.
:param statuszlines: the git status z output lines
"""
gitignore = ".gitignore"
gitignorelen = len(gitignore)
deletedfiles = Commiter.splitoutputofgitstatusz(statuszlines, " D ")
for deletedfile in deletedfiles:
if deletedfile[-gitignorelen:] == gitignore:
# only restore .gitignore if sibling .jazzignore still exists
jazzignorefile = deletedfile[:-gitignorelen] + ".jazzignore"
if os.path.exists(jazzignorefile):
shell.execute("git checkout -- %s" % deletedfile)
@staticmethod
def ignorejazzignore(repositoryfiles):
"""
If a .jazzignore file is modified or added, translate it to .gitignore,
if a .jazzignore file is deleted, delete the corresponding .gitignore file as well.
:param repositoryfiles: the modified files
"""
jazzignore = ".jazzignore"
jazzignorelen = len(jazzignore)
for repositoryfile in repositoryfiles:
if repositoryfile[-jazzignorelen:] == jazzignore:
path = repositoryfile[0:len(repositoryfile)-jazzignorelen]
gitignore = path + ".gitignore"
if os.path.exists(repositoryfile):
# update (or create) .gitignore
jazzignorelines = []
with open(repositoryfile, 'r') as jazzignorefile:
jazzignorelines = jazzignorefile.readlines()
if len(jazzignorelines) > 0:
# overwrite in any case
with open(gitignore, 'w') as gitignorefile:
gitignorefile.writelines(Commiter.translatejazzignore(jazzignorelines))
else:
# delete .gitignore
if os.path.exists(gitignore):
os.remove(gitignore)
class Differ:
@staticmethod
def has_diff():
return shell.execute("git diff --quiet") is 1
class ExtensionFilter:
@staticmethod
def match(repositoryfiles, extensions):
"""
Determine the repository files to ignore.
These filenames are returned as a list of newline terminated lines,
ready to be added to .gitignore with writelines()
:param repositoryfiles: a list of (changed) files
:param extensions the extensions to be ignored
:return: a list of newline terminated file names, possibly empty
"""
repositoryfilestoignore = []
for extension in extensions:
for repositoryfile in repositoryfiles:
extlen = len(extension)
if len(repositoryfile) >= extlen:
if repositoryfile[-extlen:] == extension:
# prepend a forward slash (for non recursive,)
# escape a backslash with a backslash
# append a newline
repositoryfilestoignore.append('/' + repositoryfile.replace('\\', '\\\\') + '\n')
return repositoryfilestoignore