diff --git a/.github/workflows/c-check.yml b/.github/workflows/c-check.yml deleted file mode 100644 index b5e2e55..0000000 --- a/.github/workflows/c-check.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Static C Check - -on: - pull_request: - branches: [main] - -jobs: - c_check: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Cache apt dependencies - uses: actions/cache@v3 - with: - path: | - /var/cache/apt - /var/lib/apt/lists - key: ${{ runner.os }}-apt-${{ hashFiles('**/Makefile') }} - restore-keys: | - ${{ runner.os }}-apt- - - - name: Install tools - run: | - sudo apt-get update - sudo apt-get install -y cppcheck clang clang-tools flawfinder mingw-w64 - - - name: Cppcheck analysis - run: | - cppcheck --enable=all --inconclusive --force --quiet ./ - - - name: Clang static analysis - run: | - scan-build --use-cc=x86_64-w64-mingw32-gcc gcc -c ./*.c - - - name: Security audit with Flawfinder - run: | - flawfinder ./ diff --git a/.github/workflows/go-check.yml b/.github/workflows/go-check.yml new file mode 100644 index 0000000..bc0226e --- /dev/null +++ b/.github/workflows/go-check.yml @@ -0,0 +1,53 @@ +name: Static Go Check + +on: + pull_request: + branches: [main] + +jobs: + go_check: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: '1.23.0' + + - name: Install dependencies + run: | + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + go install golang.org/x/tools/cmd/goimports@latest + + - name: Go mod tidy & download + run: | + go mod tidy + go mod download + + - name: Check formatting (gofmt) + run: | + if [ -n "$(gofmt -l .)" ]; then + echo "Файлы не отформатированы. Запустите 'gofmt -w .'" + gofmt -l . + exit 1 + fi + + - name: Check imports (goimports) + run: | + if [ -n "$(goimports -l .)" ]; then + echo "Неверный импорт. Запустите 'goimports -w .'" + goimports -l . + exit 1 + fi + + - name: Lint (golangci-lint) + run: | + make lint + + - name: Build binary + run: | + mkdir -p bin + make build diff --git a/Makefile b/Makefile index bcc80b9..1a0e2c1 100644 --- a/Makefile +++ b/Makefile @@ -1,41 +1,20 @@ -bindir = bin +.PHONY: fmt lint test -BUILDIN_MAIN = main.c -BUILDIN_COMMIT = commit.c -BUILDIN_DETECT = detect.c -BUILDIN_DIFF = diff.c -BUILDIN_FILE = file.c -BUILDIN_GIT_ROOT = git-root.c -BUILDIN_PARSER = parser.c -BUILDIN_STRINGS = stdlib/strings.c +fmt: + gofmt -w . + goimports -w . -MAIN_OUT = "$(bindir)/auto-commit" - -UNAME_S := $(shell uname -s) - -CC = gcc - -ifeq ($(OS),Windows_NT) - MKDIR = mkdir $(bindir) || echo "Directory already exists" - REMOVE_EXT = mv $(MAIN_OUT).exe $(MAIN_OUT) -else - MKDIR = mkdir -p $(bindir) - OS_TYPE = $(shell uname -s) - REMOVE_EXT = true -endif +lint: + golangci-lint run +check: fmt lint test + @echo "All checks passed!" build: - $(MKDIR) - $(CC) $(BUILDIN_MAIN) -o $(MAIN_OUT) $(BUILDIN_COMMIT) \ - $(BUILDIN_DETECT) $(BUILDIN_DIFF) $(BUILDIN_FILE) \ - $(BUILDIN_GET_STAGED) $(BUILDIN_STRINGS) $(BUILDIN_GIT_ROOT) \ - $(BUILDIN_PARSER) + @echo "Running build..." + @go build -o bin/auto-commit . - $(REMOVE_EXT) +test: + @go test -v ./... -buildt: - $(MKDIR) - $(CC) $(BUILDIN_MAIN) -o $(MAIN_OUT) $(BUILDIN_COMMIT) \ - $(BUILDIN_DETECT) $(BUILDIN_DIFF) $(BUILDIN_FILE) \ - $(BUILDIN_GET_STAGED) $(BUILDIN_STRINGS) $(BUILDIN_GIT_ROOT) \ - $(BUILDIN_PARSER) +run: build + @./bin/auto-commit diff --git a/README.md b/README.md index c878206..35fbd0f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ iex ((New-Object Net.WebClient).DownloadString('https://github.com/thefuture-ind Go to the root of the project and run the command. ```bash -bash <(curl -s https://github.com/thefuture-industries/git-auto-commit/blob/main/scripts/install-linux-auto-commit.sh?raw=true) +curl -fsSL https://github.com/thefuture-industries/git-auto-commit/blob/main/scripts/install-linux-auto-commit.sh?raw=true | bash ``` ## Setting up diff --git a/auto-class.go b/auto-class.go new file mode 100644 index 0000000..c9433f1 --- /dev/null +++ b/auto-class.go @@ -0,0 +1,188 @@ +package main + +import ( + "fmt" + "git-auto-commit/types" + "regexp" + "strings" +) + +func ParseToStructureClass(line, lang string) *types.ClassSignature { + switch lang { + case "typescript", "javascript": + return parseTSJSClass(line) + case "python": + return parsePythonClass(line) + case "cpp": + return parseCppClass(line) + case "csharp": + return parseCSharpClass(line) + case "go": + return parseGoStruct(line) + case "java": + return parseJavaClass(line) + default: + return nil + } +} + +func parseTSJSClass(line string) *types.ClassSignature { + classRegex := regexp.MustCompile(`class\s+(\w+)(?:\s+extends\s+(\w+))?`) + m := classRegex.FindStringSubmatch(line) + + name := m[1] + parent := "" + if len(m) > 2 { + parent = m[2] + } + + methods := parseAccessModifiers(line, "(public|private|protected)\\s+(\\w+)\\s*\\(") + return &types.ClassSignature{Name: name, Parent: parent, Methods: methods} +} + +func parsePythonClass(line string) *types.ClassSignature { + classRegex := regexp.MustCompile(`class\\s+(\\w+)(?:\\((\\w+)\\))?:`) + m := classRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[1] + parent := "" + if len(m) > 2 { + parent = m[2] + } + + methods := make(map[string]string) + methodRegex := regexp.MustCompile(`def\s+(_{0,2}\w+)\s*\(`) + for _, l := range strings.Split(line, "\n") { + mm := methodRegex.FindStringSubmatch(l) + if mm != nil { + mod := "public" + if strings.HasPrefix(mm[1], "__") { + mod = "private" + } else if strings.HasPrefix(mm[1], "_") { + mod = "protected" + } + + methods[mm[1]] = mod + } + } + + return &types.ClassSignature{Name: name, Parent: parent, Methods: methods} +} + +func parseCppClass(line string) *types.ClassSignature { + classRegex := regexp.MustCompile(`class\\s+(\\w+)(?:\\s*:\\s*(public|protected|private)\\s+(\\w+))?`) + m := classRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[1] + parent := "" + if len(m) > 3 { + parent = m[3] + } + + methods := parseAccessModifiers(line, "(public|private|protected):\\s*\\w+\\s+(\\w+)\\s*\\(") + return &types.ClassSignature{Name: name, Parent: parent, Methods: methods} +} + +func parseCSharpClass(line string) *types.ClassSignature { + classRegex := regexp.MustCompile(`(?:public\\s+)?class\\s+(\\w+)(?:\\s*:\\s*(\\w+))?`) + m := classRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[1] + parent := "" + if len(m) > 2 { + parent = m[2] + } + + methods := parseAccessModifiers(line, "(public|private|protected|internal)\\s+\\w+\\s+(\\w+)\\s*\\(") + return &types.ClassSignature{Name: name, Parent: parent, Methods: methods} +} + +func parseGoStruct(line string) *types.ClassSignature { + structRegex := regexp.MustCompile(`type\\s+(\\w+)\\s+struct\\s*{`) + m := structRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[1] + return &types.ClassSignature{Name: name, Parent: "", Methods: make(map[string]string)} +} + +func parseJavaClass(line string) *types.ClassSignature { + classRegex := regexp.MustCompile(`(?:public\\s+)?class\\s+(\\w+)(?:\\s+extends\\s+(\\w+))?`) + m := classRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[1] + parent := "" + if len(m) > 2 { + parent = m[2] + } + + methods := parseAccessModifiers(line, "(public|private|protected)\\s+(\\w+)\\s*\\(") + return &types.ClassSignature{Name: name, Parent: parent, Methods: methods} +} + +func parseAccessModifiers(line, regex string) map[string]string { + methods := make(map[string]string) + methodRegex := regexp.MustCompile(regex) + + for _, l := range strings.Split(line, "\n") { + mm := methodRegex.FindStringSubmatch(l) + if mm != nil { + methods[mm[2]] = mm[1] + } + } + + return methods +} + +func FormattedClass(diff, lang string) string { + var oldClass, newClass *types.ClassSignature + var oldLines, newLines []string + + lines := strings.Split(diff, "\n") + for _, line := range lines { + if strings.HasPrefix(line, "-") { + oldLines = append(oldLines, line[1:]) + } else if strings.HasPrefix(line, "+") { + newLines = append(newLines, line[1:]) + } + } + + oldClass = ParseToStructureClass(strings.Join(oldLines, "\n"), lang) + newClass = ParseToStructureClass(strings.Join(newLines, "\n"), lang) + + if oldClass != nil && newClass == nil { + return fmt.Sprintf("deleted class %s", oldClass.Name) + } + + if oldClass != nil && newClass != nil { + if oldClass.Name != newClass.Name { + return fmt.Sprintf("renamed class %s -> %s", oldClass.Name, newClass.Name) + } + + if oldClass.Parent != newClass.Parent { + return fmt.Sprintf("the heir was changed to %s", oldClass.Name) + } + + for m, oldMod := range oldClass.Methods { + if newMod, ok := newClass.Methods[m]; ok && oldMod != newMod { + return fmt.Sprintf("the access modifier of the %s method has been changed", m) + } + } + } + + return "" +} diff --git a/auto-function.go b/auto-function.go new file mode 100644 index 0000000..37631d4 --- /dev/null +++ b/auto-function.go @@ -0,0 +1,219 @@ +package main + +import ( + "fmt" + "git-auto-commit/types" + "regexp" + "strings" +) + +func ParseToStructureFunction(line, lang string) *types.FunctionSignature { + switch lang { + case "go": + return parseGoFunction(line) + case "python": + return parsePythonFunction(line) + case "typescript", "javascript": + return parseTSJSFunction(line) + case "c", "cpp", "java": + return parseCJavaFunction(line) + case "csharp": + return parseCSharpFunction(line) + default: + return nil + } +} + +func FormattedFunction(diff, lang string) string { + var oldFunc, newFunc *types.FunctionSignature + + lines := strings.Split(diff, "\n") + for i := 0; i < len(lines); i++ { + line := lines[i] + + if strings.HasPrefix(line, "-") { + oldFunc = ParseToStructureFunction(line[1:], lang) + + if i+1 < len(lines) && strings.HasPrefix(lines[i+1], "+") { + newFunc = ParseToStructureFunction(lines[i+1][1:], lang) + i++ + } else { + newFunc = nil + + if oldFunc != nil { + return fmt.Sprintf("deleted function %s", oldFunc.Name) + } + } + } else if strings.HasPrefix(line, "+") { + newFunc = ParseToStructureFunction(line[1:], lang) + + if oldFunc == nil && newFunc != nil { + return fmt.Sprintf("added function %s", newFunc.Name) + } + } else { + oldFunc, newFunc = nil, nil + continue + } + + if oldFunc != nil && newFunc != nil { + if oldFunc.Name != newFunc.Name { + return fmt.Sprintf("renamed function %s -> %s", oldFunc.Name, newFunc.Name) + } + + if len(oldFunc.Params) == len(newFunc.Params) { + for i := range oldFunc.Params { + if oldFunc.Params[i].Name != newFunc.Params[i].Name && oldFunc.Params[i].Type == newFunc.Params[i].Type { + return fmt.Sprintf("changed parameter in %s function", oldFunc.Name) + } + + if oldFunc.Params[i].Name == newFunc.Params[i].Name && oldFunc.Params[i].Type != newFunc.Params[i].Type { + return fmt.Sprintf("changed type '%s %s' -> '%s %s'", oldFunc.Params[i].Name, oldFunc.Params[i].Type, newFunc.Params[i].Name, newFunc.Params[i].Type) + } + } + } + + oldFunc, newFunc = nil, nil + } + } + + return "" +} + +func parseGoFunction(line string) *types.FunctionSignature { + functionRegex := regexp.MustCompile(`func\s+(\w+)\s*\(([^)]*)\)`) + m := functionRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[1] + paramsString := m[2] + params := []types.FunctionParameters{} + + for _, p := range strings.Split(paramsString, ",") { + p = strings.TrimSpace(p) + if p == "" { + continue + } + + parts := strings.Fields(p) + if len(parts) == 2 { + params = append(params, types.FunctionParameters{Name: parts[0], Type: parts[1]}) + } + } + + return &types.FunctionSignature{Name: name, Params: params} +} + +func parsePythonFunction(line string) *types.FunctionSignature { + functionRegex := regexp.MustCompile(`def\s+(\w+)\s*\(([^)]*)\)`) + m := functionRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[1] + params := []types.FunctionParameters{} + + for _, p := range strings.Split(m[2], ",") { + p = strings.TrimSpace(p) + if p == "" { + continue + } + + parts := strings.Split(p, ":") + if len(parts) == 2 { + params = append(params, types.FunctionParameters{Name: strings.TrimSpace(parts[0]), Type: strings.TrimSpace(parts[1])}) + } else { + params = append(params, types.FunctionParameters{Name: p, Type: ""}) + } + } + + return &types.FunctionSignature{Name: name, Params: params} +} + +func parseTSJSFunction(line string) *types.FunctionSignature { + functionRegex := regexp.MustCompile(`function\s+(\w+)\s*\(([^)]*)\)(:\s*(\w+))?`) + m := functionRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[1] + returnType := "" + if len(m) > 4 { + returnType = m[4] + } + + params := []types.FunctionParameters{} + for _, p := range strings.Split(m[2], ",") { + p = strings.TrimSpace(p) + if p == "" { + continue + } + + parts := strings.Split(p, ":") + if len(parts) == 2 { + params = append(params, types.FunctionParameters{Name: strings.TrimSpace(parts[0]), Type: strings.TrimSpace(parts[1])}) + } else { + params = append(params, types.FunctionParameters{Name: p, Type: ""}) + } + } + + return &types.FunctionSignature{Name: name, Params: params, ReturnType: returnType} +} + +func parseCJavaFunction(line string) *types.FunctionSignature { + functionRegex := regexp.MustCompile(`(\w+)\s+(\w+)\s*\(([^)]*)\)`) + m := functionRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + returnType := m[1] + name := m[2] + + params := []types.FunctionParameters{} + for _, p := range strings.Split(m[3], ",") { + p = strings.TrimSpace(p) + if p == "" { + continue + } + + parts := strings.Fields(p) + if len(parts) == 2 { + params = append(params, types.FunctionParameters{Name: parts[1], Type: parts[0]}) + } else if len(parts) == 1 { + params = append(params, types.FunctionParameters{Name: parts[0], Type: ""}) + } + } + + return &types.FunctionSignature{Name: name, Params: params, ReturnType: returnType} +} + +func parseCSharpFunction(line string) *types.FunctionSignature { + functionRegex := regexp.MustCompile(`(public|private|protected|internal)?\s*(static)?\s*(\w+)\s+(\w+)\s*\(([^)]*)\)`) + + m := functionRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + name := m[4] + params := []types.FunctionParameters{} + for _, p := range strings.Split(m[5], ",") { + p = strings.TrimSpace(p) + if p == "" { + continue + } + + parts := strings.Fields(p) + if len(parts) == 2 { + params = append(params, types.FunctionParameters{Name: parts[1], Type: parts[0]}) + } else if len(parts) == 1 { + params = append(params, types.FunctionParameters{Name: parts[0], Type: ""}) + } + } + + return &types.FunctionSignature{Name: name, Params: params, ReturnType: m[3]} +} diff --git a/auto-logic.go b/auto-logic.go new file mode 100644 index 0000000..99fdf1f --- /dev/null +++ b/auto-logic.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + "git-auto-commit/types" + "regexp" + "strings" +) + +func extractSwitchBlocks(lines []string, lang string, isNew bool) []types.SwitchSignature { + var blocks []types.SwitchSignature + var switchRegex, caseRegex *regexp.Regexp + + switch lang { + case "python": + switchRegex = regexp.MustCompile(`match\s+([\w\d_]+)\s*:`) + caseRegex = regexp.MustCompile(`case\s+([^:]+):`) + case "go": + switchRegex = regexp.MustCompile(`switch\s*([\w\d_]+)?\s*{`) + caseRegex = regexp.MustCompile(`case\s+([^:]+):`) + case "c", "cpp", "java", "csharp": + switchRegex = regexp.MustCompile(`switch\s*\(([^)]+)\)`) + caseRegex = regexp.MustCompile(`case\s+([^:]+):`) + case "typescript", "javascript": + switchRegex = regexp.MustCompile(`switch\s*\(([^)]+)\)`) + caseRegex = regexp.MustCompile(`case\s+([^:]+):`) + default: + switchRegex = regexp.MustCompile(`switch`) + caseRegex = regexp.MustCompile(`case`) + } + + for i := 0; i < len(lines); i++ { + line := lines[i] + if (isNew && strings.HasPrefix(line, "+")) || (!isNew && strings.HasPrefix(line, "-")) { + l := line[1:] + if m := switchRegex.FindStringSubmatch(l); m != nil { + expr := "switch" + + if len(m) > 1 && m[1] != "" { + expr = strings.TrimSpace(m[1]) + } + + cases := []string{} + for j := i + 1; j < len(lines); j++ { + cl := lines[j] + if (isNew && strings.HasPrefix(cl, "+")) || (!isNew && strings.HasPrefix(cl, "-")) { + cln := cl[1:] + if caseRegex.MatchString(cln) { + cm := caseRegex.FindStringSubmatch(cln) + if len(cm) > 1 { + cases = append(cases, strings.TrimSpace(cm[1])) + } + } + + if switchRegex.MatchString(cln) { + break + } + } else { + break + } + } + + blocks = append(blocks, types.SwitchSignature{Expr: expr, Cases: cases}) + } + } + } + + return blocks +} + +func FormattedLogic(line, lang string) string { + lines := strings.Split(line, "\n") + oldSwitches := extractSwitchBlocks(lines, lang, false) + newSwitches := extractSwitchBlocks(lines, lang, true) + + if len(oldSwitches) > 0 && len(newSwitches) == 0 { + var parts []string + for _, sw := range oldSwitches { + cases := "" + + if len(sw.Cases) > 0 { + cases = fmt.Sprintf(" (cases: %s)", strings.ReplaceAll(strings.Join(sw.Cases, ", "), "\"", "'")) + } + + parts = append(parts, fmt.Sprintf("%s%s", sw.Expr, cases)) + } + + return fmt.Sprintf("deleted switch: %s", strings.Join(parts, "; ")) + } + + if len(newSwitches) > 0 && len(oldSwitches) == 0 { + var parts []string + for _, sw := range newSwitches { + cases := "" + + if len(sw.Cases) > 0 { + cases = fmt.Sprintf(" (cases: %s)", strings.ReplaceAll(strings.Join(sw.Cases, ", "), "\"", "'")) + } + + parts = append(parts, fmt.Sprintf("%s%s", sw.Expr, cases)) + } + + return fmt.Sprintf("added switch: %s", strings.Join(parts, "; ")) + } + + if len(oldSwitches) > 0 && len(newSwitches) > 0 { + osw := oldSwitches[0] + nsw := newSwitches[0] + if osw.Expr != nsw.Expr || strings.Join(osw.Cases, ",") != strings.Join(nsw.Cases, ",") { + return fmt.Sprintf("changed logic switch '%s (cases: %s)' -> '%s (cases: %s)'", osw.Expr, strings.Join(osw.Cases, ", "), nsw.Expr, strings.ReplaceAll(strings.Join(nsw.Cases, ", "), "\"", "'")) + } + } + + return "" +} diff --git a/auto-remote.go b/auto-remote.go new file mode 100644 index 0000000..b65d923 --- /dev/null +++ b/auto-remote.go @@ -0,0 +1,38 @@ +package main + +import "fmt" + +func FormattedByRemote(token string) (string, error) { + branch, err := GetCurrentBranch() + if err != nil { + return "", err + } + + issue := ExtractIssueNumber(branch) + if issue == "" { + return "", nil + } + + owner, repo, err := GetOwnerRepository() + if err != nil { + return "", err + } + + issueName, issueNumber, err := GetIssueData(owner, repo, issue, token) + if err != nil { + return "", err + } + + commitMsg := fmt.Sprintf("%s (%d)", issueName, issueNumber) + + return commitMsg, nil +} + +func FormattedByBranch() (string, error) { + branch, err := GetCurrentBranch() + if err != nil { + return "", err + } + + return fmt.Sprintf("changed the '%s' branch", branch), err +} diff --git a/auto-structure.go b/auto-structure.go new file mode 100644 index 0000000..269ad27 --- /dev/null +++ b/auto-structure.go @@ -0,0 +1,213 @@ +package main + +import ( + "fmt" + "git-auto-commit/types" + "regexp" + "strings" +) + +func parseStruct(line, lang string) *types.StructureSignature { + var structRegex *regexp.Regexp + + switch lang { + case "go": + structRegex = regexp.MustCompile(`type\s+(\w+)\s+struct\s*{`) + case "csharp": + structRegex = regexp.MustCompile(`struct\s+(\w+)\s*{`) + case "c", "cpp": + structRegex = regexp.MustCompile(`struct\s+(\w+)\s*{`) + case "typescript": + structRegex = regexp.MustCompile(`interface\s+(\w+)\s*{`) + default: + return nil + } + + m := structRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + return &types.StructureSignature{Name: m[1]} +} + +func parseType(line, lang string) *types.TypeSignature { + var typeRegex *regexp.Regexp + switch lang { + case "go": + typeRegex = regexp.MustCompile(`type\s+(\w+)\s+`) + case "typescript": + typeRegex = regexp.MustCompile(`type\s+(\w+)\s*=`) + case "csharp": + typeRegex = regexp.MustCompile(`using\s+(\w+)\s*=`) + default: + return nil + } + + m := typeRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + return &types.TypeSignature{Name: m[1]} +} + +func parseInterface(line, lang string) *types.InterfaceSignature { + var interfaceRegex *regexp.Regexp + switch lang { + case "go": + interfaceRegex = regexp.MustCompile(`type\s+(\w+)\s+interface\s*{`) + case "typescript", "java", "csharp": + interfaceRegex = regexp.MustCompile(`interface\s+(\w+)\s*{`) + default: + return nil + } + + m := interfaceRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + return &types.InterfaceSignature{Name: m[1]} +} + +func parseEnum(line, lang string) *types.EnumSignature { + var enumRegex *regexp.Regexp + switch lang { + case "typescript", "java", "csharp", "cpp", "c": + enumRegex = regexp.MustCompile(`enum\s+(\w+)\s*{`) + default: + return nil + } + + m := enumRegex.FindStringSubmatch(line) + if m == nil { + return nil + } + + return &types.EnumSignature{Name: m[1]} +} + +func FormattedStruct(diff, lang string) string { + var oldStruct, newStruct *types.StructureSignature + var oldLines, newLines []string + + lines := strings.Split(diff, "\n") + for _, line := range lines { + if strings.HasPrefix(line, "-") { + oldLines = append(oldLines, line[1:]) + } else if strings.HasPrefix(line, "+") { + newLines = append(newLines, line[1:]) + } + } + + oldStruct = parseStruct(strings.Join(oldLines, "\n"), lang) + newStruct = parseStruct(strings.Join(newLines, "\n"), lang) + + if oldStruct != nil && newStruct == nil { + return fmt.Sprintf("deleted struct %s", oldStruct.Name) + } + + if oldStruct == nil && newStruct != nil { + return fmt.Sprintf("added struct %s", newStruct.Name) + } + + if oldStruct != nil && newStruct != nil && oldStruct.Name != newStruct.Name { + return fmt.Sprintf("renamed struct %s -> %s", oldStruct.Name, newStruct.Name) + } + + return "" +} + +func FormattedType(diff, lang string) string { + var oldType, newType *types.TypeSignature + var oldLines, newLines []string + + lines := strings.Split(diff, "\n") + for _, line := range lines { + if strings.HasPrefix(line, "-") { + oldLines = append(oldLines, line[1:]) + } else if strings.HasPrefix(line, "+") { + newLines = append(newLines, line[1:]) + } + } + + oldType = parseType(strings.Join(oldLines, "\n"), lang) + newType = parseType(strings.Join(newLines, "\n"), lang) + + if oldType != nil && newType == nil { + return fmt.Sprintf("deleted type %s", oldType.Name) + } + + if oldType == nil && newType != nil { + return fmt.Sprintf("added type %s", newType.Name) + } + + if oldType != nil && newType != nil && oldType.Name != newType.Name { + return fmt.Sprintf("renamed type %s -> %s", oldType.Name, newType.Name) + } + + return "" +} + +func FormattedInterface(diff, lang string) string { + var oldInterface, newInterface *types.InterfaceSignature + var oldLines, newLines []string + + lines := strings.Split(diff, "\n") + for _, line := range lines { + if strings.HasPrefix(line, "-") { + oldLines = append(oldLines, line[1:]) + } else if strings.HasPrefix(line, "+") { + newLines = append(newLines, line[1:]) + } + } + + oldInterface = parseInterface(strings.Join(oldLines, "\n"), lang) + newInterface = parseInterface(strings.Join(newLines, "\n"), lang) + + if oldInterface != nil && newInterface == nil { + return fmt.Sprintf("deleted interface %s", oldInterface.Name) + } + + if oldInterface == nil && newInterface != nil { + return fmt.Sprintf("added interface %s", newInterface.Name) + } + + if oldInterface != nil && newInterface != nil && oldInterface.Name != newInterface.Name { + return fmt.Sprintf("renamed interface %s -> %s", oldInterface.Name, newInterface.Name) + } + + return "" +} + +func FormattedEnum(diff, lang string) string { + var oldEnum, newEnum *types.EnumSignature + var oldLines, newLines []string + + lines := strings.Split(diff, "\n") + for _, line := range lines { + if strings.HasPrefix(line, "-") { + oldLines = append(oldLines, line[1:]) + } else if strings.HasPrefix(line, "+") { + newLines = append(newLines, line[1:]) + } + } + + oldEnum = parseEnum(strings.Join(oldLines, "\n"), lang) + newEnum = parseEnum(strings.Join(newLines, "\n"), lang) + + if oldEnum != nil && newEnum == nil { + return fmt.Sprintf("deleted enum %s", oldEnum.Name) + } + + if oldEnum == nil && newEnum != nil { + return fmt.Sprintf("added enum %s", newEnum.Name) + } + + if oldEnum != nil && newEnum != nil && oldEnum.Name != newEnum.Name { + return fmt.Sprintf("renamed enum %s -> %s", oldEnum.Name, newEnum.Name) + } + + return "" +} diff --git a/auto-variables.go b/auto-variables.go new file mode 100644 index 0000000..87b67f5 --- /dev/null +++ b/auto-variables.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + "git-auto-commit/types" + "regexp" + "strings" +) + +func ParseToStructureVariable(line, lang string) *types.VariableSignature { + switch lang { + case "python": + reg := regexp.MustCompile(`^\s*(\w+)\s*=\s*(.+)`) + m := reg.FindStringSubmatch(line) + if m == nil { + return nil + } + + return &types.VariableSignature{Type: "", Name: m[1], Value: strings.TrimSpace(m[2])} + case "typescript", "javascript": + reg := regexp.MustCompile(`^\s*(let|const|var)\s+(\w+)(\s*:\s*(\w+))?\s*=\s*(.+);?`) + m := reg.FindStringSubmatch(line) + if m == nil { + return nil + } + + typ := "" + + if len(m) > 4 { + typ = m[4] + } + + return &types.VariableSignature{Type: typ, Name: m[2], Value: strings.TrimSpace(m[5])} + case "go": + reg := regexp.MustCompile(`^\s*([\w\s,]+):=\s*(.+)`) + m := reg.FindStringSubmatch(line) + if m != nil { + names := strings.Split(m[1], ",") + value := strings.TrimSpace(m[2]) + return &types.VariableSignature{Type: "", Name: strings.TrimSpace(names[0]), Value: value} + } + + return nil + default: + reg := regexp.MustCompile(`^\s*(\w+)\s+(\w+)\s*=\s*([^;]+);`) + + m := reg.FindStringSubmatch(line) + if m == nil { + return nil + } + + return &types.VariableSignature{Type: m[1], Name: m[2], Value: strings.TrimSpace(m[3])} + } +} + +func FormattedVariables(diff, lang string) string { + var oldVar, newVar *types.VariableSignature + + lines := strings.Split(diff, "\n") + for _, line := range lines { + if strings.HasPrefix(line, "-") { + oldVar = ParseToStructureVariable(line[1:], lang) + } else if strings.HasPrefix(line, "+") { + newVar = ParseToStructureVariable(line[1:], lang) + } + + if oldVar != nil && newVar != nil { + if oldVar.Name == newVar.Name && oldVar.Type != newVar.Type { + return fmt.Sprintf("changed type of variable %s -> %s", oldVar.Type, newVar.Type) + } + + if oldVar.Type == newVar.Type && oldVar.Value == newVar.Value && oldVar.Name != newVar.Name { + return fmt.Sprintf("renamed variable %s -> %s", oldVar.Name, newVar.Name) + } + + if oldVar.Name == newVar.Name && oldVar.Type == newVar.Type && oldVar.Value != newVar.Value { + return fmt.Sprintf("changed value in variable %s", oldVar.Name) + } + + oldVar, oldVar = nil, nil + } + } + + return "" +} diff --git a/bin/auto-commit b/bin/auto-commit index a92a4fb..6d6cddc 100644 Binary files a/bin/auto-commit and b/bin/auto-commit differ diff --git a/commit.c b/commit.c deleted file mode 100644 index d3b5f9e..0000000 --- a/commit.c +++ /dev/null @@ -1,199 +0,0 @@ -#include -#include -#include - -#include "stdlib/strings.h" -#include "strings.h" -#include "define.h" -#include "file.h" - -char* build_commit(char a_funcs[][MAX_FUNC_NAME], int a_funcs_count, char d_funcs[][MAX_FUNC_NAME], int d_funcs_count) { - int add_count, del_count, rn_count, ch_count; - - char** added = ad_f(&add_count); - char** deleted = del_f(&del_count); - char** renamed = rn_f(&rn_count); - char** changed = ch_f(&ch_count); - - char* commit_message = malloc(MAX_LINE_LENGTH * sizeof(char)); - if (!commit_message) return NULL; - - commit_message[0] = '\0'; - - if (a_funcs_count > 0) { - if (a_funcs_count == 1) { - // strcat(commit_message, "| added "); - if (commit_message[0] != '\0') { - strcat(commit_message, " | added "); - } else { - strcat(commit_message, "added "); - } - - strcat(commit_message, a_funcs[0]); - strcat(commit_message, " functionality"); - } else { - char* funcs_ptr[MAX_FUNC_COUNT]; - for (int i = 0; i < a_funcs_count; ++i) { - funcs_ptr[i] = a_funcs[i]; - } - char* funcs_str = join_strings(funcs_ptr, a_funcs_count - 1, ", "); - char* last_func = a_funcs[a_funcs_count - 1]; - - strcat(commit_message, "added "); - strcat(commit_message, funcs_str); - free(funcs_str); - - strcat(commit_message, " and "); - strcat(commit_message, last_func); - strcat(commit_message, " functionality"); - } - } - - if (add_count > 0) { - char* added_str = join_strings(added, add_count, ", "); - if (commit_message[0] != '\0') { - strcat(commit_message, " | including "); - } else { - strcat(commit_message, "including "); - } - - strcat(commit_message, added_str); - - remove_all_spaces(added_str); - free(added_str); - } - - if (del_count > 0) { - char* deleted_str = join_strings(deleted, del_count, ", "); - if (commit_message[0] != '\0') { - strcat(commit_message, " | deleted "); - } else { - strcat(commit_message, "deleted "); - } - - strcat(commit_message, deleted_str); - - remove_all_spaces(deleted_str); - free(deleted_str); - } - - if (rn_count > 0) { - char* renamed_str = join_strings(renamed, rn_count, ", "); - if (commit_message[0] != '\0') { - strcat(commit_message, " | renamed "); - } else { - strcat(commit_message, "renamed "); - } - - strcat(commit_message, renamed_str); - - remove_all_spaces(renamed_str); - free(renamed_str); - } - - if (ch_count > 0) { - char* changed_str = join_strings(changed, ch_count, ", "); - if (commit_message[0] != '\0') { - strcat(commit_message, " | changed "); - } else { - strcat(commit_message, "changed "); - } - - strcat(commit_message, changed_str); - - remove_all_spaces(changed_str); - free(changed_str); - } - - if (commit_message[0] == '\0') { - free(commit_message); - return strdup("auto commit (github@git-auto-commit)"); - } - - if (strlen(commit_message) > COMMIT_LENGTH) { - free(commit_message); - - if (a_funcs_count > 0) { - char* funcs_ptr[MAX_FUNC_COUNT]; - for (int i = 0; i < a_funcs_count; ++i) { - funcs_ptr[i] = a_funcs[i]; - } - char* funcs_str = join_strings(funcs_ptr, a_funcs_count, ", "); - char* short_commit = malloc(strlen("added ") + strlen(funcs_str) + 12); - if (!short_commit) return NULL; - sprintf(short_commit, "added %s functionality", funcs_str); - free(funcs_str); - - remove_all_spaces(short_commit); - return short_commit; - } - if (add_count > 0) { - char* added_str = join_strings(added, add_count, ", "); - char* short_commit = malloc(strlen("including ") + strlen(added_str) + 2); - if (!short_commit) return NULL; - sprintf(short_commit, "including %s", added_str); - free(added_str); - - remove_all_spaces(short_commit); - return short_commit; - } - if (del_count > 0) { - char* del_str = join_strings(deleted, del_count, ", "); - char* short_commit = malloc(strlen("deleted ") + strlen(del_str) + 2); - if (!short_commit) return NULL; - sprintf(short_commit, "deleted %s", del_str); - free(del_str); - - remove_all_spaces(short_commit); - return short_commit; - } - if (rn_count > 0) { - char* renamed_str = join_strings(renamed, rn_count, ", "); - char* short_commit = malloc(strlen("renamed ") + strlen(renamed_str) + 2); - if (!short_commit) return NULL; - sprintf(short_commit, "renamed %s", renamed_str); - free(renamed_str); - - remove_all_spaces(short_commit); - return short_commit; - } - if (ch_count > 0) { - char* changed_str = join_strings(changed, ch_count, ", "); - char* short_commit = malloc(strlen("changed ") + strlen(changed_str) + 2); - if (!short_commit) return NULL; - sprintf(short_commit, "changed %s", changed_str); - free(changed_str); - - remove_all_spaces(short_commit); - return short_commit; - } - } - - if (d_funcs_count > 0) { - char* d_ptrs[MAX_FUNC_COUNT]; - for (int i = 0; i < d_funcs_count; ++i) { - d_ptrs[i] = d_funcs[i]; - } - - char* d_str = join_strings(d_ptrs, d_funcs_count, ", "); - const char* suffix = " | deleted functionality: "; - size_t extra_len = strlen(suffix) + strlen(d_str); - - if (strlen(commit_message) + extra_len < COMMIT_LENGTH) { - strcat(commit_message, suffix); - strcat(commit_message, d_str); - } - - free(d_str); - } - - remove_all_spaces(commit_message); - return commit_message; -} - -int git_commit(const char* message) { - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "git commit -m \"%s\"", message); - - return system(cmd); -} diff --git a/commit.go b/commit.go new file mode 100644 index 0000000..74d3b7b --- /dev/null +++ b/commit.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +func Commit(commitMsg string) error { + GitLogger(fmt.Sprintf("commit is: %s", commitMsg)) + + cmd := exec.Command("git", "commit", "-m", commitMsg) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/commit.h b/commit.h deleted file mode 100644 index 60c623f..0000000 --- a/commit.h +++ /dev/null @@ -1,8 +0,0 @@ -#ifndef COMMIT_H -#define COMMIT_H - -char* build_commit(char a_funcs[][MAX_FUNC_NAME], int a_funcs_count, char d_funcs[][MAX_FUNC_NAME], int d_funcs_count); - -int git_commit(const char* message); - -#endif diff --git a/define.go b/define.go new file mode 100644 index 0000000..9140ad1 --- /dev/null +++ b/define.go @@ -0,0 +1,11 @@ +package main + +const ( + MAX_BUFFER uint32 = 100000 + MAX_FUNC_COUNT uint8 = 128 + MAX_FUNC_NAME uint8 = 255 + MAX_LINE_LENGTH uint16 = 1024 + MAX_STRING_LENGTH uint16 = 1024 + COMMIT_LENGTH uint8 = 255 + MAX_COMMIT_LENGTH uint16 = 300 +) diff --git a/define.h b/define.h deleted file mode 100644 index fdfdc08..0000000 --- a/define.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef DEFINE_H -#define DEFINE_H - -#define MAX_BUFFER 100000 - -#define MAX_FUNC_COUNT 128 -#define MAX_FUNC_NAME 256 - -#define MAX_LINE_LENGTH 1024 -#define COMMIT_LENGTH 255 - -#endif diff --git a/detect.c b/detect.c deleted file mode 100644 index 52df47e..0000000 --- a/detect.c +++ /dev/null @@ -1,24 +0,0 @@ -#include -#include -#include - -#include "define.h" - -const char* detect_language(const char* filename) { - const char* ext = strrchr(filename, '.'); - if (!ext) return ""; - - if (strcmp(ext, ".go") == 0) return "go"; - if (strcmp(ext, ".py") == 0) return "python"; - if (strcmp(ext, ".js") == 0) return "javascript"; - if (strcmp(ext, ".ts") == 0) return "typescript"; - if (strcmp(ext, ".cpp") == 0) return "cpp"; - if (strcmp(ext, ".c") == 0) return "c"; - if (strcmp(ext, ".h") == 0) return "c"; - if (strcmp(ext, ".java") == 0) return "java"; - if (strcmp(ext, ".cs") == 0) return "csharp"; - if (strcmp(ext, ".rs") == 0) return "rust"; - if (strcmp(ext, ".scala") == 0) return "scala"; - - return ""; -} diff --git a/detect.h b/detect.h deleted file mode 100644 index c75abb9..0000000 --- a/detect.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef LANGUAGE_H -#define LANGUAGE_H - -const char* detect_language(const char* filename); - -#endif diff --git a/detected.go b/detected.go new file mode 100644 index 0000000..f21a77a --- /dev/null +++ b/detected.go @@ -0,0 +1,35 @@ +package main + +import ( + "path/filepath" + "strings" +) + +func DetectLanguage(filename string) string { + ext := strings.ToLower(filepath.Ext(filename)) + + switch ext { + case ".go": + return "go" + case ".py": + return "python" + case ".js": + return "javascript" + case ".ts": + return "typescript" + case ".cpp": + return "cpp" + case ".c", ".h": + return "c" + case ".java": + return "java" + case ".cs": + return "csharp" + case ".rs": + return "rust" + case ".scala": + return "scala" + default: + return "" + } +} diff --git a/diff.c b/diff.c deleted file mode 100644 index 69c3372..0000000 --- a/diff.c +++ /dev/null @@ -1,207 +0,0 @@ -#include -#include -#include - -#include "define.h" -#include "git-root.h" - -char* get_diff(const char* file) { - char* git_root = get_git_root(); - - char cmd[512]; - snprintf(cmd, sizeof(cmd), "git diff --cached -- %s/%s", git_root, file); - - FILE* fp = _popen(cmd, "r"); - if (!fp) return NULL; - - char* buffer = malloc(100000); - buffer[0] = '\0'; - - while (fgets(cmd, sizeof(cmd), fp)) { - strcat(buffer, cmd); - } - - _pclose(fp); - return buffer; -} - -void extract_added_functions(const char* diff, const char* lang, char a_funcs[][MAX_FUNC_NAME], int* a_func_count) { - const char* line = diff; - char buffer[1024]; - - while (*line) { - sscanf(line, "%[^\n]\n", buffer); - if (strncmp(buffer, "+", 1) == 0) { - char fname[128]; - - // --- C / C++ --- - if (strcmp(lang, "c") == 0 || strcmp(lang, "cpp") == 0) { - if (strchr(buffer, '(') && strchr(buffer, ')') && strchr(buffer, '{')) { - if (sscanf(buffer, "+%*[^ ] %127[^ (]", fname) == 1) { - strcpy(a_funcs[*a_func_count], fname); - (*a_func_count)++; - } - } - } - - // --- Golang --- - else if (strcmp(lang, "golang") == 0) { - if (strstr(buffer, "+func ")) { - if (sscanf(buffer, "+func %127[^ (]", fname) == 1) { - strcpy(a_funcs[*a_func_count], fname); - (*a_func_count)++; - } - } - } - - // --- Python / Scala --- - else if (strcmp(lang, "python") == 0 || strcmp(lang, "scala") == 0) { - if (strstr(buffer, "+def ")) { - if (sscanf(buffer, "+def %127[^ (]", fname) == 1) { - strcpy(a_funcs[*a_func_count], fname); - (*a_func_count)++; - } - } - } - - // --- Java / C# --- - else if (strcmp(lang, "java") == 0 || strcmp(lang, "csharp") == 0) { - if (strchr(buffer, '(') && strstr(buffer, "+public") || strstr(buffer, "+private") || strstr(buffer, "+protected")) { - if (sscanf(buffer, "+%*s %*s %127[^ (]", fname) == 1) { - strcpy(a_funcs[*a_func_count], fname); - (*a_func_count)++; - } - } - } - - // --- Rust --- - else if (strcmp(lang, "rust") == 0) { - if (strstr(buffer, "+fn ")) { - if (sscanf(buffer, "+fn %127[^ (]", fname) == 1) { - strcpy(a_funcs[*a_func_count], fname); - (*a_func_count)++; - } - } - } - - // --- JavaScript / TypeScript --- - else if (strcmp(lang, "javascript") == 0 || strcmp(lang, "typescript") == 0) { - if (strstr(buffer, "+function ")) { - if (sscanf(buffer, "+function %127[^ (]", fname) == 1) { - strcpy(a_funcs[*a_func_count], fname); - (*a_func_count)++; - } - } else { - if (sscanf(buffer, "+%127[^ =:(]", fname) == 1 && - strchr(buffer, '(') && strchr(buffer, ')')) { - strcpy(a_funcs[*a_func_count], fname); - (*a_func_count)++; - } - } - } - } - - line += strlen(buffer); - while (*line == '\n' || *line == '\r') line++; - } -} - -void extract_deleted_functions(const char* diff, const char* lang, char d_funcs[][MAX_FUNC_NAME], int* d_func_count) { - const char* line = diff; - char buffer[1024]; - - while (*line) { - sscanf(line, "%[^\n]\n", buffer); - if (strncmp(buffer, "-", 1) == 0) { - char fname[128]; - - // --- C / C++ --- - if (strcmp(lang, "c") == 0 || strcmp(lang, "cpp") == 0) { - if (strchr(buffer, '(') && strchr(buffer, ')') && strchr(buffer, '{')) { - if (sscanf(buffer, "+%*[^ ] %127[^ (]", fname) == 1) { - strcpy(d_funcs[*d_func_count], fname); - (*d_func_count)++; - } - } - } - - // --- Golang --- - else if (strcmp(lang, "golang") == 0) { - if (strstr(buffer, "+func ")) { - if (sscanf(buffer, "+func %127[^ (]", fname) == 1) { - strcpy(d_funcs[*d_func_count], fname); - (*d_func_count)++; - } - } - } - - // --- Python / Scala --- - else if (strcmp(lang, "python") == 0 || strcmp(lang, "scala") == 0) { - if (strstr(buffer, "+def ")) { - if (sscanf(buffer, "+def %127[^ (]", fname) == 1) { - strcpy(d_funcs[*d_func_count], fname); - (*d_func_count)++; - } - } - } - - // --- Java / C# --- - else if (strcmp(lang, "java") == 0 || strcmp(lang, "csharp") == 0) { - if (strchr(buffer, '(') && strstr(buffer, "+public") || strstr(buffer, "+private") || strstr(buffer, "+protected")) { - if (sscanf(buffer, "+%*s %*s %127[^ (]", fname) == 1) { - strcpy(d_funcs[*d_func_count], fname); - (*d_func_count)++; - } - } - } - - // --- Rust --- - else if (strcmp(lang, "rust") == 0) { - if (strstr(buffer, "+fn ")) { - if (sscanf(buffer, "+fn %127[^ (]", fname) == 1) { - strcpy(d_funcs[*d_func_count], fname); - (*d_func_count)++; - } - } - } - - // --- JavaScript / TypeScript --- - else if (strcmp(lang, "javascript") == 0 || strcmp(lang, "typescript") == 0) { - if (strstr(buffer, "+function ")) { - if (sscanf(buffer, "+function %127[^ (]", fname) == 1) { - strcpy(d_funcs[*d_func_count], fname); - (*d_func_count)++; - } - } else { - if (sscanf(buffer, "+%127[^ =:(]", fname) == 1 && - strchr(buffer, '(') && strchr(buffer, ')')) { - strcpy(d_funcs[*d_func_count], fname); - (*d_func_count)++; - } - } - } - } - - line += strlen(buffer); - while (*line == '\n' || *line == '\r') line++; - } -} - -char** get_staged_files(int* count) { - FILE* fp = _popen("git diff --cached --name-only", "r"); - if (!fp) return NULL; - - char** files = malloc(128 * sizeof(char*)); - char line[512]; - int i = 0; - - while (fgets(line, sizeof(line), fp)) { - line[strcspn(line, "\r\n")] = 0; - files[i] = strdup(line); - i++; - } - - _pclose(fp); - *count = i; - return files; -} diff --git a/diff.go b/diff.go new file mode 100644 index 0000000..48bad69 --- /dev/null +++ b/diff.go @@ -0,0 +1,54 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "os/exec" +) + +func GetDiff(file string) (string, error) { + root, err := GetGitRoot() + if err != nil { + return "", err + } + + cmd := exec.Command("git", "diff", "--cached", "--", fmt.Sprintf("%s/%s", root, file)) + var out bytes.Buffer + + cmd.Stdout = &out + if err := cmd.Run(); err != nil { + return "", err + } + + return out.String(), nil +} + +func GetStagedFiles() ([]string, error) { + cmd := exec.Command("git", "diff", "--cached", "--name-only") + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + if err := cmd.Start(); err != nil { + return nil, err + } + + var files []string + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + files = append(files, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + if err := cmd.Wait(); err != nil { + return nil, err + } + + return files, nil +} diff --git a/diff.h b/diff.h deleted file mode 100644 index d5e83ec..0000000 --- a/diff.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef DIFF_H -#define DIFF_H - -char* get_diff(const char* file); - -void extract_added_functions(const char* diff, const char* lang, char a_funcs[][MAX_FUNC_NAME], int* a_func_count); - -void extract_deleted_functions(const char* diff, const char* lang, char d_funcs[][MAX_FUNC_NAME], int* d_func_count); - -char** get_staged_files(int* count); - -#endif diff --git a/file.c b/file.c deleted file mode 100644 index 8ffc97f..0000000 --- a/file.c +++ /dev/null @@ -1,165 +0,0 @@ -#include -#include -#include - -#include "define.h" - -char* exec_command(const char* cmd) { - FILE *fp; - char *output = malloc(MAX_LINE_LENGTH); - if (output == NULL) { - perror("Failed to allocate memory"); - return NULL; - } - - fp = popen(cmd, "r"); - if (fp == NULL) { - perror("Failed to run command"); - free(output); - return NULL; - } - - size_t len = 0; - while (fgets(output + len, MAX_LINE_LENGTH - len, fp)) { - len += strlen(output + len); - if (len >= MAX_LINE_LENGTH - 1) { - break; - } - } - fclose(fp); - - return output; -} - -char** ad_f(int* count) { - char* cmd_output = exec_command("git diff --cached --name-status"); - if (cmd_output == NULL) { - *count = 0; - return NULL; - } - - char** add = malloc(MAX_LINE_LENGTH * sizeof(char*)); - if (add == NULL) { - perror("Failed to allocate memory for added files"); - *count = 0; - free(cmd_output); - return NULL; - } - - *count = 0; - char* line = strtok(cmd_output, "\n"); - while (line != NULL) { - if (line[0] == 'A') { - add[*count] = strdup(line + 2); - (*count)++; - } - - line = strtok(NULL, "\n"); - } - - free(cmd_output); - return add; -} - -char** del_f(int* count) { - char* cmd_output = exec_command("git diff --cached --name-status"); - if (cmd_output == NULL) { - *count = 0; - return NULL; - } - - char** del = malloc(MAX_LINE_LENGTH * sizeof(char*)); - if (del == NULL) { - perror("Failed to allocate memory for deleted files"); - *count = 0; - free(cmd_output); - return NULL; - } - - *count = 0; - char* line = strtok(cmd_output, "\n"); - while (line != NULL) { - if (line[0] == 'D') { - del[*count] = strdup(line + 2); - (*count)++; - } - - line = strtok(NULL, "\n"); - } - - free(cmd_output); - return del; -} - -char** rn_f(int* count) { - char* cmd_output = exec_command("git diff --cached --name-status"); - if (cmd_output == NULL) { - *count = 0; - return NULL; - } - - char** rn = malloc(MAX_LINE_LENGTH * sizeof(char*)); - if (rn == NULL) { - perror("Failed to allocate memory for renamed files"); - *count = 0; - free(cmd_output); - return NULL; - } - - *count = 0; - char* line = strtok(cmd_output, "\n"); - while (line != NULL) { - if (line[0] == 'R') { - char oldFile[256], newFile[256]; - sscanf(line, "R %s %s", oldFile, newFile); - - char* rename_msg = malloc(strlen(oldFile) + strlen(newFile) + 5); - sprintf(rename_msg, "%s -> %s", oldFile, newFile); - - rn[*count] = rename_msg; - (*count)++; - } - - line = strtok(NULL, "\n"); - } - - free(cmd_output); - return rn; -} - -char** ch_f(int* count) { - char* cmd_output = exec_command("git status --porcelain"); - if (cmd_output == NULL) { - *count = 0; - return NULL; - } - - char** ch = malloc(MAX_LINE_LENGTH * sizeof(char*)); - if (ch == NULL) { - perror("Failed to allocate memory for changed files"); - *count = 0; - free(cmd_output); - return NULL; - } - - *count = 0; - char* line = strtok(cmd_output, "\n"); - while (line != NULL) { - if (strlen(line) >= 4) { - ch[*count] = strdup(line + 3); - (*count)++; - } - - line = strtok(NULL, "\n"); - } - - free(cmd_output); - return ch; -} - -void free_file_array(char** arr, int count) { - for (int i = 0; i < count; i++) { - free(arr[i]); - } - free(arr); -} diff --git a/file.h b/file.h deleted file mode 100644 index 6ac954b..0000000 --- a/file.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef FILE_H -#define FILE_H - -char** ad_f(int* count); - -char** del_f(int* count); - -char** rn_f(int* count); - -char** ch_f(int* count); - -#endif diff --git a/files.go b/files.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/files.go @@ -0,0 +1 @@ +package main diff --git a/git-root.c b/git-root.c deleted file mode 100644 index aa5dd63..0000000 --- a/git-root.c +++ /dev/null @@ -1,22 +0,0 @@ -#include -#include -#include - -char* get_git_root() { - FILE* fp = _popen("git rev-parse --show-toplevel", "r"); - if (!fp) return NULL; - - char* root = malloc(512); - if (!root) return NULL; - - if (fgets(root, 512, fp) == NULL) { - free(root); - _pclose(fp); - return NULL; - } - - root[strcspn(root, "\n")] = '\0'; - - _pclose(fp); - return root; -} diff --git a/git-root.h b/git-root.h deleted file mode 100644 index 8917d94..0000000 --- a/git-root.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef GIT_ROOT_H -#define GIT_ROOT_H - -char* get_git_root(); - -#endif diff --git a/git.go b/git.go new file mode 100644 index 0000000..ecc4f2b --- /dev/null +++ b/git.go @@ -0,0 +1,30 @@ +package main + +import ( + "bytes" + "os/exec" + "strings" +) + +func GetGitRoot() (string, error) { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + var out bytes.Buffer + cmd.Stdout = &out + + if err := cmd.Run(); err != nil { + return "", err + } + + root := strings.TrimSpace(out.String()) + return root, nil +} + +func GetCurrentBranch() (string, error) { + cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") + out, err := cmd.Output() + if err != nil { + return "", err + } + + return strings.TrimSpace(string(out)), nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..12e1b8b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git-auto-commit + +go 1.23.0 diff --git a/issues.go b/issues.go new file mode 100644 index 0000000..d4a8b16 --- /dev/null +++ b/issues.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "fmt" + "git-auto-commit/types" + "io" + "net/http" + "os/exec" + "regexp" + "strings" +) + +func ExtractIssueNumber(branch string) string { + re := regexp.MustCompile(`(\\d+)`) + match := re.FindStringSubmatch(branch) + if len(match) > 1 { + return match[1] + } + + return "" +} + +func GetOwnerRepository() (string, string, error) { + cmd := exec.Command("git", "remote", "get-url", "origin") + out, err := cmd.Output() + if err != nil { + return "", "", err + } + + url := strings.TrimSpace(string(out)) + regex := regexp.MustCompile(`[:/]([^/:]+)/([^/]+?)(?:\.git)?$`) + + match := regex.FindStringSubmatch(url) + if len(match) == 3 { + return match[1], match[2], nil + } + + return "", "", fmt.Errorf("could not parse owner/repository from remote url: %s", url) +} + +func GetIssueData(owner, repo, issue, token string) (string, uint32, error) { + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues/%s", owner, repo, issue) + req, _ := http.NewRequest("GET", url, nil) + if token != "" { + req.Header.Set("Authorization", "token "+token) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", 0, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var githubIssue types.GithubIssue + if err := json.Unmarshal(body, &githubIssue); err != nil { + return "", 0, err + } + + return githubIssue.Title, githubIssue.Number, nil +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..dfeca72 --- /dev/null +++ b/logger.go @@ -0,0 +1,15 @@ +package main + +import "fmt" + +func InfoLogger(msg string) { + fmt.Printf("[git auto-commit] %s\n", msg) +} + +func GitLogger(msg string) { + fmt.Printf("\033[0;34m[git auto-commit] %s\033[0m\n", msg) +} + +func ErrorLogger(err error) { + fmt.Printf("\033[0;31m[git auto-commit] %s\033[0m\n", err.Error()) +} diff --git a/main.c b/main.c deleted file mode 100644 index a0a7414..0000000 --- a/main.c +++ /dev/null @@ -1,59 +0,0 @@ -#include -#include -#include -#include - -#include "define.h" -#include "detect.h" -#include "commit.h" -#include "parser.h" -#include "diff.h" - -int main() { - int file_count = 0; - char** files = get_staged_files(&file_count); - - if (file_count == 0) { - printf("No files staged for commit.\n"); - return 0; - } - - char a_funcs[MAX_FUNC_COUNT][MAX_FUNC_NAME]; - int a_func_count = 0; - - char d_funcs[MAX_FUNC_COUNT][MAX_FUNC_NAME]; - int d_func_count = 0; - - for (int i = 0; i < file_count; i++) { - const char* lang = detect_language(files[i]); - if (lang == NULL) continue; - - char* diff = get_diff(files[i]); - if (diff != NULL) { - extract_added_functions(diff, lang, a_funcs, &a_func_count); - extract_deleted_functions(diff, lang, d_funcs, &d_func_count); - free(diff); - } - } - - char* p_commit_msg = tb_keywords(a_funcs, file_count); - if (p_commit_msg && strlen(p_commit_msg) > 0) { - printf("[git auto-commit] commit is: %s\n", p_commit_msg); - - int result = git_commit(p_commit_msg); - free(p_commit_msg); - } else { - char* commit_msg = build_commit(a_funcs, a_func_count, d_funcs, d_func_count); - printf("\033[0;34m[git auto-commit] commit is: %s\033[0m\n", commit_msg); - - int result = git_commit(commit_msg); - free(commit_msg); - } - - for (int i = 0; i < file_count; i++) { - free(files[i]); - } - free(files); - - return 0; -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..1d9fd0d --- /dev/null +++ b/main.go @@ -0,0 +1,27 @@ +package main + +import "fmt" + +func main() { + files, err := GetStagedFiles() + if err != nil { + ErrorLogger(fmt.Errorf("error getting staged files: %s", err.Error())) + return + } + + if len(files) == 0 { + InfoLogger("No files staged for commit.") + return + } + + parserMsg, err := Parser(files) + if err != nil { + ErrorLogger(err) + return + } + + if err := Commit(parserMsg); err != nil { + ErrorLogger(fmt.Errorf("error committing: %s", err.Error())) + return + } +} diff --git a/parser.c b/parser.c deleted file mode 100644 index 300a13f..0000000 --- a/parser.c +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include -#include -#include - -#include "define.h" -#include "parser.h" - -char* ct_prepare(const char* str) { - for (size_t i = 0; i < keyword_count; i++) { - if (strstr(str, keywords[i]) != NULL) { - size_t len = snprintf(NULL, 0, "added new %s module in %s", keywords[i], str); - char* result = malloc(len + 1); - - if (result) { - snprintf(result, len + 1, "added new %s module in %s", keywords[i], str); - } - - return result; - } - } - - return NULL; -} - -char* tb_keywords(char funcs[][MAX_FUNC_NAME], size_t func_count) { - size_t total_len = 1; - char* result = malloc(total_len); - if (!result) return NULL; - result[0] = '\0'; - - for (size_t i = 0; i < func_count; i++) { - char* commit_msg = ct_prepare(funcs[i]); - if (commit_msg) { - size_t new_len = total_len + strlen(commit_msg) + 2; - char* temp = realloc(result, new_len); - if (!temp) { - free(result); - free(commit_msg); - return NULL; - } - - result = temp; - if (total_len > 1) { - strcat(result, ", "); - } - - strcat(result, commit_msg); - free(commit_msg); - total_len = new_len - 1; - } - } - - return result; -} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..c7738d0 --- /dev/null +++ b/parser.go @@ -0,0 +1,67 @@ +package main + +import "fmt" + +func appendMsg(commitMsg, addition string) string { + if len(commitMsg) == 0 { + return addition + } + + return fmt.Sprintf("%s | %s", commitMsg, addition) +} + +func Parser(files []string) (string, error) { + var commitMsg string = "" + + for _, file := range files { + if uint16(len(commitMsg)) > MAX_COMMIT_LENGTH { + break + } + + diff, err := GetDiff(file) + if err != nil { + return "", err + } + + lang := DetectLanguage(file) + if lang == "" { + commitMsg = appendMsg(commitMsg, fmt.Sprintf("the '%s' file has been changed", file)) + continue // README.md, etc. + } + + for _, formatted := range []string{ + FormattedVariables(diff, lang), + FormattedFunction(diff, lang), + FormattedClass(diff, lang), + FormattedLogic(diff, lang), + FormattedStruct(diff, lang), + FormattedType(diff, lang), + FormattedInterface(diff, lang), + FormattedEnum(diff, lang), + } { + if formatted != "" { + commitMsg = appendMsg(commitMsg, formatted) + } // else -> continue + } + } + + if len(commitMsg) == 0 { + formattedByRemote, err := FormattedByRemote("") + if err != nil { + return "", err + } + + formattedByBranch, err := FormattedByBranch() + if err != nil { + return "", err + } + + if formattedByRemote != "" { + commitMsg = appendMsg(commitMsg, formattedByRemote) + } else { + commitMsg = appendMsg(commitMsg, formattedByBranch) + } + } + + return commitMsg, nil +} diff --git a/parser.h b/parser.h deleted file mode 100644 index ccbbcf7..0000000 --- a/parser.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef PARSER_H -#define PARSER_H - -static const char* keywords[] = {"test", "tests", "testing", "http", "https", "image", "resource"}; -static const size_t keyword_count = sizeof(keywords) / sizeof(keywords[0]); - -typedef struct { - char type[32]; - char name[64]; -} VarStruct; - -char* ct_prepare(const char* str); - -char* tb_keywords(char funcs[][MAX_FUNC_NAME], size_t func_count); - -#endif diff --git a/stdlib/strings.c b/stdlib/strings.c deleted file mode 100644 index 8a3a751..0000000 --- a/stdlib/strings.c +++ /dev/null @@ -1,45 +0,0 @@ -#include -#include - -char* concat_strings(const char* str1, const char* str2) { - char* result = malloc(strlen(str1) + strlen(str2) + 1); - strcpy(result, str1); - strcat(result, str2); - - return result; -} - -char* join_strings(char* arr[], int len, const char* separator) { - char* result = malloc(1); - result[0] = '\0'; - - for (int i = 0; i < len; i++) { - result = concat_strings(result, arr[i]); - if (i < len - 1) { - result = concat_strings(result, separator); - } - } - - return result; -} - -void remove_all_spaces(char* str) { - char* dst = str; - int in_space = 0; - - while (*str) { - if (*str != ' ') { - *dst++ = *str; - in_space = 0; - } else if (!in_space) { - *dst++ = ' '; - in_space = 1; - } - str++; - } - *dst = '\0'; - - if (*dst == ' ' && dst != str) { - memmove(str, str + 1, strlen(str)); - } -} diff --git a/stdlib/strings.h b/stdlib/strings.h deleted file mode 100644 index ef84dba..0000000 --- a/stdlib/strings.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef STRINGS_H -#define STRINGS_H - -char* concat_strings(const char* str1, const char* str2); - -char* join_strings(char* arr[], int len, const char* separator); - -void remove_all_spaces(char* str); - -#endif diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..18a19a5 --- /dev/null +++ b/types/types.go @@ -0,0 +1,52 @@ +package types + +type VariableSignature struct { + Type string + Name string + Value string +} + +type FunctionSignature struct { + Name string + Params []FunctionParameters + ReturnType string +} + +type FunctionParameters struct { + Name string + Type string +} + +type ClassSignature struct { + Name string + Parent string + Methods map[string]string +} + +type SwitchSignature struct { + Expr string + Cases []string +} + +type StructureSignature struct { + Name string +} + +type TypeSignature struct { + Name string +} + +type EnumSignature struct { + Name string + Values []string +} + +type InterfaceSignature struct { + Name string + Methods []string +} + +type GithubIssue struct { + Title string `json:"title"` + Number uint32 `json:"number"` +}