X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/stgit/blobdiff_plain/d0c5824b591c3e99d693cffbefc777d6bd7c2ccb..ed9256cea646a5384fb258eb9a3610b112a78a90:/stgit/git.py diff --git a/stgit/git.py b/stgit/git.py index 539d699..deb5efc 100644 --- a/stgit/git.py +++ b/stgit/git.py @@ -21,6 +21,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os, re, gitmergeonefile from shutil import copyfile +from stgit.exception import * from stgit import basedir from stgit.utils import * from stgit.out import * @@ -28,7 +29,7 @@ from stgit.run import * from stgit.config import config # git exception class -class GitException(Exception): +class GitException(StgException): pass # When a subprocess has a problem, we want the exception to be a @@ -37,6 +38,10 @@ class GitRunException(GitException): pass class GRun(Run): exc = GitRunException + def __init__(self, *cmd): + """Initialise the Run object and insert the 'git' command name. + """ + Run.__init__(self, 'git', *cmd) # @@ -81,7 +86,7 @@ class Commit: def __init__(self, id_hash): self.__id_hash = id_hash - lines = GRun('git-cat-file', 'commit', id_hash).output_lines() + lines = GRun('cat-file', 'commit', id_hash).output_lines() for i in range(len(lines)): line = lines[i] if not line: @@ -111,7 +116,7 @@ class Commit: return None def get_parents(self): - return GRun('git-rev-list', '--parents', '--max-count=1', self.__id_hash + return GRun('rev-list', '--parents', '--max-count=1', self.__id_hash ).output_one_line().split()[1:] def get_author(self): @@ -165,22 +170,48 @@ def exclude_files(): files.append(user_exclude) return files +def ls_files(files, tree = None, full_name = True): + """Return the files known to GIT or raise an error otherwise. It also + converts the file to the full path relative the the .git directory. + """ + if not files: + return [] + + args = [] + if tree: + args.append('--with-tree=%s' % tree) + if full_name: + args.append('--full-name') + args.append('--') + args.extend(files) + try: + return GRun('ls-files', '--error-unmatch', *args).output_lines() + except GitRunException: + # just hide the details of the 'git ls-files' command we use + raise GitException, \ + 'Some of the given paths are either missing or not known to GIT' + def tree_status(files = None, tree_id = 'HEAD', unknown = False, noexclude = True, verbose = False, diff_flags = []): - """Returns a list of pairs - (status, filename) + """Get the status of all changed files, or of a selected set of + files. Returns a list of pairs - (status, filename). + + If 'not files', it will check all files, and optionally all + unknown files. If 'files' is a list, it will only check the files + in the list. """ + assert not files or not unknown + if verbose: out.start('Checking for changes in the working directory') refresh_index() - if not files: - files = [] cache_files = [] # unknown files if unknown: - cmd = ['git-ls-files', '-z', '--others', '--directory', + cmd = ['ls-files', '-z', '--others', '--directory', '--no-empty-directory'] if not noexclude: cmd += ['--exclude=%s' % s for s in @@ -197,14 +228,29 @@ def tree_status(files = None, tree_id = 'HEAD', unknown = False, conflicts = get_conflicts() if not conflicts: conflicts = [] - cache_files += [('C', filename) for filename in conflicts] + cache_files += [('C', filename) for filename in conflicts + if not files or filename in files] + reported_files = set(conflicts) - # the rest - for line in GRun('git-diff-index', *(diff_flags + [tree_id, '--'] + files) - ).output_lines(): + # files in the index + args = diff_flags + [tree_id] + if files: + args += ['--'] + files + for line in GRun('diff-index', *args).output_lines(): + fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1)) + if fs[1] not in reported_files: + cache_files.append(fs) + reported_files.add(fs[1]) + + # files in the index but changed on (or removed from) disk + args = list(diff_flags) + if files: + args += ['--'] + files + for line in GRun('diff-files', *args).output_lines(): fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1)) - if fs[1] not in conflicts: + if fs[1] not in reported_files: cache_files.append(fs) + reported_files.add(fs[1]) if verbose: out.done() @@ -219,7 +265,7 @@ def local_changes(verbose = True): def get_heads(): heads = [] hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$') - for line in GRun('git-show-ref', '--heads').output_lines(): + for line in GRun('show-ref', '--heads').output_lines(): m = hr.match(line) heads.append(m.group(1)) return heads @@ -236,11 +282,19 @@ def get_head(): __head = rev_parse('HEAD') return __head +class DetachedHeadException(GitException): + def __init__(self): + GitException.__init__(self, 'Not on any branch') + def get_head_file(): - """Returns the name of the file pointed to by the HEAD link - """ - return strip_prefix('refs/heads/', - GRun('git-symbolic-ref', 'HEAD').output_one_line()) + """Return the name of the file pointed to by the HEAD symref. + Throw an exception if HEAD is detached.""" + try: + return strip_prefix( + 'refs/heads/', GRun('symbolic-ref', '-q', 'HEAD' + ).output_one_line()) + except GitRunException: + raise DetachedHeadException() def set_head_file(ref): """Resets HEAD to point to a new ref @@ -249,14 +303,14 @@ def set_head_file(ref): # in the new head __clear_head_cache() try: - GRun('git-symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run() + GRun('symbolic-ref', 'HEAD', 'refs/heads/%s' % ref).run() except GitRunException: raise GitException, 'Could not set head to "%s"' % ref def set_ref(ref, val): """Point ref at a new commit object.""" try: - GRun('git-update-ref', ref, val).run() + GRun('update-ref', ref, val).run() except GitRunException: raise GitException, 'Could not update %s to "%s".' % (ref, val) @@ -285,13 +339,14 @@ def __clear_head_cache(): def refresh_index(): """Refresh index with stat() information from the working directory. """ - GRun('git-update-index', '-q', '--unmerged', '--refresh').run() + GRun('update-index', '-q', '--unmerged', '--refresh').run() def rev_parse(git_id): """Parse the string and return a verified SHA1 id """ try: - return GRun('git-rev-parse', '--verify', git_id).output_one_line() + return GRun('rev-parse', '--verify', git_id + ).discard_stderr().output_one_line() except GitRunException: raise GitException, 'Unknown revision: %s' % git_id @@ -311,13 +366,20 @@ def create_branch(new_branch, tree_id = None): if branch_exists(new_branch): raise GitException, 'Branch "%s" already exists' % new_branch + current_head_file = get_head_file() current_head = get_head() set_head_file(new_branch) __set_head(current_head) # a checkout isn't needed if new branch points to the current head if tree_id: - switch(tree_id) + try: + switch(tree_id) + except GitException: + # Tree switching failed. Revert the head file + set_head_file(current_head_file) + delete_branch(new_branch) + raise if os.path.isfile(os.path.join(basedir.get(), 'MERGE_HEAD')): os.remove(os.path.join(basedir.get(), 'MERGE_HEAD')) @@ -334,9 +396,9 @@ def switch_branch(new_branch): if tree_id != get_head(): refresh_index() try: - GRun('git-read-tree', '-u', '-m', get_head(), tree_id).run() + GRun('read-tree', '-u', '-m', get_head(), tree_id).run() except GitRunException: - raise GitException, 'git-read-tree failed (local changes maybe?)' + raise GitException, 'read-tree failed (local changes maybe?)' __head = tree_id set_head_file(new_branch) @@ -346,9 +408,9 @@ def switch_branch(new_branch): def delete_ref(ref): if not ref_exists(ref): raise GitException, '%s does not exist' % ref - sha1 = GRun('git-show-ref', '-s', ref).output_one_line() + sha1 = GRun('show-ref', '-s', ref).output_one_line() try: - GRun('git-update-ref', '-d', ref, sha1).run() + GRun('update-ref', '-d', ref, sha1).run() except GitRunException: raise GitException, 'Failed to delete ref %s' % ref @@ -361,21 +423,24 @@ def rename_ref(from_ref, to_ref): if ref_exists(to_ref): raise GitException, '"%s" already exists' % to_ref - sha1 = GRun('git-show-ref', '-s', from_ref).output_one_line() + sha1 = GRun('show-ref', '-s', from_ref).output_one_line() try: - GRun('git-update-ref', to_ref, sha1, '0'*40).run() + GRun('update-ref', to_ref, sha1, '0'*40).run() except GitRunException: raise GitException, 'Failed to create new ref %s' % to_ref try: - GRun('git-update-ref', '-d', from_ref, sha1).run() + GRun('update-ref', '-d', from_ref, sha1).run() except GitRunException: raise GitException, 'Failed to delete ref %s' % from_ref def rename_branch(from_name, to_name): """Rename a git branch.""" rename_ref('refs/heads/%s' % from_name, 'refs/heads/%s' % to_name) - if get_head_file() == from_name: - set_head_file(to_name) + try: + if get_head_file() == from_name: + set_head_file(to_name) + except DetachedHeadException: + pass # detached HEAD, so the renamee can't be the current branch reflog_dir = os.path.join(basedir.get(), 'logs', 'refs', 'heads') if os.path.exists(reflog_dir) \ and os.path.exists(os.path.join(reflog_dir, from_name)): @@ -403,7 +468,7 @@ def add(names): if files: try: - GRun('git-update-index', '--add', '--').xargs(files) + GRun('update-index', '--add', '--').xargs(files) except GitRunException: raise GitException, 'Unable to add file' @@ -411,7 +476,7 @@ def __copy_single(source, target, target2=''): """Copy file or dir named 'source' to name target+target2""" # "source" (file or dir) must match one or more git-controlled file - realfiles = GRun('git-ls-files', source).output_lines() + realfiles = GRun('ls-files', source).output_lines() if len(realfiles) == 0: raise GitException, '"%s" matches no git-controled files' % source @@ -479,10 +544,10 @@ def rm(files, force = False): if os.path.exists(f): raise GitException, '%s exists. Remove it first' %f if files: - GRun('git-update-index', '--remove', '--').xargs(files) + GRun('update-index', '--remove', '--').xargs(files) else: if files: - GRun('git-update-index', '--force-remove', '--').xargs(files) + GRun('update-index', '--force-remove', '--').xargs(files) # Persons caching __user = None @@ -538,9 +603,6 @@ def committer(): def update_cache(files = None, force = False): """Update the cache information for the given files """ - if not files: - files = [] - cache_files = tree_status(files, verbose = False) # everything is up-to-date @@ -557,9 +619,9 @@ def update_cache(files = None, 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']] - GRun('git-update-index', '--add', '--').xargs(add_files) - GRun('git-update-index', '--force-remove', '--').xargs(rm_files) - GRun('git-update-index', '--').xargs(m_files) + GRun('update-index', '--add', '--').xargs(add_files) + GRun('update-index', '--force-remove', '--').xargs(rm_files) + GRun('update-index', '--').xargs(m_files) return True @@ -569,8 +631,6 @@ def commit(message, files = None, parents = None, allowempty = False, committer_name = None, committer_email = None): """Commit the current tree to repository """ - if not files: - files = [] if not parents: parents = [] @@ -588,7 +648,7 @@ def commit(message, files = None, parents = None, allowempty = False, # write the index to repository if tree_id == None: - tree_id = GRun('git-write-tree').output_one_line() + tree_id = GRun('write-tree').output_one_line() set_head = True # the commit @@ -603,7 +663,7 @@ def commit(message, files = None, parents = None, allowempty = False, env['GIT_COMMITTER_NAME'] = committer_name if committer_email: env['GIT_COMMITTER_EMAIL'] = committer_email - commit_id = GRun('git-commit-tree', tree_id, + commit_id = GRun('commit-tree', tree_id, *sum([['-p', p] for p in parents], []) ).env(env).raw_input(message).output_one_line() if set_head: @@ -628,7 +688,7 @@ def apply_diff(rev1, rev2, check_index = True, files = None): diff_str = diff(files, rev1, rev2) if diff_str: try: - GRun('git-apply', *index_opt).raw_input( + GRun('apply', *index_opt).raw_input( diff_str).discard_stderr().no_output() except GitRunException: return False @@ -647,7 +707,7 @@ def merge(base, head1, head2, recursive = False): # general when pushing or picking patches) try: # discard output to mask the verbose prints of the tool - GRun('git-merge-recursive', base, '--', head1, head2 + GRun('merge-recursive', base, '--', head1, head2 ).discard_output() except GitRunException, ex: err_output = str(ex) @@ -657,16 +717,16 @@ def merge(base, head1, head2, recursive = False): # distance between base and heads is small, i.e. folding or # synchronising patches) try: - GRun('git-read-tree', '-u', '-m', '--aggressive', + GRun('read-tree', '-u', '-m', '--aggressive', base, head1, head2).run() except GitRunException: - raise GitException, 'git-read-tree failed (local changes maybe?)' + raise GitException, 'read-tree failed (local changes maybe?)' # check the index for unmerged entries files = {} stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S) - for line in GRun('git-ls-files', '--unmerged', '--stage', '-z' + for line in GRun('ls-files', '--unmerged', '--stage', '-z' ).raw_output().split('\0'): if not line: continue @@ -707,64 +767,34 @@ def merge(base, head1, head2, recursive = False): if errors: raise GitException, 'GIT index merging failed (possible conflicts)' -def status(files = None, modified = False, new = False, deleted = False, - conflict = False, unknown = False, noexclude = False, - diff_flags = []): - """Show the tree status - """ - if not files: - files = [] - - cache_files = tree_status(files, unknown = True, noexclude = noexclude, - diff_flags = diff_flags) - all = not (modified or new or deleted or conflict or unknown) - - if not all: - filestat = [] - if modified: - filestat.append('M') - if new: - filestat.append('A') - filestat.append('N') - if deleted: - filestat.append('D') - if conflict: - filestat.append('C') - if unknown: - filestat.append('?') - cache_files = [x for x in cache_files if x[0] in filestat] - - for fs in cache_files: - if files and not fs[1] in files: - continue - if all: - out.stdout('%s %s' % (fs[0], fs[1])) - else: - out.stdout('%s' % fs[1]) - -def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = []): +def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [], + binary = True): """Show the diff between rev1 and rev2 """ if not files: files = [] + if binary and '--binary' not in diff_flags: + diff_flags = diff_flags + ['--binary'] if rev1 and rev2: - return GRun('git-diff-tree', '-p', + return GRun('diff-tree', '-p', *(diff_flags + [rev1, rev2, '--'] + files)).raw_output() elif rev1 or rev2: refresh_index() if rev2: - return GRun('git-diff-index', '-p', '-R', + return GRun('diff-index', '-p', '-R', *(diff_flags + [rev2, '--'] + files)).raw_output() else: - return GRun('git-diff-index', '-p', + return GRun('diff-index', '-p', *(diff_flags + [rev1, '--'] + files)).raw_output() else: return '' +# TODO: take another parameter representing a diff string as we +# usually invoke git.diff() form the calling functions def diffstat(files = None, rev1 = 'HEAD', rev2 = None): """Return the diffstat between rev1 and rev2.""" - return GRun('git-apply', '--stat', '--summary' + return GRun('apply', '--stat', '--summary' ).raw_input(diff(files, rev1, rev2)).raw_output() def files(rev1, rev2, diff_flags = []): @@ -772,7 +802,7 @@ def files(rev1, rev2, diff_flags = []): """ result = [] - for line in GRun('git-diff-tree', *(diff_flags + ['-r', rev1, rev2]) + for line in GRun('diff-tree', *(diff_flags + ['-r', rev1, rev2]) ).output_lines(): result.append('%s %s' % tuple(line.split(' ', 4)[-1].split('\t', 1))) @@ -783,29 +813,26 @@ def barefiles(rev1, rev2): """ result = [] - for line in GRun('git-diff-tree', '-r', rev1, rev2).output_lines(): + for line in GRun('diff-tree', '-r', rev1, rev2).output_lines(): result.append(line.split(' ', 4)[-1].split('\t', 1)[-1]) return '\n'.join(result) -def pretty_commit(commit_id = 'HEAD', diff_flags = []): +def pretty_commit(commit_id = 'HEAD', flags = []): """Return a given commit (log + diff) """ - return GRun('git-diff-tree', - *(diff_flags - + ['--cc', '--always', '--pretty', '-r', commit_id]) - ).raw_output() + return GRun('show', *(flags + [commit_id])).raw_output() def checkout(files = None, tree_id = None, force = False): """Check out the given or all files """ if tree_id: try: - GRun('git-read-tree', '--reset', tree_id).run() + GRun('read-tree', '--reset', tree_id).run() except GitRunException: - raise GitException, 'Failed git-read-tree --reset %s' % tree_id + raise GitException, 'Failed "git read-tree" --reset %s' % tree_id - cmd = ['git-checkout-index', '-q', '-u'] + cmd = ['checkout-index', '-q', '-u'] if force: cmd.append('-f') if files: @@ -816,12 +843,15 @@ def checkout(files = None, tree_id = None, force = False): def switch(tree_id, keep = False): """Switch the tree to the given id """ - if not keep: + if keep: + # only update the index while keeping the local changes + GRun('read-tree', tree_id).run() + else: refresh_index() try: - GRun('git-read-tree', '-u', '-m', get_head(), tree_id).run() + GRun('read-tree', '-u', '-m', get_head(), tree_id).run() except GitRunException: - raise GitException, 'git-read-tree failed (local changes maybe?)' + raise GitException, 'read-tree failed (local changes maybe?)' __set_head(tree_id) @@ -846,7 +876,7 @@ def reset(files = None, tree_id = None, check_out = True): __set_head(tree_id) def fetch(repository = 'origin', refspec = None): - """Fetches changes from the remote repository, using 'git-fetch' + """Fetches changes from the remote repository, using 'git fetch' by default. """ # we update the HEAD @@ -858,10 +888,10 @@ def fetch(repository = 'origin', refspec = None): command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \ config.get('stgit.fetchcmd') - GRun(*(command.split() + args)).run() + Run(*(command.split() + args)).run() def pull(repository = 'origin', refspec = None): - """Fetches changes from the remote repository, using 'git-pull' + """Fetches changes from the remote repository, using 'git pull' by default. """ # we update the HEAD @@ -873,12 +903,33 @@ def pull(repository = 'origin', refspec = None): command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \ config.get('stgit.pullcmd') - GRun(*(command.split() + args)).run() + Run(*(command.split() + args)).run() + +def rebase(tree_id = None): + """Rebase the current tree to the give tree_id. The tree_id + argument may be something other than a GIT id if an external + command is invoked. + """ + command = config.get('branch.%s.stgit.rebasecmd' % get_head_file()) \ + or config.get('stgit.rebasecmd') + if tree_id: + args = [tree_id] + elif command: + args = [] + else: + raise GitException, 'Default rebasing requires a commit id' + if command: + # clear the HEAD cache as the custom rebase command will update it + __clear_head_cache() + Run(*(command.split() + args)).run() + else: + # default rebasing + reset(tree_id = tree_id) def repack(): """Repack all objects into a single pack """ - GRun('git-repack', '-a', '-d', '-f').run() + GRun('repack', '-a', '-d', '-f').run() def apply_patch(filename = None, diff = None, base = None, fail_dump = True): @@ -901,7 +952,7 @@ def apply_patch(filename = None, diff = None, base = None, refresh_index() try: - GRun('git-apply', '--index').raw_input(diff).no_output() + GRun('apply', '--index').raw_input(diff).no_output() except GitRunException: if base: switch(orig_head) @@ -922,13 +973,13 @@ def apply_patch(filename = None, diff = None, base = None, def clone(repository, local_dir): """Clone a remote repository. At the moment, just use the - 'git-clone' script + 'git clone' script """ - GRun('git-clone', repository, local_dir).run() + GRun('clone', repository, local_dir).run() def modifying_revs(files, base_rev, head_rev): """Return the revisions from the list modifying the given files.""" - return GRun('git-rev-list', '%s..%s' % (base_rev, head_rev), '--', *files + return GRun('rev-list', '%s..%s' % (base_rev, head_rev), '--', *files ).output_lines() def refspec_localpart(refspec): @@ -1011,11 +1062,14 @@ def fetch_head(): m = re.match('^([^\t]*)\t\t', line) if m: if fetch_head: - raise GitException, "StGit does not support multiple FETCH_HEAD" + raise GitException, 'StGit does not support multiple FETCH_HEAD' else: fetch_head=m.group(1) stream.close() + if not fetch_head: + out.warn('No for-merge remote head found in FETCH_HEAD') + # here we are sure to have a single fetch_head return fetch_head @@ -1023,4 +1077,4 @@ def all_refs(): """Return a list of all refs in the current repository. """ - return [line.split()[1] for line in GRun('git-show-ref').output_lines()] + return [line.split()[1] for line in GRun('show-ref').output_lines()]