Update email adress and git respository adress.
[hgct.git] / git.py
blob0a981d78f15ce183f59a20ef03997180c20f8360
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.
6 #
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
17 from sets import Set
18 from ctcore import *
20 def initialize():
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."
32 sys.exit(1)
34 files = runProgram(['git-diff-files', '--name-only', '-z']).split('\0')
35 files.pop()
36 updateIndex(['--remove'], files)
38 class GitFile(File):
39 def __init__(self, unknown=False):
40 File.__init__(self)
41 self.unknown = unknown
43 def code(self):
44 '''Only defined for non-unknown files'''
45 assert(self.text != None)
46 return (self.text, self.dstSHA, self.dstMode)
48 def getPatchImpl(self):
49 if self.unknown:
50 updateIndex(['--add'], [self.srcName])
51 patch = runProgram(['git-diff-index', '-p', '--cached', 'HEAD', '--', self.srcName])
52 updateIndex(['--force-remove'], [self.srcName])
53 return patch
54 elif self.change == 'C' or self.change == 'R':
55 return getPatch(self.srcName, self.dstName)
56 else:
57 return getPatch(self.srcName)
59 class GitFileSet(FileSet):
60 def __init__(self, addCallback, removeCallback):
61 FileSet.__init__(self, addCallback, removeCallback)
62 self.codeDict = {}
64 def add(self, file):
65 if not file.unknown:
66 self.codeDict[file.code()] = file
67 FileSet.add(self, file)
69 def remove(self, file):
70 if not file.unknown:
71 del self.codeDict[file.code()]
72 FileSet.remove(self, file)
74 def getByCode(self, file):
75 if file.unknown:
76 return None
77 else:
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]*)')
84 def parseDiff(prog):
85 inp = runProgram(prog)
86 ret = []
87 try:
88 recs = inp.split("\0")
89 recs.pop() # remove last entry (which is '')
90 it = recs.__iter__()
91 while True:
92 rec = it.next()
93 m = parseDiffRE.match(rec)
95 if not m:
96 print "Unknown output from " + str(prog) + "!: " + rec + "\n"
97 continue
99 f = GitFile()
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':
105 f.change = 'A'
106 else:
107 f.change = m.group(5)
108 f.score = m.group(6)
109 f.srcName = f.dstName = it.next()
111 if f.change == 'C' or f.change == 'R':
112 f.dstName = it.next()
114 ret.append(f)
115 except StopIteration:
116 pass
117 return ret
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):
128 for a in args:
129 assert(type(a) is list)
130 steps = range(10, len(args), 10)
131 prog = origProg[:]
132 prev = 0
133 for i in steps:
134 for a in args[prev:i]:
135 prog.extend(a)
136 runProgram(prog)
137 prog = origProg[:]
138 prev = i
140 for a in args[prev:]:
141 prog.extend(a)
142 runProgram(prog)
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
147 # join.
148 assert(type(fileNames) is list)
150 runProgram(['git-update-index'] + args + ['-z', '--stdin'], input='\0'.join(fileNames)+'\0')
152 def getUnknownFiles():
153 args = []
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 '')
165 fileObjects = []
167 for fileName in files:
168 f = GitFile(unknown=True)
169 f.srcName = f.dstName = fileName
170 f.change = '?'
172 fileObjects.append(f)
173 f.text = 'New file: ' + fileName
175 return fileObjects
177 def getChangedFiles():
178 files = parseDiff('git-diff-index -z -M --cached HEAD')
179 for f in files:
180 c = f.change
181 if c == 'C':
182 f.text = 'Copy from ' + f.srcName + ' to ' + f.dstName
183 elif c == 'R':
184 f.text = 'Rename from ' + f.srcName + ' to ' + f.dstName
185 elif c == 'A':
186 f.text = 'New file: ' + f.srcName
187 elif c == 'D':
188 f.text = 'Deleted file: ' + f.srcName
189 elif c == 'T':
190 f.text = 'Type change: ' + f.srcName
191 else:
192 f.text = f.srcName
193 return files
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()
202 for f in fileSet:
203 markForDeletion.add(f)
205 if settings().showUnknown:
206 unknowns = getUnknownFiles()
207 else:
208 unknowns = []
210 files = getChangedFiles()
212 for f in itertools.chain(files, unknowns):
213 fs = fileSet.getByCode(f)
214 if fs:
215 markForDeletion.discard(fs)
216 else:
217 fileSet.add(f)
219 for f in markForDeletion:
220 fileSet.remove(f)
222 def getPatch(file, otherFile = None):
223 if otherFile:
224 f = [file, otherFile]
225 else:
226 f = [file]
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
233 # function.
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 \
247 f.change != '?'])
249 updateIndex(['--add'], [f.dstName for f in filesToCommit if f.change == '?'])
251 tree = runProgram(['git-write-tree'])
252 tree = tree.rstrip()
254 if commitIsMerge():
255 merge = ['-p', 'MERGE_HEAD']
256 else:
257 merge = []
258 commit = runProgram(['git-commit-tree', tree, '-p', 'HEAD'] + merge, msg).rstrip()
260 runProgram(['git-update-ref', 'HEAD', commit])
262 try:
263 os.unlink(os.environ['GIT_DIR'] + '/MERGE_HEAD')
264 except OSError:
265 pass
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 \
271 f.change != '?'])
272 updateIndex(['--remove'], [f.srcName for f in filesToKeep if f.change == 'R'])
274 def discardFile(file):
275 runProgram(['git-read-tree', 'HEAD'])
276 c = file.change
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
282 # too dangerous.
283 pass
284 elif c == 'D':
285 runProgram(['git-checkout-index', '-f', '-q', '--', file.dstName])
286 elif c == 'R':
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):
298 return
299 exclude = open(excludefile, 'a')
300 print >> exclude, ignoreExpr
301 exclude.close()
303 pass
305 def commitIsMerge():
306 try:
307 os.stat(os.environ['GIT_DIR'] + '/MERGE_HEAD')
308 return True
309 except OSError:
310 return False
312 def mergeMessage():
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
317 # time.
318 cachedBranch = None
319 prevStat = None
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:
325 return cachedBranch
327 prevStat = newStat
329 b = runProgram(['git-symbolic-ref', 'HEAD'])
330 cachedBranch = b.rstrip().replace('refs/heads/', '', 1)
331 return cachedBranch