* Add Linear integration CLI with sync and status commands - Add `bd linear sync` for bidirectional issue sync with Linear - Add `bd linear status` to show configuration and sync state - Stub pull/push functions pending GraphQL client (bd-b6b.2) * Implement Linear GraphQL client with full sync support - Add LinearClient with auth, fetch, create, update methods - Implement pull/push operations with Beads type mapping - Clean up redundant comments and remove unused code * Add configurable data mapping and dependency sync for Linear - Add LinearMappingConfig with configurable priority/state/label/relation maps - Import parent-child and issue relations as Beads dependencies - Support custom workflow states via linear.state_map.* config * Add incremental sync support for Linear integration - Add FetchIssuesSince() method using updatedAt filter in GraphQL - Check linear.last_sync config to enable incremental pulls - Track sync mode (incremental vs full) in LinearPullStats * feat(linear): implement push updates for existing Linear issues Add FetchIssueByIdentifier method to retrieve single issues by identifier (e.g., "TEAM-123") for timestamp comparison during push. Update doPushToLinear to: - Fetch Linear issue to get internal ID and UpdatedAt timestamp - Compare timestamps: only update if local is newer - Build update payload with title, description, priority, and state - Call UpdateIssue for issues where local has newer changes Closes bd-b6b.5 * Implement Linear conflict resolution strategies - Add true conflict detection by fetching Linear timestamps via API - Implement --prefer-linear resolution (re-import from Linear) - Implement timestamp-based resolution (newer wins as default) - Fix linter issues: handle resp.Body.Close() and remove unused error return * Add Linear integration tests and documentation - Add comprehensive unit tests for Linear mapping (priority, state, labels, relations) - Update docs/CONFIG.md with Linear configuration reference - Add examples/linear-workflow guide for bidirectional sync - Remove AI section header comments from tests * Fix Linear GraphQL filter construction and improve test coverage - Refactor filter handling to combine team ID into main filter object - Add test for duplicate issue relation mapping - Add HTTP round-trip helper for testing request payload validation * Refactor Linear queries to use shared constant and add UUID validation - Extract linearIssuesQuery to deduplicate FetchIssues/FetchIssuesSince - Add linearMaxPageSize constant and UUID validation with regex - Expand test coverage for new functionality * Refactor Linear integration into internal/linear package - Extract types, client, and mapping logic from cmd/bd/linear.go - Create internal/linear/ package for better code organization - Update tests to work with new package structure * Add linear teams command to list available teams - Add FetchTeams GraphQL query to Linear client - Refactor config reading to support daemon mode - Add tests for teams listing functionality * Refactor Linear config to use getLinearConfig helper - Consolidate config/env var lookup using getLinearConfig function - Add LINEAR_TEAM_ID environment variable support - Update error messages to include env var configuration options * Add hash ID generation and improve Linear conflict detection - Add configurable hash ID mode for Linear imports (matches bd/Jira) - Improve conflict detection with content hash comparison - Enhance conflict resolution with skip/force update tracking * Fix test for updated doPushToLinear signature - Add missing skipUpdateIDs parameter to test call
86 lines
2.3 KiB
Go
86 lines
2.3 KiB
Go
package idgen
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// base36Alphabet is the character set for base36 encoding (0-9, a-z).
|
|
const base36Alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
|
|
// EncodeBase36 converts a byte slice to a base36 string of specified length.
|
|
// Matches the algorithm used for bd hash IDs.
|
|
func EncodeBase36(data []byte, length int) string {
|
|
// Convert bytes to big integer
|
|
num := new(big.Int).SetBytes(data)
|
|
|
|
// Convert to base36
|
|
var result strings.Builder
|
|
base := big.NewInt(36)
|
|
zero := big.NewInt(0)
|
|
mod := new(big.Int)
|
|
|
|
// Build the string in reverse
|
|
chars := make([]byte, 0, length)
|
|
for num.Cmp(zero) > 0 {
|
|
num.DivMod(num, base, mod)
|
|
chars = append(chars, base36Alphabet[mod.Int64()])
|
|
}
|
|
|
|
// Reverse the string
|
|
for i := len(chars) - 1; i >= 0; i-- {
|
|
result.WriteByte(chars[i])
|
|
}
|
|
|
|
// Pad with zeros if needed
|
|
str := result.String()
|
|
if len(str) < length {
|
|
str = strings.Repeat("0", length-len(str)) + str
|
|
}
|
|
|
|
// Truncate to exact length if needed (keep least significant digits)
|
|
if len(str) > length {
|
|
str = str[len(str)-length:]
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
// GenerateHashID creates a hash-based ID for an issue.
|
|
// Uses base36 encoding (0-9, a-z) for better information density than hex.
|
|
// The length parameter is expected to be 3-8; other values fall back to a 3-char byte width.
|
|
func GenerateHashID(prefix, title, description, creator string, timestamp time.Time, length, nonce int) string {
|
|
// Combine inputs into a stable content string
|
|
// Include nonce to handle hash collisions
|
|
content := fmt.Sprintf("%s|%s|%s|%d|%d", title, description, creator, timestamp.UnixNano(), nonce)
|
|
|
|
// Hash the content
|
|
hash := sha256.Sum256([]byte(content))
|
|
|
|
// Determine how many bytes to use based on desired output length
|
|
var numBytes int
|
|
switch length {
|
|
case 3:
|
|
numBytes = 2 // 2 bytes = 16 bits ≈ 3.09 base36 chars
|
|
case 4:
|
|
numBytes = 3 // 3 bytes = 24 bits ≈ 4.63 base36 chars
|
|
case 5:
|
|
numBytes = 4 // 4 bytes = 32 bits ≈ 6.18 base36 chars
|
|
case 6:
|
|
numBytes = 4 // 4 bytes = 32 bits ≈ 6.18 base36 chars
|
|
case 7:
|
|
numBytes = 5 // 5 bytes = 40 bits ≈ 7.73 base36 chars
|
|
case 8:
|
|
numBytes = 5 // 5 bytes = 40 bits ≈ 7.73 base36 chars
|
|
default:
|
|
numBytes = 3 // default to 3 chars
|
|
}
|
|
|
|
shortHash := EncodeBase36(hash[:numBytes], length)
|
|
|
|
return fmt.Sprintf("%s-%s", prefix, shortHash)
|
|
}
|