├── .gitignore ├── DevTeamActivity.xcodeproj └── project.pbxproj ├── DevTeamActivity ├── BitmapCanvas.swift ├── ChartDay.swift ├── ChartMonth.swift ├── CommitsExtractor.swift ├── X11Colors.swift └── main.swift ├── LICENSE ├── README.md ├── dev_team_activity.png └── swift_repo_preview.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | -------------------------------------------------------------------------------- /DevTeamActivity.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 03464F3D1C87A58A00F6C5F2 /* ChartMonth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03464F3C1C87A58A00F6C5F2 /* ChartMonth.swift */; }; 11 | 03CEB0611C73EE38007519C4 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CEB0601C73EE38007519C4 /* main.swift */; }; 12 | 03CEB06B1C73EE50007519C4 /* CommitsExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CEB0691C73EE50007519C4 /* CommitsExtractor.swift */; }; 13 | 03CEB06C1C73EE50007519C4 /* ChartDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CEB06A1C73EE50007519C4 /* ChartDay.swift */; }; 14 | 03E834FC1C8C802A00FAC6AD /* BitmapCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E834FA1C8C802A00FAC6AD /* BitmapCanvas.swift */; }; 15 | 03E834FD1C8C802A00FAC6AD /* X11Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E834FB1C8C802A00FAC6AD /* X11Colors.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 03CEB05B1C73EE38007519C4 /* CopyFiles */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = /usr/share/man/man1/; 23 | dstSubfolderSpec = 0; 24 | files = ( 25 | ); 26 | runOnlyForDeploymentPostprocessing = 1; 27 | }; 28 | /* End PBXCopyFilesBuildPhase section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 03464F3C1C87A58A00F6C5F2 /* ChartMonth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartMonth.swift; sourceTree = ""; }; 32 | 03CEB05D1C73EE38007519C4 /* DevTeamActivity */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = DevTeamActivity; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 03CEB0601C73EE38007519C4 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 34 | 03CEB0691C73EE50007519C4 /* CommitsExtractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommitsExtractor.swift; sourceTree = ""; }; 35 | 03CEB06A1C73EE50007519C4 /* ChartDay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartDay.swift; sourceTree = ""; }; 36 | 03E834FA1C8C802A00FAC6AD /* BitmapCanvas.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BitmapCanvas.swift; path = DevTeamActivity/BitmapCanvas.swift; sourceTree = ""; }; 37 | 03E834FB1C8C802A00FAC6AD /* X11Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = X11Colors.swift; path = DevTeamActivity/X11Colors.swift; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 03CEB05A1C73EE38007519C4 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 03CEB0541C73EE38007519C4 = { 52 | isa = PBXGroup; 53 | children = ( 54 | 03E834FB1C8C802A00FAC6AD /* X11Colors.swift */, 55 | 03E834FA1C8C802A00FAC6AD /* BitmapCanvas.swift */, 56 | 03CEB05F1C73EE38007519C4 /* DevTeamActivity */, 57 | 03CEB05E1C73EE38007519C4 /* Products */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | 03CEB05E1C73EE38007519C4 /* Products */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 03CEB05D1C73EE38007519C4 /* DevTeamActivity */, 65 | ); 66 | name = Products; 67 | sourceTree = ""; 68 | }; 69 | 03CEB05F1C73EE38007519C4 /* DevTeamActivity */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 03CEB0691C73EE50007519C4 /* CommitsExtractor.swift */, 73 | 03CEB06A1C73EE50007519C4 /* ChartDay.swift */, 74 | 03464F3C1C87A58A00F6C5F2 /* ChartMonth.swift */, 75 | 03CEB0601C73EE38007519C4 /* main.swift */, 76 | ); 77 | path = DevTeamActivity; 78 | sourceTree = ""; 79 | }; 80 | /* End PBXGroup section */ 81 | 82 | /* Begin PBXNativeTarget section */ 83 | 03CEB05C1C73EE38007519C4 /* DevTeamActivity */ = { 84 | isa = PBXNativeTarget; 85 | buildConfigurationList = 03CEB0641C73EE38007519C4 /* Build configuration list for PBXNativeTarget "DevTeamActivity" */; 86 | buildPhases = ( 87 | 03CEB0591C73EE38007519C4 /* Sources */, 88 | 03CEB05A1C73EE38007519C4 /* Frameworks */, 89 | 03CEB05B1C73EE38007519C4 /* CopyFiles */, 90 | ); 91 | buildRules = ( 92 | ); 93 | dependencies = ( 94 | ); 95 | name = DevTeamActivity; 96 | productName = DevTeamActivity; 97 | productReference = 03CEB05D1C73EE38007519C4 /* DevTeamActivity */; 98 | productType = "com.apple.product-type.tool"; 99 | }; 100 | /* End PBXNativeTarget section */ 101 | 102 | /* Begin PBXProject section */ 103 | 03CEB0551C73EE38007519C4 /* Project object */ = { 104 | isa = PBXProject; 105 | attributes = { 106 | LastSwiftUpdateCheck = 0720; 107 | LastUpgradeCheck = 0720; 108 | ORGANIZATIONNAME = "Nicolas Seriot"; 109 | TargetAttributes = { 110 | 03CEB05C1C73EE38007519C4 = { 111 | CreatedOnToolsVersion = 7.2; 112 | LastSwiftMigration = 0800; 113 | }; 114 | }; 115 | }; 116 | buildConfigurationList = 03CEB0581C73EE38007519C4 /* Build configuration list for PBXProject "DevTeamActivity" */; 117 | compatibilityVersion = "Xcode 3.2"; 118 | developmentRegion = English; 119 | hasScannedForEncodings = 0; 120 | knownRegions = ( 121 | en, 122 | ); 123 | mainGroup = 03CEB0541C73EE38007519C4; 124 | productRefGroup = 03CEB05E1C73EE38007519C4 /* Products */; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | 03CEB05C1C73EE38007519C4 /* DevTeamActivity */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXSourcesBuildPhase section */ 134 | 03CEB0591C73EE38007519C4 /* Sources */ = { 135 | isa = PBXSourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | 03CEB06C1C73EE50007519C4 /* ChartDay.swift in Sources */, 139 | 03464F3D1C87A58A00F6C5F2 /* ChartMonth.swift in Sources */, 140 | 03CEB0611C73EE38007519C4 /* main.swift in Sources */, 141 | 03CEB06B1C73EE50007519C4 /* CommitsExtractor.swift in Sources */, 142 | 03E834FD1C8C802A00FAC6AD /* X11Colors.swift in Sources */, 143 | 03E834FC1C8C802A00FAC6AD /* BitmapCanvas.swift in Sources */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | /* End PBXSourcesBuildPhase section */ 148 | 149 | /* Begin XCBuildConfiguration section */ 150 | 03CEB0621C73EE38007519C4 /* Debug */ = { 151 | isa = XCBuildConfiguration; 152 | buildSettings = { 153 | ALWAYS_SEARCH_USER_PATHS = NO; 154 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 155 | CLANG_CXX_LIBRARY = "libc++"; 156 | CLANG_ENABLE_MODULES = YES; 157 | CLANG_ENABLE_OBJC_ARC = YES; 158 | CLANG_WARN_BOOL_CONVERSION = YES; 159 | CLANG_WARN_CONSTANT_CONVERSION = YES; 160 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 161 | CLANG_WARN_EMPTY_BODY = YES; 162 | CLANG_WARN_ENUM_CONVERSION = YES; 163 | CLANG_WARN_INT_CONVERSION = YES; 164 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 165 | CLANG_WARN_UNREACHABLE_CODE = YES; 166 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 167 | CODE_SIGN_IDENTITY = "-"; 168 | COPY_PHASE_STRIP = NO; 169 | DEBUG_INFORMATION_FORMAT = dwarf; 170 | ENABLE_STRICT_OBJC_MSGSEND = YES; 171 | ENABLE_TESTABILITY = YES; 172 | GCC_C_LANGUAGE_STANDARD = gnu99; 173 | GCC_DYNAMIC_NO_PIC = NO; 174 | GCC_NO_COMMON_BLOCKS = YES; 175 | GCC_OPTIMIZATION_LEVEL = 0; 176 | GCC_PREPROCESSOR_DEFINITIONS = ( 177 | "DEBUG=1", 178 | "$(inherited)", 179 | ); 180 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 181 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 182 | GCC_WARN_UNDECLARED_SELECTOR = YES; 183 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 184 | GCC_WARN_UNUSED_FUNCTION = YES; 185 | GCC_WARN_UNUSED_VARIABLE = YES; 186 | MACOSX_DEPLOYMENT_TARGET = 10.11; 187 | MTL_ENABLE_DEBUG_INFO = YES; 188 | ONLY_ACTIVE_ARCH = YES; 189 | SDKROOT = macosx; 190 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 191 | }; 192 | name = Debug; 193 | }; 194 | 03CEB0631C73EE38007519C4 /* Release */ = { 195 | isa = XCBuildConfiguration; 196 | buildSettings = { 197 | ALWAYS_SEARCH_USER_PATHS = NO; 198 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 199 | CLANG_CXX_LIBRARY = "libc++"; 200 | CLANG_ENABLE_MODULES = YES; 201 | CLANG_ENABLE_OBJC_ARC = YES; 202 | CLANG_WARN_BOOL_CONVERSION = YES; 203 | CLANG_WARN_CONSTANT_CONVERSION = YES; 204 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 205 | CLANG_WARN_EMPTY_BODY = YES; 206 | CLANG_WARN_ENUM_CONVERSION = YES; 207 | CLANG_WARN_INT_CONVERSION = YES; 208 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 209 | CLANG_WARN_UNREACHABLE_CODE = YES; 210 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 211 | CODE_SIGN_IDENTITY = "-"; 212 | COPY_PHASE_STRIP = NO; 213 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 214 | ENABLE_NS_ASSERTIONS = NO; 215 | ENABLE_STRICT_OBJC_MSGSEND = YES; 216 | GCC_C_LANGUAGE_STANDARD = gnu99; 217 | GCC_NO_COMMON_BLOCKS = YES; 218 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 219 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 220 | GCC_WARN_UNDECLARED_SELECTOR = YES; 221 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 222 | GCC_WARN_UNUSED_FUNCTION = YES; 223 | GCC_WARN_UNUSED_VARIABLE = YES; 224 | MACOSX_DEPLOYMENT_TARGET = 10.11; 225 | MTL_ENABLE_DEBUG_INFO = NO; 226 | SDKROOT = macosx; 227 | }; 228 | name = Release; 229 | }; 230 | 03CEB0651C73EE38007519C4 /* Debug */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | PRODUCT_NAME = "$(TARGET_NAME)"; 234 | SWIFT_VERSION = 3.0; 235 | }; 236 | name = Debug; 237 | }; 238 | 03CEB0661C73EE38007519C4 /* Release */ = { 239 | isa = XCBuildConfiguration; 240 | buildSettings = { 241 | PRODUCT_NAME = "$(TARGET_NAME)"; 242 | SWIFT_VERSION = 3.0; 243 | }; 244 | name = Release; 245 | }; 246 | /* End XCBuildConfiguration section */ 247 | 248 | /* Begin XCConfigurationList section */ 249 | 03CEB0581C73EE38007519C4 /* Build configuration list for PBXProject "DevTeamActivity" */ = { 250 | isa = XCConfigurationList; 251 | buildConfigurations = ( 252 | 03CEB0621C73EE38007519C4 /* Debug */, 253 | 03CEB0631C73EE38007519C4 /* Release */, 254 | ); 255 | defaultConfigurationIsVisible = 0; 256 | defaultConfigurationName = Release; 257 | }; 258 | 03CEB0641C73EE38007519C4 /* Build configuration list for PBXNativeTarget "DevTeamActivity" */ = { 259 | isa = XCConfigurationList; 260 | buildConfigurations = ( 261 | 03CEB0651C73EE38007519C4 /* Debug */, 262 | 03CEB0661C73EE38007519C4 /* Release */, 263 | ); 264 | defaultConfigurationIsVisible = 0; 265 | defaultConfigurationName = Release; 266 | }; 267 | /* End XCConfigurationList section */ 268 | }; 269 | rootObject = 03CEB0551C73EE38007519C4 /* Project object */; 270 | } 271 | -------------------------------------------------------------------------------- /DevTeamActivity/BitmapCanvas.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // BitmapCanvas 4 | // 5 | // Created by nst on 04/01/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | infix operator * : MultiplicationPrecedence 12 | 13 | func *(left:CGFloat, right:Int) -> CGFloat 14 | { return left * CGFloat(right) } 15 | 16 | func *(left:Int, right:CGFloat) -> CGFloat 17 | { return CGFloat(left) * right } 18 | 19 | func *(left:CGFloat, right:Double) -> CGFloat 20 | { return left * CGFloat(right) } 21 | 22 | func *(left:Double, right:CGFloat) -> CGFloat 23 | { return CGFloat(left) * right } 24 | 25 | infix operator + : AdditionPrecedence 26 | 27 | func +(left:CGFloat, right:Int) -> CGFloat 28 | { return left + CGFloat(right) } 29 | 30 | func +(left:Int, right:CGFloat) -> CGFloat 31 | { return CGFloat(left) + right } 32 | 33 | func +(left:CGFloat, right:Double) -> CGFloat 34 | { return left + CGFloat(right) } 35 | 36 | func +(left:Double, right:CGFloat) -> CGFloat 37 | { return CGFloat(left) + right } 38 | 39 | infix operator - : AdditionPrecedence 40 | 41 | func -(left:CGFloat, right:Int) -> CGFloat 42 | { return left - CGFloat(right) } 43 | 44 | func -(left:Int, right:CGFloat) -> CGFloat 45 | { return CGFloat(left) - right } 46 | 47 | func -(left:CGFloat, right:Double) -> CGFloat 48 | { return left - CGFloat(right) } 49 | 50 | func -(left:Double, right:CGFloat) -> CGFloat 51 | { return CGFloat(left) - right } 52 | 53 | // 54 | 55 | func P(_ x:CGFloat, _ y:CGFloat) -> NSPoint { 56 | return NSMakePoint(x, y) 57 | } 58 | 59 | func P(_ x:Int, _ y:Int) -> NSPoint { 60 | return NSMakePoint(CGFloat(x), CGFloat(y)) 61 | } 62 | 63 | func RandomPoint(maxX:Int, maxY:Int) -> NSPoint { 64 | return P(CGFloat(arc4random_uniform((UInt32(maxX+1)))), CGFloat(arc4random_uniform((UInt32(maxY+1))))) 65 | } 66 | 67 | func R(_ x:CGFloat, _ y:CGFloat, _ w:CGFloat, _ h:CGFloat) -> NSRect { 68 | return NSMakeRect(x, y, w, h) 69 | } 70 | 71 | func R(_ x:Int, _ y:Int, _ w:Int, _ h:Int) -> NSRect { 72 | return NSMakeRect(CGFloat(x), CGFloat(y), CGFloat(w), CGFloat(h)) 73 | } 74 | 75 | class BitmapCanvas { 76 | 77 | let bitmapImageRep : NSBitmapImageRep 78 | let context : NSGraphicsContext 79 | 80 | var cgContext : CGContext { 81 | return context.cgContext 82 | } 83 | 84 | var width : CGFloat { 85 | return bitmapImageRep.size.width 86 | } 87 | 88 | var height : CGFloat { 89 | return bitmapImageRep.size.height 90 | } 91 | 92 | func setAllowsAntialiasing(_ antialiasing : Bool) { 93 | cgContext.setAllowsAntialiasing(antialiasing) 94 | } 95 | 96 | init(_ width:Int, _ height:Int, _ background:ConvertibleToNSColor? = nil) { 97 | 98 | self.bitmapImageRep = NSBitmapImageRep( 99 | bitmapDataPlanes:nil, 100 | pixelsWide:width, 101 | pixelsHigh:height, 102 | bitsPerSample:8, 103 | samplesPerPixel:4, 104 | hasAlpha:true, 105 | isPlanar:false, 106 | colorSpaceName:NSDeviceRGBColorSpace, 107 | bytesPerRow:width*4, 108 | bitsPerPixel:32)! 109 | 110 | self.context = NSGraphicsContext(bitmapImageRep: bitmapImageRep)! 111 | 112 | NSGraphicsContext.setCurrent(context) 113 | 114 | setAllowsAntialiasing(false) 115 | 116 | if let b = background { 117 | 118 | let rect = NSMakeRect(0, 0, CGFloat(width), CGFloat(height)) 119 | 120 | context.saveGraphicsState() 121 | 122 | b.color.setFill() 123 | NSBezierPath.fill(rect) 124 | 125 | context.restoreGraphicsState() 126 | } 127 | 128 | // makes coordinates start upper left 129 | cgContext.translateBy(x: 0, y: CGFloat(height)) 130 | cgContext.scaleBy(x: 1.0, y: -1.0) 131 | } 132 | 133 | fileprivate func _colorIsEqual(_ p:NSPoint, _ pixelBuffer:UnsafePointer, _ rgba:(UInt8,UInt8,UInt8,UInt8)) -> Bool { 134 | 135 | let offset = 4 * ((Int(self.width) * Int(p.y) + Int(p.x))) 136 | 137 | let r = pixelBuffer[offset] 138 | let g = pixelBuffer[offset+1] 139 | let b = pixelBuffer[offset+2] 140 | let a = pixelBuffer[offset+3] 141 | 142 | if r != rgba.0 { return false } 143 | if g != rgba.1 { return false } 144 | if b != rgba.2 { return false } 145 | if a != rgba.3 { return false } 146 | 147 | return true 148 | } 149 | 150 | fileprivate func _color(_ p:NSPoint, pixelBuffer:UnsafePointer) -> NSColor { 151 | 152 | let offset = 4 * ((Int(self.width) * Int(p.y) + Int(p.x))) 153 | 154 | let r = pixelBuffer[offset] 155 | let g = pixelBuffer[offset+1] 156 | let b = pixelBuffer[offset+2] 157 | let a = pixelBuffer[offset+3] 158 | 159 | return NSColor( 160 | calibratedRed: CGFloat(Double(r)/255.0), 161 | green: CGFloat(Double(g)/255.0), 162 | blue: CGFloat(Double(b)/255.0), 163 | alpha: CGFloat(Double(a)/255.0)) 164 | } 165 | 166 | func color(_ p:NSPoint) -> NSColor { 167 | 168 | guard let data = cgContext.data else { assertionFailure(); return NSColor.clear } 169 | 170 | let pixelBuffer = data.assumingMemoryBound(to: UInt8.self) 171 | 172 | return _color(p, pixelBuffer:pixelBuffer) 173 | } 174 | 175 | fileprivate func _setColor(_ p:NSPoint, pixelBuffer:UnsafeMutablePointer, normalizedColor:NSColor) { 176 | let offset = 4 * ((Int(self.width) * Int(p.y) + Int(p.x))) 177 | 178 | pixelBuffer[offset] = UInt8(normalizedColor.redComponent * 255.0) 179 | pixelBuffer[offset+1] = UInt8(normalizedColor.greenComponent * 255.0) 180 | pixelBuffer[offset+2] = UInt8(normalizedColor.blueComponent * 255.0) 181 | pixelBuffer[offset+3] = UInt8(normalizedColor.alphaComponent * 255.0) 182 | } 183 | 184 | func setColor(_ p:NSPoint, color color_:ConvertibleToNSColor) { 185 | 186 | let color = color_.color 187 | 188 | guard let normalizedColor = color.usingColorSpaceName(NSCalibratedRGBColorSpace) else { 189 | print("-- cannot normalize color \(color)") 190 | return 191 | } 192 | 193 | guard let data = cgContext.data else { assertionFailure(); return } 194 | 195 | let pixelBuffer = data.assumingMemoryBound(to: UInt8.self) 196 | 197 | _setColor(p, pixelBuffer:pixelBuffer, normalizedColor:normalizedColor) 198 | } 199 | 200 | subscript(x:Int, y:Int) -> ConvertibleToNSColor { 201 | 202 | get { 203 | let p = P(CGFloat(x),CGFloat(y)) 204 | return color(p) 205 | } 206 | 207 | set { 208 | let p = P(CGFloat(x),CGFloat(y)) 209 | setColor(p, color:newValue) 210 | } 211 | } 212 | 213 | func fill(_ p:NSPoint, color rawNewColor_:ConvertibleToNSColor) { 214 | // floodFillScanlineStack from http://lodev.org/cgtutor/floodfill.html 215 | 216 | let rawNewColor = rawNewColor_.color 217 | 218 | assert(p.x < width, "p.x \(p.x) out of range, must be < \(width)") 219 | assert(p.y < height, "p.y \(p.y) out of range, must be < \(height)") 220 | 221 | guard let data = cgContext.data else { assertionFailure(); return } 222 | 223 | let pixelBuffer = data.assumingMemoryBound(to: UInt8.self) 224 | 225 | guard let newColor = rawNewColor.usingColorSpaceName(NSCalibratedRGBColorSpace) else { 226 | print("-- cannot normalize color \(rawNewColor)") 227 | return 228 | } 229 | 230 | let oldColor = _color(p, pixelBuffer:pixelBuffer) 231 | 232 | if oldColor == newColor { return } 233 | 234 | // store rgba as [UInt8] to speed up comparisons 235 | var r : CGFloat = 0.0 236 | var g : CGFloat = 0.0 237 | var b : CGFloat = 0.0 238 | var a : CGFloat = 0.0 239 | 240 | oldColor.getRed(&r, green: &g, blue: &b, alpha: &a) 241 | 242 | let rgba = (UInt8(r*255),UInt8(g*255),UInt8(b*255),UInt8(a*255)) 243 | 244 | var stack : [NSPoint] = [p] 245 | 246 | while let pp = stack.popLast() { 247 | 248 | var x1 = pp.x 249 | 250 | while(x1 >= 0 && _color(P(x1, pp.y), pixelBuffer:pixelBuffer) == oldColor) { 251 | x1 -= 1 252 | } 253 | 254 | x1 += 1 255 | 256 | var spanAbove = false 257 | var spanBelow = false 258 | 259 | while(x1 < width && _colorIsEqual(P(x1, pp.y), pixelBuffer, rgba )) { 260 | 261 | _setColor(P(x1, pp.y), pixelBuffer:pixelBuffer, normalizedColor:newColor) 262 | 263 | let north = P(x1, pp.y-1) 264 | let south = P(x1, pp.y+1) 265 | 266 | if spanAbove == false && pp.y > 0 && _colorIsEqual(north, pixelBuffer, rgba) { 267 | stack.append(north) 268 | spanAbove = true 269 | } else if spanAbove && pp.y > 0 && !_colorIsEqual(north, pixelBuffer, rgba) { 270 | spanAbove = false 271 | } else if spanBelow == false && pp.y < height - 1 && _colorIsEqual(south, pixelBuffer, rgba) { 272 | stack.append(south) 273 | spanBelow = true 274 | } else if spanBelow && pp.y < height - 1 && !_colorIsEqual(south, pixelBuffer, rgba) { 275 | spanBelow = false 276 | } 277 | 278 | x1 += 1 279 | } 280 | } 281 | } 282 | 283 | func line(_ p1:NSPoint, _ p2:NSPoint, _ color_:ConvertibleToNSColor? = NSColor.black) { 284 | 285 | let color = color_?.color 286 | 287 | context.saveGraphicsState() 288 | 289 | // align to the pixel grid 290 | cgContext.translateBy(x: 0.5, y: 0.5) 291 | 292 | if let existingColor = color { 293 | cgContext.setStrokeColor(existingColor.cgColor); 294 | } 295 | 296 | cgContext.setLineCap(.square) 297 | cgContext.move(to: CGPoint(x: p1.x, y: p1.y)) 298 | cgContext.addLine(to: CGPoint(x: p2.x, y: p2.y)) 299 | cgContext.strokePath() 300 | 301 | context.restoreGraphicsState() 302 | } 303 | 304 | func line(_ p1:NSPoint, length:CGFloat = 1.0, degreesCW:CGFloat = 0.0, _ color_:ConvertibleToNSColor? = NSColor.black) -> NSPoint { 305 | let color = color_?.color 306 | let radians = degreesToRadians(degreesCW) 307 | let p2 = P(p1.x + sin(radians) * length, p1.y - cos(radians) * length) 308 | self.line(p1, p2, color) 309 | return p2 310 | } 311 | 312 | func lineVertical(_ p1:NSPoint, height:CGFloat, _ color_:ConvertibleToNSColor? = nil) { 313 | let color = color_?.color 314 | let p2 = P(p1.x, p1.y + height - 1) 315 | self.line(p1, p2, color) 316 | } 317 | 318 | func lineHorizontal(_ p1:NSPoint, width:CGFloat, _ color_:ConvertibleToNSColor? = nil) { 319 | let color = color_?.color 320 | let p2 = P(p1.x + width - 1, p1.y) 321 | self.line(p1, p2, color) 322 | } 323 | 324 | func line(_ p1:NSPoint, deltaX:CGFloat, deltaY:CGFloat, _ color_:ConvertibleToNSColor? = nil) { 325 | let color = color_?.color 326 | let p2 = P(p1.x + deltaX, p1.y + deltaY) 327 | self.line(p1, p2, color) 328 | } 329 | 330 | func rectangle(_ rect:NSRect, stroke stroke_:ConvertibleToNSColor? = NSColor.black, fill fill_:ConvertibleToNSColor? = nil) { 331 | 332 | let stroke = stroke_?.color 333 | let fill = fill_?.color 334 | 335 | context.saveGraphicsState() 336 | 337 | // align to the pixel grid 338 | cgContext.translateBy(x: 0.5, y: 0.5) 339 | 340 | if let existingFillColor = fill { 341 | existingFillColor.setFill() 342 | NSBezierPath.fill(rect) 343 | } 344 | 345 | if let existingStrokeColor = stroke { 346 | existingStrokeColor.setStroke() 347 | NSBezierPath.stroke(rect) 348 | } 349 | 350 | context.restoreGraphicsState() 351 | } 352 | 353 | func polygon(_ points:[NSPoint], stroke stroke_:ConvertibleToNSColor? = NSColor.black, lineWidth:CGFloat=1.0, fill fill_:ConvertibleToNSColor? = nil) { 354 | 355 | guard points.count >= 3 else { 356 | assertionFailure("at least 3 points are needed") 357 | return 358 | } 359 | 360 | context.saveGraphicsState() 361 | 362 | let path = NSBezierPath() 363 | 364 | path.move(to: points[0]) 365 | 366 | for i in 1...points.count-1 { 367 | path.line(to: points[i]) 368 | } 369 | 370 | if let existingFillColor = fill_?.color { 371 | existingFillColor.setFill() 372 | path.fill() 373 | } 374 | 375 | path.close() 376 | 377 | if let existingStrokeColor = stroke_?.color { 378 | existingStrokeColor.setStroke() 379 | path.lineWidth = lineWidth 380 | path.stroke() 381 | } 382 | 383 | context.restoreGraphicsState() 384 | } 385 | 386 | func ellipse(_ rect:NSRect, stroke stroke_:ConvertibleToNSColor? = NSColor.black, fill fill_:ConvertibleToNSColor? = nil) { 387 | 388 | let strokeColor = stroke_?.color 389 | let fillColor = fill_?.color 390 | 391 | context.saveGraphicsState() 392 | 393 | // align to the pixel grid 394 | cgContext.translateBy(x: 0.5, y: 0.5) 395 | 396 | // fill 397 | if let existingFillColor = fillColor { 398 | existingFillColor.setFill() 399 | 400 | // reduce fillRect so that is doesn't cross the stoke 401 | let fillRect = R(rect.origin.x+1, rect.origin.y+1, rect.size.width-2, rect.size.height-2) 402 | cgContext.fillEllipse(in: fillRect) 403 | } 404 | 405 | // stroke 406 | if let existingStrokeColor = strokeColor { existingStrokeColor.setStroke() } 407 | cgContext.strokeEllipse(in: rect) 408 | 409 | context.restoreGraphicsState() 410 | } 411 | 412 | fileprivate func degreesToRadians(_ x:CGFloat) -> CGFloat { 413 | return (M_PI * x / 180.0) 414 | } 415 | 416 | func save(_ path:String, open:Bool=false) { 417 | guard let data = bitmapImageRep.representation(using: .PNG, properties: [:]) else { 418 | print("\(#file) \(#function) cannot get PNG data from bitmap") 419 | return 420 | } 421 | 422 | do { 423 | try data.write(to: URL(fileURLWithPath: path), options: []) 424 | if open { 425 | NSWorkspace.shared().openFile(path) 426 | } 427 | } catch let e { 428 | print(e) 429 | } 430 | } 431 | 432 | static func textWidth(_ text:NSString, font:NSFont) -> CGFloat { 433 | let maxSize : CGSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: font.pointSize) 434 | let textRect : CGRect = text.boundingRect( 435 | with: maxSize, 436 | options: NSStringDrawingOptions.usesLineFragmentOrigin, 437 | attributes: [NSFontAttributeName: font], 438 | context: nil) 439 | return textRect.size.width 440 | } 441 | 442 | func image(fromPath path:String, _ p:NSPoint) { 443 | 444 | guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { 445 | print("\(#file) \(#function) cannot read data at \(path)"); 446 | return 447 | } 448 | 449 | guard let imgRep = NSBitmapImageRep(data: data) else { 450 | print("\(#file) \(#function) cannot create bitmap image rep from data at \(path)"); 451 | return 452 | } 453 | 454 | guard let cgImage = imgRep.cgImage else { 455 | print("\(#file) \(#function) cannot get cgImage out of imageRep from \(path)"); 456 | return 457 | } 458 | 459 | context.saveGraphicsState() 460 | 461 | cgContext.scaleBy(x: 1.0, y: -1.0) 462 | cgContext.translateBy(x: 0.0, y: -2.0 * p.y - imgRep.pixelsHigh) 463 | 464 | let rect = NSMakeRect(p.x, p.y, CGFloat(imgRep.pixelsWide), CGFloat(imgRep.pixelsHigh)) 465 | 466 | cgContext.draw(cgImage, in: rect) 467 | 468 | context.restoreGraphicsState() 469 | } 470 | 471 | func text(_ text:String, _ p:NSPoint, rotationRadians:CGFloat?, font : NSFont = NSFont(name: "Monaco", size: 10)!, color color_ : ConvertibleToNSColor = NSColor.black) { 472 | 473 | let color = color_.color 474 | 475 | let attr = [ 476 | NSFontAttributeName:font, 477 | NSForegroundColorAttributeName:color 478 | ] 479 | 480 | context.saveGraphicsState() 481 | 482 | if let radians = rotationRadians { 483 | cgContext.translateBy(x: p.x, y: p.y); 484 | cgContext.rotate(by: radians) 485 | cgContext.translateBy(x: -p.x, y: -p.y); 486 | } 487 | 488 | cgContext.scaleBy(x: 1.0, y: -1.0) 489 | cgContext.translateBy(x: 0.0, y: -2.0 * p.y - font.pointSize) 490 | 491 | text.draw(at: p, withAttributes: attr) 492 | 493 | context.restoreGraphicsState() 494 | } 495 | 496 | func text(_ text:String, _ p:NSPoint, rotationDegrees degrees:CGFloat = 0.0, font : NSFont = NSFont(name: "Monaco", size: 10)!, color : ConvertibleToNSColor = NSColor.black) { 497 | self.text(text, p, rotationRadians: degreesToRadians(degrees), font: font, color: color) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /DevTeamActivity/ChartDay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartWeek.swift 3 | // hgReport 4 | // 5 | // Created by Nicolas Seriot on 09/02/16. 6 | // Copyright © 2016 seriot.ch. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | struct ChartDay { 12 | 13 | let COL_WIDTH : CGFloat = 20 14 | let ROW_HEIGHT : CGFloat = 20 15 | let LEFT_MARGIN_WIDTH : CGFloat = 20 16 | let TOP_MARGIN_HEIGTH : CGFloat = 100 17 | 18 | let fiveLinesThresholds = [0, 1000, 2500, 4000, 5000] 19 | 20 | let weekDaysToSkip = [1,2] // Saturday, Sunday 21 | 22 | var dateFormatter: DateFormatter = { 23 | let df = DateFormatter() 24 | df.dateFormat = "yyyy-MM-dd" 25 | df.timeZone = TimeZone(identifier:"GMT") 26 | return df 27 | }() 28 | 29 | func daysTuplesFromDay(_ fromDay:String, toDay:String) -> [(day:String, weekDay:Int, offset:CGFloat)] { 30 | 31 | let calendar = Calendar.current 32 | 33 | var daysInfo : [(day:String, weekDay:Int, offset:CGFloat)] = [] 34 | 35 | var matchingComponents = DateComponents() 36 | matchingComponents.hour = 0 37 | 38 | guard let fromDate = self.dateFormatter.date(from: fromDay) else { assertionFailure(); return [] } 39 | guard let toDate = self.dateFormatter.date(from: toDay) else { assertionFailure(); return [] } 40 | 41 | var xOffset : CGFloat = 0 42 | 43 | (calendar as NSCalendar).enumerateDates(startingAfter: fromDate, matching: matchingComponents, options: .matchStrictly) { (date: Date?, exactMatch: Bool, stop: UnsafeMutablePointer) -> Void in 44 | 45 | guard let existingDate = date else { assertionFailure(); return } 46 | 47 | let day = self.dateFormatter.string(from: existingDate) 48 | 49 | let weekDay = (calendar as NSCalendar).component(.weekday, from:existingDate) 50 | 51 | daysInfo.append((day, weekDay, xOffset)) 52 | 53 | xOffset += self.weekDaysToSkip.contains(weekDay) ? 2 : self.COL_WIDTH 54 | 55 | if existingDate.compare(toDate) != ComparisonResult.orderedAscending { 56 | stop.pointee = true 57 | } 58 | } 59 | 60 | return daysInfo 61 | } 62 | 63 | func rectForDay(_ offset:CGFloat, rowIndex:Int) -> NSRect { 64 | 65 | let x = self.LEFT_MARGIN_WIDTH + offset 66 | let y = self.TOP_MARGIN_HEIGTH + rowIndex * self.ROW_HEIGHT 67 | 68 | return NSMakeRect(x, y, self.COL_WIDTH, self.ROW_HEIGHT) 69 | } 70 | 71 | func fillColorForLineCountPerDay(_ count:Int, baseColor:NSColor) -> NSColor { 72 | var intensity : CGFloat 73 | 74 | switch(count) { 75 | case count where count > fiveLinesThresholds[4]: intensity = 1.0 76 | case count where count > fiveLinesThresholds[3]: intensity = 0.8 77 | case count where count > fiveLinesThresholds[2]: intensity = 0.6 78 | case count where count > fiveLinesThresholds[1]: intensity = 0.4 79 | case count where count > fiveLinesThresholds[0]: intensity = 0.2 80 | default: intensity = 0.0 81 | } 82 | 83 | return baseColor.withAlphaComponent(intensity) 84 | } 85 | 86 | static var colorPalette = [ 87 | NSColor.blue, 88 | NSColor.green, 89 | NSColor.red, 90 | NSColor.yellow, 91 | NSColor.cyan, 92 | NSColor.purple, 93 | NSColor.orange, 94 | NSColor.magenta 95 | ] 96 | 97 | static var colorForAuthors : [String:NSColor] = [:] 98 | 99 | func colorForAuthor(_ author:String) -> NSColor { 100 | 101 | if ChartDay.colorForAuthors[author] == nil { 102 | let color = ChartDay.colorPalette.popLast() ?? NSColor.darkGray 103 | ChartDay.colorForAuthors[author] = color 104 | } 105 | 106 | return ChartDay.colorForAuthors[author]! 107 | } 108 | 109 | func drawLegend(_ bc:BitmapCanvas, x:CGFloat) { 110 | 111 | // draw title 112 | bc.text("Number of Lines Changed", P(x + 10, 10)) 113 | 114 | let numberOfLines = [ 115 | "\(fiveLinesThresholds[0])", 116 | "\(fiveLinesThresholds[0])+", 117 | "\(fiveLinesThresholds[1])+", 118 | "\(fiveLinesThresholds[2])+", 119 | "\(fiveLinesThresholds[3])+", 120 | "\(fiveLinesThresholds[4])+" 121 | ] 122 | 123 | for i in 0...fiveLinesThresholds.count { 124 | let p = P(x + 10 + i/3 * 80, COL_WIDTH + (i%3+1) * self.ROW_HEIGHT - 10) 125 | let r = NSMakeRect(p.x, p.y, self.COL_WIDTH, self.ROW_HEIGHT) 126 | let intensity = i * 0.2 127 | let fillColor = NSColor.gray.withAlphaComponent(intensity) 128 | 129 | bc.rectangle(r, stroke: NSColor.lightGray, fill: fillColor) 130 | 131 | let textPoint = P(p.x + COL_WIDTH + 10, p.y + 6) 132 | let s = numberOfLines[i] 133 | bc.text(s, textPoint) 134 | } 135 | } 136 | 137 | func drawTimeline(fromDay:String, toDay:String, repoTuples:[(repo:String, jsonPath:String)], outPath:String) throws { 138 | 139 | let bitmapCanvas = BitmapCanvas(880,560, "white") 140 | 141 | let dayTuples = daysTuplesFromDay(fromDay, toDay:toDay).filter( { weekDaysToSkip.contains($0.weekDay) == false } ) 142 | 143 | // draw days 144 | for (day, _, offset) in dayTuples { 145 | let p = P(LEFT_MARGIN_WIDTH + offset, TOP_MARGIN_HEIGTH - 10) 146 | bitmapCanvas.text("\(day)", P(p.x+7, p.y), rotationDegrees:-90) 147 | } 148 | 149 | // find legend x position 150 | guard let (_, _, offset) = dayTuples.last else { assertionFailure("period must be at least 1 day"); return } 151 | let legendAndAuthorsXPosition = LEFT_MARGIN_WIDTH + offset + COL_WIDTH + 18 152 | 153 | // draw legend 154 | self.drawLegend(bitmapCanvas, x: legendAndAuthorsXPosition) 155 | 156 | var currentRow = 0 157 | 158 | // for each repo 159 | for (repoName, jsonPath) in repoTuples { 160 | 161 | guard let 162 | data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)), 163 | let optJSON = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as? AddedRemovedForAuthorForDate, 164 | let json = optJSON else { 165 | print("-- can't read data in \(jsonPath)") 166 | return 167 | } 168 | 169 | let authorsInRepo = Array(Set(json.values.flatMap({ $0.keys }))).sorted() 170 | 171 | // draw repo name 172 | bitmapCanvas.text(repoName, P(LEFT_MARGIN_WIDTH, self.TOP_MARGIN_HEIGTH + currentRow * ROW_HEIGHT + 7)) 173 | 174 | currentRow += 1 175 | 176 | // draw authors 177 | for (authorIndex, author) in authorsInRepo.enumerated() { 178 | bitmapCanvas.text( 179 | author, 180 | P(legendAndAuthorsXPosition, self.TOP_MARGIN_HEIGTH + (currentRow+authorIndex) * ROW_HEIGHT + 5)) 181 | } 182 | 183 | // draw cells 184 | 185 | // for each author in the repo 186 | for (i, author) in authorsInRepo.enumerated() { 187 | 188 | // for each day of the timeframe 189 | for (day, _, offset) in dayTuples { 190 | 191 | // set default color 192 | var fillColor = NSColor.clear 193 | 194 | if let addedRemoved = json[day]?[author] { 195 | // that day, this author commited changes in the repo 196 | // set the cell color accordingly 197 | 198 | var linesChanged = 0 199 | 200 | linesChanged += addedRemoved["added"] ?? 0 201 | linesChanged += addedRemoved["removed"] ?? 0 202 | 203 | fillColor = fillColorForLineCountPerDay(linesChanged, baseColor:colorForAuthor(author)) 204 | } 205 | 206 | let rect = rectForDay (offset, rowIndex: currentRow+i) 207 | bitmapCanvas.rectangle(rect, stroke: NSColor.lightGray, fill: fillColor) 208 | } 209 | } 210 | 211 | currentRow += authorsInRepo.count 212 | } 213 | 214 | bitmapCanvas.save(outPath) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /DevTeamActivity/ChartMonth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartWeek.swift 3 | // hgReport 4 | // 5 | // Created by Nicolas Seriot on 09/02/16. 6 | // Copyright © 2016 seriot.ch. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | fileprivate func < (lhs: T?, rhs: T?) -> Bool { 11 | switch (lhs, rhs) { 12 | case let (l?, r?): 13 | return l < r 14 | case (nil, _?): 15 | return true 16 | default: 17 | return false 18 | } 19 | } 20 | 21 | fileprivate func > (lhs: T?, rhs: T?) -> Bool { 22 | switch (lhs, rhs) { 23 | case let (l?, r?): 24 | return l > r 25 | default: 26 | return rhs < lhs 27 | } 28 | } 29 | 30 | 31 | struct ChartMonth { 32 | 33 | let COL_WIDTH : CGFloat = 16 34 | let ROW_HEIGHT : CGFloat = 16 35 | let LEFT_MARGIN_WIDTH : CGFloat = 20 36 | let TOP_MARGIN_HEIGTH : CGFloat = 80 37 | 38 | let fiveLinesThresholds = [0, 1500, 3000, 6000, 15000] 39 | 40 | var dateFormatter: DateFormatter = { 41 | let df = DateFormatter() 42 | df.dateFormat = "yyyy-MM-dd" 43 | df.timeZone = TimeZone(identifier:"GMT") 44 | return df 45 | }() 46 | 47 | var monthYearDateFormatter: DateFormatter = { 48 | let df = DateFormatter() 49 | df.dateFormat = "yyyy-MM" 50 | df.timeZone = TimeZone(identifier:"GMT") 51 | return df 52 | }() 53 | 54 | func monthYearTuplesFromDay(_ fromDay:String, toDay:String) -> [(monthYear:String, offset:CGFloat)] { 55 | 56 | let calendar = Calendar.current 57 | 58 | var monthYearInfo : [(monthYear:String, offset:CGFloat)] = [] 59 | 60 | var matchingComponents = DateComponents() 61 | matchingComponents.day = 1 62 | 63 | guard let fromDate = self.dateFormatter.date(from: fromDay) else { assertionFailure(); return [] } 64 | guard let toDate = self.dateFormatter.date(from: toDay) else { assertionFailure(); return [] } 65 | 66 | var xOffset : CGFloat = 0 67 | 68 | (calendar as NSCalendar).enumerateDates(startingAfter: fromDate, matching: matchingComponents, options: .matchStrictly) { (date: Date?, exactMatch: Bool, stop: UnsafeMutablePointer) -> Void in 69 | 70 | guard let existingDate = date else { assertionFailure(); return } 71 | 72 | let monthYear = self.monthYearDateFormatter.string(from: existingDate) 73 | 74 | monthYearInfo.append((monthYear, xOffset)) 75 | 76 | xOffset += self.COL_WIDTH 77 | 78 | if monthYear.hasSuffix("-12") { 79 | xOffset += 4 80 | } 81 | 82 | if existingDate.compare(toDate) != ComparisonResult.orderedAscending { 83 | stop.pointee = true 84 | } 85 | } 86 | 87 | return monthYearInfo 88 | } 89 | 90 | func monthYearAuthorChangesDictionaryFromJSON(_ json:AddedRemovedForAuthorForDate) -> [String:[String:Int]] { 91 | 92 | // read json and build new structure aggregated by months 93 | 94 | var monthYearDictionary : [String:[String:Int]] = [:] // [dayMonth:[author:changes]] 95 | 96 | for (day, addedRemovedForAuthorDictionary) in json { 97 | //print(day, addedRemovedForAuthorDictionary) 98 | 99 | let monthYear = (day as NSString).substring(to: 7) 100 | 101 | if monthYearDictionary[monthYear] == nil { 102 | monthYearDictionary[monthYear] = [:] 103 | } 104 | 105 | for (author, addedRemoved) in addedRemovedForAuthorDictionary { 106 | // that day, this author commited changes in the repo 107 | // add the number of lines changed into monthYearDictionary 108 | 109 | var linesChanged = 0 110 | linesChanged += addedRemoved["added"] ?? 0 111 | linesChanged += addedRemoved["removed"] ?? 0 112 | 113 | // 114 | 115 | if monthYearDictionary[monthYear] == nil { 116 | monthYearDictionary[monthYear] = [:] // [author:changes] 117 | } 118 | 119 | if monthYearDictionary[monthYear]![author] == nil { 120 | monthYearDictionary[monthYear]![author] = 0 121 | } 122 | 123 | monthYearDictionary[monthYear]![author]! += linesChanged 124 | } 125 | } 126 | 127 | return monthYearDictionary 128 | } 129 | 130 | func rectForDay(_ offset:CGFloat, rowIndex:Int) -> NSRect { 131 | 132 | let x = self.LEFT_MARGIN_WIDTH + offset 133 | let y = self.TOP_MARGIN_HEIGTH + rowIndex * self.ROW_HEIGHT 134 | 135 | return NSMakeRect(x, y, self.COL_WIDTH, self.ROW_HEIGHT) 136 | } 137 | 138 | func fillColorForLineCountPerMonth(_ count:Int, baseColor:NSColor) -> NSColor { 139 | var intensity : CGFloat 140 | 141 | switch(count) { 142 | case count where count > fiveLinesThresholds[4]: intensity = 1.0 143 | case count where count > fiveLinesThresholds[3]: intensity = 0.8 144 | case count where count > fiveLinesThresholds[2]: intensity = 0.6 145 | case count where count > fiveLinesThresholds[1]: intensity = 0.4 146 | case count where count > fiveLinesThresholds[0]: intensity = 0.2 147 | default: intensity = 0.0 148 | } 149 | 150 | return baseColor.withAlphaComponent(intensity) 151 | } 152 | 153 | static var colorForAuthors : [String:NSColor] = [:] 154 | 155 | func colorForAuthor(_ author:String) -> NSColor { 156 | 157 | if (author as NSString).hasSuffix("@apple.com") { 158 | return NSColor.orange 159 | } 160 | return NSColor.darkGray 161 | } 162 | 163 | func drawLegend(_ bc:BitmapCanvas, x:CGFloat) { 164 | 165 | // draw title 166 | bc.text("Number of Lines Changed", P(x + 10, 10)) 167 | 168 | let numberOfLines = [ 169 | "\(fiveLinesThresholds[0])", 170 | "\(fiveLinesThresholds[0])+", 171 | "\(fiveLinesThresholds[1])+", 172 | "\(fiveLinesThresholds[2])+", 173 | "\(fiveLinesThresholds[3])+", 174 | "\(fiveLinesThresholds[4])+" 175 | ] 176 | 177 | for i in 0...fiveLinesThresholds.count { 178 | let p = P(x + 10 + i/3 * 80, COL_WIDTH + (i%3+1) * self.ROW_HEIGHT - 10) 179 | let r = NSMakeRect(p.x, p.y, self.COL_WIDTH, self.ROW_HEIGHT) 180 | let intensity = i * 0.2 181 | let fillColor = NSColor.orange.withAlphaComponent(intensity) 182 | 183 | bc.rectangle(r, stroke: NSColor.lightGray, fill: fillColor) 184 | 185 | let textPoint = P(p.x + COL_WIDTH + 10, p.y + 6) 186 | let s = numberOfLines[i] 187 | bc.text(s, textPoint) 188 | } 189 | } 190 | 191 | func linesChangedByAuthor(_ monthYearAuthorChangesDictionary:[String:[String:Int]]) -> [String:Int] { 192 | var linesChangedByAuthor : [String:Int] = [:] 193 | for (_, linesChangedByAuthorThatMonth) in monthYearAuthorChangesDictionary { 194 | for (author, linesChanged) in linesChangedByAuthorThatMonth { 195 | if linesChangedByAuthor[author] == nil { 196 | linesChangedByAuthor[author] = 0 197 | } 198 | linesChangedByAuthor[author]! += linesChanged 199 | } 200 | } 201 | return linesChangedByAuthor 202 | } 203 | 204 | func drawTimeline(fromDay:String, toDay:String, repoTuples:[(repo:String, jsonPath:String)], outPath:String) throws { 205 | 206 | let bitmapCanvas = BitmapCanvas(1500,8250, "white") 207 | 208 | let monthYearTuples = monthYearTuplesFromDay(fromDay, toDay:toDay) 209 | 210 | // draw days 211 | for (monthYear, offset) in monthYearTuples { 212 | let p = P(LEFT_MARGIN_WIDTH + offset, TOP_MARGIN_HEIGTH - 10) 213 | bitmapCanvas.text("\(monthYear)", P(p.x+7, p.y), rotationDegrees:-90) 214 | } 215 | 216 | // find legend x position 217 | guard let (_, offset) = monthYearTuples.last else { assertionFailure("period must be at least 1 day"); return } 218 | let legendAndAuthorsXPosition = LEFT_MARGIN_WIDTH + offset + COL_WIDTH + 18 219 | 220 | // draw title 221 | bitmapCanvas.text("https://github.com/apple/swift", P(450, 10)) 222 | 223 | // draw legend 224 | self.drawLegend(bitmapCanvas, x: legendAndAuthorsXPosition) 225 | 226 | var currentRow = 0 227 | 228 | // for each repo 229 | for (_, jsonPath) in repoTuples { 230 | 231 | guard let 232 | data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)), 233 | let optJSON = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as? AddedRemovedForAuthorForDate, 234 | let json = optJSON else { 235 | print("-- can't read data in \(jsonPath)") 236 | return 237 | } 238 | 239 | let monthYearAuthorChangesDictionary = monthYearAuthorChangesDictionaryFromJSON(json) 240 | 241 | let linesByAuthor = linesChangedByAuthor(monthYearAuthorChangesDictionary) 242 | 243 | let sortedAuthors = Array(linesByAuthor.keys).sorted(by: { linesByAuthor[$0] > linesByAuthor[$1] }) 244 | 245 | // draw authors 246 | for (authorIndex, author) in sortedAuthors.enumerated() { 247 | bitmapCanvas.text( 248 | "\(author) (\(linesByAuthor[author]!))", 249 | P(legendAndAuthorsXPosition, self.TOP_MARGIN_HEIGTH + (currentRow+authorIndex) * ROW_HEIGHT + 5)) 250 | } 251 | 252 | // draw cells 253 | 254 | // for each author in the repo 255 | for (i, author) in sortedAuthors.enumerated() { 256 | 257 | // for each month of the timeframe 258 | for (monthYear, offset) in monthYearTuples { 259 | 260 | // set default color 261 | var fillColor = NSColor.clear 262 | 263 | if let linesChanged = monthYearAuthorChangesDictionary[monthYear]?[author] { 264 | // that day, this author commited changes in the repo 265 | // set the cell color accordingly 266 | 267 | fillColor = fillColorForLineCountPerMonth(linesChanged, baseColor:colorForAuthor(author)) 268 | } 269 | 270 | let rect = rectForDay (offset, rowIndex: currentRow+i) 271 | bitmapCanvas.rectangle(rect, stroke: NSColor.lightGray, fill: fillColor) 272 | } 273 | } 274 | 275 | currentRow += sortedAuthors.count 276 | } 277 | 278 | let currentDateString = dateFormatter.string(from: Date()) // FIXME: doesn't consider timezones 279 | 280 | bitmapCanvas.text("Generated by Nicolas Seriot on \(currentDateString) with https://github.com/nst/DevTeamActivity", P(LEFT_MARGIN_WIDTH, self.TOP_MARGIN_HEIGTH + currentRow * ROW_HEIGHT + 10)) 281 | 282 | bitmapCanvas.save(outPath) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /DevTeamActivity/CommitsExtractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // hgReport 4 | // 5 | // Created by Nicolas Seriot on 02/02/16. 6 | // Copyright © 2016 seriot.ch. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct RegularExpression { 12 | static func findAll(string s: String, pattern: String) throws -> [String] { 13 | 14 | let regex = try NSRegularExpression(pattern: pattern, options: []) 15 | let matches = regex.matches(in: s, options: [], range: NSMakeRange(0, s.characters.count)) 16 | 17 | var results : [String] = [] 18 | 19 | for m in matches { 20 | for i in 1.. Process { 40 | 41 | let process = Process() 42 | 43 | switch(self) { 44 | case .git: 45 | process.launchPath = "/usr/bin/git" 46 | process.arguments = ["--git-dir=\(repositoryPath)/.git", "log", "--pretty=\"%aI %ae\"", "--shortstat", "--after=\(fromDay)", "--before=\(toDay)"] 47 | case .mercurial: 48 | process.launchPath = "/usr/local/bin/hg" 49 | process.arguments = ["log", "--template", "{date(date,'%Y-%m-%d')} {author|email} {diffstat}\\n", "--date", "\(fromDay) to \(toDay)", "--repository", repositoryPath] 50 | } 51 | 52 | guard let path = process.launchPath else { assertionFailure(); return process } 53 | guard FileManager.default.fileExists(atPath: path) else { assertionFailure(); return process } 54 | 55 | let pipe = Pipe() 56 | process.standardOutput = pipe 57 | // task.standardError = task.standardOutput 58 | 59 | print("-- \(process.launchPath!) \(process.arguments!.joined(separator: " "))") 60 | 61 | return process 62 | } 63 | 64 | func dataInLogLines(_ lines: [String]) throws -> [(day:String, email:String, added:Int, removed:Int)] { 65 | switch(self) { 66 | case .git: 67 | return try self.dataInGitLogLines(lines) 68 | case .mercurial: 69 | return try self.dataInHgLogLines(lines) 70 | } 71 | } 72 | 73 | func dataInGitLogLines(_ lines: [String]) throws -> [(day:String, email:String, added:Int, removed:Int)] { 74 | /* 75 | "2015-12-25T15:50:00+01:00 nicolas@seriot.ch" 76 | 77 | 2 files changed, 3 insertions(+), 4 deletions(-) 78 | 2 files changed, 4 deletions(-) 79 | */ 80 | 81 | var results = [(day:String, email:String, added:Int, removed:Int)]() 82 | 83 | var date : String? = nil 84 | var email : String? = nil 85 | var added : Int? = 0 86 | var removed : Int? = 0 87 | 88 | for line in lines { 89 | let s = (line as NSString) 90 | 91 | if s.length == 0 { 92 | continue 93 | } 94 | 95 | if s.hasPrefix(" ") == false { 96 | date = s.substring(with: NSMakeRange(1, 10)) 97 | if let emailWithEndingQuote = s.components(separatedBy: " ").last { 98 | let len = emailWithEndingQuote.lengthOfBytes(using: String.Encoding.utf8) 99 | email = (emailWithEndingQuote as NSString).substring(with: NSMakeRange(0, len-1)) 100 | } 101 | } else { 102 | print(line) 103 | 104 | let insertions = try RegularExpression.findAll(string: line, pattern: "\\s(\\d+?)\\sinsertion") 105 | let deletions = try RegularExpression.findAll(string: line, pattern: "\\s(\\d+?)\\sdeletion") 106 | 107 | if let insertionsCount = insertions.first , insertions.count == 1 { 108 | added = Int(insertionsCount) 109 | } 110 | 111 | if let deletionsCount = deletions.first , deletions.count == 1 { 112 | removed = Int(deletionsCount) 113 | } 114 | 115 | // 116 | 117 | guard let 118 | existingDate = date, 119 | let existingEmail = email, 120 | let existingAdded = added, 121 | let existingRemoved = removed 122 | else { 123 | print("***", line) 124 | throw CommitExtractorError.badValues 125 | } 126 | 127 | let t = (day:existingDate, email:existingEmail, added:existingAdded, removed:existingRemoved) 128 | results.append(t) 129 | 130 | email = nil 131 | date = nil 132 | added = 0 133 | removed = 0 134 | } 135 | } 136 | 137 | return results 138 | } 139 | 140 | func dataInHgLogLines(_ lines: [String]) throws -> [(day:String, email:String, added:Int, removed:Int)] { 141 | 142 | var results : [(day:String, email:String, added:Int, removed:Int)] = [] 143 | 144 | for line in lines { 145 | // 2016-01-25 john.doe@aol.com 1: +0/-12 146 | 147 | if line.lengthOfBytes(using: String.Encoding.utf8) == 0 { 148 | continue 149 | } 150 | 151 | let groups = try RegularExpression.findAll(string: line, pattern: "(\\S*)\\s(\\S*)\\s\\d*:\\s\\+(\\d*).-(\\d*)") 152 | guard groups.count == 4 else { 153 | print(groups) 154 | throw CommitExtractorError.badFormat 155 | } 156 | 157 | guard let 158 | existingAdded = Int(groups[2]), 159 | let existingRemoved = Int(groups[3]) 160 | else { 161 | throw CommitExtractorError.badValues 162 | } 163 | 164 | let t = (day:groups[0], email:groups[1], added:existingAdded, removed:existingRemoved) 165 | results.append(t) 166 | } 167 | 168 | return results 169 | } 170 | 171 | } 172 | 173 | typealias AddedRemoved = [String:Int] 174 | typealias AddedRemovedForAuthor = [String:AddedRemoved] 175 | typealias AddedRemovedForAuthorForDate = [String:AddedRemovedForAuthor] 176 | 177 | func readLogs(_ vcs:VCS, repositoryPath:String, fromDay:String, toDay:String, completionHandler:(AddedRemovedForAuthorForDate)->()) { 178 | 179 | var results : AddedRemovedForAuthorForDate = [:] 180 | 181 | /* 182 | hg log --template "{date(date, '%Y-%m-%d')} {author|email} {diffstat}\n" 183 | 2016-02-08 a.a@a.com 4: +45/-9 184 | 2016-02-08 b.b@b.com 8: +102/-17 185 | 2016-02-08 b.b@b.com 5: +47/-11 186 | 187 | hg log --template "{date(date, '%Y-%m-%d')} {author|email} {diffstat}\n" --date "2016-01-01 to 2016-01-31" 188 | */ 189 | 190 | let process = vcs.processToGetLogsForRepository(repositoryPath, fromDay:fromDay, toDay:toDay) 191 | 192 | let pipe = Pipe() 193 | process.standardOutput = pipe 194 | 195 | process.launch() 196 | 197 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 198 | 199 | let s = NSString(data: data, encoding: String.Encoding.utf8.rawValue) 200 | guard let lines = s?.components(separatedBy: "\n") else { 201 | completionHandler([:]) 202 | return 203 | } 204 | 205 | do { 206 | let entries = try vcs.dataInLogLines(lines) 207 | 208 | print(entries) 209 | 210 | for e in entries { 211 | let (day, author, added, removed) = e 212 | print(e) 213 | 214 | if results[day] == nil { 215 | results[day] = [:] 216 | } 217 | 218 | if results[day]?[author] == nil { 219 | results[day]?[author] = [:] 220 | } 221 | 222 | if results[day]?[author]?["added"] == nil { 223 | results[day]?[author]?["added"] = 0 224 | } 225 | 226 | if results[day]?[author]?["removed"] == nil { 227 | results[day]?[author]?["removed"] = 0 228 | } 229 | 230 | results[day]?[author]?["added"]? += added 231 | results[day]?[author]?["removed"]? += removed 232 | } 233 | 234 | } catch { 235 | print("*** ERROR:", error) 236 | } 237 | 238 | completionHandler(results) 239 | } 240 | 241 | func saveLogs(_ entries:AddedRemovedForAuthorForDate, path:String) throws -> Bool { 242 | let jsonData = try JSONSerialization.data(withJSONObject: entries, options: .prettyPrinted) 243 | return ((try? jsonData.write(to: URL(fileURLWithPath: path), options: [.atomic])) != nil) 244 | } 245 | 246 | func extractCommits(_ vcs:VCS, repositoryPath:String, fromDay:String, toDay:String, completionHandler:(_ path:String) -> ()) { 247 | 248 | readLogs(vcs, repositoryPath: repositoryPath, fromDay:fromDay, toDay:toDay) { (entries) -> () in 249 | 250 | print(entries) 251 | 252 | let repoName = (repositoryPath as NSString).lastPathComponent 253 | let path = "/Users/nst/Desktop/\(repoName)_\(fromDay)_\(toDay).json" 254 | 255 | do { 256 | _ = try saveLogs(entries, path:path) 257 | print("-- saved \(entries.count) in", path) 258 | completionHandler(path) 259 | } catch { 260 | print(error) 261 | } 262 | 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /DevTeamActivity/X11Colors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // X11ColorList.swift 3 | // BitmapCanvas 4 | // 5 | // Created by nst on 28/02/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSRegularExpression { 12 | class func findAll(string s: String, pattern: String) throws -> [String] { 13 | 14 | let regex = try NSRegularExpression(pattern: pattern, options: []) 15 | let matches = regex.matches(in: s, options: [], range: NSMakeRange(0, s.characters.count)) 16 | 17 | var results : [String] = [] 18 | 19 | for m in matches { 20 | for i in 1..> 16) & 0xff) / 255 45 | let g = CGFloat((self >> 08) & 0xff) / 255 46 | let b = CGFloat((self >> 00) & 0xff) / 255 47 | 48 | return NSColor(calibratedRed:r, green:g, blue:b, alpha:1.0) 49 | } 50 | } 51 | 52 | extension String : ConvertibleToNSColor { 53 | 54 | var color : NSColor { 55 | 56 | let scanner = Scanner(string: self) 57 | 58 | if scanner.scanString("#", into: nil) { 59 | var result : UInt32 = 0 60 | if scanner.scanHexInt32(&result) { 61 | return result.color 62 | } else { 63 | assertionFailure("cannot convert \(self) to hex color)") 64 | return NSColor.clear 65 | } 66 | } 67 | 68 | if let c = X11Colors.sharedInstance.colorList.color(withKey: self.lowercased()) { 69 | return c 70 | } 71 | 72 | assertionFailure("cannot convert \(self) into color)") 73 | return NSColor.clear 74 | } 75 | } 76 | 77 | extension NSColor { 78 | 79 | convenience init(_ r:Int, _ g:Int, _ b:Int, _ a:Int = 255) { 80 | self.init( 81 | calibratedRed: CGFloat(r)/255.0, 82 | green: CGFloat(g)/255.0, 83 | blue: CGFloat(b)/255.0, 84 | alpha: CGFloat(a)/255.0) 85 | } 86 | 87 | class var randomColor : NSColor { 88 | return C(Int(arc4random_uniform(256)), Int(arc4random_uniform(256)), Int(arc4random_uniform(256))) 89 | } 90 | } 91 | 92 | func C(_ r:Int, _ g:Int, _ b:Int, _ a:Int = 255) -> NSColor { 93 | return NSColor(r,g,b,a) 94 | } 95 | 96 | func C(_ r:CGFloat, _ g:CGFloat, _ b:CGFloat, _ a:CGFloat = 255.0) -> NSColor { 97 | return NSColor(calibratedRed: r, green: g, blue: b, alpha: a) 98 | } 99 | 100 | class X11Colors { 101 | 102 | static let sharedInstance = X11Colors(namePrettifier: { $0.lowercased() }) 103 | 104 | var colorList = NSColorList(name: "X11") 105 | 106 | init(path:String = "/opt/X11/share/X11/rgb.txt", namePrettifier:@escaping (_ original:String) -> (String)) { 107 | 108 | let contents = try! String(contentsOfFile: path, encoding: String.Encoding.utf8) 109 | 110 | contents.enumerateLines { (line, stop) in 111 | 112 | let pattern = "\\s?+(\\d+?)\\s+(\\d+?)\\s+(\\d+?)\\s+(\\w+)$" 113 | let matches = try! NSRegularExpression.findAll(string: line, pattern: pattern) 114 | if matches.count != 4 { return } // ignore names with white spaces, they also appear in camel case 115 | 116 | let r = CGFloat(Int(matches[0])!) 117 | let g = CGFloat(Int(matches[1])!) 118 | let b = CGFloat(Int(matches[2])!) 119 | 120 | let name = matches[3] 121 | 122 | let prettyName = namePrettifier(name) 123 | 124 | let color = NSColor(calibratedRed: r/255.0, green: g/255.0, blue: b/255.0, alpha: 1.0) 125 | self.colorList.setColor(color, forKey: prettyName) 126 | 127 | //print("\(name) \t -> \t \(prettyName)") 128 | } 129 | } 130 | 131 | static func dump(_ inPath:String, outPath:String) -> Bool { 132 | 133 | let x11Colors = X11Colors(namePrettifier: { 134 | let name = ($0 as NSString) 135 | let firstCharacter = name.substring(to: 1) 136 | let restOfString = name.substring(with: NSMakeRange(1, name.lengthOfBytes(using: String.Encoding.utf8.rawValue)-1)) 137 | return "\(firstCharacter.uppercased())\(restOfString)" 138 | }) 139 | 140 | return x11Colors.colorList.write(toFile: outPath) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /DevTeamActivity/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // DevTeamActivity 4 | // 5 | // Created by nst on 17/02/16. 6 | // Copyright © 2016 Nicolas Seriot. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AppKit 11 | 12 | func extractData() { 13 | 14 | let repos = [ 15 | "/Users/nst/Projects/swift" 16 | ] 17 | 18 | for path in repos { 19 | extractCommits(.git, repositoryPath: path, fromDay: "2011-01-01", toDay: "2016-10-31") { (path) -> () in 20 | print("->", path) 21 | } 22 | } 23 | } 24 | 25 | func draw() { 26 | 27 | let fromDay = "2011-01-01" 28 | let toDay = "2016-10-31" 29 | 30 | var repoTuples : [(repo:String, jsonPath:String)] = [] 31 | 32 | for s in ["swift"] { 33 | let path = ("~/Desktop/\(s)_\(fromDay)_\(toDay).json" as NSString).expandingTildeInPath 34 | let t = (repo:s, jsonPath:path) 35 | repoTuples.append(t) 36 | } 37 | 38 | let outPath = ("~/Desktop/team_activity_\(fromDay)_\(toDay).png" as NSString).expandingTildeInPath 39 | 40 | try! ChartMonth().drawTimeline(fromDay:fromDay, toDay:toDay, repoTuples: repoTuples, outPath:outPath) 41 | 42 | print(outPath) 43 | 44 | NSWorkspace.shared().openFile(outPath) 45 | 46 | } 47 | 48 | //extractData() 49 | 50 | draw() 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nicolas Seriot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DevTeamActivity 2 | Generates a picture summarising the activity of a dev team on one or several repositories 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /dev_team_activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/DevTeamActivity/845275202417edb1167f07995f80e5a313313913/dev_team_activity.png -------------------------------------------------------------------------------- /swift_repo_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nst/DevTeamActivity/845275202417edb1167f07995f80e5a313313913/swift_repo_preview.png --------------------------------------------------------------------------------