-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit-tree-diff.py
More file actions
174 lines (138 loc) · 6.03 KB
/
git-tree-diff.py
File metadata and controls
174 lines (138 loc) · 6.03 KB
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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import subprocess
import collections
import argparse
import re
class FileTreeDict:
def __init__(self, tree, spaces, utf8):
self.tree = tree
self.spaces = spaces
self.utf8 = utf8
def line_symbol(self, last):
if last:
c = "└─" if self.utf8 else "\- ";
else:
c = "├─" if self.utf8 else "|- ";
return (" "*self.spaces) + c
def indent_symbol(self, last):
if last:
c = "";
else:
c = "│ " if self.utf8 else "| ";
return (" "*self.spaces) + c
def print_node(self, node, name, depth, indent, last = False):
suffix = ''
line = indent
if depth:
line += self.line_symbol(last)
indent += self.indent_symbol(last)
if not isinstance(node, int):
suffix += '/'
print(line+name+suffix);
if not isinstance(node, int):
treelen = len(node)
i = 1
for key, val in node.items():
self.print_node(val, key, depth+1, indent, i == treelen)
i += 1
def show(self):
for key, val in self.tree.items():
self.print_node(val, key, 0, "", True);
def paths_to_dict(paths, add_status = False):
paths.sort()
if add_status:
paths = append_file_status(paths)
paths = encode_file_renames(paths)
stack = []
d = collections.OrderedDict()
paths = filter(None, paths)
for p in paths:
path_arr = p.split('/')
plen = len(path_arr)
if add_status:
path_arr[plen-1] = decode_file_rename(path_arr[plen-1])
auxd = d
depth = 0
for tk in path_arr:
if tk in auxd:
pass
else:
if depth == plen-1:
auxd[tk] = 0
else:
auxd[tk] = {}
auxd = auxd[tk]
parent_tk = tk
depth += 1
return d
def append_file_status(paths_list):
return list(map(lambda n: re.sub(r'([A-Z])(?:[0-9]{0,3})([\t\s]+)([^\t\s]+(?:[\s\t]+[^\t\s]+)?)', r'\3[\1]', n), paths_list))
def encode_file_renames(paths_list):
space = '#@@#';
a = list(map(lambda n: re.sub(r'([^\t\s]+)([\s\t]+)([^\t\s]+)', '\\1'+space+'\\3', n), paths_list))
for i in range(len(a)):
p = a[i]
if space in p:
tk = p.split(space)
rename = tk[1].replace('/', "#&&#");
a[i] = tk[0]+space+rename
return a
def decode_file_rename(path):
sep = '#@@#';
return path.replace(sep, " -> ").replace("#&&#", '/');
def get_head_revision():
return subprocess.check_output(('git', 'rev-parse', 'HEAD')).decode().replace("\n", "")
def process_branch_diff(branch, fromrev, add_status, nspaces, utf8):
if fromrev == 'HEAD':
fromrev = get_head_revision()
proc = subprocess.Popen(('git', 'rev-list', '--boundary', fromrev+'...'+branch), stdout=subprocess.PIPE)
proc1 = subprocess.Popen(("grep", "^-"), stdin=proc.stdout, stdout=subprocess.PIPE)
proc2 = subprocess.Popen(('cut', "-c2-"), stdin=proc1.stdout, stdout=subprocess.PIPE)
output = subprocess.check_output(('head', "-n1"), stdin=proc2.stdout).decode()
proc2.wait()
lines = output.split('\n')
if len(lines) and len(lines[0]):
branchnode = lines[0]
tilde = '~' if fromrev in branchnode else ''
file_mode = '--name-status' if add_status else '--name-only'
output = subprocess.check_output(('git', 'diff', file_mode, branchnode+tilde+'..'+fromrev)).decode()
files = output.split('\n')
arr = paths_to_dict(files, add_status)
FileTreeDict(arr, nspaces, utf8).show()
def process_diff(revs, add_status, nspaces, utf8):
rev0 = revs[0]
if len(revs) > 1:
rev1 = revs[1]
else:
rev1 = 'HEAD'
if rev0 == 'HEAD':
rev0 = get_head_revision()
if rev1 == 'HEAD':
rev1 = get_head_revision()
file_mode = '--name-status' if add_status else '--name-only'
tilde = '~' if (rev0 in rev1) or (rev1 in rev0) else ''
output = subprocess.check_output(('git', 'diff', file_mode, rev0+tilde+'..'+rev1)).decode()
files = output.split('\n')
arr = paths_to_dict(files, add_status)
FileTreeDict(arr, nspaces, utf8).show()
def main():
parser = argparse.ArgumentParser(description = 'Shows a tree-form view of modified files in a branch or within a range of revisions')
parser.add_argument('-s', '--status', help = 'Include file status occured in the diff', action='store_true')
parser.add_argument('-u', '--utf8', help = 'Use utf8 characters for tree nodes', action='store_true', default=False)
parser.add_argument('-n', '--nspaces', metavar='num', help = 'Number of spaces in each depth level', type=int, default=4)
group1 = parser.add_argument_group('Ancestor mode', 'Show modified files in the current branch')
group1.add_argument('-b', '--branch', help='Base branch or revision, must be an ancestor of <from>. It finds the oldest common ancestor between the branch and the HEAD of current branch')
group1.add_argument('-f', '--from', metavar='rev', dest='revfrom', help='Revision from which to obtain the changes. If not provided, uses the HEAD of the current branch. Must be used in conjunction with --branch', default='HEAD')
group2 = parser.add_argument_group('Revision diff', 'Show modified files between two arbitrary revisions')
group2.add_argument('-r', '--rev', metavar='rev', nargs='+', help='Revisions to compare with. If only one revision is specified then that revision is compared to the working directory, and, when no revisions are specified, the working directory files are compared to its first parent')
args = parser.parse_args()
# print(args)
if args.rev is None:
return process_branch_diff(args.branch, args.revfrom, args.status, args.nspaces, args.utf8)
else:
return process_diff(args.rev, args.status, args.nspaces, args.utf8)
if __name__ == "__main__":
main()