Fix up help string for "stg clone"
[stgit] / stgit / git.py
index 293d421..0cdc125 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 *
 
@@ -59,7 +59,7 @@ class Commit:
                 self.__author = field[1]
             if field[0] == 'committer':
                 self.__committer = field[1]
                 self.__author = field[1]
             if field[0] == 'committer':
                 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:
@@ -112,22 +114,25 @@ def get_conflicts():
         return None
 
 def _input(cmd, file_desc):
         return None
 
 def _input(cmd, file_desc):
-    p = popen2.Popen3(cmd)
-    for line in file_desc:
+    p = popen2.Popen3(cmd, True)
+    while True:
+        line = file_desc.readline()
+        if not line:
+            break
         p.tochild.write(line)
     p.tochild.close()
     if p.wait():
         raise GitException, '%s failed' % str(cmd)
 
 def _output(cmd):
         p.tochild.write(line)
     p.tochild.close()
     if p.wait():
         raise GitException, '%s failed' % str(cmd)
 
 def _output(cmd):
-    p=popen2.Popen3(cmd)
+    p=popen2.Popen3(cmd, True)
     string = p.fromchild.read()
     if p.wait():
         raise GitException, '%s failed' % str(cmd)
     return string
 
 def _output_one_line(cmd, file_desc = None):
     string = p.fromchild.read()
     if p.wait():
         raise GitException, '%s failed' % str(cmd)
     return string
 
 def _output_one_line(cmd, file_desc = None):
-    p=popen2.Popen3(cmd)
+    p=popen2.Popen3(cmd, True)
     if file_desc != None:
         for line in file_desc:
             p.tochild.write(line)
     if file_desc != None:
         for line in file_desc:
             p.tochild.write(line)
@@ -138,7 +143,7 @@ def _output_one_line(cmd, file_desc = None):
     return string
 
 def _output_lines(cmd):
     return string
 
 def _output_lines(cmd):
-    p=popen2.Popen3(cmd)
+    p=popen2.Popen3(cmd, True)
     lines = p.fromchild.readlines()
     if p.wait():
         raise GitException, '%s failed' % str(cmd)
     lines = p.fromchild.readlines()
     if p.wait():
         raise GitException, '%s failed' % str(cmd)
@@ -166,12 +171,14 @@ def __run(cmd, args=None):
 def __check_base_dir():
     return os.path.isdir(base_dir)
 
 def __check_base_dir():
     return os.path.isdir(base_dir)
 
