Added interactive diff gui
[ugit.git] / py / utils.py
blob8a2b228b5a095c279a31a30cf0661b9d9277f61c
1 #!/usr/bin/env python
2 import os
3 import re
4 import time
5 import commands
6 from cStringIO import StringIO
8 KNOWN_FILE_TYPES = {
9 'ascii c': 'c.png',
10 'python': 'script.png',
11 'ruby': 'script.png',
12 'shell': 'script.png',
13 'perl': 'script.png',
14 'java': 'script.png',
15 'assembler': 'binary.png',
16 'binary': 'binary.png',
17 'byte': 'binary.png',
18 'image': 'image.png',
21 ICONSDIR = os.path.join (os.path.dirname (__file__), 'icons')
23 def ident_file_type (filename):
24 '''Returns an icon based on the contents of filename.'''
25 if os.path.exists (filename):
26 quoted_filename = shell_quote (filename)
27 fileinfo = commands.getoutput('file -b %s' % quoted_filename)
28 for filetype, iconname in KNOWN_FILE_TYPES.iteritems():
29 if filetype in fileinfo.lower():
30 return iconname
31 else:
32 return 'removed.png'
33 # Fallback for modified files of an unknown type
34 return 'generic.png'
36 def get_icon (filename):
37 '''Returns the full path to an icon file corresponding to
38 filename's contents.'''
39 icon_file = ident_file_type (filename)
40 return os.path.join (ICONSDIR, icon_file)
42 def get_staged_icon (filename):
43 '''Special-case method for staged items. These are only
44 ever 'staged' and 'removed' items in the staged list.'''
46 if os.path.exists (filename):
47 return os.path.join (ICONSDIR, 'staged.png')
48 else:
49 return os.path.join (ICONSDIR, 'removed.png')
51 def get_untracked_icon():
52 return os.path.join (ICONSDIR, 'untracked.png')
54 def get_directory_icon():
55 return os.path.join (ICONSDIR, 'dir.png')
57 def get_file_icon():
58 return os.path.join (ICONSDIR, 'generic.png')
60 def shell_quote (*inputs):
61 '''Quote strings so that they can be suitably martialled
62 off to the shell. This method supports POSIX sh syntax.
63 This is crucial to properly handle command line arguments
64 with spaces, quotes, double-quotes, etc.'''
66 regex = re.compile ('[^\w!%+,\-./:@^]')
67 quote_regex = re.compile ("((?:'\\''){2,})")
69 ret = []
70 for input in inputs:
71 if not input:
72 continue
74 if '\x00' in input:
75 raise AssertionError, ('No way to quote strings '
76 'containing null (\\000) bytes')
78 # = does need quoting else in command position it's a
79 # program-local environment setting
80 match = regex.search (input)
81 if match and '=' not in input:
82 # ' -> '\''
83 input = input.replace ("'", "'\\''")
85 # make multiple ' in a row look simpler
86 # '\'''\'''\'' -> '"'''"'
87 quote_match = quote_regex.match (input)
88 if quote_match:
89 quotes = match.group (1)
90 input.replace (quotes,
91 ("'" * (len(quotes)/4)) + "\"'")
93 input = "'%s'" % input
94 if input.startswith ("''"):
95 input = input[2:]
97 if input.endswith ("''"):
98 input = input[:-2]
99 ret.append (input)
100 return ' '.join (ret)
102 def get_tmp_filename():
103 # Allow TMPDIR/TMP with a fallback to /tmp
104 return '.ugit.%s.%s' % ( os.getpid(), time.time() )
106 HEADER_LENGTH = 80
107 def header (msg):
108 pad = HEADER_LENGTH - len (msg) - 4 # len (':+') + len ('+:')
109 extra = pad % 2
110 pad /= 2
111 return (':+'
112 + (' ' * pad)
113 + msg
114 + (' ' * (pad + extra))
115 + '+:'
116 + '\n')
118 class DiffParser (object):
119 def __init__ (self, diff):
120 self.__diff_header = re.compile ('^@@\s[^@]+\s@@.*')
122 self.__idx = -1
123 self.__diffs = []
124 self.__diff_spans = []
125 self.__diff_offsets = []
127 self.parse_diff (diff)
129 def get_diffs (self):
130 return self.__diffs
132 def get_spans (self):
133 return self.__diff_spans
135 def get_offsets (self):
136 return self.__diff_offsets
138 def get_diff_for_offset (self, offset):
139 for idx, diff_offset in enumerate (self.__diff_offsets):
140 if offset < diff_offset:
141 return os.linesep.join (self.__diffs[idx])
142 return None
144 def get_diffs_for_range (self, start, end):
145 diffs = []
146 for idx, span in enumerate (self.__diff_spans):
148 has_end_of_diff = start >= span[0] and start < span[1]
149 has_all_of_diff = start <= span[0] and end >= span[1]
150 has_head_of_diff = end >= span[0] and end <= span[1]
152 selected_diff = (has_end_of_diff
153 or has_all_of_diff
154 or has_head_of_diff)
156 if selected_diff:
157 diff = os.linesep.join (self.__diffs[idx])
158 diffs.append (diff)
161 return diffs
163 def parse_diff (self, diff):
164 total_offset = 0
165 for idx, line in enumerate (diff.splitlines()):
167 if self.__diff_header.match (line):
168 self.__diffs.append ( [line] )
170 line_len = len (line) + 1
171 self.__diff_spans.append ([total_offset,
172 total_offset + line_len])
174 total_offset += line_len
175 self.__diff_offsets.append (total_offset)
177 self.__idx += 1
178 else:
179 if self.__idx < 0:
180 errmsg = 'Malformed diff?\n\n%s' % diff
181 raise AssertionError, errmsg
183 line_len = len (line) + 1
184 total_offset += line_len
186 self.__diffs[self.__idx].append (line)
187 self.__diff_spans[-1][-1] += line_len
188 self.__diff_offsets[self.__idx] += line_len