Merge branch '18947-githttpd'
[arvados.git] / apps / workbench / app / models / repository.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class Repository < ArvadosBase
6   def self.creatable?
7     false
8   end
9   def attributes_for_display
10     super.reject { |x| x[0] == 'fetch_url' }
11   end
12   def editable_attributes
13     if current_user.is_admin
14       super
15     else
16       []
17     end
18   end
19
20   def show commit_sha1
21     refresh
22     run_git 'show', commit_sha1
23   end
24
25   def cat_file commit_sha1, path
26     refresh
27     run_git 'cat-file', 'blob', commit_sha1 + ':' + path
28   end
29
30   def ls_tree_lr commit_sha1
31     refresh
32     run_git 'ls-tree', '-l', '-r', commit_sha1
33   end
34
35   # subtree returns a list of files under the given path at the
36   # specified commit. Results are returned as an array of file nodes,
37   # where each file node is an array [file mode, blob sha1, file size
38   # in bytes, path relative to the given directory]. If the path is
39   # not found, [] is returned.
40   def ls_subtree commit, path
41     path = path.chomp '/'
42     subtree = []
43     ls_tree_lr(commit).each_line do |line|
44       mode, type, sha1, size, filepath = line.split
45       next if type != 'blob'
46       if filepath[0,path.length] == path and
47           (path == '' or filepath[path.length] == '/')
48         subtree << [mode.to_i(8), sha1, size.to_i,
49                     filepath[path.length,filepath.length]]
50       end
51     end
52     subtree
53   end
54
55   # http_fetch_url returns the first http:// or https:// url (if any)
56   # in the api response's clone_urls attribute.
57   def http_fetch_url
58     clone_urls.andand.select { |u| /^http/ =~ u }.first
59   end
60
61   protected
62
63   # refresh fetches the latest repository content into the local
64   # cache. It is a no-op if it has already been run on this object:
65   # this (pretty much) avoids doing more than one remote git operation
66   # per Workbench request.
67   def refresh
68     run_git 'fetch', http_fetch_url, '+*:*' unless @fresh
69     @fresh = true
70   end
71
72   # run_git sets up the ARVADOS_API_TOKEN environment variable,
73   # creates a local git directory for this repository if necessary,
74   # executes "git --git-dir localgitdir {args to run_git}", and
75   # returns the output. It raises GitCommandError if git exits
76   # non-zero.
77   def run_git *gitcmd
78     if not @workdir
79       workdir = File.expand_path uuid+'.git', Rails.configuration.Workbench.RepositoryCache
80       if not File.exists? workdir
81         FileUtils.mkdir_p Rails.configuration.Workbench.RepositoryCache
82         [['git', 'init', '--bare', workdir],
83         ].each do |cmd|
84           system(*cmd, in: "/dev/null")
85           raise GitCommandError.new($?.to_s) unless $?.exitstatus == 0
86         end
87       end
88       @workdir = workdir
89     end
90     [['git', '--git-dir', @workdir, 'config', '--local',
91       "credential.#{http_fetch_url}.username", 'none'],
92      ['git', '--git-dir', @workdir, 'config', '--local',
93       "credential.#{http_fetch_url}.helper",
94       '!cred(){ cat >/dev/null; if [ "$1" = get ]; then echo password=$ARVADOS_API_TOKEN; fi; };cred'],
95      ['git', '--git-dir', @workdir, 'config', '--local',
96            'http.sslVerify',
97            Rails.configuration.TLS.Insecure ? 'false' : 'true'],
98      ].each do |cmd|
99       system(*cmd, in: "/dev/null")
100       raise GitCommandError.new($?.to_s) unless $?.exitstatus == 0
101     end
102     env = {}.
103       merge(ENV).
104       merge('ARVADOS_API_TOKEN' => Thread.current[:arvados_api_token],
105             'GIT_TERMINAL_PROMPT' => '0')
106     cmd = ['git', '--git-dir', @workdir] + gitcmd
107     io = IO.popen(env, cmd, err: [:child, :out], in: "/dev/null")
108     output = io.read
109     io.close
110     # "If [io] is opened by IO.popen, close sets $?." --ruby 2.2.1 docs
111     unless $?.exitstatus == 0
112       raise GitCommandError.new("`git #{gitcmd.join ' '}` #{$?}: #{output}")
113     end
114     output
115   end
116
117   class GitCommandError < StandardError
118   end
119 end