├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── filter_changelog.go ├── filter_changelog_test.go ├── getpatch.go ├── getpatch_test.go ├── loggedexec ├── example_test.go ├── loggedexec.go └── loggedexec_test.go ├── mergebot.go ├── mergebot_test.go ├── testdata ├── 831331.patch ├── 831331.soap ├── minimal-debian-package │ └── debian │ │ ├── changelog │ │ ├── compat │ │ ├── control │ │ └── rules └── minimal.soap └── travis ├── sbuild-key.pub └── sbuild-key.sec /.gitignore: -------------------------------------------------------------------------------- 1 | mergebot 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: go 5 | go: 6 | - 1.6 7 | 8 | addons: 9 | apt: 10 | packages: 11 | - git 12 | - devscripts 13 | # For sbuild: 14 | - sbuild 15 | - debootstrap 16 | # For building the package: 17 | - git-buildpackage 18 | - debhelper 19 | 20 | before_install: 21 | # Generating keys using sbuild-update --keygen takes so long that travis 22 | # times out. Hence, we use pre-generated keys. The resulting packages are 23 | # discarded anyway, so these keys being public is not an issue. 24 | - sudo cp travis/sbuild-key.pub /var/lib/sbuild/apt-keys/sbuild-key.pub 25 | - sudo cp travis/sbuild-key.sec /var/lib/sbuild/apt-keys/sbuild-key.sec 26 | - sudo sbuild-adduser travis 27 | - sudo sbuild-createchroot --include=eatmydata,ccache,gnupg unstable /srv/chroot/unstable-amd64 http://deb.debian.org/debian 28 | 29 | script: 30 | - echo go test ./ -skip_test_cleanup | newgrp sbuild 31 | - go test ./loggedexec 32 | # Check whether files are syntactically correct. 33 | - "gofmt -l $(find . -name '*.go' | tr '\\n' ' ') >/dev/null" 34 | # Check whether files were not gofmt'ed. 35 | - "gosrc=$(find . -name '*.go' | tr '\\n' ' '); [ $(gofmt -l $gosrc 2>&- | wc -l) -eq 0 ] || (echo 'gofmt was not run on these files:'; gofmt -l $gosrc 2>&-; false)" 36 | - go tool vet . 37 | 38 | after_failure: bash -c 'head -500 /tmp/test-merge-and-build-*/**/*' 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Michael Stapelberg , 2016 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mergebot 2 | 3 | [![Build Status](https://travis-ci.org/Debian/mergebot.svg?branch=master)](https://travis-ci.org/Debian/mergebot) 4 | 5 | ## Vision 6 | 7 | Minimize the number of steps required to accept contributions for Debian packages you maintain. 8 | 9 | ## Usage instructions 10 | 11 | To merge the most recent patch in Debian bug #831331 and build the resulting 12 | package, use: 13 | ``` 14 | mergebot -source_package=wit -bug=831331 15 | ``` 16 | 17 | Afterwards, inspect the resulting Debian package and git repository. 18 | If both look good, push and upload using the following commands which are 19 | suggested by the `mergebot` invocation above: 20 | ``` 21 | cd /tmp/mergebot-19384221 22 | (cd repo && git push) 23 | (cd export && debsign *.changes && dput *.changes) 24 | ``` 25 | 26 | See “Future ideas” for how to further streamline this process. 27 | 28 | ## Installation 29 | 30 | Until `mergebot` is packaged in Debian, use these instructions to install Go 31 | and build `mergebot` from source: 32 | 33 | ``` 34 | sudo apt-get install golang-go 35 | export GOPATH=~/gocode 36 | go get -u github.com/Debian/mergebot 37 | ``` 38 | 39 | ## Dependencies 40 | 41 | * `git` 42 | * `sbuild` 43 | * `gbp` 44 | * `devscripts` (pulled in by `gbp` as well) 45 | 46 | ## Assumptions 47 | 48 | * your repository can be cloned using `gbp clone --pristine-tar` 49 | * your repository uses `git` as SCM 50 | * your repository can be built using `gbp buildpackage` with `sbuild` 51 | 52 | ## Future ideas 53 | 54 | Please get in touch in case you’re interested in using or helping with any of 55 | the following features: 56 | 57 | * Run `mergebot` automatically for every incoming patch, respond to the bug 58 | with a report about whether the patch can be merged successfully and whether 59 | the resulting package builds successfully. 60 | * Add a UI to `mergebot` (web service? email? user script for the BTS?), 61 | allowing you to have `mergebot` merge, build, push and upload contributions 62 | on your behalf. 63 | -------------------------------------------------------------------------------- /filter_changelog.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // TODO: file a bug for this issue upstream with gbp dch, then remove this code. 13 | func filterChangelog(path string) error { 14 | output, err := ioutil.TempFile(filepath.Dir(path), ".mergebot-") 15 | if err != nil { 16 | return err 17 | } 18 | // Clean up in case we don’t reach the os.Rename call. 19 | defer os.Remove(output.Name()) 20 | input, err := os.Open(path) 21 | if err != nil { 22 | return err 23 | } 24 | defer input.Close() 25 | scanner := bufio.NewScanner(input) 26 | var ( 27 | lastSection string 28 | copyOnly bool 29 | lastLineEmpty bool 30 | ) 31 | for scanner.Scan() { 32 | if copyOnly { 33 | fmt.Fprintln(output, scanner.Text()) 34 | continue 35 | } 36 | lineEmpty := (scanner.Text() == "") 37 | // Defer printing section headers until the first entry of the section. 38 | if strings.HasPrefix(scanner.Text(), " [ ") { 39 | lastSection = scanner.Text() 40 | continue 41 | } 42 | if strings.HasPrefix(scanner.Text(), " * ") { 43 | fmt.Fprintln(output, lastSection) 44 | } 45 | // Only modify the most recent changelog entry. 46 | if strings.HasPrefix(scanner.Text(), " -- ") { 47 | copyOnly = true 48 | } 49 | if lastLineEmpty && lineEmpty { 50 | // Avoid printing more than one empty line at a time. 51 | } else { 52 | fmt.Fprintln(output, scanner.Text()) 53 | } 54 | lastLineEmpty = lineEmpty 55 | } 56 | if err := scanner.Err(); err != nil { 57 | return err 58 | } 59 | if err := output.Close(); err != nil { 60 | return err 61 | } 62 | return os.Rename(output.Name(), path) 63 | } 64 | -------------------------------------------------------------------------------- /filter_changelog_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestFilterChangelog(t *testing.T) { 11 | f, err := ioutil.TempFile("", "filter-changelog-test") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | defer os.Remove(f.Name()) 16 | fmt.Fprintln(f, `wit (2.31a-3) unstable; urgency=medium 17 | 18 | [ Chris Lamb ] 19 | * Fix for “wit: please make the build reproducible” (Closes: #831331) 20 | 21 | [ Michael Stapelberg ] 22 | 23 | -- Michael Stapelberg Sat, 16 Jul 2016 20:39:13 +0200 24 | 25 | wit (2.31a-2) unstable; urgency=low 26 | 27 | [ Tobias Gruetzmacher ] 28 | * Add zlib support (Closes: #815710) 29 | * Don’t link wfuse against libdl 30 | 31 | -- Michael Stapelberg Tue, 23 Feb 2016 23:40:46 +0100`) 32 | if err := f.Close(); err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | if err := filterChangelog(f.Name()); err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | content, err := ioutil.ReadFile(f.Name()) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if got, want := string(content), `wit (2.31a-3) unstable; urgency=medium 46 | 47 | [ Chris Lamb ] 48 | * Fix for “wit: please make the build reproducible” (Closes: #831331) 49 | 50 | -- Michael Stapelberg Sat, 16 Jul 2016 20:39:13 +0200 51 | 52 | wit (2.31a-2) unstable; urgency=low 53 | 54 | [ Tobias Gruetzmacher ] 55 | * Add zlib support (Closes: #815710) 56 | * Don’t link wfuse against libdl 57 | 58 | -- Michael Stapelberg Tue, 23 Feb 2016 23:40:46 +0100 59 | `; got != want { 60 | t.Fatalf("Changelog not filtered: got %q, want %q", got, want) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /getpatch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/xml" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "mime" 11 | "mime/multipart" 12 | "net/http" 13 | "net/mail" 14 | "strings" 15 | ) 16 | 17 | const ( 18 | soapAddress = "https://bugs.debian.org/cgi-bin/soap.cgi" 19 | soapNamespace = "Debbugs/SOAP" 20 | ) 21 | 22 | type patch struct { 23 | Author string 24 | Subject string 25 | Data []byte 26 | } 27 | 28 | func getMostRecentPatch(url, bug string) (patch, error) { 29 | var result patch 30 | // TODO: write a WSDL file and use a proper Go SOAP library? see https://golanglibs.com/top?q=soap 31 | req := fmt.Sprintf(` 32 | 39 | 40 | 41 | %s 42 | 43 | 44 | 45 | `, bug) 46 | resp, err := http.Post(url, "", strings.NewReader(req)) 47 | if err != nil { 48 | return result, err 49 | } 50 | if got, want := resp.StatusCode, http.StatusOK; got != want { 51 | return result, fmt.Errorf("Unexpected HTTP status code: got %d, want %d", got, want) 52 | } 53 | 54 | mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 55 | if err != nil { 56 | return result, err 57 | } 58 | 59 | if !strings.HasPrefix(mediaType, "multipart/") { 60 | return result, fmt.Errorf("Unexpected Content-Type: got %q, want multipart/*", resp.Header.Get("Content-Type")) 61 | } 62 | 63 | var r struct { 64 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` 65 | Bugs []struct { 66 | XMLName xml.Name `xml:"Debbugs/SOAP item"` 67 | MsgNum int `xml:"msg_num"` 68 | Body string `xml:"body"` 69 | Header string `xml:"header"` 70 | } `xml:"Body>get_bug_logResponse>Array>item"` 71 | } 72 | 73 | if err := xml.NewDecoder(resp.Body).Decode(&r); err != nil { 74 | return result, err 75 | } 76 | 77 | // TODO: handle bugs with multiple messages 78 | if len(r.Bugs) != 1 { 79 | log.Fatalf("len(r.Bugs) = %d", len(r.Bugs)) 80 | } 81 | 82 | body := r.Bugs[0].Body 83 | mr := multipart.NewReader(strings.NewReader(body), params["boundary"]) 84 | for { 85 | p, err := mr.NextPart() 86 | if err == io.EOF { 87 | break 88 | } 89 | if err != nil { 90 | return result, err 91 | } 92 | disposition, _, err := mime.ParseMediaType(p.Header.Get("Content-Disposition")) 93 | if err != nil { 94 | log.Printf("Skipping MIME part with invalid Content-Disposition header (%v)", err) 95 | continue 96 | } 97 | // TODO: is disposition always lowercase? 98 | if got, want := disposition, "attachment"; got != want { 99 | log.Printf("Skipping MIME part with unexpected Content-Disposition: got %q, want %q", got, want) 100 | continue 101 | } 102 | 103 | if p.Header.Get("Content-Transfer-Encoding") == "base64" { 104 | m, err := mail.ReadMessage(strings.NewReader(r.Bugs[0].Header + body)) 105 | if err != nil { 106 | return result, err 107 | } 108 | result.Author = m.Header.Get("From") 109 | result.Subject = m.Header.Get("Subject") 110 | encoded, err := ioutil.ReadAll(p) 111 | if err != nil { 112 | return result, err 113 | } 114 | result.Data, err = base64.StdEncoding.DecodeString(string(encoded)) 115 | return result, err 116 | } else { 117 | log.Fatalf("unsupported Content-Transfer-Encoding: %q", p.Header.Get("Content-Transfer-Encoding")) 118 | } 119 | } 120 | 121 | return result, fmt.Errorf("No MIME part with Content-Disposition == attachment found") 122 | } 123 | -------------------------------------------------------------------------------- /getpatch_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | const ( 12 | goldenSoapPath = "testdata/831331.soap" 13 | goldenPatchPath = "testdata/831331.patch" 14 | ) 15 | 16 | func TestGetMostRecentPatch(t *testing.T) { 17 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | w.Header().Set("Content-Type", `multipart/related; type="text/xml"; start=""; boundary="_----------=_146851316918670990"`) 19 | http.ServeFile(w, r, goldenSoapPath) 20 | })) 21 | defer ts.Close() 22 | 23 | patch, err := getMostRecentPatch(ts.URL, "831331") 24 | if err != nil { 25 | t.Fatalf("Unexpected error: %v", err) 26 | } 27 | 28 | if got, want := patch.Author, "Chris Lamb "; got != want { 29 | t.Fatalf("Incorrect patch author: got %q, want %q", got, want) 30 | } 31 | 32 | if got, want := patch.Subject, "wit: please make the build reproducible"; got != want { 33 | t.Fatalf("Incorrect patch subject: got %q, want %q", got, want) 34 | } 35 | 36 | goldenPatch, err := ioutil.ReadFile(goldenPatchPath) 37 | if err != nil { 38 | t.Fatalf("Could not read golden patch data from %q for comparison: %v", goldenPatchPath, err) 39 | } 40 | 41 | if !bytes.Equal(patch.Data, goldenPatch) { 42 | t.Fatalf("Patch data parsed from %q does not match %q", goldenSoapPath, goldenPatchPath) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /loggedexec/example_test.go: -------------------------------------------------------------------------------- 1 | package loggedexec_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Debian/mergebot/loggedexec" 7 | ) 8 | 9 | func ExampleCommand() { 10 | cmd := loggedexec.Command("ls", "/tmp/nonexistant") 11 | cmd.Env = []string{"LANG=C"} 12 | if err := cmd.Run(); err != nil { 13 | fmt.Println(err) 14 | } 15 | // Output: Running "ls /tmp/nonexistant": exit status 2 16 | // See "/tmp/000-ls.invocation.log" for invocation details. 17 | // See "/tmp/000-ls.stdoutstderr.log" for full stdout/stderr. 18 | // First stdout/stderr line: "ls: cannot access /tmp/nonexistant: No such file or directory" 19 | } 20 | -------------------------------------------------------------------------------- /loggedexec/loggedexec.go: -------------------------------------------------------------------------------- 1 | // loggedexec is a wrapper around os/exec which logs command 2 | // execution, the command’s stdout/stderr into files and provides 3 | // better error messages when command execution fails. This makes 4 | // debugging easier for end users. 5 | package loggedexec 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | "time" 20 | ) 21 | 22 | var ( 23 | cmdCount int 24 | cmdCountMu sync.Mutex 25 | ) 26 | 27 | // LoggedCmd is like (os/exec).Cmd, but its Run() method additionally: 28 | // 29 | // * Logs each invocation’s command for human consumption. 30 | // * Logs each invocation’s working directory, Args, Env and timing 31 | // into a file. 32 | // * Logs each invocation’s stdout/stderr into a file. 33 | // * Wraps the returned error (if any) with the command and pointers 34 | // to the log files with more details (including the first line 35 | // of stdout/stderr). 36 | // 37 | // All files are created in LogDir. 38 | type LoggedCmd struct { 39 | *exec.Cmd 40 | 41 | // Logger will be used to log invocation commands for human 42 | // consumption. Defaults to logging to os.Stderr. Use 43 | // ioutil.Discard to hide logs. 44 | Logger *log.Logger 45 | 46 | // LogDir is the directory in which log files will be 47 | // created. Defaults to os.TempDir(). 48 | LogDir string 49 | 50 | // LogFmt is the prefix used for naming log files in 51 | // LogDir. Defaults to "%03d-" and must contain precisely one "%d" 52 | // which will be replaced with the invocation count. 53 | LogFmt string 54 | } 55 | 56 | // Command is like (os/exec).Command, but returns a LoggedCmd. 57 | func Command(name string, arg ...string) *LoggedCmd { 58 | return &LoggedCmd{ 59 | Cmd: exec.Command(name, arg...), 60 | Logger: log.New(os.Stderr, "", log.Lshortfile), 61 | LogFmt: "%03d-", 62 | } 63 | } 64 | 65 | // capturingWriter captures data until a newline (\n) is seen, so that 66 | // we can display the first log line in error messages. 67 | type capturingWriter struct { 68 | Data []byte 69 | newlineSeen bool 70 | } 71 | 72 | func (c *capturingWriter) Write(p []byte) (n int, err error) { 73 | if !c.newlineSeen { 74 | c.Data = append(c.Data, p...) 75 | // Start searching from the end, as newlines are more likely 76 | // to occur at the end. 77 | c.newlineSeen = (bytes.LastIndexByte(p, '\n') != -1) 78 | } 79 | return len(p), nil 80 | } 81 | 82 | func (c *capturingWriter) FirstLine() string { 83 | s := string(c.Data) 84 | idx := strings.IndexByte(s, '\n') 85 | if idx == -1 { 86 | return s 87 | } 88 | return s[:idx] 89 | } 90 | 91 | func quoteStrings(input []string) []string { 92 | output := make([]string, len(input)) 93 | for idx, val := range input { 94 | output[idx] = strconv.Quote(val) 95 | } 96 | return output 97 | } 98 | 99 | // Run is a wrapper around (os/exec).Cmd’s Run(). 100 | func (l *LoggedCmd) Run() error { 101 | commandline := strings.Join(l.Args, " ") 102 | l.Logger.Printf("%s", commandline) 103 | 104 | if l.LogDir == "" { 105 | l.LogDir = os.TempDir() 106 | } 107 | cmdCountMu.Lock() 108 | // To prevent leaking private data, only l.Args[0] goes into the 109 | // file name, which is readable by other users on the same system. 110 | logPrefix := filepath.Join(l.LogDir, fmt.Sprintf(l.LogFmt, cmdCount)+l.Args[0]) 111 | cmdCount++ 112 | cmdCountMu.Unlock() 113 | invocationLogPath := logPrefix + ".invocation.log" 114 | logPath := logPrefix + ".stdoutstderr.log" 115 | 116 | workDir := l.Dir 117 | if workDir == "" { 118 | var err error 119 | workDir, err = os.Getwd() 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | started := time.Now() 125 | invocationLog := fmt.Sprintf( 126 | "Execution started: %v\n"+ 127 | "Working directory: %q\n"+ 128 | "Command (%d elements):\n\t%s\n"+ 129 | "Environment (%d elements):\n\t%s\n", 130 | started, 131 | workDir, 132 | len(l.Args), 133 | strings.Join(quoteStrings(l.Args), "\n\t"), 134 | len(l.Env), 135 | strings.Join(quoteStrings(l.Env), "\n\t")) 136 | if err := ioutil.WriteFile(invocationLogPath, []byte(invocationLog+"(Still running…)"), 0600); err != nil { 137 | return err 138 | } 139 | logFile, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 140 | if err != nil { 141 | return err 142 | } 143 | defer logFile.Close() 144 | var cw capturingWriter 145 | logWriter := io.MultiWriter(logFile, &cw) 146 | if l.Stdout == nil { 147 | l.Stdout = logWriter 148 | } else { 149 | l.Stdout = io.MultiWriter(l.Stdout, logWriter) 150 | } 151 | if l.Stderr == nil { 152 | l.Stderr = logWriter 153 | } else { 154 | l.Stderr = io.MultiWriter(l.Stderr, logWriter) 155 | } 156 | runErr := l.Cmd.Run() 157 | finished := time.Now() 158 | invocationLog = invocationLog + fmt.Sprintf( 159 | "Execution finished: %v (duration: %v)", 160 | finished, 161 | finished.Sub(started)) 162 | // Update the invocation log atomically to not lose data when 163 | // (e.g.) running out of disk space. 164 | f, err := ioutil.TempFile(filepath.Dir(invocationLogPath), ".invocation-log-") 165 | if err != nil { 166 | return err 167 | } 168 | fmt.Fprintln(f, invocationLog) 169 | if err := f.Close(); err != nil { 170 | return err 171 | } 172 | if err := os.Rename(f.Name(), invocationLogPath); err != nil { 173 | return err 174 | } 175 | if runErr == nil { 176 | return nil 177 | } 178 | firstLogLine := cw.FirstLine() 179 | return fmt.Errorf("Running %q: %v\n"+ 180 | "See %q for invocation details.\n"+ 181 | "See %q for full stdout/stderr.\n"+ 182 | "First stdout/stderr line: %q\n", 183 | commandline, 184 | runErr, 185 | invocationLogPath, 186 | logPath, 187 | firstLogLine) 188 | } 189 | -------------------------------------------------------------------------------- /loggedexec/loggedexec_test.go: -------------------------------------------------------------------------------- 1 | package loggedexec 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "log" 7 | "regexp" 8 | "testing" 9 | ) 10 | 11 | // TestErrorMessage verifies the error message contains additional 12 | // details. 13 | // 14 | // TestErrorMessage must run as the first test, since it makes 15 | // assumptions about the output file name, which depends on the number 16 | // of commands ran so far. 17 | func TestErrorMessage(t *testing.T) { 18 | cmd := Command("ls", "/tmp/nope") 19 | cmd.Logger = log.New(ioutil.Discard, "", 0) 20 | cmd.Env = []string{"LANG=C"} 21 | err := cmd.Run() 22 | if err == nil { 23 | t.Fatalf("Unexpectedly, running %v did not result in an error", cmd.Args) 24 | } 25 | want := `Running "ls /tmp/nope": exit status 2 26 | See "/tmp/000-ls.invocation.log" for invocation details. 27 | See "/tmp/000-ls.stdoutstderr.log" for full stdout/stderr. 28 | First stdout/stderr line: "ls: cannot access /tmp/nope: No such file or directory" 29 | ` 30 | if got := err.Error(); got != want { 31 | t.Fatalf("Unexpected error message: got %q, want %q", got, want) 32 | } 33 | } 34 | 35 | // TestLogExecution verifies that command executions are logged to the 36 | // specified Logger. 37 | func TestLogExecution(t *testing.T) { 38 | cmd := Command("ls", "/tmp/nope") 39 | var buf bytes.Buffer 40 | cmd.Logger = log.New(&buf, "", 0) 41 | cmd.Env = []string{"LANG=C"} 42 | cmd.Run() 43 | if got, want := buf.String(), "ls /tmp/nope\n"; got != want { 44 | t.Fatalf("Unexpected log output: got %q, want %q", got, want) 45 | } 46 | } 47 | 48 | func TestInvocationLogFile(t *testing.T) { 49 | cmd := Command("ls", "/tmp/nope") 50 | cmd.Logger = log.New(ioutil.Discard, "", 0) 51 | cmd.Env = []string{"LANG=C"} 52 | err := cmd.Run() 53 | if err == nil { 54 | t.Fatalf("Unexpectedly, running %v did not result in an error", cmd.Args) 55 | } 56 | invocationLogRe := regexp.MustCompile(`See "([^"]+)" for invocation details`) 57 | matches := invocationLogRe.FindStringSubmatch(err.Error()) 58 | if got, want := len(matches), 2; got != want { 59 | t.Fatalf("Unexpected number of regexp (%q) matches: got %d, want %d", invocationLogRe, got, want) 60 | } 61 | contents, err := ioutil.ReadFile(matches[1]) 62 | if err != nil { 63 | t.Fatalf("Could not read invocation log: %v", err) 64 | } 65 | 66 | invocationLogContentsRe := regexp.MustCompile( 67 | `(?m)Execution started: .*$ 68 | Working directory: "[^"]+"$ 69 | Command \(2 elements\):$ 70 | \t"ls"$ 71 | \t"/tmp/nope"$ 72 | Environment \(1 elements\):$ 73 | \t"LANG=C"$ 74 | Execution finished: .* \(duration: [^)]+\)$`) 75 | if !invocationLogContentsRe.Match(contents) { 76 | t.Fatalf("Invocation log contents (%q) don’t match regexp %q", string(contents), invocationLogContentsRe) 77 | } 78 | } 79 | 80 | // testLogFile contains TestLogFile’s logic, so that it can be reused 81 | // in TestTee with a slightly modified cmd. 82 | func testLogFile(t *testing.T, cmd *LoggedCmd) { 83 | err := cmd.Run() 84 | if err == nil { 85 | t.Fatalf("Unexpectedly, running %v did not result in an error", cmd.Args) 86 | } 87 | stdoutLogRe := regexp.MustCompile(`See "([^"]+)" for full stdout/stderr`) 88 | matches := stdoutLogRe.FindStringSubmatch(err.Error()) 89 | if got, want := len(matches), 2; got != want { 90 | t.Fatalf("Unexpected number of regexp (%q) matches: got %d, want %d", stdoutLogRe, got, want) 91 | } 92 | contents, err := ioutil.ReadFile(matches[1]) 93 | if err != nil { 94 | t.Fatalf("Could not read stdout/stderr log: %v", err) 95 | } 96 | if got, want := string(contents), "ls: cannot access /tmp/nope: No such file or directory\n"; got != want { 97 | t.Fatalf("Unexpected stdout/stderr log contents: got %q, want %q", got, want) 98 | } 99 | } 100 | 101 | // TestLogFile verifies the log file referenced in the error message 102 | // actually contains the stdout/stderr. 103 | func TestLogFile(t *testing.T) { 104 | cmd := Command("ls", "/tmp/nope") 105 | cmd.Logger = log.New(ioutil.Discard, "", 0) 106 | cmd.Env = []string{"LANG=C"} 107 | testLogFile(t, cmd) 108 | } 109 | 110 | func TestTee(t *testing.T) { 111 | cmd := Command("ls", "/tmp/nope") 112 | cmd.Logger = log.New(ioutil.Discard, "", 0) 113 | cmd.Env = []string{"LANG=C"} 114 | var stdouterr bytes.Buffer 115 | cmd.Stdout = &stdouterr 116 | cmd.Stderr = &stdouterr 117 | testLogFile(t, cmd) 118 | if got, want := stdouterr.String(), "ls: cannot access /tmp/nope: No such file or directory\n"; got != want { 119 | t.Fatalf("Unexpected stdout/stderr buffer contents: got %q, want %q", got, want) 120 | } 121 | } 122 | 123 | func TestResetCounter(t *testing.T) { 124 | cmdCountMu.Lock() 125 | cmdCount = 0 126 | cmdCountMu.Unlock() 127 | } 128 | -------------------------------------------------------------------------------- /mergebot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/Debian/mergebot/loggedexec" 15 | ) 16 | 17 | var ( 18 | sourcePackage = flag.String("source_package", "", "Debian source package against which the bug specified in -bug was filed.") 19 | bug = flag.String("bug", "", "Debian bug number containing the patch to merge (e.g. 831331 or #831331)") 20 | 21 | filterChangelogMode = flag.Bool("filter_changelog", false, "Not for interactive usage, will be removed! Run in filter changelog mode to work around a gbp dch issue.") 22 | ) 23 | 24 | // newCommand will be overwritten by mergeAndBuild() once the 25 | // temporary directory is created. 26 | var newCommand = loggedexec.Command 27 | 28 | const ( 29 | patchFileName = "latest.patch" 30 | ) 31 | 32 | func repositoryFor(sourcePackage string) (string, string, error) { 33 | cmd := newCommand("debcheckout", "--print", sourcePackage) 34 | output, err := cmd.Output() 35 | if err != nil { 36 | return "", "", err 37 | } 38 | parts := strings.Split(strings.TrimSpace(string(output)), "\t") 39 | if len(parts) != 2 { 40 | return "", "", fmt.Errorf("Unexpected command output: %v returned %q (split into %v), expected 2 parts", cmd.Args, string(output), parts) 41 | } 42 | scm := parts[0] 43 | url := parts[1] 44 | if strings.Contains(url, "anonscm.debian.org") { 45 | url = strings.Replace(url, "git", "git+ssh", 1) 46 | url = strings.Replace(url, "anonscm.debian.org", "git.debian.org", 1) 47 | url = strings.Replace(url, "debian.org", "debian.org/git", 1) 48 | } 49 | return scm, url, nil 50 | } 51 | 52 | func gitCheckout(dst, src string) error { 53 | wd, err := os.Getwd() 54 | if err != nil { 55 | return err 56 | } 57 | cmd := newCommand("gbp", "clone", "--pristine-tar", src, dst) 58 | cmd.Dir = wd 59 | if err := cmd.Run(); err != nil { 60 | return err 61 | } 62 | 63 | gitConfigArgs := [][]string{ 64 | // Push all (matching) branches at once. 65 | []string{"push.default", "matching"}, 66 | // Push tags automatically. 67 | []string{"--add", "remote.origin.push", "+refs/heads/*:refs/heads/*"}, 68 | []string{"--add", "remote.origin.push", "+refs/tags/*:refs/tags/*"}, 69 | } 70 | 71 | if debfullname := os.Getenv("DEBFULLNAME"); debfullname != "" { 72 | gitConfigArgs = append(gitConfigArgs, []string{"user.name", debfullname}) 73 | } 74 | 75 | if debemail := os.Getenv("DEBEMAIL"); debemail != "" { 76 | gitConfigArgs = append(gitConfigArgs, []string{"user.email", debemail}) 77 | } 78 | 79 | for _, configArgs := range gitConfigArgs { 80 | gitArgs := append([]string{"config"}, configArgs...) 81 | if err := newCommand("git", gitArgs...).Run(); err != nil { 82 | return err 83 | } 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // TODO: use git am for git format patches to respect the user’s commit metadata 90 | func applyPatch() error { 91 | return newCommand("patch", "-p1", "-i", filepath.Join("..", patchFileName)).Run() 92 | } 93 | 94 | func gitCommit(author, message string) error { 95 | if err := newCommand("git", "add", ".").Run(); err != nil { 96 | return err 97 | } 98 | 99 | return newCommand("git", "commit", "-a", 100 | "--author", author, 101 | "--message", message).Run() 102 | } 103 | 104 | func sha256of(path string) (string, error) { 105 | h := sha256.New() 106 | 107 | f, err := os.Open(path) 108 | if err != nil { 109 | return "", err 110 | } 111 | defer f.Close() 112 | 113 | if _, err := io.Copy(h, f); err != nil { 114 | return "", err 115 | } 116 | 117 | return fmt.Sprintf("%.16x", h.Sum(nil)), nil 118 | } 119 | 120 | // TODO: if gbp dch returns with “Version %s not found”, that’s fine, as the changelog is already up to date. Can we detect this case, or change our gbp dch invocation to not complain? 121 | func releaseChangelog() error { 122 | cmd := newCommand("gbp", "dch", "--release", "--git-author", "--commit") 123 | // See the comment on filterChangelog() for details: 124 | self, err := filepath.Abs(os.Args[0]) 125 | if err != nil { 126 | return err 127 | } 128 | cmd.Env = append(cmd.Env, []string{ 129 | // Set VISUAL because gbp dch has no flag to specify the editor. 130 | // Ideally we’d set this to /bin/true, but we need to filter the changelog because “gbp dch” generates an empty entry. 131 | fmt.Sprintf("VISUAL=%s -filter_changelog", self), 132 | }...) 133 | return cmd.Run() 134 | } 135 | 136 | func buildPackage() error { 137 | return newCommand("gbp", "buildpackage", 138 | // Tag debian/%(version)s after building successfully. 139 | "--git-tag", 140 | // Build in a separate directory to avoid modifying the git checkout. 141 | "--git-export-dir=../export", 142 | "--git-builder=sbuild -v -As --dist=unstable").Run() 143 | } 144 | 145 | // mergeAndBuild downloads the most recent patch in the specified bug 146 | // from the BTS, checks out the package’s packaging repository, merges 147 | // the patch and builds the package. 148 | func mergeAndBuild(url string) (string, error) { 149 | tempDir, err := ioutil.TempDir("", "mergebot-") 150 | if err != nil { 151 | return tempDir, err 152 | } 153 | newCommand = func(name string, arg ...string) *loggedexec.LoggedCmd { 154 | cmd := loggedexec.Command(name, arg...) 155 | cmd.LogDir = tempDir 156 | cmd.Logger = log.New(os.Stderr, "", log.LstdFlags) 157 | // TODO: copy passthroughEnv() from dh-make-golang/make.go 158 | for _, variable := range []string{"DEBFULLNAME", "DEBEMAIL", "SSH_AGENT_PID", "GPG_AGENT_INFO", "SSH_AUTH_SOCK"} { 159 | if value, ok := os.LookupEnv(variable); ok { 160 | cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", variable, value)) 161 | } 162 | } 163 | return cmd 164 | } 165 | 166 | patch, err := getMostRecentPatch(url, *bug) 167 | if err != nil { 168 | return tempDir, err 169 | } 170 | 171 | if err := ioutil.WriteFile(filepath.Join(tempDir, patchFileName), patch.Data, 0600); err != nil { 172 | return tempDir, err 173 | } 174 | 175 | scm, url, err := repositoryFor(*sourcePackage) 176 | if err != nil { 177 | return tempDir, err 178 | } 179 | if scm != "git" { 180 | return tempDir, fmt.Errorf("mergebot only supports git currently, but %q is using the SCM %q", url, scm) 181 | } 182 | 183 | checkoutDir := filepath.Join(tempDir, "repo") 184 | 185 | // Make every command run in checkoutDir by default from now on. 186 | previousNewCommand := newCommand 187 | newCommand = func(name string, arg ...string) *loggedexec.LoggedCmd { 188 | cmd := previousNewCommand(name, arg...) 189 | cmd.Dir = checkoutDir 190 | return cmd 191 | } 192 | 193 | if err := gitCheckout(checkoutDir, url); err != nil { 194 | return tempDir, err 195 | } 196 | 197 | // TODO: edge case: the user might supply a patch which touches changelog but doesn’t include Closes: #bugnumber. In that case, we should modify the changelog accordingly (e.g. using debchange --closes?) 198 | 199 | changelogPath := filepath.Join(checkoutDir, "debian", "changelog") 200 | oldChangelogSum, err := sha256of(changelogPath) 201 | if err != nil { 202 | return tempDir, err 203 | } 204 | 205 | if err := applyPatch(); err != nil { 206 | return tempDir, err 207 | } 208 | 209 | patchCommitMessage := fmt.Sprintf("Fix for “%s” (Closes: #%s)", patch.Subject, *bug) 210 | if err := gitCommit(patch.Author, patchCommitMessage); err != nil { 211 | return tempDir, err 212 | } 213 | 214 | newChangelogSum, err := sha256of(changelogPath) 215 | if err != nil { 216 | return tempDir, err 217 | } 218 | if newChangelogSum != oldChangelogSum { 219 | log.Printf("%q changed", changelogPath) // TODO: remove in case we can make releaseChangelog() always work 220 | } 221 | 222 | if err := releaseChangelog(); err != nil { 223 | return tempDir, err 224 | } 225 | 226 | if err := buildPackage(); err != nil { 227 | return tempDir, err 228 | } 229 | 230 | // TODO: run lintian, include result in report 231 | 232 | return tempDir, nil 233 | } 234 | 235 | func main() { 236 | flag.Parse() 237 | 238 | if *filterChangelogMode { 239 | if flag.NArg() != 1 { 240 | log.Fatalf("Syntax: %s -filter_changelog ", os.Args[0]) 241 | } 242 | 243 | if err := filterChangelog(flag.Arg(0)); err != nil { 244 | log.Fatal(err) 245 | } 246 | return 247 | } 248 | 249 | *bug = strings.TrimPrefix(*bug, "#") 250 | 251 | // TODO: infer sourcePackage from --bug 252 | log.Printf("will work on package %q, bug %q", *sourcePackage, *bug) 253 | 254 | tempDir, err := mergeAndBuild(soapAddress) 255 | if err != nil { 256 | log.Fatal(err) 257 | } 258 | 259 | log.Printf("Merge and build successful!") 260 | log.Printf("Please introspect the resulting Debian package and git repository, then push and upload:") 261 | log.Printf("cd %q", tempDir) 262 | log.Printf("(cd repo && git push)") 263 | log.Printf("(cd export && debsign *.changes && dput *.changes)") 264 | } 265 | -------------------------------------------------------------------------------- /mergebot_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/Debian/mergebot/loggedexec" 17 | ) 18 | 19 | var ( 20 | skipTestCleanup = flag.Bool("skip_test_cleanup", false, "Skip cleaning up the temporary directory in which the test case works for investigating what went wrong.") 21 | ) 22 | 23 | func init() { 24 | flag.Parse() 25 | if *filterChangelogMode { 26 | if flag.NArg() != 1 { 27 | log.Fatalf("Syntax: %s -filter_changelog ", os.Args[0]) 28 | } 29 | 30 | if err := filterChangelog(flag.Arg(0)); err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | os.Exit(0) 35 | } 36 | } 37 | 38 | func distributionIsUbuntu() bool { 39 | contents, err := ioutil.ReadFile("/etc/lsb-release") 40 | if err != nil { 41 | return false 42 | } 43 | return strings.Contains(string(contents), "DISTRIB_ID=Ubuntu") 44 | } 45 | 46 | func TestMergeAndBuild(t *testing.T) { 47 | os.Setenv("DEBFULLNAME", "Test Case") 48 | os.Setenv("DEBEMAIL", "test@case") 49 | 50 | flag.Set("source_package", "min") 51 | flag.Set("bug", "1") 52 | 53 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 54 | w.Header().Set("Content-Type", `multipart/related; type="text/xml"; start=""; boundary="_----------=_146851316918670990"`) 55 | http.ServeFile(w, r, "testdata/minimal.soap") 56 | })) 57 | defer ts.Close() 58 | 59 | tempDir, err := ioutil.TempDir("", "test-merge-and-build-") 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | if *skipTestCleanup { 64 | t.Logf("Not cleaning up temporary directory %q as -skip_test_cleanup was specified", tempDir) 65 | } else { 66 | defer os.RemoveAll(tempDir) 67 | } 68 | 69 | // To make mergeAndBuild() place its temporary directory inside the test’s 70 | os.Setenv("TMPDIR", tempDir) 71 | 72 | if err := exec.Command("cp", "-r", "testdata/minimal-debian-package", tempDir).Run(); err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | packageDir := filepath.Join(tempDir, "minimal-debian-package") 77 | 78 | // Initialize git repository for the packaging. 79 | for _, args := range [][]string{ 80 | []string{"init"}, 81 | []string{"add", "."}, 82 | []string{"config", "user.name", "Test Case"}, 83 | []string{"config", "user.email", "test@case"}, 84 | []string{"commit", "-a", "-m", "Initial commit"}, 85 | []string{"tag", "debian/1.0"}, 86 | []string{"config", "--local", "receive.denyCurrentBranch", "updateInstead"}, 87 | } { 88 | cmd := exec.Command("git", args...) 89 | cmd.Dir = packageDir 90 | if err := cmd.Run(); err != nil { 91 | t.Fatalf("git %v failed: %v", args, err) 92 | } 93 | } 94 | 95 | // divert debcheckout with a shell script 96 | debcheckoutDiversion := fmt.Sprintf( 97 | `#!/bin/sh 98 | echo "git\tfile://%s/.git" 99 | `, filepath.Join(tempDir, "minimal-debian-package")) 100 | if err := ioutil.WriteFile(filepath.Join(tempDir, "debcheckout"), []byte(debcheckoutDiversion), 0755); err != nil { 101 | t.Fatal(err) 102 | } 103 | os.Setenv("PATH", tempDir+":"+os.Getenv("PATH")) 104 | 105 | mergeTempDir, err := mergeAndBuild(ts.URL) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | cmd := loggedexec.Command("git", "push") 111 | cmd.LogDir = tempDir 112 | cmd.Dir = filepath.Join(mergeTempDir, "repo") 113 | if err := cmd.Run(); err != nil { 114 | t.Fatal(err) 115 | } 116 | 117 | version := "1.1" 118 | if distributionIsUbuntu() { 119 | version = "1.0ubuntu1" 120 | } 121 | 122 | cmd = loggedexec.Command("git", "log", "--format=%an %ae %s", "HEAD~2..") 123 | cmd.LogDir = tempDir 124 | cmd.Dir = packageDir 125 | output, err := cmd.Output() 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | if got, want := string(output), fmt.Sprintf(`Test Case test@case Update changelog for %s release 130 | Chris Lamb lamby@debian.org Fix for “wit: please make the build reproducible” (Closes: #1) 131 | `, version); got != want { 132 | t.Fatalf("Unexpected git history after push: got %q, want %q", got, want) 133 | } 134 | 135 | cmd = loggedexec.Command("git", "tag") 136 | cmd.LogDir = tempDir 137 | cmd.Dir = packageDir 138 | output, err = cmd.Output() 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | if got, want := string(output), fmt.Sprintf(`debian/1.0 143 | debian/%s 144 | `, version); got != want { 145 | t.Fatalf("Unexpected git tags after push: got %q, want %q", got, want) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /testdata/831331.patch: -------------------------------------------------------------------------------- 1 | --- a/debian/patches/0001-Reproducible-build.patch 1970-01-01 02:00:00.000000000 +0200 2 | --- b/debian/patches/0001-Reproducible-build.patch 2016-07-14 17:17:36.921795790 +0200 3 | @@ -0,0 +1,14 @@ 4 | +Author: Chris Lamb 5 | +Last-Update: 2016-07-14 6 | + 7 | +--- wit-2.31a.orig/setup.sh 8 | ++++ wit-2.31a/setup.sh 9 | +@@ -16,7 +16,7 @@ revision_num="${revision//[!0-9]/}" 10 | + revision_next=$revision_num 11 | + [[ $revision = $revision_num ]] || let revision_next++ 12 | + 13 | +-tim=($(date '+%s %Y-%m-%d %T')) 14 | ++tim=($(date --utc --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" '+%s %Y-%m-%d %T')) 15 | + defines= 16 | + 17 | + have_fuse=0 18 | --- a/debian/patches/series 2016-07-14 17:13:25.515286931 +0200 19 | --- b/debian/patches/series 2016-07-14 17:17:22.921655950 +0200 20 | @@ -1,3 +1,4 @@ 21 | use-libbz2-and-mhash.patch 22 | fix-usr-local.patch 23 | 0003-Don-t-link-wfuse-against-libdl.patch 24 | +0001-Reproducible-build.patch 25 | -------------------------------------------------------------------------------- /testdata/831331.soap: -------------------------------------------------------------------------------- 1 | This is a multi-part message in MIME format. 2 | 3 | --_----------=_146851316918670990 4 | Content-Transfer-Encoding: 7bit 5 | Content-Type: text/plain 6 | 7 | Source: wit 8 | Version: 2.31a-2 9 | Severity: wishlist 10 | Tags: patch 11 | User: reproducible-builds@lists.alioth.debian.org 12 | Usertags: timestamps 13 | X-Debbugs-Cc: reproducible-builds@lists.alioth.debian.org 14 | 15 | Hi, 16 | 17 | Whilst working on the "reproducible builds" effort [0], we noticed 18 | that wit could not be built reproducibly. 19 | 20 | Patch attached. It can probably be sent upstream. 21 | 22 | [0] https://wiki.debian.org/ReproducibleBuilds 23 | 24 | 25 | Regards, 26 | 27 | -- 28 | ,''`. 29 | : :' : Chris Lamb 30 | `. `'` lamby@debian.org / chris-lamb.co.uk 31 | `- 32 | 33 | --_----------=_146851316918670990 34 | Content-Disposition: attachment; filename="wit.diff.txt" 35 | Content-Id: <generated-529f9b51c7dd68b9f2a8dc1e37a29afa@messagingengine.com> 36 | Content-Transfer-Encoding: base64 37 | Content-Type: text/plain; charset="us-ascii"; name="wit.diff.txt" 38 | 39 | LS0tIGEvZGViaWFuL3BhdGNoZXMvMDAwMS1SZXByb2R1Y2libGUtYnVpbGQu 40 | cGF0Y2gJMTk3MC0wMS0wMSAwMjowMDowMC4wMDAwMDAwMDAgKzAyMDAKLS0t 41 | IGIvZGViaWFuL3BhdGNoZXMvMDAwMS1SZXByb2R1Y2libGUtYnVpbGQucGF0 42 | Y2gJMjAxNi0wNy0xNCAxNzoxNzozNi45MjE3OTU3OTAgKzAyMDAKQEAgLTAs 43 | MCArMSwxNCBAQAorQXV0aG9yOiBDaHJpcyBMYW1iIDxsYW1ieUBkZWJpYW4u 44 | b3JnPgorTGFzdC1VcGRhdGU6IDIwMTYtMDctMTQKKworLS0tIHdpdC0yLjMx 45 | YS5vcmlnL3NldHVwLnNoCisrKysgd2l0LTIuMzFhL3NldHVwLnNoCitAQCAt 46 | MTYsNyArMTYsNyBAQCByZXZpc2lvbl9udW09IiR7cmV2aXNpb24vL1shMC05 47 | XS99IgorIHJldmlzaW9uX25leHQ9JHJldmlzaW9uX251bQorIFtbICRyZXZp 48 | c2lvbiA9ICRyZXZpc2lvbl9udW0gXV0gfHwgbGV0IHJldmlzaW9uX25leHQr 49 | KworIAorLXRpbT0oJChkYXRlICcrJXMgJVktJW0tJWQgJVQnKSkKKyt0aW09 50 | KCQoZGF0ZSAtLXV0YyAtLWRhdGU9IkAke1NPVVJDRV9EQVRFX0VQT0NIOi0k 51 | KGRhdGUgKyVzKX0iICcrJXMgJVktJW0tJWQgJVQnKSkKKyBkZWZpbmVzPQor 52 | IAorIGhhdmVfZnVzZT0wCi0tLSBhL2RlYmlhbi9wYXRjaGVzL3Nlcmllcwky 53 | MDE2LTA3LTE0IDE3OjEzOjI1LjUxNTI4NjkzMSArMDIwMAotLS0gYi9kZWJp 54 | YW4vcGF0Y2hlcy9zZXJpZXMJMjAxNi0wNy0xNCAxNzoxNzoyMi45MjE2NTU5 55 | NTAgKzAyMDAKQEAgLTEsMyArMSw0IEBACiB1c2UtbGliYnoyLWFuZC1taGFz 56 | aC5wYXRjaAogZml4LXVzci1sb2NhbC5wYXRjaAogMDAwMy1Eb24tdC1saW5r 57 | LXdmdXNlLWFnYWluc3QtbGliZGwucGF0Y2gKKzAwMDEtUmVwcm9kdWNpYmxl 58 | LWJ1aWxkLnBhdGNoCg== 59 | 60 | --_----------=_146851316918670990--
Received: (at submit) by bugs.debian.org; 14 Jul 2016 16:19:30 +0000 61 | From lamby@debian.org Thu Jul 14 16:19:30 2016 62 | X-Spam-Checker-Version: SpamAssassin 3.4.0-bugs.debian.org_2005_01_02 63 | (2014-02-07) on buxtehude.debian.org 64 | X-Spam-Level: 65 | X-Spam-Status: No, score=-4.3 required=4.0 tests=BAYES_00,DKIM_SIGNED, 66 | DKIM_VALID,FROMDEVELOPER,MURPHY_DRUGS_REL8,RCVD_IN_DNSWL_LOW, 67 | RCVD_IN_MSPIKE_H3,RCVD_IN_MSPIKE_WL,URIBL_CNKR autolearn=ham 68 | autolearn_force=no version=3.4.0-bugs.debian.org_2005_01_02 69 | X-Spam-Bayes: score:0.0000 Tokens: new, 32; hammy, 150; neutral, 51; spammy, 70 | 0. spammytokens: hammytokens:0.000-+--xdebbugscc, 0.000-+--x-debbugs-cc, 71 | 0.000-+--UD:patch, 0.000-+--Usertags, 0.000-+--X-Debbugs-Cc 72 | Return-path: <lamby@debian.org> 73 | Received: from out5-smtp.messagingengine.com ([66.111.4.29]) 74 | by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) 75 | (Exim 4.84_2) 76 | (envelope-from <lamby@debian.org>) 77 | id 1bNjMI-0005sW-El 78 | for submit@bugs.debian.org; Thu, 14 Jul 2016 16:19:30 +0000 79 | Received: from compute7.internal (compute7.nyi.internal [10.202.2.47]) 80 | by mailout.nyi.internal (Postfix) with ESMTP id 433DB2053F 81 | for <submit@bugs.debian.org>; Thu, 14 Jul 2016 12:19:29 -0400 (EDT) 82 | Received: from web3 ([10.202.2.213]) 83 | by compute7.internal (MEProxy); Thu, 14 Jul 2016 12:19:29 -0400 84 | DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; d= 85 | messagingengine.com; h=content-transfer-encoding:content-type 86 | :date:from:message-id:mime-version:subject:to:x-sasl-enc 87 | :x-sasl-enc; s=smtpout; bh=miFAbaDyPFScCDa9IAUt1HwTX/U=; b=TiAMy 88 | 0xIuxZAdIIv+fGBeynuA8gIhHVzEAbGg1I9KYucUdau53lVUaiUU3kXPJxDMgNa3 89 | GyPBIli8fG6MH1qwRswGhepHcbbrruNLx7eSedljEatb2kkrKCmDzql+nzNlJP+9 90 | Nq9LtwY/fpuY9Y4+qa12dSj648Uzz3s1PwgygY= 91 | Received: by mailuser.nyi.internal (Postfix, from userid 99) 92 | id 1BADD16719; Thu, 14 Jul 2016 12:19:29 -0400 (EDT) 93 | Message-Id: <1468513169.1867099.666367833.09B46F8E@webmail.messagingengine.com> 94 | X-Sasl-Enc: acxzVuX/5nQVLFtZB1imxTMA9KkPvDWcA2ZjP254Ls5D 1468513169 95 | From: Chris Lamb <lamby@debian.org> 96 | To: submit@bugs.debian.org 97 | MIME-Version: 1.0 98 | Content-Transfer-Encoding: 7bit 99 | Content-Type: multipart/mixed; boundary="_----------=_146851316918670990"; 100 | charset="utf-8" 101 | X-Mailer: MessagingEngine.com Webmail Interface - ajax-bf4e2c8f 102 | Subject: wit: please make the build reproducible 103 | Date: Thu, 14 Jul 2016 18:19:29 +0200 104 | Delivered-To: submit@bugs.debian.org
5
-------------------------------------------------------------------------------- /testdata/minimal-debian-package/debian/changelog: -------------------------------------------------------------------------------- 1 | min (1.0) unstable; urgency=low 2 | 3 | * Min. 4 | 5 | -- Michael Stapelberg Sun, 17 Jul 2016 00:36:19 +0200 6 | -------------------------------------------------------------------------------- /testdata/minimal-debian-package/debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /testdata/minimal-debian-package/debian/control: -------------------------------------------------------------------------------- 1 | Source: min 2 | Priority: extra 3 | Section: devel 4 | Build-Depends: debhelper (>= 9) 5 | Maintainer: Michael Stapelberg 6 | 7 | Package: min 8 | Architecture: any 9 | Depends: ${misc:Depends} 10 | Description: min 11 | min 12 | -------------------------------------------------------------------------------- /testdata/minimal-debian-package/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | -------------------------------------------------------------------------------- /testdata/minimal.soap: -------------------------------------------------------------------------------- 1 | This is a multi-part message in MIME format. 2 | 3 | --_----------=_146851316918670990 4 | Content-Transfer-Encoding: 7bit 5 | Content-Type: text/plain 6 | 7 | Source: wit 8 | Version: 2.31a-2 9 | Severity: wishlist 10 | Tags: patch 11 | User: reproducible-builds@lists.alioth.debian.org 12 | Usertags: timestamps 13 | X-Debbugs-Cc: reproducible-builds@lists.alioth.debian.org 14 | 15 | Hi, 16 | 17 | Whilst working on the "reproducible builds" effort [0], we noticed 18 | that wit could not be built reproducibly. 19 | 20 | Patch attached. It can probably be sent upstream. 21 | 22 | [0] https://wiki.debian.org/ReproducibleBuilds 23 | 24 | 25 | Regards, 26 | 27 | -- 28 | ,''`. 29 | : :' : Chris Lamb 30 | `. `'` lamby@debian.org / chris-lamb.co.uk 31 | `- 32 | 33 | --_----------=_146851316918670990 34 | Content-Disposition: attachment; filename="wit.diff.txt" 35 | Content-Id: <generated-529f9b51c7dd68b9f2a8dc1e37a29afa@messagingengine.com> 36 | Content-Transfer-Encoding: base64 37 | Content-Type: text/plain; charset="us-ascii"; name="wit.diff.txt" 38 | 39 | ZGlmZiAtLWdpdCBpL2RlYmlhbi9jb250cm9sIHcvZGViaWFuL2NvbnRyb2wKaW5kZXggZmUzYjkw 40 | Yy4uZTY0ZDUyOSAxMDA2NDQKLS0tIGkvZGViaWFuL2NvbnRyb2wKKysrIHcvZGViaWFuL2NvbnRy 41 | b2wKQEAgLTMsNiArMyw3IEBAIFByaW9yaXR5OiBleHRyYQogU2VjdGlvbjogZGV2ZWwKIEJ1aWxk 42 | LURlcGVuZHM6IGRlYmhlbHBlciAoPj0gOSkKIE1haW50YWluZXI6IE1pY2hhZWwgU3RhcGVsYmVy 43 | ZyA8c3RhcGVsYmVyZ0BkZWJpYW4ub3JnPgorU3RhbmRhcmRzLVZlcnNpb246IDMuOS43CiAKIFBh 44 | Y2thZ2U6IG1pbgogQXJjaGl0ZWN0dXJlOiBhbnkK 45 | 46 | --_----------=_146851316918670990--
Received: (at submit) by bugs.debian.org; 14 Jul 2016 16:19:30 +0000 47 | From lamby@debian.org Thu Jul 14 16:19:30 2016 48 | X-Spam-Checker-Version: SpamAssassin 3.4.0-bugs.debian.org_2005_01_02 49 | (2014-02-07) on buxtehude.debian.org 50 | X-Spam-Level: 51 | X-Spam-Status: No, score=-4.3 required=4.0 tests=BAYES_00,DKIM_SIGNED, 52 | DKIM_VALID,FROMDEVELOPER,MURPHY_DRUGS_REL8,RCVD_IN_DNSWL_LOW, 53 | RCVD_IN_MSPIKE_H3,RCVD_IN_MSPIKE_WL,URIBL_CNKR autolearn=ham 54 | autolearn_force=no version=3.4.0-bugs.debian.org_2005_01_02 55 | X-Spam-Bayes: score:0.0000 Tokens: new, 32; hammy, 150; neutral, 51; spammy, 56 | 0. spammytokens: hammytokens:0.000-+--xdebbugscc, 0.000-+--x-debbugs-cc, 57 | 0.000-+--UD:patch, 0.000-+--Usertags, 0.000-+--X-Debbugs-Cc 58 | Return-path: <lamby@debian.org> 59 | Received: from out5-smtp.messagingengine.com ([66.111.4.29]) 60 | by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) 61 | (Exim 4.84_2) 62 | (envelope-from <lamby@debian.org>) 63 | id 1bNjMI-0005sW-El 64 | for submit@bugs.debian.org; Thu, 14 Jul 2016 16:19:30 +0000 65 | Received: from compute7.internal (compute7.nyi.internal [10.202.2.47]) 66 | by mailout.nyi.internal (Postfix) with ESMTP id 433DB2053F 67 | for <submit@bugs.debian.org>; Thu, 14 Jul 2016 12:19:29 -0400 (EDT) 68 | Received: from web3 ([10.202.2.213]) 69 | by compute7.internal (MEProxy); Thu, 14 Jul 2016 12:19:29 -0400 70 | DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; d= 71 | messagingengine.com; h=content-transfer-encoding:content-type 72 | :date:from:message-id:mime-version:subject:to:x-sasl-enc 73 | :x-sasl-enc; s=smtpout; bh=miFAbaDyPFScCDa9IAUt1HwTX/U=; b=TiAMy 74 | 0xIuxZAdIIv+fGBeynuA8gIhHVzEAbGg1I9KYucUdau53lVUaiUU3kXPJxDMgNa3 75 | GyPBIli8fG6MH1qwRswGhepHcbbrruNLx7eSedljEatb2kkrKCmDzql+nzNlJP+9 76 | Nq9LtwY/fpuY9Y4+qa12dSj648Uzz3s1PwgygY= 77 | Received: by mailuser.nyi.internal (Postfix, from userid 99) 78 | id 1BADD16719; Thu, 14 Jul 2016 12:19:29 -0400 (EDT) 79 | Message-Id: <1468513169.1867099.666367833.09B46F8E@webmail.messagingengine.com> 80 | X-Sasl-Enc: acxzVuX/5nQVLFtZB1imxTMA9KkPvDWcA2ZjP254Ls5D 1468513169 81 | From: Chris Lamb <lamby@debian.org> 82 | To: submit@bugs.debian.org 83 | MIME-Version: 1.0 84 | Content-Transfer-Encoding: 7bit 85 | Content-Type: multipart/mixed; boundary="_----------=_146851316918670990"; 86 | charset="utf-8" 87 | X-Mailer: MessagingEngine.com Webmail Interface - ajax-bf4e2c8f 88 | Subject: wit: please make the build reproducible 89 | Date: Thu, 14 Jul 2016 18:19:29 +0200 90 | Delivered-To: submit@bugs.debian.org
5
91 | -------------------------------------------------------------------------------- /travis/sbuild-key.pub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Debian/mergebot/f79670e3e967b471599fc90e22b1242e44e307cd/travis/sbuild-key.pub -------------------------------------------------------------------------------- /travis/sbuild-key.sec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Debian/mergebot/f79670e3e967b471599fc90e22b1242e44e307cd/travis/sbuild-key.sec --------------------------------------------------------------------------------