From 37f49ed934b085142f0bec840492d4ad3196126d Mon Sep 17 00:00:00 2001 From: Kelly Norton Date: Tue, 20 Jan 2015 22:48:03 -0500 Subject: [PATCH] Initial commit --- .gaan | 1 + .gitignore | 6 + LICENSE | 18 + README.md | 58 + Rakefile | 36 + config-example.json | 11 + hound.sublime-project | 16 + pub/assets/css/hound.css | 353 + pub/assets/css/octicons/LICENSE.txt | 9 + pub/assets/css/octicons/README.md | 1 + pub/assets/css/octicons/octicons-local.ttf | Bin 0 -> 52764 bytes pub/assets/css/octicons/octicons.css | 247 + pub/assets/css/octicons/octicons.eot | Bin 0 -> 31440 bytes pub/assets/css/octicons/octicons.less | 246 + pub/assets/css/octicons/octicons.svg | 198 + pub/assets/css/octicons/octicons.ttf | Bin 0 -> 31272 bytes pub/assets/css/octicons/octicons.woff | Bin 0 -> 17492 bytes .../css/octicons/sprockets-octicons.scss | 243 + pub/assets/images/busy.gif | Bin 0 -> 4178 bytes pub/assets/js/JSXTransformer-0.12.2.js | 15199 ++++++++++++++++ pub/assets/js/ReactZeroClipboard.js | 193 + pub/assets/js/excluded_files.js | 146 + pub/assets/js/hound.js | 777 + pub/assets/js/jquery-2.1.3.min.js | 4 + pub/assets/js/react-0.12.2.min.js | 16 + pub/excluded_files.tpl.html | 16 + pub/favicon.ico | Bin 0 -> 1150 bytes pub/index.tpl.html | 23 + src/ansi/ansi.go | 105 + src/ansi/ansi_test.go | 88 + src/ansi/tty.go | 21 + src/ansi/tty_darwin.go | 5 + src/ansi/tty_linux.go | 4 + src/code.google.com/p/codesearch/AUTHORS | 4 + src/code.google.com/p/codesearch/CONTRIBUTORS | 5 + src/code.google.com/p/codesearch/LICENSE | 27 + src/code.google.com/p/codesearch/README | 16 + .../p/codesearch/cmd/cgrep/cgrep.go | 77 + .../p/codesearch/cmd/cindex/cindex.go | 160 + .../p/codesearch/cmd/csearch/csearch.go | 147 + .../p/codesearch/index/merge.go | 344 + .../p/codesearch/index/merge_test.go | 102 + .../p/codesearch/index/mmap_bsd.go | 33 + .../p/codesearch/index/mmap_linux.go | 31 + .../p/codesearch/index/mmap_windows.go | 37 + .../p/codesearch/index/read.go | 455 + .../p/codesearch/index/read_test.go | 60 + .../p/codesearch/index/regexp.go | 872 + .../p/codesearch/index/regexp_test.go | 94 + .../p/codesearch/index/write.go | 687 + .../p/codesearch/index/write_test.go | 165 + .../p/codesearch/lib/README.template | 15 + src/code.google.com/p/codesearch/lib/buildall | 31 + src/code.google.com/p/codesearch/lib/setup | 23 + .../p/codesearch/lib/uploadall | 18 + src/code.google.com/p/codesearch/lib/version | 1 + .../p/codesearch/regexp/copy.go | 223 + .../p/codesearch/regexp/match.go | 473 + .../p/codesearch/regexp/regexp.go | 59 + .../p/codesearch/regexp/regexp_test.go | 219 + .../p/codesearch/regexp/utf.go | 268 + .../p/codesearch/sparse/set.go | 65 + src/hound/api/api.go | 213 + src/hound/client/ack.go | 114 + src/hound/client/client.go | 152 + src/hound/client/coalesce.go | 99 + src/hound/client/coalesce_test.go | 259 + src/hound/client/grep.go | 33 + src/hound/cmds/hound/main.go | 129 + src/hound/cmds/houndd/main.go | 312 + src/hound/config/config.go | 57 + src/hound/git/git.go | 80 + src/hound/index/grep.go | 251 + src/hound/index/grep_test.go | 293 + src/hound/index/index.go | 326 + src/hound/searcher/searcher.go | 186 + 76 files changed, 25255 insertions(+) create mode 100644 .gaan create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Rakefile create mode 100644 config-example.json create mode 100644 hound.sublime-project create mode 100644 pub/assets/css/hound.css create mode 100644 pub/assets/css/octicons/LICENSE.txt create mode 100644 pub/assets/css/octicons/README.md create mode 100644 pub/assets/css/octicons/octicons-local.ttf create mode 100644 pub/assets/css/octicons/octicons.css create mode 100644 pub/assets/css/octicons/octicons.eot create mode 100644 pub/assets/css/octicons/octicons.less create mode 100644 pub/assets/css/octicons/octicons.svg create mode 100644 pub/assets/css/octicons/octicons.ttf create mode 100644 pub/assets/css/octicons/octicons.woff create mode 100644 pub/assets/css/octicons/sprockets-octicons.scss create mode 100644 pub/assets/images/busy.gif create mode 100644 pub/assets/js/JSXTransformer-0.12.2.js create mode 100644 pub/assets/js/ReactZeroClipboard.js create mode 100644 pub/assets/js/excluded_files.js create mode 100644 pub/assets/js/hound.js create mode 100644 pub/assets/js/jquery-2.1.3.min.js create mode 100644 pub/assets/js/react-0.12.2.min.js create mode 100644 pub/excluded_files.tpl.html create mode 100644 pub/favicon.ico create mode 100644 pub/index.tpl.html create mode 100644 src/ansi/ansi.go create mode 100644 src/ansi/ansi_test.go create mode 100644 src/ansi/tty.go create mode 100644 src/ansi/tty_darwin.go create mode 100644 src/ansi/tty_linux.go create mode 100644 src/code.google.com/p/codesearch/AUTHORS create mode 100644 src/code.google.com/p/codesearch/CONTRIBUTORS create mode 100644 src/code.google.com/p/codesearch/LICENSE create mode 100644 src/code.google.com/p/codesearch/README create mode 100644 src/code.google.com/p/codesearch/cmd/cgrep/cgrep.go create mode 100644 src/code.google.com/p/codesearch/cmd/cindex/cindex.go create mode 100644 src/code.google.com/p/codesearch/cmd/csearch/csearch.go create mode 100644 src/code.google.com/p/codesearch/index/merge.go create mode 100644 src/code.google.com/p/codesearch/index/merge_test.go create mode 100644 src/code.google.com/p/codesearch/index/mmap_bsd.go create mode 100644 src/code.google.com/p/codesearch/index/mmap_linux.go create mode 100644 src/code.google.com/p/codesearch/index/mmap_windows.go create mode 100644 src/code.google.com/p/codesearch/index/read.go create mode 100644 src/code.google.com/p/codesearch/index/read_test.go create mode 100644 src/code.google.com/p/codesearch/index/regexp.go create mode 100644 src/code.google.com/p/codesearch/index/regexp_test.go create mode 100644 src/code.google.com/p/codesearch/index/write.go create mode 100644 src/code.google.com/p/codesearch/index/write_test.go create mode 100644 src/code.google.com/p/codesearch/lib/README.template create mode 100755 src/code.google.com/p/codesearch/lib/buildall create mode 100644 src/code.google.com/p/codesearch/lib/setup create mode 100644 src/code.google.com/p/codesearch/lib/uploadall create mode 100644 src/code.google.com/p/codesearch/lib/version create mode 100644 src/code.google.com/p/codesearch/regexp/copy.go create mode 100644 src/code.google.com/p/codesearch/regexp/match.go create mode 100644 src/code.google.com/p/codesearch/regexp/regexp.go create mode 100644 src/code.google.com/p/codesearch/regexp/regexp_test.go create mode 100644 src/code.google.com/p/codesearch/regexp/utf.go create mode 100644 src/code.google.com/p/codesearch/sparse/set.go create mode 100644 src/hound/api/api.go create mode 100644 src/hound/client/ack.go create mode 100644 src/hound/client/client.go create mode 100644 src/hound/client/coalesce.go create mode 100644 src/hound/client/coalesce_test.go create mode 100644 src/hound/client/grep.go create mode 100644 src/hound/cmds/hound/main.go create mode 100644 src/hound/cmds/houndd/main.go create mode 100644 src/hound/config/config.go create mode 100644 src/hound/git/git.go create mode 100644 src/hound/index/grep.go create mode 100644 src/hound/index/grep_test.go create mode 100644 src/hound/index/index.go create mode 100644 src/hound/searcher/searcher.go diff --git a/.gaan b/.gaan new file mode 100644 index 00000000..9c558e35 --- /dev/null +++ b/.gaan @@ -0,0 +1 @@ +. diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9168f343 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +pkg +bin +hound.sublime-workspace +data +.vagrant +pub/index.html diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..71f9d32e --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2014, Etsy, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..4a6d6b17 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Hound + +Hound is an extremely fast source code search engine. The core is based on this article (and code) from Russ Cox: +[Regular Expression Matching with a Trigram Index](http://swtch.com/~rsc/regexp/regexp4.html). Hound itself is a static +[React](http://facebook.github.io/react/) frontend that talks to a [Go](http://golang.org/) backend. The backend keeps an up-to-date index for each +repository and and answers searches through a minimal API. + +## Why Another Code Search Tool? + +We've used many similar tools in the past, and most of them are either too slow, too hard to configure, or require too much software to be installed. +Which brings us to... + +## Requirements + +### Hard Requirements +* Go 1.3+ + +### Optional, Recommended Software +* Rake (for building the binaries, not strictly required) +* nodejs (for the command line react-tools) + +Yup, that's it. You can proxy requests to the Go service through Apache/nginx/etc., but that's not required. + +## Hacking on Hound + +### Building + +``` +rake +``` + +This will build `./bin/houndd` which is the server binary and `./bin/hound` which is the command line client. + +### Running in development + +``` +./bin/houndd +``` + +This will start up the combined server and indexer. The first time you start the server, it will take a bit of time to initialize your `data` directory with the repository data. +You can access the web frontend at http://localhost:6080/ + +### Running in production + +``` +./bin/houndd --prod --addr=address:port +``` + +The will start up the combined server/indexer and build all static assets in production mode. The default addr is ":6080", and thus the `--addr` flag can be used to have the server listen on a different port. + +## Get in Touch + +IRC: #codeascraft on freenode + +Created at [Etsy](https://www.etsy.com) by: + +* [Kelly Norton](https://github.com/kellegous) +* [Jonathan Klein](https://github.com/jklein) diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..145b7a4c --- /dev/null +++ b/Rakefile @@ -0,0 +1,36 @@ +require 'fileutils' + +GOPATH=File.open('.gaan').map do |x| + File.absolute_path(File.join(File.dirname(__FILE__), x.strip)) +end.join(':') + +ENV.update({ + 'GOPATH' => GOPATH +}) + +file 'bin/hound' => FileList['src/hound/**/*', 'src/ansi/**/*'] do + host = ENV['HOST'] || 'localhost:6080' + args = ['go', 'build', '-o', 'bin/hound', '-ldflags', "-X main.defaultHost #{host}"] + FileList['src/hound/cmds/hound/*.go'] + sh *args +end + +file 'bin/houndd' => FileList['src/hound/**/*'] do + sh 'go', 'build', '-o', 'bin/houndd', 'src/hound/cmds/houndd/main.go' +end + +task :default => [ + 'bin/houndd', + 'bin/hound', +] + +task :clean do + FileUtils::rm_rf('bin') +end + +task :test do + pkgs = FileList['src/**/*_test.go'].map do |x| + File.dirname(x[4,x.length]) + end.uniq + args = ['go', 'test'].concat(pkgs) + sh *args +end diff --git a/config-example.json b/config-example.json new file mode 100644 index 00000000..608092b9 --- /dev/null +++ b/config-example.json @@ -0,0 +1,11 @@ +{ + "dbpath" : "data", + "repos" : { + "SomeRepo" : { + "url" : "https://www.github.com/YourOrganization/RepoOne.git" + }, + "AnotherRepo" : { + "url" : "https://www.github.com/YourOrganization/RepoTwo.git" + } + } +} diff --git a/hound.sublime-project b/hound.sublime-project new file mode 100644 index 00000000..f4a7bc11 --- /dev/null +++ b/hound.sublime-project @@ -0,0 +1,16 @@ +{ + "folders": + [ + { + "follow_symlinks": true, + "path": ".", + "folder_exclude_patterns": ["data"] + } + ], + "settings": + { + "tab_size": 2, + "convert_tabspaces_on_save": true, + "translate_tabs_to_spaces": true + } +} diff --git a/pub/assets/css/hound.css b/pub/assets/css/hound.css new file mode 100644 index 00000000..87ed8c54 --- /dev/null +++ b/pub/assets/css/hound.css @@ -0,0 +1,353 @@ + +body { + margin: 0; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: #333; + background-color: #fff; +} + +a { + color: #09f; +} + +.link-gray { color: #aaa } + +input { + font-family: inherit; + background-color: #fff; + border: 1px solid #ccc; +} + +input:focus { + outline: none; + border-color: #09f; + box-shadow: 0 0 5px rgba(0,153,255,.5); +} + +button { + font-family: inherit; + font-size: 14px; + text-align: center; + display: inline-block; + color: #fff; + background-color: #09f; + border: 0; + border-radius: 3px; + cursor: pointer; +} + +button:focus { + box-shadow: 0 0 6px rgba(0,153,255,.75); +} + +#root { + max-width: 960px; + margin: 0 auto; + padding: 0 20px 20px 20px; +} + + +#input { + margin-top: 40px; + margin-bottom: 40px; +} + +/* Search container */ +#ina { + display: table; + width: 100%; + position: relative; + z-index: 1; + box-shadow: 0 1px 6px rgba(0,0,0,0.2); +} + +/* Search input */ +#ina > input { + height: 55px; + padding: 0 7px; + border-right: 0; + border-radius: 3px 0 0 3px; + box-sizing: border-box; + color: #666; + font-size: 18px; + font-weight: 300; + display: table-cell; + width: 100%; + vertical-align: middle; +} + +#ina > .button-add-on { + display: table-cell; + vertical-align: middle; + width: 1%; +} + +/* Search submit button */ +#dodat { + border-radius: 0 3px 3px 0; + height: 55px; + width: 72px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAYAAAA6RwvCAAABVUlEQVRYw+2YQRGEMAxFkVAJlYAEJCABCUhAQiWshJWABCRUAg7+XsrMTkibNDBdDvtnek0fSfqToQPQPeE8AuK5IDkBGAC8AESctQEIAHxXKTUIgB7ACr0CAHcrCIAJNm0A+ltALkAc2jWlKoKkcnCKCdCR0i3p4lNmroJwPRGEgC6VhGo2gaTXUQUhwEQryKsmUOaZU40WEOoTk8Eb3iTGYgGhcgaQmcRYL4N0BjHl+RnIeDuI1iFJ8EBivC0gm7bRCsGj1ktKIIGx6pohNjNZ9RYQnxliTgHRM1a/XrH4kIEZhEzstT0mgeTmBpJZzemJjgk65naTO9aAPvOFtavAWMqKdjHyhcxIW9quGRVVO2uh/lTr8fWMqbEw1cvzV/AlXXico2c884IgwZhADMY2STBNQDQwzUAkmKYgAoxvClKAGZqDMDBb89IwRjlI0/f/W+ID3tQLQPib2qQAAAAASUVORK5CYII=); + background-repeat: no-repeat; + background-position: center center; + background-size: 17px; +} + +#inb { + box-sizing: border-box; + width: 95%; + margin: 0 auto; + padding: 4px 8px; + position: relative; + box-shadow: 0 1px 6px rgba(0,0,0,0.2); + color: #aaa; + font-size: 12px; + line-height: 24px; + cursor: pointer; + background-color: #fff; +} + +#adv { + box-sizing: border-box; + overflow: hidden; + height: 0; + padding: 0; + transition: height, padding 0.1s ease-in-out; +} + +/* Media object clearfix */ +#adv > .field { + overflow: hidden; +} + +#adv > .field > label { + /* Media object left */ + float: left; + width: 90px; + color: #999; +} + +#adv > .field > .field-input { + /* New block formatting context for media object */ + overflow: hidden; +} + +#adv > .field input[type=text] { + line-height: 24px; + box-sizing: border-box; + width: 100%; + padding: 0 10px; + color: #666; +} + +.multiselect { + box-sizing: border-box; + width: 100%; + margin-top:5px; + padding:5px; + border: 1px solid #ccc; + color: #666; +} + +#inb > .ban { + transition: max-height, opacity 0.1s ease-in-out; + opacity: 1; + overflow: hidden; +} + +#inb > .ban > em { + font-style: normal; + color: #aaa; +} + +#input > .stats { + width: 95%; + font-size: 12px; + padding: 4px 0; + margin: 0 auto; + color: #aaa; +} + +/* Clearfix .stats */ +.stats:before, +.stats:after { + content: " "; + display: table; +} +.stats:after { + clear: both; +} + +.stats-left { float: left } +.stats-right { float: right } + +#input > .stats .val, +#input > .stats a { + display: inline-block; + margin: 5px; +} + +#no-result { + margin: 0 auto; + font-size: 24px; + text-align: center; + margin-top: 100px; + color: #999; + text-shadow: 1px 1px 0 #fff; +} + +#no-result > div { + font-size: 12px; +} + +#no-result.error { + color: #8a6d3b; + background-color: #fcf8e3; + border: 1px solid #faebcc; + padding: 10px 0; + border-radius: 3px; +} + +#no-result.error > strong { + margin-right: 10px; +} + +.repo { + margin-bottom: 100px; +} + +.repo > .title { + color: #666; + font-size: 24px; + padding-bottom: 5px; +} + +.repo > .title > .name { + vertical-align: top; +} + +.repo > .title > .octicon-repo { + color: #bbb; + margin-right: 10px; +} + +.files > .moar { + height: 55px; + vertical-align: top; + width: 100%; +} + +.file { + margin: 10px 0 20px; + border: 1px solid #d8d8d8; + border-radius: 3px; +} + +.file > .title { + padding: 10px 10px 10px 20px; + display: block; + line-height: 30px; + background-color: #f5f5f5; + border-bottom: 1px solid #d8d8d8; +} + +.title a { + color: #666; +} + +.copy-path-container { + float: right; + cursor: pointer; +} + +.copy-path-btn { + padding: 6px 10px; + border-radius: 3px; + border: none; + color: #999; + background-color: transparent; + text-shadow: 1px 1px 0 #fff; +} + +.zeroclipboard-is-hover .copy-path-btn { + color: #333; +} + +.zeroclipboard-is-active .copy-path-btn { + color: #999; +} + +.file-body { + /* Allow horizontal scrolling in code, similar to github.com */ + overflow: auto; +} + +.match > .line { + white-space: pre; +} + +.match > .line > .lnum { + font-family: 'Source Code Pro', monospace; + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + width: 40px; + text-align: right; + padding: 3px 5px 3px 0; + border-right: 1px solid #eee; + display: inline-block; + font-size: 14px; + color: #aaa; +} + +.match > .line > .lnum:hover { + text-decoration: underline; + background-color: #eee; +} + +.match > .line > .lval { + font-family: 'Source Code Pro', monospace; + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + padding: 3px 0 3px 5px; + font-size: 14px; + white-space: pre; +} + +.match > .line > .lval > em { + font-style: normal; + font-weight: bold; + color: #333; + background-color: rgba(255,255,140,0.5); +} + +.table-container table { + width:100%; + border-collapse: collapse; +} + +.table-container th { + background-color: #eee; + padding: 10px; + text-align: left; +} + +.table-container .list td { + padding:5px; + border: 1px solid #eee +} + +.table-container .list .reason { + white-space: nowrap; +} + +.repo-button { + margin:0 5px 5px 0; +} + +.repo-button.selected { + background-color: gray; +} + +#excluded_container { + margin-top: 40px; + margin-bottom: 40px; +} + diff --git a/pub/assets/css/octicons/LICENSE.txt b/pub/assets/css/octicons/LICENSE.txt new file mode 100644 index 00000000..259b43d1 --- /dev/null +++ b/pub/assets/css/octicons/LICENSE.txt @@ -0,0 +1,9 @@ +(c) 2012-2014 GitHub + +When using the GitHub logos, be sure to follow the GitHub logo guidelines (https://github.com/logos) + +Font License: SIL OFL 1.1 (http://scripts.sil.org/OFL) +Applies to all font files + +Code License: MIT (http://choosealicense.com/licenses/mit/) +Applies to all other files diff --git a/pub/assets/css/octicons/README.md b/pub/assets/css/octicons/README.md new file mode 100644 index 00000000..10070733 --- /dev/null +++ b/pub/assets/css/octicons/README.md @@ -0,0 +1 @@ +If you intend to install Octicons locally, install `octicons-local.ttf`. It should appear as “github-octicons” in your font list. It is specially designed not to conflict with GitHub's web fonts. diff --git a/pub/assets/css/octicons/octicons-local.ttf b/pub/assets/css/octicons/octicons-local.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6baeb94f521a19dece4d94774b5e70e28b8e5efb GIT binary patch literal 52764 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 XTNld2$Q}M7uNceEo&lafx%>YBCsjk5 literal 0 HcmV?d00001 diff --git a/pub/assets/css/octicons/octicons.css b/pub/assets/css/octicons/octicons.css new file mode 100644 index 00000000..282b8651 --- /dev/null +++ b/pub/assets/css/octicons/octicons.css @@ -0,0 +1,247 @@ +@font-face { + font-family: 'octicons'; + src: url('octicons.eot?#iefix') format('embedded-opentype'), + url('octicons.woff') format('woff'), + url('octicons.ttf') format('truetype'), + url('octicons.svg#octicons') format('svg'); + font-weight: normal; + font-style: normal; +} + +/* + +.octicon is optimized for 16px. +.mega-octicon is optimized for 32px but can be used larger. + +*/ +.octicon { + font: normal normal 16px octicons; + line-height: 1; + display: inline-block; + text-decoration: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; +} +.mega-octicon { + font: normal normal 32px octicons; + line-height: 1; + display: inline-block; + text-decoration: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; +} + +.octicon-alert:before { content: '\f02d'} /*  */ +.octicon-alignment-align:before { content: '\f08a'} /*  */ +.octicon-alignment-aligned-to:before { content: '\f08e'} /*  */ +.octicon-alignment-unalign:before { content: '\f08b'} /*  */ +.octicon-arrow-down:before { content: '\f03f'} /*  */ +.octicon-arrow-left:before { content: '\f040'} /*  */ +.octicon-arrow-right:before { content: '\f03e'} /*  */ +.octicon-arrow-small-down:before { content: '\f0a0'} /*  */ +.octicon-arrow-small-left:before { content: '\f0a1'} /*  */ +.octicon-arrow-small-right:before { content: '\f071'} /*  */ +.octicon-arrow-small-up:before { content: '\f09f'} /*  */ +.octicon-arrow-up:before { content: '\f03d'} /*  */ +.octicon-beer:before { content: '\f069'} /*  */ +.octicon-book:before { content: '\f007'} /*  */ +.octicon-bookmark:before { content: '\f07b'} /*  */ +.octicon-briefcase:before { content: '\f0d3'} /*  */ +.octicon-broadcast:before { content: '\f048'} /*  */ +.octicon-browser:before { content: '\f0c5'} /*  */ +.octicon-bug:before { content: '\f091'} /*  */ +.octicon-calendar:before { content: '\f068'} /*  */ +.octicon-check:before { content: '\f03a'} /*  */ +.octicon-checklist:before { content: '\f076'} /*  */ +.octicon-chevron-down:before { content: '\f0a3'} /*  */ +.octicon-chevron-left:before { content: '\f0a4'} /*  */ +.octicon-chevron-right:before { content: '\f078'} /*  */ +.octicon-chevron-up:before { content: '\f0a2'} /*  */ +.octicon-circle-slash:before { content: '\f084'} /*  */ +.octicon-circuit-board:before { content: '\f0d6'} /*  */ +.octicon-clippy:before { content: '\f035'} /*  */ +.octicon-clock:before { content: '\f046'} /*  */ +.octicon-cloud-download:before { content: '\f00b'} /*  */ +.octicon-cloud-upload:before { content: '\f00c'} /*  */ +.octicon-code:before { content: '\f05f'} /*  */ +.octicon-color-mode:before { content: '\f065'} /*  */ +.octicon-comment-add:before, +.octicon-comment:before { content: '\f02b'} /*  */ +.octicon-comment-discussion:before { content: '\f04f'} /*  */ +.octicon-credit-card:before { content: '\f045'} /*  */ +.octicon-dash:before { content: '\f0ca'} /*  */ +.octicon-dashboard:before { content: '\f07d'} /*  */ +.octicon-database:before { content: '\f096'} /*  */ +.octicon-device-camera:before { content: '\f056'} /*  */ +.octicon-device-camera-video:before { content: '\f057'} /*  */ +.octicon-device-desktop:before { content: '\f27c'} /*  */ +.octicon-device-mobile:before { content: '\f038'} /*  */ +.octicon-diff:before { content: '\f04d'} /*  */ +.octicon-diff-added:before { content: '\f06b'} /*  */ +.octicon-diff-ignored:before { content: '\f099'} /*  */ +.octicon-diff-modified:before { content: '\f06d'} /*  */ +.octicon-diff-removed:before { content: '\f06c'} /*  */ +.octicon-diff-renamed:before { content: '\f06e'} /*  */ +.octicon-ellipsis:before { content: '\f09a'} /*  */ +.octicon-eye-unwatch:before, +.octicon-eye-watch:before, +.octicon-eye:before { content: '\f04e'} /*  */ +.octicon-file-binary:before { content: '\f094'} /*  */ +.octicon-file-code:before { content: '\f010'} /*  */ +.octicon-file-directory:before { content: '\f016'} /*  */ +.octicon-file-media:before { content: '\f012'} /*  */ +.octicon-file-pdf:before { content: '\f014'} /*  */ +.octicon-file-submodule:before { content: '\f017'} /*  */ +.octicon-file-symlink-directory:before { content: '\f0b1'} /*  */ +.octicon-file-symlink-file:before { content: '\f0b0'} /*  */ +.octicon-file-text:before { content: '\f011'} /*  */ +.octicon-file-zip:before { content: '\f013'} /*  */ +.octicon-flame:before { content: '\f0d2'} /*  */ +.octicon-fold:before { content: '\f0cc'} /*  */ +.octicon-gear:before { content: '\f02f'} /*  */ +.octicon-gift:before { content: '\f042'} /*  */ +.octicon-gist:before { content: '\f00e'} /*  */ +.octicon-gist-secret:before { content: '\f08c'} /*  */ +.octicon-git-branch-create:before, +.octicon-git-branch-delete:before, +.octicon-git-branch:before { content: '\f020'} /*  */ +.octicon-git-commit:before { content: '\f01f'} /*  */ +.octicon-git-compare:before { content: '\f0ac'} /*  */ +.octicon-git-merge:before { content: '\f023'} /*  */ +.octicon-git-pull-request-abandoned:before, +.octicon-git-pull-request:before { content: '\f009'} /*  */ +.octicon-globe:before { content: '\f0b6'} /*  */ +.octicon-graph:before { content: '\f043'} /*  */ +.octicon-heart:before { content: '\2665'} /* ♥ */ +.octicon-history:before { content: '\f07e'} /*  */ +.octicon-home:before { content: '\f08d'} /*  */ +.octicon-horizontal-rule:before { content: '\f070'} /*  */ +.octicon-hourglass:before { content: '\f09e'} /*  */ +.octicon-hubot:before { content: '\f09d'} /*  */ +.octicon-inbox:before { content: '\f0cf'} /*  */ +.octicon-info:before { content: '\f059'} /*  */ +.octicon-issue-closed:before { content: '\f028'} /*  */ +.octicon-issue-opened:before { content: '\f026'} /*  */ +.octicon-issue-reopened:before { content: '\f027'} /*  */ +.octicon-jersey:before { content: '\f019'} /*  */ +.octicon-jump-down:before { content: '\f072'} /*  */ +.octicon-jump-left:before { content: '\f0a5'} /*  */ +.octicon-jump-right:before { content: '\f0a6'} /*  */ +.octicon-jump-up:before { content: '\f073'} /*  */ +.octicon-key:before { content: '\f049'} /*  */ +.octicon-keyboard:before { content: '\f00d'} /*  */ +.octicon-law:before { content: '\f0d8'} /* */ +.octicon-light-bulb:before { content: '\f000'} /*  */ +.octicon-link:before { content: '\f05c'} /*  */ +.octicon-link-external:before { content: '\f07f'} /*  */ +.octicon-list-ordered:before { content: '\f062'} /*  */ +.octicon-list-unordered:before { content: '\f061'} /*  */ +.octicon-location:before { content: '\f060'} /*  */ +.octicon-gist-private:before, +.octicon-mirror-private:before, +.octicon-git-fork-private:before, +.octicon-lock:before { content: '\f06a'} /*  */ +.octicon-logo-github:before { content: '\f092'} /*  */ +.octicon-mail:before { content: '\f03b'} /*  */ +.octicon-mail-read:before { content: '\f03c'} /*  */ +.octicon-mail-reply:before { content: '\f051'} /*  */ +.octicon-mark-github:before { content: '\f00a'} /*  */ +.octicon-markdown:before { content: '\f0c9'} /*  */ +.octicon-megaphone:before { content: '\f077'} /*  */ +.octicon-mention:before { content: '\f0be'} /*  */ +.octicon-microscope:before { content: '\f089'} /*  */ +.octicon-milestone:before { content: '\f075'} /*  */ +.octicon-mirror-public:before, +.octicon-mirror:before { content: '\f024'} /*  */ +.octicon-mortar-board:before { content: '\f0d7'} /* */ +.octicon-move-down:before { content: '\f0a8'} /*  */ +.octicon-move-left:before { content: '\f074'} /*  */ +.octicon-move-right:before { content: '\f0a9'} /*  */ +.octicon-move-up:before { content: '\f0a7'} /*  */ +.octicon-mute:before { content: '\f080'} /*  */ +.octicon-no-newline:before { content: '\f09c'} /*  */ +.octicon-octoface:before { content: '\f008'} /*  */ +.octicon-organization:before { content: '\f037'} /*  */ +.octicon-package:before { content: '\f0c4'} /*  */ +.octicon-paintcan:before { content: '\f0d1'} /*  */ +.octicon-pencil:before { content: '\f058'} /*  */ +.octicon-person-add:before, +.octicon-person-follow:before, +.octicon-person:before { content: '\f018'} /*  */ +.octicon-pin:before { content: '\f041'} /*  */ +.octicon-playback-fast-forward:before { content: '\f0bd'} /*  */ +.octicon-playback-pause:before { content: '\f0bb'} /*  */ +.octicon-playback-play:before { content: '\f0bf'} /*  */ +.octicon-playback-rewind:before { content: '\f0bc'} /*  */ +.octicon-plug:before { content: '\f0d4'} /*  */ +.octicon-repo-create:before, +.octicon-gist-new:before, +.octicon-file-directory-create:before, +.octicon-file-add:before, +.octicon-plus:before { content: '\f05d'} /*  */ +.octicon-podium:before { content: '\f0af'} /*  */ +.octicon-primitive-dot:before { content: '\f052'} /*  */ +.octicon-primitive-square:before { content: '\f053'} /*  */ +.octicon-pulse:before { content: '\f085'} /*  */ +.octicon-puzzle:before { content: '\f0c0'} /*  */ +.octicon-question:before { content: '\f02c'} /*  */ +.octicon-quote:before { content: '\f063'} /*  */ +.octicon-radio-tower:before { content: '\f030'} /*  */ +.octicon-repo-delete:before, +.octicon-repo:before { content: '\f001'} /*  */ +.octicon-repo-clone:before { content: '\f04c'} /*  */ +.octicon-repo-force-push:before { content: '\f04a'} /*  */ +.octicon-gist-fork:before, +.octicon-repo-forked:before { content: '\f002'} /*  */ +.octicon-repo-pull:before { content: '\f006'} /*  */ +.octicon-repo-push:before { content: '\f005'} /*  */ +.octicon-rocket:before { content: '\f033'} /*  */ +.octicon-rss:before { content: '\f034'} /*  */ +.octicon-ruby:before { content: '\f047'} /*  */ +.octicon-screen-full:before { content: '\f066'} /*  */ +.octicon-screen-normal:before { content: '\f067'} /*  */ +.octicon-search-save:before, +.octicon-search:before { content: '\f02e'} /*  */ +.octicon-server:before { content: '\f097'} /*  */ +.octicon-settings:before { content: '\f07c'} /*  */ +.octicon-log-in:before, +.octicon-sign-in:before { content: '\f036'} /*  */ +.octicon-log-out:before, +.octicon-sign-out:before { content: '\f032'} /*  */ +.octicon-split:before { content: '\f0c6'} /*  */ +.octicon-squirrel:before { content: '\f0b2'} /*  */ +.octicon-star-add:before, +.octicon-star-delete:before, +.octicon-star:before { content: '\f02a'} /*  */ +.octicon-steps:before { content: '\f0c7'} /*  */ +.octicon-stop:before { content: '\f08f'} /*  */ +.octicon-repo-sync:before, +.octicon-sync:before { content: '\f087'} /*  */ +.octicon-tag-remove:before, +.octicon-tag-add:before, +.octicon-tag:before { content: '\f015'} /*  */ +.octicon-telescope:before { content: '\f088'} /*  */ +.octicon-terminal:before { content: '\f0c8'} /*  */ +.octicon-three-bars:before { content: '\f05e'} /*  */ +.octicon-tools:before { content: '\f031'} /*  */ +.octicon-trashcan:before { content: '\f0d0'} /*  */ +.octicon-triangle-down:before { content: '\f05b'} /*  */ +.octicon-triangle-left:before { content: '\f044'} /*  */ +.octicon-triangle-right:before { content: '\f05a'} /*  */ +.octicon-triangle-up:before { content: '\f0aa'} /*  */ +.octicon-unfold:before { content: '\f039'} /*  */ +.octicon-unmute:before { content: '\f0ba'} /*  */ +.octicon-versions:before { content: '\f064'} /*  */ +.octicon-remove-close:before, +.octicon-x:before { content: '\f081'} /*  */ +.octicon-zap:before { content: '\26A1'} /* ⚡ */ diff --git a/pub/assets/css/octicons/octicons.eot b/pub/assets/css/octicons/octicons.eot new file mode 100644 index 0000000000000000000000000000000000000000..29e17a6e0de0f07330bd474d0971eaae813e0eeb GIT binary patch literal 31440 zcmdtLd3{hU7=nmgLg{O%sRQkY8`o)FsPJ+VS!IKJSra z&p?}gZh!ZmD@*6R+j*Dgecoq#pZEB$&q-1{ek92fe`Lw7hyYS95Qh$4WyM#oA{kL5 zbo!Rn)N-gwDoP{LoHUMWQJRuwk>QuFkqXk7G%rm^!_o{=BGPq8nL+NjC>4|Xq#kKK za^CEDBy*!~NtYBUcIa?#=FaaO9z_nm1)bM2CgJWOqlcb3nNwP0Y7iZ`G?#g$e>}{WuB5y?eu=$sg$0joW>ZN-`Px!v_FDqL->y$mn zaF(VAT|n$<>17EmDbzRM`XSSsp5zUYUBdVtLwCZm+EqYTo;b4cfVdAyah z$4m7XrE(}E>O-9G@;^utV{o~xa{u|x`JycD+@JEV+y?if94_BQoq$!PeLTzIN?iyG zJm7TxX;yq~zNiD`%Fk8sru+=<B1OZKIzrIw|{QfjGV>EP1UOV=$;FTHi?o~8Si-isQ3Z|Q?ekD-P? zMh$w%@f&jSAx_-WuL zf&UTs$H4y=_;KJzfmZ|H4}3T9_kouKFZr|9=(rdE`yW#O_n-a0>d7hTA?ah(On1;v z<(={;lxvhP+ceuB+1u!(l)vtrzU@SNg+#7s% zaH%>~Jy89D>KChj6}mI@Y)w_o$7)^;4}{N!9|}Je{`1I&$g5FX^v39^=$YtuqCc%w zYujp%)ZSJ5)!LudMd~{0PSky%zP0{b{R<6|hU*%hXn3u$zOk!OYdq38(fGTKKW)0M z=~UChP2Xug(`+=q+Tv_!ZMm*xuH|#Fjj?yfo{qf|_s4VbTjCG5?oXUaJlodR_H0s0 zW|C);zfR4h-ky3m^|92SrCvxarhb)nr(4sT(?`=M)33DGx9@C!XZ!0N10C<~c&y{; zj=$&}=-l3Qs_Tia#cqH1k?wbPf4TcxJ>H(Ko+owX6!s^o}NMS(5@pZMyi ziR5$D`4p(hE9Up@it%WuicalOLP=_FvZvaUM{8|C?~M=GsG&}EhIa1Tde7EYPX{^Mo~AZT}qyOb**l7>_W%R2KU~a z;_3|sdtFLyue)Jq$AvM~+uQblC#dAe_S7pB2a|YxWl{0V3sMj=Lt8G5A)vR`RgI8N z%|#!L9y>Nl*40;xz4pD$yKc8Rr=9rPZr`=}dnFiYvJM!5Mo~OOaYfcwBxBhi$v`a# zm>SA~_^qH6=xDvvCheBqg3=ar073vV+7AHYn-y?WYSd0@O>Log9)-e*Y(ATI@LxEJ z2s+tATo8}dsE%|tnvdqgvULA53hmrPZ4LP94Q+()rk$ib`YgJ@)-FxW%XOS}-N?;>3r8}w2Ynx^Xw z8@nQ{*Y%+e)YNy~6{ReQdKfQ83%DouW7@P<$wg}g&3WO6Y~k(i@F(dTX{Ck;w_(OU~4Y9i4v;JlR> z_UJXoDePzT2eRh}Qt2#DNV1t~)YjBM4v!7wGLafJR89d;+ndktr9_iqG$qPmk}eG| z>-CAmKq5i)*8Mf>CL4GX&$Mj}-uj*I+#1~2R*64mzBaiwK1ubD1vicm*D{c}_|8&5 zIadml$7#)t3R%@JZNxa0{viGcQx|k~&!m+6l76_1Wb$WnS)qb9jWkbVbT4}D(VS!764yjTCib+C)X^x|h?HQe2UA zPy&)Db=A3{Kw#+H+s@U_?*sky2Z@4y+XFZ5JyUzmJQpq8e{1!SF+@i7t@jtA2&gK$EkGcFn!F*vE!3f<$Eht8Y9l#Qzx*e@HiueM4ftU3Rby(}eDePK%<^A&s{&s7 zLR~ywXYTR_s=V|?4ljrb?iSbcXreA=fDjG@?sC@%XUC=E2VQPfCwP< zP*jb84TU4Ne54HvmDE&vAjfbO;KUjR6#|I2(m*O%2`Qg)d7J6<%=*H~!upxh)a-So zyKGeZ!yi(;t;_6s@+DhqY~Z7#qaPiJwc1`XzZmcAjN?aqiO0G+<&LV`4%_|veNLxu zzu$iNwyKWP+g|^Ks(#}2ZKs!4)O`;pcQkgoY&KVC4R0gbHmu2&5aSHE z3it-o(IzP+Y0^GmjFcSMf_YCTTKlt(XttlRVht_Ape^FV^c6!}o0kSsVWBAHB9(w_ zC^H8(xMjo8ut+OZ>E?#Ewp?4=@{a`Rsw6jwkd@b9-KMG@`KJRjiVkXk=`yE+SCom= z6(1(9BiE)3G7Y;TXx_XgGe}R0*v02crR4LafJ7Mk|E=2 zq%0{%Zza0*sDfIML9WK6eVenVwUMjgheJ1=k>xWtLqrpCTT9ZmO=)cw#h*0|!vGHR zSPd~&RgBdkcuna4k0F;R@qc~5;9{mh8ol{|yA%JZ0b{I?CQD;wxyK^~q>xmHwHud` z(mJVo4fooGSue5Mbcq`&5O|XPL9koSvE#@5%FY}YKP4CsDl8Boou6Pv%{)xjA&FJK zq+uN3rJ5-*qtYRaB5Y{*U=Fm4x}t-@f^%^he8HX#gbm#^AQNf^ZaQ;Bh-|z7)HHZZN6XB9>UlK0HPFISm1}mb3@DM3mU)-k|EMXGGVB) zv8*eS!F`72Ea)g0Fsyez%u^WiA`#?=HT44iNCT|Ol`k8z#9~6}$B?VE01=Wu zXa_(kwPOQp;@m>H*U6x@OnEh9AzSObKbU5TXSET%w6Lt@C9b-ztTu&twHEE#E9MJr z!|qraqlJ4lCCuxQGUy59xP0lycvJ3KKV+x=Xc8$w{KAfKJey8r6Rc^FH7;DK3<-!c z2BQ|I@eRm=n=Zaac0v2wjf&U>dNl2G9atiGWanASMvm4)|GcVJUcwD^G|=!=ea0GX8eV<-A){H?XuDjF(hJq|>EGrsVo`zYBLPF=U0B9Hoy0}${ z<>jRt?PTGK)jtdoiReG0ES3=d!VKxs$jGobr^OK0tl9m#ZpFx2&u*yOSVmC2S?`u` zmvxJKi)+ssvak(det1QKge`(AC=MD8GEh?eag!u3I-q)@06T}y&;RCr2DZB`Nm^Y9KmRRKA2WO8j@?mvSIWM^%}Ny#~6sR0bm|m zXIqBeb6vNh$vbbkWv9%Ovlxr%u~-a*V4)mRFgdBZ3G>(9Wqynw*SzGMxTsZC(c*gJ z!hzdt^6Y_keYku1+p-ojjaZEM1}d{Qg6S&bxU7MiGmcwx1+FNjE8m~BBc(iJ@}EZU zJ$CHg=r_Jmx|`aX*&zMMt+IUUk@C%018&CgmAhf%kpRO@(oX40w1vrLBLHsD#Masc zR;BF}+GdImRN!@2F%E>v!Z$i>(EwQz;rrRkE? zO$9586)HsQ>dZlz8mV`!fW1cC9!OM2Z66q|wz3CHg7z{{P6+6imbzu;dB)~(iX0@0G(isLNL2T zllIYLL6|he>@0ut(&7Hdijv;eMDkt7t20g3B(hd<6Gl~u{dnh9M9o&6=C;zXDyoH9 z(J?SUmcy||T%cfBG>|y5@o*lNg%E*!2bCmvqCH43j}U?P5#(Z12Md((r$H{VmYpt3 zh5-Qy+K*ux%#Btgh#yd=Gy^mYG5!g92_DV3#5ATsYO!$1!dRi0RWLRUZk&ZQNXE0v zcKh`-ucqmuE7UPh>64 z5lTcfn_#gZ#A_XSnC*19pK%zSQyu-`tt6jsAI`CrvHnL(CQSt|{pudsIo<6-76{e&2oO*I(O2FamBOV;x1!e_Z!B=HEQ> z2=Q()Kc@+v2v&^ME3BM(;7{4cDJ|=LSIV#=Yarkng7QJx!0I>ko7!!RDe|FSLJ`!( zw8z3v1+&+R)hMx~7DA<1W`Kwl09+I{&`Z&r1VcNPTm_BDfB?ptIi)fwLZdop1eAR? z#oHwdFJB1< zZ4L&TTYG`QfPsvPQDD(MyhW zzL&Ctl+H&7slSCF=(PYhj9tfM`Pi;qV3Nm3YTmT>%%1j>UAyP@ZfLsmtxX%(M(I^i z&?>&XrpWGfmpp+edml(EI@U=Sr0BHNFYS>I!+qmMIBrM)Lpn8(&kdwnAxVQnMKa22 z5Sh$!kj?URc_~o}B59Nj)iBLPGI=(IQD19X>CeZb{fg>{CLB<~LZVQ(X0=c{)k^(d zZ%d1}F6L{%#}|9AFYfK`?(Xu%>%8$;EbgVDu1!Rny2O?KrjlM4_jPr5o4xJHZye7< z1UmkWwsuR4IOv5fa7~r;d0*TYzpmBWTIX%ry?Yxmoj(n&lruA+8Ozyr$#+byQlF4tK^;z}L8Y|L#Vwq^$gA zWe{-P20RlhjUt#!=(4G3u19u61F*(K04Pj+GXd`a&QO}76d+7H?k!awj9px+!B9Ly$3X86VfOG6#nbIZtA>z{CYx1~>7JEFRW!~t!NKbfd_e;*WvQC?B6hKaWQVSgiNL~Q z=86Fb9SGfK!ViTzvx&AUnPxf4EEQ=A=wUMJBiJ0+sKdqx2gFx4TfnFT8Q!oqig-f~ zF&VNS%sWzzyS1TBhR7zT>a^N4K_^VKfdv~pzXY2KPc2(7FuRy;1n``-V}x3kIflC~ zqXkRsL#(-#m3o$6fG0o}P18`B;A0qP1Xv*qBaZA^R0GP%I?FX^c@@*R7eK2E7j&bH zXGntE8q+OJz%2s(coTL%PQx)xm)?ci6U-*VEK&mqXlg6A|FE|NZ3@ABzbf(|ie0W> zK?2WPg|<*iC@qk)u{O(6sAWGz-Kmn?kjYg9@ib-uIf8BAYRTeki7|jhi6+1je=L_S ze^boHH9<}Vlu8g3S{k+eNYha$cn!e1yi~vc=>GcP#-xRZ>E^1!?%jo|vY|D(ksh}s z*dJL1#I+>jLW|v((;&&dsOv96mW6%-$yMUUni4wFdrGpVwY$qx*wA)x_g7?t_-q z;-YTdf%;fuEKgW#OqF6n5`>W|?dXrf<{5`%91lm?P?Z3SO5mQ(XJIO3>o;^@#$sbp zH#L3Hghi0FMXU{dku(@1tz6g&Su)4+Kw9O_=4u0N1Mj&5=m&ORT#!Fq(8c*l_R6$E zjPIO_a@=1$Jycw{7-{GuvrDm(4Jh=2qwS~3!d}`Kl|ASby#^d z-e`q!X|-Lk8+ojt>xE<70sWWX{R{}0S8=3udPjQ zJMOX{)G}8`B3Ea$gLc>PZP#n36YCrUyVp7Ue~{>RuG@{@88#R)kgNwaa>|O`Iizhn zsH%fKJ%g%xaGN&dbUS~pi1ND!7VQ1IMJ)$QXbvc@>J|>6aY-P!{rt1DsDAk`2sVX0 z8P5YFjEh)3rH^itpz0bh#p+OhnL?KTE;2JQ4W8o_^j9``;Xs>anZ(pFmG}UrJf*<9 z)jg9WxT*vbP6D<;EOOvK_B%q%Bv<`LO6rs&2xp^s51bKDB%8{yMG>^Y!Al)0T(Beb zM??Rl{pPz(7@*kfr0CY2+|i2$e26r#D>qD`2=ZFT$S?+-nIT)UOsQn7!hHp{*G3s% zf_2LZ#dHjzOtW}IA)(pF^RT3+9hb;5g59tzgRN(*0d7WPb-1E~LW8wEbKiZ>*pz2D z+rYPyS5onji6ZwuW3_1jb_M{*dodElU2+^$k)X$uBAHlZAX?Hzj7|rdVd^y5U^}vm zDKvFif-8o`;4ky$I?(>SbTw*1A8XXWT2&zm{Q>-#wNGLD>@V3q(-ma`OHQ%Y9*S1f zu~H>uKqO1Yt9D%juOzl%&^5cNT2sn}?#2Y~=nJQUM5AU&J>q&EkdZ%epLgs|mg1nU z$#;%<@B4&oUk0e^$&L5oH#ZxdBOY&Ge^|Pb5DszW;VJd0EsL8 zDFFI=ulDZk{rv5g26+4D5&6OUpIyc@ig`4YyA_gZ;Z?wve2l0jN@3VSu_o}Bk61P? zkSc=OS+e5YjnS#*Suvsdp2lXOWttB$abXdRg)LarE=)ExqLBFPaR^1z+yV9g1gu!! zO0*88HF!?t(E|Weu;c6i10Ehzuu82u<-q35PB|<*g5*)ZhhP<9haN=&?pB;yD*iZ9 z#U007x?mU()!2|^VfeD-b%hb+H2|L-&2q?r4V{+(s1*q)B+Tw7a<% zm~&J*E{&ki=nd~^0(h5rd1d4FnkHzQiISfee5bgiSlhy^UMbCkVGpDAmNQ(xorehc zgD=5>gZD*wgGwjKEUjgIXoxg;obX;N>T0Gpfem#a}OXW~lxQ(F%D1*$<6>XzN0Uq?j+!pf1c+UYPB~X-@ z29%N@E0knk-XV$tui^qi7(3q60#m>SVYL3@NOw9PC#@< zf-LkY-aIO!5}aO2s3BPsS*Ly3s7;aq)7I9Wrx&ehe)h|n2#}WBzL#f;%YpzxYmyJr z)0d?1o)Py2e2Rf4FMyg#E;$Q4!_u>)k(*x6s6T$6ykIT@vy^j>|NTEz!H*PHT@YB_ zGm@AZ3c?P?FBUd?MNVEIO;7^%v{WvjdeMe>2D1b0Jq{v_I)F}+1j!5S8-PVvtf)bF zOsx8fW|+=ejpSegOD57#7)wAYal>SPG_LSg4sZ&B(I#wA)WC{Sc@Fwgx?h$o^~2?C z>n6%|BnMPDa1e}ekQd9Mxgr2-1Gj_d*Hx;c_~ zrg|v_Qlf(Tw7_`=KpM#L255LKuYx?8m(gz~*Ob&7BhdzQ^ewkK>bEDYkz-+FNIEkW zjkW2vhKe$W96SRzxk#Ohoo<~6W?2m)s6cBC6y(-4xlHcAq-)VuwQhTgYaclnt+(J( zo(D|_YA|O20Tw$XxHhzzck7{saJ>tzy*}4|s|C1ER!Ts3{1O-uum6?v(773@ozPH( zM+J6ifYX2o+ZrGwUDWAI5C_-;L{lUrn9_5Egk`V*%nQo~x~Rj6b51cxyzFDg@^(#x zMS(K0xP+Vl_>7riUU;!47zY@p4VTF}i26{r%6-7Hg&R z!p39hE7sN;ITQK@$fj(VxQaXm16UVU8uoS|e(4~oxC$D?1C`m&ZWD?oFetXtUx)`)A0 zhWNeahhKV$M((};8FR-P`H;J1(M$<_Th$+;m|>=X`0*Xi1>%oKh(Y1Qx;#@|>nAK$ zKzIJ&X7THlH883MHI~XS67a&#oFkkMQlxoM1tpSLlE?fIeRg3a;)nb*1kjB%^8$QmfHsJSXR#dF#(p*|z zLV#5TPN=NxW5#d;LBR4r5Bb?;rVk(q5U2no8B_wR3&fC#1kbSnco~=j%m??8;4Ch| z3W4<}&4J7K-UcB9i9Od4I7T+1uSTOkeQMD$i3XpA`W1%rDmg7-;C3 z^<{t|E*t_iP2(v4bn%s$?i=W~Cj9Y`&mb-5au|vUK#$`1Q#fqk2Vx@90R?>6tMH-$ zj9_NPdAM?M3;vpbN!bgM&w)rR%$4@=SlVa4U6$yuUU>_ee->L7o1AQTQ1sZrBet(U z^2pb1)>V-Gw&kQ4o}p=r$Ir|1`Dgh;cVl5`C8ckf{VPB;bm*7_C3E>X8OV&a&!_`l z&MX`(br1-{{8AHoS3P`Y`Y-8e_%cz;0Yq8-W75L5Y77SsF`*hu^kq&4&Lr#N{xUP0 z$`7;w=p=|ebaRW)8z@}1P!Pr^4zgel42@-56b7DgA1$2by%Lt`p$k^@h&R|r6!5UP z<=i8@y)b6+A#J<)<+A2f(*ACMC)6_J7Tn;L1J5t3EZ9d$8N!{5F2|7>49M`x5^i8? z`V=TxlFE8tNiMy-7fegp|I>ocztg;+FgjTFf6%(Q05rR-{Wq~*U*5ibt>E%o`(dnQ zNB?1W7A|E@t8>M0Nnx&j3tX52guCgtg9dsQlcWC_IEz@V2EZdh{{)fNL4qw0MfOOR zLQG(RJIo_l85c$cCW9dT{X!zZBId8G;e)C`_p!>CfyDS42HVLD{4@kKtbH`WG*@Dd zA*r}01zrjlM_pK_xrl^Jo-p)97Nz23hXBGt2QLoJ;EFl-l5%}<7m0E#&0*L&xSY;K zL^&JRr?3xfAI4<2tjLDJvL8xnGIy6L9Xee!XH=W`jT zVIbRaY|twux*@C?LP*K>V=@x*Z!&hD{neXs$=L0W;zlj@<<~N;$6D)VaHLSXDQ082 zihn#M)`vFt+3i2L*>3MsefCFfHaz3soQcKiCP-V>Zc5xpA4fr56b9iJ;~#?ECI)@G z4b*rLc>8Ji<~ixoqHP< z&)&TrrE#y@f3(4FZ#e2_Ax75~>1zXred@q~>N`B(qQgGlVPZ?c8kxpgUzVH!;f?Lr z%*&XIGtI-c3bc0k`6G>Yu-NXUF@~Y55VbK7h+2qgLe`MbdB{458rBXEVwc@ffKGbD}mI3KqiGRC-=*OGe1YI7dD5-{kd#9j$&)HqzmYIUocw7ilB zQVvV4%jDqRSjsA=L>&>f4_Oks<%`7((ZZD!E%d?(Vz9S&P$pUXaIUJhcE`;h_l80Z z)zu9l13!e!1!%(vU1_BbS*c`L(U*65%gW&WnHy7wvhUdUnV;BfKlu!MtP%G|nLd#uN2Mw%LOgPiEl1%Ml8U;&Ho^>>M1d7c??7hS6#~grZ zlYu{tOi*WuATEeF6kDcMk}`yK!Fv)CW1^kuV;YBvtu4wlkWGkri#Sg)cwC&S7!uob zvM!Q_tt225*Hz*n61GJvHOM+IvC>zT${{47bF0sUgnwlPWqQbwi8`3Rgi`4| zD{$#ZTnxe#(vdWAgZfzVdc?lL+kzJp9oI~lZ>Or|HU!WF`nK{i^om!+9!T2Z;q`_c zLM%Ud@ly}(e*OcmZQS_U$yZ4G*k90}nI^W(I8NEh8(xw!I0#U_6RP>!Q2sBwPy zawS82WDxEy$*lmmU2&vrNN=H3PJWLo)w}=M!w1ZNN*y^v;nSP*%JP5X1PtZK8S^Kc zaGNVdV}p0B-yIy-mo|TX^^ss<^ZVZNX%Qu#h(3H}bnp)Fqm_SyJokr62W>`#|Ryw+VkklUd;5gJi^dK66J0?~kR6xX<34AL|{dwpxQJgdYQ~_a;4m8DfquK*w zO*5$n6E~fdX~*WxJBX_L9JTFx8sqJ?j!)Frt2U?G9*(JMUHvcX>wN3#=p#&5_z$e+ zQ-K}reftN~iN@XSTMzW5V~2gs0Y^i#-$5V5`G^oCBY9so7*J%@wPE~-SCyxnJ@tN9 zb%g3_>bsoVoSg&p>IcEGfM`1Cu{4l#`@%u*;qgtbVA$t9+NLxPv^(%)RKYxILjLBR zRmqMqz~Uz?Ecq->6(z9q6a}FM)xj{~Z37praKaJq#UR-kp*1CsZ$CoG78{PjV;!-o z!|QISBTHaN&>#K5IMZ=-g{@J!Hmz!WPuGF^Xe~~@mmWZQeW>o5EU-^=JN-ktUb0=4 z`)2^HJ=md_)|$U?lp&a>ykQv$#g%?{!PFKghW0F@p13Z*%BkieBOi~~qB{=k3&5_z zf7rakVJaBu@>ophKco(gNxsG;9}yXy7a2%pqa7!s7^;)N{XjGBmYK(~M{|fdT^=Uk zWIXDKhmr|gv9;icD_Fkq=kOR#opRkE0p?RLb!oWk;4v~LpBrWNqlwE&c#5Vp>}r5A zmHKUy00$?-p#f|Se<89%fJzhau;0*tK$vx~HK@-zgfQ+kco(PPiU?=0$XBXQ6SiNK z>w^UavQnP6y?_B?vib3t09geBcv6hK~B17WNw#v}Ak1KndNz{WvwY&+wplmbi# z*~TFo5Gc`2(J9M@0D~F+AkpHZpugn;rona){u5pd7#6yYmkOCH-;%#CzYgv@Cvbuf zZNo|p+YrbHRwNwfTj7BOV+~#{0YCYHbOcM&%18r`c?X*}urXW|O{c{2g;j|O5f*q3 zr$0H!N1*@=xHqw%18T?0KrphEQol&reko! zUGb}KygKd*)b=4B3N`!P-u4aljQnKJF#Fi?cc{nT>JP|1x8J7t+&(z~cg$-QQZqfW z-=A)leZK0(+L|th`D|(1e7mEoCfX46`s9{W1NCGaBs*IZ9=I@U?Q;j~`f96c{ef6p zGF}xxu&=J#-M7^yJH4%~mfS7g!qp+|h85)&=`QI5(nqDg#QY{;ev4pl6kCfpy(j^lGpu9#y0M@DnOIR zRl6)-bLf^)vcGkiY-M+b@*Qe4i$bH5` z50Pp=`k@aVvy)#XhkeFwQ=4S<$$$Chha9#(#b&2zH}#R*{0%v{yUSPbmDZbo&3F0h z?f_*DWbpl=o18YY!|imr=m#kOBt_)bHd@cc%+H_NFDspj&kOUthyKAGl$+&$DOczA z2W&lx^42H*;AZo*9HJk{L7Te%$PJrRxjFhgxu@K8pM#vgQ>{3Uy6MH+wkqUS9Yc0i z=yRaM66l%bM-haqVx8EP!Y~O2;kE$Z9+uf5R|KJ|v6`IiPgX)KS#h2W?Z<#&31rPM zn4jRHgNE`=6Dc6~2Af5pP=MqxxfOXj7vR^%XiQX?ep&sgf<&K{G>~>+ahBJAj2At` zB}VJ)K)$b%))!&lCy@+v17fmf2DB6;1KOMpX9x1NK2KH8xBAHPr%)BUm0(v?E2VN3 z`<1WorlpR3B|RAE$YwhNL2P8LaNO-yRZdY=x4WEXX@I=dg~LT9yBf=`rLkC)m@T5c zu&w2VJ=C&<9xM3_uyv%el?N3QFpjSIh5n2ATo*Js-n)ZMsbrN~{y2la^!+bnY8v)J ztz(PU?Muu*@B!0!vt;?rOKX^GM#Pd_z;>e~b1&u-nA3mHo(A<27H#dfTGUQZ^9hAJ zw6^3W_B`|LTt?s^cLXAeby`}SBefiLOZ|WeckK+x`pz$|NS2*aw(jOzbe1IWmD$v( zTbZ~C6OYb#&JD>i$Wu{_G>wsBaq(*{Ks&QcRM8f&xrYw~=UHKhwp!W({3;E@00X*c z*?o@->+ko0nvJ!e@alY_eX_)*bhvUGG5Nez1lR8YL%pA<9mTIPw+~%lxk(rDj2~W? zebNqo&j#U0O$FpBNde(5JH2wpco&doh3}=U|8(o+@WBBQNUdq0G0srrvGdMnXaEU? z;RE0DbFj9G_cK*fHDd{WD{K_i@!S0Top(OJS*m*Gy* zD->V0mwaha3BGuezo$;j6V0EfMVM>1ZY2Y=2CQQkpf<=%eEy*u9(a2J0|9#$QZNtJ zRis!FIGjZ+cQB-Zw+4AXPf#0_5}NMW^B&)o z{rM}#v-<b z@961mY&8FwhC3#;+NP#jZL-5xQ)9l5f{l&6J!g-!bc60gZ^Q-2DIpRP8$$iS7e-e| z)cU3Qyi=3_2NgI4d6XA6KfbmOVt;-ZPw_a)u>ftL8mkO+X8~mwWP3>W5G)05mhP1@ z0vmCti2aTwJPy%R69JY|rE+_Qtn-Xf+69jeDEyYB4SZ)ar*5De2DO4xvVjmrhr^6) ze1i0<^G+w4*R73S6urD`*aeFyPOO28p+=2`cizLZ%4tx>HPi@OD-5P6C+xBY>{Q&e ztfL2(G;MIpz^T>p;GnQypz>^7)(uOD#uM00lo%W=XNzY9P0-OI+8}g9oUFhxdd*ly zF|HjQj}c(Mei|)-OHe3zY89z+j#R z!k5ksX#dd*bj&1$y0GSOP@wc0uyW~=X8E6Zea?Aa!w*?UcTo!;d7ZM}xP$4jblEcB zkT6a@>6rrbuo9slV_T7}jcK^h!up84HfYuG5>#-u0)~VBC~4@O2KhwHH*d1W{MB$| z%bp=9sxfU5phNo_@C>XHoDPT$Pq^9;UBq-?)`2A&;0zpDBS*pF#FlmItsQpklW&Dg z(oNPY8#L@A7=~%gJRsNIf1DT0n%7+DPky*?6YnudqE1{emEc1xm7z}16W%@)JW;&M z0ERj^^1!K@cuO3ytR0FQ4x}@gtGv|U;*YVhC0=O&B+`%HUnf5>14n>2yxt(r+gdT! zT5l?R_(>g|U_3rA_`^o<1_4!g8TXK|m*O6FSPBLbp?ip3nSlR%U z-~6CRO!un8uFvjlQv5c5#N`-s`G@=3WpDe{z3bO^Y`*dB#gK;cOQgf24t}h?J3G4{ zfTeu7A3Q_65kkK8(Djc$__xndZMDY}uu)RJ{%kPhwDP32&V%0+NzH4B{Beyh?f^NBzzoEj zZL(x}_okCha(W_>sH=h8P;>nbn=9&6U8-|;2=*Nn)@3hLP`k|*3)^JBs@m+%c3H0J z2sO3XJ>J@2!lQUM@e*2X?ts2y(-afaGdyQ|9n4wBIWQfp%>? zrpjII3Xr``A)nLk-`QCm2)cYpk0T_ze2reGSN7QK)jV8#z#Z^61>7n*?XulhRqe6a zyqFLt+2am1=yca%|HY{^HAWjTZ8CYfofNUDjjAtXN98p)`&>=6^&U@y+Va8!PrhtE zrr07d0|H2PyN$MyY$w#KxV`osn}W{4OH}oG9Ewt_hGbdwsZLuk8e-av^{vTlqmDuX z%&*$1qwHiBjg$_Z<_7X_9WtCphzV%AlK|1AIYW*~dfd zH8P_#3=mI?A(hIM_z*ZW_%5?v!9sD>u;&k2$JcUwt7Wq;MKE1O0-3e!OeC3H4sRcS zOgSX~GtQbY{edl7<+ris`-B~-PX*Frtzyw&IY1y~8vvi06b?4!hP=Cvg zf;;%aJJiir_qy?u{{atk9V%!SRiacIY< zNi}b47d5yyGYp6?0;T{7U?pS3(4WB?khk)yl`S^6vP1fBJZh#dYzY&WC@4w!kN36s z%PyWUi-}^QpdYkAq=}tNHlXnVR*?oeg>N2IY4y)4P$|VF`Hx>g#9z=aQJ9N~3ITE} zt<)>NacwAPb;zNX>yUpDj$-|}@D4SvT~@F<0;;3A+wXJO-A=pnf?c%-T737q+%8vD zqg%DvWTiRksr9xu$;}GcR8MD9!0&c?9Cn+-;eJPB05+BPIz2(RO;O0+buA7a40G|30q84rw61Jw7jX^TkDB7 zE3(bza5n@zPPgkGP*&A`0poVt9X@||GbZr=X!Srb)^7}9W47K~7KV%@!D4Ms!dDHi z_{iHGh~?o1kx%y}^LAP^W3)(%7qN6reSsG6WwrJ0yUih;9>*R6JmPc+GyhDV88fev zukS2&ns=R~PbVI0eXO;)*jkh%RkW!?24D3FOQ2QQjnbtbOGsJ#!FALUXs}XQ$!AkL z3svls%fh$V_JxkKELl#~3g2a9h-H>-%8IfLlnN(!aVQb%eh?e{ zjH8gsfY7)JI!Jl<72ECS@BP}o4Nz}r;A{87gara*nBUnnwo@hP-mmSx=QQ`=Et#*~ zYkAPF=<<2UWHsy=$@?I{dGs8{X^aEihqt!%Y7u1B@Oc?tYdv)T#w%gRi@Gx%of#aO zm*s9{3+1lZqIAo0l=?e6GRP#l5;XDr@&3&hb{>=W?5NdZ**$%?%&gGNEq!~kF|BsT z9{JeL3!D3op9kx}dybw{&dbli(qDS38``WW?l648Pc`aTNANfryaaj#=P{klats6r zg3mE{bqkDq+S=SEay7I8`~jl^-RJYMt(d{f>@jescBI2F0z%0T;eaZ{YUNKZKHSnl z)X}0$h`V_j2UK+aed!N|^dZQd9W7>!RfK-p(qWSE*0&n0SeiqNhQUpiXd^Md5q`Q* zl#tIcVIR_P^2RzJi!w1Q1t2=N-k_r-+; z4)bO)hgsi+z|9vKXs53|8;f*<4Vt1)f^lJfSpO6!&J9^3w+V}5@YX4E-@QzY>dt}w0((R=EOe(rZ~$TLYUYXmsduWo0X8zOT{ib3o#H} z)CklE2Xq0kT?YRmNdDqICMo(%TV3b+w+w&QKt)$td`&z&u(M-TL4S7=M)mRA_*fPe;W zb@0l?fnMMq=oR!dsK>ZeFb}6|EL&d_C2%eaZnv=dLvvObnG^N*;5=~|y96PoHZ$pR z3^`m_Pq4qhrVr9A>)TNd#kT`KYfs4cF?y_L!#`-Ofi5wvzDF^Y&Dj5Eza7hm46hi0 z>;gx!6l5Df1!69QVYSuLy@Ycas4AC%5rWAtzmp_2Fi5=Z1y~}gaA~{}W~+Fb-O)hx z#hD-uL5=zeh5AE1H+F~c7QFt@`t`+*x@&3%ZM{8i2){S<-QCyQ>#WJtP<6G_U0v_K zvG49oPp`{m8~X0Gb?e+M>qGZ6A8dXVQ@^R9qq>?Kl8h7eR>X+_uYAU*o=^m)7^4N3P30SaWqQ8h zu56CDeCw>f^c?H1L6z0j9gQ1#O09Lq^Xi0rd{DX;Ff6}&oflj@kHZ@5VvNa#9U=i4 zSlDc_nwa2#Yh9X;6&FW3fM|IYDCI!;B&Yl+_M1)3xVrqRvK-!N_FnV59esQhj^7%= z2~VueeFDhE@AQN1!7|r?st6YZ8OuimO!iSQ#KTy6i6-;A_19))35oBuDq8!VwC3P@ zMHj4NHT0-;2P^=Dl@(=0eh+Nw{3d%TnZ_6~#4Lo-P|9Ivw?quQ2Aj%ZxF=#e8av48 ztUZ!X0)Omf0|ou^m&yg~|NPS94XF+@T2)o_SG%A}Pdjl1$-Ba#(mz&JnblU)zc{$C z9-oDSkVii#%O7wwITE)yo7(*T>W|jff3(`~Z)4TyIk*F<1IXHlDR-g;{prn9w2Nc${{JFrO$wFkq?;cypA9(X66gBNrhDv+?v zVOkao7a^J zK-G3TEz6A4Wph#>>Gs-wHZJeecpfEqZhX&@SkR{w;-L8A>O{;6%M#w^Rtn=p z8kPSq4m@#bODP>EHE35UtRnrYQrInZ($P}bBZcT4+&7yG#PcBiUMWmcmGWvSEK7dn z^-@@osz4&F@-~F4N@2ScvBgSZ73sNB*ezXW+g%EKq?GM3cl*e?SY~}+rW==m*q-sZ zz4IsB?(0t#regE6<5Od?xl@Hwa%`eFR-EmMohZa+=VuDBxnk^Oablu)`{iX~WAo#q zg^BU0!fdSl)ZE>52)qj_(WmW?cPxw zEv(jf<-Q}8nnq3)i?fB{39B`$8&-I>cXE8L_YKt+G4PpEy~UBa@sZ-x>@|h4`HA6~ zaqI#P!&>%Yfopp#h}r0WY1yF4jC~3jbfW>!njt0sjOq zh7|r+cT>_B!gC_^($X=UJS`%(h;m&ha{|xB5T3_B$>Y!*a$?d+Tqi`y+eI4J{_oWj zLv8bDa};GJMB4?_7eoI~p|&}kbDf24rx$-?qSvQHuX`}MqBM!r)&8s#W7&=LDLfYg zx8r~A$K#sCbt6&^Ly9${{atEE{yH-o3AQO_*OFia+JFCujeZCcNB zd)rWETJ+oMwKe`>q)eb*hJ)3YlcEML!|>jLbRO@U_3=vd@yMI>X$0^n;*ZC|^{*Y< zT4=0IpG99MQF0FV-`?9I%JEFih@SI^t^QvAj}kBaCyAHmULJq*f1r4IsXb_GO0+$U zUK9Ww#;vos&Przh^9)kw08fVL44^QHPg(cI>HOkTt92*(jGU zLrv&ypvTx#d(+R;AlR_*kd5pF4<49nU2xFxz&7p!=M7L5te7|%L^Uw*Mj#v2QXSP( z1Ku~=1X-(vV%TzPg|v(#(BP?QYNvJ7L7mh^-QeAj>)`&Ap?(^`TQhR7Z)~6|Xd_+< zw;9$-HVZ@w3BwxZrVe82}jcD0AAsJCB20X(N%OcT|xAX1axr(+~~Q2^yhMD$q$9qf<0ax6)~vph=pdB2Ck6G()pANAq-sZpVKQJ4@&2 zt#l{7jqalJq|@E>c6tZhL-*1<={~xj-bL@G-=PQSLHb>K4_%=5!pZ4<^e{a_zen$< z56}ncL-Z&;Mjxi%r$3+tdYqo1C+Q>fQTiBtoIXK+NS~zthn}KO(I3$t)2Hb(^t2uS z?Q&*LIX67*o){lHHP?M&e&U2}rZ8Rf@TL1?aprVk)Fr~x^RuVQp@|9GiDL1z6Lhn9 za(JXr1vb$=JwGwgJyW=CzA!uInH-)u-EA=s|HwpfezbeEc>B~uad^~gMdznE;yhhA zd!jfzGin<1?Vj z#hJ5Kbawv4WN~zUqM%NL$`_~9TX8F#b#p&Kslhr#@WjmU)W|6pr%o1T#tQ1>_{>ak z#ydVcJ74H7PJ>o{ zb9id}-0&Rg^oiiUV_^5s4RAJ<_ZE|>g!o?RrA0QpZ5N_|DpY~XR z8J<|T73Mi81t$t8=alL3DFFWDoPBI&c>0uYZf1OVY7A4rnVyju03G1M?AS3}G&D1R z;;ie$4CZZQcy>;~+y%;Lg03-wxJ4YLrwX>w@slT&!r4M~2|nGUF~d_M;}f=VfW%)J zff)7bfOz0Gh&HEf(-ZTv?zvMlg+lj<;h9+*W1Exzt1CmV{%Ov#xUVX^C9ibOmWJp$H}Xjm#%ZR zFgFJ|a@I9EJbOy4P{%1ehy~_jFm_{Q7G|b~Cv21Ra|OD?J2E~q0>qh}Kyf=L!EC`c zdvvYLJA$R-o*W;UDT-hK^4!=IFPUx;3a-ZIQ6=&`Obxnc3xMmK?No8HP_tS@ zgbJfrOjv!zY30QHn1@Bv61DLNa&TgNYIx?Xb98ua_{8vR0c&vv6K4S&^%pU@PKa&e z)3f8V?x|w;RN;0OeC?2Ri*v41#rc^rjAYio7Si+6RcoV+QkNv~qH&kOIX~?!2f1J+ z!iB8a62--Ai79l52`f*WSg0aJWO^!ijb&Y;VbfS0>NN0WeljS=JbQLhObtgOm!wpP zqZ5k?6s|B~ADbwiU^2qf=bxS!K6_$#IeIZXcf-4OikPvFe#8+zyi9 z;Q3?1RLSLlIz4~x+(f}KjoOCC3Jws9+W~v~EM)&V`|Mm{de#ZVnZ)!=I2qBz!ekt? z34%60b)tBOb8ZF+Bg0e9>EZFIIRxw{C$JDedd3SUL3#?jipPA6bMv530zJKx#TgJ| WE22yc-|jEbiqXRCY2XQjjQ<7t;9J`O literal 0 HcmV?d00001 diff --git a/pub/assets/css/octicons/octicons.less b/pub/assets/css/octicons/octicons.less new file mode 100644 index 00000000..cda25e17 --- /dev/null +++ b/pub/assets/css/octicons/octicons.less @@ -0,0 +1,246 @@ +@octicons-font-path: "."; +@octicons-version: "0ba238babad928a8b468c644ef3a15e66545d466"; + +@font-face { + font-family: 'octicons'; + src: ~"url('@{octicons-font-path}/octicons.eot?#iefix&v=@{octicons-version}') format('embedded-opentype')", + ~"url('@{octicons-font-path}/octicons.woff?v=@{octicons-version}') format('woff')", + ~"url('@{octicons-font-path}/octicons.ttf?v=@{octicons-version}') format('truetype')", + ~"url('@{octicons-font-path}/octicons.svg?v=@{octicons-version}#octicons') format('svg')"; + font-weight: normal; + font-style: normal; +} + +// .octicon is optimized for 16px. +// .mega-octicon is optimized for 32px but can be used larger. +.octicon { + font: normal normal 16px octicons; + line-height: 1; + display: inline-block; + text-decoration: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; +} +.mega-octicon { + font: normal normal 32px octicons; + line-height: 1; + display: inline-block; + text-decoration: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: default; +} + +.octicon-alert:before { content: '\f02d'} /*  */ +.octicon-alignment-align:before { content: '\f08a'} /*  */ +.octicon-alignment-aligned-to:before { content: '\f08e'} /*  */ +.octicon-alignment-unalign:before { content: '\f08b'} /*  */ +.octicon-arrow-down:before { content: '\f03f'} /*  */ +.octicon-arrow-left:before { content: '\f040'} /*  */ +.octicon-arrow-right:before { content: '\f03e'} /*  */ +.octicon-arrow-small-down:before { content: '\f0a0'} /*  */ +.octicon-arrow-small-left:before { content: '\f0a1'} /*  */ +.octicon-arrow-small-right:before { content: '\f071'} /*  */ +.octicon-arrow-small-up:before { content: '\f09f'} /*  */ +.octicon-arrow-up:before { content: '\f03d'} /*  */ +.octicon-beer:before { content: '\f069'} /*  */ +.octicon-book:before { content: '\f007'} /*  */ +.octicon-bookmark:before { content: '\f07b'} /*  */ +.octicon-briefcase:before { content: '\f0d3'} /*  */ +.octicon-broadcast:before { content: '\f048'} /*  */ +.octicon-browser:before { content: '\f0c5'} /*  */ +.octicon-bug:before { content: '\f091'} /*  */ +.octicon-calendar:before { content: '\f068'} /*  */ +.octicon-check:before { content: '\f03a'} /*  */ +.octicon-checklist:before { content: '\f076'} /*  */ +.octicon-chevron-down:before { content: '\f0a3'} /*  */ +.octicon-chevron-left:before { content: '\f0a4'} /*  */ +.octicon-chevron-right:before { content: '\f078'} /*  */ +.octicon-chevron-up:before { content: '\f0a2'} /*  */ +.octicon-circle-slash:before { content: '\f084'} /*  */ +.octicon-circuit-board:before { content: '\f0d6'} /*  */ +.octicon-clippy:before { content: '\f035'} /*  */ +.octicon-clock:before { content: '\f046'} /*  */ +.octicon-cloud-download:before { content: '\f00b'} /*  */ +.octicon-cloud-upload:before { content: '\f00c'} /*  */ +.octicon-code:before { content: '\f05f'} /*  */ +.octicon-color-mode:before { content: '\f065'} /*  */ +.octicon-comment-add:before, +.octicon-comment:before { content: '\f02b'} /*  */ +.octicon-comment-discussion:before { content: '\f04f'} /*  */ +.octicon-credit-card:before { content: '\f045'} /*  */ +.octicon-dash:before { content: '\f0ca'} /*  */ +.octicon-dashboard:before { content: '\f07d'} /*  */ +.octicon-database:before { content: '\f096'} /*  */ +.octicon-device-camera:before { content: '\f056'} /*  */ +.octicon-device-camera-video:before { content: '\f057'} /*  */ +.octicon-device-desktop:before { content: '\f27c'} /*  */ +.octicon-device-mobile:before { content: '\f038'} /*  */ +.octicon-diff:before { content: '\f04d'} /*  */ +.octicon-diff-added:before { content: '\f06b'} /*  */ +.octicon-diff-ignored:before { content: '\f099'} /*  */ +.octicon-diff-modified:before { content: '\f06d'} /*  */ +.octicon-diff-removed:before { content: '\f06c'} /*  */ +.octicon-diff-renamed:before { content: '\f06e'} /*  */ +.octicon-ellipsis:before { content: '\f09a'} /*  */ +.octicon-eye-unwatch:before, +.octicon-eye-watch:before, +.octicon-eye:before { content: '\f04e'} /*  */ +.octicon-file-binary:before { content: '\f094'} /*  */ +.octicon-file-code:before { content: '\f010'} /*  */ +.octicon-file-directory:before { content: '\f016'} /*  */ +.octicon-file-media:before { content: '\f012'} /*  */ +.octicon-file-pdf:before { content: '\f014'} /*  */ +.octicon-file-submodule:before { content: '\f017'} /*  */ +.octicon-file-symlink-directory:before { content: '\f0b1'} /*  */ +.octicon-file-symlink-file:before { content: '\f0b0'} /*  */ +.octicon-file-text:before { content: '\f011'} /*  */ +.octicon-file-zip:before { content: '\f013'} /*  */ +.octicon-flame:before { content: '\f0d2'} /*  */ +.octicon-fold:before { content: '\f0cc'} /*  */ +.octicon-gear:before { content: '\f02f'} /*  */ +.octicon-gift:before { content: '\f042'} /*  */ +.octicon-gist:before { content: '\f00e'} /*  */ +.octicon-gist-secret:before { content: '\f08c'} /*  */ +.octicon-git-branch-create:before, +.octicon-git-branch-delete:before, +.octicon-git-branch:before { content: '\f020'} /*  */ +.octicon-git-commit:before { content: '\f01f'} /*  */ +.octicon-git-compare:before { content: '\f0ac'} /*  */ +.octicon-git-merge:before { content: '\f023'} /*  */ +.octicon-git-pull-request-abandoned:before, +.octicon-git-pull-request:before { content: '\f009'} /*  */ +.octicon-globe:before { content: '\f0b6'} /*  */ +.octicon-graph:before { content: '\f043'} /*  */ +.octicon-heart:before { content: '\2665'} /* ♥ */ +.octicon-history:before { content: '\f07e'} /*  */ +.octicon-home:before { content: '\f08d'} /*  */ +.octicon-horizontal-rule:before { content: '\f070'} /*  */ +.octicon-hourglass:before { content: '\f09e'} /*  */ +.octicon-hubot:before { content: '\f09d'} /*  */ +.octicon-inbox:before { content: '\f0cf'} /*  */ +.octicon-info:before { content: '\f059'} /*  */ +.octicon-issue-closed:before { content: '\f028'} /*  */ +.octicon-issue-opened:before { content: '\f026'} /*  */ +.octicon-issue-reopened:before { content: '\f027'} /*  */ +.octicon-jersey:before { content: '\f019'} /*  */ +.octicon-jump-down:before { content: '\f072'} /*  */ +.octicon-jump-left:before { content: '\f0a5'} /*  */ +.octicon-jump-right:before { content: '\f0a6'} /*  */ +.octicon-jump-up:before { content: '\f073'} /*  */ +.octicon-key:before { content: '\f049'} /*  */ +.octicon-keyboard:before { content: '\f00d'} /*  */ +.octicon-law:before { content: '\f0d8'} /* */ +.octicon-light-bulb:before { content: '\f000'} /*  */ +.octicon-link:before { content: '\f05c'} /*  */ +.octicon-link-external:before { content: '\f07f'} /*  */ +.octicon-list-ordered:before { content: '\f062'} /*  */ +.octicon-list-unordered:before { content: '\f061'} /*  */ +.octicon-location:before { content: '\f060'} /*  */ +.octicon-gist-private:before, +.octicon-mirror-private:before, +.octicon-git-fork-private:before, +.octicon-lock:before { content: '\f06a'} /*  */ +.octicon-logo-github:before { content: '\f092'} /*  */ +.octicon-mail:before { content: '\f03b'} /*  */ +.octicon-mail-read:before { content: '\f03c'} /*  */ +.octicon-mail-reply:before { content: '\f051'} /*  */ +.octicon-mark-github:before { content: '\f00a'} /*  */ +.octicon-markdown:before { content: '\f0c9'} /*  */ +.octicon-megaphone:before { content: '\f077'} /*  */ +.octicon-mention:before { content: '\f0be'} /*  */ +.octicon-microscope:before { content: '\f089'} /*  */ +.octicon-milestone:before { content: '\f075'} /*  */ +.octicon-mirror-public:before, +.octicon-mirror:before { content: '\f024'} /*  */ +.octicon-mortar-board:before { content: '\f0d7'} /* */ +.octicon-move-down:before { content: '\f0a8'} /*  */ +.octicon-move-left:before { content: '\f074'} /*  */ +.octicon-move-right:before { content: '\f0a9'} /*  */ +.octicon-move-up:before { content: '\f0a7'} /*  */ +.octicon-mute:before { content: '\f080'} /*  */ +.octicon-no-newline:before { content: '\f09c'} /*  */ +.octicon-octoface:before { content: '\f008'} /*  */ +.octicon-organization:before { content: '\f037'} /*  */ +.octicon-package:before { content: '\f0c4'} /*  */ +.octicon-paintcan:before { content: '\f0d1'} /*  */ +.octicon-pencil:before { content: '\f058'} /*  */ +.octicon-person-add:before, +.octicon-person-follow:before, +.octicon-person:before { content: '\f018'} /*  */ +.octicon-pin:before { content: '\f041'} /*  */ +.octicon-playback-fast-forward:before { content: '\f0bd'} /*  */ +.octicon-playback-pause:before { content: '\f0bb'} /*  */ +.octicon-playback-play:before { content: '\f0bf'} /*  */ +.octicon-playback-rewind:before { content: '\f0bc'} /*  */ +.octicon-plug:before { content: '\f0d4'} /*  */ +.octicon-repo-create:before, +.octicon-gist-new:before, +.octicon-file-directory-create:before, +.octicon-file-add:before, +.octicon-plus:before { content: '\f05d'} /*  */ +.octicon-podium:before { content: '\f0af'} /*  */ +.octicon-primitive-dot:before { content: '\f052'} /*  */ +.octicon-primitive-square:before { content: '\f053'} /*  */ +.octicon-pulse:before { content: '\f085'} /*  */ +.octicon-puzzle:before { content: '\f0c0'} /*  */ +.octicon-question:before { content: '\f02c'} /*  */ +.octicon-quote:before { content: '\f063'} /*  */ +.octicon-radio-tower:before { content: '\f030'} /*  */ +.octicon-repo-delete:before, +.octicon-repo:before { content: '\f001'} /*  */ +.octicon-repo-clone:before { content: '\f04c'} /*  */ +.octicon-repo-force-push:before { content: '\f04a'} /*  */ +.octicon-gist-fork:before, +.octicon-repo-forked:before { content: '\f002'} /*  */ +.octicon-repo-pull:before { content: '\f006'} /*  */ +.octicon-repo-push:before { content: '\f005'} /*  */ +.octicon-rocket:before { content: '\f033'} /*  */ +.octicon-rss:before { content: '\f034'} /*  */ +.octicon-ruby:before { content: '\f047'} /*  */ +.octicon-screen-full:before { content: '\f066'} /*  */ +.octicon-screen-normal:before { content: '\f067'} /*  */ +.octicon-search-save:before, +.octicon-search:before { content: '\f02e'} /*  */ +.octicon-server:before { content: '\f097'} /*  */ +.octicon-settings:before { content: '\f07c'} /*  */ +.octicon-log-in:before, +.octicon-sign-in:before { content: '\f036'} /*  */ +.octicon-log-out:before, +.octicon-sign-out:before { content: '\f032'} /*  */ +.octicon-split:before { content: '\f0c6'} /*  */ +.octicon-squirrel:before { content: '\f0b2'} /*  */ +.octicon-star-add:before, +.octicon-star-delete:before, +.octicon-star:before { content: '\f02a'} /*  */ +.octicon-steps:before { content: '\f0c7'} /*  */ +.octicon-stop:before { content: '\f08f'} /*  */ +.octicon-repo-sync:before, +.octicon-sync:before { content: '\f087'} /*  */ +.octicon-tag-remove:before, +.octicon-tag-add:before, +.octicon-tag:before { content: '\f015'} /*  */ +.octicon-telescope:before { content: '\f088'} /*  */ +.octicon-terminal:before { content: '\f0c8'} /*  */ +.octicon-three-bars:before { content: '\f05e'} /*  */ +.octicon-tools:before { content: '\f031'} /*  */ +.octicon-trashcan:before { content: '\f0d0'} /*  */ +.octicon-triangle-down:before { content: '\f05b'} /*  */ +.octicon-triangle-left:before { content: '\f044'} /*  */ +.octicon-triangle-right:before { content: '\f05a'} /*  */ +.octicon-triangle-up:before { content: '\f0aa'} /*  */ +.octicon-unfold:before { content: '\f039'} /*  */ +.octicon-unmute:before { content: '\f0ba'} /*  */ +.octicon-versions:before { content: '\f064'} /*  */ +.octicon-remove-close:before, +.octicon-x:before { content: '\f081'} /*  */ +.octicon-zap:before { content: '\26A1'} /* ⚡ */ diff --git a/pub/assets/css/octicons/octicons.svg b/pub/assets/css/octicons/octicons.svg new file mode 100644 index 00000000..ea3e0f16 --- /dev/null +++ b/pub/assets/css/octicons/octicons.svg @@ -0,0 +1,198 @@ + + + + +(c) 2012-2014 GitHub + +When using the GitHub logos, be sure to follow the GitHub logo guidelines (https://github.com/logos) + +Font License: SIL OFL 1.1 (http://scripts.sil.org/OFL) +Applies to all font files + +Code License: MIT (http://choosealicense.com/licenses/mit/) +Applies to all other files + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pub/assets/css/octicons/octicons.ttf b/pub/assets/css/octicons/octicons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c5396f289547bc463544dffca02074231de1fe1c GIT binary patch literal 31272 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 E7gXm-r~m)} literal 0 HcmV?d00001 diff --git a/pub/assets/css/octicons/octicons.woff b/pub/assets/css/octicons/octicons.woff new file mode 100644 index 0000000000000000000000000000000000000000..d997adfd8e6ebd23a560b1cb73615d7009d51a23 GIT binary patch literal 17492 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 1) { + source += ' (' + inlineScriptCount + ')'; + } + } else if (dummyAnchor) { + // Firefox has problems when the sourcemap source is a proper URL with a + // protocol and hostname, so use the pathname. We could use just the + // filename, but hopefully using the full path will prevent potential + // issues where the same filename exists in multiple directories. + dummyAnchor.href = url; + source = dummyAnchor.pathname.substr(1); + } + map.sources = [source]; + map.sourcesContent = [code]; + + return ( + transformed.code + + '\n//# sourceMappingURL=data:application/json;base64,' + + buffer.Buffer(JSON.stringify(map)).toString('base64') + ); +} + + +/** + * Appends a script element at the end of the with the content of code, + * after transforming it. + * + * @param {string} code The original source code + * @param {string?} url Where the code came from. null if inline + * @param {object?} options Options to pass to jstransform + * @internal + */ +function run(code, url, options) { + var scriptEl = document.createElement('script'); + scriptEl.text = transformCode(code, url, options); + headEl.appendChild(scriptEl); +} + +/** + * Load script from the provided url and pass the content to the callback. + * + * @param {string} url The location of the script src + * @param {function} callback Function to call with the content of url + * @internal + */ +function load(url, successCallback, errorCallback) { + var xhr; + xhr = window.ActiveXObject ? new window.ActiveXObject('Microsoft.XMLHTTP') + : new XMLHttpRequest(); + + // async, however scripts will be executed in the order they are in the + // DOM to mirror normal script loading. + xhr.open('GET', url, true); + if ('overrideMimeType' in xhr) { + xhr.overrideMimeType('text/plain'); + } + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if (xhr.status === 0 || xhr.status === 200) { + successCallback(xhr.responseText); + } else { + errorCallback(); + throw new Error("Could not load " + url); + } + } + }; + return xhr.send(null); +} + +/** + * Loop over provided script tags and get the content, via innerHTML if an + * inline script, or by using XHR. Transforms are applied if needed. The scripts + * are executed in the order they are found on the page. + * + * @param {array} scripts The + + {{ .Source }} + + diff --git a/pub/favicon.ico b/pub/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7425e812c1866890bf7c17d6977f86118c3ba03f GIT binary patch literal 1150 zcmbtSP0MLf6y9_bnY<{a6lLIMCT>P1qWB4z$mh*pkbyA<22%cnWWa!_-YY2sAEn4w zKL5jW_u6mgcFubkU0P?e&w8HqtY*}FEEWRSZnsgZ)zE6S;P?MRI2?vfr&Hh!27{JVv9DY9FcgYPCW<9v3y_eanB_ zY&Lv;{!wWs{iK2R(ChWSt?zQVFrUv~`Y&;mN~O0N$iMUXtnyy+k^0zdHb|vXirt~T oNhA_NALU5-uTJWNGzNn~zK4F~ayc{_4Y6PHy(295=D$9F0r2UWy#N3J literal 0 HcmV?d00001 diff --git a/pub/index.tpl.html b/pub/index.tpl.html new file mode 100644 index 00000000..2b3fcde1 --- /dev/null +++ b/pub/index.tpl.html @@ -0,0 +1,23 @@ + + + + + Hound + + + + +
+
+
+
+ + + + + + {{ .Source }} + + diff --git a/src/ansi/ansi.go b/src/ansi/ansi.go new file mode 100644 index 00000000..644fec06 --- /dev/null +++ b/src/ansi/ansi.go @@ -0,0 +1,105 @@ +package ansi + +import ( + "fmt" + "os" +) + +var ( + start = "\033[" + reset = "\033[0m" + bold = "1;" + blink = "5;" + underline = "4;" + inverse = "7;" +) + +type Style byte + +const ( + Normal Style = 0x00 + Bold Style = 0x01 + Blink Style = 0x02 + Underline Style = 0x04 + Invert Style = 0x08 + Intense Style = 0x10 +) + +type Color int + +const ( + Black Color = iota + Red + Green + Yellow + Blue + Magenta + Cyan + White + Colorless +) + +const ( + normalFg = 30 + intenseFg = 90 + normalBg = 40 + intenseBg = 100 +) + +type Colorer struct { + enabled bool +} + +func NewFor(f *os.File) *Colorer { + return &Colorer{isTTY(f.Fd())} +} + +func (c *Colorer) Fg(s string, color Color, style Style) string { + return c.FgBg(s, color, style, Colorless, Normal) +} + +func (c *Colorer) FgBg(s string, fgColor Color, fgStyle Style, bgColor Color, bgStyle Style) string { + if !c.enabled { + return s + } + + buf := make([]byte, 0, 24) + buf = append(buf, start...) + + if fgStyle&Bold != 0 { + buf = append(buf, bold...) + } + + if fgStyle&Blink != 0 { + buf = append(buf, blink...) + } + + if fgStyle&Underline != 0 { + buf = append(buf, underline...) + } + + if fgStyle&Invert != 0 { + buf = append(buf, inverse...) + } + + var fgBase int + if fgStyle&Intense == 0 { + fgBase = normalFg + } else { + fgBase = intenseFg + } + buf = append(buf, fmt.Sprintf("%d;", fgBase+int(fgColor))...) + + if bgColor != Colorless { + var bgBase int + if bgStyle&Intense == 0 { + bgBase = normalBg + } else { + bgBase = intenseBg + } + buf = append(buf, fmt.Sprintf("%d;", bgBase+int(bgColor))...) + } + + buf = append(buf[:len(buf)-1], "m"...) + return string(buf) + s + reset +} diff --git a/src/ansi/ansi_test.go b/src/ansi/ansi_test.go new file mode 100644 index 00000000..efe6ae7a --- /dev/null +++ b/src/ansi/ansi_test.go @@ -0,0 +1,88 @@ +package ansi + +import ( + "fmt" + "os" + "strings" + "testing" +) + +var ( + printTests = false +) + +func makeReal(s string) string { + return strings.Replace(s, "~", "\x1b", -1) +} + +func makeFake(s string) string { + return strings.Replace(s, "\x1b", "~", -1) +} + +func assertEqual(t *testing.T, got string, exp string) { + if printTests { + fmt.Println(got) + } + + exp = strings.Replace(exp, "~", "\x1b", -1) + if got != exp { + t.Errorf("mismatch: %s & %s", makeFake(got), makeFake(exp)) + } +} + +func TestEnabled(t *testing.T) { + a := Colorer{true} + assertEqual(t, + a.FgBg("x", Black, Normal, Colorless, Normal), + "~[30mx~[0m") + assertEqual(t, + a.FgBg("x", Red, Normal, Colorless, Normal), + "~[31mx~[0m") + assertEqual(t, + a.FgBg("x", Red, Intense, Colorless, Normal), + "~[91mx~[0m") + assertEqual(t, + a.FgBg("x", Green, Bold|Blink|Underline|Invert, Colorless, Normal), + "~[1;5;4;7;32mx~[0m") + assertEqual(t, + a.FgBg("x", Green, Bold|Blink|Underline|Invert|Intense, Colorless, Normal), + "~[1;5;4;7;92mx~[0m") + + assertEqual(t, + a.FgBg("x", Green, Bold|Blink|Underline|Intense, Magenta, Normal), + "~[1;5;4;92;45mx~[0m") + assertEqual(t, + a.FgBg("x", Yellow, Bold|Blink|Underline|Intense, Cyan, Intense), + "~[1;5;4;93;106mx~[0m") +} + +func TestDisabled(t *testing.T) { + a := Colorer{false} + assertEqual(t, + a.FgBg("x", Black, Normal, Colorless, Normal), + "x") + assertEqual(t, + a.FgBg("foo", Red, Normal, Colorless, Normal), + "foo") + assertEqual(t, + a.FgBg("butter", Red, Intense, Colorless, Normal), + "butter") + assertEqual(t, + a.FgBg("x", Green, Bold|Blink|Underline|Invert, Colorless, Normal), + "x") + assertEqual(t, + a.FgBg("x", Green, Bold|Blink|Underline|Invert|Intense, Colorless, Normal), + "x") + + assertEqual(t, + a.FgBg("x", Green, Bold|Blink|Underline|Intense, Magenta, Normal), + "x") + assertEqual(t, + a.FgBg("x", Yellow, Bold|Blink|Underline|Intense, Cyan, Intense), + "x") +} + +func TestIsTerminal(t *testing.T) { + // just make sure we can call this thing. + isTTY(os.Stdout.Fd()) +} diff --git a/src/ansi/tty.go b/src/ansi/tty.go new file mode 100644 index 00000000..86e8ffa4 --- /dev/null +++ b/src/ansi/tty.go @@ -0,0 +1,21 @@ +package ansi + +import ( + "syscall" + "unsafe" +) + +// Issue a ioctl syscall to try to read a termios for the descriptor. If +// we are unable to read one, this is not a tty. +func isTTY(fd uintptr) bool { + var termios syscall.Termios + _, _, err := syscall.Syscall6( + syscall.SYS_IOCTL, + fd, + ioctlReadTermios, + uintptr(unsafe.Pointer(&termios)), + 0, + 0, + 0) + return err == 0 +} diff --git a/src/ansi/tty_darwin.go b/src/ansi/tty_darwin.go new file mode 100644 index 00000000..74d09ef4 --- /dev/null +++ b/src/ansi/tty_darwin.go @@ -0,0 +1,5 @@ +package ansi + +import "syscall" + +const ioctlReadTermios = syscall.TIOCGETA diff --git a/src/ansi/tty_linux.go b/src/ansi/tty_linux.go new file mode 100644 index 00000000..d6fad23e --- /dev/null +++ b/src/ansi/tty_linux.go @@ -0,0 +1,4 @@ +package ansi + +const ioctlReadTermios = 0x5401 // syscall.TCGETS +const ioctlWriteTermios = 0x5402 // syscall.TCSETS diff --git a/src/code.google.com/p/codesearch/AUTHORS b/src/code.google.com/p/codesearch/AUTHORS new file mode 100644 index 00000000..d7fda85e --- /dev/null +++ b/src/code.google.com/p/codesearch/AUTHORS @@ -0,0 +1,4 @@ +# This source code is copyright "The Go Authors", +# as defined by the AUTHORS file in the root of the Go tree. +# +# http://tip.golang.org/AUTHORS. diff --git a/src/code.google.com/p/codesearch/CONTRIBUTORS b/src/code.google.com/p/codesearch/CONTRIBUTORS new file mode 100644 index 00000000..4546fcee --- /dev/null +++ b/src/code.google.com/p/codesearch/CONTRIBUTORS @@ -0,0 +1,5 @@ +# The official list of people who can contribute code to the repository +# is maintained in the standard Go repository as the CONTRIBUTORS +# file in the root of the Go tree. +# +# http://tip.golang.org/CONTRIBUTORS diff --git a/src/code.google.com/p/codesearch/LICENSE b/src/code.google.com/p/codesearch/LICENSE new file mode 100644 index 00000000..3d2350c7 --- /dev/null +++ b/src/code.google.com/p/codesearch/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2011 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/code.google.com/p/codesearch/README b/src/code.google.com/p/codesearch/README new file mode 100644 index 00000000..839082db --- /dev/null +++ b/src/code.google.com/p/codesearch/README @@ -0,0 +1,16 @@ +Code Search is a tool for indexing and then performing +regular expression searches over large bodies of source code. +It is a set of command-line programs written in Go. +Binary downloads are available for those who do not have Go installed. +See http://code.google.com/p/codesearch/ + +For background and an overview of the commands, +see http://swtch.com/~rsc/regexp/regexp4.html. + +To install: + + go get code.google.com/p/codesearch/cmd/... + +Russ Cox +rsc@swtch.com +January 2012 diff --git a/src/code.google.com/p/codesearch/cmd/cgrep/cgrep.go b/src/code.google.com/p/codesearch/cmd/cgrep/cgrep.go new file mode 100644 index 00000000..15b96dde --- /dev/null +++ b/src/code.google.com/p/codesearch/cmd/cgrep/cgrep.go @@ -0,0 +1,77 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "flag" + "fmt" + "log" + "os" + "runtime/pprof" + + "code.google.com/p/codesearch/regexp" +) + +var usageMessage = `usage: cgrep [-c] [-h] [-i] [-l] [-n] regexp [file...] + +Cgrep behaves like grep, searching for regexp, an RE2 (nearly PCRE) regular expression. + +The -c, -h, -i, -l, and -n flags are as in grep, although note that as per Go's +flag parsing convention, they cannot be combined: the option pair -i -n +cannot be abbreviated to -in. +` + +func usage() { + fmt.Fprintf(os.Stderr, usageMessage) + os.Exit(2) +} + +var ( + iflag = flag.Bool("i", false, "case-insensitive match") + cpuProfile = flag.String("cpuprofile", "", "write cpu profile to this file") +) + +func main() { + var g regexp.Grep + g.AddFlags() + g.Stdout = os.Stdout + g.Stderr = os.Stderr + flag.Usage = usage + flag.Parse() + args := flag.Args() + if len(args) == 0 { + flag.Usage() + } + + if *cpuProfile != "" { + f, err := os.Create(*cpuProfile) + if err != nil { + log.Fatal(err) + } + defer f.Close() + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + + pat := "(?m)" + args[0] + if *iflag { + pat = "(?i)" + pat + } + re, err := regexp.Compile(pat) + if err != nil { + log.Fatal(err) + } + g.Regexp = re + if len(args) == 1 { + g.Reader(os.Stdin, "") + } else { + for _, arg := range args[1:] { + g.File(arg) + } + } + if !g.Match { + os.Exit(1) + } +} diff --git a/src/code.google.com/p/codesearch/cmd/cindex/cindex.go b/src/code.google.com/p/codesearch/cmd/cindex/cindex.go new file mode 100644 index 00000000..040101ed --- /dev/null +++ b/src/code.google.com/p/codesearch/cmd/cindex/cindex.go @@ -0,0 +1,160 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + "runtime/pprof" + "sort" + + "code.google.com/p/codesearch/index" +) + +var usageMessage = `usage: cindex [-list] [-reset] [path...] + +Cindex prepares the trigram index for use by csearch. The index is the +file named by $CSEARCHINDEX, or else $HOME/.csearchindex. + +The simplest invocation is + + cindex path... + +which adds the file or directory tree named by each path to the index. +For example: + + cindex $HOME/src /usr/include + +or, equivalently: + + cindex $HOME/src + cindex /usr/include + +If cindex is invoked with no paths, it reindexes the paths that have +already been added, in case the files have changed. Thus, 'cindex' by +itself is a useful command to run in a nightly cron job. + +The -list flag causes cindex to list the paths it has indexed and exit. + +By default cindex adds the named paths to the index but preserves +information about other paths that might already be indexed +(the ones printed by cindex -list). The -reset flag causes cindex to +delete the existing index before indexing the new paths. +With no path arguments, cindex -reset removes the index. +` + +func usage() { + fmt.Fprintf(os.Stderr, usageMessage) + os.Exit(2) +} + +var ( + listFlag = flag.Bool("list", false, "list indexed paths and exit") + resetFlag = flag.Bool("reset", false, "discard existing index") + verboseFlag = flag.Bool("verbose", false, "print extra information") + cpuProfile = flag.String("cpuprofile", "", "write cpu profile to this file") +) + +func main() { + flag.Usage = usage + flag.Parse() + args := flag.Args() + + if *listFlag { + ix := index.Open(index.File()) + for _, arg := range ix.Paths() { + fmt.Printf("%s\n", arg) + } + return + } + + if *cpuProfile != "" { + f, err := os.Create(*cpuProfile) + if err != nil { + log.Fatal(err) + } + defer f.Close() + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + + if *resetFlag && len(args) == 0 { + os.Remove(index.File()) + return + } + if len(args) == 0 { + ix := index.Open(index.File()) + for _, arg := range ix.Paths() { + args = append(args, arg) + } + } + + // Translate paths to absolute paths so that we can + // generate the file list in sorted order. + for i, arg := range args { + a, err := filepath.Abs(arg) + if err != nil { + log.Printf("%s: %s", arg, err) + args[i] = "" + continue + } + args[i] = a + } + sort.Strings(args) + + for len(args) > 0 && args[0] == "" { + args = args[1:] + } + + master := index.File() + if _, err := os.Stat(master); err != nil { + // Does not exist. + *resetFlag = true + } + file := master + if !*resetFlag { + file += "~" + } + + ix := index.Create(file) + ix.Verbose = *verboseFlag + ix.AddPaths(args) + for _, arg := range args { + log.Printf("index %s", arg) + filepath.Walk(arg, func(path string, info os.FileInfo, err error) error { + if _, elem := filepath.Split(path); elem != "" { + // Skip various temporary or "hidden" files or directories. + if elem[0] == '.' || elem[0] == '#' || elem[0] == '~' || elem[len(elem)-1] == '~' { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + } + if err != nil { + log.Printf("%s: %s", path, err) + return nil + } + if info != nil && info.Mode()&os.ModeType == 0 { + ix.AddFile(path) + } + return nil + }) + } + log.Printf("flush index") + ix.Flush() + + if !*resetFlag { + log.Printf("merge %s %s", master, file) + index.Merge(file+"~", master, file) + os.Remove(file) + os.Rename(file+"~", master) + } + log.Printf("done") + return +} diff --git a/src/code.google.com/p/codesearch/cmd/csearch/csearch.go b/src/code.google.com/p/codesearch/cmd/csearch/csearch.go new file mode 100644 index 00000000..5b55d5ca --- /dev/null +++ b/src/code.google.com/p/codesearch/cmd/csearch/csearch.go @@ -0,0 +1,147 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "flag" + "fmt" + "log" + "os" + "runtime/pprof" + + "code.google.com/p/codesearch/index" + "code.google.com/p/codesearch/regexp" +) + +var usageMessage = `usage: csearch [-c] [-f fileregexp] [-h] [-i] [-l] [-n] regexp + +Csearch behaves like grep over all indexed files, searching for regexp, +an RE2 (nearly PCRE) regular expression. + +The -c, -h, -i, -l, and -n flags are as in grep, although note that as per Go's +flag parsing convention, they cannot be combined: the option pair -i -n +cannot be abbreviated to -in. + +The -f flag restricts the search to files whose names match the RE2 regular +expression fileregexp. + +Csearch relies on the existence of an up-to-date index created ahead of time. +To build or rebuild the index that csearch uses, run: + + cindex path... + +where path... is a list of directories or individual files to be included in the index. +If no index exists, this command creates one. If an index already exists, cindex +overwrites it. Run cindex -help for more. + +Csearch uses the index stored in $CSEARCHINDEX or, if that variable is unset or +empty, $HOME/.csearchindex. +` + +func usage() { + fmt.Fprintf(os.Stderr, usageMessage) + os.Exit(2) +} + +var ( + fFlag = flag.String("f", "", "search only files with names matching this regexp") + iFlag = flag.Bool("i", false, "case-insensitive search") + verboseFlag = flag.Bool("verbose", false, "print extra information") + bruteFlag = flag.Bool("brute", false, "brute force - search all files in index") + cpuProfile = flag.String("cpuprofile", "", "write cpu profile to this file") + + matches bool +) + +func Main() { + g := regexp.Grep{ + Stdout: os.Stdout, + Stderr: os.Stderr, + } + g.AddFlags() + + flag.Usage = usage + flag.Parse() + args := flag.Args() + + if len(args) != 1 { + usage() + } + + if *cpuProfile != "" { + f, err := os.Create(*cpuProfile) + if err != nil { + log.Fatal(err) + } + defer f.Close() + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + + pat := "(?m)" + args[0] + if *iFlag { + pat = "(?i)" + pat + } + re, err := regexp.Compile(pat) + if err != nil { + log.Fatal(err) + } + g.Regexp = re + var fre *regexp.Regexp + if *fFlag != "" { + fre, err = regexp.Compile(*fFlag) + if err != nil { + log.Fatal(err) + } + } + q := index.RegexpQuery(re.Syntax) + if *verboseFlag { + log.Printf("query: %s\n", q) + } + + ix := index.Open(index.File()) + ix.Verbose = *verboseFlag + var post []uint32 + if *bruteFlag { + post = ix.PostingQuery(&index.Query{Op: index.QAll}) + } else { + post = ix.PostingQuery(q) + } + if *verboseFlag { + log.Printf("post query identified %d possible files\n", len(post)) + } + + if fre != nil { + fnames := make([]uint32, 0, len(post)) + + for _, fileid := range post { + name := ix.Name(fileid) + if fre.MatchString(name, true, true) < 0 { + continue + } + fnames = append(fnames, fileid) + } + + if *verboseFlag { + log.Printf("filename regexp matched %d files\n", len(fnames)) + } + post = fnames + } + + for _, fileid := range post { + name := ix.Name(fileid) + g.File(name) + } + + matches = g.Match +} + +func main() { + Main() + if !matches { + os.Exit(1) + } + os.Exit(0) +} diff --git a/src/code.google.com/p/codesearch/index/merge.go b/src/code.google.com/p/codesearch/index/merge.go new file mode 100644 index 00000000..708a531b --- /dev/null +++ b/src/code.google.com/p/codesearch/index/merge.go @@ -0,0 +1,344 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +// Merging indexes. +// +// To merge two indexes A and B (newer) into a combined index C: +// +// Load the path list from B and determine for each path the docid ranges +// that it will replace in A. +// +// Read A's and B's name lists together, merging them into C's name list. +// Discard the identified ranges from A during the merge. Also during the merge, +// record the mapping from A's docids to C's docids, and also the mapping from +// B's docids to C's docids. Both mappings can be summarized in a table like +// +// 10-14 map to 20-24 +// 15-24 is deleted +// 25-34 maps to 40-49 +// +// The number of ranges will be at most the combined number of paths. +// Also during the merge, write the name index to a temporary file as usual. +// +// Now merge the posting lists (this is why they begin with the trigram). +// During the merge, translate the docid numbers to the new C docid space. +// Also during the merge, write the posting list index to a temporary file as usual. +// +// Copy the name index and posting list index into C's index and write the trailer. +// Rename C's index onto the new index. + +import ( + "encoding/binary" + "os" + "strings" +) + +// An idrange records that the half-open interval [lo, hi) maps to [new, new+hi-lo). +type idrange struct { + lo, hi, new uint32 +} + +type postIndex struct { + tri uint32 + count uint32 + offset uint32 +} + +// Merge creates a new index in the file dst that corresponds to merging +// the two indices src1 and src2. If both src1 and src2 claim responsibility +// for a path, src2 is assumed to be newer and is given preference. +func Merge(dst, src1, src2 string) { + ix1 := Open(src1) + ix2 := Open(src2) + paths1 := ix1.Paths() + paths2 := ix2.Paths() + + // Build docid maps. + var i1, i2, new uint32 + var map1, map2 []idrange + for _, path := range paths2 { + // Determine range shadowed by this path. + old := i1 + for i1 < uint32(ix1.numName) && ix1.Name(i1) < path { + i1++ + } + lo := i1 + limit := path[:len(path)-1] + string(path[len(path)-1]+1) + for i1 < uint32(ix1.numName) && ix1.Name(i1) < limit { + i1++ + } + hi := i1 + + // Record range before the shadow. + if old < lo { + map1 = append(map1, idrange{old, lo, new}) + new += lo - old + } + + // Determine range defined by this path. + // Because we are iterating over the ix2 paths, + // there can't be gaps, so it must start at i2. + if i2 < uint32(ix2.numName) && ix2.Name(i2) < path { + panic("merge: inconsistent index") + } + lo = i2 + for i2 < uint32(ix2.numName) && ix2.Name(i2) < limit { + i2++ + } + hi = i2 + if lo < hi { + map2 = append(map2, idrange{lo, hi, new}) + new += hi - lo + } + } + + if i1 < uint32(ix1.numName) { + map1 = append(map1, idrange{i1, uint32(ix1.numName), new}) + new += uint32(ix1.numName) - i1 + } + if i2 < uint32(ix2.numName) { + panic("merge: inconsistent index") + } + numName := new + + ix3 := bufCreate(dst) + ix3.writeString(magic) + + // Merged list of paths. + pathData := ix3.offset() + mi1 := 0 + mi2 := 0 + last := "\x00" // not a prefix of anything + for mi1 < len(paths1) || mi2 < len(paths2) { + var p string + if mi2 >= len(paths2) || mi1 < len(paths1) && paths1[mi1] <= paths2[mi2] { + p = paths1[mi1] + mi1++ + } else { + p = paths2[mi2] + mi2++ + } + if strings.HasPrefix(p, last) { + continue + } + last = p + ix3.writeString(p) + ix3.writeString("\x00") + } + ix3.writeString("\x00") + + // Merged list of names. + nameData := ix3.offset() + nameIndexFile := bufCreate("") + new = 0 + mi1 = 0 + mi2 = 0 + for new < numName { + if mi1 < len(map1) && map1[mi1].new == new { + for i := map1[mi1].lo; i < map1[mi1].hi; i++ { + name := ix1.Name(i) + nameIndexFile.writeUint32(ix3.offset() - nameData) + ix3.writeString(name) + ix3.writeString("\x00") + new++ + } + mi1++ + } else if mi2 < len(map2) && map2[mi2].new == new { + for i := map2[mi2].lo; i < map2[mi2].hi; i++ { + name := ix2.Name(i) + nameIndexFile.writeUint32(ix3.offset() - nameData) + ix3.writeString(name) + ix3.writeString("\x00") + new++ + } + mi2++ + } else { + panic("merge: inconsistent index") + } + } + if new*4 != nameIndexFile.offset() { + panic("merge: inconsistent index") + } + nameIndexFile.writeUint32(ix3.offset()) + + // Merged list of posting lists. + postData := ix3.offset() + var r1 postMapReader + var r2 postMapReader + var w postDataWriter + r1.init(ix1, map1) + r2.init(ix2, map2) + w.init(ix3) + for { + if r1.trigram < r2.trigram { + w.trigram(r1.trigram) + for r1.nextId() { + w.fileid(r1.fileid) + } + r1.nextTrigram() + w.endTrigram() + } else if r2.trigram < r1.trigram { + w.trigram(r2.trigram) + for r2.nextId() { + w.fileid(r2.fileid) + } + r2.nextTrigram() + w.endTrigram() + } else { + if r1.trigram == ^uint32(0) { + break + } + w.trigram(r1.trigram) + r1.nextId() + r2.nextId() + for r1.fileid < ^uint32(0) || r2.fileid < ^uint32(0) { + if r1.fileid < r2.fileid { + w.fileid(r1.fileid) + r1.nextId() + } else if r2.fileid < r1.fileid { + w.fileid(r2.fileid) + r2.nextId() + } else { + panic("merge: inconsistent index") + } + } + r1.nextTrigram() + r2.nextTrigram() + w.endTrigram() + } + } + + // Name index + nameIndex := ix3.offset() + copyFile(ix3, nameIndexFile) + + // Posting list index + postIndex := ix3.offset() + copyFile(ix3, w.postIndexFile) + + ix3.writeUint32(pathData) + ix3.writeUint32(nameData) + ix3.writeUint32(postData) + ix3.writeUint32(nameIndex) + ix3.writeUint32(postIndex) + ix3.writeString(trailerMagic) + ix3.flush() + + os.Remove(nameIndexFile.name) + os.Remove(w.postIndexFile.name) +} + +type postMapReader struct { + ix *Index + idmap []idrange + triNum uint32 + trigram uint32 + count uint32 + offset uint32 + d []byte + oldid uint32 + fileid uint32 + i int +} + +func (r *postMapReader) init(ix *Index, idmap []idrange) { + r.ix = ix + r.idmap = idmap + r.trigram = ^uint32(0) + r.load() +} + +func (r *postMapReader) nextTrigram() { + r.triNum++ + r.load() +} + +func (r *postMapReader) load() { + if r.triNum >= uint32(r.ix.numPost) { + r.trigram = ^uint32(0) + r.count = 0 + r.fileid = ^uint32(0) + return + } + r.trigram, r.count, r.offset = r.ix.listAt(r.triNum * postEntrySize) + if r.count == 0 { + r.fileid = ^uint32(0) + return + } + r.d = r.ix.slice(r.ix.postData+r.offset+3, -1) + r.oldid = ^uint32(0) + r.i = 0 +} + +func (r *postMapReader) nextId() bool { + for r.count > 0 { + r.count-- + delta64, n := binary.Uvarint(r.d) + delta := uint32(delta64) + if n <= 0 || delta == 0 { + corrupt(r.ix.data.f) + } + r.d = r.d[n:] + r.oldid += delta + for r.i < len(r.idmap) && r.idmap[r.i].hi <= r.oldid { + r.i++ + } + if r.i >= len(r.idmap) { + r.count = 0 + break + } + if r.oldid < r.idmap[r.i].lo { + continue + } + r.fileid = r.idmap[r.i].new + r.oldid - r.idmap[r.i].lo + return true + } + + r.fileid = ^uint32(0) + return false +} + +type postDataWriter struct { + out *bufWriter + postIndexFile *bufWriter + buf [10]byte + base uint32 + count, offset uint32 + last uint32 + t uint32 +} + +func (w *postDataWriter) init(out *bufWriter) { + w.out = out + w.postIndexFile = bufCreate("") + w.base = out.offset() +} + +func (w *postDataWriter) trigram(t uint32) { + w.offset = w.out.offset() + w.count = 0 + w.t = t + w.last = ^uint32(0) +} + +func (w *postDataWriter) fileid(id uint32) { + if w.count == 0 { + w.out.writeTrigram(w.t) + } + w.out.writeUvarint(id - w.last) + w.last = id + w.count++ +} + +func (w *postDataWriter) endTrigram() { + if w.count == 0 { + return + } + w.out.writeUvarint(0) + w.postIndexFile.writeTrigram(w.t) + w.postIndexFile.writeUint32(w.count) + w.postIndexFile.writeUint32(w.offset - w.base) +} diff --git a/src/code.google.com/p/codesearch/index/merge_test.go b/src/code.google.com/p/codesearch/index/merge_test.go new file mode 100644 index 00000000..3e36d106 --- /dev/null +++ b/src/code.google.com/p/codesearch/index/merge_test.go @@ -0,0 +1,102 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +import ( + "io/ioutil" + "os" + "testing" +) + +var mergePaths1 = []string{ + "/a", + "/b", + "/c", +} + +var mergePaths2 = []string{ + "/b", + "/cc", +} + +var mergeFiles1 = map[string]string{ + "/a/x": "hello world", + "/a/y": "goodbye world", + "/b/xx": "now is the time", + "/b/xy": "for all good men", + "/c/ab": "give me all the potatoes", + "/c/de": "or give me death now", +} + +var mergeFiles2 = map[string]string{ + "/b/www": "world wide indeed", + "/b/xx": "no, not now", + "/b/yy": "first potatoes, now liberty?", + "/cc": "come to the aid of his potatoes", +} + +func TestMerge(t *testing.T) { + f1, _ := ioutil.TempFile("", "index-test") + f2, _ := ioutil.TempFile("", "index-test") + f3, _ := ioutil.TempFile("", "index-test") + defer os.Remove(f1.Name()) + defer os.Remove(f2.Name()) + defer os.Remove(f3.Name()) + + out1 := f1.Name() + out2 := f2.Name() + out3 := f3.Name() + + buildIndex(out1, mergePaths1, mergeFiles1) + buildIndex(out2, mergePaths2, mergeFiles2) + + Merge(out3, out1, out2) + + ix1 := Open(out1) + ix2 := Open(out2) + ix3 := Open(out3) + + nameof := func(ix *Index) string { + switch { + case ix == ix1: + return "ix1" + case ix == ix2: + return "ix2" + case ix == ix3: + return "ix3" + } + return "???" + } + + checkFiles := func(ix *Index, l ...string) { + for i, s := range l { + if n := ix.Name(uint32(i)); n != s { + t.Errorf("%s: Name(%d) = %s, want %s", nameof(ix), i, n, s) + } + } + } + + checkFiles(ix1, "/a/x", "/a/y", "/b/xx", "/b/xy", "/c/ab", "/c/de") + checkFiles(ix2, "/b/www", "/b/xx", "/b/yy", "/cc") + checkFiles(ix3, "/a/x", "/a/y", "/b/www", "/b/xx", "/b/yy", "/c/ab", "/c/de", "/cc") + + check := func(ix *Index, trig string, l ...uint32) { + l1 := ix.PostingList(tri(trig[0], trig[1], trig[2])) + if !equalList(l1, l) { + t.Errorf("PostingList(%s, %s) = %v, want %v", nameof(ix), trig, l1, l) + } + } + + check(ix1, "wor", 0, 1) + check(ix1, "now", 2, 5) + check(ix1, "all", 3, 4) + + check(ix2, "now", 1, 2) + + check(ix3, "all", 5) + check(ix3, "wor", 0, 1, 2) + check(ix3, "now", 3, 4, 6) + check(ix3, "pot", 4, 5, 7) +} diff --git a/src/code.google.com/p/codesearch/index/mmap_bsd.go b/src/code.google.com/p/codesearch/index/mmap_bsd.go new file mode 100644 index 00000000..5195bf83 --- /dev/null +++ b/src/code.google.com/p/codesearch/index/mmap_bsd.go @@ -0,0 +1,33 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin freebsd openbsd netbsd + +package index + +import ( + "log" + "os" + "syscall" +) + +func mmapFile(f *os.File) mmapData { + st, err := f.Stat() + if err != nil { + log.Fatal(err) + } + size := st.Size() + if int64(int(size+4095)) != size+4095 { + log.Fatalf("%s: too large for mmap", f.Name()) + } + n := int(size) + if n == 0 { + return mmapData{f, nil, nil} + } + data, err := syscall.Mmap(int(f.Fd()), 0, (n+4095)&^4095, syscall.PROT_READ, syscall.MAP_PRIVATE) + if err != nil { + log.Fatalf("mmap %s: %v", f.Name(), err) + } + return mmapData{f, data[:n], data} +} diff --git a/src/code.google.com/p/codesearch/index/mmap_linux.go b/src/code.google.com/p/codesearch/index/mmap_linux.go new file mode 100644 index 00000000..dcc6c976 --- /dev/null +++ b/src/code.google.com/p/codesearch/index/mmap_linux.go @@ -0,0 +1,31 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +import ( + "log" + "os" + "syscall" +) + +func mmapFile(f *os.File) mmapData { + st, err := f.Stat() + if err != nil { + log.Fatal(err) + } + size := st.Size() + if int64(int(size+4095)) != size+4095 { + log.Fatalf("%s: too large for mmap", f.Name()) + } + n := int(size) + if n == 0 { + return mmapData{f, nil, nil} + } + data, err := syscall.Mmap(int(f.Fd()), 0, (n+4095)&^4095, syscall.PROT_READ, syscall.MAP_SHARED) + if err != nil { + log.Fatalf("mmap %s: %v", f.Name(), err) + } + return mmapData{f, data[:n], data} +} diff --git a/src/code.google.com/p/codesearch/index/mmap_windows.go b/src/code.google.com/p/codesearch/index/mmap_windows.go new file mode 100644 index 00000000..4d547d9e --- /dev/null +++ b/src/code.google.com/p/codesearch/index/mmap_windows.go @@ -0,0 +1,37 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +import ( + "log" + "os" + "syscall" + "unsafe" +) + +func mmapFile(f *os.File) mmapData { + st, err := f.Stat() + if err != nil { + log.Fatal(err) + } + size := st.Size() + if int64(int(size+4095)) != size+4095 { + log.Fatalf("%s: too large for mmap", f.Name()) + } + if size == 0 { + return mmapData{f, nil} + } + h, err := syscall.CreateFileMapping(f.Fd(), nil, syscall.PAGE_READONLY, uint32(size>>32), uint32(size), nil) + if err != nil { + log.Fatalf("CreateFileMapping %s: %v", f.Name(), err) + } + + addr, err := syscall.MapViewOfFile(h, syscall.FILE_MAP_READ, 0, 0, 0) + if err != nil { + log.Fatalf("MapViewOfFile %s: %v", f.Name(), err) + } + data := (*[1 << 30]byte)(unsafe.Pointer(addr)) + return mmapData{f, data[:size]} +} diff --git a/src/code.google.com/p/codesearch/index/read.go b/src/code.google.com/p/codesearch/index/read.go new file mode 100644 index 00000000..49b30d84 --- /dev/null +++ b/src/code.google.com/p/codesearch/index/read.go @@ -0,0 +1,455 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +// Index format. +// +// An index stored on disk has the format: +// +// "csearch index 1\n" +// list of paths +// list of names +// list of posting lists +// name index +// posting list index +// trailer +// +// The list of paths is a sorted sequence of NUL-terminated file or directory names. +// The index covers the file trees rooted at those paths. +// The list ends with an empty name ("\x00"). +// +// The list of names is a sorted sequence of NUL-terminated file names. +// The initial entry in the list corresponds to file #0, +// the next to file #1, and so on. The list ends with an +// empty name ("\x00"). +// +// The list of posting lists are a sequence of posting lists. +// Each posting list has the form: +// +// trigram [3] +// deltas [v]... +// +// The trigram gives the 3 byte trigram that this list describes. The +// delta list is a sequence of varint-encoded deltas between file +// IDs, ending with a zero delta. For example, the delta list [2,5,1,1,0] +// encodes the file ID list 1, 6, 7, 8. The delta list [0] would +// encode the empty file ID list, but empty posting lists are usually +// not recorded at all. The list of posting lists ends with an entry +// with trigram "\xff\xff\xff" and a delta list consisting a single zero. +// +// The indexes enable efficient random access to the lists. The name +// index is a sequence of 4-byte big-endian values listing the byte +// offset in the name list where each name begins. The posting list +// index is a sequence of index entries describing each successive +// posting list. Each index entry has the form: +// +// trigram [3] +// file count [4] +// offset [4] +// +// Index entries are only written for the non-empty posting lists, +// so finding the posting list for a specific trigram requires a +// binary search over the posting list index. In practice, the majority +// of the possible trigrams are never seen, so omitting the missing +// ones represents a significant storage savings. +// +// The trailer has the form: +// +// offset of path list [4] +// offset of name list [4] +// offset of posting lists [4] +// offset of name index [4] +// offset of posting list index [4] +// "\ncsearch trailr\n" + +import ( + "bytes" + "encoding/binary" + "log" + "os" + "runtime" + "sort" + "syscall" +) + +const ( + magic = "csearch index 1\n" + trailerMagic = "\ncsearch trailr\n" +) + +// An Index implements read-only access to a trigram index. +type Index struct { + Verbose bool + data mmapData + pathData uint32 + nameData uint32 + postData uint32 + nameIndex uint32 + postIndex uint32 + numName int + numPost int +} + +const postEntrySize = 3 + 4 + 4 + +func Open(file string) *Index { + mm := mmap(file) + if len(mm.d) < 4*4+len(trailerMagic) || string(mm.d[len(mm.d)-len(trailerMagic):]) != trailerMagic { + corrupt(mm.f) + } + n := uint32(len(mm.d) - len(trailerMagic) - 5*4) + ix := &Index{data: mm} + ix.pathData = ix.uint32(n) + ix.nameData = ix.uint32(n + 4) + ix.postData = ix.uint32(n + 8) + ix.nameIndex = ix.uint32(n + 12) + ix.postIndex = ix.uint32(n + 16) + ix.numName = int((ix.postIndex-ix.nameIndex)/4) - 1 + ix.numPost = int((n - ix.postIndex) / postEntrySize) + return ix +} + +// slice returns the slice of index data starting at the given byte offset. +// If n >= 0, the slice must have length at least n and is truncated to length n. +func (ix *Index) slice(off uint32, n int) []byte { + o := int(off) + if uint32(o) != off || n >= 0 && o+n > len(ix.data.d) { + corrupt(ix.data.f) + } + if n < 0 { + return ix.data.d[o:] + } + return ix.data.d[o : o+n] +} + +// uint32 returns the uint32 value at the given offset in the index data. +func (ix *Index) uint32(off uint32) uint32 { + return binary.BigEndian.Uint32(ix.slice(off, 4)) +} + +func (ix *Index) Close() error { + return ix.data.close() +} + +// uvarint returns the varint value at the given offset in the index data. +func (ix *Index) uvarint(off uint32) uint32 { + v, n := binary.Uvarint(ix.slice(off, -1)) + if n <= 0 { + corrupt(ix.data.f) + } + return uint32(v) +} + +// Paths returns the list of indexed paths. +func (ix *Index) Paths() []string { + off := ix.pathData + var x []string + for { + s := ix.str(off) + if len(s) == 0 { + break + } + x = append(x, string(s)) + off += uint32(len(s) + 1) + } + return x +} + +// NameBytes returns the name corresponding to the given fileid. +func (ix *Index) NameBytes(fileid uint32) []byte { + off := ix.uint32(ix.nameIndex + 4*fileid) + return ix.str(ix.nameData + off) +} + +func (ix *Index) str(off uint32) []byte { + str := ix.slice(off, -1) + i := bytes.IndexByte(str, '\x00') + if i < 0 { + corrupt(ix.data.f) + } + return str[:i] +} + +// Name returns the name corresponding to the given fileid. +func (ix *Index) Name(fileid uint32) string { + return string(ix.NameBytes(fileid)) +} + +// listAt returns the index list entry at the given offset. +func (ix *Index) listAt(off uint32) (trigram, count, offset uint32) { + d := ix.slice(ix.postIndex+off, postEntrySize) + trigram = uint32(d[0])<<16 | uint32(d[1])<<8 | uint32(d[2]) + count = binary.BigEndian.Uint32(d[3:]) + offset = binary.BigEndian.Uint32(d[3+4:]) + return +} + +func (ix *Index) dumpPosting() { + d := ix.slice(ix.postIndex, postEntrySize*ix.numPost) + for i := 0; i < ix.numPost; i++ { + j := i * postEntrySize + t := uint32(d[j])<<16 | uint32(d[j+1])<<8 | uint32(d[j+2]) + count := int(binary.BigEndian.Uint32(d[j+3:])) + offset := binary.BigEndian.Uint32(d[j+3+4:]) + log.Printf("%#x: %d at %d", t, count, offset) + } +} + +func (ix *Index) findList(trigram uint32) (count int, offset uint32) { + // binary search + d := ix.slice(ix.postIndex, postEntrySize*ix.numPost) + i := sort.Search(ix.numPost, func(i int) bool { + i *= postEntrySize + t := uint32(d[i])<<16 | uint32(d[i+1])<<8 | uint32(d[i+2]) + return t >= trigram + }) + if i >= ix.numPost { + return 0, 0 + } + i *= postEntrySize + t := uint32(d[i])<<16 | uint32(d[i+1])<<8 | uint32(d[i+2]) + if t != trigram { + return 0, 0 + } + count = int(binary.BigEndian.Uint32(d[i+3:])) + offset = binary.BigEndian.Uint32(d[i+3+4:]) + return +} + +type postReader struct { + ix *Index + count int + offset uint32 + fileid uint32 + d []byte + restrict []uint32 +} + +func (r *postReader) init(ix *Index, trigram uint32, restrict []uint32) { + count, offset := ix.findList(trigram) + if count == 0 { + return + } + r.ix = ix + r.count = count + r.offset = offset + r.fileid = ^uint32(0) + r.d = ix.slice(ix.postData+offset+3, -1) + r.restrict = restrict +} + +func (r *postReader) max() int { + return int(r.count) +} + +func (r *postReader) next() bool { + for r.count > 0 { + r.count-- + delta64, n := binary.Uvarint(r.d) + delta := uint32(delta64) + if n <= 0 || delta == 0 { + corrupt(r.ix.data.f) + } + r.d = r.d[n:] + r.fileid += delta + if r.restrict != nil { + i := 0 + for i < len(r.restrict) && r.restrict[i] < r.fileid { + i++ + } + r.restrict = r.restrict[i:] + if len(r.restrict) == 0 || r.restrict[0] != r.fileid { + continue + } + } + return true + } + // list should end with terminating 0 delta + if r.d != nil && (len(r.d) == 0 || r.d[0] != 0) { + corrupt(r.ix.data.f) + } + r.fileid = ^uint32(0) + return false +} + +func (ix *Index) PostingList(trigram uint32) []uint32 { + return ix.postingList(trigram, nil) +} + +func (ix *Index) postingList(trigram uint32, restrict []uint32) []uint32 { + var r postReader + r.init(ix, trigram, restrict) + x := make([]uint32, 0, r.max()) + for r.next() { + x = append(x, r.fileid) + } + return x +} + +func (ix *Index) PostingAnd(list []uint32, trigram uint32) []uint32 { + return ix.postingAnd(list, trigram, nil) +} + +func (ix *Index) postingAnd(list []uint32, trigram uint32, restrict []uint32) []uint32 { + var r postReader + r.init(ix, trigram, restrict) + x := list[:0] + i := 0 + for r.next() { + fileid := r.fileid + for i < len(list) && list[i] < fileid { + i++ + } + if i < len(list) && list[i] == fileid { + x = append(x, fileid) + i++ + } + } + return x +} + +func (ix *Index) PostingOr(list []uint32, trigram uint32) []uint32 { + return ix.postingOr(list, trigram, nil) +} + +func (ix *Index) postingOr(list []uint32, trigram uint32, restrict []uint32) []uint32 { + var r postReader + r.init(ix, trigram, restrict) + x := make([]uint32, 0, len(list)+r.max()) + i := 0 + for r.next() { + fileid := r.fileid + for i < len(list) && list[i] < fileid { + x = append(x, list[i]) + i++ + } + x = append(x, fileid) + if i < len(list) && list[i] == fileid { + i++ + } + } + x = append(x, list[i:]...) + return x +} + +func (ix *Index) PostingQuery(q *Query) []uint32 { + return ix.postingQuery(q, nil) +} + +func (ix *Index) postingQuery(q *Query, restrict []uint32) (ret []uint32) { + var list []uint32 + switch q.Op { + case QNone: + // nothing + case QAll: + if restrict != nil { + return restrict + } + list = make([]uint32, ix.numName) + for i := range list { + list[i] = uint32(i) + } + return list + case QAnd: + for _, t := range q.Trigram { + tri := uint32(t[0])<<16 | uint32(t[1])<<8 | uint32(t[2]) + if list == nil { + list = ix.postingList(tri, restrict) + } else { + list = ix.postingAnd(list, tri, restrict) + } + if len(list) == 0 { + return nil + } + } + for _, sub := range q.Sub { + if list == nil { + list = restrict + } + list = ix.postingQuery(sub, list) + if len(list) == 0 { + return nil + } + } + case QOr: + for _, t := range q.Trigram { + tri := uint32(t[0])<<16 | uint32(t[1])<<8 | uint32(t[2]) + if list == nil { + list = ix.postingList(tri, restrict) + } else { + list = ix.postingOr(list, tri, restrict) + } + } + for _, sub := range q.Sub { + list1 := ix.postingQuery(sub, restrict) + list = mergeOr(list, list1) + } + } + return list +} + +func mergeOr(l1, l2 []uint32) []uint32 { + var l []uint32 + i := 0 + j := 0 + for i < len(l1) || j < len(l2) { + switch { + case j == len(l2) || (i < len(l1) && l1[i] < l2[j]): + l = append(l, l1[i]) + i++ + case i == len(l1) || (j < len(l2) && l1[i] > l2[j]): + l = append(l, l2[j]) + j++ + case l1[i] == l2[j]: + l = append(l, l1[i]) + i++ + j++ + } + } + return l +} + +func corrupt(file *os.File) { + log.Fatalf("corrupt index: %s", file.Name()) +} + +// An mmapData is mmap'ed read-only data from a file. +type mmapData struct { + f *os.File + d []byte + o []byte +} + +func (m *mmapData) close() error { + if err := syscall.Munmap(m.o); err != nil { + return err + } + return m.f.Close() +} + +// mmap maps the given file into memory. +func mmap(file string) mmapData { + f, err := os.Open(file) + if err != nil { + log.Fatal(err) + } + return mmapFile(f) +} + +// File returns the name of the index file to use. +// It is either $CSEARCHINDEX or $HOME/.csearchindex. +func File() string { + f := os.Getenv("CSEARCHINDEX") + if f != "" { + return f + } + var home string + if runtime.GOOS == "windows" { + home = os.Getenv("HOMEPATH") + } else { + home = os.Getenv("HOME") + } + return home + "/.csearchindex" +} diff --git a/src/code.google.com/p/codesearch/index/read_test.go b/src/code.google.com/p/codesearch/index/read_test.go new file mode 100644 index 00000000..4a75c3a6 --- /dev/null +++ b/src/code.google.com/p/codesearch/index/read_test.go @@ -0,0 +1,60 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +import ( + "io/ioutil" + "os" + "testing" +) + +var postFiles = map[string]string{ + "file0": "", + "file1": "Google Code Search", + "file2": "Google Code Project Hosting", + "file3": "Google Web Search", +} + +func tri(x, y, z byte) uint32 { + return uint32(x)<<16 | uint32(y)<<8 | uint32(z) +} + +func TestTrivialPosting(t *testing.T) { + f, _ := ioutil.TempFile("", "index-test") + defer os.Remove(f.Name()) + out := f.Name() + buildIndex(out, nil, postFiles) + ix := Open(out) + if l := ix.PostingList(tri('S', 'e', 'a')); !equalList(l, []uint32{1, 3}) { + t.Errorf("PostingList(Sea) = %v, want [1 3]", l) + } + if l := ix.PostingList(tri('G', 'o', 'o')); !equalList(l, []uint32{1, 2, 3}) { + t.Errorf("PostingList(Goo) = %v, want [1 2 3]", l) + } + if l := ix.PostingAnd(ix.PostingList(tri('S', 'e', 'a')), tri('G', 'o', 'o')); !equalList(l, []uint32{1, 3}) { + t.Errorf("PostingList(Sea&Goo) = %v, want [1 3]", l) + } + if l := ix.PostingAnd(ix.PostingList(tri('G', 'o', 'o')), tri('S', 'e', 'a')); !equalList(l, []uint32{1, 3}) { + t.Errorf("PostingList(Goo&Sea) = %v, want [1 3]", l) + } + if l := ix.PostingOr(ix.PostingList(tri('S', 'e', 'a')), tri('G', 'o', 'o')); !equalList(l, []uint32{1, 2, 3}) { + t.Errorf("PostingList(Sea|Goo) = %v, want [1 2 3]", l) + } + if l := ix.PostingOr(ix.PostingList(tri('G', 'o', 'o')), tri('S', 'e', 'a')); !equalList(l, []uint32{1, 2, 3}) { + t.Errorf("PostingList(Goo|Sea) = %v, want [1 2 3]", l) + } +} + +func equalList(x, y []uint32) bool { + if len(x) != len(y) { + return false + } + for i, xi := range x { + if xi != y[i] { + return false + } + } + return true +} diff --git a/src/code.google.com/p/codesearch/index/regexp.go b/src/code.google.com/p/codesearch/index/regexp.go new file mode 100644 index 00000000..4f336fea --- /dev/null +++ b/src/code.google.com/p/codesearch/index/regexp.go @@ -0,0 +1,872 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +import ( + "regexp/syntax" + "sort" + "strconv" + "strings" + "unicode" +) + +// A Query is a matching machine, like a regular expression, +// that matches some text and not other text. When we compute a +// Query from a regexp, the Query is a conservative version of the +// regexp: it matches everything the regexp would match, and probably +// quite a bit more. We can then filter target files by whether they match +// the Query (using a trigram index) before running the comparatively +// more expensive regexp machinery. +type Query struct { + Op QueryOp + Trigram []string + Sub []*Query +} + +type QueryOp int + +const ( + QAll QueryOp = iota // Everything matches + QNone // Nothing matches + QAnd // All in Sub and Trigram must match + QOr // At least one in Sub or Trigram must match +) + +var allQuery = &Query{Op: QAll} +var noneQuery = &Query{Op: QNone} + +// and returns the query q AND r, possibly reusing q's and r's storage. +func (q *Query) and(r *Query) *Query { + return q.andOr(r, QAnd) +} + +// or returns the query q OR r, possibly reusing q's and r's storage. +func (q *Query) or(r *Query) *Query { + return q.andOr(r, QOr) +} + +// andOr returns the query q AND r or q OR r, possibly reusing q's and r's storage. +// It works hard to avoid creating unnecessarily complicated structures. +func (q *Query) andOr(r *Query, op QueryOp) (out *Query) { + opstr := "&" + if op == QOr { + opstr = "|" + } + //println("andOr", q.String(), opstr, r.String()) + //defer func() { println(" ->", out.String()) }() + _ = opstr + + if len(q.Trigram) == 0 && len(q.Sub) == 1 { + q = q.Sub[0] + } + if len(r.Trigram) == 0 && len(r.Sub) == 1 { + r = r.Sub[0] + } + + // Boolean simplification. + // If q ⇒ r, q AND r ≡ q. + // If q ⇒ r, q OR r ≡ r. + if q.implies(r) { + //println(q.String(), "implies", r.String()) + if op == QAnd { + return q + } + return r + } + if r.implies(q) { + //println(r.String(), "implies", q.String()) + if op == QAnd { + return r + } + return q + } + + // Both q and r are QAnd or QOr. + // If they match or can be made to match, merge. + qAtom := len(q.Trigram) == 1 && len(q.Sub) == 0 + rAtom := len(r.Trigram) == 1 && len(r.Sub) == 0 + if q.Op == op && (r.Op == op || rAtom) { + q.Trigram = stringSet.union(q.Trigram, r.Trigram, false) + q.Sub = append(q.Sub, r.Sub...) + return q + } + if r.Op == op && qAtom { + r.Trigram = stringSet.union(r.Trigram, q.Trigram, false) + return r + } + if qAtom && rAtom { + q.Op = op + q.Trigram = append(q.Trigram, r.Trigram...) + return q + } + + // If one matches the op, add the other to it. + if q.Op == op { + q.Sub = append(q.Sub, r) + return q + } + if r.Op == op { + r.Sub = append(r.Sub, q) + return r + } + + // We are creating an AND of ORs or an OR of ANDs. + // Factor out common trigrams, if any. + common := stringSet{} + i, j := 0, 0 + wi, wj := 0, 0 + for i < len(q.Trigram) && j < len(r.Trigram) { + qt, rt := q.Trigram[i], r.Trigram[j] + if qt < rt { + q.Trigram[wi] = qt + wi++ + i++ + } else if qt > rt { + r.Trigram[wj] = rt + wj++ + j++ + } else { + common = append(common, qt) + i++ + j++ + } + } + for ; i < len(q.Trigram); i++ { + q.Trigram[wi] = q.Trigram[i] + wi++ + } + for ; j < len(r.Trigram); j++ { + r.Trigram[wj] = r.Trigram[j] + wj++ + } + q.Trigram = q.Trigram[:wi] + r.Trigram = r.Trigram[:wj] + if len(common) > 0 { + // If there were common trigrams, rewrite + // + // (abc|def|ghi|jkl) AND (abc|def|mno|prs) => + // (abc|def) OR ((ghi|jkl) AND (mno|prs)) + // + // (abc&def&ghi&jkl) OR (abc&def&mno&prs) => + // (abc&def) AND ((ghi&jkl) OR (mno&prs)) + // + // Build up the right one of + // (ghi|jkl) AND (mno|prs) + // (ghi&jkl) OR (mno&prs) + // Call andOr recursively in case q and r can now be simplified + // (we removed some trigrams). + s := q.andOr(r, op) + + // Add in factored trigrams. + otherOp := QAnd + QOr - op + t := &Query{Op: otherOp, Trigram: common} + return t.andOr(s, t.Op) + } + + // Otherwise just create the op. + return &Query{Op: op, Sub: []*Query{q, r}} +} + +// implies reports whether q implies r. +// It is okay for it to return false negatives. +func (q *Query) implies(r *Query) bool { + if q.Op == QNone || r.Op == QAll { + // False implies everything. + // Everything implies True. + return true + } + if q.Op == QAll || r.Op == QNone { + // True implies nothing. + // Nothing implies False. + return false + } + + if q.Op == QAnd || (q.Op == QOr && len(q.Trigram) == 1 && len(q.Sub) == 0) { + return trigramsImply(q.Trigram, r) + } + + if q.Op == QOr && r.Op == QOr && + len(q.Trigram) > 0 && len(q.Sub) == 0 && + stringSet.isSubsetOf(q.Trigram, r.Trigram) { + return true + } + return false +} + +func trigramsImply(t []string, q *Query) bool { + switch q.Op { + case QOr: + for _, qq := range q.Sub { + if trigramsImply(t, qq) { + return true + } + } + for i := range t { + if stringSet.isSubsetOf(t[i:i+1], q.Trigram) { + return true + } + } + return false + case QAnd: + for _, qq := range q.Sub { + if !trigramsImply(t, qq) { + return false + } + } + if !stringSet.isSubsetOf(q.Trigram, t) { + return false + } + return true + } + return false +} + +// maybeRewrite rewrites q to use op if it is possible to do so +// without changing the meaning. It also simplifies if the node +// is an empty OR or AND. +func (q *Query) maybeRewrite(op QueryOp) { + if q.Op != QAnd && q.Op != QOr { + return + } + + // AND/OR doing real work? Can't rewrite. + n := len(q.Sub) + len(q.Trigram) + if n > 1 { + return + } + + // Nothing left in the AND/OR? + if n == 0 { + if q.Op == QAnd { + q.Op = QAll + } else { + q.Op = QNone + } + return + } + + // Just a sub-node: throw away wrapper. + if len(q.Sub) == 1 { + *q = *q.Sub[0] + } + + // Just a trigram: can use either op. + q.Op = op +} + +// andTrigrams returns q AND the OR of the AND of the trigrams present in each string. +func (q *Query) andTrigrams(t stringSet) *Query { + if t.minLen() < 3 { + // If there is a short string, we can't guarantee + // that any trigrams must be present, so use ALL. + // q AND ALL = q. + return q + } + + //println("andtrigrams", strings.Join(t, ",")) + or := noneQuery + for _, tt := range t { + var trig stringSet + for i := 0; i+3 <= len(tt); i++ { + trig.add(tt[i : i+3]) + } + trig.clean(false) + //println(tt, "trig", strings.Join(trig, ",")) + or = or.or(&Query{Op: QAnd, Trigram: trig}) + } + q = q.and(or) + return q +} + +func (q *Query) String() string { + if q == nil { + return "?" + } + if q.Op == QNone { + return "-" + } + if q.Op == QAll { + return "+" + } + + if len(q.Sub) == 0 && len(q.Trigram) == 1 { + return strconv.Quote(q.Trigram[0]) + } + + var ( + s string + sjoin string + end string + tjoin string + ) + if q.Op == QAnd { + sjoin = " " + tjoin = " " + } else { + s = "(" + sjoin = ")|(" + end = ")" + tjoin = "|" + } + for i, t := range q.Trigram { + if i > 0 { + s += tjoin + } + s += strconv.Quote(t) + } + if len(q.Sub) > 0 { + if len(q.Trigram) > 0 { + s += sjoin + } + s += q.Sub[0].String() + for i := 1; i < len(q.Sub); i++ { + s += sjoin + q.Sub[i].String() + } + } + s += end + return s +} + +// RegexpQuery returns a Query for the given regexp. +func RegexpQuery(re *syntax.Regexp) *Query { + info := analyze(re) + info.simplify(true) + info.addExact() + return info.match +} + +// A regexpInfo summarizes the results of analyzing a regexp. +type regexpInfo struct { + // canEmpty records whether the regexp matches the empty string + canEmpty bool + + // exact is the exact set of strings matching the regexp. + exact stringSet + + // if exact is nil, prefix is the set of possible match prefixes, + // and suffix is the set of possible match suffixes. + prefix stringSet // otherwise: the exact set of matching prefixes ... + suffix stringSet // ... and suffixes + + // match records a query that must be satisfied by any + // match for the regexp, in addition to the information + // recorded above. + match *Query +} + +const ( + // Exact sets are limited to maxExact strings. + // If they get too big, simplify will rewrite the regexpInfo + // to use prefix and suffix instead. It's not worthwhile for + // this to be bigger than maxSet. + // Because we allow the maximum length of an exact string + // to grow to 5 below (see simplify), it helps to avoid ridiculous + // alternations if maxExact is sized so that 3 case-insensitive letters + // triggers a flush. + maxExact = 7 + + // Prefix and suffix sets are limited to maxSet strings. + // If they get too big, simplify will replace groups of strings + // sharing a common leading prefix (or trailing suffix) with + // that common prefix (or suffix). It is useful for maxSet + // to be at least 2³ = 8 so that we can exactly + // represent a case-insensitive abc by the set + // {abc, abC, aBc, aBC, Abc, AbC, ABc, ABC}. + maxSet = 20 +) + +// anyMatch returns the regexpInfo describing a regexp that +// matches any string. +func anyMatch() regexpInfo { + return regexpInfo{ + canEmpty: true, + prefix: []string{""}, + suffix: []string{""}, + match: allQuery, + } +} + +// anyChar returns the regexpInfo describing a regexp that +// matches any single character. +func anyChar() regexpInfo { + return regexpInfo{ + prefix: []string{""}, + suffix: []string{""}, + match: allQuery, + } +} + +// noMatch returns the regexpInfo describing a regexp that +// matches no strings at all. +func noMatch() regexpInfo { + return regexpInfo{ + match: noneQuery, + } +} + +// emptyString returns the regexpInfo describing a regexp that +// matches only the empty string. +func emptyString() regexpInfo { + return regexpInfo{ + canEmpty: true, + exact: []string{""}, + match: allQuery, + } +} + +// analyze returns the regexpInfo for the regexp re. +func analyze(re *syntax.Regexp) (ret regexpInfo) { + //println("analyze", re.String()) + //defer func() { println("->", ret.String()) }() + var info regexpInfo + switch re.Op { + case syntax.OpNoMatch: + return noMatch() + + case syntax.OpEmptyMatch, + syntax.OpBeginLine, syntax.OpEndLine, + syntax.OpBeginText, syntax.OpEndText, + syntax.OpWordBoundary, syntax.OpNoWordBoundary: + return emptyString() + + case syntax.OpLiteral: + if re.Flags&syntax.FoldCase != 0 { + switch len(re.Rune) { + case 0: + return emptyString() + case 1: + // Single-letter case-folded string: + // rewrite into char class and analyze. + re1 := &syntax.Regexp{ + Op: syntax.OpCharClass, + } + re1.Rune = re1.Rune0[:0] + r0 := re.Rune[0] + re1.Rune = append(re1.Rune, r0, r0) + for r1 := unicode.SimpleFold(r0); r1 != r0; r1 = unicode.SimpleFold(r1) { + re1.Rune = append(re1.Rune, r1, r1) + } + info = analyze(re1) + return info + } + // Multi-letter case-folded string: + // treat as concatenation of single-letter case-folded strings. + re1 := &syntax.Regexp{ + Op: syntax.OpLiteral, + Flags: syntax.FoldCase, + } + info = emptyString() + for i := range re.Rune { + re1.Rune = re.Rune[i : i+1] + info = concat(info, analyze(re1)) + } + return info + } + info.exact = stringSet{string(re.Rune)} + info.match = allQuery + + case syntax.OpAnyCharNotNL, syntax.OpAnyChar: + return anyChar() + + case syntax.OpCapture: + return analyze(re.Sub[0]) + + case syntax.OpConcat: + return fold(concat, re.Sub, emptyString()) + + case syntax.OpAlternate: + return fold(alternate, re.Sub, noMatch()) + + case syntax.OpQuest: + return alternate(analyze(re.Sub[0]), emptyString()) + + case syntax.OpStar: + // We don't know anything, so assume the worst. + return anyMatch() + + case syntax.OpRepeat: + if re.Min == 0 { + // Like OpStar + return anyMatch() + } + fallthrough + case syntax.OpPlus: + // x+ + // Since there has to be at least one x, the prefixes and suffixes + // stay the same. If x was exact, it isn't anymore. + info = analyze(re.Sub[0]) + if info.exact.have() { + info.prefix = info.exact + info.suffix = info.exact.copy() + info.exact = nil + } + + case syntax.OpCharClass: + info.match = allQuery + + // Special case. + if len(re.Rune) == 0 { + return noMatch() + } + + // Special case. + if len(re.Rune) == 1 { + info.exact = stringSet{string(re.Rune[0])} + break + } + + n := 0 + for i := 0; i < len(re.Rune); i += 2 { + n += int(re.Rune[i+1] - re.Rune[i]) + } + // If the class is too large, it's okay to overestimate. + if n > 100 { + return anyChar() + } + + info.exact = []string{} + for i := 0; i < len(re.Rune); i += 2 { + lo, hi := re.Rune[i], re.Rune[i+1] + for rr := lo; rr <= hi; rr++ { + info.exact.add(string(rr)) + } + } + } + + info.simplify(false) + return info +} + +// fold is the usual higher-order function. +func fold(f func(x, y regexpInfo) regexpInfo, sub []*syntax.Regexp, zero regexpInfo) regexpInfo { + if len(sub) == 0 { + return zero + } + if len(sub) == 1 { + return analyze(sub[0]) + } + info := f(analyze(sub[0]), analyze(sub[1])) + for i := 2; i < len(sub); i++ { + info = f(info, analyze(sub[i])) + } + return info +} + +// concat returns the regexp info for xy given x and y. +func concat(x, y regexpInfo) (out regexpInfo) { + //println("concat", x.String(), "...", y.String()) + //defer func() { println("->", out.String()) }() + var xy regexpInfo + xy.match = x.match.and(y.match) + if x.exact.have() && y.exact.have() { + xy.exact = x.exact.cross(y.exact, false) + } else { + if x.exact.have() { + xy.prefix = x.exact.cross(y.prefix, false) + } else { + xy.prefix = x.prefix + if x.canEmpty { + xy.prefix = xy.prefix.union(y.prefix, false) + } + } + if y.exact.have() { + xy.suffix = x.suffix.cross(y.exact, true) + } else { + xy.suffix = y.suffix + if y.canEmpty { + xy.suffix = xy.suffix.union(x.suffix, true) + } + } + } + + // If all the possible strings in the cross product of x.suffix + // and y.prefix are long enough, then the trigram for one + // of them must be present and would not necessarily be + // accounted for in xy.prefix or xy.suffix yet. Cut things off + // at maxSet just to keep the sets manageable. + if !x.exact.have() && !y.exact.have() && + x.suffix.size() <= maxSet && y.prefix.size() <= maxSet && + x.suffix.minLen()+y.prefix.minLen() >= 3 { + xy.match = xy.match.andTrigrams(x.suffix.cross(y.prefix, false)) + } + + xy.simplify(false) + return xy +} + +// alternate returns the regexpInfo for x|y given x and y. +func alternate(x, y regexpInfo) (out regexpInfo) { + //println("alternate", x.String(), "...", y.String()) + //defer func() { println("->", out.String()) }() + var xy regexpInfo + if x.exact.have() && y.exact.have() { + xy.exact = x.exact.union(y.exact, false) + } else if x.exact.have() { + xy.prefix = x.exact.union(y.prefix, false) + xy.suffix = x.exact.union(y.suffix, true) + x.addExact() + } else if y.exact.have() { + xy.prefix = x.prefix.union(y.exact, false) + xy.suffix = x.suffix.union(y.exact.copy(), true) + y.addExact() + } else { + xy.prefix = x.prefix.union(y.prefix, false) + xy.suffix = x.suffix.union(y.suffix, true) + } + xy.canEmpty = x.canEmpty || y.canEmpty + xy.match = x.match.or(y.match) + + xy.simplify(false) + return xy +} + +// addExact adds to the match query the trigrams for matching info.exact. +func (info *regexpInfo) addExact() { + if info.exact.have() { + info.match = info.match.andTrigrams(info.exact) + } +} + +// simplify simplifies the regexpInfo when the exact set gets too large. +func (info *regexpInfo) simplify(force bool) { + //println(" simplify", info.String(), " force=", force) + //defer func() { println(" ->", info.String()) }() + // If there are now too many exact strings, + // loop over them, adding trigrams and moving + // the relevant pieces into prefix and suffix. + info.exact.clean(false) + if len(info.exact) > maxExact || (info.exact.minLen() >= 3 && force) || info.exact.minLen() >= 4 { + info.addExact() + for _, s := range info.exact { + n := len(s) + if n < 3 { + info.prefix.add(s) + info.suffix.add(s) + } else { + info.prefix.add(s[:2]) + info.suffix.add(s[n-2:]) + } + } + info.exact = nil + } + + if !info.exact.have() { + info.simplifySet(&info.prefix) + info.simplifySet(&info.suffix) + } +} + +// simplifySet reduces the size of the given set (either prefix or suffix). +// There is no need to pass around enormous prefix or suffix sets, since +// they will only be used to create trigrams. As they get too big, simplifySet +// moves the information they contain into the match query, which is +// more efficient to pass around. +func (info *regexpInfo) simplifySet(s *stringSet) { + t := *s + t.clean(s == &info.suffix) + + // Add the OR of the current prefix/suffix set to the query. + info.match = info.match.andTrigrams(t) + + for n := 3; n == 3 || t.size() > maxSet; n-- { + // Replace set by strings of length n-1. + w := 0 + for _, str := range t { + if len(str) >= n { + if s == &info.prefix { + str = str[:n-1] + } else { + str = str[len(str)-n+1:] + } + } + if w == 0 || t[w-1] != str { + t[w] = str + w++ + } + } + t = t[:w] + t.clean(s == &info.suffix) + } + + // Now make sure that the prefix/suffix sets aren't redundant. + // For example, if we know "ab" is a possible prefix, then it + // doesn't help at all to know that "abc" is also a possible + // prefix, so delete "abc". + w := 0 + f := strings.HasPrefix + if s == &info.suffix { + f = strings.HasSuffix + } + for _, str := range t { + if w == 0 || !f(str, t[w-1]) { + t[w] = str + w++ + } + } + t = t[:w] + + *s = t +} + +func (info regexpInfo) String() string { + s := "" + if info.canEmpty { + s += "canempty " + } + if info.exact.have() { + s += "exact:" + strings.Join(info.exact, ",") + } else { + s += "prefix:" + strings.Join(info.prefix, ",") + s += " suffix:" + strings.Join(info.suffix, ",") + } + s += " match: " + info.match.String() + return s +} + +// A stringSet is a set of strings. +// The nil stringSet indicates not having a set. +// The non-nil but empty stringSet is the empty set. +type stringSet []string + +// have reports whether we have a stringSet. +func (s stringSet) have() bool { + return s != nil +} + +// contains reports whether s contains str. +func (s stringSet) contains(str string) bool { + for _, ss := range s { + if ss == str { + return true + } + } + return false +} + +type byPrefix []string + +func (x *byPrefix) Len() int { return len(*x) } +func (x *byPrefix) Swap(i, j int) { (*x)[i], (*x)[j] = (*x)[j], (*x)[i] } +func (x *byPrefix) Less(i, j int) bool { return (*x)[i] < (*x)[j] } + +type bySuffix []string + +func (x *bySuffix) Len() int { return len(*x) } +func (x *bySuffix) Swap(i, j int) { (*x)[i], (*x)[j] = (*x)[j], (*x)[i] } +func (x *bySuffix) Less(i, j int) bool { + s := (*x)[i] + t := (*x)[j] + for i := 1; i <= len(s) && i <= len(t); i++ { + si := s[len(s)-i] + ti := t[len(t)-i] + if si < ti { + return true + } + if si > ti { + return false + } + } + return len(s) < len(t) +} + +// add adds str to the set. +func (s *stringSet) add(str string) { + *s = append(*s, str) +} + +// clean removes duplicates from the stringSet. +func (s *stringSet) clean(isSuffix bool) { + t := *s + if isSuffix { + sort.Sort((*bySuffix)(s)) + } else { + sort.Sort((*byPrefix)(s)) + } + w := 0 + for _, str := range t { + if w == 0 || t[w-1] != str { + t[w] = str + w++ + } + } + *s = t[:w] +} + +// size returns the number of strings in s. +func (s stringSet) size() int { + return len(s) +} + +// minLen returns the length of the shortest string in s. +func (s stringSet) minLen() int { + if len(s) == 0 { + return 0 + } + m := len(s[0]) + for _, str := range s { + if m > len(str) { + m = len(str) + } + } + return m +} + +// maxLen returns the length of the longest string in s. +func (s stringSet) maxLen() int { + if len(s) == 0 { + return 0 + } + m := len(s[0]) + for _, str := range s { + if m < len(str) { + m = len(str) + } + } + return m +} + +// union returns the union of s and t, reusing s's storage. +func (s stringSet) union(t stringSet, isSuffix bool) stringSet { + s = append(s, t...) + s.clean(isSuffix) + return s +} + +// cross returns the cross product of s and t. +func (s stringSet) cross(t stringSet, isSuffix bool) stringSet { + p := stringSet{} + for _, ss := range s { + for _, tt := range t { + p.add(ss + tt) + } + } + p.clean(isSuffix) + return p +} + +// clear empties the set but preserves the storage. +func (s *stringSet) clear() { + *s = (*s)[:0] +} + +// copy returns a copy of the set that does not share storage with the original. +func (s stringSet) copy() stringSet { + return append(stringSet{}, s...) +} + +// isSubsetOf returns true if all strings in s are also in t. +// It assumes both sets are sorted. +func (s stringSet) isSubsetOf(t stringSet) bool { + j := 0 + for _, ss := range s { + for j < len(t) && t[j] < ss { + j++ + } + if j >= len(t) || t[j] != ss { + return false + } + } + return true +} diff --git a/src/code.google.com/p/codesearch/index/regexp_test.go b/src/code.google.com/p/codesearch/index/regexp_test.go new file mode 100644 index 00000000..ae0dd807 --- /dev/null +++ b/src/code.google.com/p/codesearch/index/regexp_test.go @@ -0,0 +1,94 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +import ( + "regexp/syntax" + "testing" +) + +var queryTests = []struct { + re string + q string +}{ + {`Abcdef`, `"Abc" "bcd" "cde" "def"`}, + {`(abc)(def)`, `"abc" "bcd" "cde" "def"`}, + {`abc.*(def|ghi)`, `"abc" ("def"|"ghi")`}, + {`abc(def|ghi)`, `"abc" ("bcd" "cde" "def")|("bcg" "cgh" "ghi")`}, + {`a+hello`, `"ahe" "ell" "hel" "llo"`}, + {`(a+hello|b+world)`, `("ahe" "ell" "hel" "llo")|("bwo" "orl" "rld" "wor")`}, + {`a*bbb`, `"bbb"`}, + {`a?bbb`, `"bbb"`}, + {`(bbb)a?`, `"bbb"`}, + {`(bbb)a*`, `"bbb"`}, + {`^abc`, `"abc"`}, + {`abc$`, `"abc"`}, + {`ab[cde]f`, `("abc" "bcf")|("abd" "bdf")|("abe" "bef")`}, + {`(abc|bac)de`, `"cde" ("abc" "bcd")|("acd" "bac")`}, + + // These don't have enough letters for a trigram, so they return the + // always matching query "+". + {`ab[^cde]f`, `+`}, + {`ab.f`, `+`}, + {`.`, `+`}, + {`()`, `+`}, + + // No matches. + {`[^\s\S]`, `-`}, + + // Factoring works. + {`(abc|abc)`, `"abc"`}, + {`(ab|ab)c`, `"abc"`}, + {`ab(cab|cat)`, `"abc" "bca" ("cab"|"cat")`}, + {`(z*(abc|def)z*)(z*(abc|def)z*)`, `("abc"|"def")`}, + {`(z*abcz*defz*)|(z*abcz*defz*)`, `"abc" "def"`}, + {`(z*abcz*defz*(ghi|jkl)z*)|(z*abcz*defz*(mno|prs)z*)`, + `"abc" "def" ("ghi"|"jkl"|"mno"|"prs")`}, + {`(z*(abcz*def)|(ghiz*jkl)z*)|(z*(mnoz*prs)|(tuvz*wxy)z*)`, + `("abc" "def")|("ghi" "jkl")|("mno" "prs")|("tuv" "wxy")`}, + {`(z*abcz*defz*)(z*(ghi|jkl)z*)`, `"abc" "def" ("ghi"|"jkl")`}, + {`(z*abcz*defz*)|(z*(ghi|jkl)z*)`, `("ghi"|"jkl")|("abc" "def")`}, + + // analyze keeps track of multiple possible prefix/suffixes. + {`[ab][cd][ef]`, `("ace"|"acf"|"ade"|"adf"|"bce"|"bcf"|"bde"|"bdf")`}, + {`ab[cd]e`, `("abc" "bce")|("abd" "bde")`}, + + // Different sized suffixes. + {`(a|ab)cde`, `"cde" ("abc" "bcd")|("acd")`}, + {`(a|b|c|d)(ef|g|hi|j)`, `+`}, + + {`(?s).`, `+`}, + + // Expanding case. + {`(?i)a~~`, `("A~~"|"a~~")`}, + {`(?i)ab~`, `("AB~"|"Ab~"|"aB~"|"ab~")`}, + {`(?i)abc`, `("ABC"|"ABc"|"AbC"|"Abc"|"aBC"|"aBc"|"abC"|"abc")`}, + {`(?i)abc|def`, `("ABC"|"ABc"|"AbC"|"Abc"|"DEF"|"DEf"|"DeF"|"Def"|"aBC"|"aBc"|"abC"|"abc"|"dEF"|"dEf"|"deF"|"def")`}, + {`(?i)abcd`, `("ABC"|"ABc"|"AbC"|"Abc"|"aBC"|"aBc"|"abC"|"abc") ("BCD"|"BCd"|"BcD"|"Bcd"|"bCD"|"bCd"|"bcD"|"bcd")`}, + {`(?i)abc|abc`, `("ABC"|"ABc"|"AbC"|"Abc"|"aBC"|"aBc"|"abC"|"abc")`}, + + // Word boundary. + {`\b`, `+`}, + {`\B`, `+`}, + {`\babc`, `"abc"`}, + {`\Babc`, `"abc"`}, + {`abc\b`, `"abc"`}, + {`abc\B`, `"abc"`}, + {`ab\bc`, `"abc"`}, + {`ab\Bc`, `"abc"`}, +} + +func TestQuery(t *testing.T) { + for _, tt := range queryTests { + re, err := syntax.Parse(tt.re, syntax.Perl) + if err != nil { + t.Fatal(err) + } + q := RegexpQuery(re).String() + if q != tt.q { + t.Errorf("RegexpQuery(%#q) = %#q, want %#q", tt.re, q, tt.q) + } + } +} diff --git a/src/code.google.com/p/codesearch/index/write.go b/src/code.google.com/p/codesearch/index/write.go new file mode 100644 index 00000000..76ea904d --- /dev/null +++ b/src/code.google.com/p/codesearch/index/write.go @@ -0,0 +1,687 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package index + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "os" + "strings" + "syscall" + "unsafe" + + "code.google.com/p/codesearch/sparse" +) + +// Index writing. See read.go for details of on-disk format. +// +// It would suffice to make a single large list of (trigram, file#) pairs +// while processing the files one at a time, sort that list by trigram, +// and then create the posting lists from subsequences of the list. +// However, we do not assume that the entire index fits in memory. +// Instead, we sort and flush the list to a new temporary file each time +// it reaches its maximum in-memory size, and then at the end we +// create the final posting lists by merging the temporary files as we +// read them back in. +// +// It would also be useful to be able to create an index for a subset +// of the files and then merge that index into an existing one. This would +// allow incremental updating of an existing index when a directory changes. +// But we have not implemented that. + +// An IndexWriter creates an on-disk index corresponding to a set of files. +type IndexWriter struct { + LogSkip bool // log information about skipped files + Verbose bool // log status using package log + + trigram *sparse.Set // trigrams for the current file + buf [8]byte // scratch buffer + + paths []string + + nameData *bufWriter // temp file holding list of names + nameLen uint32 // number of bytes written to nameData + nameIndex *bufWriter // temp file holding name index + numName int // number of names written + totalBytes int64 + + post []postEntry // list of (trigram, file#) pairs + postFile []*os.File // flushed post entries + postData [][]byte // mmap buffers to be unmapped + postIndex *bufWriter // temp file holding posting list index + + inbuf []byte // input buffer + main *bufWriter // main index file +} + +const npost = 64 << 20 / 8 // 64 MB worth of post entries + +// Create returns a new IndexWriter that will write the index to file. +func Create(file string) *IndexWriter { + return &IndexWriter{ + trigram: sparse.NewSet(1 << 24), + nameData: bufCreate(""), + nameIndex: bufCreate(""), + postIndex: bufCreate(""), + main: bufCreate(file), + post: make([]postEntry, 0, npost), + inbuf: make([]byte, 16384), + } +} + +// A postEntry is an in-memory (trigram, file#) pair. +type postEntry uint64 + +func (p postEntry) trigram() uint32 { + return uint32(p >> 32) +} + +func (p postEntry) fileid() uint32 { + return uint32(p) +} + +func makePostEntry(trigram, fileid uint32) postEntry { + return postEntry(trigram)<<32 | postEntry(fileid) +} + +// Tuning constants for detecting text files. +// A file is assumed not to be text files (and thus not indexed) +// if it contains an invalid UTF-8 sequences, if it is longer than maxFileLength +// bytes, if it contains more than maxLongLineRatio lines longer than maxLineLen bytes, +// or if it contains more than maxTextTrigrams distinct trigrams AND +// it has a ratio of trigrams to filesize > maxTrigramRatio. +const ( + maxFileLen = 1 << 25 + maxLineLen = 2000 + maxLongLineRatio = 0.1 + maxTextTrigrams = 20000 + maxTrigramRatio = 0.1 +) + +// AddPaths adds the given paths to the index's list of paths. +func (ix *IndexWriter) AddPaths(paths []string) { + ix.paths = append(ix.paths, paths...) +} + +// AddFile adds the file with the given name (opened using os.Open) +// to the index. It logs errors using package log. +func (ix *IndexWriter) AddFile(name string) { + f, err := os.Open(name) + if err != nil { + log.Print(err) + return + } + defer f.Close() + ix.Add(name, f) +} + +// Add adds the file f to the index under the given name. +// It logs errors using package log. +func (ix *IndexWriter) Add(name string, f io.Reader) string { + ix.trigram.Reset() + var ( + c = byte(0) + i = 0 + buf = ix.inbuf[:0] + tv = uint32(0) + n = int64(0) + linelen = 0 + numLines = 0 + longLines = 0 + skipReason = "" + ) + + for { + tv = (tv << 8) & (1<<24 - 1) + if i >= len(buf) { + n, err := f.Read(buf[:cap(buf)]) + if n == 0 { + if err != nil { + if err == io.EOF { + break + } + log.Printf("%s: %v\n", name, err) + return "" + } + log.Printf("%s: 0-length read\n", name) + return "" + } + buf = buf[:n] + i = 0 + } + c = buf[i] + i++ + tv |= uint32(c) + if n++; n >= 3 { + ix.trigram.Add(tv) + } + if !validUTF8((tv>>8)&0xFF, tv&0xFF) { + skipReason = "Invalid UTF-8" + if ix.LogSkip { + log.Printf("%s: %s\n", name, skipReason) + } + return skipReason + } + if n > maxFileLen { + skipReason = "Too long" + if ix.LogSkip { + log.Printf("%s: %s\n", name, skipReason) + } + return skipReason + } + linelen++ + if c == '\n' { + numLines++ + if linelen > maxLineLen { + longLines++ + } + linelen = 0 + } + } + + if n > 0 { + trigramRatio := float32(ix.trigram.Len()) / float32(n) + if trigramRatio > maxTrigramRatio && ix.trigram.Len() > maxTextTrigrams { + skipReason = fmt.Sprintf("Trigram ratio too high (%0.2f), probably not text", trigramRatio) + if ix.LogSkip { + log.Printf("%s: %s\n", name, skipReason) + } + return skipReason + } + + longLineRatio := float32(longLines) / float32(numLines) + if longLineRatio > maxLongLineRatio { + skipReason = fmt.Sprintf("Too many long lines, ratio: %0.2f", longLineRatio) + if ix.LogSkip { + log.Printf("%s: %s\n", name, skipReason) + } + return skipReason + } + } + + ix.totalBytes += n + + if ix.Verbose { + log.Printf("%d %d %s\n", n, ix.trigram.Len(), name) + } + + fileid := ix.addName(name) + for _, trigram := range ix.trigram.Dense() { + if len(ix.post) >= cap(ix.post) { + ix.flushPost() + } + ix.post = append(ix.post, makePostEntry(trigram, fileid)) + } + + return "" +} + +// Flush flushes the index entry to the target file. +func (ix *IndexWriter) Flush() { + ix.addName("") + + var off [5]uint32 + ix.main.writeString(magic) + off[0] = ix.main.offset() + for _, p := range ix.paths { + ix.main.writeString(p) + ix.main.writeString("\x00") + } + ix.main.writeString("\x00") + off[1] = ix.main.offset() + copyFile(ix.main, ix.nameData) + off[2] = ix.main.offset() + ix.mergePost(ix.main) + off[3] = ix.main.offset() + copyFile(ix.main, ix.nameIndex) + off[4] = ix.main.offset() + copyFile(ix.main, ix.postIndex) + for _, v := range off { + ix.main.writeUint32(v) + } + ix.main.writeString(trailerMagic) + + os.Remove(ix.nameData.name) + for _, d := range ix.postData { + syscall.Munmap(d) + } + for _, f := range ix.postFile { + f.Close() + os.Remove(f.Name()) + } + os.Remove(ix.nameIndex.name) + os.Remove(ix.postIndex.name) + + log.Printf("%d data bytes, %d index bytes", ix.totalBytes, ix.main.offset()) + + ix.main.flush() +} + +func copyFile(dst, src *bufWriter) { + dst.flush() + _, err := io.Copy(dst.file, src.finish()) + if err != nil { + log.Fatalf("copying %s to %s: %v", src.name, dst.name, err) + } +} + +// addName adds the file with the given name to the index. +// It returns the assigned file ID number. +func (ix *IndexWriter) addName(name string) uint32 { + if strings.Contains(name, "\x00") { + log.Fatalf("%q: file has NUL byte in name", name) + } + + ix.nameIndex.writeUint32(ix.nameData.offset()) + ix.nameData.writeString(name) + ix.nameData.writeByte(0) + id := ix.numName + ix.numName++ + return uint32(id) +} + +// flushPost writes ix.post to a new temporary file and +// clears the slice. +func (ix *IndexWriter) flushPost() { + w, err := ioutil.TempFile("", "csearch-index") + if err != nil { + log.Fatal(err) + } + if ix.Verbose { + log.Printf("flush %d entries to %s", len(ix.post), w.Name()) + } + sortPost(ix.post) + + // Write the raw ix.post array to disk as is. + // This process is the one reading it back in, so byte order is not a concern. + data := (*[npost * 8]byte)(unsafe.Pointer(&ix.post[0]))[:len(ix.post)*8] + if n, err := w.Write(data); err != nil || n < len(data) { + if err != nil { + log.Fatal(err) + } + log.Fatalf("short write writing %s", w.Name()) + } + + ix.post = ix.post[:0] + w.Seek(0, 0) + ix.postFile = append(ix.postFile, w) +} + +// mergePost reads the flushed index entries and merges them +// into posting lists, writing the resulting lists to out. +func (ix *IndexWriter) mergePost(out *bufWriter) { + var h postHeap + + log.Printf("merge %d files + mem", len(ix.postFile)) + for _, f := range ix.postFile { + ix.postData = append( + ix.postData, + h.addFile(f)) + } + sortPost(ix.post) + h.addMem(ix.post) + + npost := 0 + e := h.next() + offset0 := out.offset() + for { + npost++ + offset := out.offset() - offset0 + trigram := e.trigram() + ix.buf[0] = byte(trigram >> 16) + ix.buf[1] = byte(trigram >> 8) + ix.buf[2] = byte(trigram) + + // posting list + fileid := ^uint32(0) + nfile := uint32(0) + out.write(ix.buf[:3]) + for ; e.trigram() == trigram && trigram != 1<<24-1; e = h.next() { + out.writeUvarint(e.fileid() - fileid) + fileid = e.fileid() + nfile++ + } + out.writeUvarint(0) + + // index entry + ix.postIndex.write(ix.buf[:3]) + ix.postIndex.writeUint32(nfile) + ix.postIndex.writeUint32(offset) + + if trigram == 1<<24-1 { + break + } + } +} + +// A postChunk represents a chunk of post entries flushed to disk or +// still in memory. +type postChunk struct { + e postEntry // next entry + m []postEntry // remaining entries after e +} + +const postBuf = 4096 + +// A postHeap is a heap (priority queue) of postChunks. +type postHeap struct { + ch []*postChunk +} + +func (h *postHeap) addFile(f *os.File) []byte { + data := mmapFile(f).d + m := (*[npost]postEntry)(unsafe.Pointer(&data[0]))[:len(data)/8] + h.addMem(m) + return data +} + +func (h *postHeap) addMem(x []postEntry) { + h.add(&postChunk{m: x}) +} + +// step reads the next entry from ch and saves it in ch.e. +// It returns false if ch is over. +func (h *postHeap) step(ch *postChunk) bool { + old := ch.e + m := ch.m + if len(m) == 0 { + return false + } + ch.e = postEntry(m[0]) + m = m[1:] + ch.m = m + if old >= ch.e { + panic("bad sort") + } + return true +} + +// add adds the chunk to the postHeap. +// All adds must be called before the first call to next. +func (h *postHeap) add(ch *postChunk) { + if len(ch.m) > 0 { + ch.e = ch.m[0] + ch.m = ch.m[1:] + h.push(ch) + } +} + +// empty reports whether the postHeap is empty. +func (h *postHeap) empty() bool { + return len(h.ch) == 0 +} + +// next returns the next entry from the postHeap. +// It returns a postEntry with trigram == 1<<24 - 1 if h is empty. +func (h *postHeap) next() postEntry { + if len(h.ch) == 0 { + return makePostEntry(1<<24-1, 0) + } + ch := h.ch[0] + e := ch.e + m := ch.m + if len(m) == 0 { + h.pop() + } else { + ch.e = m[0] + ch.m = m[1:] + h.siftDown(0) + } + return e +} + +func (h *postHeap) pop() *postChunk { + ch := h.ch[0] + n := len(h.ch) - 1 + h.ch[0] = h.ch[n] + h.ch = h.ch[:n] + if n > 1 { + h.siftDown(0) + } + return ch +} + +func (h *postHeap) push(ch *postChunk) { + n := len(h.ch) + h.ch = append(h.ch, ch) + if len(h.ch) >= 2 { + h.siftUp(n) + } +} + +func (h *postHeap) siftDown(i int) { + ch := h.ch + for { + j1 := 2*i + 1 + if j1 >= len(ch) { + break + } + j := j1 + if j2 := j1 + 1; j2 < len(ch) && ch[j1].e >= ch[j2].e { + j = j2 + } + if ch[i].e < ch[j].e { + break + } + ch[i], ch[j] = ch[j], ch[i] + i = j + } +} + +func (h *postHeap) siftUp(j int) { + ch := h.ch + for { + i := (j - 1) / 2 + if i == j || ch[i].e < ch[j].e { + break + } + ch[i], ch[j] = ch[j], ch[i] + } +} + +// A bufWriter is a convenience wrapper: a closeable bufio.Writer. +type bufWriter struct { + name string + file *os.File + buf []byte + tmp [8]byte +} + +// bufCreate creates a new file with the given name and returns a +// corresponding bufWriter. If name is empty, bufCreate uses a +// temporary file. +func bufCreate(name string) *bufWriter { + var ( + f *os.File + err error + ) + if name != "" { + f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + } else { + f, err = ioutil.TempFile("", "csearch") + } + if err != nil { + log.Fatal(err) + } + return &bufWriter{ + name: f.Name(), + buf: make([]byte, 0, 256<<10), + file: f, + } +} + +func (b *bufWriter) write(x []byte) { + n := cap(b.buf) - len(b.buf) + if len(x) > n { + b.flush() + if len(x) >= cap(b.buf) { + if _, err := b.file.Write(x); err != nil { + log.Fatalf("writing %s: %v", b.name, err) + } + return + } + } + b.buf = append(b.buf, x...) +} + +func (b *bufWriter) writeByte(x byte) { + if len(b.buf) >= cap(b.buf) { + b.flush() + } + b.buf = append(b.buf, x) +} + +func (b *bufWriter) writeString(s string) { + n := cap(b.buf) - len(b.buf) + if len(s) > n { + b.flush() + if len(s) >= cap(b.buf) { + if _, err := b.file.WriteString(s); err != nil { + log.Fatalf("writing %s: %v", b.name, err) + } + return + } + } + b.buf = append(b.buf, s...) +} + +// offset returns the current write offset. +func (b *bufWriter) offset() uint32 { + off, _ := b.file.Seek(0, 1) + off += int64(len(b.buf)) + if int64(uint32(off)) != off { + log.Fatalf("index is larger than 4GB") + } + return uint32(off) +} + +func (b *bufWriter) flush() { + if len(b.buf) == 0 { + return + } + _, err := b.file.Write(b.buf) + if err != nil { + log.Fatalf("writing %s: %v", b.name, err) + } + b.buf = b.buf[:0] +} + +// finish flushes the file to disk and returns an open file ready for reading. +func (b *bufWriter) finish() *os.File { + b.flush() + f := b.file + f.Seek(0, 0) + return f +} + +func (b *bufWriter) writeTrigram(t uint32) { + if cap(b.buf)-len(b.buf) < 3 { + b.flush() + } + b.buf = append(b.buf, byte(t>>16), byte(t>>8), byte(t)) +} + +func (b *bufWriter) writeUint32(x uint32) { + if cap(b.buf)-len(b.buf) < 4 { + b.flush() + } + b.buf = append(b.buf, byte(x>>24), byte(x>>16), byte(x>>8), byte(x)) +} + +func (b *bufWriter) writeUvarint(x uint32) { + if cap(b.buf)-len(b.buf) < 5 { + b.flush() + } + switch { + case x < 1<<7: + b.buf = append(b.buf, byte(x)) + case x < 1<<14: + b.buf = append(b.buf, byte(x|0x80), byte(x>>7)) + case x < 1<<21: + b.buf = append(b.buf, byte(x|0x80), byte(x>>7|0x80), byte(x>>14)) + case x < 1<<28: + b.buf = append(b.buf, byte(x|0x80), byte(x>>7|0x80), byte(x>>14|0x80), byte(x>>21)) + default: + b.buf = append(b.buf, byte(x|0x80), byte(x>>7|0x80), byte(x>>14|0x80), byte(x>>21|0x80), byte(x>>28)) + } +} + +// validUTF8 reports whether the byte pair can appear in a +// valid sequence of UTF-8-encoded code points. +func validUTF8(c1, c2 uint32) bool { + switch { + case c1 < 0x80: + // 1-byte, must be followed by 1-byte or first of multi-byte + return c2 < 0x80 || 0xc0 <= c2 && c2 < 0xf8 + case c1 < 0xc0: + // continuation byte, can be followed by nearly anything + return c2 < 0xf8 + case c1 < 0xf8: + // first of multi-byte, must be followed by continuation byte + return 0x80 <= c2 && c2 < 0xc0 + } + return false +} + +// sortPost sorts the postentry list. +// The list is already sorted by fileid (bottom 32 bits) +// and the top 8 bits are always zero, so there are only +// 24 bits to sort. Run two rounds of 12-bit radix sort. +const sortK = 12 + +// TODO(knorton): sortTmp and sortN were previously static state +// presumably to avoid allocations of the large buffers. Sadly, +// this makes it impossible for us to run concurrent indexers. +// I have moved them into local allocations but if we really do +// need to share a buffer, they can easily go into a reusable +// object, similar to the way buffers are reused in grepper. +func sortPost(post []postEntry) { + var sortN [1 << sortK]int + + sortTmp := make([]postEntry, len(post)) + tmp := sortTmp[:len(post)] + + const k = sortK + for i := range sortN { + sortN[i] = 0 + } + for _, p := range post { + r := uintptr(p>>32) & (1<>32) & (1<>(32+k)) & (1<>(32+k)) & (1<> 24) + buf[1] = byte(x >> 16) + buf[2] = byte(x >> 8) + buf[3] = byte(x) + return string(buf[:]) +} + +func fileList(list ...uint32) string { + var buf []byte + + last := ^uint32(0) + for _, x := range list { + delta := x - last + for delta >= 0x80 { + buf = append(buf, byte(delta)|0x80) + delta >>= 7 + } + buf = append(buf, byte(delta)) + last = x + } + buf = append(buf, 0) + return string(buf) +} + +func buildFlushIndex(out string, paths []string, doFlush bool, fileData map[string]string) { + ix := Create(out) + ix.AddPaths(paths) + var files []string + for name := range fileData { + files = append(files, name) + } + sort.Strings(files) + for _, name := range files { + ix.Add(name, strings.NewReader(fileData[name])) + } + if doFlush { + ix.flushPost() + } + ix.Flush() +} + +func buildIndex(name string, paths []string, fileData map[string]string) { + buildFlushIndex(name, paths, false, fileData) +} + +func testTrivialWrite(t *testing.T, doFlush bool) { + f, _ := ioutil.TempFile("", "index-test") + defer os.Remove(f.Name()) + out := f.Name() + buildFlushIndex(out, nil, doFlush, trivialFiles) + + data, err := ioutil.ReadFile(out) + if err != nil { + t.Fatalf("reading _test/index.triv: %v", err) + } + want := []byte(trivialIndex) + if !bytes.Equal(data, want) { + i := 0 + for i < len(data) && i < len(want) && data[i] == want[i] { + i++ + } + t.Fatalf("wrong index:\nhave: %q %q\nwant: %q %q", data[:i], data[i:], want[:i], want[i:]) + } +} + +func TestTrivialWrite(t *testing.T) { + testTrivialWrite(t, false) +} + +func TestTrivialWriteDisk(t *testing.T) { + testTrivialWrite(t, true) +} diff --git a/src/code.google.com/p/codesearch/lib/README.template b/src/code.google.com/p/codesearch/lib/README.template new file mode 100644 index 00000000..17661679 --- /dev/null +++ b/src/code.google.com/p/codesearch/lib/README.template @@ -0,0 +1,15 @@ +These are the command-line Code Search tools from +https://code.google.com/p/codesearch. + +These binaries are for ARCH systems running OPERSYS. + +To get started, run cindex with a list of directories to index: + + cindex /usr/include $HOME/src + +Then run csearch to run grep over all the indexed sources: + + csearch DATAKIT + +For details, run either command with the -help option, and +read http://swtch.com/~rsc/regexp/regexp4.html. diff --git a/src/code.google.com/p/codesearch/lib/buildall b/src/code.google.com/p/codesearch/lib/buildall new file mode 100755 index 00000000..947250e8 --- /dev/null +++ b/src/code.google.com/p/codesearch/lib/buildall @@ -0,0 +1,31 @@ +#!/bin/bash + +# This script builds the code search binaries for a variety of OS/architecture combinations. + +. ./setup + +for i in {5,6,8}{c,g,a,l} +do + go tool dist install cmd/$i +done + +build() { + echo "# $1" + goos=$(echo $1 | sed 's;/.*;;') + goarch=$(echo $1 | sed 's;.*/;;') + GOOS=$goos GOARCH=$goarch CGO_ENABLED=0 \ + go install -a code.google.com/p/codesearch/cmd/{cgrep,cindex,csearch} + rm -rf codesearch-$version + mkdir codesearch-$version + mv ~/g/bin/{cgrep,cindex,csearch}* codesearch-$version + chmod +x codesearch-$version/* + cat README.template | sed "s/ARCH/$(arch $goarch)/; s/OPERSYS/$(os $goos)/" >codesearch-$version/README.txt + rm -f codesearch-$version-$goos-$goarch.zip + zip -z -r codesearch-$version-$goos-$goarch.zip codesearch-$version < codesearch-$version/README.txt + rm -rf codesearch-0.01 +} + +for i in {linux,darwin,freebsd,windows}/{amd64,386} +do + build $i +done diff --git a/src/code.google.com/p/codesearch/lib/setup b/src/code.google.com/p/codesearch/lib/setup new file mode 100644 index 00000000..d1db2504 --- /dev/null +++ b/src/code.google.com/p/codesearch/lib/setup @@ -0,0 +1,23 @@ +set -e + +os() { + case "$1" in + freebsd) echo FreeBSD;; + linux) echo Linux;; + darwin) echo Mac OS X;; + openbsd) echo OpenBSD;; + netbsd) echo NetBSD;; + windows) echo Windows;; + *) echo $1;; + esac +} + +arch() { + case "$1" in + 386) echo 32-bit x86;; + amd64) echo 64-bit x86;; + *) echo $1;; + esac +} + +version=$(cat version) diff --git a/src/code.google.com/p/codesearch/lib/uploadall b/src/code.google.com/p/codesearch/lib/uploadall new file mode 100644 index 00000000..8edd51ac --- /dev/null +++ b/src/code.google.com/p/codesearch/lib/uploadall @@ -0,0 +1,18 @@ +#!/bin/sh + +# gcodeup is a copy of $GOROOT/misc/dashboard/googlecode_upload.py. + +. ./setup +user=$(sed -n 's/^re2.username = //' ~/.hgrc) +password=$(sed -n 's/^re2\.password = //' ~/.hgrc) + +upload() { + goos=$(echo $1 | sed "s/codesearch-$version-//; s/-.*//") + goarch=$(echo $1 | sed "s/codesearch-$version-//; s/[a-z0-9]*-//; s/-.*//") + gcodeup -s "binaries for $(os $goos) $(arch $goarch)" -p codesearch -u "$user" -w "$password" codesearch-$version-$1-$2.zip +} + +for i in codesearch-$version-* +do + upload $i +done diff --git a/src/code.google.com/p/codesearch/lib/version b/src/code.google.com/p/codesearch/lib/version new file mode 100644 index 00000000..6e6566ce --- /dev/null +++ b/src/code.google.com/p/codesearch/lib/version @@ -0,0 +1 @@ +0.01 diff --git a/src/code.google.com/p/codesearch/regexp/copy.go b/src/code.google.com/p/codesearch/regexp/copy.go new file mode 100644 index 00000000..9be19874 --- /dev/null +++ b/src/code.google.com/p/codesearch/regexp/copy.go @@ -0,0 +1,223 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Copied from Go's regexp/syntax. +// Formatters edited to handle instByteRange. + +package regexp + +import ( + "bytes" + "fmt" + "regexp/syntax" + "sort" + "strconv" + "unicode" +) + +// cleanClass sorts the ranges (pairs of elements of r), +// merges them, and eliminates duplicates. +func cleanClass(rp *[]rune) []rune { + + // Sort by lo increasing, hi decreasing to break ties. + sort.Sort(ranges{rp}) + + r := *rp + if len(r) < 2 { + return r + } + + // Merge abutting, overlapping. + w := 2 // write index + for i := 2; i < len(r); i += 2 { + lo, hi := r[i], r[i+1] + if lo <= r[w-1]+1 { + // merge with previous range + if hi > r[w-1] { + r[w-1] = hi + } + continue + } + // new disjoint range + r[w] = lo + r[w+1] = hi + w += 2 + } + + return r[:w] +} + +// appendRange returns the result of appending the range lo-hi to the class r. +func appendRange(r []rune, lo, hi rune) []rune { + // Expand last range or next to last range if it overlaps or abuts. + // Checking two ranges helps when appending case-folded + // alphabets, so that one range can be expanding A-Z and the + // other expanding a-z. + n := len(r) + for i := 2; i <= 4; i += 2 { // twice, using i=2, i=4 + if n >= i { + rlo, rhi := r[n-i], r[n-i+1] + if lo <= rhi+1 && rlo <= hi+1 { + if lo < rlo { + r[n-i] = lo + } + if hi > rhi { + r[n-i+1] = hi + } + return r + } + } + } + + return append(r, lo, hi) +} + +const ( + // minimum and maximum runes involved in folding. + // checked during test. + minFold = 0x0041 + maxFold = 0x1044f +) + +// appendFoldedRange returns the result of appending the range lo-hi +// and its case folding-equivalent runes to the class r. +func appendFoldedRange(r []rune, lo, hi rune) []rune { + // Optimizations. + if lo <= minFold && hi >= maxFold { + // Range is full: folding can't add more. + return appendRange(r, lo, hi) + } + if hi < minFold || lo > maxFold { + // Range is outside folding possibilities. + return appendRange(r, lo, hi) + } + if lo < minFold { + // [lo, minFold-1] needs no folding. + r = appendRange(r, lo, minFold-1) + lo = minFold + } + if hi > maxFold { + // [maxFold+1, hi] needs no folding. + r = appendRange(r, maxFold+1, hi) + hi = maxFold + } + + // Brute force. Depend on appendRange to coalesce ranges on the fly. + for c := lo; c <= hi; c++ { + r = appendRange(r, c, c) + f := unicode.SimpleFold(c) + for f != c { + r = appendRange(r, f, f) + f = unicode.SimpleFold(f) + } + } + return r +} + +// ranges implements sort.Interface on a []rune. +// The choice of receiver type definition is strange +// but avoids an allocation since we already have +// a *[]rune. +type ranges struct { + p *[]rune +} + +func (ra ranges) Less(i, j int) bool { + p := *ra.p + i *= 2 + j *= 2 + return p[i] < p[j] || p[i] == p[j] && p[i+1] > p[j+1] +} + +func (ra ranges) Len() int { + return len(*ra.p) / 2 +} + +func (ra ranges) Swap(i, j int) { + p := *ra.p + i *= 2 + j *= 2 + p[i], p[i+1], p[j], p[j+1] = p[j], p[j+1], p[i], p[i+1] +} + +func progString(p *syntax.Prog) string { + var b bytes.Buffer + dumpProg(&b, p) + return b.String() +} + +func instString(i *syntax.Inst) string { + var b bytes.Buffer + dumpInst(&b, i) + return b.String() +} + +func bw(b *bytes.Buffer, args ...string) { + for _, s := range args { + b.WriteString(s) + } +} + +func dumpProg(b *bytes.Buffer, p *syntax.Prog) { + for j := range p.Inst { + i := &p.Inst[j] + pc := strconv.Itoa(j) + if len(pc) < 3 { + b.WriteString(" "[len(pc):]) + } + if j == p.Start { + pc += "*" + } + bw(b, pc, "\t") + dumpInst(b, i) + bw(b, "\n") + } +} + +func u32(i uint32) string { + return strconv.FormatUint(uint64(i), 10) +} + +func dumpInst(b *bytes.Buffer, i *syntax.Inst) { + switch i.Op { + case syntax.InstAlt: + bw(b, "alt -> ", u32(i.Out), ", ", u32(i.Arg)) + case syntax.InstAltMatch: + bw(b, "altmatch -> ", u32(i.Out), ", ", u32(i.Arg)) + case syntax.InstCapture: + bw(b, "cap ", u32(i.Arg), " -> ", u32(i.Out)) + case syntax.InstEmptyWidth: + bw(b, "empty ", u32(i.Arg), " -> ", u32(i.Out)) + case syntax.InstMatch: + bw(b, "match") + case syntax.InstFail: + bw(b, "fail") + case syntax.InstNop: + bw(b, "nop -> ", u32(i.Out)) + case instByteRange: + fmt.Fprintf(b, "byte %02x-%02x", (i.Arg>>8)&0xFF, i.Arg&0xFF) + if i.Arg&argFold != 0 { + bw(b, "/i") + } + bw(b, " -> ", u32(i.Out)) + + // Should not happen + case syntax.InstRune: + if i.Rune == nil { + // shouldn't happen + bw(b, "rune ") + } + bw(b, "rune ", strconv.QuoteToASCII(string(i.Rune))) + if syntax.Flags(i.Arg)&syntax.FoldCase != 0 { + bw(b, "/i") + } + bw(b, " -> ", u32(i.Out)) + case syntax.InstRune1: + bw(b, "rune1 ", strconv.QuoteToASCII(string(i.Rune)), " -> ", u32(i.Out)) + case syntax.InstRuneAny: + bw(b, "any -> ", u32(i.Out)) + case syntax.InstRuneAnyNotNL: + bw(b, "anynotnl -> ", u32(i.Out)) + } +} diff --git a/src/code.google.com/p/codesearch/regexp/match.go b/src/code.google.com/p/codesearch/regexp/match.go new file mode 100644 index 00000000..f275c304 --- /dev/null +++ b/src/code.google.com/p/codesearch/regexp/match.go @@ -0,0 +1,473 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package regexp + +import ( + "bytes" + "encoding/binary" + "flag" + "fmt" + "io" + "os" + "regexp/syntax" + "sort" + + "code.google.com/p/codesearch/sparse" +) + +// A matcher holds the state for running regular expression search. +type matcher struct { + prog *syntax.Prog // compiled program + dstate map[string]*dstate // dstate cache + start *dstate // start state + startLine *dstate // start state for beginning of line + z1, z2 nstate // two temporary nstates +} + +// An nstate corresponds to an NFA state. +type nstate struct { + q sparse.Set // queue of program instructions + partial rune // partially decoded rune (TODO) + flag flags // flags (TODO) +} + +// The flags record state about a position between bytes in the text. +type flags uint32 + +const ( + flagBOL flags = 1 << iota // beginning of line + flagEOL // end of line + flagBOT // beginning of text + flagEOT // end of text + flagWord // last byte was word byte +) + +// A dstate corresponds to a DFA state. +type dstate struct { + next [256]*dstate // next state, per byte + enc string // encoded nstate + matchNL bool // match when next byte is \n + matchEOT bool // match in this state at end of text +} + +func (z *nstate) String() string { + return fmt.Sprintf("%v/%#x+%#x", z.q.Dense(), z.flag, z.partial) +} + +// enc encodes z as a string. +func (z *nstate) enc() string { + var buf []byte + var v [10]byte + last := ^uint32(0) + n := binary.PutUvarint(v[:], uint64(z.partial)) + buf = append(buf, v[:n]...) + n = binary.PutUvarint(v[:], uint64(z.flag)) + buf = append(buf, v[:n]...) + dense := z.q.Dense() + ids := make([]int, 0, len(dense)) + for _, id := range z.q.Dense() { + ids = append(ids, int(id)) + } + sort.Ints(ids) + for _, id := range ids { + n := binary.PutUvarint(v[:], uint64(uint32(id)-last)) + buf = append(buf, v[:n]...) + last = uint32(id) + } + return string(buf) +} + +// dec decodes the encoding s into z. +func (z *nstate) dec(s string) { + b := []byte(s) + i, n := binary.Uvarint(b) + if n <= 0 { + bug() + } + b = b[n:] + z.partial = rune(i) + i, n = binary.Uvarint(b) + if n <= 0 { + bug() + } + b = b[n:] + z.flag = flags(i) + z.q.Reset() + last := ^uint32(0) + for len(b) > 0 { + i, n = binary.Uvarint(b) + if n <= 0 { + bug() + } + b = b[n:] + last += uint32(i) + z.q.Add(last) + } +} + +// dmatch is the state we're in when we've seen a match and are just +// waiting for the end of the line. +var dmatch = dstate{ + matchNL: true, + matchEOT: true, +} + +func init() { + var z nstate + dmatch.enc = z.enc() + for i := range dmatch.next { + if i != '\n' { + dmatch.next[i] = &dmatch + } + } +} + +// init initializes the matcher. +func (m *matcher) init(prog *syntax.Prog) error { + m.prog = prog + m.dstate = make(map[string]*dstate) + + m.z1.q.Init(uint32(len(prog.Inst))) + m.z2.q.Init(uint32(len(prog.Inst))) + + m.addq(&m.z1.q, uint32(prog.Start), syntax.EmptyBeginLine|syntax.EmptyBeginText) + m.z1.flag = flagBOL | flagBOT + m.start = m.cache(&m.z1) + + m.z1.q.Reset() + m.addq(&m.z1.q, uint32(prog.Start), syntax.EmptyBeginLine) + m.z1.flag = flagBOL + m.startLine = m.cache(&m.z1) + + return nil +} + +// stepEmpty steps runq to nextq expanding according to flag. +func (m *matcher) stepEmpty(runq, nextq *sparse.Set, flag syntax.EmptyOp) { + nextq.Reset() + for _, id := range runq.Dense() { + m.addq(nextq, id, flag) + } +} + +// stepByte steps runq to nextq consuming c and then expanding according to flag. +// It returns true if a match ends immediately before c. +// c is either an input byte or endText. +func (m *matcher) stepByte(runq, nextq *sparse.Set, c int, flag syntax.EmptyOp) (match bool) { + nextq.Reset() + m.addq(nextq, uint32(m.prog.Start), flag) + for _, id := range runq.Dense() { + i := &m.prog.Inst[id] + switch i.Op { + default: + continue + case syntax.InstMatch: + match = true + continue + case instByteRange: + if c == endText { + break + } + lo := int((i.Arg >> 8) & 0xFF) + hi := int(i.Arg & 0xFF) + ch := c + if i.Arg&argFold != 0 && 'a' <= ch && ch <= 'z' { + ch += 'A' - 'a' + } + if lo <= ch && ch <= hi { + m.addq(nextq, i.Out, flag) + } + } + } + return +} + +// addq adds id to the queue, expanding according to flag. +func (m *matcher) addq(q *sparse.Set, id uint32, flag syntax.EmptyOp) { + if q.Has(id) { + return + } + q.Add(id) + i := &m.prog.Inst[id] + switch i.Op { + case syntax.InstCapture, syntax.InstNop: + m.addq(q, i.Out, flag) + case syntax.InstAlt, syntax.InstAltMatch: + m.addq(q, i.Out, flag) + m.addq(q, i.Arg, flag) + case syntax.InstEmptyWidth: + if syntax.EmptyOp(i.Arg)&^flag == 0 { + m.addq(q, i.Out, flag) + } + } +} + +const endText = -1 + +// computeNext computes the next DFA state if we're in d reading c (an input byte or endText). +func (m *matcher) computeNext(d *dstate, c int) *dstate { + this, next := &m.z1, &m.z2 + this.dec(d.enc) + + // compute flags in effect before c + flag := syntax.EmptyOp(0) + if this.flag&flagBOL != 0 { + flag |= syntax.EmptyBeginLine + } + if this.flag&flagBOT != 0 { + flag |= syntax.EmptyBeginText + } + if this.flag&flagWord != 0 { + if !isWordByte(c) { + flag |= syntax.EmptyWordBoundary + } else { + flag |= syntax.EmptyNoWordBoundary + } + } else { + if isWordByte(c) { + flag |= syntax.EmptyWordBoundary + } else { + flag |= syntax.EmptyNoWordBoundary + } + } + if c == '\n' { + flag |= syntax.EmptyEndLine + } + if c == endText { + flag |= syntax.EmptyEndLine | syntax.EmptyEndText + } + + // re-expand queue using new flags. + // TODO: only do this when it matters + // (something is gating on word boundaries). + m.stepEmpty(&this.q, &next.q, flag) + this, next = next, this + + // now compute flags after c. + flag = 0 + next.flag = 0 + if c == '\n' { + flag |= syntax.EmptyBeginLine + next.flag |= flagBOL + } + if isWordByte(c) { + next.flag |= flagWord + } + + // re-add start, process rune + expand according to flags. + if m.stepByte(&this.q, &next.q, c, flag) { + return &dmatch + } + return m.cache(next) +} + +func (m *matcher) cache(z *nstate) *dstate { + enc := z.enc() + d := m.dstate[enc] + if d != nil { + return d + } + + d = &dstate{enc: enc} + m.dstate[enc] = d + d.matchNL = m.computeNext(d, '\n') == &dmatch + d.matchEOT = m.computeNext(d, endText) == &dmatch + return d +} + +func (m *matcher) match(b []byte, beginText, endText bool) (end int) { + // fmt.Printf("%v\n", m.prog) + + d := m.startLine + if beginText { + d = m.start + } + // m.z1.dec(d.enc) + // fmt.Printf("%v (%v)\n", &m.z1, d==&dmatch) + for i, c := range b { + d1 := d.next[c] + if d1 == nil { + if c == '\n' { + if d.matchNL { + return i + } + d1 = m.startLine + } else { + d1 = m.computeNext(d, int(c)) + } + d.next[c] = d1 + } + d = d1 + // m.z1.dec(d.enc) + // fmt.Printf("%#U: %v (%v, %v, %v)\n", c, &m.z1, d==&dmatch, d.matchNL, d.matchEOT) + } + if d.matchNL || endText && d.matchEOT { + return len(b) + } + return -1 +} + +func (m *matcher) matchString(b string, beginText, endText bool) (end int) { + d := m.startLine + if beginText { + d = m.start + } + for i := 0; i < len(b); i++ { + c := b[i] + d1 := d.next[c] + if d1 == nil { + if c == '\n' { + if d.matchNL { + return i + } + d1 = m.startLine + } else { + d1 = m.computeNext(d, int(c)) + } + d.next[c] = d1 + } + d = d1 + } + if d.matchNL || endText && d.matchEOT { + return len(b) + } + return -1 +} + +// isWordByte reports whether the byte c is a word character: ASCII only. +// This is used to implement \b and \B. This is not right for Unicode, but: +// - it's hard to get right in a byte-at-a-time matching world +// (the DFA has only one-byte lookahead) +// - this crude approximation is the same one PCRE uses +func isWordByte(c int) bool { + return 'A' <= c && c <= 'Z' || + 'a' <= c && c <= 'z' || + '0' <= c && c <= '9' || + c == '_' +} + +// TODO: +type Grep struct { + Regexp *Regexp // regexp to search for + Stdout io.Writer // output target + Stderr io.Writer // error target + + L bool // L flag - print file names only + C bool // C flag - print count of matches + N bool // N flag - print line numbers + H bool // H flag - do not print file names + + Match bool + + buf []byte +} + +func (g *Grep) AddFlags() { + flag.BoolVar(&g.L, "l", false, "list matching files only") + flag.BoolVar(&g.C, "c", false, "print match counts only") + flag.BoolVar(&g.N, "n", false, "show line numbers") + flag.BoolVar(&g.H, "h", false, "omit file names") +} + +func (g *Grep) File(name string) { + f, err := os.Open(name) + if err != nil { + fmt.Fprintf(g.Stderr, "%s\n", err) + return + } + defer f.Close() + g.Reader(f, name) +} + +var nl = []byte{'\n'} + +func countNL(b []byte) int { + n := 0 + for { + i := bytes.IndexByte(b, '\n') + if i < 0 { + break + } + n++ + b = b[i+1:] + } + return n +} + +func (g *Grep) Reader(r io.Reader, name string) { + if g.buf == nil { + g.buf = make([]byte, 1<<20) + } + var ( + buf = g.buf[:0] + needLineno = g.N + lineno = 1 + count = 0 + prefix = "" + beginText = true + endText = false + ) + if !g.H { + prefix = name + ":" + } + for { + n, err := io.ReadFull(r, buf[len(buf):cap(buf)]) + buf = buf[:len(buf)+n] + end := len(buf) + if err == nil { + end = bytes.LastIndex(buf, nl) + 1 + } else { + endText = true + } + chunkStart := 0 + for chunkStart < end { + m1 := g.Regexp.Match(buf[chunkStart:end], beginText, endText) + chunkStart + beginText = false + if m1 < chunkStart { + break + } + g.Match = true + if g.L { + fmt.Fprintf(g.Stdout, "%s\n", name) + return + } + lineStart := bytes.LastIndex(buf[chunkStart:m1], nl) + 1 + chunkStart + lineEnd := m1 + 1 + if lineEnd > end { + lineEnd = end + } + if needLineno { + lineno += countNL(buf[chunkStart:lineStart]) + } + line := buf[lineStart:lineEnd] + switch { + case g.C: + count++ + case g.N: + fmt.Fprintf(g.Stdout, "%s%d:%s", prefix, lineno, line) + default: + fmt.Fprintf(g.Stdout, "%s%s", prefix, line) + } + if needLineno { + lineno++ + } + chunkStart = lineEnd + } + if needLineno && err == nil { + lineno += countNL(buf[chunkStart:end]) + } + n = copy(buf, buf[end:]) + buf = buf[:n] + if len(buf) == 0 && err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + fmt.Fprintf(g.Stderr, "%s: %v\n", name, err) + } + break + } + } + if g.C && count > 0 { + fmt.Fprintf(g.Stdout, "%s: %d\n", name, count) + } +} diff --git a/src/code.google.com/p/codesearch/regexp/regexp.go b/src/code.google.com/p/codesearch/regexp/regexp.go new file mode 100644 index 00000000..591b3c74 --- /dev/null +++ b/src/code.google.com/p/codesearch/regexp/regexp.go @@ -0,0 +1,59 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package regexp implements regular expression search tuned for +// use in grep-like programs. +package regexp + +import "regexp/syntax" + +func bug() { + panic("codesearch/regexp: internal error") +} + +// Regexp is the representation of a compiled regular expression. +// A Regexp is NOT SAFE for concurrent use by multiple goroutines. +type Regexp struct { + Syntax *syntax.Regexp + expr string // original expression + m matcher +} + +// String returns the source text used to compile the regular expression. +func (re *Regexp) String() string { + return re.expr +} + +// Compile parses a regular expression and returns, if successful, +// a Regexp object that can be used to match against lines of text. +func Compile(expr string) (*Regexp, error) { + re, err := syntax.Parse(expr, syntax.Perl) + if err != nil { + return nil, err + } + sre := re.Simplify() + prog, err := syntax.Compile(sre) + if err != nil { + return nil, err + } + if err := toByteProg(prog); err != nil { + return nil, err + } + r := &Regexp{ + Syntax: re, + expr: expr, + } + if err := r.m.init(prog); err != nil { + return nil, err + } + return r, nil +} + +func (r *Regexp) Match(b []byte, beginText, endText bool) (end int) { + return r.m.match(b, beginText, endText) +} + +func (r *Regexp) MatchString(s string, beginText, endText bool) (end int) { + return r.m.matchString(s, beginText, endText) +} diff --git a/src/code.google.com/p/codesearch/regexp/regexp_test.go b/src/code.google.com/p/codesearch/regexp/regexp_test.go new file mode 100644 index 00000000..08bde8dd --- /dev/null +++ b/src/code.google.com/p/codesearch/regexp/regexp_test.go @@ -0,0 +1,219 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package regexp + +import ( + "bytes" + "reflect" + "strings" + "testing" +) + +var nstateTests = []struct { + q []uint32 + partial rune +}{ + {[]uint32{1, 2, 3}, 1}, + {[]uint32{1}, 1}, + {[]uint32{}, 0}, + {[]uint32{1, 2, 8}, 0x10FFF}, +} + +func TestNstateEnc(t *testing.T) { + var n1, n2 nstate + n1.q.Init(10) + n2.q.Init(10) + for _, tt := range nstateTests { + n1.q.Reset() + n1.partial = tt.partial + for _, id := range tt.q { + n1.q.Add(id) + } + enc := n1.enc() + n2.dec(enc) + if n2.partial != n1.partial || !reflect.DeepEqual(n1.q.Dense(), n2.q.Dense()) { + t.Errorf("%v.enc.dec = %v", &n1, &n2) + } + } +} + +var matchTests = []struct { + re string + s string + m []int +}{ + // Adapted from go/src/pkg/regexp/find_test.go. + {`a+`, "abc\ndef\nghi\n", []int{1}}, + {``, ``, []int{1}}, + {`^abcdefg`, "abcdefg", []int{1}}, + {`a+`, "baaab", []int{1}}, + {"abcd..", "abcdef", []int{1}}, + {`a`, "a", []int{1}}, + {`x`, "y", nil}, + {`b`, "abc", []int{1}}, + {`.`, "a", []int{1}}, + {`.*`, "abcdef", []int{1}}, + {`^`, "abcde", []int{1}}, + {`$`, "abcde", []int{1}}, + {`^abcd$`, "abcd", []int{1}}, + {`^bcd'`, "abcdef", nil}, + {`^abcd$`, "abcde", nil}, + {`a+`, "baaab", []int{1}}, + {`a*`, "baaab", []int{1}}, + {`[a-z]+`, "abcd", []int{1}}, + {`[^a-z]+`, "ab1234cd", []int{1}}, + {`[a\-\]z]+`, "az]-bcz", []int{1}}, + {`[^\n]+`, "abcd\n", []int{1}}, + {`[日本語]+`, "日本語日本語", []int{1}}, + {`日本語+`, "日本語", []int{1}}, + {`日本語+`, "日本語語語語", []int{1}}, + {`()`, "", []int{1}}, + {`(a)`, "a", []int{1}}, + {`(.)(.)`, "日a", []int{1}}, + {`(.*)`, "", []int{1}}, + {`(.*)`, "abcd", []int{1}}, + {`(..)(..)`, "abcd", []int{1}}, + {`(([^xyz]*)(d))`, "abcd", []int{1}}, + {`((a|b|c)*(d))`, "abcd", []int{1}}, + {`(((a|b|c)*)(d))`, "abcd", []int{1}}, + {`\a\f\r\t\v`, "\a\f\r\t\v", []int{1}}, + {`[\a\f\n\r\t\v]+`, "\a\f\r\t\v", []int{1}}, + + {`a*(|(b))c*`, "aacc", []int{1}}, + {`(.*).*`, "ab", []int{1}}, + {`[.]`, ".", []int{1}}, + {`/$`, "/abc/", []int{1}}, + {`/$`, "/abc", nil}, + + // multiple matches + {`.`, "abc", []int{1}}, + {`(.)`, "abc", []int{1}}, + {`.(.)`, "abcd", []int{1}}, + {`ab*`, "abbaab", []int{1}}, + {`a(b*)`, "abbaab", []int{1}}, + + // fixed bugs + {`ab$`, "cab", []int{1}}, + {`axxb$`, "axxcb", nil}, + {`data`, "daXY data", []int{1}}, + {`da(.)a$`, "daXY data", []int{1}}, + {`zx+`, "zzx", []int{1}}, + {`ab$`, "abcab", []int{1}}, + {`(aa)*$`, "a", []int{1}}, + {`(?:.|(?:.a))`, "", nil}, + {`(?:A(?:A|a))`, "Aa", []int{1}}, + {`(?:A|(?:A|a))`, "a", []int{1}}, + {`(a){0}`, "", []int{1}}, + // {`(?-s)(?:(?:^).)`, "\n", nil}, + // {`(?s)(?:(?:^).)`, "\n", []int{1}}, + // {`(?:(?:^).)`, "\n", nil}, + {`\b`, "x", []int{1}}, + {`\b`, "xx", []int{1}}, + {`\b`, "x y", []int{1}}, + {`\b`, "xx yy", []int{1}}, + {`\B`, "x", nil}, + {`\B`, "xx", []int{1}}, + {`\B`, "x y", nil}, + {`\B`, "xx yy", []int{1}}, + {`(?im)^[abc]+$`, "abcABC", []int{1}}, + {`(?im)^[α]+$`, "αΑ", []int{1}}, + {`[Aa]BC`, "abc", nil}, + {`[Aa]bc`, "abc", []int{1}}, + + // RE2 tests + {`[^\S\s]`, "abcd", nil}, + {`[^\S[:space:]]`, "abcd", nil}, + {`[^\D\d]`, "abcd", nil}, + {`[^\D[:digit:]]`, "abcd", nil}, + {`(?i)\W`, "x", nil}, + {`(?i)\W`, "k", nil}, + {`(?i)\W`, "s", nil}, + + // can backslash-escape any punctuation + {`\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\{\|\}\~`, + `!"#$%&'()*+,-./:;<=>?@[\]^_{|}~`, []int{1}}, + {`[\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\{\|\}\~]+`, + `!"#$%&'()*+,-./:;<=>?@[\]^_{|}~`, []int{1}}, + {"\\`", "`", []int{1}}, + {"[\\`]+", "`", []int{1}}, + + // long set of matches (longer than startSize) + { + ".", + "qwertyuiopasdfghjklzxcvbnm1234567890", + []int{1}, + }, +} + +func TestMatch(t *testing.T) { + for _, tt := range matchTests { + re, err := Compile("(?m)" + tt.re) + if err != nil { + t.Errorf("Compile(%#q): %v", tt.re, err) + continue + } + b := []byte(tt.s) + lines := grep(re, b) + if !reflect.DeepEqual(lines, tt.m) { + t.Errorf("grep(%#q, %q) = %v, want %v", tt.re, tt.s, lines, tt.m) + } + } +} + +func grep(re *Regexp, b []byte) []int { + var m []int + lineno := 1 + for { + i := re.Match(b, true, true) + if i < 0 { + break + } + start := bytes.LastIndex(b[:i], nl) + 1 + end := i + 1 + if end > len(b) { + end = len(b) + } + lineno += bytes.Count(b[:start], nl) + m = append(m, lineno) + if start < end && b[end-1] == '\n' { + lineno++ + } + b = b[end:] + if len(b) == 0 { + break + } + } + return m +} + +var grepTests = []struct { + re string + s string + out string + err string + g Grep +}{ + {re: `a+`, s: "abc\ndef\nghalloo\n", out: "input:abc\ninput:ghalloo\n"}, + {re: `x.*y`, s: "xay\nxa\ny\n", out: "input:xay\n"}, +} + +func TestGrep(t *testing.T) { + for i, tt := range grepTests { + re, err := Compile("(?m)" + tt.re) + if err != nil { + t.Errorf("Compile(%#q): %v", tt.re, err) + continue + } + g := tt.g + g.Regexp = re + var out, errb bytes.Buffer + g.Stdout = &out + g.Stderr = &errb + g.Reader(strings.NewReader(tt.s), "input") + if out.String() != tt.out || errb.String() != tt.err { + t.Errorf("#%d: grep(%#q, %q) = %q, %q, want %q, %q", i, tt.re, tt.s, out.String(), errb.String(), tt.out, tt.err) + } + } +} diff --git a/src/code.google.com/p/codesearch/regexp/utf.go b/src/code.google.com/p/codesearch/regexp/utf.go new file mode 100644 index 00000000..d587eaaf --- /dev/null +++ b/src/code.google.com/p/codesearch/regexp/utf.go @@ -0,0 +1,268 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package regexp + +import ( + "regexp/syntax" + "unicode" + "unicode/utf8" +) + +const ( + instFail = syntax.InstFail + instAlt = syntax.InstAlt + instByteRange = syntax.InstRune | 0x80 // local opcode + + argFold = 1 << 16 +) + +func toByteProg(prog *syntax.Prog) error { + var b runeBuilder + for pc := range prog.Inst { + i := &prog.Inst[pc] + switch i.Op { + case syntax.InstRune, syntax.InstRune1: + // General rune range. PIA. + // TODO: Pick off single-byte case. + if lo, hi, fold, ok := oneByteRange(i); ok { + i.Op = instByteRange + i.Arg = uint32(lo)<<8 | uint32(hi) + if fold { + i.Arg |= argFold + } + break + } + + r := i.Rune + if syntax.Flags(i.Arg)&syntax.FoldCase != 0 { + // Build folded list. + var rr []rune + if len(r) == 1 { + rr = appendFoldedRange(rr, r[0], r[0]) + } else { + for j := 0; j < len(r); j += 2 { + rr = appendFoldedRange(rr, r[j], r[j+1]) + } + } + r = rr + } + + b.init(prog, uint32(pc), i.Out) + if len(r) == 1 { + b.addRange(r[0], r[0], false) + } else { + for j := 0; j < len(r); j += 2 { + b.addRange(r[j], r[j+1], false) + } + } + + case syntax.InstRuneAny, syntax.InstRuneAnyNotNL: + // All runes. + // AnyNotNL should exclude \n but the line-at-a-time + // execution takes care of that for us. + b.init(prog, uint32(pc), i.Out) + b.addRange(0, unicode.MaxRune, false) + } + } + return nil +} + +func oneByteRange(i *syntax.Inst) (lo, hi byte, fold, ok bool) { + if i.Op == syntax.InstRune1 { + r := i.Rune[0] + if r < utf8.RuneSelf { + return byte(r), byte(r), false, true + } + } + if i.Op != syntax.InstRune { + return + } + fold = syntax.Flags(i.Arg)&syntax.FoldCase != 0 + if len(i.Rune) == 1 || len(i.Rune) == 2 && i.Rune[0] == i.Rune[1] { + r := i.Rune[0] + if r >= utf8.RuneSelf { + return + } + if fold && !asciiFold(r) { + return + } + return byte(r), byte(r), fold, true + } + if len(i.Rune) == 2 && i.Rune[1] < utf8.RuneSelf { + if fold { + for r := i.Rune[0]; r <= i.Rune[1]; r++ { + if asciiFold(r) { + return + } + } + } + return byte(i.Rune[0]), byte(i.Rune[1]), fold, true + } + if len(i.Rune) == 4 && i.Rune[0] == i.Rune[1] && i.Rune[2] == i.Rune[3] && unicode.SimpleFold(i.Rune[0]) == i.Rune[2] && unicode.SimpleFold(i.Rune[2]) == i.Rune[0] { + return byte(i.Rune[0]), byte(i.Rune[0]), true, true + } + + return +} + +func asciiFold(r rune) bool { + if r >= utf8.RuneSelf { + return false + } + r1 := unicode.SimpleFold(r) + if r1 >= utf8.RuneSelf { + return false + } + if r1 == r { + return true + } + return unicode.SimpleFold(r1) == r +} + +func maxRune(n int) rune { + b := 0 + if n == 1 { + b = 7 + } else { + b = 8 - (n + 1) + 6*(n-1) + } + return 1< 0xbf { + // Not a continuation byte, no need to cache. + return b.uncachedSuffix(lo, hi, fold, next) + } + + key := cacheKey{lo, hi, fold, next} + if pc, ok := b.cache[key]; ok { + return pc + } + + pc := b.uncachedSuffix(lo, hi, fold, next) + b.cache[key] = pc + return pc +} + +func (b *runeBuilder) addBranch(pc uint32) { + // Add pc to the branch at the beginning. + i := &b.p.Inst[b.begin] + switch i.Op { + case syntax.InstFail: + i.Op = syntax.InstNop + i.Out = pc + return + case syntax.InstNop: + i.Op = syntax.InstAlt + i.Arg = pc + return + case syntax.InstAlt: + apc := uint32(len(b.p.Inst)) + b.p.Inst = append(b.p.Inst, syntax.Inst{Op: instAlt, Out: i.Arg, Arg: pc}) + i = &b.p.Inst[b.begin] + i.Arg = apc + b.begin = apc + } +} + +func (b *runeBuilder) addRange(lo, hi rune, fold bool) { + if lo > hi { + return + } + + // TODO: Pick off 80-10FFFF for special handling? + if lo == 0x80 && hi == 0x10FFFF { + } + + // Split range into same-length sized ranges. + for i := 1; i < utf8.UTFMax; i++ { + max := maxRune(i) + if lo <= max && max < hi { + b.addRange(lo, max, fold) + b.addRange(max+1, hi, fold) + return + } + } + + // ASCII range is special. + if hi < utf8.RuneSelf { + b.addBranch(b.suffix(byte(lo), byte(hi), fold, 0)) + return + } + + // Split range into sections that agree on leading bytes. + for i := 1; i < utf8.UTFMax; i++ { + m := rune(1)<= 0; i-- { + pc = b.suffix(ulo[i], uhi[i], false, pc) + } + b.addBranch(pc) +} diff --git a/src/code.google.com/p/codesearch/sparse/set.go b/src/code.google.com/p/codesearch/sparse/set.go new file mode 100644 index 00000000..00521755 --- /dev/null +++ b/src/code.google.com/p/codesearch/sparse/set.go @@ -0,0 +1,65 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package sparse implements sparse sets. +package sparse + +// For comparison: running cindex over the Linux 2.6 kernel with this +// implementation of trigram sets takes 11 seconds. If I change it to +// a bitmap (which must be cleared between files) it takes 25 seconds. + +// A Set is a sparse set of uint32 values. +// http://research.swtch.com/2008/03/using-uninitialized-memory-for-fun-and.html +type Set struct { + dense []uint32 + sparse []uint32 +} + +// NewSet returns a new Set with a given maximum size. +// The set can contain numbers in [0, max-1]. +func NewSet(max uint32) *Set { + return &Set{ + sparse: make([]uint32, max), + } +} + +// Init initializes a Set to have a given maximum size. +// The set can contain numbers in [0, max-1]. +func (s *Set) Init(max uint32) { + s.sparse = make([]uint32, max) +} + +// Reset clears (empties) the set. +func (s *Set) Reset() { + s.dense = s.dense[:0] +} + +// Add adds x to the set if it is not already there. +func (s *Set) Add(x uint32) { + v := s.sparse[x] + if v < uint32(len(s.dense)) && s.dense[v] == x { + return + } + n := len(s.dense) + s.sparse[x] = uint32(n) + s.dense = append(s.dense, x) +} + +// Has reports whether x is in the set. +func (s *Set) Has(x uint32) bool { + v := s.sparse[x] + return v < uint32(len(s.dense)) && s.dense[v] == x +} + +// Dense returns the values in the set. +// The values are listed in the order in which they +// were inserted. +func (s *Set) Dense() []uint32 { + return s.dense +} + +// Len returns the number of values in the set. +func (s *Set) Len() int { + return len(s.dense) +} diff --git a/src/hound/api/api.go b/src/hound/api/api.go new file mode 100644 index 00000000..187df4b7 --- /dev/null +++ b/src/hound/api/api.go @@ -0,0 +1,213 @@ +package api + +import ( + "encoding/json" + "fmt" + "hound/config" + "hound/index" + "hound/searcher" + "log" + "net/http" + "strconv" + "strings" + "time" +) + +const ( + defaultLinesOfContext uint = 2 + maxLinesOfContext uint = 20 +) + +type Stats struct { + FilesOpened int + Duration int +} + +func writeJson(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json;charset=utf-8") + w.Header().Set("Access-Control-Allow", "*") + if err := json.NewEncoder(w).Encode(data); err != nil { + log.Panicf("Failed to encode JSON: %v\n", err) + } +} + +func writeError(w http.ResponseWriter, err error) { + writeJson(w, map[string]interface{}{ + "Error": fmt.Sprint(err), + }) +} + +type searchResponse struct { + repo string + res *index.SearchResponse + err error +} + +/** + * Searches all repos in parallel. + */ +func searchAll( + query string, + opts *index.SearchOptions, + repos []string, + idx map[string]*searcher.Searcher, + filesOpened *int, + duration *int) (map[string]*index.SearchResponse, error) { + + startedAt := time.Now() + + n := len(repos) + + // use a buffered channel to avoid routine leaks on errs. + ch := make(chan *searchResponse, n) + for _, repo := range repos { + go func(repo string) { + fms, err := idx[repo].Search(query, opts) + ch <- &searchResponse{repo, fms, err} + }(repo) + } + + res := map[string]*index.SearchResponse{} + for i := 0; i < n; i++ { + r := <-ch + if r.err != nil { + return nil, r.err + } + + if r.res.Matches == nil { + continue + } + + res[r.repo] = r.res + *filesOpened += r.res.FilesOpened + } + + *duration = int(time.Now().Sub(startedAt).Seconds() * 1000) + + return res, nil +} + +// Used for parsing flags from form values. +func parseAsBool(v string) bool { + v = strings.ToLower(v) + return v == "true" || v == "1" || v == "fosho" +} + +func parseAsRepoList(v string, idx map[string]*searcher.Searcher) []string { + v = strings.TrimSpace(strings.ToLower(v)) + var repos []string + if v == "*" { + for repo, _ := range idx { + repos = append(repos, repo) + } + return repos + } + + for _, repo := range strings.Split(v, ",") { + if idx[repo] == nil { + continue + } + repos = append(repos, repo) + } + return repos +} + +func parseAsUintValue(sv string, min, max, def uint) uint { + iv, err := strconv.ParseUint(sv, 10, 54) + if err != nil { + return def + } + if max != 0 && uint(iv) > max { + return max + } + if min != 0 && uint(iv) < min { + return max + } + return uint(iv) +} + +func parseRangeInt(v string, i *int) { + *i = 0 + if v == "" { + return + } + + vi, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return + } + + *i = int(vi) +} + +func parseRangeValue(rv string) (int, int) { + ix := strings.Index(rv, ":") + if ix < 0 { + return 0, 0 + } + + var b, e int + parseRangeInt(rv[:ix], &b) + parseRangeInt(rv[ix+1:], &e) + return b, e +} + +func Setup(m *http.ServeMux, idx map[string]*searcher.Searcher) { + + m.HandleFunc("/api/v1/repos", func(w http.ResponseWriter, r *http.Request) { + res := map[string]*config.Repo{} + for name, srch := range idx { + res[name] = srch.Repo + } + + writeJson(w, res) + }) + + m.HandleFunc("/api/v1/search", func(w http.ResponseWriter, r *http.Request) { + var opt index.SearchOptions + + stats := parseAsBool(r.FormValue("stats")) + repos := parseAsRepoList(r.FormValue("repos"), idx) + query := r.FormValue("q") + opt.Offset, opt.Limit = parseRangeValue(r.FormValue("rng")) + opt.FileRegexp = r.FormValue("files") + opt.IgnoreCase = parseAsBool(r.FormValue("i")) + opt.LinesOfContext = parseAsUintValue( + r.FormValue("ctx"), + 0, + maxLinesOfContext, + defaultLinesOfContext) + + var filesOpened int + var durationMs int + + results, err := searchAll(query, &opt, repos, idx, &filesOpened, &durationMs) + if err != nil { + writeError(w, err) + return + } + + var res struct { + Results map[string]*index.SearchResponse + Stats *Stats `json:",omitempty"` + } + + res.Results = results + if stats { + res.Stats = &Stats{ + FilesOpened: filesOpened, + Duration: durationMs, + } + } + + writeJson(w, &res) + }) + + m.HandleFunc("/api/v1/excludes", func(w http.ResponseWriter, r *http.Request) { + repo := r.FormValue("repo") + res := idx[repo].GetExcludedFiles() + w.Header().Set("Content-Type", "application/json;charset=utf-8") + w.Header().Set("Access-Control-Allow", "*") + fmt.Fprint(w, res) + }) +} diff --git a/src/hound/client/ack.go b/src/hound/client/ack.go new file mode 100644 index 00000000..58cf4df6 --- /dev/null +++ b/src/hound/client/ack.go @@ -0,0 +1,114 @@ +package client + +import ( + "ansi" + "bytes" + "fmt" + "hound/config" + "os" + "regexp" +) + +type ackPresenter struct { + f *os.File +} + +func hiliteMatches(c *ansi.Colorer, p *regexp.Regexp, line string) string { + // find the indexes for all matches + idxs := p.FindAllStringIndex(line, -1) + + var buf bytes.Buffer + beg := 0 + + for _, idx := range idxs { + // for each match add the contents before the match ... + buf.WriteString(line[beg:idx[0]]) + // and the highlighted version of the match + buf.WriteString(c.FgBg(line[idx[0]:idx[1]], + ansi.Black, + ansi.Bold, + ansi.Yellow, + ansi.Intense)) + beg = idx[1] + } + + buf.WriteString(line[beg:]) + + return buf.String() +} + +func lineNumber(c *ansi.Colorer, buf *bytes.Buffer, n int, hasMatch bool) string { + defer buf.Reset() + + s := fmt.Sprintf("%d", n) + buf.WriteString(c.Fg(s, ansi.Yellow, ansi.Bold)) + if hasMatch { + buf.WriteByte(':') + } else { + buf.WriteByte('-') + } + for i := len(s); i < 6; i++ { + buf.WriteByte(' ') + } + return buf.String() +} + +func (p *ackPresenter) Present( + re *regexp.Regexp, + ctx int, + repos map[string]*config.Repo, + res *Response) error { + + c := ansi.NewFor(p.f) + + buf := bytes.NewBuffer(make([]byte, 0, 20)) + + for repo, resp := range res.Results { + if _, err := fmt.Fprintf(p.f, "%s\n", + c.Fg(repoNameFor(repos, repo), ansi.Red, ansi.Bold)); err != nil { + return err + } + + for _, file := range resp.Matches { + if _, err := fmt.Fprintf(p.f, "%s\n", + c.Fg(file.Filename, ansi.Green, ansi.Bold)); err != nil { + return err + } + + blocks := coalesceMatches(file.Matches) + + for _, block := range blocks { + for i, n := 0, len(block.Lines); i < n; i++ { + line := block.Lines[i] + hasMatch := block.Matches[i] + + if hasMatch { + line = hiliteMatches(c, re, line) + } + + if _, err := fmt.Fprintf(p.f, "%s%s\n", + lineNumber(c, buf, block.Start+i, hasMatch), + line); err != nil { + return err + } + } + + if ctx > 0 { + if _, err := fmt.Fprintln(p.f, "--"); err != nil { + return err + } + } + } + + if _, err := fmt.Fprintln(p.f); err != nil { + return err + } + } + } + + return nil +} + +func NewAckPresenter(w *os.File) Presenter { + return &ackPresenter{w} +} diff --git a/src/hound/client/client.go b/src/hound/client/client.go new file mode 100644 index 00000000..c302e72d --- /dev/null +++ b/src/hound/client/client.go @@ -0,0 +1,152 @@ +package client + +import ( + "encoding/json" + "fmt" + "hound/config" + "hound/index" + "net/http" + "net/url" + "regexp" + "strings" +) + +type Response struct { + Results map[string]*index.SearchResponse + Stats *struct { + FilesOpened int + Duration int + } `json:",omitempty"` +} + +type Presenter interface { + Present( + re *regexp.Regexp, + ctx int, + repos map[string]*config.Repo, + res *Response) error +} + +type Config struct { + HttpHeaders map[string]string `json:"http-headers"` + Host string `json:"host"` +} + +// Extract a repo name from the given url. +func repoNameFromUrl(uri string) string { + ax := strings.LastIndex(uri, "/") + if ax < 0 { + return "" + } + + name := uri[ax+1:] + if strings.HasSuffix(name, ".git") { + name = name[:len(name)-4] + } + + bx := strings.LastIndex(uri[:ax-1], "/") + if bx < 0 { + return name + } + + return fmt.Sprintf("%s/%s", uri[bx+1:ax], name) +} + +// Find the proper name for the given repo using the map of repo +// information. +func repoNameFor(repos map[string]*config.Repo, repo string) string { + data := repos[repo] + if data == nil { + return repo + } + + name := repoNameFromUrl(data.Url) + if name == "" { + return repo + } + + return name +} + +func doHttpGet(cfg *Config, uri string) (*http.Response, error) { + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + return nil, err + } + + for key, val := range cfg.HttpHeaders { + if strings.ToLower(key) == "host" { + req.Host = val + } else { + req.Header.Set(key, val) + } + } + + var c http.Client + return c.Do(req) +} + +// Executes a search on the API running on host. +func Search(r *Response, cfg *Config, pattern, repos, files string, context int, ignoreCase, stats bool) error { + u := fmt.Sprintf("http://%s/api/v1/search?%s", + cfg.Host, + url.Values{ + "q": {pattern}, + "repos": {repos}, + "files": {files}, + "ctx": {fmt.Sprintf("%d", context)}, + "i": {fmt.Sprintf("%t", ignoreCase)}, + "stats": {fmt.Sprintf("%t", stats)}, + }.Encode()) + + res, err := doHttpGet(cfg, u) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return fmt.Errorf("Status %d", res.StatusCode) + } + + return json.NewDecoder(res.Body).Decode(r) +} + +// Load the list of repositories from the API running on host. +func LoadRepos(repos map[string]*config.Repo, cfg *Config) error { + res, err := doHttpGet(cfg, fmt.Sprintf("http://%s/api/v1/repos", cfg.Host)) + if err != nil { + return err + } + defer res.Body.Close() + + return json.NewDecoder(res.Body).Decode(&repos) +} + +// Execute a search and load the list of repositories in parallel on the host. +func SearchAndLoadRepos(cfg *Config, pattern, repos, files string, context int, ignoreCase, stats bool) (*Response, map[string]*config.Repo, error) { + chs := make(chan error) + var res Response + go func() { + chs <- Search(&res, cfg, pattern, repos, files, context, ignoreCase, stats) + }() + + chr := make(chan error) + rep := map[string]*config.Repo{} + go func() { + chr <- LoadRepos(rep, cfg) + }() + + // must ready both channels before returning to avoid routine/channel leak. + errS, errR := <-chs, <-chr + + if errS != nil { + return nil, nil, errS + } + + if errR != nil { + return nil, nil, errR + } + + return &res, rep, nil +} diff --git a/src/hound/client/coalesce.go b/src/hound/client/coalesce.go new file mode 100644 index 00000000..469f9297 --- /dev/null +++ b/src/hound/client/coalesce.go @@ -0,0 +1,99 @@ +package client + +import ( + "hound/index" +) + +type Block struct { + Lines []string + Matches []bool + Start int +} + +func endOfBlock(b *Block) int { + return b.Start + len(b.Lines) - 1 +} + +func startOfMatch(m *index.Match) int { + return m.LineNumber - len(m.Before) +} + +func matchIsInBlock(m *index.Match, b *Block) bool { + return startOfMatch(m) <= endOfBlock(b) +} + +func matchToBlock(m *index.Match) *Block { + b, a := len(m.Before), len(m.After) + n := 1 + b + a + l := make([]string, 0, n) + v := make([]bool, n) + + v[b] = true + + for _, line := range m.Before { + l = append(l, line) + } + + l = append(l, m.Line) + + for _, line := range m.After { + l = append(l, line) + } + + return &Block{ + Lines: l, + Matches: v, + Start: m.LineNumber - len(m.Before), + } +} + +func clampZero(n int) int { + if n < 0 { + return 0 + } + return n +} + +func mergeMatchIntoBlock(m *index.Match, b *Block) { + off := endOfBlock(b) - startOfMatch(m) + 1 + idx := len(b.Lines) - off + nb := len(m.Before) + + for i := off; i < nb; i++ { + b.Lines = append(b.Lines, m.Before[i]) + b.Matches = append(b.Matches, false) + } + + if off < nb+1 { + b.Lines = append(b.Lines, m.Line) + b.Matches = append(b.Matches, true) + } else { + b.Matches[idx+nb] = true + } + + for i, n := clampZero(off-nb-1), len(m.After); i < n; i++ { + b.Lines = append(b.Lines, m.After[i]) + b.Matches = append(b.Matches, false) + } +} + +func coalesceMatches(matches []*index.Match) []*Block { + var res []*Block + var curr *Block + for _, match := range matches { + if curr != nil && matchIsInBlock(match, curr) { + mergeMatchIntoBlock(match, curr) + } else { + if curr != nil { + res = append(res, curr) + } + curr = matchToBlock(match) + } + } + + if curr != nil { + res = append(res, curr) + } + + return res +} diff --git a/src/hound/client/coalesce_test.go b/src/hound/client/coalesce_test.go new file mode 100644 index 00000000..c070744c --- /dev/null +++ b/src/hound/client/coalesce_test.go @@ -0,0 +1,259 @@ +package client + +import ( + "hound/index" + "testing" +) + +// TODO(knorton): +// - Test multiple overlapping. +// - Test asymmetric context + +func stringSlicesAreSame(a, b []string) bool { + if len(a) != len(b) { + return false + } + + for i, n := 0, len(a); i < n; i++ { + if a[i] != b[i] { + return false + } + } + + return true +} + +func boolSlicesAreSame(a, b []bool) bool { + if len(a) != len(b) { + return false + } + + for i, n := 0, len(a); i < n; i++ { + if a[i] != b[i] { + return false + } + } + + return true +} + +func assertBlocksAreSame(t *testing.T, a, b *Block) bool { + if !stringSlicesAreSame(a.Lines, b.Lines) { + t.Errorf("bad lines: expected: %v, got: %v", a.Lines, b.Lines) + return false + } + + if !boolSlicesAreSame(a.Matches, b.Matches) { + t.Errorf("bad matches: expected: %v, got: %v", a.Matches, b.Matches) + return false + } + + if a.Start != b.Start { + t.Errorf("bad start: expected %d, got %d", a.Start, b.Start) + return false + } + + return true +} + +func assertBlockSlicesAreSame(t *testing.T, a, b []*Block) bool { + if len(a) != len(b) { + t.Errorf("blocks do not match, len(a)=%d & len(b)=%d", len(a), len(b)) + return false + } + + for i, n := 0, len(a); i < n; i++ { + if !assertBlocksAreSame(t, a[i], b[i]) { + return false + } + } + + return true +} + +func testThis(t *testing.T, subj []*index.Match, expt []*Block, desc string) { + if !assertBlockSlicesAreSame(t, expt, coalesceMatches(subj)) { + t.Errorf("case failed: %s", desc) + } +} + +func TestNonOverlap(t *testing.T) { + subj := []*index.Match{ + &index.Match{ + Line: "c", + LineNumber: 40, + Before: []string{"a", "b"}, + After: []string{"d", "e"}, + }, + &index.Match{ + Line: "n", + LineNumber: 50, + Before: []string{"l", "m"}, + After: []string{"o", "p"}, + }, + } + + expt := []*Block{ + &Block{ + Lines: []string{"a", "b", "c", "d", "e"}, + Matches: []bool{false, false, true, false, false}, + Start: 38, + }, + &Block{ + Lines: []string{"l", "m", "n", "o", "p"}, + Matches: []bool{false, false, true, false, false}, + Start: 48, + }, + } + + testThis(t, subj, expt, + "non-overlap w/ context") +} +func TestNonOverlapWithNoContext(t *testing.T) { + subj := []*index.Match{ + &index.Match{ + Line: "a", + LineNumber: 40, + }, + &index.Match{ + Line: "b", + LineNumber: 50, + }, + } + + expt := []*Block{ + &Block{ + Lines: []string{"a"}, + Matches: []bool{true}, + Start: 40, + }, + + &Block{ + Lines: []string{"b"}, + Matches: []bool{true}, + Start: 50, + }, + } + + testThis(t, subj, expt, + "non-overlap w/o context") +} + +func TestOverlappingInBefore(t *testing.T) { + subj := []*index.Match{ + &index.Match{ + Line: "c", + LineNumber: 40, + Before: []string{"a", "b"}, + After: []string{"d", "e"}, + }, + &index.Match{ + Line: "g", + LineNumber: 44, + Before: []string{"e", "f"}, + After: []string{"h", "i"}, + }, + } + + expt := []*Block{ + &Block{ + Lines: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, + Matches: []bool{false, false, true, false, false, false, true, false, false}, + Start: 38, + }, + } + + testThis(t, subj, expt, + "overlap in before") +} +func TestOverlappingInAfter(t *testing.T) { + subj := []*index.Match{ + &index.Match{ + Line: "c", + LineNumber: 40, + Before: []string{"a", "b"}, + After: []string{"d", "e"}, + }, + &index.Match{ + Line: "d", + LineNumber: 41, + Before: []string{"b", "c"}, + After: []string{"e", "f"}, + }, + } + + expt := []*Block{ + &Block{ + Lines: []string{"a", "b", "c", "d", "e", "f"}, + Matches: []bool{false, false, true, true, false, false}, + Start: 38, + }, + } + + testThis(t, subj, expt, + "overlap in after") +} + +func TestOverlapOnMatch(t *testing.T) { + subj := []*index.Match{ + &index.Match{ + Line: "c", + LineNumber: 40, + Before: []string{"a", "b"}, + After: []string{"d", "e"}, + }, + &index.Match{ + Line: "e", + LineNumber: 42, + Before: []string{"c", "d"}, + After: []string{"f", "g"}, + }, + } + + expt := []*Block{ + &Block{ + Lines: []string{"a", "b", "c", "d", "e", "f", "g"}, + Matches: []bool{false, false, true, false, true, false, false}, + Start: 38, + }, + } + + testThis(t, subj, expt, + "overlap on match") +} + +func TestMatchesToEnd(t *testing.T) { + file := []string{ + "import analytics.sequence._;", + "import analytics._;", + "println(\"Try running\")", + "println(\"val visits = VisitExplorer(100)\");", + } + + subj := []*index.Match{ + &index.Match{ + Line: file[2], + LineNumber: 3, + Before: []string{file[0], file[1]}, + After: []string{file[3]}, + }, + + &index.Match{ + Line: file[3], + LineNumber: 4, + Before: []string{file[1], file[2]}, + After: nil, + }, + } + + expt := []*Block{ + &Block{ + Lines: []string{file[0], file[1], file[2], file[3]}, + Matches: []bool{false, false, true, true}, + Start: 1, + }, + } + + testThis(t, subj, expt, + "test matches at end of file") +} diff --git a/src/hound/client/grep.go b/src/hound/client/grep.go new file mode 100644 index 00000000..51b81112 --- /dev/null +++ b/src/hound/client/grep.go @@ -0,0 +1,33 @@ +package client + +import ( + "ansi" + "fmt" + "hound/config" + "os" + "regexp" +) + +type grepPresenter struct { + f *os.File +} + +func (p *grepPresenter) Present( + re *regexp.Regexp, + ctx int, + repos map[string]*config.Repo, + res *Response) error { + + c := ansi.NewFor(p.f) + + if _, err := fmt.Fprintf(p.f, "%s\n", + c.Fg("// TODO(knorton): Implement", ansi.Yellow, ansi.Bold)); err != nil { + return err + } + + return nil +} + +func NewGrepPresenter(w *os.File) Presenter { + return &grepPresenter{w} +} diff --git a/src/hound/cmds/hound/main.go b/src/hound/cmds/hound/main.go new file mode 100644 index 00000000..c0d5e899 --- /dev/null +++ b/src/hound/cmds/hound/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "encoding/json" + "flag" + "hound/client" + "hound/index" + "log" + "os" + "os/user" + "regexp" +) + +// A uninitialized variable that can be defined during the build process with +// -ldflags -X main.defaultHouse addr. This should remain uninitialized. +var defaultHost string + +// a convenience method for creating a new presenter that is either +// ack-like or grep-like. +func newPresenter(likeGrep bool) client.Presenter { + if likeGrep { + return client.NewGrepPresenter(os.Stdout) + } + + return client.NewAckPresenter(os.Stdout) +} + +// the paths we will attempt to load config from +var configPaths = []string{ + "/etc/hound.conf", + "$HOME/.hound", +} + +// Attempt to populate a client.Config from the json found in +// filename. +func loadConfigFrom(filename string, cfg *client.Config) error { + r, err := os.Open(filename) + if err != nil { + return err + } + defer r.Close() + + return json.NewDecoder(r).Decode(cfg) +} + +// Attempt to populate a client.Config from the json found in +// any of the configPaths. +func loadConfig(cfg *client.Config) error { + u, err := user.Current() + if err != nil { + return err + } + + env := map[string]string{ + "HOME": u.HomeDir, + } + + for _, path := range configPaths { + err = loadConfigFrom(os.Expand(path, func(name string) string { + return env[name] + }), cfg) + + if os.IsNotExist(err) { + continue + } else if err != nil { + return err + } + } + + return nil +} + +// A simple way to determine what the default value should be +// for the --host flag. +func defaultFlagForHost() string { + if defaultHost != "" { + return defaultHost + } + return "localhost:6080" +} + +func main() { + flagHost := flag.String("host", defaultFlagForHost(), "") + flagRepos := flag.String("repos", "*", "") + flagFiles := flag.String("files", "", "") + flagContext := flag.Int("context", 2, "") + flagCase := flag.Bool("ignore-case", false, "") + flagStats := flag.Bool("show-stats", false, "") + flagGrep := flag.Bool("like-grep", false, "") + + flag.Parse() + + if flag.NArg() != 1 { + flag.Usage() + return + } + + pat := index.GetRegexpPattern(flag.Arg(0), *flagCase) + + reg, err := regexp.Compile(pat) + if err != nil { + // TODO(knorton): Better error reporting + log.Panic(err) + } + + cfg := client.Config{ + Host: *flagHost, + HttpHeaders: nil, + } + + if err := loadConfig(&cfg); err != nil { + log.Panic(err) + } + + res, repos, err := client.SearchAndLoadRepos(&cfg, + flag.Arg(0), + *flagRepos, + *flagFiles, + *flagContext, + *flagCase, + *flagStats) + if err != nil { + log.Panic(err) + } + + if err := newPresenter(*flagGrep).Present(reg, *flagContext, repos, res); err != nil { + log.Panic(err) + } +} diff --git a/src/hound/cmds/houndd/main.go b/src/hound/cmds/houndd/main.go new file mode 100644 index 00000000..571bf019 --- /dev/null +++ b/src/hound/cmds/houndd/main.go @@ -0,0 +1,312 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "hound/api" + "hound/config" + "hound/searcher" + "html/template" + "io" + "log" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strings" +) + +var ( + info_log *log.Logger + error_log *log.Logger +) + +type content struct { + template string + dest string + sources []string +} + +const ( + ReactVersion = "0.12.2" + jQueryVersion = "2.1.3" +) + +func checkForJsx() error { + return exec.Command("jsx", "--version").Run() +} + +func (c *content) render(w io.Writer, root string, cfg *config.Config, prod bool) error { + t, err := template.ParseFiles(filepath.Join(root, c.template)) + if err != nil { + return err + } + + json, err := cfg.ToJsonString() + if err != nil { + return err + } + + var src template.HTML + if prod { + s, err := sourceForPrd(root, c.sources) + if err != nil { + return err + } + src = s + } else { + src = sourceForDev(c.sources) + } + + return t.Execute(w, map[string]interface{}{ + "ReactVersion": ReactVersion, + "jQueryVersion": jQueryVersion, + "ReposAsJson": json, + "Source": src, + }) +} + +func (c *content) renderToFile(filename string, root string, cfg *config.Config, prod bool) error { + w, err := os.Create(filename) + if err != nil { + return err + } + defer w.Close() + + return c.render(w, root, cfg, prod) +} + +type devHandler struct { + http.Handler + root string + cfg *config.Config + content map[string]*content +} + +func sourceForPrd(root string, paths []string) (template.HTML, error) { + var buf bytes.Buffer + fmt.Fprintln(&buf, "") + return template.HTML(buf.String()), nil +} + +func sourceForDev(paths []string) template.HTML { + var buf bytes.Buffer + fmt.Fprintf(&buf, "\n", ReactVersion) + for _, path := range paths { + fmt.Fprintf(&buf, "", path) + } + return template.HTML(buf.String()) +} + +func (h *devHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + p := r.URL.Path + + c := h.content[p] + if c == nil { + h.Handler.ServeHTTP(w, r) + return + } + + w.Header().Set("Content-Type", "text/html;charset=utf-8") + if err := c.render(w, h.root, h.cfg, false); err != nil { + panic(err) + } +} + +func BuildContentFor(root string, prod bool, cnts []*content, cfg *config.Config) (http.Handler, error) { + if prod { + for _, cnt := range cnts { + if err := cnt.renderToFile(filepath.Join(root, cnt.dest), root, cfg, prod); err != nil { + return nil, err + } + } + + return http.FileServer(http.Dir(root)), nil + } + + m := map[string]*content{} + for _, cnt := range cnts { + if strings.HasSuffix(cnt.dest, "index.html") { + m[path.Clean("/"+filepath.Dir(cnt.dest)+"/")] = cnt + } else { + m["/"+cnt.dest] = cnt + } + } + + return &devHandler{ + Handler: http.FileServer(http.Dir(root)), + root: root, + cfg: cfg, + content: m, + }, nil +} + +func makeSearchers( + cfg *config.Config, + useStaleIndex bool) (map[string]*searcher.Searcher, error) { + // Ensure we have a dbpath + if _, err := os.Stat(cfg.DbPath); err != nil { + if err := os.MkdirAll(cfg.DbPath, os.ModePerm); err != nil { + return nil, err + } + } + + // Now build and initialize a searcher for each repo. + // TODO(knorton): These could be done in parallel. + m := map[string]*searcher.Searcher{} + for name, repo := range cfg.Repos { + path := filepath.Join(cfg.DbPath, name) + + var s *searcher.Searcher + var err error + + if useStaleIndex { + s, err = searcher.NewFromExisting(path, repo) + } else { + s, err = searcher.New(path, repo) + } + + if err != nil { + return nil, err + } + m[strings.ToLower(name)] = s + + } + + return m, nil +} + +func makeTemplateData(cfg *config.Config) (interface{}, error) { + var data struct { + ReposAsJson string + } + + res := map[string]*config.Repo{} + for name, repo := range cfg.Repos { + res[strings.ToLower(name)] = repo + } + + b, err := json.Marshal(res) + if err != nil { + return nil, err + } + + data.ReposAsJson = string(b) + return &data, nil +} + +func runHttp(addr, root string, prod bool, cfg *config.Config, idx map[string]*searcher.Searcher) error { + m := http.DefaultServeMux + + contents := []*content{ + &content{ + template: "index.tpl.html", + dest: "index.html", + sources: []string{ + "assets/js/hound.js", + }, + }, + &content{ + template: "excluded_files.tpl.html", + dest: "excluded_files.html", + sources: []string{ + "assets/js/excluded_files.js"}, + }, + } + + handler, err := BuildContentFor( + filepath.Join(root, "pub"), + prod, + contents, + cfg) + if err != nil { + return err + } + + m.Handle("/", handler) + + api.Setup(m, idx) + return http.ListenAndServe(addr, m) +} + +func findRoot(root *string) error { + if *root == "" { + return nil + } + + _, file, _, _ := runtime.Caller(0) + dir, err := filepath.Abs( + filepath.Join(filepath.Dir(file), "../../")) + if err != nil { + return err + } + + *root = dir + return nil +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) + info_log = log.New(os.Stdout, "", log.LstdFlags) + error_log = log.New(os.Stderr, "", log.LstdFlags) + + flagConf := flag.String("conf", "config.json", "") + flagAddr := flag.String("addr", ":6080", "") + flagRoot := flag.String("root", "", "") + flagProd := flag.Bool("prod", false, "") + flagStale := flag.Bool("use-existing-stale-index", false, + "DEV: Do not talk to git via pull or clone (requires an existing index)") + + flag.Parse() + + // In prod mode, we will need jsx. + if *flagProd { + if err := checkForJsx(); err != nil { + panic("You need to install jsx. (npm install -g react-tools)") + } + } + + if err := findRoot(flagRoot); err != nil { + panic(err) + } + + var cfg config.Config + if err := cfg.LoadFromFile(*flagConf); err != nil { + panic(err) + } + + idx, err := makeSearchers(&cfg, *flagStale) + if err != nil { + panic(err) + } + + info_log.Printf("All indexes built, running server at http://localhost%s...\n", *flagAddr) + + if err := runHttp(*flagAddr, *flagRoot, *flagProd, &cfg, idx); err != nil { + panic(err) + } +} diff --git a/src/hound/config/config.go b/src/hound/config/config.go new file mode 100644 index 00000000..a2d100ff --- /dev/null +++ b/src/hound/config/config.go @@ -0,0 +1,57 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" +) + +const defaultMsBetweenPoll = 30000 + +type Repo struct { + Url string `json:"url"` + MsBetweenPolls int `json:"ms-between-poll"` +} + +type Config struct { + DbPath string `json:"dbpath"` + Repos map[string]*Repo `json:"repos"` +} + +func (c *Config) LoadFromFile(filename string) error { + r, err := os.Open(filename) + if err != nil { + return err + } + defer r.Close() + + if err := json.NewDecoder(r).Decode(c); err != nil { + return err + } + + if !filepath.IsAbs(c.DbPath) { + path, err := filepath.Abs( + filepath.Join(filepath.Dir(filename), c.DbPath)) + if err != nil { + return err + } + c.DbPath = path + } + + for _, repo := range c.Repos { + if repo.MsBetweenPolls == 0 { + repo.MsBetweenPolls = defaultMsBetweenPoll + } + } + + return nil +} + +func (c *Config) ToJsonString() (string, error) { + b, err := json.Marshal(c.Repos) + if err != nil { + return "", err + } + + return string(b), nil +} diff --git a/src/hound/git/git.go b/src/hound/git/git.go new file mode 100644 index 00000000..564721ef --- /dev/null +++ b/src/hound/git/git.go @@ -0,0 +1,80 @@ +package git + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func pull(dir string) error { + cmd := exec.Command("git", "pull") + cmd.Dir = dir + return cmd.Run() +} + +func HeadHash(dir string) (string, error) { + cmd := exec.Command( + "git", + "rev-parse", + "HEAD") + cmd.Dir = dir + r, err := cmd.StdoutPipe() + if err != nil { + return "", err + } + defer r.Close() + + if err := cmd.Start(); err != nil { + return "", err + } + + var buf bytes.Buffer + + if _, err := io.Copy(&buf, r); err != nil { + return "", err + } + + return strings.TrimSpace(buf.String()), cmd.Wait() +} + +func Pull(dir string) (string, error) { + if err := pull(dir); err != nil { + return "", err + } + + return HeadHash(dir) +} + +func Clone(dir, url string) (string, error) { + par, rep := filepath.Split(dir) + cmd := exec.Command( + "git", + "clone", + url, + rep) + cmd.Dir = par + cmd.Stdout = ioutil.Discard + if err := cmd.Run(); err != nil { + return "", err + } + + return HeadHash(dir) +} + +func exists(path string) bool { + if _, err := os.Stat(path); err != nil { + return false + } + return true +} + +func PullOrClone(dir, url string) (string, error) { + if exists(dir) { + return Pull(dir) + } + return Clone(dir, url) +} diff --git a/src/hound/index/grep.go b/src/hound/index/grep.go new file mode 100644 index 00000000..0a0711c6 --- /dev/null +++ b/src/hound/index/grep.go @@ -0,0 +1,251 @@ +package index + +import ( + "bytes" + "code.google.com/p/codesearch/regexp" + "compress/gzip" + "io" + "os" +) + +var nl = []byte{'\n'} + +type grepper struct { + buf []byte +} + +func countLines(b []byte) int { + n := 0 + for { + i := bytes.IndexByte(b, '\n') + if i < 0 { + break + } + n++ + b = b[i+1:] + } + return n +} + +func (g *grepper) grepFile(filename string, re *regexp.Regexp, + fn func(line []byte, lineno int) (bool, error)) error { + r, err := os.Open(filename) + if err != nil { + return err + } + defer r.Close() + + c, err := gzip.NewReader(r) + if err != nil { + return err + } + defer c.Close() + + return g.grep(c, re, fn) +} + +func (g *grepper) grep2File(filename string, re *regexp.Regexp, nctx int, + fn func(line []byte, lineno int, before [][]byte, after [][]byte) (bool, error)) error { + r, err := os.Open(filename) + if err != nil { + return err + } + defer r.Close() + + c, err := gzip.NewReader(r) + if err != nil { + return err + } + defer c.Close() + + return g.grep2(c, re, nctx, fn) +} + +func (g *grepper) fillFrom(r io.Reader) ([]byte, error) { + if g.buf == nil { + g.buf = make([]byte, 1<<20) + } + + off := 0 + for { + n, err := io.ReadFull(r, g.buf[off:]) + if err == io.ErrUnexpectedEOF || err == io.EOF { + return g.buf[:off+n], nil + } else if err != nil { + return nil, err + } + + // grow the storage + buf := make([]byte, len(g.buf)*2) + copy(buf, g.buf) + g.buf = buf + off += n + } +} + +func lastNLines(buf []byte, n int) [][]byte { + if len(buf) == 0 || n == 0 { + return nil + } + + r := make([][]byte, n) + for i := 0; i < n; i++ { + m := bytes.LastIndex(buf, nl) + if m < 0 { + if len(buf) == 0 { + return r[n-i:] + } + r[n-i-1] = buf + return r[n-i-1:] + } + r[n-i-1] = buf[m+1:] + buf = buf[:m] + } + + return r +} + +func firstNLines(buf []byte, n int) [][]byte { + if len(buf) == 0 || n == 0 { + return nil + } + + r := make([][]byte, n) + for i := 0; i < n; i++ { + m := bytes.Index(buf, nl) + if m < 0 { + if len(buf) == 0 { + return r[:i] + } + r[i] = buf + return r[:i+1] + } + r[i] = buf[:m] + buf = buf[m+1:] + } + return r +} + +// TODO(knorton): This is still being tested. This is a grep that supports context lines. Unlike the version +// in codesearch, this one does not operate on chunks. The downside is that we have to have the whole file +// in memory to do the grep. Fortunately, we limit the size of files that get indexed anyway. 10M files tend +// to not be source code. +func (g *grepper) grep2( + r io.Reader, + re *regexp.Regexp, + nctx int, + fn func(line []byte, lineno int, before [][]byte, after [][]byte) (bool, error)) error { + + buf, err := g.fillFrom(r) + if err != nil { + return err + } + + lineno := 0 + for { + if len(buf) == 0 { + return nil + } + + m := re.Match(buf, true, true) + if m < 0 { + return nil + } + + // start of matched line. + str := bytes.LastIndex(buf[:m], nl) + 1 + + //end of previous line + endl := str - 1 + if endl < 0 { + endl = 0 + } + + //end of current line + end := m + 1 + if end > len(buf) { + end = len(buf) + } + + lineno += countLines(buf[:str]) + + more, err := fn( + bytes.TrimRight(buf[str:end], "\n"), + lineno+1, + lastNLines(buf[:endl], nctx), + firstNLines(buf[end:], nctx)) + if err != nil { + return err + } + if !more { + return nil + } + + lineno++ + buf = buf[end:] + } +} + +// This nonsense is adapted from https://code.google.com/p/codesearch/source/browse/regexp/match.go#399 +// and I assume it is a mess to make it faster, but I would like to try a much simpler cleaner version. +func (g *grepper) grep(r io.Reader, re *regexp.Regexp, fn func(line []byte, lineno int) (bool, error)) error { + if g.buf == nil { + g.buf = make([]byte, 1<<20) + } + + var ( + buf = g.buf[:0] + lineno = 1 + beginText = true + endText = false + ) + + for { + n, err := io.ReadFull(r, buf[len(buf):cap(buf)]) + buf = buf[:len(buf)+n] + end := len(buf) + if err == nil { + end = bytes.LastIndex(buf, nl) + 1 + } else { + endText = true + } + chunkStart := 0 + for chunkStart < end { + m1 := re.Match(buf[chunkStart:end], beginText, endText) + chunkStart + beginText = false + if m1 < chunkStart { + break + } + lineStart := bytes.LastIndex(buf[chunkStart:m1], nl) + 1 + chunkStart + lineEnd := m1 + 1 + if lineEnd > end { + lineEnd = end + } + lineno += countLines(buf[chunkStart:lineStart]) + line := buf[lineStart:lineEnd] + more, err := fn(line, lineno) + if err != nil { + return err + } + if !more { + return nil + } + lineno++ + chunkStart = lineEnd + } + if err == nil { + lineno += countLines(buf[chunkStart:end]) + } + + n = copy(buf, buf[end:]) + buf = buf[:n] + if len(buf) == 0 && err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + return err + } + return nil + } + } + + return nil +} diff --git a/src/hound/index/grep_test.go b/src/hound/index/grep_test.go new file mode 100644 index 00000000..659f3853 --- /dev/null +++ b/src/hound/index/grep_test.go @@ -0,0 +1,293 @@ +package index + +import ( + "bytes" + "code.google.com/p/codesearch/regexp" + "fmt" + "strings" + "testing" +) + +var ( + subjA = []byte("first\nsecond\nthird\nfourth\nfifth\nsixth") + subjB = []byte("\n") + subjC = []byte("\n\n\n\nfoo\nbar\n\nbaz") +) + +func formatLines(lines []string) string { + return fmt.Sprintf("[%s]", strings.Join(lines, ",")) +} + +func formatLinesFromBytes(lines [][]byte) string { + n := len(lines) + s := make([]string, n) + for i := 0; i < n; i++ { + s[i] = string(lines[i]) + } + return formatLines(s) +} + +func assertLinesMatch(t *testing.T, lines [][]byte, expected []string) { + if len(lines) != len(expected) { + t.Errorf("lines do not match: %s vs %s", + formatLinesFromBytes(lines), + formatLines(expected)) + return + } + for i, str := range expected { + if str != string(lines[i]) { + t.Errorf("lines do not match: %s vs %s", + formatLinesFromBytes(lines), + formatLines(expected)) + } + } +} + +func GenBuf(n int) []byte { + b := make([]byte, n) + for i := 0; i < n; i++ { + b[i] = byte(i) + } + return b +} + +func TestFillFrom(t *testing.T) { + var g grepper + // this is to force buffer growth + g.buf = make([]byte, 2) + + d := GenBuf(1024) + b, _ := g.fillFrom(bytes.NewBuffer(d)) + if !bytes.Equal(d, b) { + t.Errorf("filled buffer doesn't match original: len=%d & len=%d", len(d), len(b)) + } +} + +func TestFirstNLines(t *testing.T) { + assertLinesMatch(t, firstNLines(subjA, 1), []string{ + "first", + }) + + assertLinesMatch(t, firstNLines(subjA, 2), []string{ + "first", + "second", + }) + + assertLinesMatch(t, firstNLines(subjA, 6), []string{ + "first", + "second", + "third", + "fourth", + "fifth", + "sixth", + }) + + assertLinesMatch(t, firstNLines(subjB, 1), []string{ + "", + }) + + assertLinesMatch(t, firstNLines(subjB, 2), []string{ + "", + }) + + assertLinesMatch(t, firstNLines(subjC, 5), []string{ + "", + "", + "", + "", + "foo", + }) +} + +func TestLastNLines(t *testing.T) { + assertLinesMatch(t, lastNLines(subjA, 1), []string{ + "sixth", + }) + + assertLinesMatch(t, lastNLines(subjA, 2), []string{ + "fifth", + "sixth", + }) + + assertLinesMatch(t, lastNLines(subjA, 6), []string{ + "first", + "second", + "third", + "fourth", + "fifth", + "sixth", + }) + + assertLinesMatch(t, lastNLines(subjB, 1), []string{ + "", + }) + + assertLinesMatch(t, lastNLines(subjB, 2), []string{ + "", + }) + + assertLinesMatch(t, lastNLines(subjC, 5), []string{ + "", + "foo", + "bar", + "", + "baz", + }) +} + +type match struct { + line string + no int +} + +func aMatch(line string, no int) *match { + return &match{ + line: line, + no: no, + } +} + +func formatMatches(matches []*match) string { + str := make([]string, len(matches)) + for i, match := range matches { + str[i] = fmt.Sprintf("%s:%d", match.line, match.no) + } + return strings.Join(str, ",") +} + +func assertMatchesMatch(t *testing.T, a, b []*match) { + if len(a) != len(b) { + t.Errorf("matches no match: %s vs %s", + formatMatches(a), + formatMatches(b)) + return + } + + for i, n := 0, len(a); i < n; i++ { + if a[i].line != b[i].line || a[i].no != b[i].no { + t.Errorf("matches no match: %s vs %s", + formatMatches(a), + formatMatches(b)) + return + } + } +} + +func assertGrepTest(t *testing.T, buf []byte, exp string, expects []*match) { + re, err := regexp.Compile(exp) + if err != nil { + t.Error(err) + return + } + + var g grepper + var m []*match + if err := g.grep2(bytes.NewBuffer(buf), re, 0, + func(line []byte, lineno int, before [][]byte, after [][]byte) (bool, error) { + m = append(m, aMatch(string(line), lineno)) + return true, nil + }); err != nil { + t.Error(err) + return + } + + assertMatchesMatch(t, m, expects) +} + +func TestGrep(t *testing.T) { + assertGrepTest(t, subjA, "s", []*match{ + aMatch("first", 1), + aMatch("second", 2), + aMatch("sixth", 6), + }) + + // BUG(knorton): rsc's regexp has bugs. + // assertGrepTest(t, subjB, "^$", []*match{ + // aMatch("", 1), + // }) + + assertGrepTest(t, subjB, "^", []*match{ + aMatch("", 1), + }) + + assertGrepTest(t, subjC, "^", []*match{ + aMatch("", 1), + aMatch("", 2), + aMatch("", 3), + aMatch("", 4), + aMatch("foo", 5), + aMatch("bar", 6), + aMatch("", 7), + aMatch("baz", 8), + }) + + assertGrepTest(t, subjA, "th$", []*match{ + aMatch("sixth", 6), + }) +} + +func assertContextTest(t *testing.T, buf []byte, exp string, ctx int, expectsBefore [][]string, expectsAfter [][]string) { + re, err := regexp.Compile(exp) + if err != nil { + t.Error(err) + return + } + + var gotBefore [][][]byte + var gotAfter [][][]byte + var g grepper + if err := g.grep2(bytes.NewBuffer(buf), re, ctx, + func(line []byte, lineno int, before [][]byte, after [][]byte) (bool, error) { + gotBefore = append(gotBefore, before) + gotAfter = append(gotAfter, after) + return true, nil + }); err != nil { + t.Error(err) + return + } + + if len(expectsBefore) != len(gotBefore) { + t.Errorf("Before had %d lines, should have had %d", + len(gotBefore), + len(expectsBefore)) + return + } + + if len(expectsAfter) != len(gotAfter) { + t.Errorf("After had %d lines, should have had %d", + len(gotBefore), + len(expectsBefore)) + return + } + + for i, n := 0, len(gotBefore); i < n; i++ { + assertLinesMatch(t, gotBefore[i], expectsBefore[i]) + } + + for i, n := 0, len(gotAfter); i < n; i++ { + assertLinesMatch(t, gotAfter[i], expectsAfter[i]) + } +} + +func TestContext(t *testing.T) { + assertContextTest(t, subjA, "third", 2, + [][]string{ + []string{"first", "second"}, + }, [][]string{ + []string{"fourth", "fifth"}, + }) + + assertContextTest(t, subjA, "third", 3, + [][]string{ + []string{"first", "second"}, + }, [][]string{ + []string{"fourth", "fifth", "sixth"}, + }) + + assertContextTest(t, subjA, "first", 2, + [][]string{ + []string{}, + }, [][]string{ + []string{"second", "third"}, + }) +} diff --git a/src/hound/index/index.go b/src/hound/index/index.go new file mode 100644 index 00000000..12e359c7 --- /dev/null +++ b/src/hound/index/index.go @@ -0,0 +1,326 @@ +package index + +import ( + "code.google.com/p/codesearch/index" + "code.google.com/p/codesearch/regexp" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "time" + "unicode/utf8" +) + +const matchLimit = 5000 + +type Snapshot string + +type Index struct { + dir string + idx *index.Index + lck sync.RWMutex +} + +type SearchOptions struct { + IgnoreCase bool + LinesOfContext uint + FileRegexp string + Offset int + Limit int +} + +type Match struct { + Line string + LineNumber int + Before []string + After []string +} + +type SearchResponse struct { + Matches []*FileMatch + FilesWithMatch int + FilesOpened int `json:"-"` + Duration time.Duration `json:"-"` +} + +type FileMatch struct { + Filename string + Matches []*Match +} + +type ExcludedFile struct { + Filename string + Reason string +} + +func (s Snapshot) Open() (*Index, error) { + return Open(string(s)) +} + +func (n *Index) Destroy() error { + n.lck.Lock() + defer n.lck.Unlock() + if err := n.idx.Close(); err != nil { + return err + } + return os.RemoveAll(n.dir) +} + +func (n *Index) GetDir() string { + return n.dir +} + +func toStrings(lines [][]byte) []string { + strs := make([]string, len(lines)) + for i, n := 0, len(lines); i < n; i++ { + strs[i] = string(lines[i]) + } + return strs +} + +func GetRegexpPattern(pat string, ignoreCase bool) string { + if ignoreCase { + return "(?i)(?m)" + pat + } + return "(?m)" + pat +} + +func (n *Index) Search(pat string, opt *SearchOptions) (*SearchResponse, error) { + startedAt := time.Now() + + n.lck.RLock() + defer n.lck.RUnlock() + + re, err := regexp.Compile(GetRegexpPattern(pat, opt.IgnoreCase)) + if err != nil { + return nil, err + } + + var ( + g grepper + results []*FileMatch + filesOpened int + filesFound int + filesCollected int + matchesCollected int + ) + + var fre *regexp.Regexp + if opt.FileRegexp != "" { + fre, err = regexp.Compile(opt.FileRegexp) + if err != nil { + return nil, err + } + } + + files := n.idx.PostingQuery(index.RegexpQuery(re.Syntax)) + for _, file := range files { + var matches []*Match + name := n.idx.Name(file) + hasMatch := false + + // reject files that do not match the file pattern + if fre != nil && fre.MatchString(name, true, true) < 0 { + continue + } + + filesOpened++ + if err := g.grep2File(filepath.Join(n.dir, "raw", name), re, int(opt.LinesOfContext), + func(line []byte, lineno int, before [][]byte, after [][]byte) (bool, error) { + + hasMatch = true + if filesFound < opt.Offset || (opt.Limit > 0 && filesCollected >= opt.Limit) { + return false, nil + } + + matchesCollected++ + matches = append(matches, &Match{ + Line: string(line), + LineNumber: lineno, + Before: toStrings(before), + After: toStrings(after), + }) + + if matchesCollected > matchLimit { + return false, fmt.Errorf("search exceeds limit on matches: %d", matchLimit) + } + + return true, nil + }); err != nil { + return nil, err + } + + if !hasMatch { + continue + } + + filesFound++ + if len(matches) > 0 { + filesCollected++ + results = append(results, &FileMatch{ + Filename: name, + Matches: matches, + }) + } + } + + return &SearchResponse{ + Matches: results, + FilesWithMatch: filesFound, + FilesOpened: filesOpened, + Duration: time.Now().Sub(startedAt), + }, nil +} + +func isTextFile(filename string) (bool, error) { + buf := make([]byte, 2048) + r, err := os.Open(filename) + if err != nil { + return false, err + } + defer r.Close() + + n, err := io.ReadFull(r, buf) + if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF { + return false, err + } + + buf = buf[:n] + + return utf8.Valid(buf), nil +} + +func addFileToIndex(ix *index.IndexWriter, dst, src, path string) (string, error) { + rel, err := filepath.Rel(src, path) + if err != nil { + return "", err + } + + r, err := os.Open(path) + if err != nil { + return "", err + } + defer r.Close() + + dup := filepath.Join(dst, "raw", rel) + w, err := os.Create(dup) + if err != nil { + return "", err + } + defer w.Close() + + g := gzip.NewWriter(w) + defer g.Close() + + return ix.Add(rel, io.TeeReader(r, g)), nil +} + +func addDirToIndex(dst, src, path string) error { + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + + if rel == "." { + return nil + } + + dup := filepath.Join(dst, "raw", rel) + return os.Mkdir(dup, os.ModePerm) +} + +func indexAllFiles(dst, src string) error { + ix := index.Create(filepath.Join(dst, "tri")) + var excluded []*ExcludedFile + + // Make a file to store the excluded files for this repo + fileHandle, err := os.Create(filepath.Join(dst, "excluded_files.json")) + if err != nil { + return err + } + defer fileHandle.Close() + + if err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + name := info.Name() + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + + if name[0] == '.' { + if info.IsDir() { + return filepath.SkipDir + } + excluded = append(excluded, &ExcludedFile{rel, "Dot file"}) + return nil + } + + if info.IsDir() { + return addDirToIndex(dst, src, path) + } + + if info.Mode()&os.ModeType != 0 { + excluded = append(excluded, &ExcludedFile{rel, "Invalid Mode"}) + return nil + } + + txt, err := isTextFile(path) + if err != nil { + return err + } + + if !txt { + excluded = append(excluded, &ExcludedFile{rel, "Not a text file"}) + return nil + } + + reasonForExclusion, err := addFileToIndex(ix, dst, src, path) + if err != nil { + return err + } + if reasonForExclusion != "" { + excluded = append(excluded, &ExcludedFile{rel, reasonForExclusion}) + } + + return nil + }); err != nil { + return err + } + + if err := json.NewEncoder(fileHandle).Encode(excluded); err != nil { + return err + } + + ix.Flush() + + return nil +} + +func Build(dst, src string) (Snapshot, error) { + if _, err := os.Stat(dst); err != nil { + if err := os.MkdirAll(dst, os.ModePerm); err != nil { + return Snapshot(""), err + } + } + + if err := os.Mkdir(filepath.Join(dst, "raw"), os.ModePerm); err != nil { + return Snapshot(""), err + } + + if err := indexAllFiles(dst, src); err != nil { + return Snapshot(""), err + } + + return Snapshot(dst), nil +} + +// Open an existing snapshot +func Open(dir string) (*Index, error) { + return &Index{ + dir: dir, + idx: index.Open(filepath.Join(dir, "tri")), + }, nil +} diff --git a/src/hound/searcher/searcher.go b/src/hound/searcher/searcher.go new file mode 100644 index 00000000..f87f7b33 --- /dev/null +++ b/src/hound/searcher/searcher.go @@ -0,0 +1,186 @@ +package searcher + +import ( + "fmt" + "hound/config" + "hound/git" + "hound/index" + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" +) + +type Searcher struct { + idx *index.Index + lck sync.RWMutex + Repo *config.Repo +} + +func (s *Searcher) swapIndexes(idx *index.Index) error { + s.lck.Lock() + defer s.lck.Unlock() + + oldIdx := s.idx + s.idx = idx + + return oldIdx.Destroy() +} + +func (s *Searcher) Search(pat string, opt *index.SearchOptions) (*index.SearchResponse, error) { + s.lck.RLock() + defer s.lck.RUnlock() + return s.idx.Search(pat, opt) +} + +func (s *Searcher) GetExcludedFiles() string { + path := filepath.Join(s.idx.GetDir(), "excluded_files.json") + dat, err := ioutil.ReadFile(path) + if err != nil { + log.Printf("Couldn't read excluded_files.json %v\n", err) + } + return string(dat) +} + +func expungeOldIndexes(sha, gitDir string) error { + name := fmt.Sprintf("%s-%s", filepath.Base(gitDir), sha) + + dirs, err := filepath.Glob(fmt.Sprintf("%s-*", gitDir)) + if err != nil { + return err + } + + for _, dir := range dirs { + if strings.HasSuffix(dir, name) { + continue + } + + if err := os.RemoveAll(dir); err != nil { + return err + } + } + + return nil +} + +func buildAndOpenIndex(sha, gitDir string) (*index.Index, error) { + idxDir := fmt.Sprintf("%s-%s", gitDir, sha) + if _, err := os.Stat(idxDir); err != nil { + _, err := index.Build(idxDir, gitDir) + if err != nil { + return nil, err + } + } + + return index.Open(idxDir) +} + +func reportOnMemory() { + var ms runtime.MemStats + + // Print out interesting heap info. + runtime.ReadMemStats(&ms) + fmt.Printf("HeapInUse = %0.2f\n", float64(ms.HeapInuse)/1e6) + fmt.Printf("HeapIdle = %0.2f\n", float64(ms.HeapIdle)/1e6) +} + +// Creates a new Searcher for the gitDir but avoids any remote git operations. +// This requires that an existing gitDir be available in the data directory. This +// is intended for debugging and testing only. This will not start a watcher to +// monitor the remote repo for changes. +func NewFromExisting(gitDir string, repo *config.Repo) (*Searcher, error) { + name := filepath.Base(gitDir) + + log.Printf("Search started for %s", name) + log.Println(" WARNING: index is static and will not update") + + sha, err := git.HeadHash(gitDir) + if err != nil { + return nil, err + } + + idx, err := buildAndOpenIndex(sha, gitDir) + if err != nil { + return nil, err + } + + return &Searcher{ + idx: idx, + Repo: repo, + }, nil +} + +// Creates a new Searcher that is available for searches as soon as this returns. +// This will pull or clone the target repo and start watching the repo for changes. +func New(gitDir string, repo *config.Repo) (*Searcher, error) { + name := filepath.Base(gitDir) + + log.Printf("Searcher started for %s", name) + + sha, err := git.PullOrClone(gitDir, repo.Url) + if err != nil { + return nil, err + } + + if err := expungeOldIndexes(sha, gitDir); err != nil { + return nil, err + } + + idx, err := buildAndOpenIndex(sha, gitDir) + if err != nil { + return nil, err + } + + s := &Searcher{ + idx: idx, + Repo: repo, + } + + go func() { + for { + time.Sleep(time.Duration(repo.MsBetweenPolls) * time.Millisecond) + + newSha, err := git.PullOrClone(gitDir, repo.Url) + if err != nil { + log.Printf("git pull error (%s): %s", name, err) + continue + } + + if newSha == sha { + continue + } + + log.Printf("Rebuilding %s for %s", name, newSha) + idx, err := buildAndOpenIndex(newSha, gitDir) + if err != nil { + log.Printf("failed index build (%s): %s", name, err) + os.RemoveAll(fmt.Sprintf("%s-%s", gitDir, newSha)) + continue + } + + if err := s.swapIndexes(idx); err != nil { + log.Printf("failed index swap (%s): %s", name, err) + if err := idx.Destroy(); err != nil { + log.Printf("failed to destroy index (%s): %s\n", name, err) + } + continue + } + + sha = newSha + + // This is just a good time to GC since we know there will be a + // whole set of dead posting lists on the heap. Ensuring these + // go away quickly helps to prevent the heap from expanding + // uncessarily. + runtime.GC() + + reportOnMemory() + } + }() + + return s, nil +}