diff --git a/GitSvnExt/__init__.py b/GitSvnExt/__init__.py new file mode 120000 index 0000000..624cc90 --- /dev/null +++ b/GitSvnExt/__init__.py @@ -0,0 +1 @@ +../git-svn-ext \ No newline at end of file diff --git a/README.txt b/README.txt index 25ef868..5cceffb 100644 --- a/README.txt +++ b/README.txt @@ -1,9 +1,13 @@ -git-svn-clone-externals -======================= +git-svn-ext +=========== -This is a very simple shell script to make git-svn clone your svn:externals -definitions. Place the script in a directory where you have one or more -svn:externals definitions, run it, and it will: +This work is based on git-svn-clone-externals and the associated +scripts written by Andre Pang. + +The methodology remains the same. Put git-svn-ext in your path and +run it from the top-level tree checked out with git-svn. git-svn-ext +will detect and git-svn clone the externals using the following +method: * git svn clone each external into a .git_externals/ directory. * symlink the cloned repository in .git_externals/ to the proper directory @@ -11,74 +15,34 @@ svn:externals definitions, run it, and it will: * add the symlink and .git_externals/ to the .git/info/excludes/ file, so that you're not pestered about it when performing a git status. -That's pretty much about it. Low-tech and cheap and cheery, but I couldn't -find anything else like it after extensive Googling, so hopefully some other -people out there with low-tech minds like mine will find this useful. - -You could certainly make the script a lot more complex and do things such as -share svn:externals repositories between different git repositories, traverse -through the entire git repository to detect svn:externals definitions instead -of having to place the script in the correct directory, etc... but this works, -it's simple, and it does just the one thing, unlike a lot of other git/svn -integration scripts that I've found. I absolutely do welcome those features, -but I figured I'd push this out since it works for me and is probably useful -for others. - -NB: This assumes you have passwordless svn. - -Enjoy, - -- Andre Pang - - -Tools -===== - -The git-svn-clone-externals script does a great job of cloning the -svn:externals. However, in day-to-day work I want to check whether I -need to push stuff and update a buch of 'external' repositories in one -go. Therefore I creates some additional scripts. - -* ``git-svn-check-unpushed`` tries to determine whether there are - commits which are not yet pushed back to the subversion - repository. Originally I took this idea from Magit (an interface to - the version control system Git, implemented as an extension to - Emacs) and implemented it in Python instead of Lisp. +but the code has been converted to python as one script with +sub-commands, supports newer subversion urls, and adds a few new +features. - This script can be run in every location of a git repository. +From the usage output: -* git-svn-externals-check is a script that displays whether there are - uncommitted changes or commits that are not pushed to the subversion - repository yet. Basically it executes ``git status`` and the - ``git-svn-check-unpushed`` scripts for each directory in the current - directory. +Usage: ./git-svn-ext [sub command args] - This script must be run in the directory where the original - svn:externals property was set. It does not walk the complete tree - to search for these kind of repositories. + sub commmands: + clone : clone all svn externals into .git_externals + (warning: removes local changes and commits on subsequent runs) + update : Updates all svn externals (git svn fetch[ --revision]/rebase --local) + check-unpushed : Check if local git-svn checkout has unpushed commits + check : run 'git status' and 'check-unpushed' for all externals + for-all : run a command against all externals + (ie: git svn-ext for-all git grep 'whatever') -* git-svn-externals-update complements the git-svn-externals-check - script. The update script does an ``git svn fetch`` and ``git svn - rebase`` for every directory in the location where the script is - executed. + Notes: + externals may be ignored if listed in .git_external_excludes - This script must also be run in the directory where the original - svn:externals property was set. - - -Feel free to use and improve these scripts. - -- Mark van Lent - -Options +Authors ======= - -* External repository's url can be rewritten, to use SVN+SSH instead of - plain HTTP or HTTPS. To do so, do `export USE_SSH=yes` in your environment. - This can be useful if you use ssh authentication, but other developers don't. -* If you don't want to pull all external repositories, you can create a - .git_externals_exclude file which contains the local paths to be excluded, - one per line, the same way they show up on the first field of git svn show-externals - +- Andre Pang +- Mark van Lent - Alexander Artemenko +- Wade Berrier +TODO +==== +- make clone not blow stuff away +- handle the case when externals are removed or altered diff --git a/git-svn-check-unpushed b/git-svn-check-unpushed deleted file mode 100755 index 8d6ddb8..0000000 --- a/git-svn-check-unpushed +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/python - -from commands import getoutput -import sys -import logging - -logger = logging.getLogger() -console = logging.StreamHandler() -formatter = logging.Formatter('%(levelname)-8s %(message)s') -console.setFormatter(formatter) -logger.addHandler(console) -logger.level = logging.INFO - - -def list_references(): - """List the references in the local repo. - - Return a tuple with the SHA1 of HEAD and a dictionary with the - SHA1 of the references as keys and the reference name as value. - """ - references = getoutput('git show-ref --head') - HEAD_ref = None - refs = {} - for item in references.split('\n'): - sha1, name = item.split() - if name == 'HEAD': - HEAD_ref = sha1 - refs[sha1] = name - return HEAD_ref, refs - - -def commit_objects(): - """List commit objects in reverse chronological order. - - Return a dict with the SHA1 of the commits as keys and the SHA1 of - the parents as values. - """ - commit_list = getoutput('git rev-list --all --parents') - commits = {} - for item in commit_list.split('\n'): - splitted_item = item.split() - if len(splitted_item) != 2: - commit = splitted_item[0] - parent = None - else: - commit, parent = splitted_item - commits[commit] = parent - return commits - - -def find_svn_branch_name(): - """Return the reference name of the current remote branch.""" - head, references = list_references() - commits = commit_objects() - - current_commit = head - while current_commit: - if current_commit in references: - reference_name = references[current_commit] - if ('remote' in reference_name or - 'trunk' in reference_name or - 'git-svn' in reference_name): - logger.debug('Found remote: %s', reference_name) - return reference_name - # find the parent of the current commit - # and make it the next commit to investigate - if current_commit in commits: - current_commit = commits[current_commit] - return None - - -def find_uncommitted(svn_branch): - """Given the name of the remote branch, show log of the commits.""" - output = getoutput('git log --pretty="format:%%h %%s" %s..HEAD' % - svn_branch) - if output: - print 'Possible unpushed commits (against %s):' % svn_branch - print output - else: - print 'No unpushed commits found.' - - -if __name__ == '__main__': - status = getoutput('git status') - if status.startswith('fatal'): - print status - sys.exit(1) - svn_branch = find_svn_branch_name() - if svn_branch is None: - print "No svn branch found" - sys.exit(1) - logger.debug('Found branch: %s', svn_branch) - find_uncommitted(svn_branch) diff --git a/git-svn-clone-externals b/git-svn-clone-externals deleted file mode 100755 index 091cab3..0000000 --- a/git-svn-clone-externals +++ /dev/null @@ -1,141 +0,0 @@ -#!/bin/bash - -set -e - -toplevel_directory="$(git rev-parse --show-cdup)" -[ -n "$toplevel_directory" ] && { echo "please run from the toplevel directory"; exit 1; } - - -function call() -{ - cmd="$@" - echo "$cmd" - eval "$cmd" - return "$?" -} - -function do_clone() -{ - test -d .git_externals || return 1 - module=`echo $remote_url|sed 's,\(.*\)\(/trunk\|/branch.*\|/tag.*\),\1,'` - branch=`echo $remote_url|sed 's,\(.*\)\(/trunk\|/branch.*\|/tag.*\),\2,'|sed 's,^/,,'` - if [[ $branch = $remote_url ]]; then - branch="" - fi - ( - cd .git_externals - if [ -d "$local_directory" ]; then - ( - cd "$local_directory" - call git svn fetch --all - ) - else - tags="tags" - brch="branches" - branchpath=$(echo $branch|cut -f1 -d/) - echo $tags|grep $branchpath >/dev/null 2>&1 && tags=$branchpath - echo $brch|grep $branchpath >/dev/null 2>&1 && brch=$branchpath - - if [ "$module" = "$remote_url" ]; then - # URL does not contains any trunk, branches or tags part, so we dont need - # additional options for git-svn - call git svn clone "$revision" "$module" "$local_directory" - else - call git svn clone "$revision" "$module" -T trunk -b $brch -t $tags "$local_directory" - fi - - fi - ( - branch="$(echo ${branch}|sed 's,/$,,')" - if [ -n "$branch" ]; then - cd "$local_directory" - call git reset --hard $branch - fi - ) - ) -} - -function do_link() -{ - dir="$1" - base="$(dirname $dir)" - ( - mkdir -p "$base" - cd $base - rel=$(git rev-parse --show-cdup) - ln -sf ${rel}.git_externals/"$dir" - ) -} - -function do_excludes() -{ - dir="$1" - git_excludes_path=.git/info/exclude - if ! grep -q '^.git_externals$' "$git_excludes_path" - then - echo .git_externals >> "$git_excludes_path" - fi - - if ! grep -q '^'"$dir"'$' "$git_excludes_path" - then - echo "$dir" >> "$git_excludes_path" - fi -} - -function is_excluded() -{ - local result=0 - if [ -f .git_externals_exclude ] ; then - matches=`grep -v "^#" .git_externals_exclude|grep "^/$1$"|wc -l` - if [ $matches -gt 0 ] ; then - local result=1 - fi - fi - echo $result - return -} - - -git svn show-externals | grep -vE '#|^$' | \ - sed 's/\(-r\)[ ]*\([0-9]\{1,\}\)/\1\2/' | \ - while read svn_externals -do - - number_fields="$(echo ${svn_externals}|awk '{print NF}')" - case $number_fields in - 2) - local_directory="$(echo ${svn_externals} | awk '{print $1}' | sed 's,^/,,')" - revision="" - remote_url="$(echo ${svn_externals} | awk '{print $2}')" - ;; - 3) - local_directory="$(echo ${svn_externals} | awk '{print $1}' | sed 's,^/,,')" - revision=""$(echo ${svn_externals} | awk '{print $2}') - remote_url="$(echo ${svn_externals} | awk '{print $3}')" - ;; - *) continue ;; - esac - - check_excluded=$(is_excluded $local_directory) - if [ $check_excluded -eq 0 ] ; then - if [ -n "$USE_SSH" ]; then - echo "Rewriting url to use SVN+SSH." - shopt -s extglob - remote_url="${remote_url/+(http|https)/svn+ssh}" - fi - - [ -z "${remote_url}" ] && continue - - export local_directory revision remote_url - - echo "$local_directory -> $remote_url" - - dir=`dirname $local_directory` - [ -d ".git_externals/$dir" ] || mkdir -p ".git_externals/$dir" - - do_clone "$revision" "$remote_url" "$local_directory" || exit - do_link "$local_directory" - do_excludes "$local_directory" - fi - -done \ No newline at end of file diff --git a/git-svn-ext b/git-svn-ext new file mode 100755 index 0000000..260f294 --- /dev/null +++ b/git-svn-ext @@ -0,0 +1,718 @@ +#!/usr/bin/env python + +import subprocess +import os +import re +import sys +import logging + +logger = logging.getLogger() +console = logging.StreamHandler() +formatter = logging.Formatter('%(levelname)-8s %(message)s') +console.setFormatter(formatter) +logger.addHandler(console) +logger.level = logging.INFO +#logger.level = logging.DEBUG + +# This is so we can change directories but still have a reference +# to ourselves +git_svn_ext_fullpath = os.path.abspath(sys.argv[0]) + +################## shell helpers ########################### +def getoutput(command, include_stderr=True): + """Get the output of a command, optionally ignoring stderr""" + + if include_stderr: + stderr=subprocess.STDOUT + else: + # Pipe stderr, but we ignore it + stderr=subprocess.PIPE + + proc = subprocess.Popen(command, shell=True, stderr=stderr, stdout=subprocess.PIPE) + + # Wait for the process to finish + proc.wait() + + # Get stdout (which may optionally include stderr) + output = proc.communicate()[0] + + # Keep compatibility with commands.getoutput() by removing trailing newline + output = output.rstrip(b'\n') + + return output + +################## git/svn Helper Methods ################## + +def list_references(): + """List the references in the local repo. + + Return a tuple with the SHA1 of HEAD and a dictionary with the + SHA1 of the references as keys and the reference name as value. + """ + references = getoutput('git show-ref --head') + HEAD_ref = None + refs = {} + for item in references.decode('utf-8').split('\n'): + stuff = item.split() + if len(stuff) == 2: + (sha1, name) = stuff + if name == 'HEAD': + HEAD_ref = sha1 + refs[sha1] = name + return HEAD_ref, refs + + +def commit_objects(): + """List commit objects in reverse chronological order. + + Return a dict with the SHA1 of the commits as keys and the SHA1 of + the parents as values. + """ + commit_list = getoutput('git rev-list --all --parents') + commits = {} + for item in commit_list.decode('utf-8').split('\n'): + splitted_item = item.split() + if len(splitted_item) != 2: + commit = splitted_item[0] + parent = None + else: + commit, parent = splitted_item + commits[commit] = parent + return commits + + +def find_svn_branch_name(): + """Return the reference name of the current remote branch.""" + head, references = list_references() + commits = commit_objects() + current_commit = head + while current_commit: + if current_commit in references: + reference_name = references[current_commit] + if ('remote' in reference_name or + 'trunk' in reference_name or + 'git-svn' in reference_name): + logger.debug('Found remote: %s', reference_name) + return reference_name + # find the parent of the current commit + # and make it the next commit to investigate + if current_commit in commits: + current_commit = commits[current_commit] + return None + + +def find_uncommitted(svn_branch): + """Given the name of the remote branch, show log of the commits.""" + + # One line commit of uncommitted commits + results = [] + output = getoutput('git log --pretty="format:%%h %%s" %s..HEAD' % + svn_branch) + if output: + results += output.decode('utf-8').split('\n') + + return results + + +def get_svn_info(): + """Parse 'git svn info'""" + + results = {} + found = False + for line in getoutput("git svn info").decode('utf-8').split("\n"): + match = re.search("(.+): (.+)", line) + if match: + found = True + results[match.group(1)] = match.group(2) + + if not found: + logger.error("'git svn info' failed") + sys.exit(1) + + return results + +def get_relative_base(): + """Get relative directory to the base of the checkout""" + return getoutput("git rev-parse --show-cdup") + + +def get_git_svn_externals(): + """returns a hash of string->array where the key is the location + in the source of the external property, and the array is the list + of externals""" + + results = {} + + external_dir = "" + for line in getoutput("git svn show-externals", include_stderr=False).decode('utf-8').split("\n"): + + external_dir_match = re.search(r"# (.*)", line) + if external_dir_match: + # the external dir is a relative dir, but starts with a slash + # remove the leading / + if external_dir_match.group(1) and external_dir_match.group(1)[0] == "/": + tmp = re.sub("^/", "", external_dir_match.group(1)) + if tmp != "": + external_dir = tmp + elif line != "": + # start array + if external_dir not in results: results[external_dir] = [] + + # NOTE: git-svn prepends the external with external_dir, + # undo that + line = re.sub("^/" + external_dir, "", line) + results[external_dir].append(line.rstrip()) + + logger.debug("Externals: " + str(results)) + return results + +def exit_if_not_toplevel(): + """exit if not run from the top level git directory""" + toplevel_directory = get_relative_base() + if toplevel_directory.decode("utf-8") != "": + logger.error("not in toplevel directory") + sys.exit(1) + +################## Misc Helper Methods ################## + +def create_dir_if_not_exist(dir): + """helper to create a directory if it doesn't exist""" + if not os.path.exists(dir): os.makedirs(dir) + +def file_contains_line(filename, line): + found = False + for l in open(filename).readlines(): + if line == l.rstrip(): + found = True + break + + return found + + +def append_line_if_not_included(filename, line): + + if not file_contains_line(filename, line): + fd = open(filename, "a") + # TODO: do we need to have a newline to the end of the file + # before appending? + fd.write(line + "\n") + fd.close() + + +def run_command(command): + """print out and run a command""" + print (command) + return os.system(command) + + +######## Class to parse and represent svn External ########## + +class SvnExternal: + """Represent and parse svn externals from a string + NOTE: must be in the directory where the external is! + because 'git svn info' is queried as part of this + + svn_info is passed in for ease of testing + """ + + def __init__(self, external_string, svn_info): + self.dir = "" + self.url = "" + self.rev = "" + + # 1.6 allows you to reference directly to a file (ignore for now) + self.is_file = False + + # There are a lot of different formats + # http://svnbook.red-bean.com/en/1.7/svn.advanced.externals.html + # + # <1.5 format: + # third-party/skins/toolkit -r21 http://svn.example.com/skin-maker + # 1.5 formats: + # -r21 http://svn.example.com/skin-maker third-party/skins/toolkit + # http://svn.example.com/skin-maker@21 third-party/skins/toolkit + # >=1.6 formats: + # ^/sounds third-party/sounds + # /skinproj@148 third-party/skins + # //svn.example.com/skin-maker@21 third-party/skins/toolkit + # ../skinproj@148 third-party/skins + # file format: + # ^/trunk/bikeshed/blue.html@40 green.html + # + # There's also a quoting and url encode mechanism which isn't implemented yet + + svn_access_part = r"(http|https|svn|svn\+ssh|file)" + scheme_part = r"(%s://)" % svn_access_part + dash_rev_part = r"(-r\s*(?P\d+))?\s*" + + re_pre_1_5 = re.compile(r"(?P.+?)\s+%s(?P%s.+)" % ( dash_rev_part, scheme_part)) + re_1_5_plus = re.compile(r"/?%s'?(?P.+?)(@(?P\d+))?'?\s+(?P.+)" % (dash_rev_part)) + + match_dict = {} + postprocess_1_5 = False + match_pre_1_5 = re_pre_1_5.search(external_string) + match_1_5_plus = re_1_5_plus.search(external_string) + if match_pre_1_5 and not re.search("^\s*-r", external_string): # make sure it doesn't start with -r + logger.debug("Matched as pre 1.5 external: " + external_string) + match_dict = match_pre_1_5.groupdict() + elif match_1_5_plus: + logger.debug("Matched as 1.5 or later external: " + external_string) + postprocess_1_5 = True + match_dict = match_1_5_plus.groupdict() + else: + logger.error("Unable to parse external: " + external_string) + sys.exit(1) + + logger.debug("Matches: " + str(match_dict)) + + self.dir = match_dict["dir"].lstrip().rstrip() + self.url = match_dict["url"].lstrip().rstrip() + if match_dict["rev"] != None: + self.rev = match_dict["rev"].lstrip().rstrip() + # this key may not always be in the dict + elif "rev2" in match_dict and match_dict["rev2"] != None: + self.rev = match_dict["rev2"].lstrip().rstrip() + + # Handle the several additional options available in 1.5 and beyond + if postprocess_1_5: + url = self.url + logger.debug("Url before post process: " + url) + if url.startswith("^/"): + # Relative to the repository root + url = url[2:] # remove the prefix + url = svn_info["Repository Root"] + os.sep + url + elif url.startswith("//"): + # Relative to the repository URL scheme + url = url[2:] # remove the prefix + scheme = re.search(scheme_part, svn_info["URL"]).group(1) + url = scheme + url + elif url.startswith("/"): + # Relative to the repository server + scheme_server = re.search("(%s.+?)/" % scheme_part, svn_info["URL"]).group(1) + url = scheme_server + url + elif url.startswith("../"): + # Relative to the repository URL + url = svn_info["URL"] + os.sep + url + + logger.debug("Url after post process: " + url) + + # Normalize the path (since .. can also appear in the middle) + + # split apart because normpath turns "//" into "/" + matches = re.search("(%s.+?)/(.+)" % scheme_part, url) + + #logger.debug("Groups: " + str(matches.groups())) + + scheme_server = matches.group(1) + # group 4 because of all the nested parens + path = os.path.normpath(matches.group(4)) + + #logger.debug("Scheme and server: " + scheme_server) + #logger.debug("Path: " + path) + + # Stick them back together + # maybe a better url parsing method would be better? + url = scheme_server + os.sep + path + + # Legacy: Honor env var to override scheme + # NOTE: projects should use the "//" notation instead + if "USE_SSH" in os.environ: + url = re.sub(svn_access_part, "svn+ssh", url) + + logger.debug("Url after normalization and optional scheme change: " + url) + + self.url = url + + +######## Class to represent git-svn External ########## + +class GitSvnExternal: + """Class to hold and manipulate data about an svn external + + external_dir: directory in the source that contains the svn:external property + local_dir : the location relative to external_dir where the external source will be + remote_url : svn external + revision : optional pinned revision + """ + + ExternalsDir = ".git_externals" + LocalExclude = ".git/info/exclude" + ExcludesFile = ".git_external_excludes" + + def __init__(self, external_dir, svn_external): + self.external_dir = external_dir + self.local_dir = svn_external.dir + self.remote_url = svn_external.url + self.revision = svn_external.rev + + # maybe have this go to the repo root automatically? + @staticmethod + def GetClonedPaths(): + """Get a list of paths to externals for the current dir""" + + results = [] + + if os.path.isdir(GitSvnExternal.ExternalsDir): + for external in os.listdir(GitSvnExternal.ExternalsDir): + gitdir = os.path.join(GitSvnExternal.ExternalsDir, external, ".git") + if os.path.isdir(gitdir): + results.append(os.path.dirname(gitdir)) + + return results + + @staticmethod + def GetExternals(): + ret = [] + + for src_dir, externals in list(get_git_svn_externals().items()): + for ext in externals: + + logger.debug("Source dir: %s, External: %s" % (src_dir, ext)) + + svn_ext = SvnExternal(ext, get_svn_info()) + + git_svn_ext = GitSvnExternal(src_dir, svn_ext) + + # Allow exclusion of externals + if not git_svn_ext.is_excluded_(): + ret.append(git_svn_ext) + else: + logger.info("Excluding svn external: " + git_svn_ext.symlink_()) + + return ret + + def local_storage(self): + """Get the directory that this external will be cloned to""" + + # TODO/NOTE: it's possible to have 2 externals of the same name + # in different directories + return GitSvnExternal.ExternalsDir + os.sep + self.local_dir + + def symlink_(self): + """Get the path of the symlink that points to local_storage()""" + return self.external_dir + self.local_dir + + def revision_arguments(self): + """Format argumntes if there's a revision""" + + # Format the revision argument + rev_arg = "" + if self.revision != "": + rev_arg = "--revision BASE:" + self.revision + return rev_arg + + def is_excluded_(self): + """return True if excluding this external""" + + excluded = False + if os.path.exists(GitSvnExternal.ExcludesFile): + excluded = file_contains_line(GitSvnExternal.ExcludesFile, self.symlink_()) + + return excluded + + def create_link(self): + """create the link to the external""" + + # NOTE: the original used -f, so remove and recreate + # BUG: it looks like os.remove follows links? + # this fails when an invalid link exists... + if os.path.exists(self.symlink_()): + logger.debug("Removing symlink: " + self.symlink_()) + os.remove(self.symlink_()) + + # construct relative link + current_dir = os.getcwd() + # Create parent directories (eg: foo/bar/baz) if required + if not os.path.exists(os.path.dirname(self.symlink_())): + os.makedirs(os.path.dirname(self.symlink_())) + os.chdir(os.path.dirname(self.symlink_())) + rel = get_relative_base() + os.chdir(current_dir) + + source = rel + self.local_storage() + link_name = self.symlink_() + logger.debug("Creating link from %s to %s" % (source, link_name)) + os.symlink(source, link_name) + + def update_excludes(self): + """add symlink to the git excludes path""" + + append_line_if_not_included(GitSvnExternal.LocalExclude, GitSvnExternal.ExternalsDir) + append_line_if_not_included(GitSvnExternal.LocalExclude, self.symlink_()) + + + def clone(self): + """do the actual cloning""" + + (module, branch) = re.search("(.*)/(trunk|branch.*|tag.*)", self.remote_url).groups() + + if branch == self.remote_url: + branch = "" + + # Default + has_branches_tags = False + + # if we found references to branches and tags, clone them + if module != self.remote_url: + has_branches_tags = True + + # if we are cloning a subdirectory of a branch path, use the remote_url, and don't get tags/branch/trunk + # ie: if it contains some of these, but does not end with it + if re.search("trunk|tags|branch.*", self.remote_url): + if not re.search("trunk$|tags$|branch.*$", self.remote_url): + has_branches_tags = False + module = self.remote_url + + # Try to figure out what the tags and branches portions are + tags = "tags" + brch = "branches" + branchpath = branch.decode('utf-8').split("/")[0] + if tags.count(branchpath): tags = branchpath + if brch.count(branchpath): brch = branchpath + + # Format the revision argument + rev_arg = self.revision_arguments() + + # If the directory is alread there, update it + if os.path.isdir(self.local_storage()): + current_dir = os.getcwd() + os.chdir(self.local_storage()) + run_command("git svn fetch " + rev_arg) + # --local skips fetching (for rev pinning) + run_command("git svn rebase --local") + os.chdir(current_dir) + # Otherwise, clone it + else: + clone_dir = os.path.dirname(self.local_storage()) + rel_local_storage = os.path.basename(self.local_storage()) + + # Create dir to run clone from + create_dir_if_not_exist(clone_dir) + + current_dir = os.getcwd() + os.chdir(clone_dir) + + if has_branches_tags: + run_command("git svn clone %s %s --trunk trunk --branches %s --tags %s %s" % (rev_arg, module, brch, tags, rel_local_storage)) + else: + # URL does not contains any trunk, branches or tags part, so we dont need + # additional options for git-svn + run_command("git svn clone %s %s %s" % (rev_arg, module, rel_local_storage)) + + os.chdir(current_dir) + + # If we have branches and tags, checkout that directory + # careful, can blow aways changes! + # remove trailing slash + branch = re.sub("/$", "", branch) + + if branch != "" and has_branches_tags: + current_dir = os.getcwd() + os.chdir(self.local_storage()) + run_command("git reset --hard " + branch) + os.chdir(current_dir) + + + +########### sub-command handlers ############# + + +def clone_handler(args): + """clone all svn externals into .git_externals + (warning: removes local changes and commits on subsequent runs)""" + + exit_if_not_toplevel() + + externals = GitSvnExternal.GetExternals() + + if not len(externals): + logger.error("No svn externals found") + sys.exit(1) + + for external in externals: + + print("%s -> %s" % (external.local_dir, external.remote_url)) + + external.clone() + external.create_link() + external.update_excludes() + +# TODO: maybe clone should just do this instead? +# but clone can blow stuff away... (reset --hard) +def update_handler(args): + """Updates all svn externals (git svn fetch[ --revision]/rebase --local)""" + + exit_if_not_toplevel() + + # Do show externals to get possible revisions + # looks at svn server + externals = GitSvnExternal.GetExternals() + + if not len(externals): + logger.error("No svn externals found") + sys.exit(1) + + # get the externals dirs + # looks at disk + ext_paths = GitSvnExternal.GetClonedPaths() + + for ext in externals: + ext_path = ext.local_storage() + if ext_paths.count(ext_path): + print(">>> " + ext_path) + current_dir = os.getcwd() + os.chdir(ext_path) + run_command("git svn fetch %s" % ext.revision_arguments()) + run_command("git svn rebase --local") + + os.chdir(current_dir) + + +# TODO: would it be nice to have a recursive option? +# otherwise, you have to run it from the root dir +def check_unpushed_handler(args): + """Check if local git-svn checkout has unpushed commits""" + + status = getoutput('git status') + if status.decode('utf-8').startswith('fatal'): + print(status) + sys.exit(1) + svn_branch = find_svn_branch_name() + if svn_branch is None: + logger.error("no svn branch found for: " + os.getcwd()) + sys.exit(1) + logger.debug('Found branch: %s', svn_branch) + + commits = find_uncommitted(svn_branch) + + if len(commits) > 0: + print('Possible unpushed commits (against %s):' % svn_branch) + print("\n".join(commits)) + else: + print('No unpushed commits found.') + +def check_handler(args): + """run 'git status' and 'check-unpushed' for all externals""" + + exit_if_not_toplevel() + + for dir in GitSvnExternal.GetClonedPaths(): + + current_dir = os.getcwd() + os.chdir(dir) + + status = getoutput("git status") + unpushed = getoutput(git_svn_ext_fullpath + " check-unpushed") + if not re.search("clean", status.decode('utf-8')) or not re.search("No unpushed", unpushed.decode('utf-8')): + print(">>>>>>>>>>>>>>>> %s <<<<<<<<<<<<<<<<" % dir) + # Run these again to get color output + os.system("git status") + os.system(git_svn_ext_fullpath + " check-unpushed") + print("----------------------------------------") + else: + print(dir + " is clean") + + os.chdir(current_dir) + +def for_all_handler(args): + """run a command against all externals + (ie: git svn-ext for-all git grep 'whatever')""" + + command = " ".join(args) + + if command: + for dir in GitSvnExternal.GetClonedPaths(): + + current_dir = os.getcwd() + os.chdir(dir) + + print(">>> %s: %s" % (dir, command)) + + os.system(command) + + os.chdir(current_dir) + + else: + usage() + sys.exit(1) + + +class Handlers: + """Register and dispatch handlers, as well as provide documentation""" + def __init__(self): + # keep ordering + self.names = [] + self.handlers = {} + + def add(self, name, handler): + self.names.append(name) + self.handlers[name] = handler + + def get(self, name): + if name in self.handlers: + return self.handlers[name] + else: + return None + + def aligned_name_(self, name): + """string of spaces to pad command to align text""" + + # find out how long the lines are so they can line up + max_len = 0 + for n in self.names: + if len(n) > max_len: + max_len = len(n) + + return name + " " * (1 + max_len - len(name)) + + def helptext(self): + """get description of all the handlers""" + + ret = "" + + for n in self.names: + ret += " %s: %s\n" % (self.aligned_name_(n), self.handlers[n].__doc__) + + return ret + + +# NOTE: this order is preserved for usage help text +handlers = Handlers() +handlers.add('clone', clone_handler) +handlers.add('update', update_handler) +handlers.add('check-unpushed', check_unpushed_handler) +handlers.add('check', check_handler) +handlers.add('for-all', for_all_handler) + +def usage(): + """Descriptive text of all options""" + print("") + print("Usage: " + sys.argv[0] + " [sub command args]") + print("") + print(" sub commmands:") + print(handlers.helptext()) + print(" Notes:") + print(" externals may be ignored if listed in " + GitSvnExternal.ExcludesFile) + print("") + +if __name__ == '__main__': + + handler = None + + try: + sub_command = sys.argv[1] + handler = handlers.get(sub_command) + + if handler == None: + raise BaseException + + except: + usage() + sys.exit(1) + + args = [] + if len(sys.argv) > 2: + args += sys.argv[2:] + + handler(args) + diff --git a/git-svn-externals-check b/git-svn-externals-check deleted file mode 100755 index fddc313..0000000 --- a/git-svn-externals-check +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -for dir in *; do - if [ -d $dir ]; then - cd $dir - STATUS=$(git status) - UNPUSHED=$(git-svn-check-unpushed) - if [ $(echo $STATUS|grep -c "clean") -lt 1 -o \ - $(echo $UNPUSHED|grep -c "No unpushed") -lt 1 ]; then - echo '>>>>>>>>>>>>>>>>' $dir '<<<<<<<<<<<<<<<<' - git status - git-svn-check-unpushed - echo '----------------------------------------' - else - echo $dir 'is clean' - fi - cd .. - fi -done diff --git a/git-svn-externals-update b/git-svn-externals-update deleted file mode 100755 index 4313b29..0000000 --- a/git-svn-externals-update +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -toplevel_directory="$(git rev-parse --show-cdup)" -[ -n "$toplevel_directory" ] && { echo "please run from the toplevel directory"; exit 1; } - -find .git_externals -type d -name .git | while read gitdir; do - dir=$(dirname "$gitdir") - if [ -d $dir ]; then - pushd $dir - echo $dir - git svn fetch - git svn rebase - popd - fi -done diff --git a/tests b/tests new file mode 100755 index 0000000..c69ed68 --- /dev/null +++ b/tests @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +# There are a lot of formats... test them out with some examples + +git_svn_info = { + "Path": ".", + "URL": "svn+ssh://svn.example.com/skin-maker/mydir", + "Repository Root": "svn+ssh://svn.example.com/skin-maker", + "Repository UUID": "a4bac7da-eecc-44ef-9201-0e2b325a63d8", + "Revision": "1000", + "Node Kind": "directory", + "Schedule": "normal", + "Last Changed Author": "wberrier", + "Last Changed Rev": "1000", + "Last Changed Date": "2012-05-25 15:49:11 -0600 (Fri, 25 May 2012)" + } + + +import GitSvnExt + +import unittest + +class TestSvnExternalParsing(unittest.TestCase): + + def setUp(self): + pass + + def testPRE1_5_a(self): + ext = GitSvnExt.SvnExternal("third-party/skins/toolkit -r21 http://svn.example.com/skin-maker", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "21") + self.assertEqual(ext.url, "http://svn.example.com/skin-maker") + + def testPRE1_5_b(self): + """Surrounding space, no revision, and file scheme""" + ext = GitSvnExt.SvnExternal(" third-party/skins/toolkit file:///svn.example.com/skin-maker ", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "") + self.assertEqual(ext.url, "file:///svn.example.com/skin-maker") + + def test1_5_a(self): + ext = GitSvnExt.SvnExternal("http://svn.example.com/skin-maker third-party/skins/toolkit", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "") + self.assertEqual(ext.url, "http://svn.example.com/skin-maker") + + def test1_5_b(self): + ext = GitSvnExt.SvnExternal("http://svn.example.com/skin-maker@21 third-party/skins/toolkit", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "21") + self.assertEqual(ext.url, "http://svn.example.com/skin-maker") + + def test1_5_c(self): + ext = GitSvnExt.SvnExternal("-r21 http://svn.example.com/skin-maker third-party/skins/toolkit", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "21") + self.assertEqual(ext.url, "http://svn.example.com/skin-maker") + + def test1_6_a(self): + ext = GitSvnExt.SvnExternal("-r21 /skin-maker third-party/skins/toolkit", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "21") + self.assertEqual(ext.url, "svn+ssh://svn.example.com/skin-maker") + + def test1_6_b(self): + ext = GitSvnExt.SvnExternal("//svn.example.com/skin-maker@21 third-party/skins/toolkit", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "21") + self.assertEqual(ext.url, "svn+ssh://svn.example.com/skin-maker") + + def test1_6_c(self): + ext = GitSvnExt.SvnExternal("^/subdir@21 third-party/skins/toolkit", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "21") + self.assertEqual(ext.url, "svn+ssh://svn.example.com/skin-maker/subdir") + + def test1_6_d(self): + ext = GitSvnExt.SvnExternal("../subdir@21 third-party/skins/toolkit", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "21") + self.assertEqual(ext.url, "svn+ssh://svn.example.com/skin-maker/subdir") + + def test1_6_e(self): + ext = GitSvnExt.SvnExternal("^/../super-skin-maker@21 third-party/skins/toolkit", git_svn_info) + + self.assertEqual(ext.dir, "third-party/skins/toolkit") + self.assertEqual(ext.rev, "21") + self.assertEqual(ext.url, "svn+ssh://svn.example.com/super-skin-maker") + + def tearDown(self): + pass + + +if __name__ == '__main__': + unittest.main()