Print the git version when running the "stg version" command
[stgit] / stgit / git.py
index 687bee2..d5bd724 100644 (file)
@@ -18,7 +18,7 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os, glob, popen2
+import sys, os, popen2
 
 from stgit.utils import *
 
 
 from stgit.utils import *
 
@@ -57,9 +57,9 @@ class Commit:
                 self.__parents.append(field[1])
             if field[0] == 'author':
                 self.__author = field[1]
                 self.__parents.append(field[1])
             if field[0] == 'author':
                 self.__author = field[1]
-            if field[0] == 'comitter':
+            if field[0] == 'committer':
                 self.__committer = field[1]
                 self.__committer = field[1]
-        self.__log = ''.join(lines[i:])
+        self.__log = ''.join(lines[i+1:])
 
     def get_id_hash(self):
         return self.__id_hash
 
     def get_id_hash(self):
         return self.__id_hash
@@ -92,6 +92,8 @@ def get_commit(id_hash):
     """Commit objects factory. Save/look-up them in the __commits
     dictionary
     """
     """Commit objects factory. Save/look-up them in the __commits
     dictionary
     """
+    global __commits
+
     if id_hash in __commits:
         return __commits[id_hash]
     else:
     if id_hash in __commits:
         return __commits[id_hash]
     else:
@@ -113,7 +115,10 @@ def get_conflicts():
 
 def _input(cmd, file_desc):
     p = popen2.Popen3(cmd)
 
 def _input(cmd, file_desc):
     p = popen2.Popen3(cmd)
-    for line in file_desc:
+    while True:
+        line = file_desc.readline()
+        if not line:
+            break
         p.tochild.write(line)
     p.tochild.close()
     if p.wait():
         p.tochild.write(line)
     p.tochild.close()
     if p.wait():
@@ -126,8 +131,12 @@ def _output(cmd):
         raise GitException, '%s failed' % str(cmd)
     return string
 
         raise GitException, '%s failed' % str(cmd)
     return string
 
-def _output_one_line(cmd):
+def _output_one_line(cmd, file_desc = None):
     p=popen2.Popen3(cmd)
     p=popen2.Popen3(cmd)
+    if file_desc != None:
+        for line in file_desc:
+            p.tochild.write(line)
+        p.tochild.close()
     string = p.fromchild.readline().strip()
     if p.wait():
         raise GitException, '%s failed' % str(cmd)
     string = p.fromchild.readline().strip()
     if p.wait():
         raise GitException, '%s failed' % str(cmd)
@@ -166,7 +175,7 @@ def __tree_status(files = [], tree_id = 'HEAD', unknown = False,
                   noexclude = True):
     """Returns a list of pairs - [status, filename]
     """
                   noexclude = True):
     """Returns a list of pairs - [status, filename]
     """
-    os.system('git-update-cache --refresh > /dev/null')
+    os.system('git-update-index --refresh > /dev/null')
 
     cache_files = []
 
 
     cache_files = []
 
