├── .gitattributes
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── controllers
│ └── git_mirror_controller.rb
├── models
│ └── repository
│ │ └── git_mirror.rb
└── views
│ └── git_mirror
│ └── _settings.html.erb
├── config
├── locales
│ ├── en.yml
│ └── ru.yml
└── routes.rb
├── init.rb
├── lib
└── redmine_git_mirror
│ ├── git.rb
│ ├── patches
│ └── repositories_helper_patch.rb
│ ├── settings.rb
│ ├── ssh.rb
│ └── url.rb
└── repos
└── .gitignore
/.gitattributes:
--------------------------------------------------------------------------------
1 | .editorconfig export-ignore
2 | .travis.yml export-ignore
3 | docs export-ignore
4 | test export-ignore
5 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: push
3 |
4 | jobs:
5 | test:
6 | runs-on: ubuntu-latest
7 | strategy:
8 | matrix:
9 | include:
10 | - redmine-version: '4.2'
11 | ruby-version: '2.7'
12 |
13 | # - redmine-version: '5.0'
14 | # ruby-version: '3.0'
15 |
16 | env:
17 | RAILS_ENV: test
18 | REDMINE_DIR: ${{ github.workspace }}/redmine
19 |
20 | steps:
21 | - uses: browser-actions/setup-chrome@v1
22 | with: { chrome-version: 'stable' }
23 |
24 | - uses: actions/checkout@v3
25 |
26 | - name: Set up Ruby ${{ matrix.ruby-version }}
27 | uses: ruby/setup-ruby@v1
28 | with:
29 | ruby-version: ${{ matrix.ruby-version }}
30 |
31 | - name: Checkout redmine ${{ matrix.redmine-version }}
32 | uses: actions/checkout@v3
33 | with:
34 | path: redmine
35 | repository: redmine/redmine
36 | ref: ${{ matrix.redmine-version }}-stable
37 |
38 | - name: Install deps
39 | run: |
40 | cd $REDMINE_DIR
41 | mkdir -p plugins
42 | ln -s ${{ github.workspace }} plugins/redmine_git_mirror
43 | ./../test/docker/redmine/scripts/configure.sh
44 |
45 | - name: Migrate DB
46 | run: test/docker/redmine/scripts/migrate-db.sh
47 |
48 | - name: Run tests
49 | run: CHROMIUM_BIN=$(which chrome) test/docker/redmine/run-tests.sh
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Sergey Linnik
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Redmine Git Mirror plugin [](https://github.com/linniksa/redmine_git_mirror/actions/workflows/main.yml)
2 | ==================
3 |
4 | Adds ability to clone and fetch remote git repositories to redmine.
5 |
6 | ## Key Features
7 | * Easy install (just clone to redmine plugins folder)
8 | * Webhooks integration (gitlab and custom)
9 | * Works well with enabled autofetch changesets setting and in mix with other scm types
10 | * Automatic deletes unreachable commits
11 |
12 | # Install
13 |
14 | cd [redmine-root]/plugins
15 | git clone https://github.com/linniksa/redmine_git_mirror
16 |
17 | Restart redmine, and enable `Git Mirror` scm type at `redmine.site/settings?tab=repositories`
18 |
19 | ## Accessing private repositories
20 |
21 | At this moment only ssh access with redmine user ssh key is supported.
22 |
23 | # Fetching changes
24 |
25 | This plugin supports 2 ways of fetching changes, via cronjob or via hooks.
26 | You can use only one or both of them together.
27 |
28 | ## Cronjob
29 |
30 | Run ```./bin/rails runner "Repository::GitMirror.fetch"```, for example:
31 |
32 | 5,20,35,50 * * * * cd /usr/src/redmine && ./bin/rails runner "Repository::GitMirror.fetch" -e production >> log/cron_rake.log 2>&1
33 |
34 | ## Hooks
35 |
36 | Hooks is preferred way because you can immediately see changes of you repository.
37 |
38 | ### GitLab hooks
39 |
40 | You can setup per-project or system wide hook, for both variants use `redmine.site/sys/git_mirror/gitlab` as `URL`
41 |
42 | ###### For system wide setup
43 |
44 | Go to `gitlab.site/admin/hooks`, and select only `Repository update events` trigger.
45 |
46 | ###### For per-project setup
47 |
48 | Go to `gitlab.site/user/project/settings/integrations`, and select only `Push` and `Tags` events
49 |
50 | ### GitHub hooks
51 |
52 | You can setup per-project or group wide hook, for both variants
53 | use `redmine.site/sys/git_mirror/github` as `Payload URL` and `Just the push event` option.
54 |
55 | Don't worry about `Content type` both `application/json` and `application/x-www-form-urlencoded` are supported.
56 |
--------------------------------------------------------------------------------
/app/controllers/git_mirror_controller.rb:
--------------------------------------------------------------------------------
1 |
2 | class GitMirrorController < ActionController::Base
3 |
4 | # abstract hook for repo update via remote url
5 | def fetch
6 | url = params[:url]
7 | begin
8 | RedmineGitMirror::URL.parse(url)
9 | rescue
10 | head 400
11 | return
12 | end
13 | found = fetch_by_urls([url])
14 |
15 | head found ? 202 : 404
16 | end
17 |
18 | # process gitlab webhook request
19 | def gitlab
20 | event = params[:event_name]
21 | unless request.post? && event
22 | head 400
23 | return
24 | end
25 |
26 | unless %w[push repository_update].include?(event.to_s)
27 | head 200
28 | return
29 | end
30 |
31 | project = params[:project]
32 | unless project
33 | head 422
34 | return
35 | end
36 |
37 | urls = []
38 |
39 | [:git_ssh_url, :git_http_url].each do |p|
40 | url = project[p].to_s
41 |
42 | urls.push(url) if url.length > 0
43 | end
44 |
45 | if urls.length <= 0
46 | head 422
47 | return
48 | end
49 |
50 | found = fetch_by_urls(urls)
51 | head found ? 202 : 404
52 | end
53 |
54 | # process github webhook request
55 | def github
56 | event = request.headers["x-github-event"]
57 | unless request.post? && event
58 | head 400
59 | return
60 | end
61 |
62 | unless %w[push].include?(event.to_s)
63 | head 200
64 | return
65 | end
66 |
67 | payload = params[:payload]
68 |
69 | if payload && request.content_type != 'application/json'
70 | payload = JSON.parse(payload, :symbolize_names => true)
71 | else
72 | payload = params
73 | end
74 |
75 | unless payload
76 | head 422
77 | return
78 | end
79 |
80 | repository = payload[:repository]
81 | unless repository
82 | head 422
83 | return
84 | end
85 |
86 | urls = []
87 |
88 | [:ssh_url, :clone_url, :git_url].each do |p|
89 | url = repository[p].to_s
90 |
91 | urls.push(url) if url.length > 0
92 | end
93 |
94 | if urls.length <= 0
95 | head 422
96 | return
97 | end
98 |
99 | found = fetch_by_urls(urls)
100 | head found ? 202 : 404
101 | end
102 |
103 | private def fetch_by_urls(urls)
104 | urls_to_search = []
105 |
106 | urls.each do |url|
107 | begin
108 | urls_to_search.concat RedmineGitMirror::URL.parse(url).vary
109 | rescue Exception => _
110 | urls_to_search.push(url)
111 | end
112 | end
113 |
114 | found = false
115 | Repository::GitMirror.active.where(url: urls_to_search).find_each do |repository|
116 | found = true unless found
117 | repository.fetch()
118 | end
119 |
120 | found
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/app/models/repository/git_mirror.rb:
--------------------------------------------------------------------------------
1 |
2 | class Repository::GitMirror < Repository::Git
3 |
4 | before_validation :validate_and_normalize_url, on: [:create, :update]
5 | before_validation :set_defaults, on: :create
6 | after_validation :init_repo, on: :create
7 | after_commit :fetch, on: :create
8 |
9 | after_validation :update_remote_url, on: :update
10 |
11 | before_destroy :remove_repo
12 |
13 | scope :active, lambda {
14 | joins(:project).merge(Project.active)
15 | }
16 |
17 | safe_attributes 'url', :if => lambda { |repository, user|
18 | repository.new_record? || RedmineGitMirror::Settings.url_change_allowed?
19 | }
20 |
21 | private def update_remote_url
22 | return unless self.errors.empty?
23 | return unless self.url_changed?
24 |
25 | r, err = RedmineGitMirror::Git.get_remote_url(root_url)
26 | if err
27 | errors.add :url, err
28 | return
29 | end
30 |
31 | return if r == url
32 |
33 | err = RedmineGitMirror::Git.set_remote_url(root_url, url)
34 | errors.add :url, err if err
35 | end
36 |
37 | private def remove_repo
38 | root_url = self.root_url.to_s
39 |
40 | return if root_url.empty?
41 | return if root_url == '/'
42 | return if root_url.to_s.length <= 15
43 |
44 | # check git dirs and files
45 | return unless File.exist? root_url + '/config'
46 | return unless Dir.exist? root_url + '/objects'
47 |
48 | FileUtils.rm_rf root_url
49 | end
50 |
51 | private def validate_and_normalize_url
52 | return unless self.new_record? || self.url_changed?
53 |
54 | url = self.url.to_s.strip
55 |
56 | return if url.to_s.empty?
57 |
58 | begin
59 | parsed_url = RedmineGitMirror::URL.parse(url)
60 | rescue Exception => msg
61 | errors.add :url, msg.to_s
62 | return
63 | end
64 |
65 | unless parsed_url.remote?
66 | errors.add :url, 'should be remote url'
67 | return
68 | end
69 |
70 | unless parsed_url.scheme?(*RedmineGitMirror::Settings.allowed_schemes)
71 | s = RedmineGitMirror::Settings.allowed_schemes
72 | err = s.empty?? 'no allowed schemes' : "scheme not allowed, only #{s.join', '} is allowed"
73 | errors.add :url, err
74 | return
75 | end
76 |
77 | if parsed_url.has_credential?
78 | errors.add :url, 'cannot use credentials'
79 | return
80 | end
81 |
82 | self.url = parsed_url.normalize
83 |
84 | err = RedmineGitMirror::Git.check_remote_url(self.url)
85 | if err
86 | errors.add :url, err
87 | return
88 | end
89 |
90 | if RedmineGitMirror::Settings.prevent_multiple_clones?
91 | urls = RedmineGitMirror::URL.parse(url).vary(
92 | :all => RedmineGitMirror::Settings.search_clones_in_all_schemes?
93 | )
94 |
95 | if Repository::GitMirror.where(url: urls).where.not(id: self.id).exists?
96 | errors.add :url, 'is already mirrored in redmine'
97 | return
98 | end
99 | end
100 | end
101 |
102 | private def set_defaults
103 | return unless self.errors.empty? && !url.to_s.empty?
104 |
105 | parsed_url = RedmineGitMirror::URL.parse(url)
106 | if identifier.empty?
107 | identifier = File.basename(parsed_url.path, ".*")
108 | self.identifier = identifier if /^[a-z][a-z0-9_-]*$/.match(identifier)
109 | end
110 |
111 | self.root_url = RedmineGitMirror::Settings.path + '/' +
112 | Time.now.strftime("%Y%m%d%H%M%S%L") +
113 | "_" +
114 | (parsed_url.host + parsed_url.path.gsub(/\.git$/, '')).gsub(/[\\\/]+/, '_').gsub(/[^A-Za-z._-]/, '')[0..64]
115 | end
116 |
117 | private def init_repo
118 | return unless self.errors.empty?
119 |
120 | err = RedmineGitMirror::Git.init(root_url, url)
121 | errors.add :url, err if err
122 | end
123 |
124 | def fetch_changesets(fetch = false)
125 | return unless fetch
126 | super()
127 | end
128 |
129 | def fetch
130 | return if @fetched
131 | @fetched = true
132 |
133 | puts "Fetching repo #{url} to #{root_url}"
134 |
135 | err = RedmineGitMirror::Git.fetch(root_url, url)
136 | Rails.logger.warn 'Err with fetching: ' + err if err
137 |
138 | remove_unreachable_commits
139 | fetch_changesets(true)
140 | end
141 |
142 | private def remove_unreachable_commits
143 | commits, e = RedmineGitMirror::Git.unreachable_commits(root_url)
144 | if e
145 | Rails.logger.warn 'Err when fetching unreachable commits: ' + e
146 | return
147 | end
148 |
149 | return if commits.empty?
150 |
151 | # remove commits from heads extra info
152 | h = extra_info ? extra_info["heads"] : nil
153 | if h
154 | h1 = h.dup
155 | commits.each { |c| h1.delete(c) }
156 |
157 | if h1.length != h.length
158 | n = {}
159 | n["heads"] = h1
160 |
161 | merge_extra_info(n)
162 | save
163 | end
164 | end
165 |
166 | Changeset.where(repository: self, revision: commits).destroy_all
167 |
168 | RedmineGitMirror::Git.prune(root_url) if commits.length >= 10
169 | end
170 |
171 | class << self
172 | def scm_name
173 | 'Git Mirror'
174 | end
175 |
176 | def human_attribute_name(attribute_key_name, *args)
177 | attr_name = attribute_key_name.to_s
178 |
179 | Repository.human_attribute_name(attr_name, *args)
180 | end
181 |
182 | # Fetches new changes for all git mirror repositories in active projects
183 | # Can be called periodically by an external script
184 | # eg. bin/rails runner "Repository::GitMirror.fetch"
185 | def fetch
186 | Repository::GitMirror.active.find_each(&:fetch)
187 | end
188 | end
189 |
190 | end
191 |
--------------------------------------------------------------------------------
/app/views/git_mirror/_settings.html.erb:
--------------------------------------------------------------------------------
1 |
4 | <%= ''.html_safe %>
5 |
8 |
9 |
10 |
45 |
46 |
67 | <%= check_all_links 'git_mirror_schemas' %>
68 |
69 |
70 |
71 | <%= ''.html_safe %>
72 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | text_git_mirror_url_note: URL of remote repository.
3 |
--------------------------------------------------------------------------------
/config/locales/ru.yml:
--------------------------------------------------------------------------------
1 | ru:
2 | text_git_mirror_url_note: URL удаленного репозитория.
3 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 |
2 | match 'sys/git_mirror/fetch', to: 'git_mirror#fetch', via: [:get, :post]
3 | match 'sys/git_mirror/gitlab', to: 'git_mirror#gitlab', via: [:post]
4 | match 'sys/git_mirror/github', to: 'git_mirror#github', via: [:post]
5 |
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | require 'redmine'
2 | require_dependency 'redmine_git_mirror/git'
3 | require_dependency 'redmine_git_mirror/ssh'
4 | require_dependency 'redmine_git_mirror/url'
5 | require_dependency 'redmine_git_mirror/settings'
6 |
7 | Redmine::Scm::Base.add 'GitMirror'
8 |
9 | Redmine::Plugin.register :redmine_git_mirror do
10 | name 'Git Mirror'
11 | author 'Sergey Linnik'
12 | description 'Add ability to create readonly mirror of remote git repository'
13 | version '0.8.0'
14 | url 'https://github.com/linniksa/redmine_git_mirror'
15 | author_url 'https://github.com/linniksa'
16 |
17 | requires_redmine :version_or_higher => '3.3.0'
18 |
19 | settings :default => RedmineGitMirror::Settings::DEFAULT, :partial => 'git_mirror/settings'
20 |
21 | end
22 |
23 | redmine_git_mirror_patches = proc do
24 | require 'repositories_helper'
25 | require 'redmine_git_mirror/patches/repositories_helper_patch'
26 |
27 | def include(klass, patch)
28 | klass.send(:include, patch) unless klass.included_modules.include?(patch)
29 | end
30 |
31 | include(RepositoriesHelper, RedmineGitMirror::Patches::RepositoriesHelperPatch)
32 | end
33 |
34 | # Patches to the Redmine core.
35 | require 'dispatcher' unless Rails::VERSION::MAJOR >= 3
36 |
37 | if Rails::VERSION::MAJOR >= 5
38 | ActiveSupport::Reloader.to_prepare &redmine_git_mirror_patches
39 | elsif Rails::VERSION::MAJOR >= 3
40 | ActionDispatch::Callbacks.to_prepare &redmine_git_mirror_patches
41 | else
42 | Dispatcher.to_prepare &redmine_git_mirror_patches
43 | end
44 |
--------------------------------------------------------------------------------
/lib/redmine_git_mirror/git.rb:
--------------------------------------------------------------------------------
1 | require 'open3'
2 |
3 | module RedmineGitMirror
4 | class Git
5 | class << self
6 | GIT_BIN = Redmine::Configuration['scm_git_command'] || 'git'
7 |
8 | def check_remote_url(url)
9 | url = RedmineGitMirror::URL.parse(url)
10 | RedmineGitMirror::SSH.ensure_host_known(url.host) if url.uses_ssh?
11 |
12 | _, e = git 'ls-remote', '-h', url.to_s, 'master'
13 | e
14 | end
15 |
16 | def unreachable_commits(path)
17 | o, e = git "--git-dir", path, "fsck", "--unreachable", "--no-reflogs", "--no-progress"
18 | return nil, e if e
19 |
20 | prefix = 'unreachable commit '
21 |
22 | commits = o.lines.lazy
23 | .select { | line | line.start_with?(prefix) }
24 | .map { |line| line[prefix.length..-1].strip }
25 | .to_a
26 |
27 | return commits, nil
28 | end
29 |
30 | def prune(path)
31 | _, e = git "--git-dir", path, "prune"
32 | e
33 | end
34 |
35 | def init(clone_path, url)
36 | url = RedmineGitMirror::URL.parse(url)
37 | RedmineGitMirror::SSH.ensure_host_known(url.host) if url.uses_ssh?
38 |
39 | if Dir.exists? clone_path
40 | o, e = get_remote_url(clone_path)
41 | return e if e
42 |
43 | return "#{clone_path} remote url differs" unless o == url.to_s
44 | else
45 | _, e = git "init", "--bare", clone_path
46 | return e if e
47 |
48 | _, e = git "--git-dir", clone_path, "remote", "add", "origin", url.to_s
49 | return e if e
50 | end
51 |
52 | set_fetch_refs(clone_path, [
53 | '+refs/heads/*:refs/heads/*',
54 | '+refs/tags/*:refs/tags/*',
55 | # uncomment next line if you want to show (gitlab) merge requests as braches in redmine
56 | # '+refs/merge-requests/*/head:refs/heads/MR-*',
57 | ])
58 | end
59 |
60 | def fetch(clone_path, url)
61 | e = RedmineGitMirror::Git.init(clone_path, url)
62 | return e if e
63 |
64 | _, e = git "--git-dir", clone_path, "fetch", "--prune", "--all"
65 | e
66 | end
67 |
68 | def get_remote_url(clone_path)
69 | o, e = git "--git-dir", clone_path, "config", "--get", "remote.origin.url"
70 |
71 | return o.to_s.strip, e
72 | end
73 |
74 | def set_remote_url(clone_path, url)
75 | _, e = git "--git-dir", clone_path, "remote", "set-url", "origin", url
76 | e
77 | end
78 |
79 | private def set_fetch_refs(clone_path, configs)
80 | o, e = git "--git-dir", clone_path, "config", "--get-all", "remote.origin.fetch"
81 | return e if e
82 |
83 | # special ref that removes all refs outside specified
84 | expected = ["+__-=_=-__/*:refs/*"] + configs
85 |
86 | if o && o.lines
87 | actual = o.lines.map(&:strip)
88 | return if expected.eql?(actual)
89 | end
90 |
91 | # need change
92 | _, e = git "--git-dir", clone_path, "config", "--unset-all", "remote.origin.fetch"
93 | return e if e
94 |
95 | expected.each do |v|
96 | _, e = git "--git-dir", clone_path, "config", "--add", "remote.origin.fetch", v
97 | return e if e
98 | end
99 |
100 | nil
101 | end
102 |
103 | private def git(*cmd)
104 | s, e, status = Open3.capture3({
105 | 'GIT_SSH_COMMAND' => RedmineGitMirror::SSH.command.to_s
106 | }, GIT_BIN, *cmd)
107 | s.to_s.strip!
108 |
109 | return s, nil if status.success?
110 |
111 | e = guess_error_message(e).to_s.truncate(100) || "git exit with status #{status}"
112 |
113 | return s, e
114 | end
115 |
116 | private def guess_error_message(msg)
117 | msg = msg.to_s.strip
118 |
119 | return msg if msg.empty?
120 |
121 | msg.each_line do |line|
122 | line.strip!
123 | return line unless line.start_with?('Warning')
124 | end
125 |
126 | msg.lines.first.strip
127 | end
128 | end
129 | end
130 | end
131 |
--------------------------------------------------------------------------------
/lib/redmine_git_mirror/patches/repositories_helper_patch.rb:
--------------------------------------------------------------------------------
1 | module RedmineGitMirror
2 | module Patches
3 | unloadable
4 | module RepositoriesHelperPatch
5 |
6 | def git_mirror_field_tags(form, repository)
7 | content_tag('p', form.text_field(
8 | :url,
9 | :size => 60,
10 | :required => true,
11 | :disabled => !repository.safe_attribute?('url'),
12 | ) +
13 | content_tag('em', l(:text_git_mirror_url_note), :class => 'info')
14 | )
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/redmine_git_mirror/settings.rb:
--------------------------------------------------------------------------------
1 |
2 | require 'singleton'
3 |
4 | module RedmineGitMirror
5 | module Settings
6 | DEFAULT = {
7 | :schemes => %w[http https scp],
8 | :url_change_allowed => false,
9 | :prevent_multiple_clones => true,
10 | :search_clones_in_all_schemes => true,
11 | }.freeze
12 |
13 | class << self
14 | def path
15 | File.expand_path(File.dirname(__FILE__) + '/../../repos/')
16 | end
17 |
18 | def allowed_schemes
19 | self[:schemes] || []
20 | end
21 |
22 | def url_change_allowed?
23 | s = self[:url_change_allowed] || false
24 |
25 | s == true || s.to_s == '1'
26 | end
27 |
28 | def prevent_multiple_clones?
29 | s = self[:prevent_multiple_clones] || false
30 |
31 | s == true || s.to_s == '1'
32 | end
33 |
34 | def search_clones_in_all_schemes?
35 | s = self[:search_clones_in_all_schemes] || false
36 |
37 | s == true || s.to_s == '1'
38 | end
39 |
40 | private def [](key)
41 | key = key.intern if key.is_a?(String)
42 | settings = Setting[:plugin_redmine_git_mirror] || {}
43 |
44 | return settings[key] if settings.key?(key)
45 | return settings[key.to_s] if settings.key?(key.to_s)
46 |
47 | DEFAULT[key]
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/redmine_git_mirror/ssh.rb:
--------------------------------------------------------------------------------
1 |
2 | module RedmineGitMirror
3 | module SSH
4 | class << self
5 | def ensure_host_known(host)
6 | return unless host
7 |
8 | known_hosts = known_hosts_file
9 |
10 | o, status = Open3.capture2("ssh-keygen", "-F", host, "-f", known_hosts)
11 | if status.success?
12 | return if o.match /found/
13 | else
14 | # ssh-keygen fail if known_hosts file is not exists just log and continue
15 | puts "ssh-keygen exited with non-zero status: #{status}"
16 | end
17 |
18 | FileUtils.mkdir_p File.dirname(known_hosts), :mode => 0700
19 |
20 | o, status = Open3.capture2("ssh-keyscan", host)
21 | unless status.success?
22 | puts "ssh-keyscan exited with non-zero status: #{status}"
23 | return
24 | end
25 |
26 | puts "Adding #{host} to #{known_hosts}"
27 | File.open(known_hosts, 'a', 0600) do |file|
28 | file.puts o
29 | end
30 | end
31 |
32 | def command
33 | 'ssh -o UserKnownHostsFile=' + known_hosts_file
34 | end
35 |
36 | private def known_hosts_file
37 | Dir.home + "/.ssh/known_hosts"
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/redmine_git_mirror/url.rb:
--------------------------------------------------------------------------------
1 |
2 | require 'uri'
3 |
4 | module URI
5 | class GIT < Generic
6 | DEFAULT_PORT = 9418
7 | end
8 | @@schemes['GIT'] = GIT
9 | end
10 |
11 | module RedmineGitMirror
12 | class URL
13 | attr_reader :scheme, :user, :password, :host, :port, :path
14 |
15 | private def initialize(url)
16 | url = url.to_s
17 | raise 'Empty url' if url.empty?
18 |
19 | begin
20 | url = URI.parse(url)
21 |
22 | @scheme = url.scheme
23 | @user = url.user
24 | @password = url.password
25 | @host = url.host unless url.host.to_s.empty?
26 | @port = url.port
27 | @default_port = url.default_port
28 | @path = url.path
29 |
30 | return
31 | rescue
32 | end
33 |
34 | host, path = url.to_s.split(':', 2)
35 | if host.length == 1 && path[0] == '\\'
36 | #local windows path
37 | @path = url
38 | return
39 | end
40 |
41 | if host.length > 0 && path.length > 0
42 | return if parse_scp_like_url(host, path)
43 | end
44 |
45 | raise 'Unknown git remote url'
46 | end
47 |
48 | private def parse_scp_like_url(host, path)
49 | return if !path || path.include?(':') || path[0] == '/'
50 |
51 | if host.include? '@'
52 | user, host = host.split('@', 2)
53 |
54 | return if user.length <= 0
55 | return if host.include?('@')
56 |
57 | @user = user
58 | @host = host
59 | else
60 | @host = host
61 | end
62 |
63 | @path = '/' + path
64 | end
65 |
66 | def remote?
67 | !self.local?
68 | end
69 |
70 | def local?
71 | (@scheme.nil? && !scp_like?) || @scheme == "file"
72 | end
73 |
74 | def scp_like?
75 | @scheme.nil? && !@host.nil?
76 | end
77 |
78 | def uses_ssh?
79 | @scheme == 'ssh' || self.scp_like?
80 | end
81 |
82 | def scheme?(*schemes)
83 | schemes.include?(self.scp_like? ? 'scp' : self.scheme)
84 | end
85 |
86 | def has_credential?
87 | return false if uses_ssh? && password.nil?
88 |
89 | !password.nil? || !user.nil?
90 | end
91 |
92 | def normalize
93 | o = self.dup
94 |
95 | path = o.path.gsub(/\/{2,}/, '/')
96 | o.instance_variable_set(:@path, path)
97 |
98 | o.to_s
99 | end
100 |
101 | def vary(all: false)
102 | schemes = %w[http https ssh scp]
103 | http_schemes = %w[http https]
104 | ssh_schemes = %w[ssh scp]
105 |
106 | current_scheme = self.scp_like?? 'scp' : self.scheme
107 |
108 | unless all
109 | if http_schemes.include?(current_scheme)
110 | schemes = http_schemes
111 | elsif ssh_schemes.include?(current_scheme)
112 | schemes = ssh_schemes
113 | else
114 | schemes = [current_scheme]
115 | end
116 | end
117 |
118 | rez = []
119 | schemes.each do |scheme|
120 | s = to_scheme(scheme)
121 | rez.concat(s.vary_suffix('.git'))
122 | end
123 |
124 | rez
125 | end
126 |
127 | def vary_suffix(suffix)
128 | return self.dup unless path
129 |
130 | [
131 | to_suffix(suffix, present: false).to_s,
132 | to_suffix(suffix, present: true).to_s
133 | ]
134 | end
135 |
136 | private def to_scheme(scheme)
137 | n = self.dup
138 | if scheme == 'scp'
139 | return n if self.scp_like?
140 |
141 | n.instance_variable_set(:@user, 'git') unless n.user
142 | n.instance_variable_set(:@scheme, nil)
143 |
144 | return n
145 | elsif scheme == 'ssh'
146 | n.instance_variable_set(:@user, 'git') unless n.user
147 | end
148 |
149 | n.instance_variable_set(:@scheme, scheme)
150 | if self.scp_like?
151 | n.instance_variable_set(:@user, nil) unless %w[ssh scp].include? scheme
152 | end
153 |
154 | n
155 | end
156 |
157 | private def to_suffix(suffix, present: )
158 | n = self.dup
159 | return n unless path
160 |
161 | if present && !path.end_with?(suffix)
162 | n.instance_variable_set(:@path, path + suffix)
163 | elsif !present
164 | n.instance_variable_set(:@path, path.chomp(suffix))
165 | end
166 |
167 | n
168 | end
169 |
170 | def to_h
171 | rez = {}
172 | rez[:scheme] = @scheme if @scheme
173 | rez[:user] = @user if @user
174 | rez[:password] = @password if @password
175 | rez[:host] = @host if @host
176 | rez[:port] = @port if @port
177 | rez[:path] = @path if @path
178 | rez
179 | end
180 |
181 | def to_s
182 | s = StringIO.new
183 |
184 | if @scheme
185 | s << @scheme
186 | s << '://'
187 |
188 | if @user
189 | s << @user
190 | if @password
191 | s << ':'
192 | s << @password
193 | end
194 | s << '@'
195 | end
196 |
197 | s << @host
198 | if @port and @port != @default_port
199 | s << ':'
200 | s << @port
201 | end
202 | s << path
203 | elsif @host
204 | if @user
205 | s << @user
206 | s << '@'
207 | end
208 |
209 | s << host
210 | s << ':'
211 | s << path[1..-1]
212 | else
213 | return path
214 | end
215 |
216 | s.string
217 | end
218 |
219 | class << self
220 | def parse(url)
221 | return url if url.is_a? self
222 |
223 | self.new(url.to_s)
224 | end
225 | end
226 | end
227 | end
228 |
--------------------------------------------------------------------------------
/repos/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------