-def __tree_status(files = [], tree_id = 'HEAD', unknown = False,
+def __tree_status(files = None, 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-index --refresh > /dev/null')
+    refresh_index()
 
 
+    if not files:
+        files = []
     cache_files = []
 
     # unknown files
     cache_files = []
 
     # unknown files
@@ -211,24 +218,129 @@ 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 refresh_index():
+    """Refresh index with stat() information from the working directory.
+    """
+    __run('git-update-index -q --unmerged --refresh')
+
+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():
+        refresh_index()
+        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
@@ -257,11 +369,6 @@ def add(names):
 def rm(files, force = False):
     """Remove a file from the repository
     """
 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):
     if not force:
         for f in files:
             if os.path.exists(f):
@@ -272,9 +379,12 @@ def rm(files, force = False):
         if files:
             __run('git-update-index --force-remove --', files)
 
         if files:
             __run('git-update-index --force-remove --', files)
 
-def update_cache(files = [], force = False):
+def update_cache(files = None, force = False):
     """Update the cache information for the given files
     """
     """Update the cache information for the given files
     """
+    if not files:
+        files = []
+
     cache_files = __tree_status(files)
 
     # everything is up-to-date
     cache_files = __tree_status(files)
 
     # everything is up-to-date
@@ -300,12 +410,17 @@ def update_cache(files = [], force = False):
 
     return True
 
 
     return True
 
-def commit(message, files = [], parents = [], allowempty = False,
+def commit(message, files = None, parents = None, allowempty = False,
            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
     """
            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
     """
+    if not files:
+        files = []
+    if not parents:
+        parents = []
+
     # Get the tree status
     if cache_update and parents != []:
         changes = update_cache(files)
     # Get the tree status
     if cache_update and parents != []:
         changes = update_cache(files)
@@ -347,21 +462,34 @@ def commit(message, files = [], parents = [], allowempty = False,
 
     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
     """
+    refresh_index()
     if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
         raise GitException, 'git-read-tree failed (local changes maybe?)'
 
     # this can fail if there are conflicts
     if __run('git-read-tree -u -m', [base, head1, head2]) != 0:
         raise GitException, 'git-read-tree failed (local changes maybe?)'
 
     # this can fail if there are conflicts
-    if os.system('git-merge-index -o -q gitmergeonefile.py -a') != 0:
-        raise GitException, 'git-merge-cache failed (possible conflicts)'
+    if __run('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,
+def status(files = None, modified = False, new = False, deleted = False,
            conflict = False, unknown = False, noexclude = False):
     """Show the tree status
     """
            conflict = False, unknown = False, noexclude = False):
     """Show the tree status
     """
+    if not files:
+        files = []
+
     cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
     all = not (modified or new or deleted or conflict or unknown)
 
     cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
     all = not (modified or new or deleted or conflict or unknown)
 
@@ -386,14 +514,16 @@ def status(files = [], modified = False, new = False, deleted = False,
         else:
             print '%s' % fs[1]
 
         else:
             print '%s' % fs[1]
 
-def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
+def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
     """Show the diff between rev1 and rev2
     """
     """Show the diff between rev1 and rev2
     """
+    if not files:
+        files = []
 
     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:
-        os.system('git-update-index --refresh > /dev/null')
+        refresh_index()
         diff_str = _output(['git-diff-index', '-p', rev1] + files)
 
     if out_fd:
         diff_str = _output(['git-diff-index', '-p', rev1] + files)
 
     if out_fd:
@@ -401,9 +531,11 @@ def diff(files = [], rev1 = 'HEAD', rev2 = None, out_fd = None):
     else:
         return diff_str
 
     else:
         return diff_str
 
-def diffstat(files = [], rev1 = 'HEAD', rev2 = None):
+def diffstat(files = None, rev1 = 'HEAD', rev2 = None):
     """Return the diffstat between rev1 and rev2
     """
     """Return the diffstat between rev1 and rev2
     """
+    if not files:
+        files = []
 
     p=popen2.Popen3('git-apply --stat')
     diff(files, rev1, rev2, p.tochild)
 
     p=popen2.Popen3('git-apply --stat')
     diff(files, rev1, rev2, p.tochild)
@@ -433,9 +565,12 @@ def barefiles(rev1, rev2):
 
     return str.rstrip()
 
 
     return str.rstrip()
 
-def checkout(files = [], tree_id = None, force = False):
+def checkout(files = None, tree_id = None, force = False):
     """Check out the given or all files
     """
     """Check out the given or all files
     """
+    if not files:
+        files = []
+
     if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
         raise GitException, 'Failed git-read-tree -m %s' % tree_id
 
     if tree_id and __run('git-read-tree -m', [tree_id]) != 0:
         raise GitException, 'Failed git-read-tree -m %s' % tree_id
 
@@ -453,6 +588,7 @@ def checkout(files = [], tree_id = None, force = False):
 def switch(tree_id):
     """Switch the tree to the given id
     """
 def switch(tree_id):
     """Switch the tree to the given id
     """
+    refresh_index()
     if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
         raise GitException, 'git-read-tree failed (local changes maybe?)'
 
     if __run('git-read-tree -u -m', [get_head(), tree_id]) != 0:
         raise GitException, 'git-read-tree failed (local changes maybe?)'
 
@@ -478,6 +614,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)
@@ -485,17 +624,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-index --refresh > /dev/null')
-
-    if filename:
-        if __run('git-apply --index', [filename]) != 0:
-            raise GitException, 'Patch does not apply cleanly'
+    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
+
+    if base:
+        orig_head = get_head()
+        switch(base)
     else:
     else:
-        _input('git-apply --index', sys.stdin)
+        refresh_index()         # needed since __apply_patch() doesn't do it
+
+    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
@@ -504,3 +661,11 @@ def clone(repository, local_dir):
     if __run('git clone', [repository, local_dir]) != 0:
         raise GitException, 'Failed "git clone %s %s"' \
               % (repository, local_dir)
     if __run('git clone', [repository, local_dir]) != 0:
         raise GitException, 'Failed "git clone %s %s"' \
               % (repository, local_dir)
+
+def modifying_revs(files, base_rev):
+    """Return the revisions from the list modifying the given files
+    """
+    cmd = ['git-rev-list', '%s..' % base_rev, '--']
+    revs = [line.strip() for line in _output_lines(cmd + files)]
+
+    return revs