1 # Copyright (c) 2005 Fredrik Kuivinen <frekui@gmail.com>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License version 2 as
5 # published by the Free Software Foundation.
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 import sys
, os
, re
, itertools
21 if not os
.environ
.has_key('GIT_DIR'):
22 os
.environ
['GIT_DIR'] = '.git'
24 if not os
.environ
.has_key('GIT_OBJECT_DIRECTORY'):
25 os
.environ
['GIT_OBJECT_DIRECTORY'] = os
.environ
['GIT_DIR'] + '/objects'
27 if not (os
.path
.exists(os
.environ
['GIT_DIR']) and
28 os
.path
.exists(os
.environ
['GIT_DIR'] + '/refs') and
29 os
.path
.exists(os
.environ
['GIT_OBJECT_DIRECTORY'])):
30 print "Git archive not found."
31 print "Make sure that the current working directory contains a '.git' directory, or\nthat GIT_DIR is set appropriately."
34 files
= runProgram(['git-diff-files', '--name-only', '-z']).split('\0')
36 updateIndex(['--remove'], files
)
39 def __init__(self
, unknown
=False):
41 self
.unknown
= unknown
44 '''Only defined for non-unknown files'''
45 assert(self
.text
!= None)
46 return (self
.text
, self
.dstSHA
, self
.dstMode
)
48 def getPatchImpl(self
):
50 updateIndex(['--add'], [self
.srcName
])
51 patch
= runProgram(['git-diff-index', '-p', '--cached', 'HEAD', '--', self
.srcName
])
52 updateIndex(['--force-remove'], [self
.srcName
])
54 elif self
.change
== 'C' or self
.change
== 'R':
55 return getPatch(self
.srcName
, self
.dstName
)
57 return getPatch(self
.srcName
)
59 class GitFileSet(FileSet
):
60 def __init__(self
, addCallback
, removeCallback
):
61 FileSet
.__init
__(self
, addCallback
, removeCallback
)
66 self
.codeDict
[file.code()] = file
67 FileSet
.add(self
, file)
69 def remove(self
, file):
71 del self
.codeDict
[file.code()]
72 FileSet
.remove(self
, file)
74 def getByCode(self
, file):
78 return self
.codeDict
.get(file.code())
80 def fileSetFactory(addCallback
, removeCallback
):
81 return GitFileSet(addCallback
, removeCallback
)
83 parseDiffRE
= re
.compile(':([0-9]+) ([0-9]+) ([0-9a-f]{40}) ([0-9a-f]{40}) ([MCRNADUT])([0-9]*)')
85 inp
= runProgram(prog
)
88 recs
= inp
.split("\0")
89 recs
.pop() # remove last entry (which is '')
93 m
= parseDiffRE
.match(rec
)
96 print "Unknown output from " + str(prog
) + "!: " + rec
+ "\n"
100 f
.srcMode
= m
.group(1)
101 f
.dstMode
= m
.group(2)
102 f
.srcSHA
= m
.group(3)
103 f
.dstSHA
= m
.group(4)
104 if m
.group(5) == 'N':
107 f
.change
= m
.group(5)
109 f
.srcName
= f
.dstName
= it
.next()
111 if f
.change
== 'C' or f
.change
== 'R':
112 f
.dstName
= it
.next()
115 except StopIteration:
119 # origProg is a sequence of strings the first element is the program
120 # name and subsequent elements are arguments. args is a sequence of
121 # sequences. The function will repeatedly feed
123 # origProg.extend(flatten(args[i:j]))
125 # for some indices i and j to runProgram in such a way that every
126 # sequence in args is fed to runProgram exactly once.
127 def runXargsStyle(origProg
, args
):
129 assert(type(a
) is list)
130 steps
= range(10, len(args
), 10)
134 for a
in args
[prev
:i
]:
140 for a
in args
[prev
:]:
144 def updateIndex(args
, fileNames
):
145 # Make sure we don't get one single string as fileNames. As
146 # strings are sequences strange things happen in the call to
148 assert(type(fileNames
) is list)
150 runProgram(['git-update-index'] + args
+ ['-z', '--stdin'], input='\0'.join(fileNames
)+'\0')
152 def getUnknownFiles():
155 if settings().gitExcludeFile():
156 if os
.path
.exists(settings().gitExcludeFile()):
157 args
.append('--exclude-from=' + settings().gitExcludeFile())
158 if settings().gitExcludeDir():
159 args
.append('--exclude-per-directory=' + settings().gitExcludeDir())
161 inp
= runProgram(['git-ls-files', '-z', '--others'] + args
)
162 files
= inp
.split("\0")
163 files
.pop() # remove last entry (which is '')
167 for fileName
in files
:
168 f
= GitFile(unknown
=True)
169 f
.srcName
= f
.dstName
= fileName
172 fileObjects
.append(f
)
173 f
.text
= 'New file: ' + fileName
177 def getChangedFiles():
178 files
= parseDiff('git-diff-index -z -M --cached HEAD')
182 f
.text
= 'Copy from ' + f
.srcName
+ ' to ' + f
.dstName
184 f
.text
= 'Rename from ' + f
.srcName
+ ' to ' + f
.dstName
186 f
.text
= 'New file: ' + f
.srcName
188 f
.text
= 'Deleted file: ' + f
.srcName
190 f
.text
= 'Type change: ' + f
.srcName
195 # HEAD is src in the returned File objects. That is, srcName is the
196 # name in HEAD and dstName is the name in the cache.
197 def updateFiles(fileSet
):
198 files
= parseDiff('git-diff-files -z')
199 updateIndex(['--remove', '--add', '--replace'], [f
.srcName
for f
in files
])
201 markForDeletion
= Set()
203 markForDeletion
.add(f
)
205 if settings().showUnknown
:
206 unknowns
= getUnknownFiles()
210 files
= getChangedFiles()
212 for f
in itertools
.chain(files
, unknowns
):
213 fs
= fileSet
.getByCode(f
)
215 markForDeletion
.discard(fs
)
219 for f
in markForDeletion
:
222 def getPatch(file, otherFile
= None):
224 f
= [file, otherFile
]
227 return runProgram(['git-diff-index', '-p', '-M', '--cached', 'HEAD', '--'] + f
)
229 def doCommit(filesToKeep
, filesToCommit
, msg
):
230 # If we have a new file in the cache which we do not want to
231 # commit we have to remove it from the cache. We will add this
232 # cache entry back in to the cache at the end of this
234 updateIndex(['--force-remove'],
235 [f
.srcName
for f
in filesToKeep
if f
.change
== 'A'])
237 updateIndex(['--force-remove'],
238 [f
.dstName
for f
in filesToKeep
if f
.change
== 'R'])
239 runXargsStyle(['git-update-index', '--add', '--replace'],
240 [['--cacheinfo', f
.srcMode
, f
.srcSHA
, f
.srcName
] \
241 for f
in filesToKeep
if f
.change
== 'R'])
243 runXargsStyle(['git-update-index', '--add', '--replace'],
244 [['--cacheinfo', f
.srcMode
, f
.srcSHA
, f
.srcName
] \
245 for f
in filesToKeep
if f
.change
!= 'A' and \
246 f
.change
!= 'R' and \
249 updateIndex(['--add'], [f
.dstName
for f
in filesToCommit
if f
.change
== '?'])
251 tree
= runProgram(['git-write-tree'])
255 merge
= ['-p', 'MERGE_HEAD']
258 commit
= runProgram(['git-commit-tree', tree
, '-p', 'HEAD'] + merge
, msg
).rstrip()
260 runProgram(['git-update-ref', 'HEAD', commit
])
263 os
.unlink(os
.environ
['GIT_DIR'] + '/MERGE_HEAD')
267 # Don't add files that are going to be deleted back to the cache
268 runXargsStyle(['git-update-index', '--add', '--replace'],
269 [['--cacheinfo', f
.dstMode
, f
.dstSHA
, f
.dstName
] \
270 for f
in filesToKeep
if f
.change
!= 'D' and \
272 updateIndex(['--remove'], [f
.srcName
for f
in filesToKeep
if f
.change
== 'R'])
274 def discardFile(file):
275 runProgram(['git-read-tree', 'HEAD'])
277 if c
== 'M' or c
== 'T':
278 runProgram(['git-checkout-index', '-f', '-q', '--', file.dstName
])
279 elif c
== 'A' or c
== 'C':
280 # The file won't be tracked by git now. We could unlink it
281 # from the working directory, but that seems a little bit
285 runProgram(['git-checkout-index', '-f', '-q', '--', file.dstName
])
287 # Same comment applies here as to the 'A' or 'C' case.
288 runProgram(['git-checkout-index', '-f', '-q', '--', file.srcName
])
290 def ignoreFile(file):
291 ignoreExpr
= re
.sub(r
'([][*?!\\])', r
'\\\1', file.dstName
)
293 excludefile
= settings().gitExcludeFile()
294 excludefiledir
= os
.path
.dirname(excludefile
)
295 if not os
.path
.exists(excludefiledir
):
296 os
.mkdir(excludefiledir
)
297 if not os
.path
.isdir(excludefiledir
):
299 exclude
= open(excludefile
, 'a')
300 print >> exclude
, ignoreExpr
307 os
.stat(os
.environ
['GIT_DIR'] + '/MERGE_HEAD')
313 return '''This is a merge commit if you do not want to commit a ''' + \
314 '''merge remove the file $GIT_DIR/MERGE_HEAD.'''
316 # This caching is here to avoid forking and execing git-symbolic-ref all the
320 def getCurrentBranch():
321 global prevStat
, cachedBranch
322 newStat
= list(os
.lstat(os
.environ
['GIT_DIR'] + '/HEAD'))
323 newStat
[7] = 0 # Number 7 is atime and we don't care about atime
324 if newStat
== prevStat
:
329 b
= runProgram(['git-symbolic-ref', 'HEAD'])
330 cachedBranch
= b
.rstrip().replace('refs/heads/', '', 1)