diff --git a/.gitignore b/.gitignore index a01ee289f..5412c5a54 100755 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .*.swp +bin/ +selinux/tmp/ diff --git a/README.mkd b/README.mkd index 945b9cf82..4cdb4bdd1 100755 --- a/README.mkd +++ b/README.mkd @@ -1,9 +1,9 @@ -# Redmine Git Hosting Plugin (v0.4.2) +# Redmine Git Hosting Plugin (v0.4.3x) A ChiliProject / Redmine plugin which makes configuring your own git hosting easy. This plugin allows straightforward management of gitolite and associated public keys, the git daemon, and integrates code from Scott Schacon's "grack" utility to provide Git Smart HTTP access. Git repositories are automatically created when the repository is created in -redmine. There is also an option to automatically create a git repository for a project, when the project is created. +Redmine. There is also an option to automatically create a git repository for a project, when the project is created. Caching functionality is also implemented to speed page-load times for viewing git repositories. @@ -41,6 +41,9 @@ Note that this guide refers to the "web server user" as the user under which Rai not always) the same as the user that runs the main web server. If you are running Rails under a different user, follow these instructions using that user, not the one for the main web server. +As of the most recent set of patches, this plugin is compatible with running multiple Redmine installations on the same server, each +with the same *or* different gitolite users/repositories. The later configuration (multiple Redmine installations, each with a different +gitolite installation) is particularly useful for a web-hosting scenario with independent developers. ## Step-By-Step configuration instructions @@ -96,40 +99,53 @@ root directory: **(6)** It is best to set several plugin variables BEFORE you run the db:migrate\_plugins task in step 7. In particular it is important -that the *httpServer*, *gitServer*, *gitUser*, *gitRepositoryBasePath*, *gitoliteIdentityFile* and *gitoliteIdentityPublicKeyFile* -variables are set correctly. To adjust these variables, open an editor and edit [redmine_rails_root]/vendor/plugins/redmine_git_hosting/init.rb file. -Starting on line 22 you will see the settings definitions you should edit. +that the *httpServer*, *gitServer*, *gitUser*, *gitoliteIdentityFile* and *gitoliteIdentityPublicKeyFile* +variables are set correctly. Others that should be set include *gitRepositoryBasePath*, *gitRedmineSubdir*, and *gitRepositoryHierarchy*; however, +the default values for these variables should be sufficient for most installations. +To adjust these variables, open an editor and edit [redmine_rails_root]/vendor/plugins/redmine_git_hosting/init.rb file. +Starting on line 22, you will see the settings definitions you should edit. -The *httpServer* variable should be set to the url used to access your redmine site, e.g. www.my-own-personal-git-host-server.com. Note that if Redmine is not -installed in the site root this should include the path to your redmine root, e.g. www.my-own-personal-git-host-server.com/path/to/redmine +The *httpServer* variable should be set to the hostname which will be used to access your Redmine site, e.g. www.my-own-personal-git-host-server.com. This variable +may optionally include a port using the ':portnum' syntax, i.e. www.my-own-person-git-host-server.com:8000. Unlike earlier versions of this plugin, this variable should +*not* include the path to your Redmine root. +The *gitServer* variable should be set to the hostname which will be used to access the gitolite repositories via ssh. Like *httpServer*, this variable may optionally include a port using the ':portnum' syntax, i.e. www.my-own-person-git-host-server.com:444. In most configurations, the *gitServer* +variable will be identical to the *httpServer*, except when ports are involved. -The *gitServer* will usually be the same as the the httpServer variable -- this is the server name to use to access the gitolite repositories via ssh. This should be -the hostname only, so this will be different from *httpServer* if Redmine is not installed in the site root. In other words, even if redmine is installed in -www.my-own-personal-git-host-server.com/path/to/redmine, *gitServer* should be set to www.my-own-personal-git-host-server.com - - -The *gitUser* is the user under which gitolite is installed +The *gitUser* is the user under which gitolite is installed. +If you followed the above directions you will not need to modify the *gitoliteIdentityFile* or *gitoliteIdentityPublicKeyFile* variables -- these specify +the path to the private/public key files for accessing the gitolite admin repository. -The *gitRepositoryBasePath* is the path *relative to the git user root* where the repositories are located. This should always end in a file separator, e.g. '/'. -Since gitolite always uses repositories/ as the default place for repositories you probably shouldn't have to change this. +Although you can change the following three variables, their default values provide for a very reasonable installation: +The *gitRepositoryBasePath* is the path *relative to the git user root* where the repositories are located. This should always be non-empty and should end +in a file separator, e.g. '/'. Since gitolite always uses repositories/ as the default place for repositories you probably shouldn't have to change this. -If you followed the above directions you will not need to modify the *gitoliteIdentityFile* or *gitoliteIdentityPublicKeyFile* variables -- these specify -the path to the private/public key files for accessing the gitolite admin repository. +The *gitRedmineSubdir* is an optional subdirectory under the *gitRepositoryBasePath* which can be used for all plugin-managed repositories. Its default value +is the empty string (no special subdirectory). If you choose to set it, make sure that the resulting path ends in a file separator, e.g. '/'. +The *gitRepositoryHierarchy* variable is a boolean value which denotes whether or not the plugin-managed repositories are placed into a hierarchy that +mirrors the project hierarchy. Its value is either 'true' (default) or 'false'. These variables can be modified at a later time in the Administration => Plugins => Redmine Git Hosting Plugin configuration page. However to ensure that the database migration from your existing repositories goes smoothly it is best to modify these variables now. +As an example of the significance of the previous three variables, suppose that project-3 is a child of project-2 which is a child of project-1. +Assume *gitRepositoryBasePath* == "repository/" and *gitRedmineSubdir* == "projects". When *gitRepositoryHierachy* is 'true', project-3.git will be stored in +repository/projects/project-1/project-2/project-3.git, which will further be reflected in the ssh access URL of repository/projects/project-1/project-2/project-3.git. +In contrast, when *gitRepositoryHierarchy* is 'false', project-3.git will be stored directly under repository/projects -- regardless of the number and identity of +any parents that it might have. Note that the top of the settings page (Administration => Plugins => Redmine Git Hosting Plugin configuration page) provides +information about how your chosen configuration affects the storage locations and URLs for accessing projects. **(7)** Run the rake db:migrate\_plugins task to update the database. You will need to do this once for every rails environment you have configured (e.g. production, development, testing). For the production environment run: RAILS_ENV=production rake db:migrate_plugins +At this point, if you wish to utilize selinux to protect your installation, you should follow the instructions given in the "Selinux" section, below. + **(8)** Unless you want to access your repositories exclusively via Smart HTTP users will need to set a public key to connect via SSH. To do this, open a browser, login to ChiliProject/Redmine and follow the "My Account" Link in the upper right-hand corner of the page. The right-hand column contains controls for adding your public key(s). @@ -140,18 +156,57 @@ do not re-use the key you set as the gitolite admin key. **(9)** The plugin is now configured, but you may now want to set some additional settings on the Administration => Plugins => Redmine Git Hosting Plugin page. +The *gitLockWaitTime* represents the amount of time that the plugin will wait in attempting to acquire its internal synchronization lock before giving up. +You probably will not need to change this value. + *Automatically Initialize Git Repositories For New Projects* can be enabled to automatically create a new git repository every time you create a new project. You won't have to create the project, and then create the repository -- this will be done all it one step. However, if you have both git and svn (or hg, or cvs etc.) repositories, this may cause problems so it is disabled by default. *Delete Git Repository When Project Is Deleted* can be enabled to let this plugin control repository deletion as well as repository creation. By default, this feature is disabled and when a repository is deleted in ChiliProject / Redmine, it is not deleted in gitolite. This is a safety feature to prevent -the accidental loss of data. If this feature is enabled, the safety is turned off and the repository files will be permanently deleted when the Project/Repository is deleted in ChiliProject/Redmine. +the accidental loss of data. If this feature is enabled, the safety is turned off and the repository files will be deleted when the Project/Repository is +deleted in ChiliProject/Redmine. Note, however, that even when this feature is enabled, deleted repositories are placed into a "recycle_bin" for a configurable +amount of time (defaulting to 24 hours) and can be recovered by recreating the project in Redmine with the same Identifier. Details are placed in the log. + +The *Git Recycle Bin Base Path* is the path *relative to the git user root* where deleted repositories are placed. This path should end in a path separator, +e.g. '/'. Deleted repositories are kept here for up to *gitRecycleExpireTime* hours (configurable, defaults to 24.0 hours). *Show Checkout URLs* can be disabled to hide the git URL bar in the repository tab. It is enabled by default. See below in the "Caching" section of this readme for more information on caching and how the caching variables should be configured. +## Resychronization of gitolite configuration + +Whenever a Redmine `fetch_changesets()` operation is executed (i.e. `http://REDMINE_ROOT/sys/fetch_changesets?key=xxx`), this plugin will check the +gitolite keydir and configuration file for consistency. It will correct any errors that it finds. Further, regular execution of a fetch_changesets operation +will make sure that repositories placed in the recycle_bin (during delete operations) will be expired and removed. Since there still seem to be some +phantom synchronization problems, it is recommended that you execute `fetch_changesets()` regularly (every 15 to 30 minutes). + +Two rake tasks can additionally be used for resynchronization (although these are redundant with executing `fetch_changesets()` through other means). + +**(1)** To fixup the gitolite configuration file, fix errors, and delete expired files in the recycle_bin, execute: + + RAILS_ENV=production rake gitolite:update_repositories + +**(2)** To perform all the above operations while at the same time fetching changesets for all repositories, execute: + + RAILS_ENV=production rake gitolite:fetch_changes + +**Note that it is very important that these commands be run as *www-user* (or whatever your web server user happens to be), lest you get permission problems later.** +(The same is true of any `fetch_changesets()` operation initiated without using the web server, i.e. through the command line or from the cron daemon). + +## Interaction with non-Redmine gitolite users + +This plugin respects gitolite repositories that are managed outside of Redmine or managed by both Redmine and non-Redmine users: + +* When performing a *fetch_changesets()* operation, this plugin will delete and reestablish all keys that are of the form "redmine_", + since it considers these to be under its exclusive control. A special token, called "redmine_dummy_key", is used as a placeholder when no access + is granted for a given repository. +* Keys other than "redmine_" are left untouched and can be in projects by themselves or mixed in with projects managed by redmine. +* When a Redmine-managed project is deleted (with the *Delete Git Repository When Project Is Deleted* option enabled), its corresponding git repository + *will not be deleted/recycled* if there are non-Redmine keys in the gitolite.conf file. + ## A Note About PATH variables One major source of issues with this plugin is that Rails needs to be able to run both *sudo* and *git*. Specifically, these programs need to be in one of the directories specified by @@ -246,11 +301,57 @@ This library allows you to quickly deploy ChiliProject, with this plugin to an u chili\_test.sh script, modifying the variables in those scripts as desired. This library is still under development, so these instructions may need to be updated in the near future. +## Selinux Configuration for Redmine + +This plugin can be configured to run with selinux. We have included a rakefile in tasks/selinux.rake to assist +with installing with selinux. You should start by editing init.rb and migrating as described above. Then, you +can execute one of the selinux rake tasks (from the Redmine root). For instance, the simplest option installs +a selinux configuration for both Redmine and the redmine_git_hosting plugin: + + rake selinux:install RAILS_ENV=production + +This will generate the redmine_git_hosting binaries in ./bin, install a selinux policy for these binaries (called +redmine_git.pp), then install a complete context for Redmine as follows: + +**(1)** Most of Redmine will be marked with "public_content_rw_t". + +**(2)** The dispatch files in Rails.root/public/dispatch.* will be marked with "httpd_sys_script_exec_t" + +**(3)** The redmine_git_hosting binaries in Rails.root/vendor/plugins/redmine_git_hosting/bin will be labeled +with "httpd_redmine_git_script_exec_t", which has been crafted to allow the sudo behavior required by these +binaries. + +Note that this rake file has additional options. For instance, you can specify Redmine roots with regular +expressions (not globbed expessions!) as follows (notice the use of double quotes): + + rake selinux:install RAILS_ENV=production ROOT_PATTERN="/source/.*/redmine" + +These additional options are documented in the selinux.rake file. + +Once this plugin is placed under selinux control, three of the redmine_git_hosting settings can +no longer be modified from the settings page. They are: 'gitUser', 'gitoliteIdentityFile', and +'gitoliteIdentityPublicKeyFile'. The plugin settings page will make this clear. The simplest way to +modify these options is to temporarily place your system into permissive mode, refresh the setting page, +change options, then place your system back into enforcing mode. Alternatively, you can alter the init.rb +file and reinstall the plugin. Under normal operation, you will get one selinux complaint about /bin/touch +in your log each time that you visit the plugin settings page. + +One final comment: The selinux policy exists in binary form as selinux/redmine_git.pp. Should this policy +need to be rebuilt, an additional rake task exists which will build the policy from selinux/redmine_git.te: + + rake selinux:redmine_git_hosting:build_policy + +This task can be followed by the selinux:install task. + +The rakefile and selinux configuration has been primarily tested on Redhat Enterprise Linux version 6.x +with apache and fcgi. Other configurations may require slight tweaking. + ## Tested Configurations This plugin has been primarily tested on Ubuntu Server 10.10 and 11.04 (32 and 64 bit) with ChiliProject v1.x, ChiliProject 2.0.0 and Redmine 1.2.1 with PostgreSQL as the database (July, 2011). It is possible that some -debugging will be necessary for other configurations. +debugging will be necessary for other configurations. Selinux configurations were tested under Redhat Enterprise Linux +version 6.x with apache and fcgi. ## Required gems @@ -270,7 +371,8 @@ This plugin is based largely on the Gitosis plugin by Jan Schulz-Hofen for http: were provided by github users untoldwind, tingar and ericpaulbishop. These updates were merged together and expanded upon by Eric Bishop to create this more comprehensive Git Hosting plugin. +Copyright (c) 2011 John Kubiatowicz (kubitron@cs.berkeley.edu) MIT License. + Copyright (c) 2010-2011 Eric Bishop (ericpaulbishop@gmail.com) MIT License. Copyright (c) 2009-2010 Jan Schulz-Hofen, ROCKET RENTALS GmbH (http://www.rocket-rentals.de). MIT License. - diff --git a/app/controllers/git_http_controller.rb b/app/controllers/git_http_controller.rb index b5faaba1a..cd42f97a6 100644 --- a/app/controllers/git_http_controller.rb +++ b/app/controllers/git_http_controller.rb @@ -10,71 +10,83 @@ class GitHttpController < ApplicationController before_filter :authenticate - def index - p1 = params[:p1] - p2 = params[:p2] - p3 = params[:p3] - proj_id = params[:id] - - - @git_http_repo_path = (params[:path]).gsub(/\.git$/, "") - - reqfile = p2 == "" ? p1 : ( p3 == "" ? p1 + "/" + p2 : p1 + "/" + p2 + "/" + p3); - - if p1 == "git-upload-pack" - service_rpc("upload-pack") - elsif p1 == "git-receive-pack" - service_rpc("receive-pack") - elsif p1 == "info" && p2 == "refs" - get_info_refs(reqfile) - elsif p1 == "HEAD" - get_text_file(reqfile) - elsif p1 == "objects" && p2 == "info" - if p3 != packs - get_text_file(reqfile) - else - get_info_packs(reqfile) - end - elsif p1 == "objects" && p2 != "pack" - get_loose_object(reqfile) - elsif p1 == "objects" && p2 == "pack" && p3.match(/\.pack$/) - get_pack_file(reqfile) - elsif p1 == "objects" && p2 == "pack" && p3.match(/\.idx$/) - get_idx_file(reqfile) - else - render_not_found - end - + if proj_path_split = (params[:project_path].match(/^(.*?([^\/]+))\.git$/)) + proj_id = proj_path_split[2] + project = Project.find_by_identifier(proj_id) + @git_http_repo_path = project_path = proj_path_split[1] + + if GitHosting.http_access_url(project) == project_path + p1 = params[:path][0] || "" + p2 = params[:path][1] || "" + p3 = params[:path][2] || "" + + # Full requested path from .git repo + reqfile = params[:path].join('/') + + # git http protocol + if p1 == "git-upload-pack" + service_rpc("upload-pack") + elsif p1 == "git-receive-pack" + service_rpc("receive-pack") + elsif p1 == "info" && p2 == "refs" + get_info_refs(reqfile) + elsif p1 == "HEAD" + get_text_file(reqfile) + elsif p1 == "objects" && p2 == "info" + if p3 != "packs" + get_text_file(reqfile) + else + get_info_packs(reqfile) + end + elsif p1 == "objects" && p2 != "pack" + get_loose_object(reqfile) + elsif p1 == "objects" && p2 == "pack" && p3.match(/\.pack$/) + get_pack_file(reqfile) + elsif p1 == "objects" && p2 == "pack" && p3.match(/\.idx$/) + get_idx_file(reqfile) + else + render_not_found + end + else + # repository URL doesn't match + render_not_found + end + return + end + # Wrong prefix or :base doesn't end in .git or doesn't at least have one char in base name + render_not_found end private def authenticate - is_push = params[:p1] == "git-receive-pack" + is_push = (params[:path][0] == "git-receive-pack") query_valid = false authentication_valid = true - project = Project.find(params[:id]) - repository = project != nil ? project.repository : nil - if(project != nil && repository !=nil) - if repository.extra[:git_http] == 2 || (repository.extra[:git_http] == 1 && is_ssl?) - query_valid = true - allow_anonymous_read = project.is_public - if is_push || (!allow_anonymous_read) - authentication_valid = false - authenticate_or_request_with_http_basic do |login, password| - user = User.find_by_login(login); - if user.is_a?(User) - if user.allowed_to?( :commit_access, project ) || ((!is_push) && user.allowed_to?( :view_changesets, project )) - authentication_valid = user.check_password?(password) + if proj_path_split = (params[:project_path].match(/^(.*?([^\/]+))\.git$/)) + project = Project.find_by_identifier(proj_path_split[2]) + repository = project != nil ? project.repository : nil + if(project != nil && repository !=nil) + if repository.extra[:git_http] == 2 || (repository.extra[:git_http] == 1 && is_ssl?) + query_valid = true + allow_anonymous_read = project.is_public + if is_push || (!allow_anonymous_read) + authentication_valid = false + authenticate_or_request_with_http_basic do |login, password| + user = User.find_by_login(login); + if user.is_a?(User) + if user.allowed_to?( :commit_access, project ) || ((!is_push) && user.allowed_to?( :view_changesets, project )) + authentication_valid = user.check_password?(password) + end end + authentication_valid end - authentication_valid end end end - end + end #if authentication failed, error already rendered #so, just render case where user queried a project diff --git a/app/controllers/gitolite_public_keys_controller.rb b/app/controllers/gitolite_public_keys_controller.rb index e11721d2b..131d2af40 100644 --- a/app/controllers/gitolite_public_keys_controller.rb +++ b/app/controllers/gitolite_public_keys_controller.rb @@ -10,21 +10,26 @@ def edit end def delete + GitHostingObserver.set_update_active(false) @gitolite_public_key[:active] = 0 @gitolite_public_key.save redirect_to url_for(:controller => 'my', :action => 'account') + GitHostingObserver.set_update_active(true) end def update + GitHostingObserver.set_update_active(false) if @gitolite_public_key.update_attributes(params[:public_key]) flash[:notice] = l(:notice_public_key_updated) redirect_to url_for(:controller => 'my', :action => 'account') else render :action => 'edit' end + GitHostingObserver.set_update_active(true) end def create + GitHostingObserver.set_update_active(false) @gitolite_public_key = GitolitePublicKey.new(params[:public_key].merge(:user => @user)) if @gitolite_public_key.save flash[:notice] = l(:notice_public_key_added) @@ -32,6 +37,7 @@ def create @gitolite_public_key = GitolitePublicKey.new(:user => @user) end redirect_to url_for(:controller => 'my', :action => 'account') + GitHostingObserver.set_update_active(true) end protected diff --git a/app/controllers/repository_mirrors_controller.rb b/app/controllers/repository_mirrors_controller.rb index 7878a5229..22d7e70ad 100644 --- a/app/controllers/repository_mirrors_controller.rb +++ b/app/controllers/repository_mirrors_controller.rb @@ -96,7 +96,7 @@ def settings def push respond_to do |format| format.html { - @mirror.push + (@push_failed,@shellout) = @mirror.push } end end diff --git a/app/models/git_hosting_observer.rb b/app/models/git_hosting_observer.rb index 8b8de5462..d254e8e1f 100644 --- a/app/models/git_hosting_observer.rb +++ b/app/models/git_hosting_observer.rb @@ -2,6 +2,8 @@ class GitHostingObserver < ActiveRecord::Observer observe :project, :user, :gitolite_public_key, :member, :role, :repository @@updating_active = true + @@updating_active_stack = 0 + @@updating_active_flags = {} @@cached_project_updates = [] def reload_this_observer @@ -11,28 +13,40 @@ def reload_this_observer end - def self.set_update_active(is_active) - @@updating_active = is_active - if is_active - if @@cached_project_updates.length > 0 + def self.set_update_active(*is_active) + if !is_active || !is_active.first + @@updating_active_stack += 1 + else + is_active.each do |item| + case item + when Symbol then @@updating_active_flags[item] = true + when Hash then @@updating_active_flags.merge!(item) + when Project then @@cached_project_updates |= [item] + end + end + + # If about to transition to zero and have something to run, do it + if @@updating_active_stack == 1 && (@@cached_project_updates.length > 0 || !@@updating_active_flags.empty?) @@cached_project_updates = @@cached_project_updates.flatten.uniq.compact - GitHosting::update_repositories(@@cached_project_updates, false) - end - end - @@cached_project_updates = [] - end - - - def before_destroy(object) - if object.is_a?(Repository::Git) - if Setting.plugin_redmine_git_hosting['deleteGitRepositories'] == "true" - GitHosting::update_repositories(object.project, true) - %x[#{GitHosting::git_user_runner} 'rm -rf #{object.url}' ] - end - GitHosting::clear_cache_for_project(object.project) + GitHosting::update_repositories(@@cached_project_updates, @@updating_active_flags) + @@cached_project_updates = [] + @@updating_active_flags = {} + end + + # Wait until after running update_repositories before releasing + @@updating_active_stack -= 1 + if @@updating_active_stack < 0 + @@updating_active_stack = 0 + end end + @@updating_active = (@@updating_active_stack == 0) end - + + # Register args for updating and then do it without allowing recursive calls + def self.bracketed_update_repositories(*args) + set_update_active(false) + set_update_active(*args) + end def after_create(object) if not object.is_a?(Project) @@ -51,20 +65,21 @@ def before_save(object) def after_save(object) update_repositories(object) end - + def after_destroy(object) - if !object.is_a?(Repository::Git) - update_repositories(object) + if object.is_a?(Repository::Git) + update_repositories(object,:delete=>true) + GitHosting::clear_cache_for_project(object.project) + else + update_repositories(object) end end protected - - def update_repositories(object) - + def update_repositories(object,*flags) projects = [] case object when Repository::Git then projects.push(object.project) @@ -73,11 +88,12 @@ def update_repositories(object) when Member then projects.push(object.project) when Role then projects = object.members.map(&:project).flatten.uniq.compact end - if(projects.length > 0) + if (projects.length > 0) if (@@updating_active) - GitHosting::update_repositories(projects, false) + GitHosting::update_repositories(projects,*flags) else @@cached_project_updates.concat(projects) + @@updating_active_flags.merge!(*flags) unless flags.empty? end end end diff --git a/app/models/git_hosting_settings_observer.rb b/app/models/git_hosting_settings_observer.rb index fee8d8a3f..0d586cf9d 100644 --- a/app/models/git_hosting_settings_observer.rb +++ b/app/models/git_hosting_settings_observer.rb @@ -1,12 +1,7 @@ class GitHostingSettingsObserver < ActiveRecord::Observer observe :setting - @@old_hook_debug = Setting.plugin_redmine_git_hosting['gitHooksDebug'] - @@old_hook_asynch = Setting.plugin_redmine_git_hosting['gitHooksAreAsynchronous'] - @@old_http_server = Setting.plugin_redmine_git_hosting['httpServer'] - @@old_git_user = Setting.plugin_redmine_git_hosting['gitUser'] - @@old_repo_base = Setting.plugin_redmine_git_hosting['gitRepositoryBasePath'] - + @@old_valuehash = (Setting.plugin_redmine_git_hosting).clone def reload_this_observer observed_classes.each do |klass| @@ -14,44 +9,146 @@ def reload_this_observer end end + # There is a long-running bug in ActiveRecord::Observer that prevents us from + # returning from before_save() with false to signal verification failure. + # + # Thus, we can only silently refuse to perform bad changes and/or perform + # slight corrections to badly formatted values. + def before_save(object) + # Only validate settings for our plugin + if object.name == "plugin_redmine_git_hosting" + valuehash = object.value + if !GitHosting.bin_dir_writeable? + # If bin directory not alterable, don't allow changes to + # Git Username, or Gitolite public or private keys + valuehash['gitUser'] = @@old_valuehash['gitUser'] + valuehash['gitoliteIdentityFile'] = @@old_valuehash['gitoliteIdentityFile'] + valuehash['gitoliteIdentityPublicKeyFile'] = @@old_valuehash['gitoliteIdentityPublicKeyFile'] + end + + # Server should not include any path components. Also, ports should be numeric. + if valuehash['gitServer'] + normalizedServer = valuehash['gitServer'].lstrip.rstrip.split('/').first + if (!normalizedServer.match(/^[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*(:\d+)?$/)) + valuehash['gitServer'] = @@old_valuehash['gitServer'] + else + valuehash['gitServer'] = normalizedServer + end + end + + # Server should not include any path components. Also, ports should be numeric. + if valuehash['httpServer'] + normalizedServer = valuehash['httpServer'].lstrip.rstrip.split('/').first + if (!normalizedServer.match(/^[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*(:\d+)?$/)) + valuehash['httpServer'] = @@old_valuehash['httpServer'] + else + valuehash['httpServer'] = normalizedServer + end + end + + # Normalize http repository subdirectory path, should be either empty or relative and end in '/' + if valuehash['httpServerSubdir'] + normalizedFile = File.expand_path(valuehash['httpServerSubdir'].lstrip.rstrip,"/") + if (normalizedFile != "/") + valuehash['httpServerSubdir'] = normalizedFile[1..-1] + "/" # Clobber leading '/' add trailing '/' + else + valuehash['httpServerSubdir'] = '' + end + end + + # Normalize Repository path, should be relative and end in '/' + if valuehash['gitRepositoryBasePath'] + normalizedFile = File.expand_path(valuehash['gitRepositoryBasePath'].lstrip.rstrip,"/") + if (normalizedFile != "/") + valuehash['gitRepositoryBasePath'] = normalizedFile[1..-1] + "/" # Clobber leading '/' add trailing '/' + else + valuehash['gitRepositoryBasePath'] = @@old_valuehash['gitRepositoryBasePath'] + end + end + + # Normalize Redmine Subdirectory path, should be either empty or relative and end in '/' + if valuehash['gitRedmineSubdir'] + normalizedFile = File.expand_path(valuehash['gitRedmineSubdir'].lstrip.rstrip,"/") + if (normalizedFile != "/") + valuehash['gitRedmineSubdir'] = normalizedFile[1..-1] + "/" # Clobber leading '/' add trailing '/' + else + valuehash['gitRedmineSubdir'] = '' + end + end + # Normalize Recycle bin path, should be relative and end in '/' + if valuehash['gitRecycleBasePath'] + normalizedFile = File.expand_path(valuehash['gitRecycleBasePath'].lstrip.rstrip,"/") + if (normalizedFile != "/") + valuehash['gitRecycleBasePath'] = normalizedFile[1..-1] + "/" # Clobber leading '/' add trailing '/' + else + valuehash['gitRecycleBasePath'] = @@old_valuehash['gitRecycleBasePath'] + end + end + + # Exclude bad expire times (and exclude non-numbers) + if valuehash['gitRecycleExpireTime'] + if valuehash['gitRecycleExpireTime'].to_f > 0 + valuehash['gitRecycleExpireTime'] = "#{(valuehash['gitRecycleExpireTime'].to_f * 10).to_i / 10.0}" + else + valuehash['gitRecycleExpireTime'] = @@old_valuehash['gitRecycleExpireTime'] + end + end + # Validate wait time > 0 (and exclude non-numbers) + if valuehash['gitLockWaitTime'] + if valuehash['gitLockWaitTime'].to_i > 0 + valuehash['gitLockWaitTime'] = "#{valuehash['gitLockWaitTime'].to_i}" + else + valuehash['gitLockWaitTime'] = @@old_valuehash['gitLockWaitTime'] + end + end + # Save back results + object.value = valuehash + end + end + def after_save(object) + # Only perform after-actions on settings for our plugin if object.name == "plugin_redmine_git_hosting" + valuehash = object.value + + # Settings cache doesn't seem to invalidate symbolic versions of Settings immediately, + # so, any use of Setting.plugin_redmine_git_hosting[] by things called during this + # callback will be outdated.... True for at least some versions of redmine plugin... + # + # John Kubiatowicz 12/21/2011 + if Setting.respond_to?(:check_cache) + # Clear out all cached settings. + Setting.check_cache + end - %x[ rm -rf '#{ GitHosting.get_tmp_dir }' ] - - if @@old_repo_base != object.value['gitRepositoryBasePath'] - GitHostingObserver.set_update_active(false) - all_projects = Project.find(:all) - all_projects.each do |p| - if p.repository.is_a?(Repository::Git) - r = p.repository - repo_name= p.parent ? File.join(GitHosting::get_full_parent_path(p, true),p.identifier) : p.identifier - r.url = File.join(object.value['gitRepositoryBasePath'], "#{repo_name}.git") - r.root_url = r.url - r.save - end - end - GitHostingObserver.set_update_active(true) + if GitHosting.bin_dir_writeable? + %x[ rm -rf '#{ GitHosting.get_tmp_dir }' ] + %x[ rm -rf '#{ GitHosting.get_bin_dir }' ] + end + + if @@old_valuehash['gitRepositoryBasePath'] != valuehash['gitRepositoryBasePath'] || + @@old_valuehash['gitRedmineSubdir'] != valuehash['gitRedmineSubdir'] || + @@old_valuehash['gitRepositoryHierarchy'] != valuehash['gitRepositoryHierarchy'] || + @@old_valuehash['gitUser'] != valuehash['gitUser'] + # Need to update everyone! + GitHostingObserver.bracketed_update_repositories(:resync_all) end - if @@old_git_user != object.value['gitUser'] + if @@old_valuehash['gitUser'] != valuehash['gitUser'] GitHosting.setup_hooks - GitHosting.update_repositories( Project.find(:all), false) - elsif @@old_http_server != object.value['httpServer'] || @@old_hook_debug != object.value['gitHooksDebug'] || @@old_repo_base != object.value['gitRepositoryBasePath'] || @@old_hook_asynch != object.value['gitHooksAreAsynchronous'] + elsif @@old_valuehash['httpServer'] != valuehash['httpServer'] || + @@old_valuehash['gitHooksDebug'] != valuehash['gitHooksDebug'] || + @@old_valuehash['gitRedmineSubdir'] != valuehash['gitRedmineSubdir'] || + @@old_valuehash['gitRepositoryHierarchy'] != valuehash['gitRepositoryHierarchy'] || + @@old_valuehash['gitHooksAreAsynchronous'] != valuehash['gitHooksAreAsynchronous'] GitHosting.update_global_hook_params end - @@old_hook_debug = object.value['gitHooksDebug'] - @@old_hook_asynch = object.value['gitHooksAreAsynchronous'] - @@old_http_server = object.value['httpServer'] - @@old_git_user = object.value['gitUser'] - @@old_repo_base = object.value['gitRepositoryBasePath'] - + @@old_valuehash = valuehash.clone end end - end diff --git a/app/models/gitolite_public_key.rb b/app/models/gitolite_public_key.rb index 1c36e36e1..809e4c5f2 100755 --- a/app/models/gitolite_public_key.rb +++ b/app/models/gitolite_public_key.rb @@ -32,4 +32,14 @@ def set_identifier end def to_s ; title ; end + + @@myregular = /^redmine_(.*)_\d*_\d*(.pub)?$/ + def self.ident_to_user_token(identifier) + result = @@myregular.match(identifier) + (result!=nil) ? result[1] : nil + end + + def self.user_to_user_token(user) + user.login.underscore.gsub(/[^0-9a-zA-Z\-]/,'_') + end end diff --git a/app/models/repository_mirror.rb b/app/models/repository_mirror.rb index 59baffd41..7a80fc7e6 100644 --- a/app/models/repository_mirror.rb +++ b/app/models/repository_mirror.rb @@ -18,19 +18,12 @@ def to_s def push repo_path = GitHosting.repository_path(project) - shellout = %x[ echo 'cd "#{repo_path}" ; env GIT_SSH=~/.ssh/run_gitolite_admin_ssh git push --mirror "#{url}" 2>&1' | #{GitHosting.git_user_runner} "bash" ] + shellout = %x[ echo 'cd "#{repo_path}" ; env GIT_SSH=~/.ssh/run_gitolite_admin_ssh git push --mirror "#{url}" 2>&1' | #{GitHosting.git_user_runner} "bash" ].chomp push_failed = ($?.to_i!=0) ? true : false - - err_output = push_failed ? "" : "" if push_failed - ms = " #{mirror.url} push error " - nr = (70-ms.length)/2 - GitHosting.logger.debug "Failed:\n%{nrs} #{ms} %{nrs}\n#{shellout}%{nre} #{ms} %{nre}\n" % {:nrs => ">"*nr, :nre => "<"*nr} - err_output = err_output + ("%{nrs} #{ms} %{nrs}\n" % {:nrs => ">"*nr}) - err_output = err_output + "#{shellout}" - err_output = err_output + ("%{nre} #{ms} %{nre}\n" % {:nre => "<"*nr}) - end - err_output + GitHosting.logger.error "Mirror push error: #{url}\n#{shellout}" + end + [push_failed,shellout] end end diff --git a/app/views/projects/_git_urls.erb b/app/views/projects/_git_urls.erb index be2121e58..fa9c4388c 100644 --- a/app/views/projects/_git_urls.erb +++ b/app/views/projects/_git_urls.erb @@ -10,17 +10,22 @@