summaryrefslogtreecommitdiff
path: root/lib/git/base.py
blob: 4e5298e48960c8b868c3ace3f717fdb689df0d9e (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
# base.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

class LazyMixin(object):
	lazy_properties = []
	__slots__ = tuple()
	
	def __getattr__(self, attr):
		"""
		Whenever an attribute is requested that we do not know, we allow it 
		to be created and set. Next time the same attribute is reqeusted, it is simply
		returned from our dict/slots.
		"""
		self._set_cache_(attr)
		# will raise in case the cache was not created
		return object.__getattribute__(self, attr)

	def _set_cache_(self, attr):
		""" This method should be overridden in the derived class. 
		It should check whether the attribute named by attr can be created
		and cached. Do nothing if you do not know the attribute or call your subclass
		
		The derived class may create as many additional attributes as it deems 
		necessary in case a git command returns more information than represented 
		in the single attribute."""
		pass
	
		
class Object(LazyMixin):
	"""
	Implements an Object which may be Blobs, Trees, Commits and Tags
	"""
	TYPES = ("blob", "tree", "commit", "tag")
	__slots__ = ("repo", "id", "size", "data" )
	type = None			# to be set by subclass
	
	def __init__(self, repo, id):
		"""
		Initialize an object by identifying it by its id. All keyword arguments
		will be set on demand if None.
		
		``repo``
			repository this object is located in
			
		``id``
			SHA1 or ref suitable for git-rev-parse
		"""
		super(Object,self).__init__()
		self.repo = repo
		self.id = id
		
	def _set_self_from_args_(self, args_dict):
		"""
		Initialize attributes on self from the given dict that was retrieved
		from locals() in the calling method.
		
		Will only set an attribute on self if the corresponding value in args_dict
		is not None
		"""
		for attr, val in args_dict.items():
			if attr != "self" and val is not None:
				setattr( self, attr, val )
		# END set all non-None attributes
	
	def _set_cache_(self, attr):
		"""
		Retrieve object information
		"""
		if attr  == "size":
			self.size = int(self.repo.git.cat_file(self.id, s=True).rstrip())
		elif attr == "data":
			self.data = self.repo.git.cat_file(self.id, p=True, with_raw_output=True)
		
	def __eq__(self, other):
		"""
		Returns
			True if the objects have the same SHA1
		"""
		return self.id == other.id
		
	def __ne__(self, other):
		"""
		Returns
			True if the objects do not have the same SHA1
		"""
		return self.id != other.id
		
	def __hash__(self):
		"""
		Returns
			Hash of our id allowing objects to be used in dicts and sets
		"""
		return hash(self.id)
		
	def __str__(self):
		"""
		Returns
			string of our SHA1 as understood by all git commands
		"""
		return self.id
		
	def __repr__(self):
		"""
		Returns
			string with pythonic representation of our object
		"""
		return '<git.%s "%s">' % (self.__class__.__name__, self.id)
	
	@property
	def id_abbrev(self):
		"""
		Returns
			First 7 bytes of the commit's sha id as an abbreviation of the full string.
		"""
		return self.id[0:7]
	
	@classmethod
	def get_type_by_name(cls, object_type_name):
		"""
		Returns
			type suitable to handle the given object type name.
			Use the type to create new instances.
			
		``object_type_name``
			Member of TYPES
			
		Raises
			ValueError: In case object_type_name is unknown
		"""
		if object_type_name == "commit":
			import commit
			return commit.Commit
		elif object_type_name == "tag":
			import tag
			return tag.TagObject
		elif object_type_name == "blob":
			import blob
			return blob.Blob
		elif object_type_name == "tree":
			import tree
			return tree.Tree
		else:
			raise ValueError("Cannot handle unknown object type: %s" % object_type_name)
		
		
class IndexObject(Object):
	"""
	Base for all objects that can be part of the index file , namely Tree, Blob and
	SubModule objects
	"""
	__slots__ = ("path", "mode") 
	
	def __init__(self, repo, id, mode=None, path=None):
		"""
		Initialize a newly instanced IndexObject
		``repo``
			is the Repo we are located in

		``id`` : string
			is the git object id as hex sha

		``mode`` : int
			is the file mode as int, use the stat module to evaluate the infomration

		``path`` : str
			is the path to the file in the file system, relative to the git repository root, i.e.
			file.ext or folder/other.ext
		"""
		super(IndexObject, self).__init__(repo, id)
		if isinstance(mode, basestring):
			mode = self._mode_str_to_int(mode)
		self.mode = mode
		self.path = path
	
	@classmethod
	def _mode_str_to_int( cls, modestr ):
		"""
		``modestr``
			string like 755 or 644 or 100644 - only the last 3 chars will be used
			
		Returns
			String identifying a mode compatible to the mode methods ids of the 
			stat module regarding the rwx permissions for user, group and other
		"""
		mode = 0
		for iteration,char in enumerate(reversed(modestr[-3:])):
			mode += int(char) << iteration*3
		# END for each char
		return mode
		
	@property
	def basename(self):
	  """
	  Returns
		  The basename of the IndexObject's file path
	  """
	  return os.path.basename(self.path)
	  
		
class Ref(object):
	"""
	Represents a named reference to any object
	"""
	__slots__ = ("path", "object")
	
	def __init__(self, path, object = None):
		"""
		Initialize this instance
		
		``path``
			Path relative to the .git/ directory pointing to the ref in question, i.e.
			refs/heads/master
			
		``object``
			Object instance, will be retrieved on demand if None
		"""
		self.path = path
		self.object = object
		
	def __str__(self):
		return self.name()
		
	def __repr__(self):
		return '<git.%s "%s">' % (self.__class__.__name__, self.path)
		
	def __eq__(self, other):
		return self.path == other.path and self.object == other.object
		
	def __ne__(self, other):
		return not ( self == other )
		
	def __hash__(self):
		return hash(self.path)
		
	@property
	def name(self):
		"""
		Returns
			(shortest) Name of this reference - it may contain path components
		"""
		# first two path tokens are can be removed as they are 
		# refs/heads or refs/tags or refs/remotes
		tokens = self.path.split('/')
		if len(tokens) < 3:
			return self.path	# could be refs/HEAD
		
		return '/'.join(tokens[2:])
		
	@classmethod
	def find_all(cls, repo, common_path = "refs", **kwargs):
		"""
		Find all refs in the repository

		``repo``
			is the Repo

		``common_path``
			Optional keyword argument to the path which is to be shared by all
			returned Ref objects

		``kwargs``
			Additional options given as keyword arguments, will be passed
			to git-for-each-ref

		Returns
			git.Ref[]
			
			List is sorted by committerdate
			The returned objects are compatible to the Ref base, but represent the 
			actual type, such as Head or Tag
		"""

		options = {'sort': "committerdate",
				   'format': "%(refname)%00%(objectname)%00%(objecttype)%00%(objectsize)"}
				   
		options.update(kwargs)

		output = repo.git.for_each_ref(common_path, **options)
		return cls.list_from_string(repo, output)

	@classmethod
	def list_from_string(cls, repo, text):
		"""
		Parse out ref information into a list of Ref compatible objects

		``repo``
			is the Repo
		``text``
			is the text output from the git-for-each-ref command

		Returns
			git.Ref[]
			
			list of Ref objects
		"""
		heads = []

		for line in text.splitlines():
			heads.append(cls.from_string(repo, line))

		return heads

	@classmethod
	def from_string(cls, repo, line):
		"""
		Create a new Ref instance from the given string.

		``repo``
			is the Repo

		``line``
			is the formatted ref information

		Format::
		
			name: [a-zA-Z_/]+
			<null byte>
			id: [0-9A-Fa-f]{40}

		Returns
			git.Head
		"""
		full_path, hexsha, type_name, object_size = line.split("\x00")
		obj = Object.get_type_by_name(type_name)(repo, hexsha)
		obj.size = object_size
		return cls(full_path, obj)