|
| 1 | +#! /usr/bin/python3 |
| 2 | + |
| 3 | +from libreselery.contribution_distribution_engine_types import ( |
| 4 | + ContributionAction, |
| 5 | + ContributionActionPlugin, |
| 6 | +) |
| 7 | + |
| 8 | +### Start User Imports |
| 9 | +### specialzed plugin imports can be added here |
| 10 | +################################################################################## |
| 11 | +import subprocess |
| 12 | +from datetime import datetime |
| 13 | + |
| 14 | +### End User Imports |
| 15 | +################################################################################## |
| 16 | + |
| 17 | + |
| 18 | +class GitFileContributionAction(ContributionActionPlugin): |
| 19 | + """ |
| 20 | + This class is a plugin containing the implementation of a single ContributorAction. |
| 21 | + It is responsible for gathering contributor information and evaluating scores |
| 22 | + for each contributor based on configurated metrics |
| 23 | +
|
| 24 | + Plugin description: |
| 25 | + This plugin will evaluate the line-of-code contributions made from all |
| 26 | + contributors of local files (under git version control). |
| 27 | + """ |
| 28 | + |
| 29 | + _alias_ = "git_file_contribution_action" |
| 30 | + GITBLAMESEPERATOR = "***\n" |
| 31 | + |
| 32 | + def __init__(self): |
| 33 | + super(GitFileContributionAction, self).__init__() |
| 34 | + |
| 35 | + def initialize_(self, action): |
| 36 | + """ |
| 37 | + Overload of abstract method which is responsible for initializing this plugin |
| 38 | +
|
| 39 | + Parameters: |
| 40 | + action (ContributionAction): |
| 41 | + action object which contains all necessary information of what |
| 42 | + a contributor has to doto be scored and recognized as such |
| 43 | +
|
| 44 | + Returns: |
| 45 | + bool: True if successfully initialized |
| 46 | + """ |
| 47 | + return True |
| 48 | + |
| 49 | + def gather_(self, cachedContributors=[]): |
| 50 | + """ |
| 51 | + Overload of abstract method which is responsible for gathering |
| 52 | + contributor information and scoring contributors based on the action defined |
| 53 | +
|
| 54 | + Parameters: |
| 55 | + [optional] cachedContributors (list): |
| 56 | + list of contributors from various external (remote) sources which had been chached earlier |
| 57 | + so that plugins don't need to do expensive lookups all the time |
| 58 | +
|
| 59 | + Returns: |
| 60 | + tuple: (list of contributors, list of scores) |
| 61 | + """ |
| 62 | + contributors = [] |
| 63 | + scores = [] |
| 64 | + |
| 65 | + fileContributions = self.execGit() |
| 66 | + for filename, fileContributorDict in fileContributions.items(): |
| 67 | + print("%s" % filename) |
| 68 | + self.printFileContributorDict(fileContributorDict) |
| 69 | + |
| 70 | + return contributors, scores |
| 71 | + |
| 72 | + ### Start User Methods |
| 73 | + ### specialzed plugin methods can be added here |
| 74 | + ################################################################################## |
| 75 | + |
| 76 | + def execGit(self): |
| 77 | + cmd = [ |
| 78 | + "git", |
| 79 | + "ls-files", |
| 80 | + "|", |
| 81 | + "while read f;", |
| 82 | + 'do echo "%s$f";' % self.GITBLAMESEPERATOR, |
| 83 | + "git blame -CCC --line-porcelain $f;", |
| 84 | + "done", |
| 85 | + ] |
| 86 | + |
| 87 | + ps = subprocess.Popen( |
| 88 | + " ".join(cmd), shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT |
| 89 | + ) |
| 90 | + stdout, stderr = ps.communicate() |
| 91 | + |
| 92 | + fileContributions = {} |
| 93 | + if not stderr: |
| 94 | + encoded = stdout.decode("utf-8") |
| 95 | + ### split output for each file (we added "\n" manually to seperate each file output) |
| 96 | + fileblames = encoded.split(self.GITBLAMESEPERATOR)[ |
| 97 | + 1: |
| 98 | + ] ### 0 entry is empty because reasons ... :D |
| 99 | + for blame in fileblames: |
| 100 | + ### seperate lines into array |
| 101 | + lines = blame.split("\n") |
| 102 | + filename = lines[0] |
| 103 | + lines = lines[1:] |
| 104 | + #print("Blame > %s [%s]" % (filename, len(lines))) |
| 105 | + ### put lines through blameParser |
| 106 | + fileContributorDict = self.parseBlame(lines) |
| 107 | + fileContributions[filename] = fileContributorDict |
| 108 | + return fileContributions |
| 109 | + |
| 110 | + def printFileContributorDict(self, fcDict): |
| 111 | + for author, data in fcDict.items(): |
| 112 | + print(" %s [%s]" % (author, data["count"])) |
| 113 | + for stamp, count in data["stamps"].items(): |
| 114 | + datetimeStr = datetime.fromtimestamp(float(stamp)).strftime( |
| 115 | + "%Y-%m-%d/%H:%M:%S" |
| 116 | + ) |
| 117 | + print(" -- %s [%s]" % (datetimeStr, count)) |
| 118 | + |
| 119 | + def parseBlame(self, lines): |
| 120 | + lineDescriptions = [] |
| 121 | + lineDescription = {} |
| 122 | + currentCommitSha = None |
| 123 | + headerLength = None |
| 124 | + newEntry = True |
| 125 | + for line in lines: |
| 126 | + if newEntry: |
| 127 | + # print("######################################") |
| 128 | + newEntry = False |
| 129 | + ### commit hash extraction |
| 130 | + key = "commit" |
| 131 | + val = line.split(" ")[0] |
| 132 | + else: |
| 133 | + splits = line.split(" ") |
| 134 | + key = splits[0] |
| 135 | + val = " ".join(splits[1:]) |
| 136 | + |
| 137 | + ### the amount of lines per entry is not consistent inside git blame |
| 138 | + ### so parsing is nearly impossible |
| 139 | + ### the only proper criteria in this stupid pocelain implementation is when we have a codeline which allways starts with \t |
| 140 | + ### thanks git ... idiots -.- |
| 141 | + if line and line[0] == "\t": |
| 142 | + ### valuable lines done |
| 143 | + lineDescriptions.append(lineDescription) |
| 144 | + lineDescription = {} |
| 145 | + newEntry = True |
| 146 | + |
| 147 | + lineDescription[key] = val |
| 148 | + |
| 149 | + fileContributions = {} |
| 150 | + for d in lineDescriptions: |
| 151 | + author = d["author-mail"] |
| 152 | + timestamp = d["committer-time"] |
| 153 | + key = author |
| 154 | + dd = fileContributions.get(author, None) |
| 155 | + if dd: |
| 156 | + c = dd["count"] |
| 157 | + stamps = dd["stamps"] |
| 158 | + c += 1 |
| 159 | + if timestamp in stamps: |
| 160 | + stamps[timestamp] += 1 |
| 161 | + else: |
| 162 | + stamps[timestamp] = 1 |
| 163 | + ### rewrite the dict data |
| 164 | + fileContributions[key] = {"count": c, "stamps": stamps} |
| 165 | + else: |
| 166 | + fileContributions[key] = {"count": 1, "stamps": {timestamp: 1}} |
| 167 | + return fileContributions |
| 168 | + |
| 169 | + ### End User Methods |
| 170 | + ################################################################################## |
| 171 | + |
| 172 | + |
| 173 | +def test(): |
| 174 | + success = False |
| 175 | + print("This is a Test!") |
| 176 | + ### define our input configuration (action) which normally comes from .yml configuration |
| 177 | + d = { |
| 178 | + "contributions_to_code": { |
| 179 | + "type": "git_file_contribution_action", ### type of action (also the name of the plugin _alias_ used!) |
| 180 | + "applies_to": [ |
| 181 | + "*.md", |
| 182 | + "docs/", |
| 183 | + ], ### simple filter, not really thought out yet |
| 184 | + "metrics": [ ### metrics applied to this action, what gets score and what doesnt |
| 185 | + { |
| 186 | + "UNIFORM": { ### metric identifier |
| 187 | + "degradation_type": "linear", |
| 188 | + "degradation_value": 1, |
| 189 | + } |
| 190 | + } |
| 191 | + ], |
| 192 | + } |
| 193 | + } |
| 194 | + ### create an action object |
| 195 | + action = ContributionAction(d) |
| 196 | + ### initialize the action |
| 197 | + ### which will in turn use this specific plugin |
| 198 | + ### if configured correctly |
| 199 | + init = action.initialize_() |
| 200 | + if init: |
| 201 | + ### let us do our work |
| 202 | + data = action.gather_() |
| 203 | + ### visualize and evaluate test data |
| 204 | + print(data) |
| 205 | + success = True |
| 206 | + return success |
| 207 | + |
| 208 | + |
| 209 | +if __name__ == "__main__": |
| 210 | + assert test() == True |
0 commit comments