Unverified Commit f526b365 authored by Matteo "triex" Suppo's avatar Matteo "triex" Suppo Committed by GitHub

Merge pull request #140 from arduino/config

Change the directory of the configuration files
parents 494bc941 fc648888
......@@ -3,5 +3,5 @@
/main
/.vscode/settings.json
/cmd/formatter/debug.test
/.cli-config.yml
/arduino-cli.yaml
/wiki
......@@ -30,11 +30,11 @@
[[projects]]
branch = "master"
digest = "1:fd2ebfc02b6ad10599b226d2c0265f160e95e7c80e23f01dcf34a8aff0de98c9"
digest = "1:045d5ae3596598b9b9591042561fbcbf2d484d2a744fd007151cab3862a6b8f6"
name = "github.com/arduino/go-paths-helper"
packages = ["."]
pruneopts = "UT"
revision = "751652ddd9f0a98650e681673c2c73937002e889"
revision = "c3c98d1bf2e1069f60ab84bff3a2eb3c5422f3b0"
[[projects]]
branch = "master"
......@@ -266,6 +266,14 @@
revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5"
version = "v1.5.1"
[[projects]]
digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
name = "github.com/sergi/go-diff"
packages = ["diffmatchpatch"]
pruneopts = "UT"
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
version = "v1.0.0"
[[projects]]
digest = "1:9e9193aa51197513b3abcb108970d831fbcf40ef96aa845c4f03276e1fa316d2"
name = "github.com/sirupsen/logrus"
......@@ -441,6 +449,7 @@
"github.com/mitchellh/go-homedir",
"github.com/pkg/errors",
"github.com/pmylund/sortutil",
"github.com/sergi/go-diff/diffmatchpatch",
"github.com/sirupsen/logrus",
"github.com/spf13/cobra",
"github.com/spf13/cobra/doc",
......
......@@ -162,7 +162,7 @@ Great! Now we have the Board FQBN (Fully Qualified Board Name) `arduino:samd:mkr
and the Board Name look good, we are ready to compile and upload the sketch
#### Adding 3rd party cores
To add 3rd party core packages add a link of the additional package to the file `.cli-config.yml`
To add 3rd party core packages add a link of the additional package to the file `arduino-cli.yaml`
If you want to add the ESP8266 core, for example:
......@@ -295,7 +295,7 @@ Flags:
-h, --help help for core
Global Flags:
--config-file string The custom config file (if not specified ./.cli-config.yml will be used). (default "/home/megabug/Workspace/go/src/github.com/arduino/arduino-cli/.cli-config.yml")
--config-file string The custom config file (if not specified the default one will be used). (example "/home/megabug/.config/arduino/arduino-cli/arduino-cli.yaml")
--debug Enables debug output (super verbose, used to debug the CLI).
--format string The output format, can be [text|json]. (default "text")
......
......@@ -74,6 +74,15 @@ func executeWithArgs(t *testing.T, args ...string) (int, []byte) {
// This closure is here because we won't that the defer are executed after the end of the "executeWithArgs" method
func() {
// Create an empty config for the CLI test
conf := paths.New("arduino-cli.yaml")
require.False(t, conf.Exist())
err := conf.WriteFile([]byte("board_manager:\n additional_urls:\n"))
require.NoError(t, err)
defer func() {
require.NoError(t, conf.Remove())
}()
redirect := &stdOutRedirect{}
redirect.Open(t)
defer func() {
......@@ -512,27 +521,33 @@ func TestCompileCommands(t *testing.T) {
require.True(t, paths.New(test1).Join("Test1.arduino.avr.nano.hex").Exist())
// Build sketch with --output path
require.NoError(t, os.Chdir(tmp))
exitCode, d = executeWithArgs(t, "compile", "-b", "arduino:avr:nano", "-o", "test", test1)
require.Zero(t, exitCode, "exit code")
require.Contains(t, string(d), "Sketch uses")
require.True(t, paths.New("test.hex").Exist())
exitCode, d = executeWithArgs(t, "compile", "-b", "arduino:avr:nano", "-o", "test2.hex", test1)
require.Zero(t, exitCode, "exit code")
require.Contains(t, string(d), "Sketch uses")
require.True(t, paths.New("test2.hex").Exist())
require.NoError(t, paths.New(tmp, "anothertest").MkdirAll())
exitCode, d = executeWithArgs(t, "compile", "-b", "arduino:avr:nano", "-o", "anothertest/test", test1)
require.Zero(t, exitCode, "exit code")
require.Contains(t, string(d), "Sketch uses")
require.True(t, paths.New("anothertest", "test.hex").Exist())
exitCode, d = executeWithArgs(t, "compile", "-b", "arduino:avr:nano", "-o", tmp+"/anothertest/test2", test1)
require.Zero(t, exitCode, "exit code")
require.Contains(t, string(d), "Sketch uses")
require.True(t, paths.New("anothertest", "test2.hex").Exist())
{
pwd, err := os.Getwd()
require.NoError(t, err)
defer func() { require.NoError(t, os.Chdir(pwd)) }()
require.NoError(t, os.Chdir(tmp))
exitCode, d = executeWithArgs(t, "compile", "-b", "arduino:avr:nano", "-o", "test", test1)
require.Zero(t, exitCode, "exit code")
require.Contains(t, string(d), "Sketch uses")
require.True(t, paths.New("test.hex").Exist())
exitCode, d = executeWithArgs(t, "compile", "-b", "arduino:avr:nano", "-o", "test2.hex", test1)
require.Zero(t, exitCode, "exit code")
require.Contains(t, string(d), "Sketch uses")
require.True(t, paths.New("test2.hex").Exist())
require.NoError(t, paths.New(tmp, "anothertest").MkdirAll())
exitCode, d = executeWithArgs(t, "compile", "-b", "arduino:avr:nano", "-o", "anothertest/test", test1)
require.Zero(t, exitCode, "exit code")
require.Contains(t, string(d), "Sketch uses")
require.True(t, paths.New("anothertest", "test.hex").Exist())
exitCode, d = executeWithArgs(t, "compile", "-b", "arduino:avr:nano", "-o", tmp+"/anothertest/test2", test1)
require.Zero(t, exitCode, "exit code")
require.Contains(t, string(d), "Sketch uses")
require.True(t, paths.New("anothertest", "test2.hex").Exist())
}
}
func TestInvalidCoreURL(t *testing.T) {
......@@ -544,7 +559,7 @@ func TestInvalidCoreURL(t *testing.T) {
require.NoError(t, err, "making temporary dir")
defer tmp.RemoveAll()
configFile := tmp.Join("cli-config.yml")
configFile := tmp.Join("arduino-cli.yaml")
configFile.WriteFile([]byte(`
board_manager:
additional_urls:
......
......@@ -42,7 +42,7 @@ func initInitCommand() *cobra.Command {
initCommand.Flags().BoolVar(&initFlags._default, "default", false,
"If omitted, ask questions to the user about setting configuration properties, otherwise use default configuration.")
initCommand.Flags().StringVar(&initFlags.location, "save-as", "",
"Sets where to save the configuration file [default is ./.cli-config.yml].")
"Sets where to save the configuration file [default is ./arduino-cli.yaml].")
return initCommand
}
......@@ -65,7 +65,14 @@ func runInitCommand(cmd *cobra.Command, args []string) {
if filepath == "" {
filepath = commands.Config.ConfigFile.String()
}
err := commands.Config.SaveToYAML(filepath)
err := commands.Config.ConfigFile.Parent().MkdirAll()
if err != nil {
formatter.PrintError(err, "Cannot create config file.")
os.Exit(commands.ErrGeneric)
}
err = commands.Config.SaveToYAML(filepath)
if err != nil {
formatter.PrintError(err, "Cannot create config file.")
os.Exit(commands.ErrGeneric)
......
......@@ -18,6 +18,7 @@
package root
import (
"fmt"
"io/ioutil"
"os"
......@@ -25,9 +26,9 @@ import (
"golang.org/x/crypto/ssh/terminal"
"github.com/mattn/go-colorable"
colorable "github.com/mattn/go-colorable"
"github.com/arduino/go-paths-helper"
paths "github.com/arduino/go-paths-helper"
"github.com/arduino/arduino-cli/commands"
"github.com/arduino/arduino-cli/commands/board"
......@@ -56,7 +57,7 @@ func Init() *cobra.Command {
}
command.PersistentFlags().BoolVar(&commands.GlobalFlags.Debug, "debug", false, "Enables debug output (super verbose, used to debug the CLI).")
command.PersistentFlags().StringVar(&commands.GlobalFlags.Format, "format", "text", "The output format, can be [text|json].")
command.PersistentFlags().StringVar(&yamlConfigFile, "config-file", "", "The custom config file (if not specified ./.cli-config.yml will be used).")
command.PersistentFlags().StringVar(&yamlConfigFile, "config-file", "", "The custom config file (if not specified the default will be used).")
command.AddCommand(board.InitCommand())
command.AddCommand(compile.InitCommand())
command.AddCommand(config.InitCommand())
......@@ -115,6 +116,17 @@ func preRun(cmd *cobra.Command, args []string) {
// initConfigs initializes the configuration from the specified file.
func initConfigs() {
// Return error if an old configuration file is found
if old := paths.New(".cli-config.yml"); old.Exist() {
logrus.Errorf("Old configuration file detected: %s.", old)
logrus.Info("The name of this file has been changed to `arduino-cli.yaml`, please rename the file fix it.")
formatter.PrintError(
fmt.Errorf("old configuration file detected: %s", old),
"The name of this file has been changed to `arduino-cli.yaml`, please rename the file fix it.")
os.Exit(commands.ErrGeneric)
}
// Start with default configuration
if conf, err := configs.NewConfiguration(); err != nil {
logrus.WithError(err).Error("Error creating default configuration")
formatter.PrintError(err, "Error creating default configuration")
......@@ -123,14 +135,11 @@ func initConfigs() {
commands.Config = conf
}
if yamlConfigFile != "" {
commands.Config.ConfigFile = paths.New(yamlConfigFile)
// Read configuration from global config file
if commands.Config.ConfigFile.Exist() {
readConfigFrom(commands.Config.ConfigFile)
}
logrus.Info("Initiating configuration")
if err := commands.Config.LoadFromYAML(commands.Config.ConfigFile); err != nil {
logrus.WithError(err).Warn("Did not manage to get config file, using default configuration")
}
if commands.Config.IsBundledInDesktopIDE() {
logrus.Info("CLI is bundled into the IDE")
err := commands.Config.LoadFromDesktopIDEPreferences()
......@@ -140,6 +149,32 @@ func initConfigs() {
} else {
logrus.Info("CLI is not bundled into the IDE")
}
// Read configuration from parent folders (project config)
if pwd, err := paths.Getwd(); err != nil {
logrus.WithError(err).Warn("Did not manage to find current path")
if path := paths.New("arduino-cli.yaml"); path.Exist() {
readConfigFrom(path)
}
} else {
commands.Config.Navigate("/", pwd.String())
}
// Read configuration from environment vars
commands.Config.LoadFromEnv()
// Read configuration from user specified file
if yamlConfigFile != "" {
commands.Config.ConfigFile = paths.New(yamlConfigFile)
readConfigFrom(commands.Config.ConfigFile)
}
logrus.Info("Configuration set")
}
func readConfigFrom(path *paths.Path) {
logrus.Infof("Reading configuration from %s", path)
if err := commands.Config.LoadFromYAML(path); err != nil {
logrus.WithError(err).Warnf("Could not read configuration from %s", path)
}
}
......@@ -19,23 +19,37 @@ package configs
import (
"fmt"
"os"
"os/user"
"runtime"
"github.com/arduino/go-paths-helper"
"github.com/arduino/go-win32-utils"
)
// getDefaultConfigFilePath returns the default path for .cli-config.yml,
// this is the directory where the arduino-cli executable resides.
// getDefaultConfigFilePath returns the default path for arduino-cli.yaml
func getDefaultConfigFilePath() *paths.Path {
executablePath, err := os.Executable()
usr, err := user.Current()
if err != nil {
executablePath = "."
panic(fmt.Errorf("retrieving user home dir: %s", err))
}
return paths.New(executablePath).Parent().Join(".cli-config.yml")
arduinoDataDir := paths.New(usr.HomeDir)
switch runtime.GOOS {
case "linux":
arduinoDataDir = arduinoDataDir.Join(".arduino15")
case "darwin":
arduinoDataDir = arduinoDataDir.Join("Library", "arduino15")
case "windows":
localAppDataPath, err := win32.GetLocalAppDataFolder()
if err != nil {
panic(err)
}
arduinoDataDir = paths.New(localAppDataPath).Join("Arduino15")
default:
panic(fmt.Errorf("unsupported OS: %s", runtime.GOOS))
}
return arduinoDataDir.Join("arduino-cli.yaml")
}
func getDefaultArduinoDataDir() (*paths.Path, error) {
......
/*
* This file is part of arduino-cli.
*
* Copyright 2018 ARDUINO SA (http://www.arduino.cc/)
*
* This software is released under the GNU General Public License version 3,
* which covers the main part of arduino-cli.
* The terms of this license can be found at:
* https://www.gnu.org/licenses/gpl-3.0.en.html
*
* You can be released from the requirements of the above licenses by purchasing
* a commercial license. Buying such a license is mandatory if you want to modify or
* otherwise use the software for commercial activities involving the Arduino
* software without disclosing the source code of your own applications. To purchase
* a commercial license, send an email to license@arduino.cc.
*/
package configs
import (
"path/filepath"
"strings"
paths "github.com/arduino/go-paths-helper"
)
func (c *Configuration) Navigate(root, pwd string) {
relativePath, err := filepath.Rel(root, pwd)
if err != nil {
return
}
// From the root to the current folder, search for arduino-cli.yaml files
parts := strings.Split(relativePath, string(filepath.Separator))
for i := range parts {
path := paths.New(root)
path = path.Join(parts[:i+1]...)
path = path.Join("arduino-cli.yaml")
_ = c.LoadFromYAML(path)
}
}
/*
* This file is part of arduino-cli.
*
* Copyright 2018 ARDUINO SA (http://www.arduino.cc/)
*
* This software is released under the GNU General Public License version 3,
* which covers the main part of arduino-cli.
* The terms of this license can be found at:
* https://www.gnu.org/licenses/gpl-3.0.en.html
*
* You can be released from the requirements of the above licenses by purchasing
* a commercial license. Buying such a license is mandatory if you want to modify or
* otherwise use the software for commercial activities involving the Arduino
* software without disclosing the source code of your own applications. To purchase
* a commercial license, send an email to license@arduino.cc.
*/
package configs_test
import (
"io/ioutil"
"path/filepath"
"strings"
"testing"
"github.com/arduino/arduino-cli/configs"
homedir "github.com/mitchellh/go-homedir"
"github.com/sergi/go-diff/diffmatchpatch"
)
func TestNavigate(t *testing.T) {
tests := []string{
"noconfig",
"local",
"inheritance",
}
for _, tt := range tests {
t.Run(tt, func(t *testing.T) {
root := filepath.Join("testdata", "navigate", tt)
pwd := filepath.Join("testdata", "navigate", tt, "first", "second")
golden := filepath.Join("testdata", "navigate", tt, "golden.yaml")
config, _ := configs.NewConfiguration()
config.Navigate(root, pwd)
data, _ := config.SerializeToYAML()
diff(t, data, golden)
})
}
}
func diff(t *testing.T, data []byte, goldenFile string) {
golden, err := ioutil.ReadFile(goldenFile)
if err != nil {
t.Error(err)
return
}
dataStr := strings.TrimSpace(string(data))
goldenStr := strings.TrimSpace(string(golden))
// Substitute home folder
homedir, _ := homedir.Dir()
dataStr = strings.Replace(dataStr, homedir, "$HOME", -1)
if dataStr != goldenStr {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(goldenStr, dataStr, false)
t.Errorf(dmp.DiffPrettyText(diffs))
}
}
sketchbook_path: /tmp
board_manager:
additional_urls:
- https://downloads.arduino.cc/package_index_mraa.json
\ No newline at end of file
board_manager:
additional_urls:
- https://downloads.arduino.cc/package_index_mraa.json
\ No newline at end of file
proxy_type: auto
sketchbook_path: /tmp
arduino_data: $HOME/.arduino15
board_manager:
additional_urls:
- https://downloads.arduino.cc/package_index_mraa.json
\ No newline at end of file
board_manager:
additional_urls:
- https://downloads.arduino.cc/package_index_mraa.json
\ No newline at end of file
proxy_type: auto
sketchbook_path: $HOME/Arduino
arduino_data: $HOME/.arduino15
board_manager:
additional_urls:
- https://downloads.arduino.cc/package_index_mraa.json
\ No newline at end of file
proxy_type: auto
sketchbook_path: $HOME/Arduino
arduino_data: $HOME/.arduino15
board_manager: {}
\ No newline at end of file
......@@ -23,7 +23,6 @@ import (
"net/url"
paths "github.com/arduino/go-paths-helper"
"github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
......@@ -48,16 +47,13 @@ type yamlProxyConfig struct {
// LoadFromYAML loads the configs from a yaml file.
func (config *Configuration) LoadFromYAML(path *paths.Path) error {
logrus.Info("Unserializing configurations from ", path)
content, err := path.ReadFile()
if err != nil {
logrus.WithError(err).Warn("Error reading config, using default configuration")
return err
}
var ret yamlConfig
err = yaml.Unmarshal(content, &ret)
if err != nil {
logrus.WithError(err).Warn("Error parsing config, using default configuration")
return err
}
......@@ -81,15 +77,18 @@ func (config *Configuration) LoadFromYAML(path *paths.Path) error {
}
}
if ret.BoardsManager != nil {
if len(config.BoardManagerAdditionalUrls) > 1 {
config.BoardManagerAdditionalUrls = config.BoardManagerAdditionalUrls[:1]
}
for _, rawurl := range ret.BoardsManager.AdditionalURLS {
url, err := url.Parse(rawurl)
if err != nil {
logrus.WithError(err).Warn("Error parsing config")
continue
}
config.BoardManagerAdditionalUrls = append(config.BoardManagerAdditionalUrls, url)
}
}
return nil
}
......@@ -113,10 +112,10 @@ func (config *Configuration) SerializeToYAML() ([]byte, error) {
Password: config.ProxyPassword,
}
}
c.BoardsManager = &yamlBoardsManagerConfig{AdditionalURLS: []string{}}
if len(config.BoardManagerAdditionalUrls) > 1 {
c.BoardsManager = &yamlBoardsManagerConfig{AdditionalURLS: []string{}}
for _, URL := range config.BoardManagerAdditionalUrls[1:] {
c.BoardsManager.AdditionalURLS = append(c.BoardsManager.AdditionalURLS, URL.String())
c.BoardsManager.AdditionalURLS = appendIfMissing(c.BoardsManager.AdditionalURLS, URL.String())
}
}
return yaml.Marshal(c)
......@@ -134,3 +133,12 @@ func (config *Configuration) SaveToYAML(path string) error {
}
return nil
}
func appendIfMissing(slice []string, i string) []string {
for _, ele := range slice {
if ele == i {
return slice
}
}
return append(slice, i)
}
......@@ -444,6 +444,25 @@ func (p *Path) EquivalentTo(other *Path) bool {
return p.Clean().path == other.Clean().path
}
// Parents returns all the parents directories of the current path. If the path is absolute
// it starts from the current path to the root, if the path is relative is starts from the
// current path to the current directory.
// The path should be clean for this method to work properly (no .. or . or other shortcuts).
// This function does not performs any check on the returned paths.
func (p *Path) Parents() []*Path {
res := []*Path{}
dir := p
for {
res = append(res, dir)
parent := dir.Parent()
if parent.EquivalentTo(dir) {
break
}
dir = parent
}
return res
}
func (p *Path) String() string {
return p.path
}
# This is the official list of go-diff authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files.
# See the latter for an explanation.
# Names should be added to this file as
# Name or Organization <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
Danny Yoo <dannyyoo@google.com>
James Kolb <jkolb@google.com>
Jonathan Amsterdam <jba@google.com>
Markus Zimmermann <markus.zimmermann@nethead.at> <markus.zimmermann@symflower.com> <zimmski@gmail.com>
Matt Kovars <akaskik@gmail.com>
Örjan Persson <orjan@spotify.com>
Osman Masood <oamasood@gmail.com>
Robert Carlsen <rwcarlsen@gmail.com>
Rory Flynn <roryflynn@users.noreply.github.com>
Sergi Mansilla <sergi.mansilla@gmail.com>
Shatrugna Sadhu <ssadhu@apcera.com>
Shawn Smith <shawnpsmith@gmail.com>
Stas Maksimov <maksimov@gmail.com>
Tor Arvid Lund <torarvid@gmail.com>
Zac Bergquist <zbergquist99@gmail.com>
# This is the official list of people who can contribute
# (and typically have contributed) code to the go-diff
# repository.
#
# The AUTHORS file lists the copyright holders; this file
# lists people. For example, ACME Inc. employees would be listed here
# but not in AUTHORS, because ACME Inc. would hold the copyright.
#
# When adding J Random Contributor's name to this file,
# either J's name or J's organization's name should be
# added to the AUTHORS file.
#
# Names should be added to this file like so:
# Name <email address>
#
# Please keep the list sorted.
Danny Yoo <dannyyoo@google.com>
James Kolb <jkolb@google.com>
Jonathan Amsterdam <jba@google.com>
Markus Zimmermann <markus.zimmermann@nethead.at> <markus.zimmermann@symflower.com> <zimmski@gmail.com>
Matt Kovars <akaskik@gmail.com>
Örjan Persson <orjan@spotify.com>
Osman Masood <oamasood@gmail.com>
Robert Carlsen <rwcarlsen@gmail.com>
Rory Flynn <roryflynn@users.noreply.github.com>
Sergi Mansilla <sergi.mansilla@gmail.com>
Shatrugna Sadhu <ssadhu@apcera.com>
Shawn Smith <shawnpsmith@gmail.com>
Stas Maksimov <maksimov@gmail.com>
Tor Arvid Lund <torarvid@gmail.com>
Zac Bergquist <zbergquist99@gmail.com>
Copyright (c) 2012-2016 The go-diff Authors. All rights reserved.
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.
This diff is collapsed.
// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
// https://github.com/sergi/go-diff
// See the included LICENSE file for license details.
//
// go-diff is a Go implementation of Google's Diff, Match, and Patch library
// Original library is Copyright (c) 2006 Google Inc.
// http://code.google.com/p/google-diff-match-patch/
// Package diffmatchpatch offers robust algorithms to perform the operations required for synchronizing plain text.
package diffmatchpatch
import (
"time"
)
// DiffMatchPatch holds the configuration for diff-match-patch operations.
type DiffMatchPatch struct {
// Number of seconds to map a diff before giving up (0 for infinity).
DiffTimeout time.Duration
// Cost of an empty edit operation in terms of edit characters.
DiffEditCost int
// How far to search for a match (0 = exact location, 1000+ = broad match). A match this many characters away from the expected location will add 1.0 to the score (0.0 is a perfect match).
MatchDistance int
// When deleting a large block of text (over ~64 characters), how close do the contents have to be to match the expected contents. (0.0 = perfection, 1.0 = very loose). Note that MatchThreshold controls how closely the end points of a delete need to match.
PatchDeleteThreshold float64
// Chunk size for context length.
PatchMargin int
// The number of bits in an int.
MatchMaxBits int
// At what point is no match declared (0.0 = perfection, 1.0 = very loose).
MatchThreshold float64
}
// New creates a new DiffMatchPatch object with default parameters.
func New() *DiffMatchPatch {
// Defaults.
return &DiffMatchPatch{
DiffTimeout: time.Second,
DiffEditCost: 4,
MatchThreshold: 0.5,
MatchDistance: 1000,
PatchDeleteThreshold: 0.5,
PatchMargin: 4,
MatchMaxBits: 32,
}
}
// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
// https://github.com/sergi/go-diff
// See the included LICENSE file for license details.
//
// go-diff is a Go implementation of Google's Diff, Match, and Patch library
// Original library is Copyright (c) 2006 Google Inc.
// http://code.google.com/p/google-diff-match-patch/
package diffmatchpatch
import (
"math"
)
// MatchMain locates the best instance of 'pattern' in 'text' near 'loc'.
// Returns -1 if no match found.
func (dmp *DiffMatchPatch) MatchMain(text, pattern string, loc int) int {
// Check for null inputs not needed since null can't be passed in C#.
loc = int(math.Max(0, math.Min(float64(loc), float64(len(text)))))
if text == pattern {
// Shortcut (potentially not guaranteed by the algorithm)
return 0
} else if len(text) == 0 {
// Nothing to match.
return -1
} else if loc+len(pattern) <= len(text) && text[loc:loc+len(pattern)] == pattern {
// Perfect match at the perfect spot! (Includes case of null pattern)
return loc
}
// Do a fuzzy compare.
return dmp.MatchBitap(text, pattern, loc)
}
// MatchBitap locates the best instance of 'pattern' in 'text' near 'loc' using the Bitap algorithm.
// Returns -1 if no match was found.
func (dmp *DiffMatchPatch) MatchBitap(text, pattern string, loc int) int {
// Initialise the alphabet.
s := dmp.MatchAlphabet(pattern)
// Highest score beyond which we give up.
scoreThreshold := dmp.MatchThreshold
// Is there a nearby exact match? (speedup)
bestLoc := indexOf(text, pattern, loc)
if bestLoc != -1 {
scoreThreshold = math.Min(dmp.matchBitapScore(0, bestLoc, loc,
pattern), scoreThreshold)
// What about in the other direction? (speedup)
bestLoc = lastIndexOf(text, pattern, loc+len(pattern))
if bestLoc != -1 {
scoreThreshold = math.Min(dmp.matchBitapScore(0, bestLoc, loc,
pattern), scoreThreshold)
}
}
// Initialise the bit arrays.
matchmask := 1 << uint((len(pattern) - 1))
bestLoc = -1
var binMin, binMid int
binMax := len(pattern) + len(text)
lastRd := []int{}
for d := 0; d < len(pattern); d++ {
// Scan for the best match; each iteration allows for one more error. Run a binary search to determine how far from 'loc' we can stray at this error level.
binMin = 0
binMid = binMax
for binMin < binMid {
if dmp.matchBitapScore(d, loc+binMid, loc, pattern) <= scoreThreshold {
binMin = binMid
} else {
binMax = binMid
}
binMid = (binMax-binMin)/2 + binMin
}
// Use the result from this iteration as the maximum for the next.
binMax = binMid
start := int(math.Max(1, float64(loc-binMid+1)))
finish := int(math.Min(float64(loc+binMid), float64(len(text))) + float64(len(pattern)))
rd := make([]int, finish+2)
rd[finish+1] = (1 << uint(d)) - 1
for j := finish; j >= start; j-- {
var charMatch int
if len(text) <= j-1 {
// Out of range.
charMatch = 0
} else if _, ok := s[text[j-1]]; !ok {
charMatch = 0
} else {
charMatch = s[text[j-1]]
}
if d == 0 {
// First pass: exact match.
rd[j] = ((rd[j+1] << 1) | 1) & charMatch
} else {
// Subsequent passes: fuzzy match.
rd[j] = ((rd[j+1]<<1)|1)&charMatch | (((lastRd[j+1] | lastRd[j]) << 1) | 1) | lastRd[j+1]
}
if (rd[j] & matchmask) != 0 {
score := dmp.matchBitapScore(d, j-1, loc, pattern)
// This match will almost certainly be better than any existing match. But check anyway.
if score <= scoreThreshold {
// Told you so.
scoreThreshold = score
bestLoc = j - 1
if bestLoc > loc {
// When passing loc, don't exceed our current distance from loc.
start = int(math.Max(1, float64(2*loc-bestLoc)))
} else {
// Already passed loc, downhill from here on in.
break
}
}
}
}
if dmp.matchBitapScore(d+1, loc, loc, pattern) > scoreThreshold {
// No hope for a (better) match at greater error levels.
break
}
lastRd = rd
}
return bestLoc
}
// matchBitapScore computes and returns the score for a match with e errors and x location.
func (dmp *DiffMatchPatch) matchBitapScore(e, x, loc int, pattern string) float64 {
accuracy := float64(e) / float64(len(pattern))
proximity := math.Abs(float64(loc - x))
if dmp.MatchDistance == 0 {
// Dodge divide by zero error.
if proximity == 0 {
return accuracy
}
return 1.0
}
return accuracy + (proximity / float64(dmp.MatchDistance))
}
// MatchAlphabet initialises the alphabet for the Bitap algorithm.
func (dmp *DiffMatchPatch) MatchAlphabet(pattern string) map[byte]int {
s := map[byte]int{}
charPattern := []byte(pattern)
for _, c := range charPattern {
_, ok := s[c]
if !ok {
s[c] = 0
}
}
i := 0
for _, c := range charPattern {
value := s[c] | int(uint(1)<<uint((len(pattern)-i-1)))
s[c] = value
i++
}
return s
}
// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
// https://github.com/sergi/go-diff
// See the included LICENSE file for license details.
//
// go-diff is a Go implementation of Google's Diff, Match, and Patch library
// Original library is Copyright (c) 2006 Google Inc.
// http://code.google.com/p/google-diff-match-patch/
package diffmatchpatch
func min(x, y int) int {
if x < y {
return x
}
return y
}
func max(x, y int) int {
if x > y {
return x
}
return y
}
This diff is collapsed.
// Copyright (c) 2012-2016 The go-diff authors. All rights reserved.
// https://github.com/sergi/go-diff
// See the included LICENSE file for license details.
//
// go-diff is a Go implementation of Google's Diff, Match, and Patch library
// Original library is Copyright (c) 2006 Google Inc.
// http://code.google.com/p/google-diff-match-patch/
package diffmatchpatch
import (
"strings"
"unicode/utf8"
)
// unescaper unescapes selected chars for compatibility with JavaScript's encodeURI.
// In speed critical applications this could be dropped since the receiving application will certainly decode these fine. Note that this function is case-sensitive. Thus "%3F" would not be unescaped. But this is ok because it is only called with the output of HttpUtility.UrlEncode which returns lowercase hex. Example: "%3f" -> "?", "%24" -> "$", etc.
var unescaper = strings.NewReplacer(
"%21", "!", "%7E", "~", "%27", "'",
"%28", "(", "%29", ")", "%3B", ";",
"%2F", "/", "%3F", "?", "%3A", ":",
"%40", "@", "%26", "&", "%3D", "=",
"%2B", "+", "%24", "$", "%2C", ",", "%23", "#", "%2A", "*")
// indexOf returns the first index of pattern in str, starting at str[i].
func indexOf(str string, pattern string, i int) int {
if i > len(str)-1 {
return -1
}
if i <= 0 {
return strings.Index(str, pattern)
}
ind := strings.Index(str[i:], pattern)
if ind == -1 {
return -1
}
return ind + i
}
// lastIndexOf returns the last index of pattern in str, starting at str[i].
func lastIndexOf(str string, pattern string, i int) int {
if i < 0 {
return -1
}
if i >= len(str) {
return strings.LastIndex(str, pattern)
}
_, size := utf8.DecodeRuneInString(str[i:])
return strings.LastIndex(str[:i+size], pattern)
}
// runesIndexOf returns the index of pattern in target, starting at target[i].
func runesIndexOf(target, pattern []rune, i int) int {
if i > len(target)-1 {
return -1
}
if i <= 0 {
return runesIndex(target, pattern)
}
ind := runesIndex(target[i:], pattern)
if ind == -1 {
return -1
}
return ind + i
}
func runesEqual(r1, r2 []rune) bool {
if len(r1) != len(r2) {
return false
}
for i, c := range r1 {
if c != r2[i] {
return false
}
}
return true
}
// runesIndex is the equivalent of strings.Index for rune slices.
func runesIndex(r1, r2 []rune) int {
last := len(r1) - len(r2)
for i := 0; i <= last; i++ {
if runesEqual(r1[i:i+len(r2)], r2) {
return i
}
}
return -1
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment