summaryrefslogtreecommitdiff
path: root/git/util.py
blob: a1d0cb12633207cc90d1eff76fddeeb427df6eb6 (plain)
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
# utils.py
# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php

import os
import re
import sys
import time
import tempfile

from gitdb.util import (
							make_sha, 
							LockedFD, 
							file_contents_ro, 
							LazyMixin, 
							to_hex_sha, 
							to_bin_sha,
							join_path, 
							join_path_native,
							to_native_path_linux,
							to_native_path_windows,
							assure_directory_exists,
							LockFile,
							BlockingLockFile,
							Actor,
							Iterable,
							stream_copy,
							IterableList,
							get_user_id
						)

__all__ = ( "stream_copy", "join_path", "to_native_path_windows", "to_native_path_linux", 
			"join_path_native", "Stats", "IndexFileSHA1Writer", "Iterable", "IterableList", 
			"BlockingLockFile", "LockFile", 'Actor', 'get_user_id', 'assure_directory_exists',
			'RemoteProgress', 'RepoAliasMixin')

#{ Classes

class RemoteProgress(object):
	"""
	Handler providing an interface to parse progress information emitted by git-push
	and git-fetch and to dispatch callbacks allowing subclasses to react to the progress.
	"""
	_num_op_codes = 5
	BEGIN, END, COUNTING, COMPRESSING, WRITING =  [1 << x for x in range(_num_op_codes)]
	STAGE_MASK = BEGIN|END
	OP_MASK = ~STAGE_MASK
	
	__slots__ = ("_cur_line", "_seen_ops")
	re_op_absolute = re.compile("(remote: )?([\w\s]+):\s+()(\d+)()(.*)")
	re_op_relative = re.compile("(remote: )?([\w\s]+):\s+(\d+)% \((\d+)/(\d+)\)(.*)")
	
	def __init__(self):
		self._seen_ops = list()
	
	def _parse_progress_line(self, line):
		"""Parse progress information from the given line as retrieved by git-push
		or git-fetch
		
		:return: list(line, ...) list of lines that could not be processed"""
		# handle
		# Counting objects: 4, done. 
		# Compressing objects:	50% (1/2)	\rCompressing objects: 100% (2/2)	\rCompressing objects: 100% (2/2), done.
		self._cur_line = line
		sub_lines = line.split('\r')
		failed_lines = list()
		for sline in sub_lines:
			# find esacpe characters and cut them away - regex will not work with 
			# them as they are non-ascii. As git might expect a tty, it will send them
			last_valid_index = None
			for i,c in enumerate(reversed(sline)):
				if ord(c) < 32:
					# its a slice index
					last_valid_index = -i-1 
				# END character was non-ascii
			# END for each character in sline
			if last_valid_index is not None:
				sline = sline[:last_valid_index]
			# END cut away invalid part
			sline = sline.rstrip()
			
			cur_count, max_count = None, None
			match = self.re_op_relative.match(sline)
			if match is None:
				match = self.re_op_absolute.match(sline)
				
			if not match:
				self.line_dropped(sline)
				failed_lines.append(sline)
				continue
			# END could not get match
			
			op_code = 0
			remote, op_name, percent, cur_count, max_count, message = match.groups()
			
			# get operation id
			if op_name == "Counting objects":
				op_code |= self.COUNTING
			elif op_name == "Compressing objects":
				op_code |= self.COMPRESSING
			elif op_name == "Writing objects":
				op_code |= self.WRITING
			else:
				raise ValueError("Operation name %r unknown" % op_name)
			
			# figure out stage
			if op_code not in self._seen_ops:
				self._seen_ops.append(op_code)
				op_code |= self.BEGIN
			# END begin opcode
			
			if message is None:
				message = ''
			# END message handling
			
			message = message.strip()
			done_token = ', done.'
			if message.endswith(done_token):
				op_code |= self.END
				message = message[:-len(done_token)]
			# END end message handling
			
			self.update(op_code, cur_count, max_count, message)
			self(message)
		# END for each sub line
		return failed_lines
	
	def line_dropped(self, line):
		"""Called whenever a line could not be understood and was therefore dropped."""
		pass
	
	def update(self, op_code, cur_count, max_count=None, message=''):
		"""Called whenever the progress changes
		
		:param op_code:
			Integer allowing to be compared against Operation IDs and stage IDs.
			
			Stage IDs are BEGIN and END. BEGIN will only be set once for each Operation 
			ID as well as END. It may be that BEGIN and END are set at once in case only
			one progress message was emitted due to the speed of the operation.
			Between BEGIN and END, none of these flags will be set
			
			Operation IDs are all held within the OP_MASK. Only one Operation ID will 
			be active per call.
		:param cur_count: Current absolute count of items
			
		:param max_count:
			The maximum count of items we expect. It may be None in case there is 
			no maximum number of items or if it is (yet) unknown.
		
		:param message:
			In case of the 'WRITING' operation, it contains the amount of bytes
			transferred. It may possibly be used for other purposes as well.
		
		You may read the contents of the current line in self._cur_line"""
		pass
	
	def __call__(self, message):
		"""Same as update, but with a simpler interface which only provides the
		message of the operation
		:note: This method will be called in addition to the update method. It is 
			up to you which one you implement"""
		pass