@@ -178,7 +187,7 @@ def __tree_status(files = [], tree_id = 'HEAD', unknown = False,
         base_exclude.append('--exclude-per-directory=.gitignore')
 
         if os.path.exists(exclude_file):
         base_exclude.append('--exclude-per-directory=.gitignore')
 
         if os.path.exists(exclude_file):
-            extra_exclude = '--exclude-from=%s' % exclude_file
+            extra_exclude = ['--exclude-from=%s' % exclude_file]
         else:
             extra_exclude = []
         if noexclude:
         else:
             extra_exclude = []
         if noexclude:
@@ -195,7 +204,7 @@ def __tree_status(files = [], tree_id = 'HEAD', unknown = False,
     cache_files += [('C', filename) for filename in conflicts]
 
     # the rest
     cache_files += [('C', filename) for filename in conflicts]
 
     # the rest
-    for line in _output_lines(['git-diff-cache', '-r', tree_id] + files):
+    for line in _output_lines(['git-diff-index', '-r', tree_id] + files):
         fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
         if fs[1] not in conflicts:
             cache_files.append(fs)
         fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
         if fs[1] not in conflicts:
             cache_files.append(fs)
@@ -207,24 +216,123 @@ def local_changes():
     """
     return len(__tree_status()) != 0
 
     """
     return len(__tree_status()) != 0
 
+# HEAD value cached
+__head = None
+
 def get_head():
 def get_head():
-    """Returns a string representing the HEAD
+    """Verifies the HEAD and returns the SHA1 id that represents it
     """
     """
-    return read_string(head_link)
+    global __head
+
+    if not __head:
+        __head = rev_parse('HEAD')
+    return __head
 
 def get_head_file():
     """Returns the name of the file pointed to by the HEAD link
     """
 
 def get_head_file():
     """Returns the name of the file pointed to by the HEAD link
     """
-    # valid link
-    if os.path.islink(head_link) and os.path.isfile(head_link):
-        return os.path.basename(os.readlink(head_link))
-    else:
-        raise GitException, 'Invalid .git/HEAD link. Git tree not initialised?'
+    return os.path.basename(_output_one_line('git-symbolic-ref HEAD'))
+
+def set_head_file(ref):
+    """Resets HEAD to point to a new ref
+    """
+    # head cache flushing is needed since we might have a different value
+    # in the new head
+    __clear_head_cache()
+    if __run('git-symbolic-ref HEAD', [ref]) != 0:
+        raise GitException, 'Could not set head to "%s"' % ref
 
 def __set_head(val):
     """Sets the HEAD value
     """
 
 def __set_head(val):
     """Sets the HEAD value
     """
-    write_string(head_link, val)
+    global __head
+
+    if not __head or __head != val:
+        if __run('git-update-ref HEAD', [val]) != 0:
+            raise GitException, 'Could not update HEAD to "%s".' % val
+        __head = val
+
+def __clear_head_cache():
+    """Sets the __head to None so that a re-read is forced
+    """
+    global __head
+
+    __head = None
+
+def rev_parse(git_id):
+    """Parse the string and return a verified SHA1 id
+    """
+    try:
+        return _output_one_line(['git-rev-parse', '--verify', git_id])
+    except GitException:
+        raise GitException, 'Unknown revision: %s' % git_id
+
+def branch_exists(branch):
+    """Existance check for the named branch
+    """
+    for line in _output_lines(['git-rev-parse', '--symbolic', '--all']):
+        if line.strip() == branch:
+            return True
+    return False
+
+def create_branch(new_branch, tree_id = None):
+    """Create a new branch in the git repository
+    """
+    new_head = os.path.join('refs', 'heads', new_branch)
+    if branch_exists(new_head):
+        raise GitException, 'Branch "%s" already exists' % new_branch
+
+    current_head = get_head()
+    set_head_file(new_head)
+    __set_head(current_head)
+
+    # a checkout isn't needed if new branch points to the current head
+    if tree_id:
+        switch(tree_id)
+
+    if os.path.isfile(os.path.join(base_dir, 'MERGE_HEAD')):
+        os.remove(os.path.join(base_dir, 'MERGE_HEAD'))
+
+def switch_branch(name):
+    """Switch to a git branch
+    """
+    global __head
+
+    new_head = os.path.join('refs', 'heads', name)
+    if not branch_exists(new_head):
+        raise GitException, 'Branch "%s" does not exist' % name
+
+    tree_id = rev_parse(new_head + '^0')
+    if tree_id != get_head():
+        if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
+            raise GitException, 'git-read-tree failed (local changes maybe?)'
+        __head = tree_id
+    set_head_file(new_head)
+
+    if os.path.isfile(os.path.join(base_dir, 'MERGE_HEAD')):
+        os.remove(os.path.join(base_dir, 'MERGE_HEAD'))
+
+def delete_branch(name):
+    """Delete a git branch
+    """
+    branch_head = os.path.join('refs', 'heads', name)
+    if not branch_exists(branch_head):
+        raise GitException, 'Branch "%s" does not exist' % name
+    os.remove(os.path.join(base_dir, branch_head))
+
+def rename_branch(from_name, to_name):
+    """Rename a git branch
+    """
+    from_head = os.path.join('refs', 'heads', from_name)
+    if not branch_exists(from_head):
+        raise GitException, 'Branch "%s" does not exist' % from_name
+    to_head = os.path.join('refs', 'heads', to_name)
+    if branch_exists(to_head):
+        raise GitException, 'Branch "%s" already exists' % to_name
+
+    if get_head_file() == from_name:
+        set_head_file(to_head)
+    os.rename(os.path.join(base_dir, from_head), os.path.join(base_dir, to_head))
 
 def add(names):
     """Add the files or recursively add the directory contents
 
 def add(names):
     """Add the files or recursively add the directory contents
@@ -247,26 +355,21 @@ def add(names):
             raise GitException, '%s is not a file or directory' % i
 
     if files:
             raise GitException, '%s is not a file or directory' % i
 
     if files:
-        if __run('git-update-cache --add --', files):
+        if __run('git-update-index --add --', files):
             raise GitException, 'Unable to add file'
 
 def rm(files, force = False):
     """Remove a file from the repository
     """
             raise GitException, 'Unable to add file'
 
 def rm(files, force = False):
     """Remove a file from the repository
     """
-    if force:
-        git_opt = '--force-remove'
-    else:
-        git_opt = '--remove'
-
     if not force:
         for f in files:
             if os.path.exists(f):
                 raise GitException, '%s exists. Remove it first' %f
         if files:
     if not force:
         for f in files:
             if os.path.exists(f):
                 raise GitException, '%s exists. Remove it first' %f
         if files:
-            __run('git-update-cache --remove --', files)
+            __run('git-update-index --remove --', files)
     else:
         if files:
     else:
         if files:
-            __run('git-update-cache --force-remove --', files)
+            __run('git-update-index --force-remove --', files)
 
 def update_cache(files = [], force = False):
     """Update the cache information for the given files
 
 def update_cache(files = [], force = False):
     """Update the cache information for the given files
@@ -287,17 +390,17 @@ def update_cache(files = [], force = False):
     rm_files =  [x[1] for x in cache_files if x[0] in ['D']]
     m_files =   [x[1] for x in cache_files if x[0] in ['M']]
 
     rm_files =  [x[1] for x in cache_files if x[0] in ['D']]
     m_files =   [x[1] for x in cache_files if x[0] in ['M']]
 
-    if add_files and __run('git-update-cache --add --', add_files) != 0:
-        raise GitException, 'Failed git-update-cache --add'
-    if rm_files and __run('git-update-cache --force-remove --', rm_files) != 0:
-        raise GitException, 'Failed git-update-cache --rm'
-    if m_files and __run('git-update-cache --', m_files) != 0:
-        raise GitException, 'Failed git-update-cache'
+    if add_files and __run('git-update-index --add --', add_files) != 0:
+        raise GitException, 'Failed git-update-index --add'
+    if rm_files and __run('git-update-index --force-remove --', rm_files) != 0:
+        raise GitException, 'Failed git-update-index --rm'
+    if m_files and __run('git-update-index --', m_files) != 0:
+        raise GitException, 'Failed git-update-index'
 
     return True
 
 def commit(message, files = [], parents = [], allowempty = False,
 
     return True
 
 def commit(message, files = [], parents = [], allowempty = False,
-           cache_update = True,
+           cache_update = True, tree_id = None,
            author_name = None, author_email = None, author_date = None,
            committer_name = None, committer_email = None):
     """Commit the current tree to repository
            author_name = None, author_email = None, author_date = None,
            committer_name = None, committer_email = None):
     """Commit the current tree to repository
@@ -309,15 +412,15 @@ def commit(message, files = [], parents = [], allowempty = False,
             raise GitException, 'No changes to commit'
 
     # get the commit message
             raise GitException, 'No changes to commit'
 
     # get the commit message
-    f = file('.commitmsg', 'w+')
-    if message[-1:] == '\n':
-        f.write(message)
-    else:
-        print >> f, message
-    f.close()
+    if message[-1:] != '\n':
+        message += '\n'
 
 
+    must_switch = True
     # write the index to repository
     # write the index to repository
-    tree_id = _output_one_line('git-write-tree')
+    if tree_id == None:
+        tree_id = _output_one_line('git-write-tree')
+    else:
+        must_switch = False
 
     # the commit
     cmd = ''
 
     # the commit
     cmd = ''
@@ -337,14 +440,21 @@ def commit(message, files = [], parents = [], allowempty = False,
     for p in parents:
         cmd += ' -p %s' % p
 
     for p in parents:
         cmd += ' -p %s' % p
 
-    cmd += ' < .commitmsg'
-
-    commit_id = _output_one_line(cmd)
-    __set_head(commit_id)
-    os.remove('.commitmsg')
+    commit_id = _output_one_line(cmd, message)
+    if must_switch:
+        __set_head(commit_id)
 
     return commit_id
 
 
     return commit_id
 
+def apply_diff(rev1, rev2):
+    """Apply the diff between rev1 and rev2 onto the current
+    index. This function doesn't need to raise an exception since it
+    is only used for fast-pushing a patch. If this operation fails,
+    the pushing would fall back to the three-way merge.
+    """
+    return os.system('git-diff-tree -p %s %s | git-apply --index 2> /dev/null'
+                     % (rev1, rev2)) == 0
+
 def merge(base, head1, head2):
     """Perform a 3-way merge between base, head1 and head2 into the
     local tree
 def merge(base, head1, head2):
     """Perform a 3-way merge between base, head1 and head2 into the
     local tree
@@ -353,8 +463,8 @@ def merge(base, head1, head2):
         raise GitException, 'git-read-tree failed (local changes maybe?)'
 
     # this can fail if there are conflicts
         raise GitException, 'git-read-tree failed (local changes maybe?)'
 
     # this can fail if there are conflicts
-    if os.system('git-merge-cache -o -q gitmergeonefile.py -a') != 0:
-        raise GitException, 'git-merge-cache failed (possible conflicts)'
+    if os.system('git-merge-index -o -q gitmergeonefile.py -a') != 0:
+        raise GitException, 'git-merge-index failed (possible conflicts)'
 
 def status(files = [], modified = False, new = False, deleted = False,
            conflict = False, unknown = False, noexclude = False):
 
 def status(files = [], modified = False, new = False, deleted = False,
            conflict = False, unknown = False, noexclude = False):
@@ -387,12 +497,12 @@ def status(files = [], modified = False, new = False, deleted = False,
 def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
     """Show the diff between rev1 and rev2
     """
 def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
     """Show the diff between rev1 and rev2
     """
-    os.system('git-update-cache --refresh > /dev/null')
 
     if rev2:
         diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
     else:
 
     if rev2:
         diff_str = _output(['git-diff-tree', '-p', rev1, rev2] + files)
     else:
-        diff_str = _output(['git-diff-cache', '-p', rev1] + files)
+        os.system('git-update-index --refresh > /dev/null')
+        diff_str = _output(['git-diff-index', '-p', rev1] + files)
 
     if out_fd:
         out_fd.write(diff_str)
 
     if out_fd:
         out_fd.write(diff_str)
@@ -403,7 +513,6 @@ def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
     """Return the diffstat between rev1 and rev2
     """
 
     """Return the diffstat between rev1 and rev2
     """
 
-    os.system('git-update-cache --refresh > /dev/null')
     p=popen2.Popen3('git-apply --stat')
     diff(files, rev1, rev2, p.tochild)
     p.tochild.close()
     p=popen2.Popen3('git-apply --stat')
     diff(files, rev1, rev2, p.tochild)
     p.tochild.close()
@@ -415,7 +524,6 @@ def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
 def files(rev1, rev2):
     """Return the files modified between rev1 and rev2
     """
 def files(rev1, rev2):
     """Return the files modified between rev1 and rev2
     """
-    os.system('git-update-cache --refresh > /dev/null')
 
     str = ''
     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
 
     str = ''
     for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
@@ -423,13 +531,23 @@ def files(rev1, rev2):
 
     return str.rstrip()
 
 
     return str.rstrip()
 
+def barefiles(rev1, rev2):
+    """Return the files modified between rev1 and rev2, without status info
+    """
+
+    str = ''
+    for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
+        str += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
+
+    return str.rstrip()
+
 def checkout(files = [], tree_id = None, force = False):
     """Check out the given or all files
     """
     if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
         raise GitException, 'Failed git-read-tree -m %s' % tree_id
 
 def checkout(files = [], tree_id = None, force = False):
     """Check out the given or all files
     """
     if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
         raise GitException, 'Failed git-read-tree -m %s' % tree_id
 
-    checkout_cmd = 'git-checkout-cache -q -u'
+    checkout_cmd = 'git-checkout-index -q -u'
     if force:
         checkout_cmd += ' -f'
     if len(files) == 0:
     if force:
         checkout_cmd += ' -f'
     if len(files) == 0:
@@ -438,7 +556,7 @@ def checkout(files = [], tree_id = None, force = False):
         checkout_cmd += ' --'
 
     if __run(checkout_cmd, files) != 0:
         checkout_cmd += ' --'
 
     if __run(checkout_cmd, files) != 0:
-        raise GitException, 'Failed git-checkout-cache'
+        raise GitException, 'Failed git-checkout-index'
 
 def switch(tree_id):
     """Switch the tree to the given id
 
 def switch(tree_id):
     """Switch the tree to the given id
@@ -468,6 +586,9 @@ def pull(repository = 'origin', refspec = None):
     """Pull changes from the remote repository. At the moment, just
     use the 'git pull' command
     """
     """Pull changes from the remote repository. At the moment, just
     use the 'git pull' command
     """
+    # 'git pull' updates the HEAD
+    __clear_head_cache()
+
     args = [repository]
     if refspec:
         args.append(refspec)
     args = [repository]
     if refspec:
         args.append(refspec)
@@ -475,17 +596,35 @@ def pull(repository = 'origin', refspec = None):
     if __run('git pull', args) != 0:
         raise GitException, 'Failed "git pull %s"' % repository
 
     if __run('git pull', args) != 0:
         raise GitException, 'Failed "git pull %s"' % repository
 
-def apply_patch(filename = None):
-    """Apply a patch onto the current index. There must not be any
-    local changes in the tree, otherwise the command fails
+def apply_patch(filename = None, base = None):
+    """Apply a patch onto the current or given index. There must not
+    be any local changes in the tree, otherwise the command fails
     """
     """
-    os.system('git-update-cache --refresh > /dev/null')
-
-    if filename:
-        if __run('git-apply --index', [filename]) != 0:
-            raise GitException, 'Patch does not apply cleanly'
-    else:
-        _input('git-apply --index', sys.stdin)
+    def __apply_patch():
+        if filename:
+            return __run('git-apply --index', [filename]) == 0
+        else:
+            try:
+                _input('git-apply --index', sys.stdin)
+            except GitException:
+                return False
+            return True
+
+    os.system('git-update-index --refresh > /dev/null')
+
+    if base:
+        orig_head = get_head()
+        switch(base)
+
+    if not __apply_patch():
+        if base:
+            switch(orig_head)
+        raise GitException, 'Patch does not apply cleanly'
+    elif base:
+        top = commit(message = 'temporary commit used for applying a patch',
+                     parents = [base])
+        switch(orig_head)
+        merge(base, orig_head, top)
 
 def clone(repository, local_dir):
     """Clone a remote repository. At the moment, just use the
 
 def clone(repository, local_dir):
     """Clone a remote repository. At the moment, just use the