aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cmd/update.rb
blob: 1073ef1f0a2dd85e9de354e6f035324d57299f71 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
module Homebrew extend self
  def update
    abort "Please `brew install git' first." unless system "/usr/bin/which -s git"

    updater = RefreshBrew.new
    if updater.update_from_masterbrew!
      updater.report
    else
      puts "Already up-to-date."
    end
  end
end

class RefreshBrew
  REPOSITORY_URL = "https://github.com/mxcl/homebrew.git"
  FORMULA_DIR = 'Library/Formula/'
  EXAMPLE_DIR = 'Library/Contributions/examples/'

  attr_reader :added_formulae, :updated_formulae, :deleted_formulae, :installed_formulae
  attr_reader :added_examples, :deleted_examples
  attr_reader :initial_revision, :current_revision

  def initialize
    @added_formulae, @updated_formulae, @deleted_formulae, @installed_formulae = [], [], [], []
    @added_examples, @deleted_examples = [], [], []
    @initial_revision, @current_revision = nil
  end

  # Performs an update of the homebrew source. Returns +true+ if a newer
  # version was available, +false+ if already up-to-date.
  def update_from_masterbrew!
    HOMEBREW_REPOSITORY.cd do
      if git_repo?
        safe_system "git checkout -q master"
        @initial_revision = read_revision
        # originally we fetched by URL but then we decided that we should
        # use origin so that it's easier for forks to operate seamlessly
        unless `git remote`.split.include? 'origin'
          safe_system "git remote add origin #{REPOSITORY_URL}"
        end
      else
        begin
          safe_system "git init"
          safe_system "git remote add origin #{REPOSITORY_URL}"
          safe_system "git fetch origin"
          safe_system "git reset --hard origin/master"
        rescue Exception
          safe_system "/bin/rm -rf .git"
          raise
        end
      end

      # specify a refspec so that 'origin/master' gets updated
      refspec = "refs/heads/master:refs/remotes/origin/master"
      rebase = "--rebase" if ARGV.include? "--rebase"
      execute "git pull #{rebase} origin #{refspec}"
      @current_revision = read_revision
    end

    if initial_revision && initial_revision != current_revision
      # hash with status characters for keys:
      # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R)
      @changes_map = Hash.new {|h,k| h[k] = [] }

      changes = HOMEBREW_REPOSITORY.cd do
        execute("git diff-tree -r --name-status -z #{initial_revision} #{current_revision}").split("\0")
      end

      while status = changes.shift
        file = changes.shift
        @changes_map[status] << file
      end

      if @changes_map.any?
        @added_formulae   = changed_items('A', FORMULA_DIR)
        @deleted_formulae = changed_items('D', FORMULA_DIR)
        @updated_formulae = changed_items('M', FORMULA_DIR)
        @added_examples   = changed_items('A', EXAMPLE_DIR)
        @deleted_examples = changed_items('D', EXAMPLE_DIR)
        @added_internal_commands = changed_items('A', "Library/Homebrew/cmd")
        @deleted_internal_commands = changed_items('D', "Library/Homebrew/cmd")

        @installed_formulae = HOMEBREW_CELLAR.children.
          select{ |pn| pn.directory? }.
          map{ |pn| pn.basename.to_s }.sort if HOMEBREW_CELLAR.directory?

        return true
      end
    end
    # assume nothing was updated
    return false
  end

  def git_repo?
    Dir['.git/*'].length > 0
  end

  def pending_formulae_changes?
    !@updated_formulae.empty?
  end

  def pending_new_formulae?
    !@added_formulae.empty?
  end

  def deleted_formulae?
    !@deleted_formulae.empty?
  end

  def pending_examples_changes?
    !@updated_examples.empty?
  end

  def pending_new_examples?
    !@added_examples.empty?
  end

  def deleted_examples?
    !@deleted_examples.empty?
  end

  def report
    puts "Updated Homebrew from #{initial_revision[0,8]} to #{current_revision[0,8]}."
    if pending_new_formulae?
      ohai "New formulae"
      puts_columns added_formulae
    end
    if deleted_formulae?
      ohai "Removed formulae"
      puts_columns deleted_formulae, installed_formulae
    end
    if pending_formulae_changes?
      ohai "Updated formulae"
      puts_columns updated_formulae, installed_formulae
    end

    unless @added_internal_commands.empty?
      ohai "New commands"
      puts_columns @added_internal_commands
    end
    unless @deleted_internal_commands.empty?
      ohai "Removed commands"
      puts_columns @deleted_internal_commands
    end

    # external commands aren't generally documented but the distinction
    # is loose. They are less "supported" and more "playful".
    if pending_new_examples?
      ohai "New external commands"
      puts_columns added_examples
    end
    if deleted_examples?
      ohai "Removed external commands"
      puts_columns deleted_examples
    end
  end

  private

  def read_revision
    execute("git rev-parse HEAD").chomp
  end

  def filter_by_directory(files, dir)
    files.select { |f| f.index(dir) == 0 }
  end

  def basenames(files)
    files.map { |f| File.basename(f, '.rb') }
  end

  # extracts items by status from @changes_map
  def changed_items(status, dir)
    basenames(filter_by_directory(@changes_map[status], dir)).sort
  end

  def execute(cmd)
    out = `#{cmd}`
    if $? && !$?.success?
      $stderr.puts out
      raise "Failed while executing #{cmd}"
    end
    ohai(cmd, out) if ARGV.verbose?
    out
  end
end