diff --git a/roles/archbuild/files/gitpkg b/roles/archbuild/files/gitpkg
new file mode 100755
index 0000000000000000000000000000000000000000..5b330675f752876f0e3535decc4e53b00f0aac1b
--- /dev/null
+++ b/roles/archbuild/files/gitpkg
@@ -0,0 +1,384 @@
+#!/usr/bin/ruby
+require 'optparse'
+require 'readline'
+
+$options = {}
+OptionParser.new do |opts|
+  opts.banner = <<~END
+    Usage: #{File.basename($0)} [Git URI]
+
+      Maintains Git-source PKGBUILDs. Allows selecting a new commit to build.
+
+      If first source is not a git URI, tries to alter PKGBUILD to port from
+      autotools distributed source to a git repo. If an absolute Git URI is
+      not provided, derives a GNOME Git URI automatically.
+
+      Otherwise, if a Git URI is provided, replaces the first source with the
+      provided URI.
+
+      Submodules aren't yet handled. Remember to properly source and inject
+      any submodules required.
+
+    Options:
+  END
+
+  opts.on("-h", "--help", "Print this help") do
+    puts opts
+    exit
+  end
+end.parse!
+
+def header(s)
+  puts "\n\e[1m#{s}:\e[0m"
+end
+
+class Pkgbuild
+  attr_reader :filename, :contents
+
+  def initialize(filename="PKGBUILD")
+    @filename = filename
+    @contents = File.read filename
+  end
+
+  def =~(regex)
+    @contents =~ regex
+  end
+
+  def has_var?(name)
+    @contents =~ /^#{name}=/
+  end
+
+  def has_func?(name)
+    @contents =~ /^#{name}\(\)/
+  end
+end
+
+class PkgbuildWriter < Pkgbuild
+  def write
+    File.write(@filename, @contents)
+  end
+
+  def sub(*args, &block)
+    @contents.sub!(*args, &block)
+  end
+
+  def gsub(*args, &block)
+    @contents.gsub!(*args, &block)
+  end
+
+  def insert_func(content, where=/[A-Za-z0-9_-]+/)
+    @contents.sub!(/^#{where}\(\)/, "#{content}\n\\&")
+  end
+
+  def replace_var(name, content=nil, after: nil, before: nil)
+    content.nil? ^ !block_given? or raise ArgumentError, "need either content or block"
+    after.nil? ^ before.nil? or raise ArgumentError, "need either after: or before:"
+
+    var_re = lambda { |var| /^#{var}=(\([^)]*\)|.*$)/ }
+
+    if has_var? name
+      @contents.sub!(var_re[name]) { "#{name}=#{content || yield($1)}" }
+      return false
+    end
+
+    repl = "#{name}=#{content || yield("")}"
+
+    if !after.nil?
+      @contents.sub!(var_re[after]) { $& + "\n" + repl }
+    else
+      @contents.sub!(var_re[before]) { repl + "\n" + $& }
+    end
+
+    true
+  end
+
+  def append_array(name, content, **kwargs)
+    replace_var(name, **kwargs) do |orig|
+      if orig =~ /^\(([^)]+)\)$/
+        "(#{$1} #{content})"
+      else
+        "(#{content})"
+      end
+    end
+  end
+end
+
+class PkgbuildReader < Pkgbuild
+  attr_reader :pkgbase, :basevar, :makedepends, :source
+
+  def initialize(*args)
+    super
+
+    @bash = IO.popen ["bash"], "w+"
+    @bash.puts "{", @contents, "} &>/dev/null"
+
+    @pkgbase = read :pkgbase
+    @basevar = :pkgbase
+    if @pkgbase.empty?
+      @pkgbase = read :pkgname
+      @basevar = :pkgname
+    end
+
+    @makedepends = readarray :makedepends
+    @source = readarray :source
+  end
+
+  def exec(*lines)
+    lines.each { |l| @bash.puts l }
+    @bash.puts "printf '\\x00ENDEXEC\\x00'"
+    buf = ""
+    buf << @bash.readchar until buf[-1] == ?\x00 && buf =~ /\x00ENDEXEC\x00\Z/
+    buf[0..-10]
+  end
+
+  def read(varname)
+    exec("printf '%s' \"${#{varname}}\"")
+  end
+
+  def readarray(varname)
+    exec("printf '%s\\x00' \"${#{varname}[@]}\"").split("\x00")
+  end
+end
+
+class Repo
+  SRCDEST = "/var/lib/archbuilddest/srcdest"
+  DISCOVER_URLS = [
+    "https://anongit.freedesktop.org/git/",
+    "https://git.gnome.org/browse/",
+    "https://github.com/",
+  ]
+
+  attr_reader :name, :url
+
+  def self.is_git_src?(src)
+    src =~ /(^|::)git(\+https?)?:\/\//
+  end
+
+  def self.split_src(src)
+    name, url = src =~ /::/ ? src.split("::", 2) : [nil, src.dup]
+    url.sub!(/#.*/, "")
+    url.sub!(/^git\+/, "")
+    [name, url]
+  end
+
+  def self.exists?(src)
+    _name, url = split_src src
+    !!Kernel.system('git', 'ls-remote', url, out: "/dev/null", err: [:child, :out])
+  end
+
+  def self.discover(src)
+    name, url = split_src src
+
+    if url !~ /:\/\//
+      url = DISCOVER_URLS.map { |u| u + url }.find do |u|
+        puts "Trying #{u}"
+        exists?(u)
+      end
+      raise "Discover failure" if url.nil?
+    end
+
+    url.prepend "git+" unless Repo.is_git_src?(name)
+    url << "#commit=$_commit"
+
+    return "#{name}::#{url}" unless name.nil?
+    return url
+  end
+
+  def initialize(src)
+    @name, @url = self.class.split_src(src)
+    @name = url.sub(/(\.git)?\/?$/, "").sub(/^.*\//, "") if @name.nil?
+
+    @dir = File.join(SRCDEST, name)
+
+    if test ?e, @dir
+       system('git', 'fetch', '--all', '-p')
+    else
+      Dir.chdir(SRCDEST) do
+        Kernel.system('git', 'clone', '--mirror', url, name) or raise "Clone failure"
+      end
+    end
+
+    @refs = read('git', 'for-each-ref', '--sort=v:refname', '--format=%(refname)',
+                 'refs/heads/*', 'refs/tags/*').lines.
+            map { |l| l.strip.sub(/^refs\/(heads|tags)\//, "") }
+  end
+
+  def system(*args)
+    Dir.chdir(@dir) do
+      Kernel.system(*args)
+    end
+  end
+
+  def read(*args)
+    ret = IO.popen(args, chdir: @dir) { |f| f.read }
+
+    if !$?.exited?
+      raise "#{args[0] == "git" ? args[1] : args[0]} failed"
+    elsif !$?.exitstatus.zero?
+      raise "#{args[0] == "git" ? args[1] : args[0]} failed, exit #{$?.exitstatus}"
+    end
+
+    ret
+  end
+
+  def resolve(ref)
+    read('git', 'rev-parse', '--verify', '-q', "#{ref}^{commit}").chomp
+  end
+
+  def describe(*args)
+    read('git', 'describe', '--tags', *args).chomp
+  end
+
+  def read_file(ref, file)
+    read 'git', 'show', "#{ref}:#{file}"
+  end
+
+  def log(*args)
+    system('git', 'log', '--first-parent', '-m', '--decorate', *args)
+  end
+
+  def complete_ref(s)
+    @refs.grep(/^#{Regexp.escape s}/)
+  end
+
+  def choose_commit
+    header "Branches"
+    system 'git', 'branch'
+
+    header "Tags"
+    system 'git', 'tag'
+
+    begin
+      old_char = Readline.completion_append_character
+      old_proc = Readline.completion_proc
+      Readline.completion_append_character = ""
+      Readline.completion_proc = method(:complete_ref).to_proc
+      commit = nil
+
+      loop do
+        puts
+        if (ref = Readline.readline("Enter ref (or nothing to view all): ", true).strip).empty?
+          system('tig', '--all', '--date-order') or log('--graph', '--all', '--oneline')
+        else
+          begin
+            commit = resolve ref
+            break
+          rescue => e
+            puts "Failed to resolve ref (#{e})"
+          end
+        end
+      end
+    ensure
+      Readline.completion_append_character = old_char
+      Readline.completion_proc = old_proc
+    end
+
+    log '--stat=80', commit
+    system 'git', 'show', "#{commit}:NEWS"
+
+    commit
+  end
+end
+
+pbr = PkgbuildReader.new
+reposrc = pbr.source.first
+gitify = !Repo.is_git_src?(reposrc)
+
+case ARGV.count
+when 0
+  reposrc = Repo.discover(pbr.pkgbase) if gitify
+when 1
+  reposrc = Repo.discover(ARGV.first)
+else
+  raise "Invalid number of arguments"
+end
+
+repo = Repo.new reposrc
+commit = repo.choose_commit
+commit_comment = repo.describe('--contains', '--all', commit)
+
+pbw = PkgbuildWriter.new
+pbw.replace_var :_commit, "#{commit}  # #{commit_comment}", before: :source
+pbw.sub(/source=\([^) \n]+/, "source=(\"#{reposrc}\"") unless pbr.source.first == reposrc
+
+if gitify
+  autogen = repo.read_file(commit, "autogen.sh") rescue ""
+  configure = repo.read_file(commit, "configure.ac") rescue ""
+
+  add_md = lambda do |name, cond=true|
+    if !pbr.makedepends.grep(name).empty?
+      true
+    elsif cond
+      pbw.append_array(:makedepends, name, after: :depends)
+      true
+    else
+      false
+    end
+  end
+
+  unless add_md["gnome-common",
+      (autogen =~ /gnome-autogen\.sh/ || configure =~ /^GNOME_[A-Z_]+(\(|$)/)]
+    add_md["gtk-doc", configure =~ /^GTK_DOC_CHECK/]
+    add_md["yelp-tools", configure =~ /^YELP_HELP_INIT/]
+    add_md["autoconf-archive", configure =~ /^AX_/]
+    add_md["intltool", configure =~ /^IT_PROG_INTLTOOL/]
+  end
+  add_md["appstream-glib", configure =~ /^APPSTREAM_/]
+  add_md["vala", configure =~ /^AM_PROG_VALAC/]
+  add_md["git"]
+
+  if pbr.pkgbase == repo.name
+    localname = "$#{pbr.basevar}"
+  else
+    localname = repo.name
+  end
+
+  pbw.gsub(/(\${?#{pbr.basevar}}?|#{repo.name})-\${?pkgver}?/, localname)
+
+  if pbr !~ /autogen\.sh/
+    pbw.insert_func <<~END
+      prepare() {
+        cd #{localname}
+        NOCONFIGURE=1 ./autogen.sh
+      }
+    END
+  end
+
+  sed = ""
+
+  begin
+    describe = repo.describe(commit)
+
+    sed << "s/^#{$&}//;" if describe =~ /^\D+/
+    sed << "s/_/./g;" if describe =~ /_/
+  rescue => e
+    warn "pkgver() deduction failed: #{e}"
+  end
+
+  sed << "s/-/+/g"
+
+  pbw.insert_func <<~END
+    pkgver() {
+      cd #{localname}
+      git describe --tags | sed '#{sed}'
+    }
+  END
+end
+
+pbw.write
+system "updpkgsums"
+system "makepkg", "--verifysource"
+
+header "Diff"
+IO.popen(['diff', '-u', '-', pbw.filename], "w") { |f| f.write pbr.contents }
+
+header "Submodules"
+gitmodules = repo.read_file(commit, ".gitmodules") rescue ""
+if gitmodules =~ /submodule/
+  puts gitmodules
+else
+  puts "None"
+end
+
+Dir.unlink "src"
+File.unlink(*Dir.glob("*.pkg.tar.xz"))
diff --git a/roles/archbuild/tasks/main.yml b/roles/archbuild/tasks/main.yml
index 6ee531739044ed24131796c4f91dd6bd5d02d42f..f0d84757f374987a969a41bdfb9d1ef10eaa55fe 100644
--- a/roles/archbuild/tasks/main.yml
+++ b/roles/archbuild/tasks/main.yml
@@ -10,6 +10,7 @@
     - diffrepo
     - clean-chroots
     - clean-dests
+    - gitpkg
 
 - name: install archbuild units
   copy: src={{ item }} dest=/etc/systemd/system/{{ item }} owner=root group=root mode=0644