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