chiark / gitweb /
Only check for upstream merges if not fast-forwarding
[stgit] / stgit / git.py
1 """Python GIT interface
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 import sys, os, popen2, re, gitmergeonefile
22
23 from stgit import basedir
24 from stgit.utils import *
25 from stgit.config import config
26 from sets import Set
27
28 # git exception class
29 class GitException(Exception):
30     pass
31
32
33
34 #
35 # Classes
36 #
37
38 class Person:
39     """An author, committer, etc."""
40     def __init__(self, name = None, email = None, date = '',
41                  desc = None):
42         self.name = self.email = self.date = None
43         if name or email or date:
44             assert not desc
45             self.name = name
46             self.email = email
47             self.date = date
48         elif desc:
49             assert not (name or email or date)
50             def parse_desc(s):
51                 m = re.match(r'^(.+)<(.+)>(.*)$', s)
52                 assert m
53                 return [x.strip() or None for x in m.groups()]
54             self.name, self.email, self.date = parse_desc(desc)
55     def set_name(self, val):
56         if val:
57             self.name = val
58     def set_email(self, val):
59         if val:
60             self.email = val
61     def set_date(self, val):
62         if val:
63             self.date = val
64     def __str__(self):
65         if self.name and self.email:
66             return '%s <%s>' % (self.name, self.email)
67         else:
68             raise GitException, 'not enough identity data'
69
70 class Commit:
71     """Handle the commit objects
72     """
73     def __init__(self, id_hash):
74         self.__id_hash = id_hash
75
76         lines = _output_lines('git-cat-file commit %s' % id_hash)
77         for i in range(len(lines)):
78             line = lines[i]
79             if line == '\n':
80                 break
81             field = line.strip().split(' ', 1)
82             if field[0] == 'tree':
83                 self.__tree = field[1]
84             if field[0] == 'author':
85                 self.__author = field[1]
86             if field[0] == 'committer':
87                 self.__committer = field[1]
88         self.__log = ''.join(lines[i+1:])
89
90     def get_id_hash(self):
91         return self.__id_hash
92
93     def get_tree(self):
94         return self.__tree
95
96     def get_parent(self):
97         parents = self.get_parents()
98         if parents:
99             return parents[0]
100         else:
101             return None
102
103     def get_parents(self):
104         return _output_lines('git-rev-list --parents --max-count=1 %s'
105                              % self.__id_hash)[0].split()[1:]
106
107     def get_author(self):
108         return self.__author
109
110     def get_committer(self):
111         return self.__committer
112
113     def get_log(self):
114         return self.__log
115
116     def __str__(self):
117         return self.get_id_hash()
118
119 # dictionary of Commit objects, used to avoid multiple calls to git
120 __commits = dict()
121
122 #
123 # Functions
124 #
125
126 def get_commit(id_hash):
127     """Commit objects factory. Save/look-up them in the __commits
128     dictionary
129     """
130     global __commits
131
132     if id_hash in __commits:
133         return __commits[id_hash]
134     else:
135         commit = Commit(id_hash)
136         __commits[id_hash] = commit
137         return commit
138
139 def get_conflicts():
140     """Return the list of file conflicts
141     """
142     conflicts_file = os.path.join(basedir.get(), 'conflicts')
143     if os.path.isfile(conflicts_file):
144         f = file(conflicts_file)
145         names = [line.strip() for line in f.readlines()]
146         f.close()
147         return names
148     else:
149         return None
150
151 def _input(cmd, file_desc):
152     p = popen2.Popen3(cmd, True)
153     while True:
154         line = file_desc.readline()
155         if not line:
156             break
157         p.tochild.write(line)
158     p.tochild.close()
159     if p.wait():
160         raise GitException, '%s failed (%s)' % (str(cmd),
161                                                 p.childerr.read().strip())
162
163 def _input_str(cmd, string):
164     p = popen2.Popen3(cmd, True)
165     p.tochild.write(string)
166     p.tochild.close()
167     if p.wait():
168         raise GitException, '%s failed (%s)' % (str(cmd),
169                                                 p.childerr.read().strip())
170
171 def _output(cmd):
172     p=popen2.Popen3(cmd, True)
173     output = p.fromchild.read()
174     if p.wait():
175         raise GitException, '%s failed (%s)' % (str(cmd),
176                                                 p.childerr.read().strip())
177     return output
178
179 def _output_one_line(cmd, file_desc = None):
180     p=popen2.Popen3(cmd, True)
181     if file_desc != None:
182         for line in file_desc:
183             p.tochild.write(line)
184         p.tochild.close()
185     output = p.fromchild.readline().strip()
186     if p.wait():
187         raise GitException, '%s failed (%s)' % (str(cmd),
188                                                 p.childerr.read().strip())
189     return output
190
191 def _output_lines(cmd):
192     p=popen2.Popen3(cmd, True)
193     lines = p.fromchild.readlines()
194     if p.wait():
195         raise GitException, '%s failed (%s)' % (str(cmd),
196                                                 p.childerr.read().strip())
197     return lines
198
199 def __run(cmd, args=None):
200     """__run: runs cmd using spawnvp.
201
202     Runs cmd using spawnvp.  The shell is avoided so it won't mess up
203     our arguments.  If args is very large, the command is run multiple
204     times; args is split xargs style: cmd is passed on each
205     invocation.  Unlike xargs, returns immediately if any non-zero
206     return code is received.  
207     """
208     
209     args_l=cmd.split()
210     if args is None:
211         args = []
212     for i in range(0, len(args)+1, 100):
213         r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
214     if r:
215         return r
216     return 0
217
218 def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
219                   noexclude = True, verbose = False):
220     """Returns a list of pairs - [status, filename]
221     """
222     if verbose and sys.stdout.isatty():
223         print 'Checking for changes in the working directory...',
224         sys.stdout.flush()
225
226     refresh_index()
227
228     if not files:
229         files = []
230     cache_files = []
231
232     # unknown files
233     if unknown:
234         exclude_file = os.path.join(basedir.get(), 'info', 'exclude')
235         base_exclude = ['--exclude=%s' % s for s in
236                         ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
237         base_exclude.append('--exclude-per-directory=.gitignore')
238
239         if os.path.exists(exclude_file):
240             extra_exclude = ['--exclude-from=%s' % exclude_file]
241         else:
242             extra_exclude = []
243         if noexclude:
244             extra_exclude = base_exclude = []
245
246         lines = _output_lines(['git-ls-files', '--others', '--directory']
247                         + base_exclude + extra_exclude)
248         cache_files += [('?', line.strip()) for line in lines]
249
250     # conflicted files
251     conflicts = get_conflicts()
252     if not conflicts:
253         conflicts = []
254     cache_files += [('C', filename) for filename in conflicts]
255
256     # the rest
257     for line in _output_lines(['git-diff-index', tree_id, '--'] + files):
258         fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
259         if fs[1] not in conflicts:
260             cache_files.append(fs)
261
262     if verbose and sys.stdout.isatty():
263         print 'done'
264
265     return cache_files
266
267 def local_changes(verbose = True):
268     """Return true if there are local changes in the tree
269     """
270     return len(__tree_status(verbose = verbose)) != 0
271
272 # HEAD value cached
273 __head = None
274
275 def get_head():
276     """Verifies the HEAD and returns the SHA1 id that represents it
277     """
278     global __head
279
280     if not __head:
281         __head = rev_parse('HEAD')
282     return __head
283
284 def get_head_file():
285     """Returns the name of the file pointed to by the HEAD link
286     """
287     return strip_prefix('refs/heads/',
288                         _output_one_line('git-symbolic-ref HEAD'))
289
290 def set_head_file(ref):
291     """Resets HEAD to point to a new ref
292     """
293     # head cache flushing is needed since we might have a different value
294     # in the new head
295     __clear_head_cache()
296     if __run('git-symbolic-ref HEAD',
297              [os.path.join('refs', 'heads', ref)]) != 0:
298         raise GitException, 'Could not set head to "%s"' % ref
299
300 def __set_head(val):
301     """Sets the HEAD value
302     """
303     global __head
304
305     if not __head or __head != val:
306         if __run('git-update-ref HEAD', [val]) != 0:
307             raise GitException, 'Could not update HEAD to "%s".' % val
308         __head = val
309
310     # only allow SHA1 hashes
311     assert(len(__head) == 40)
312
313 def __clear_head_cache():
314     """Sets the __head to None so that a re-read is forced
315     """
316     global __head
317
318     __head = None
319
320 def refresh_index():
321     """Refresh index with stat() information from the working directory.
322     """
323     __run('git-update-index -q --unmerged --refresh')
324
325 def rev_parse(git_id):
326     """Parse the string and return a verified SHA1 id
327     """
328     try:
329         return _output_one_line(['git-rev-parse', '--verify', git_id])
330     except GitException:
331         raise GitException, 'Unknown revision: %s' % git_id
332
333 def branch_exists(branch):
334     """Existence check for the named branch
335     """
336     branch = os.path.join('refs', 'heads', branch)
337     for line in _output_lines('git-rev-parse --symbolic --all 2>&1'):
338         if line.strip() == branch:
339             return True
340         if re.compile('[ |/]'+branch+' ').search(line):
341             raise GitException, 'Bogus branch: %s' % line
342     return False
343
344 def create_branch(new_branch, tree_id = None):
345     """Create a new branch in the git repository
346     """
347     if branch_exists(new_branch):
348         raise GitException, 'Branch "%s" already exists' % new_branch
349
350     current_head = get_head()
351     set_head_file(new_branch)
352     __set_head(current_head)
353
354     # a checkout isn't needed if new branch points to the current head
355     if tree_id:
356         switch(tree_id)
357
358     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
359         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
360
361 def switch_branch(new_branch):
362     """Switch to a git branch
363     """
364     global __head
365
366     if not branch_exists(new_branch):
367         raise GitException, 'Branch "%s" does not exist' % new_branch
368
369     tree_id = rev_parse(os.path.join('refs', 'heads', new_branch)
370                         + '^{commit}')
371     if tree_id != get_head():
372         refresh_index()
373         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
374             raise GitException, 'git-read-tree failed (local changes maybe?)'
375         __head = tree_id
376     set_head_file(new_branch)
377
378     if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')):
379         os.remove(os.path.join(basedir.get(), 'MERGE_HEAD'))
380
381 def delete_branch(name):
382     """Delete a git branch
383     """
384     if not branch_exists(name):
385         raise GitException, 'Branch "%s" does not exist' % name
386     remove_file_and_dirs(os.path.join(basedir.get(), 'refs', 'heads'),
387                          name)
388
389 def rename_branch(from_name, to_name):
390     """Rename a git branch
391     """
392     if not branch_exists(from_name):
393         raise GitException, 'Branch "%s" does not exist' % from_name
394     if branch_exists(to_name):
395         raise GitException, 'Branch "%s" already exists' % to_name
396
397     if get_head_file() == from_name:
398         set_head_file(to_name)
399     rename(os.path.join(basedir.get(), 'refs', 'heads'),
400            from_name, to_name)
401
402     reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads')
403     if os.path.exists(reflog_dir) \
404            and os.path.exists(os.path.join(reflog_dir, from_name)):
405         rename(reflog_dir, from_name, to_name)
406
407 def add(names):
408     """Add the files or recursively add the directory contents
409     """
410     # generate the file list
411     files = []
412     for i in names:
413         if not os.path.exists(i):
414             raise GitException, 'Unknown file or directory: %s' % i
415
416         if os.path.isdir(i):
417             # recursive search. We only add files
418             for root, dirs, local_files in os.walk(i):
419                 for name in [os.path.join(root, f) for f in local_files]:
420                     if os.path.isfile(name):
421                         files.append(os.path.normpath(name))
422         elif os.path.isfile(i):
423             files.append(os.path.normpath(i))
424         else:
425             raise GitException, '%s is not a file or directory' % i
426
427     if files:
428         if __run('git-update-index --add --', files):
429             raise GitException, 'Unable to add file'
430
431 def rm(files, force = False):
432     """Remove a file from the repository
433     """
434     if not force:
435         for f in files:
436             if os.path.exists(f):
437                 raise GitException, '%s exists. Remove it first' %f
438         if files:
439             __run('git-update-index --remove --', files)
440     else:
441         if files:
442             __run('git-update-index --force-remove --', files)
443
444 # Persons caching
445 __user = None
446 __author = None
447 __committer = None
448
449 def user():
450     """Return the user information.
451     """
452     global __user
453     if not __user:
454         name=config.get('user.name')
455         email=config.get('user.email')
456         __user = Person(name, email)
457     return __user;
458
459 def author():
460     """Return the author information.
461     """
462     global __author
463     if not __author:
464         try:
465             # the environment variables take priority over config
466             try:
467                 date = os.environ['GIT_AUTHOR_DATE']
468             except KeyError:
469                 date = ''
470             __author = Person(os.environ['GIT_AUTHOR_NAME'],
471                               os.environ['GIT_AUTHOR_EMAIL'],
472                               date)
473         except KeyError:
474             __author = user()
475     return __author
476
477 def committer():
478     """Return the author information.
479     """
480     global __committer
481     if not __committer:
482         try:
483             # the environment variables take priority over config
484             try:
485                 date = os.environ['GIT_COMMITTER_DATE']
486             except KeyError:
487                 date = ''
488             __committer = Person(os.environ['GIT_COMMITTER_NAME'],
489                                  os.environ['GIT_COMMITTER_EMAIL'],
490                                  date)
491         except KeyError:
492             __committer = user()
493     return __committer
494
495 def update_cache(files = None, force = False):
496     """Update the cache information for the given files
497     """
498     if not files:
499         files = []
500
501     cache_files = __tree_status(files, verbose = False)
502
503     # everything is up-to-date
504     if len(cache_files) == 0:
505         return False
506
507     # check for unresolved conflicts
508     if not force and [x for x in cache_files
509                       if x[0] not in ['M', 'N', 'A', 'D']]:
510         raise GitException, 'Updating cache failed: unresolved conflicts'
511
512     # update the cache
513     add_files = [x[1] for x in cache_files if x[0] in ['N', 'A']]
514     rm_files =  [x[1] for x in cache_files if x[0] in ['D']]
515     m_files =   [x[1] for x in cache_files if x[0] in ['M']]
516
517     if add_files and __run('git-update-index --add --', add_files) != 0:
518         raise GitException, 'Failed git-update-index --add'
519     if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
520         raise GitException, 'Failed git-update-index --rm'
521     if m_files and __run('git-update-index --', m_files) != 0:
522         raise GitException, 'Failed git-update-index'
523
524     return True
525
526 def commit(message, files = None, parents = None, allowempty = False,
527            cache_update = True, tree_id = None,
528            author_name = None, author_email = None, author_date = None,
529            committer_name = None, committer_email = None):
530     """Commit the current tree to repository
531     """
532     if not files:
533         files = []
534     if not parents:
535         parents = []
536
537     # Get the tree status
538     if cache_update and parents != []:
539         changes = update_cache(files)
540         if not changes and not allowempty:
541             raise GitException, 'No changes to commit'
542
543     # get the commit message
544     if not message:
545         message = '\n'
546     elif message[-1:] != '\n':
547         message += '\n'
548
549     must_switch = True
550     # write the index to repository
551     if tree_id == None:
552         tree_id = _output_one_line('git-write-tree')
553     else:
554         must_switch = False
555
556     # the commit
557     cmd = ''
558     if author_name:
559         cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
560     if author_email:
561         cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
562     if author_date:
563         cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
564     if committer_name:
565         cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
566     if committer_email:
567         cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
568     cmd += 'git-commit-tree %s' % tree_id
569
570     # get the parents
571     for p in parents:
572         cmd += ' -p %s' % p
573
574     commit_id = _output_one_line(cmd, message)
575     if must_switch:
576         __set_head(commit_id)
577
578     return commit_id
579
580 def apply_diff(rev1, rev2, check_index = True, files = None):
581     """Apply the diff between rev1 and rev2 onto the current
582     index. This function doesn't need to raise an exception since it
583     is only used for fast-pushing a patch. If this operation fails,
584     the pushing would fall back to the three-way merge.
585     """
586     if check_index:
587         index_opt = '--index'
588     else:
589         index_opt = ''
590
591     if not files:
592         files = []
593
594     diff_str = diff(files, rev1, rev2)
595     if diff_str:
596         try:
597             _input_str('git-apply %s' % index_opt, diff_str)
598         except GitException:
599             return False
600
601     return True
602
603 def merge(base, head1, head2, recursive = False):
604     """Perform a 3-way merge between base, head1 and head2 into the
605     local tree
606     """
607     refresh_index()
608
609     if recursive:
610         # this operation tracks renames but it is slower (used in
611         # general when pushing or picking patches)
612         try:
613             # use _output() to mask the verbose prints of the tool
614             _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
615         except GitException:
616             pass
617     else:
618         # the fast case where we don't track renames (used when the
619         # distance between base and heads is small, i.e. folding or
620         # synchronising patches)
621         if __run('git-read-tree -u -m --aggressive',
622                  [base, head1, head2]) != 0:
623             raise GitException, 'git-read-tree failed (local changes maybe?)'
624
625     # check the index for unmerged entries
626     files = {}
627     stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
628
629     for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
630         if not line:
631             continue
632
633         mode, hash, stage, path = stages_re.findall(line)[0]
634
635         if not path in files:
636             files[path] = {}
637             files[path]['1'] = ('', '')
638             files[path]['2'] = ('', '')
639             files[path]['3'] = ('', '')
640
641         files[path][stage] = (mode, hash)
642
643     # merge the unmerged files
644     errors = False
645     for path in files:
646         # remove additional files that might be generated for some
647         # newer versions of GIT
648         for suffix in [base, head1, head2]:
649             if not suffix:
650                 continue
651             fname = path + '~' + suffix
652             if os.path.exists(fname):
653                 os.remove(fname)
654
655         stages = files[path]
656         if gitmergeonefile.merge(stages['1'][1], stages['2'][1],
657                                  stages['3'][1], path, stages['1'][0],
658                                  stages['2'][0], stages['3'][0]) != 0:
659             errors = True
660
661     if errors:
662         raise GitException, 'GIT index merging failed (possible conflicts)'
663
664 def status(files = None, modified = False, new = False, deleted = False,
665            conflict = False, unknown = False, noexclude = False):
666     """Show the tree status
667     """
668     if not files:
669         files = []
670
671     cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
672     all = not (modified or new or deleted or conflict or unknown)
673
674     if not all:
675         filestat = []
676         if modified:
677             filestat.append('M')
678         if new:
679             filestat.append('A')
680             filestat.append('N')
681         if deleted:
682             filestat.append('D')
683         if conflict:
684             filestat.append('C')
685         if unknown:
686             filestat.append('?')
687         cache_files = [x for x in cache_files if x[0] in filestat]
688
689     for fs in cache_files:
690         if files and not fs[1] in files:
691             continue
692         if all:
693             print '%s %s' % (fs[0], fs[1])
694         else:
695             print '%s' % fs[1]
696
697 def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
698     """Show the diff between rev1 and rev2
699     """
700     if not files:
701         files = []
702
703     if rev1 and rev2:
704         diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
705     elif rev1 or rev2:
706         refresh_index()
707         if rev2:
708             diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
709         else:
710             diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
711     else:
712         diff_str = ''
713
714     if out_fd:
715         out_fd.write(diff_str)
716     else:
717         return diff_str
718
719 def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
720     """Return the diffstat between rev1 and rev2
721     """
722     if not files:
723         files = []
724
725     p=popen2.Popen3('git-apply --stat')
726     diff(files, rev1, rev2, p.tochild)
727     p.tochild.close()
728     diff_str = p.fromchild.read().rstrip()
729     if p.wait():
730         raise GitException, 'git.diffstat failed'
731     return diff_str
732
733 def files(rev1, rev2):
734     """Return the files modified between rev1 and rev2
735     """
736
737     result = ''
738     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
739         result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
740
741     return result.rstrip()
742
743 def barefiles(rev1, rev2):
744     """Return the files modified between rev1 and rev2, without status info
745     """
746
747     result = ''
748     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
749         result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
750
751     return result.rstrip()
752
753 def pretty_commit(commit_id = 'HEAD'):
754     """Return a given commit (log + diff)
755     """
756     return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
757                     commit_id])
758
759 def checkout(files = None, tree_id = None, force = False):
760     """Check out the given or all files
761     """
762     if not files:
763         files = []
764
765     if tree_id and __run('git-read-tree --reset', [tree_id]) != 0:
766         raise GitException, 'Failed git-read-tree --reset %s' % tree_id
767
768     checkout_cmd = 'git-checkout-index -q -u'
769     if force:
770         checkout_cmd += ' -f'
771     if len(files) == 0:
772         checkout_cmd += ' -a'
773     else:
774         checkout_cmd += ' --'
775
776     if __run(checkout_cmd, files) != 0:
777         raise GitException, 'Failed git-checkout-index'
778
779 def switch(tree_id, keep = False):
780     """Switch the tree to the given id
781     """
782     if not keep:
783         refresh_index()
784         if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
785             raise GitException, 'git-read-tree failed (local changes maybe?)'
786
787     __set_head(tree_id)
788
789 def reset(files = None, tree_id = None, check_out = True):
790     """Revert the tree changes relative to the given tree_id. It removes
791     any local changes
792     """
793     if not tree_id:
794         tree_id = get_head()
795
796     if check_out:
797         cache_files = __tree_status(files, tree_id)
798         # files which were added but need to be removed
799         rm_files =  [x[1] for x in cache_files if x[0] in ['A']]
800
801         checkout(files, tree_id, True)
802         # checkout doesn't remove files
803         map(os.remove, rm_files)
804
805     # if the reset refers to the whole tree, switch the HEAD as well
806     if not files:
807         __set_head(tree_id)
808
809 def fetch(repository = 'origin', refspec = None):
810     """Fetches changes from the remote repository, using 'git-fetch'
811     by default.
812     """
813     # we update the HEAD
814     __clear_head_cache()
815
816     args = [repository]
817     if refspec:
818         args.append(refspec)
819
820     command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
821               config.get('stgit.fetchcmd')
822     if __run(command, args) != 0:
823         raise GitException, 'Failed "%s %s"' % (command, repository)
824
825 def pull(repository = 'origin', refspec = None):
826     """Fetches changes from the remote repository, using 'git-pull'
827     by default.
828     """
829     # we update the HEAD
830     __clear_head_cache()
831
832     args = [repository]
833     if refspec:
834         args.append(refspec)
835
836     command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
837               config.get('stgit.pullcmd')
838     if __run(command, args) != 0:
839         raise GitException, 'Failed "%s %s"' % (command, repository)
840
841 def repack():
842     """Repack all objects into a single pack
843     """
844     __run('git-repack -a -d -f')
845
846 def apply_patch(filename = None, diff = None, base = None,
847                 fail_dump = True):
848     """Apply a patch onto the current or given index. There must not
849     be any local changes in the tree, otherwise the command fails
850     """
851     if diff is None:
852         if filename:
853             f = file(filename)
854         else:
855             f = sys.stdin
856         diff = f.read()
857         if filename:
858             f.close()
859
860     if base:
861         orig_head = get_head()
862         switch(base)
863     else:
864         refresh_index()
865
866     try:
867         _input_str('git-apply --index', diff)
868     except GitException:
869         if base:
870             switch(orig_head)
871         if fail_dump:
872             # write the failed diff to a file
873             f = file('.stgit-failed.patch', 'w+')
874             f.write(diff)
875             f.close()
876             print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
877
878         raise
879
880     if base:
881         top = commit(message = 'temporary commit used for applying a patch',
882                      parents = [base])
883         switch(orig_head)
884         merge(base, orig_head, top)
885
886 def clone(repository, local_dir):
887     """Clone a remote repository. At the moment, just use the
888     'git-clone' script
889     """
890     if __run('git-clone', [repository, local_dir]) != 0:
891         raise GitException, 'Failed "git-clone %s %s"' \
892               % (repository, local_dir)
893
894 def modifying_revs(files, base_rev, head_rev):
895     """Return the revisions from the list modifying the given files
896     """
897     cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
898     revs = [line.strip() for line in _output_lines(cmd + files)]
899
900     return revs
901
902
903 def refspec_localpart(refspec):
904     m = re.match('^[^:]*:([^:]*)$', refspec)
905     if m:
906         return m.group(1)
907     else:
908         raise GitException, 'Cannot parse refspec "%s"' % line
909
910 def refspec_remotepart(refspec):
911     m = re.match('^([^:]*):[^:]*$', refspec)
912     if m:
913         return m.group(1)
914     else:
915         raise GitException, 'Cannot parse refspec "%s"' % line
916     
917
918 def __remotes_from_config():
919     return config.sections_matching(r'remote\.(.*)\.url')
920
921 def __remotes_from_dir(dir):
922     d = os.path.join(basedir.get(), dir)
923     if os.path.exists(d):
924         return os.listdir(d)
925     else:
926         return None
927
928 def remotes_list():
929     """Return the list of remotes in the repository
930     """
931
932     return Set(__remotes_from_config()) | \
933            Set(__remotes_from_dir('remotes')) | \
934            Set(__remotes_from_dir('branches'))
935
936 def remotes_local_branches(remote):
937     """Returns the list of local branches fetched from given remote
938     """
939
940     branches = []
941     if remote in __remotes_from_config():
942         for line in config.getall('remote.%s.fetch' % remote):
943             branches.append(refspec_localpart(line))
944     elif remote in __remotes_from_dir('remotes'):
945         stream = open(os.path.join(basedir.get(), 'remotes', remote), 'r')
946         for line in stream:
947             # Only consider Pull lines
948             m = re.match('^Pull: (.*)\n$', line)
949             if m:
950                 branches.append(refspec_localpart(m.group(1)))
951         stream.close()
952     elif remote in __remotes_from_dir('branches'):
953         # old-style branches only declare one branch
954         branches.append('refs/heads/'+remote);
955     else:
956         raise GitException, 'Unknown remote "%s"' % remote
957
958     return branches
959
960 def identify_remote(branchname):
961     """Return the name for the remote to pull the given branchname
962     from, or None if we believe it is a local branch.
963     """
964
965     for remote in remotes_list():
966         if branchname in remotes_local_branches(remote):
967             return remote
968
969     # if we get here we've found nothing, the branch is a local one
970     return None
971
972 def fetch_head():
973     """Return the git id for the tip of the parent branch as left by
974     'git fetch'.
975     """
976
977     fetch_head=None
978     stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
979     for line in stream:
980         # Only consider lines not tagged not-for-merge
981         m = re.match('^([^\t]*)\t\t', line)
982         if m:
983             if fetch_head:
984                 raise GitException, "StGit does not support multiple FETCH_HEAD"
985             else:
986                 fetch_head=m.group(1)
987     stream.close()
988
989     # here we are sure to have a single fetch_head
990     return fetch_head