aboutsummaryrefslogtreecommitdiff
path: root/git/util.py
blob: ec1ece1e732d4965d206dcc051500b65040fc8ed (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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# 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
import platform

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
						)

__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')

#{ Utility Methods

def stream_copy(source, destination, chunk_size=512*1024):
	"""Copy all data from the source stream into the destination stream in chunks
	of size chunk_size
	
	:return: amount of bytes written"""
	br = 0
	while True:
		chunk = source.read(chunk_size)
		destination.write(chunk)
		br += len(chunk)
		if len(chunk) < chunk_size:
			break
	# END reading output stream
	return br
	
	
def get_user_id():
	""":return: string identifying the currently active system user as name@node
	:note: user can be set with the 'USER' environment variable, usually set on windows"""
	ukn = 'UNKNOWN'
	username = os.environ.get('USER', os.environ.get('USERNAME', ukn))
	if username == ukn and hasattr(os, 'getlogin'):
		username = os.getlogin()
	# END get username from login
	return "%s@%s" % (username, platform.node())

#} END utilities

#{ 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()


	

class IterableList(list):
	"""
	List of iterable objects allowing to query an object by id or by named index::
	 
	 heads = repo.heads
	 heads.master
	 heads['master']
	 heads[0]
	 
	It requires an id_attribute name to be set which will be queried from its 
	contained items to have a means for comparison.
	
	A prefix can be specified which is to be used in case the id returned by the 
	items always contains a prefix that does not matter to the user, so it 
	can be left out."""
	__slots__ = ('_id_attr', '_prefix')
	
	def __new__(cls, id_attr, prefix=''):
		return super(IterableList,cls).__new__(cls)
		
	def __init__(self, id_attr, prefix=''):
		self._id_attr = id_attr
		self._prefix = prefix
		if not isinstance(id_attr, basestring):
			raise ValueError("First parameter must be a string identifying the name-property. Extend the list after initialization")
		# END help debugging !
		
	def __getattr__(self, attr):
		attr = self._prefix + attr
		for item in self:
			if getattr(item, self._id_attr) == attr:
				return item
		# END for each item
		return list.__getattribute__(self, attr)
		
	def __getitem__(self, index):
		if isinstance(index, int):
			return list.__getitem__(self,index)
		
		try:
			return getattr(self, index)
		except AttributeError:
			raise IndexError( "No item found with id %r" % (self._prefix + index) )
			

#} END classes