Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Result is a print with aggregated contribution and churn per author for a given

- **-h, --h, --help**    show this help message and exit
- **-exdir**                   exclude Git repository subdirectory
-- **--show-file-data** display line count changes per file

## Usage Example 1

Expand Down Expand Up @@ -63,6 +64,73 @@ contribution: 4423
churn: -543
```

## Usage Example 3

```bash
python ./gitcodechurn.py after="2018-11-29" before="2021-11-05" author="flacle" dir="/Users/myname/myrepo" --show-file-data
```

## Output 3

```bash
author: flacle
contribution: 337
churn: -19
-------------------------------------------------------------------------------
FILE NAME | LINE # | ADDED | REMOVED
-------------------------------------------------------------------------------
gitcodechurn.py | 1 | 190 | 0
-------------------------------------------------------------------------------
gitcodechurn.py | 2 | 4 | 0
-------------------------------------------------------------------------------
gitcodechurn.py | 37 | 2 | 0
-------------------------------------------------------------------------------
gitcodechurn.py | 40 | 0 | 1
-------------------------------------------------------------------------------
gitcodechurn.py | 42 | 1 | 0
-------------------------------------------------------------------------------
gitcodechurn.py | 45 | 0 | 1
-------------------------------------------------------------------------------
gitcodechurn.py | 47 | 1 | 0
-------------------------------------------------------------------------------
gitcodechurn.py | 50 | 0 | 1
-------------------------------------------------------------------------------
gitcodechurn.py | 52 | 1 | 0
-------------------------------------------------------------------------------
gitcodechurn.py | 55 | 0 | 1
-------------------------------------------------------------------------------
gitcodechurn.py | 57 | 8 | 1
-------------------------------------------------------------------------------
gitcodechurn.py | 66 | 2 | 0
-------------------------------------------------------------------------------
gitcodechurn.py | 62 | 0 | 1
...
-------------------------------------------------------------------------------
gitcodechurn.py | 200 | 1 | 0
-------------------------------------------------------------------------------
README.md | 12 | 2 | 0
-------------------------------------------------------------------------------
README.md | 16 | 0 | 1
-------------------------------------------------------------------------------
README.md | 18 | 1 | 0
-------------------------------------------------------------------------------
README.md | 21 | 11 | 0
-------------------------------------------------------------------------------
README.md | 20 | 0 | 1
-------------------------------------------------------------------------------
README.md | 33 | 1 | 0
-------------------------------------------------------------------------------
README.md | 22 | 0 | 1
-------------------------------------------------------------------------------
README.md | 35 | 1 | 0
-------------------------------------------------------------------------------
README.md | 24 | 0 | 2
-------------------------------------------------------------------------------
README.md | 37 | 3 | 0
-------------------------------------------------------------------------------
README.md | 41 | 12 | 0
```

Outputs of Usage Example 1 can be used as part of a pipeline that generates bar charts for reports:
![contribution vs churn example chart](/chart.png)

Expand Down
158 changes: 144 additions & 14 deletions gitcodechurn.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ def main():
default = '',
help = 'the Git repository subdirectory to be excluded'
)
parser.add_argument(
"--show-file-data",
action="store_true",
help="Display line change information for the analyzed file(s)"
)
args = parser.parse_args()

after = args.after
Expand Down Expand Up @@ -99,6 +104,53 @@ def main():
# print files in case more granular results are needed
#print('files: ', files)

if args.show_file_data:
display_file_metrics(files)


def display_file_metrics(files):
display_file_metrics_header()
for file_name, line_change_info in files.items():
for line_number, line_diff_stats in line_change_info.items():
display_file_metrics_row(file_name, line_number, line_diff_stats)


def display_file_metrics_header():
print("-" * 79)
print(
"{file}|{line_number}|{lines_added}|{lines_removed}".format(
file=format_column("FILE NAME", 34),
line_number=format_column("LINE #", 10),
lines_added=format_column("ADDED", 10),
lines_removed=format_column("REMOVED", 10),
)
)


def display_file_metrics_row(file_name, line_number, line_diff_stats):
added = line_diff_stats.get("lines_added")
removed = line_diff_stats.get("lines_removed")

if added == 0 and removed == 0:
return
print("-" * 79)
print(
"{file}|{ln}|{lines_added}|{lines_removed}".format(
file=format_column(file_name, 34),
ln=format_column(str(line_number), 10),
lines_added=format_column(str(added), 10),
lines_removed=format_column(str(removed), 10),
)
)


def format_column(text, width):
text_length = len(text)
total_pad = width - text_length
pad_left = total_pad // 2
pad_right = total_pad - pad_left
return (" " * pad_left) + text + (" " * pad_right)


def calculate_statistics(commits, dir, exdir):
# structured like this: files -> LOC
Expand Down Expand Up @@ -142,21 +194,97 @@ def get_loc(commit, dir, files, contribution, churn, exdir):
new_loc_changes = is_loc_change(result, loc_changes)
if loc_changes != new_loc_changes:
loc_changes = new_loc_changes
locc = get_loc_change(loc_changes)
for loc in locc:
if loc in files[file]:
files[file][loc] += locc[loc]
churn += abs(locc[loc])
else:
files[file][loc] = locc[loc]
contribution += abs(locc[loc])
(removal, addition) = get_loc_change(loc_changes)

files, contribution, churn = merge_operations(removal, addition, files, contribution, churn, file)
else:
continue
return [files, contribution, churn]


def get_commit_results(command, dir):
return get_proc_out(command, dir).splitlines()
def merge_operations(removal, addition, files, contribution, churn, file):
# Ensure all required data is in place
ensure_file_exists(files, file)

file_line_churn_dict = files[file]

if is_noop(removal, addition):
# In the case of a noop, it's not counted in change metrics, but should
# be marked as changed to accurately include future churn metrics
# An example of this is a diff like:
# "diff --git README.md README.md",
# "index bedbc85..bb033cd 100644",
# "--- README.md",
# "+++ README.md",
# "@@ -8 +8 @@ Code churn has several definitions, the one that to me provides the most value a",
# "-*Reference: https://blog.gitprime.com/why-code-churn-matters/*",
# "+*Reference: https://www.pluralsight.com/blog/teams/why-code-churn-matters*",
# In this example, we deleted the line, and then added the line by updating the link
# This repo would consider this a "No-Op" as it nets to no change
# However, we want to mark line 8 as changed so that all subsequent
# changes to line 8 are marked as churn
# The thinking behind this is the other updates should have been made
# while this change was being made.
remove_line_number = removal[0]
ensure_line_exists(file_line_churn_dict, remove_line_number)
return files, contribution, churn

for (line_number, lines_removed, lines_added) in compute_changes(removal, addition):
# Churn check performed before line modification changes
is_churn = is_this_churn(file_line_churn_dict, line_number)

ensure_line_exists(file_line_churn_dict, line_number)
line_count_change_metrics = file_line_churn_dict[line_number]

line_count_change_metrics["lines_removed"] += lines_removed
line_count_change_metrics["lines_added"] += lines_added

if is_churn:
churn += abs(lines_removed) + abs(lines_added)
else:
contribution += abs(lines_removed) + abs(lines_added)

return files, contribution, churn


def compute_changes(removal, addition):
# If both removal and addition affect the same line, net out the change
# Returns a list of tuples of type (line_number, lines_removed, lines_added)
removed_line_number, lines_removed = removal
added_line_number, lines_added = addition

if removed_line_number == added_line_number:
if lines_added >= lines_removed:
return [(removed_line_number, 0, (lines_added - lines_removed))]
else:
return [(removed_line_number, (lines_removed - lines_added), 0)]
else:
return [
(removed_line_number, lines_removed, 0),
(added_line_number, 0, lines_added),
]


def is_this_churn(file_line_churn_dict, line_number):
# The definition of churn is any change to a line
# after the first time the line has been changed
# This is detected by a line operation logged in the file_line_churn_dict
return line_number in file_line_churn_dict


def ensure_line_exists(file_line_churn_dict, line_number):
if line_number not in file_line_churn_dict:
file_line_churn_dict[line_number] = {"lines_removed": 0, "lines_added": 0}


def ensure_file_exists(files, file):
if file not in files:
files[file] = {}


def is_noop(removal, addition):
# A noop event occurs when a change indicates one delete and one add on the same line
return removal == addition


# arrives in a format such as -13 +27,5 (no commas mean 1 loc change)
Expand All @@ -175,6 +303,8 @@ def get_loc_change(loc_changes):
left = int(left[1:])
left_dec = 1

removal = (left, left_dec)

# additions
right = loc_changes[loc_changes.find(' ')+1:]
right_dec = 0
Expand All @@ -186,10 +316,10 @@ def get_loc_change(loc_changes):
right = int(right[1:])
right_dec = 1

if left == right:
return {left: (right_dec - left_dec)}
else:
return {left : left_dec, right: right_dec}
addition = (right, right_dec)

return (removal, addition)


def is_loc_change(result, loc_changes):
# search for loc changes (@@ ) and update loc_changes variable
Expand Down
Loading