class RepoAliasMixin(object):
	"""Simple utility providing a repo-property which resolves to the 'odb' attribute
	of the actual type. This is for api compatability only, as the types previously
	held repository instances, now they hold odb instances instead"""
	__slots__ = tuple()
	
	@property
	def repo(self):
		return self.odb
	

class Stats(object):
	"""
	Represents stat information as presented by git at the end of a merge. It is 
	created from the output of a diff operation.
	
	``Example``::
	
	 c = Commit( sha1 )
	 s = c.stats
	 s.total		 # full-stat-dict
	 s.files		 # dict( filepath : stat-dict )
	 
	``stat-dict``
	
	A dictionary with the following keys and values::
	 
	  deletions = number of deleted lines as int
	  insertions = number of inserted lines as int
	  lines = total number of lines changed as int, or deletions + insertions
	  
	``full-stat-dict``
	
	In addition to the items in the stat-dict, it features additional information::
	
	 files = number of changed files as int"""
	__slots__ = ("total", "files")
	
	def __init__(self, total, files):
		self.total = total
		self.files = files

	@classmethod
	def _list_from_string(cls, repo, text):
		"""Create a Stat object from output retrieved by git-diff.
		
		:return: git.Stat"""
		hsh = {'total': {'insertions': 0, 'deletions': 0, 'lines': 0, 'files': 0}, 'files': dict()}
		for line in text.splitlines():
			(raw_insertions, raw_deletions, filename) = line.split("\t")
			insertions = raw_insertions != '-' and int(raw_insertions) or 0
			deletions = raw_deletions != '-' and int(raw_deletions) or 0
			hsh['total']['insertions'] += insertions
			hsh['total']['deletions'] += deletions
			hsh['total']['lines'] += insertions + deletions
			hsh['total']['files'] += 1
			hsh['files'][filename.strip()] = {'insertions': insertions,
											  'deletions': deletions,
											  'lines': insertions + deletions}
		return Stats(hsh['total'], hsh['files'])


class IndexFileSHA1Writer(object):
	"""Wrapper around a file-like object that remembers the SHA1 of 
	the data written to it. It will write a sha when the stream is closed
	or if the asked for explicitly usign write_sha.
	
	Only useful to the indexfile
	
	:note: Based on the dulwich project"""
	__slots__ = ("f", "sha1")
	
	def __init__(self, f):
		self.f = f
		self.sha1 = make_sha("")

	def write(self, data):
		self.sha1.update(data)
		return self.f.write(data)

	def write_sha(self):
		sha = self.sha1.digest()
		self.f.write(sha)
		return sha

	def close(self):
		sha = self.write_sha()
		self.f.close()
		return sha

	def tell(self):
		return self.f.tell()


#} END classes